|
// ==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); |
|
|
|
})(); |
I want to give you an update to allow English game 😄
I also added a frozen and stayed message