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);
})();
@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