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