Skip to content

Instantly share code, notes, and snippets.

@KuRRe8
Last active June 4, 2025 23:12
Show Gist options
  • Save KuRRe8/5d4199a862e0180df78da68f5e01f7fe to your computer and use it in GitHub Desktop.
Save KuRRe8/5d4199a862e0180df78da68f5e01f7fe to your computer and use it in GitHub Desktop.
Flip 7 card counter on BoardGameArena

Flip 7

A simple game that highly relies on luck.

Card counter

This is a script for TamperMonkey on Chrome/Edge.

Try to read TamperMonkey docs to know how to run it.

The 0.1 version does only support to Chinese UI of BGA. There are several places related to Chinese, 新的一轮 弃牌堆洗牌 爆牌。Please search for other languages translation for these phrase for the purpose of substitution.

There is an update for English UI in comments section

Screenshot 2025-05-16 at 7 26 09 AM

I am C++/ Python coder, not familiar with the coding standards of JavaScript, so please tolerate any shortcomings in my code

// ==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);
})();
@fpronto
Copy link

fpronto commented Jun 4, 2025

I want to give you an update to allow English game 😄
I also added a frozen and stayed message

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

@KuRRe8
Copy link
Author

KuRRe8 commented Jun 4, 2025

I want to give you an update to allow English game 😄 I also added a frozen and stayed message

// ==UserScript==
...

many thanks. I will update this on Greasy Fork and add you as contributor. but here in Gist, I will refactor the code at some point in the future to automatically detect languages

@KuRRe8
Copy link
Author

KuRRe8 commented Jun 4, 2025

I want to give you an update to allow English game 😄 I also added a frozen and stayed message

// ==UserScript==
...

you may have made modifications based on the version I posted on Greasy Fork(which is an old version). There was a small bug in a previous version. please refer to the line 300 in my code, I added a logic handler for 'second chance' card in last round before shuffling cards.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment