BGA Flip Seven Card Counter

Card counter for Flip Seven on BoardGameArena

当前为 2025-05-17 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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);

})();