您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Card counter for Flip Seven on BoardGameArena
// ==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); })();