let finished = false; let boardDrawn = false; let readyagain = false; const refreshPeriod = 1000; const ringColors = ['ring-red', 'ring-blue', 'ring-green', 'ring-purple', 'dot']; // --- Client-side timer & money decay --- const gameStart = js_vars.game_start * 1000; // ms let timeLeft = js_vars.total_time; // s const startMoney = js_vars.initial_money; const decayRate = startMoney / js_vars.total_time; let moneyLeft = startMoney; // --- Client-side coalition timer & end-game trigger --- const coalitionThreshold = js_vars.coalitionThreshold; let coalitionTimerId = null; let coalitionTimeLeft = 0; let lastCoalitionSig = null; // track identity (cell+members) of the active coalition function handleEndGame(reason) { if (finished) return; finished = true; clearInterval(tickInterval); if (coalitionTimerId) { clearInterval(coalitionTimerId); coalitionTimerId = null; } liveSend({ end_game: true, reason, remaining_money: moneyLeft }); disableBoard(); } function renderCoalitionTimer(sec) { const el = document.getElementById("coalition_timer"); el.style.display = sec > 0 ? "block" : "none"; el.textContent = sec > 0 ? `Time Until the Agreement is Finalized: ${sec}s` : ""; } // Coalition identity should NOT change when membership grows/shrinks (e.g., 2→3), // only when the coalition *moves* to a different cell. So we key it on red-cell positions. function coalitionSignature(cellStates) { if (!cellStates) return null; const reds = Object.keys(cellStates) .filter(pos => !!cellStates[pos].red) .sort(); // canonical order if >1 red cell exists return reds.length ? reds.join(";") : null; } function startCoalitionTimer() { if (coalitionTimerId) { clearInterval(coalitionTimerId); coalitionTimerId = null; } coalitionTimeLeft = coalitionThreshold; renderCoalitionTimer(coalitionTimeLeft); coalitionTimerId = setInterval(() => { coalitionTimeLeft--; renderCoalitionTimer(coalitionTimeLeft); if (coalitionTimeLeft <= 0) { handleEndGame("coalition"); } }, 1000); } function stopCoalitionTimer() { if (coalitionTimerId) { clearInterval(coalitionTimerId); coalitionTimerId = null; } renderCoalitionTimer(0); readyagain = true; } function clientTick() { const now = Date.now(); const elapsed = Math.floor((now - gameStart) / 1000); timeLeft = Math.max(0, js_vars.total_time - elapsed); moneyLeft = Math.max(0, startMoney - decayRate * elapsed); document.getElementById('remaining_money').innerText = `Each point is worth: £${moneyLeft.toFixed(3)}`; if (timeLeft === 0) { handleEndGame("time"); } } const tickInterval = setInterval(clientTick, refreshPeriod); // --- Coalition timer display --- function updateCoalitionTimer(timeToCoalition) { const coalitionTimerElement = document.getElementById("coalition_timer"); if (timeToCoalition > 0) { coalitionTimerElement.style.display = "block"; // Show the timer coalitionTimerElement.textContent = `Time Until the Agreement is Finalized: ${timeToCoalition}s`; } else { coalitionTimerElement.style.display = "none"; // Hide the timer coalitionTimerElement.textContent = ""; // Clear content } } // --- Board utilities --- function disableBoard() { document.querySelectorAll(".cell").forEach(cell => { cell.style.pointerEvents = "none"; cell.style.opacity = "0.5"; }); } function rotate(row, col, by) { const N = 13, R = N + 1; if (by === 0) { return [row, col]; } else if (by === 120) { return [col, R - row - col + 1]; } else if (by === 240) { return [R - row - col + 1, row]; } else { throw new Error("Invalid rotation angle"); } } function clearRingsAndDot(el) { el.classList.remove(...Array.from(el.classList).filter(c => ringColors.includes(c))); el.querySelectorAll('.ring').forEach(ring => { ring.classList.remove(...Array.from(ring.classList).filter(c => ringColors.includes(c))); }); } function clearSelection() { liveSend({ undo_move: true }); createBaseBoard(); } function calculateGain(row, col) { const you = row - 1; const A = 14 - row - col; const B = col - 1; document.getElementById("gain-you").textContent = ` ${you}`; document.getElementById("gain-a").textContent = ` ${A}`; document.getElementById("gain-b").textContent = ` ${B}`; } function resetGains() { document.getElementById("gain-you").textContent = " --"; document.getElementById("gain-a").textContent = " --"; document.getElementById("gain-b").textContent = " --"; } function addHoverEffect() { document.querySelectorAll(".cell").forEach(cell => { cell.addEventListener("mouseenter", () => { calculateGain(+cell.dataset.row, +cell.dataset.col); }); cell.addEventListener("mouseleave", () => { resetGains(); }); }); } function createCell(row, col) { const cellDiv = document.createElement("div"); cellDiv.classList.add("cell"); cellDiv.id = `square_${row}_${col}`; cellDiv.dataset.row = row; cellDiv.dataset.col = col; for (let i = 1; i <= 4; i++) { const ring = document.createElement("div"); ring.classList.add("ring", `ring-${i}`); ring.id = `ring_${row}_${col}_${i}`; cellDiv.appendChild(ring); } cellDiv.onclick = e => { e.stopPropagation(); sendMove(cellDiv); }; return cellDiv; } function toggleRing(cell, ringNumber, color) { let ring = ringNumber >= 1 ? cell.querySelector(`#ring_${cell.dataset.row}_${cell.dataset.col}_${ringNumber}`) : cell; ring.classList.remove(...ringColors); if (color) ring.classList.add(`ring-${color}`); } // --- only changed cells get rendered --- let prevStates = {}; // keyed by "r,c" -> { green, blue, purple, red, order[] } const elCache = new Map(); // key "r,c" -> cell element function cacheBoardElements() { if (elCache.size) return; document.querySelectorAll('.cell').forEach(cell => { elCache.set(`${cell.dataset.row},${cell.dataset.col}`, cell); }); } // Replace createBaseBoard()’s "clear all rings" path with a no-op: function createBaseBoard() { const counts = [13,12,11,10,9,8,7,6,5,4,3,2,1]; if (!boardDrawn) { counts.forEach((cnt, idx) => { const rowDiv = document.createElement("div"); rowDiv.classList.add("row"); const row = idx + 1; for (let col = 1; col <= cnt; col++) { rowDiv.appendChild(createCell(row, col)); } document.querySelector(".board").appendChild(rowDiv); }); boardDrawn = true; cacheBoardElements(); } } // Patch only changed cells function applyDiff(cellStates) { // clear cells that disappeared for (const key of Object.keys(prevStates)) { if (!(key in cellStates)) { const cell = elCache.get(key); if (cell) clearRingsAndDot(cell); } } // apply changes / draws for (const [key, state] of Object.entries(cellStates)) { const prev = prevStates[key]; if (!prev || JSON.stringify(prev) !== JSON.stringify(state)) { const [r, c] = key.split(',').map(Number); const [rr, cc] = rotate(r, c, angleFromSource); const cell = document.getElementById(`square_${rr}_${cc}`); // reset specific cell only clearRingsAndDot(cell); if (state.red) { document.getElementById(`ring_${rr}_${cc}_4`).classList.add('dot'); } state.order.forEach((color, i) => toggleRing(cell, i + 1, color)); } } prevStates = cellStates; } function drawRings(cellStates) { createBaseBoard(); applyDiff(cellStates); } function sendMove(cell) { const raw = [parseInt(cell.dataset.row), parseInt(cell.dataset.col)]; const move = rotate(raw[0], raw[1], angleToSource); if (!finished) liveSend({ move }); } // --- Live method integration --- function liveRecv(data) { if (data.cell_states) { drawRings(data.cell_states); } // compute identity of the coalition (cell + members) const sig = coalitionSignature(data.cell_states); if (data.coalition !== undefined) { if (data.coalition) { // only start if not already running if (!coalitionTimerId && readyagain && !finished) { startCoalitionTimer(); } // A) coalition still true, but identity changed -> restart timer if (coalitionTimerId && lastCoalitionSig !== null && sig !== lastCoalitionSig) { clearInterval(coalitionTimerId); coalitionTimerId = null; if (!finished) startCoalitionTimer(); } // B) no timer running -> start if your original gate allows it else if (!coalitionTimerId && readyagain && !finished) { startCoalitionTimer(); } } else { stopCoalitionTimer(); } } // remember which coalition we are timing lastCoalitionSig = sig; if (data.end_game) { finished = true; disableBoard(); } if (data.end_live === true) { finished = true; disableBoard(); document.getElementById("form").submit(); } } // --- Page setup --- document.addEventListener("DOMContentLoaded", () => { const myColor = js_vars.my_color; const participantsColors = js_vars.participants_colors; const myId = js_vars.my_id; const labels = { 1: { bottom:1, left:3, right:2 }, 2: { bottom:2, left:1, right:3 }, 3: { bottom:3, left:2, right:1 } }; const pos = labels[myId]; document.querySelector(".player-info p") .style.color = participantsColors[pos.bottom]; document.querySelector(".participant-left p") .style.color = participantsColors[pos.left]; document.querySelector(".participant-right p").style.color = participantsColors[pos.right]; liveSend({ ready: true }); createBaseBoard(); addHoverEffect(); }); // Clear selection if user clicks or mouses down outside the triangle board document.addEventListener("mousedown", function (event) { const board = document.getElementById("triangle-board"); if (board && !board.contains(event.target)) { clearSelection(); } }); // Also clear on any click anywhere window.onclick = clearSelection; window.addEventListener('load', function () { window.scrollTo({ top: 80, // adjust this value as needed behavior: 'smooth' }); });