您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Card counter for Flip Seven on BoardGameArena
当前为
// ==UserScript== // @name BGA Flip Seven Card Counter // @namespace http://tampermonkey.net/ // @version 0.1 // @description Card counter for Flip Seven on BoardGameArena // @author KuRRe8 // @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 = {}; 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 = {}; playerNames.forEach(name => { busted_players[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 getPlayerSafeRate(idx) { // Calculate the safe card probability for a specific player let safe = 0, total = 0; if (!playerBoardDict) return -1; if (!playerBoardDict[idx]) return -2; for (const k in cardDict) { if (playerBoardDict[idx][k] === 0) { safe += cardDict[k]; } total += cardDict[k]; } if (total === 0) return -3; 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 > 10 ? n.slice(0, 10) : 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 { let rate = getPlayerSafeRate(idx); let rateColor = '#888'; 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 > 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() { let lastLogInfo = null; // 记录上一次log的有用信息 {playerName, cardKey} 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'); if (firstDiv && firstDiv.innerText && firstDiv.innerText.trim().includes('新的一轮')) { clearRoundCardDict(); resetBustedPlayers(); updateCardCounterPanel(); lc++; return; } if (firstDiv && firstDiv.innerText && firstDiv.innerText.includes('弃牌堆洗牌')) { 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 && firstDiv.innerText && firstDiv.innerText.includes('爆牌')) { // 查找 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 && firstDiv.innerText && firstDiv.innerText.includes('第二次机会') && firstDiv.innerText.includes('卡牌被弃除')) { if (cardDict['Second chance'] > 0) { cardDict['Second chance']--; console.log('[Flip Seven Counter] "第二次机会"卡牌被弃除,cardDict[Second chance]--,当前剩余:', cardDict['Second chance']); updateCardCounterPanel('Second chance'); } } if (firstDiv && firstDiv.innerText && firstDiv.innerText.includes('放弃“第二次机会”以及他们刚抽到的牌')) { if (lastLogInfo && lastLogInfo.cardKey) { if (roundCardDict && roundCardDict[lastLogInfo.cardKey] > 0) { roundCardDict[lastLogInfo.cardKey]--; console.log(`[Flip Seven Counter] LogicA: ${lastLogInfo.cardKey}从roundCardDict中剔除`); } if (roundCardDict && roundCardDict['Second chance'] > 0) { roundCardDict['Second chance']--; console.log('[Flip Seven Counter] LogicA: 剔除一张Second chance卡 from roundCardDict, 剩余:', roundCardDict['Second chance']); } updateCardCounterPanel(lastLogInfo.cardKey); } } // 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 || !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'; } let playerName = null; const nameSpan = firstDiv.querySelector && firstDiv.querySelector('span.playername'); if (nameSpan) { playerName = nameSpan.innerText.trim(); } if (playerName && key) { lastLogInfo = { playerName, cardKey: key }; } 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(`[Flip Seven Counter] log_${lc} unknown card type`, spriteClass); } lc++; }, 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 && 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); })();