// ==UserScript==
// @name BGA Flip Seven Card Counter
// @namespace http://tampermonkey.net/
// @version 0.3
// @description Card counter for Flip Seven on BoardGameArena
// @author KuRRe8, fpronto
// @match https://boardgamearena.com/*/flipseven?table=*
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_setValue
// ==/UserScript==
(function () {
"use strict";
function isInGameUrl(url) {
return /https:\/\/boardgamearena\.com\/\d+\/flipseven\?table=\d+/.test(url);
}
// Card counting data initialization
function getInitialCardDict() {
return {
"12card": 12,
"11card": 11,
"10card": 10,
"9card": 9,
"8card": 8,
"7card": 7,
"6card": 6,
"5card": 5,
"4card": 4,
"3card": 3,
"2card": 2,
"1card": 1,
"0card": 1,
flip3: 3,
"Second chance": 3,
Freeze: 3,
Plus2: 1,
Plus4: 1,
Plus6: 1,
Plus8: 1,
Plus10: 1,
double: 1,
};
}
let cardDict = null;
let roundCardDict = null; // Current round card counting data
let playerBoardDict = null; // All players' board cards, array, each element is a player's card object
let busted_players = {};
let stayedPlayers = {};
let frozenPlayers = {};
function getInitialPlayerBoardDict() {
// Same structure as cardDict, all values initialized to 0
return Object.fromEntries(
Object.keys(getInitialCardDict()).map((k) => [k, 0])
);
}
function clearPlayerBoardDict(idx) {
// idx: optional, specify player index, if not provided, clear all
if (Array.isArray(playerBoardDict)) {
if (typeof idx === "number") {
Object.keys(playerBoardDict[idx]).forEach(
(k) => (playerBoardDict[idx][k] = 0)
);
// console.log(
// `[Flip Seven Counter] Player ${idx + 1} board cleared`,
// playerBoardDict[idx]
// );
} else {
playerBoardDict.forEach((dict, i) => {
Object.keys(dict).forEach((k) => (dict[k] = 0));
});
console.log(
"[Flip Seven Counter] All players board cleared",
playerBoardDict
);
}
}
}
function clearRoundCardDict() {
if (roundCardDict) {
Object.keys(roundCardDict).forEach((k) => (roundCardDict[k] = 0));
console.log(
"[Flip Seven Counter] Round card data cleared",
roundCardDict
);
}
}
function resetBustedPlayers() {
const playerNames = window.flipsevenPlayerNames || [];
busted_players = {};
stayedPlayers = {};
frozenPlayers = {};
playerNames.forEach((name) => {
busted_players[name] = false;
stayedPlayers[name] = false;
frozenPlayers[name] = false;
});
}
function createCardCounterPanel() {
// Create floating panel
let panel = document.createElement("div");
panel.id = "flipseven-card-counter-panel";
panel.style.position = "fixed";
panel.style.top = "80px";
panel.style.right = "20px";
panel.style.zIndex = "99999";
panel.style.background = "rgba(173, 216, 230, 0.85)"; // light blue semi-transparent
panel.style.border = "1px solid #5bb";
panel.style.borderRadius = "8px";
panel.style.boxShadow = "0 2px 8px rgba(0,0,0,0.15)";
panel.style.padding = "12px 16px";
panel.style.fontSize = "15px";
panel.style.color = "#222";
panel.style.maxHeight = "80vh";
panel.style.overflowY = "auto";
panel.style.minWidth = "180px";
panel.style.userSelect = "text";
panel.style.cursor = "move"; // draggable cursor
panel.innerHTML =
'<b>Flip Seven Counter</b><hr style="margin:6px 0;">' +
renderCardDictTable(cardDict) +
'<div style="height:18px;"></div>' +
'<div style="font-size: 1.5em; font-weight: bold; text-align:left;">rate <span style="float:right;">100%</span></div>';
document.body.appendChild(panel);
makePanelDraggable(panel);
}
function getSafeRate() {
// Calculate the probability of safe cards (not in any player's hand)
let safe = 0,
total = 0;
for (const k in cardDict) {
if (!playerBoardDict || playerBoardDict.every((dict) => dict[k] === 0)) {
safe += cardDict[k];
}
total += cardDict[k];
}
if (total === 0) return 100;
return Math.round((safe / total) * 100);
}
function getPlayerSafeRate(idx) {
// Calculate the safe card probability for a specific player
let safe = 0,
total = 0;
if (!playerBoardDict || !playerBoardDict[idx]) return 100;
for (const k in cardDict) {
if (playerBoardDict[idx][k] === 0) {
safe += cardDict[k];
}
total += cardDict[k];
}
if (total === 0) return 100;
return Math.round((safe / total) * 100);
}
function updateCardCounterPanel(flashKey) {
const panel = document.getElementById("flipseven-card-counter-panel");
if (panel) {
const playerNames = window.flipsevenPlayerNames || [];
let namesHtml = playerNames
.map((n, idx) => {
let shortName = n.length > 6 ? n.slice(0, 6) : n;
if (busted_players[n]) {
return `<div style="margin-bottom:2px;"><span style="display:inline-block;max-width:6em;overflow:hidden;text-overflow:ellipsis;vertical-align:middle;">${shortName}</span> <span style='color:#888;font-size:0.95em;'>Busted</span></div>`;
} else if (frozenPlayers[n]) {
return `<div style="margin-bottom:2px;"><span style="display:inline-block;max-width:6em;overflow:hidden;text-overflow:ellipsis;vertical-align:middle;">${shortName}</span> <span style='color:#888;font-size:0.95em;'>Frozen</span></div>`;
} else if (stayedPlayers[n]) {
return `<div style="margin-bottom:2px;"><span style="display:inline-block;max-width:6em;overflow:hidden;text-overflow:ellipsis;vertical-align:middle;">${shortName}</span> <span style='color:#888;font-size:0.95em;'>Stayed</span></div>`;
} else {
let rate = getPlayerSafeRate(idx);
let rateColor;
if (rate < 30) rateColor = "#b94a48";
else if (rate < 50) rateColor = "#bfae3b";
else rateColor = "#4a7b5b";
return `<div style="margin-bottom:2px;"><span style="display:inline-block;max-width:6em;overflow:hidden;text-overflow:ellipsis;vertical-align:middle;">${shortName}</span> <span style='color:${rateColor};font-size:0.95em;'>${rate}%</span></div>`;
}
})
.join("");
panel.innerHTML =
'<b>Flip Seven Counter</b><hr style="margin:6px 0;">' +
renderCardDictTable(cardDict) +
'<div style="height:18px;"></div>' +
`<div style="font-size: 1.2em; font-weight: bold; text-align:left;">${namesHtml}</div>`;
if (flashKey) flashNumberCell(flashKey);
}
}
// Draggable panel functionality
function makePanelDraggable(panel) {
let isDragging = false;
let offsetX = 0,
offsetY = 0;
panel.addEventListener("mousedown", function (e) {
isDragging = true;
offsetX = e.clientX - panel.getBoundingClientRect().left;
offsetY = e.clientY - panel.getBoundingClientRect().top;
document.body.style.userSelect = "none";
});
document.addEventListener("mousemove", function (e) {
if (isDragging) {
panel.style.left = e.clientX - offsetX + "px";
panel.style.top = e.clientY - offsetY + "px";
panel.style.right = "";
}
});
document.addEventListener("mouseup", function () {
isDragging = false;
document.body.style.userSelect = "";
});
}
function renderCardDictTable(dict) {
let html = '<table style="border-collapse:collapse;width:100%;">';
const totalLeft = Object.values(dict).reduce((a, b) => a + b, 0) || 1;
for (const [k, v] of Object.entries(dict)) {
const percent = Math.round((v / totalLeft) * 100);
const percentColor = "#888";
let numColor = "#888";
if (v === 1 || v === 2) numColor = "#2ecc40";
else if (v >= 3 && v <= 5) numColor = "#ffdc00";
else if (v > 5) numColor = "#ff4136";
html += `<tr><td style='padding:2px 6px;'>${k}</td><td class='flipseven-anim-num' data-key='${k}' style='padding:2px 6px;text-align:right;color:${numColor};font-weight:bold;'>${v} <span style='font-size:0.9em;color:${percentColor};'>(${percent}%)</span></td></tr>`;
}
html += "</table>";
return html;
}
function flashNumberCell(key) {
const cell = document.querySelector(
`#flipseven-card-counter-panel .flipseven-anim-num[data-key='${key}']`
);
if (cell) {
cell.style.transition = "background 0.2s";
cell.style.background = "#fff7b2";
setTimeout(() => {
cell.style.background = "";
}, 200);
}
}
function updatePlayerBoardDictFromDOM() {
// Get player count
const playerNames = window.flipsevenPlayerNames || [];
const playerCount = playerNames.length;
// Process each player
for (let i = 0; i < playerCount; i++) {
const container = document.querySelector(
`#app > div > div > div.f7_scalable.f7_scalable_zoom > div > div.f7_players_container.grid > div:nth-child(${
i + 1
}) > div:nth-child(3)`
);
if (!container) {
console.warn(
`[Flip Seven Counter] Player ${i + 1} board container not found`
);
continue;
}
// Clear this player's stats
clearPlayerBoardDict(i);
// Count all cards
const cardDivs = container.querySelectorAll(".flippable-front");
cardDivs.forEach((frontDiv) => {
// class like 'flippable-front sprite sprite-c8', get the number
const classList = frontDiv.className.split(" ");
const spriteClass = classList.find((cls) => cls.startsWith("sprite-c"));
if (spriteClass) {
const num = spriteClass.replace("sprite-c", "");
if (/^\d+$/.test(num)) {
const key = num + "card";
if (playerBoardDict[i].hasOwnProperty(key)) {
playerBoardDict[i][key] += 1;
}
}
}
});
// console.log(`[Flip Seven Counter] Player ${i+1} board:`, JSON.parse(JSON.stringify(playerBoardDict[i])));
}
}
// Periodic event: check every 300ms
function startPlayerBoardMonitor() {
setInterval(updatePlayerBoardDictFromDOM, 300);
}
// Log monitor
let lc = 0; // log counter
function startLogMonitor() {
setInterval(() => {
const logElem = document.getElementById("log_" + lc);
if (!logElem) return; // No such log, wait for next
// Check for new round
const firstDiv = logElem.querySelector("div");
console.log(
`[Flip Seven Counter] firstDiv.innerText = ${firstDiv.innerText.trim()} `
);
if (
firstDiv?.innerText &&
(firstDiv.innerText.trim().includes("新的一轮") ||
/new round/gi.exec(firstDiv.innerText))
) {
clearRoundCardDict();
resetBustedPlayers();
updateCardCounterPanel();
lc++;
return;
}
if (
firstDiv?.innerText &&
(firstDiv.innerText.includes("弃牌堆洗牌") ||
/shuffle/gi.exec(firstDiv.innerText))
) {
cardDict = getInitialCardDict();
for (const k in roundCardDict) {
if (cardDict.hasOwnProperty(k)) {
cardDict[k] = Math.max(0, cardDict[k] - roundCardDict[k]);
}
}
updateCardCounterPanel();
lc++;
return;
}
if (
firstDiv?.innerText &&
(firstDiv.innerText.includes("爆牌") ||
/bust/gi.exec(firstDiv.innerText))
) {
// 查找 span.playername
const nameSpan = firstDiv.querySelector("span.playername");
if (nameSpan) {
const bustedName = nameSpan.innerText.trim();
if (busted_players.hasOwnProperty(bustedName)) {
busted_players[bustedName] = true;
updateCardCounterPanel();
}
}
}
if (firstDiv?.innerText && /stay/gi.exec(firstDiv.innerText)) {
const nameSpan = firstDiv.querySelector("span.playername");
if (nameSpan) {
const stayedName = nameSpan.innerText.trim();
if (stayedPlayers.hasOwnProperty(stayedName)) {
stayedPlayers[stayedName] = true;
updateCardCounterPanel();
}
}
}
if (firstDiv?.innerText && /freezes/gi.exec(firstDiv.innerText)) {
const nameSpan = firstDiv.querySelector("span.playername");
if (nameSpan) {
const frozenName = nameSpan.innerText.trim();
if (frozenPlayers.hasOwnProperty(frozenName)) {
frozenPlayers[frozenName] = true;
updateCardCounterPanel();
}
}
}
if (
firstDiv?.innerText &&
((firstDiv.innerText.includes("第二次机会") &&
firstDiv.innerText.includes("卡牌被弃除")) ||
/second chance/gi.exec(firstDiv.innerText))
) {
if (cardDict["Second chance"] > 0) {
cardDict["Second chance"]--;
console.log(
'[Flip Seven Counter] "第二次机会"卡牌被弃除,cardDict[Second chance]--,当前剩余:',
cardDict["Second chance"]
);
updateCardCounterPanel("Second chance");
}
}
// Check for card type
const cardElem = logElem.querySelector(
".visible_flippable.f7_token_card.f7_logs"
);
if (!cardElem) {
lc++;
return; // No card, skip
}
// Find the only child div's only child div
let frontDiv = cardElem;
frontDiv = frontDiv.children[0];
frontDiv = frontDiv.children[0];
if (!frontDiv?.className) {
lc++;
return;
}
// Parse className
const classList = frontDiv.className.split(" ");
const spriteClass = classList.find((cls) => cls.startsWith("sprite-"));
if (!spriteClass) {
lc++;
return;
}
// Handle number cards
let key = null;
if (/^sprite-c(\d+)$/.test(spriteClass)) {
const num = spriteClass.match(/^sprite-c(\d+)$/)[1];
key = num + "card";
} else if (/^sprite-s(\d+)$/.test(spriteClass)) {
// Plus2/4/6/8/10
const num = spriteClass.match(/^sprite-s(\d+)$/)[1];
key = "Plus" + num;
} else if (spriteClass === "sprite-sf") {
key = "Freeze";
} else if (spriteClass === "sprite-sch") {
key = "Second chance";
} else if (spriteClass === "sprite-sf3") {
key = "flip3";
} else if (spriteClass === "sprite-sx2") {
key = "double";
}
if (
key &&
cardDict.hasOwnProperty(key) &&
roundCardDict.hasOwnProperty(key)
) {
if (cardDict[key] > 0) cardDict[key]--;
roundCardDict[key]++;
console.log(
`[Flip Seven Counter] log_${lc} found ${key}, global left ${cardDict[key]}, round used ${roundCardDict[key]}`
);
updateCardCounterPanel(key);
} else {
console.log("spriteClass -> ", spriteClass);
console.log(
`[Flip Seven Counter] log_${lc} unknown card type`,
spriteClass
);
}
lc++;
if (lc) {
console.log(`[Flip Seven Counter] ${lc} unknown card type`);
}
}, 200);
}
function initializeGame() {
cardDict = getInitialCardDict();
roundCardDict = Object.fromEntries(
Object.keys(cardDict).map((k) => [k, 0])
);
playerBoardDict = Array.from({ length: 12 }, () =>
getInitialPlayerBoardDict()
);
resetBustedPlayers();
console.log("[Flip Seven Counter] Card data initialized", cardDict);
console.log(
"[Flip Seven Counter] Round card data initialized",
roundCardDict
);
console.log(
"[Flip Seven Counter] All players board initialized",
playerBoardDict
);
createCardCounterPanel();
startPlayerBoardMonitor();
startLogMonitor();
// You can continue to extend initialization logic here
}
function runLogic() {
setTimeout(() => {
// Detect all player names
let playerNames = [];
for (let i = 1; i <= 12; i++) {
const selector = `#app > div > div > div.f7_scalable.f7_scalable_zoom > div > div.f7_players_container > div:nth-child(${i}) > div.f7_player_name.flex.justify-between > div:nth-child(1)`;
const nameElem = document.querySelector(selector);
if (nameElem?.innerText?.trim()) {
playerNames.push(nameElem.innerText.trim());
} else {
break;
}
}
alert(
`[Flip Seven Counter] Entered game room. Player list:\n` +
playerNames.map((n, idx) => `${idx + 1}. ${n}`).join("\n")
);
window.flipsevenPlayerNames = playerNames; // global access
initializeGame();
// You can continue your logic here
}, 1500);
}
// First enter page
if (isInGameUrl(window.location.href)) {
runLogic();
}
// Listen for SPA navigation
function onUrlChange() {
if (isInGameUrl(window.location.href)) {
runLogic();
}
}
const _pushState = history.pushState;
const _replaceState = history.replaceState;
history.pushState = function () {
_pushState.apply(this, arguments);
setTimeout(onUrlChange, 0);
};
history.replaceState = function () {
_replaceState.apply(this, arguments);
setTimeout(onUrlChange, 0);
};
window.addEventListener("popstate", onUrlChange);
})();