Add zero state, disconnected overlay, game over confetti, and debug panel

- Zero state: bouncing dice + message when no game exists
- Disconnected: greyed overlay after 3 consecutive fetch failures,
  with retry countdown
- Game over: gold banner with winner name + confetti animation,
  detected from log ('WINS!') or single remaining player
- Debug panel: Ctrl+Shift+D toggles hidden panel with buttons to
  test all four states (zero, playing, gameover, disconnected, reset)
- Also fixed player panel to use p.number for current-turn matching
  and property ownership display
This commit is contained in:
Jarvis 2026-02-21 11:08:06 +00:00
parent 19fc56d982
commit 34a4f47402
2 changed files with 547 additions and 250 deletions

View file

@ -3,8 +3,8 @@
{
"name": "alice",
"number": 1,
"money": 624,
"location": 21,
"money": 919,
"location": 25,
"inJail": false,
"jailTurns": 0,
"doublesCount": 0,
@ -13,8 +13,8 @@
{
"name": "bob",
"number": 2,
"money": 1229,
"location": 23,
"money": 1292,
"location": 37,
"inJail": false,
"jailTurns": 0,
"doublesCount": 0,
@ -23,15 +23,15 @@
{
"name": "charlie",
"number": 3,
"money": -12,
"location": 24,
"inJail": false,
"money": 320,
"location": 40,
"inJail": true,
"jailTurns": 0,
"doublesCount": 0,
"getOutOfJailFreeCards": 1
}
],
"currentPlayer": 3,
"currentPlayer": 2,
"squares": [
{
"id": 0,
@ -96,7 +96,7 @@
"id": 8,
"name": "Vermont ave. (L)",
"type": "property",
"owner": null,
"owner": 3,
"mortgaged": false,
"group": "lightblue",
"cost": 100,
@ -370,150 +370,149 @@
],
"log": [
{
"text": "roll is 6, 5",
"text": "charlie's turn \u2014 $320 on Electric Co.",
"player": "charlie",
"timestamp": "2026-02-21 11:01:40"
"timestamp": "2026-02-21 11:03:37"
},
{
"text": "Passed GO \u2014 collected $200",
"text": "roll is 4, 6",
"player": "charlie",
"timestamp": "2026-02-21 11:01:40"
"timestamp": "2026-02-21 11:03:38"
},
{
"text": "Landed on Income Tax",
"text": "Landed on Chance ii",
"player": "charlie",
"timestamp": "2026-02-21 11:01:41"
"timestamp": "2026-02-21 11:03:39"
},
{
"text": "alice's turn \u2014 $289 on Just Visiting",
"player": "alice",
"timestamp": "2026-02-21 11:01:47"
},
{
"text": "Trade completed between alice and bob",
"player": null
},
{
"text": "alice's turn \u2014 $484 on Just Visiting",
"player": "alice",
"timestamp": "2026-02-21 11:01:58"
},
{
"text": "roll is 3, 2",
"player": "alice",
"timestamp": "2026-02-21 11:01:59"
},
{
"text": "Landed on Pennsylvania RR",
"player": "alice",
"timestamp": "2026-02-21 11:01:59"
},
{
"text": "bob's turn \u2014 $1279 on Electric Co.",
"player": "bob",
"timestamp": "2026-02-21 11:02:00"
},
{
"text": "roll is 1, 5",
"player": "bob",
"timestamp": "2026-02-21 11:02:02"
},
{
"text": "Landed on Tennessee ave. (O)",
"player": "bob",
"timestamp": "2026-02-21 11:02:02"
},
{
"text": "Paid $14 rent to alice",
"player": "bob"
},
{
"text": "charlie's turn \u2014 $78 on Income Tax",
"player": "charlie",
"timestamp": "2026-02-21 11:02:04"
},
{
"text": "roll is 6, 5",
"player": "charlie",
"timestamp": "2026-02-21 11:02:05"
},
{
"text": "Landed on Pennsylvania RR",
"player": "charlie",
"timestamp": "2026-02-21 11:02:05"
},
{
"text": "Paid $50 rent to alice",
"text": "Go Back 3 Spaces",
"player": "charlie"
},
{
"text": "alice's turn \u2014 $548 on Pennsylvania RR",
"text": "Landed on New York ave. (O)",
"player": "charlie",
"timestamp": "2026-02-21 11:03:41"
},
{
"text": "alice's turn \u2014 $912 on States ave. (V)",
"player": "alice",
"timestamp": "2026-02-21 11:02:07"
},
{
"text": "roll is 5, 1",
"player": "alice",
"timestamp": "2026-02-21 11:02:08"
},
{
"text": "Landed on Kentucky ave. (R)",
"player": "alice",
"timestamp": "2026-02-21 11:02:09"
},
{
"text": "bob's turn \u2014 $1265 on Tennessee ave. (O)",
"player": "bob",
"timestamp": "2026-02-21 11:02:10"
},
{
"text": "roll is 1, 1",
"player": "bob",
"timestamp": "2026-02-21 11:02:11"
},
{
"text": "Landed on Free Parking",
"player": "bob",
"timestamp": "2026-02-21 11:02:11"
},
{
"text": "bob's turn \u2014 $1265 on Free Parking",
"player": "bob",
"timestamp": "2026-02-21 11:02:13"
"timestamp": "2026-02-21 11:03:42"
},
{
"text": "roll is 1, 2",
"player": "bob",
"timestamp": "2026-02-21 11:02:14"
"player": "alice",
"timestamp": "2026-02-21 11:03:43"
},
{
"text": "Landed on Indiana ave. (R)",
"text": "Landed on St. James pl. (O)",
"player": "alice",
"timestamp": "2026-02-21 11:03:43"
},
{
"text": "Paid $14 rent to bob",
"player": "alice"
},
{
"text": "bob's turn \u2014 $1113 on Pennsylvania RR",
"player": "bob",
"timestamp": "2026-02-21 11:02:14"
"timestamp": "2026-02-21 11:03:45"
},
{
"text": "roll is 5, 1",
"player": "bob",
"timestamp": "2026-02-21 11:03:46"
},
{
"text": "Landed on Kentucky ave. (R)",
"player": "bob",
"timestamp": "2026-02-21 11:03:47"
},
{
"text": "Paid $36 rent to alice",
"player": "bob"
},
{
"text": "charlie's turn \u2014 $28 on Pennsylvania RR",
"text": "charlie's turn \u2014 $320 on New York ave. (O)",
"player": "charlie",
"timestamp": "2026-02-21 11:02:16"
"timestamp": "2026-02-21 11:03:48"
},
{
"text": "roll is 6, 5",
"player": "charlie",
"timestamp": "2026-02-21 11:03:50"
},
{
"text": "Landed on GO TO JAIL!",
"player": "charlie",
"timestamp": "2026-02-21 11:03:50"
},
{
"text": "alice's turn \u2014 $934 on St. James pl. (O)",
"player": "alice",
"timestamp": "2026-02-21 11:03:51"
},
{
"text": "roll is 5, 4",
"player": "charlie",
"timestamp": "2026-02-21 11:02:17"
"player": "alice",
"timestamp": "2026-02-21 11:03:52"
},
{
"text": "Landed on Illinois ave. (R)",
"player": "charlie",
"timestamp": "2026-02-21 11:02:18"
"text": "Landed on B&O RR",
"player": "alice",
"timestamp": "2026-02-21 11:03:52"
},
{
"text": "Paid $40 rent to alice",
"player": "charlie"
"text": "Paid $50 rent to bob",
"player": "alice"
},
{
"text": "bob's turn \u2014 $1127 on Kentucky ave. (R)",
"player": "bob",
"timestamp": "2026-02-21 11:03:54"
},
{
"text": "bob's turn \u2014 $1127 on Kentucky ave. (R)",
"player": "bob",
"timestamp": "2026-02-21 11:04:04"
},
{
"text": "roll is 6, 6",
"player": "bob",
"timestamp": "2026-02-21 11:04:05"
},
{
"text": "Landed on Community Chest iii",
"player": "bob",
"timestamp": "2026-02-21 11:04:06"
},
{
"text": "Bank Error in Your Favor.",
"player": "bob"
},
{
"text": "bob's turn \u2014 $1327 on Community Chest iii",
"player": "bob",
"timestamp": "2026-02-21 11:04:09"
},
{
"text": "roll is 2, 2",
"player": "bob",
"timestamp": "2026-02-21 11:04:10"
},
{
"text": "Landed on Park place (D)",
"player": "bob",
"timestamp": "2026-02-21 11:04:10"
},
{
"text": "Paid $35 rent to alice",
"player": "bob"
},
{
"text": "bob's turn \u2014 $1292 on Park place (D)",
"player": "bob",
"timestamp": "2026-02-21 11:04:12"
}
],
"lastUpdated": "2026-02-21T11:02:22.605328+00:00"
"lastUpdated": "2026-02-21T11:04:14.993563+00:00"
}

View file

@ -111,6 +111,69 @@ h1 { color: #fff; font-size: 1.5em; text-align: center; }
}
.board-center .subtitle { font-size: 0.35em; color: #888; letter-spacing: 0; margin-top: 8px; font-weight: normal; }
/* ===== ZERO STATE ===== */
.zero-state {
display: flex; flex-direction: column; align-items: center; justify-content: center;
min-height: 80vh; text-align: center; gap: 20px; padding: 40px;
}
.zero-state .dice { font-size: 5em; animation: dice-bounce 2s ease-in-out infinite; }
@keyframes dice-bounce {
0%, 100% { transform: translateY(0) rotate(0deg); }
50% { transform: translateY(-20px) rotate(15deg); }
}
.zero-state h2 { color: #fff; font-size: 2em; }
.zero-state p { color: #888; font-size: 1.1em; max-width: 400px; line-height: 1.5; }
/* ===== DISCONNECTED OVERLAY ===== */
.disconnected-overlay {
position: fixed; inset: 0; background: rgba(0,0,0,0.7);
display: flex; align-items: center; justify-content: center;
z-index: 1000; backdrop-filter: blur(3px);
}
.disconnected-overlay .message {
background: #16213e; border: 2px solid #e74c3c; border-radius: 12px;
padding: 40px; text-align: center; max-width: 400px;
}
.disconnected-overlay .icon { font-size: 3em; margin-bottom: 15px; }
.disconnected-overlay h3 { color: #e74c3c; margin-bottom: 10px; }
.disconnected-overlay p { color: #888; font-size: 0.9em; }
.disconnected-overlay .retry { color: #555; font-size: 0.8em; margin-top: 15px; }
body.greyed-out .container { filter: grayscale(80%) brightness(0.5); pointer-events: none; }
/* ===== GAME OVER ===== */
.game-over-banner {
position: fixed; top: 0; left: 0; right: 0;
background: linear-gradient(135deg, #ffd700, #ff8c00);
color: #1a1a2e; text-align: center; padding: 20px;
font-size: 1.5em; font-weight: bold; z-index: 500;
box-shadow: 0 4px 20px rgba(255, 215, 0, 0.5);
animation: banner-slide 0.5s ease-out;
}
@keyframes banner-slide { from { transform: translateY(-100%); } to { transform: translateY(0); } }
.game-over-banner .winner { font-size: 1.4em; }
.game-over-banner .sub { font-size: 0.5em; font-weight: normal; color: #333; }
body.game-over .container { padding-top: 100px; }
/* Confetti canvas */
#confetti-canvas {
position: fixed; inset: 0; z-index: 499; pointer-events: none;
}
/* ===== DEBUG PANEL ===== */
.debug-panel {
position: fixed; bottom: 10px; right: 10px; background: #0d1117;
border: 1px solid #30363d; border-radius: 8px; padding: 12px;
font-size: 0.75em; z-index: 2000; display: none;
}
.debug-panel.visible { display: block; }
.debug-panel h4 { color: #58a6ff; margin-bottom: 8px; }
.debug-panel button {
display: block; width: 100%; margin: 4px 0; padding: 6px 10px;
background: #21262d; border: 1px solid #30363d; color: #c9d1d9;
border-radius: 4px; cursor: pointer; text-align: left; font-size: 1em;
}
.debug-panel button:hover { background: #30363d; }
@media (max-width: 600px) {
.board-wrapper { width: 95vw; height: 95vw; }
.square { font-size: 0.4em; }
@ -121,7 +184,17 @@ h1 { color: #fff; font-size: 1.5em; text-align: center; }
</style>
</head>
<body>
<div class="container">
<!-- Zero state (shown when no game exists) -->
<div class="zero-state" id="zero-state" style="display:none">
<div class="dice">🎲</div>
<h2>No Game in Progress</h2>
<p>Start a Monopoly game on IRC and the board will appear here automatically.</p>
<div style="color:#555; font-size:0.85em">Checking for game data every 2 seconds...</div>
</div>
<!-- Main game UI -->
<div class="container" id="game-container" style="display:none">
<div>
<h1>🎲 Monopoly Board</h1>
<div class="status" id="status">Loading game state...</div>
@ -140,6 +213,35 @@ h1 { color: #fff; font-size: 1.5em; text-align: center; }
</div>
</div>
<!-- Disconnected overlay -->
<div class="disconnected-overlay" id="disconnect-overlay" style="display:none">
<div class="message">
<div class="icon">📡</div>
<h3>Connection Lost</h3>
<p>Unable to reach the game server. The bridge may be down or the network is unreachable.</p>
<div class="retry" id="disconnect-retry">Retrying in <span id="retry-countdown">--</span>s...</div>
</div>
</div>
<!-- Confetti canvas -->
<canvas id="confetti-canvas"></canvas>
<!-- Game over banner -->
<div class="game-over-banner" id="game-over-banner" style="display:none">
<div class="winner" id="winner-name">🏆 WINS!</div>
<div class="sub">Game Over</div>
</div>
<!-- Debug panel (hidden, activate with Ctrl+Shift+D) -->
<div class="debug-panel" id="debug-panel">
<h4>🔧 Debug States</h4>
<button onclick="debugState('zero')">Zero State (no game)</button>
<button onclick="debugState('playing')">Playing (demo)</button>
<button onclick="debugState('gameover')">Game Over (confetti)</button>
<button onclick="debugState('disconnected')">Disconnected</button>
<button onclick="debugState('reset')">Reset (live mode)</button>
</div>
<script>
const PLAYER_COLORS = ['#e74c3c','#3498db','#2ecc71','#f39c12','#9b59b6','#1abc9c','#e91e63','#00bcd4','#ff5722'];
@ -150,21 +252,37 @@ const GROUP_COLORS = {
};
// Board square positions: [row, col] in an 11x11 grid
// Bottom row (right to left): squares 0-10
// Left col (bottom to top): squares 10-20
// Top row (left to right): squares 20-30
// Right col (top to bottom): squares 30-39 + 0
const POSITIONS = [];
// Bottom row: 0=Go (bottom-right corner) to 10 (bottom-left corner)
for (let i = 0; i <= 10; i++) POSITIONS[i] = [10, 10 - i];
// Left column: 11-19 (going up, excluding corners)
for (let i = 1; i <= 9; i++) POSITIONS[10 + i] = [10 - i, 0];
// Top row: 20=Free Parking (top-left) to 30 (top-right)
for (let i = 0; i <= 10; i++) POSITIONS[20 + i] = [0, i];
// Right column: 31-39 (going down, excluding corners)
for (let i = 1; i <= 9; i++) POSITIONS[30 + i] = [i, 10];
// ===== STATE MANAGEMENT =====
let lastUpdate = '';
let currentView = 'loading'; // loading, zero, playing, gameover, disconnected
let debugMode = null; // null = live, or a forced state name
let consecutiveFailures = 0;
let retryCountdown = 0;
let retryTimer = null;
let gameOverShown = false; // only trigger confetti once per game
let confettiRunning = false;
function showView(view) {
currentView = view;
document.getElementById('zero-state').style.display = view === 'zero' ? 'flex' : 'none';
document.getElementById('game-container').style.display = (view === 'playing' || view === 'gameover') ? 'flex' : 'none';
document.getElementById('disconnect-overlay').style.display = view === 'disconnected' ? 'flex' : 'none';
document.getElementById('game-over-banner').style.display = view === 'gameover' ? 'block' : 'none';
document.body.classList.toggle('greyed-out', view === 'disconnected');
document.body.classList.toggle('game-over', view === 'gameover');
if (view !== 'gameover') {
stopConfetti();
}
}
// ===== RENDERING =====
function renderBoard(state) {
const board = document.getElementById('board');
board.innerHTML = '';
@ -180,7 +298,6 @@ function renderBoard(state) {
const el = document.createElement('div');
el.className = 'square' + (sq.mortgaged ? ' mortgaged' : '');
// Position and size
let x, y, w, h;
const isCorner = [0, 10, 20, 30].includes(idx);
@ -189,12 +306,10 @@ function renderBoard(state) {
x = col === 0 ? 0 : col === 10 ? bw - cornerSize : col * sideSize;
y = row === 0 ? 0 : row === 10 ? bw - cornerSize : row * sideSize;
} else if (row === 0 || row === 10) {
// Top or bottom row
w = sideSize; h = cornerSize;
x = cornerSize + (col - 1) * sideSize;
y = row === 0 ? 0 : bw - cornerSize;
} else {
// Left or right column
w = cornerSize; h = sideSize;
x = col === 0 ? 0 : bw - cornerSize;
y = cornerSize + (row - 1) * sideSize;
@ -205,12 +320,10 @@ function renderBoard(state) {
el.style.width = w + 'px';
el.style.height = h + 'px';
// Color bar for property groups
if (sq.group && GROUP_COLORS[sq.group]) {
const bar = document.createElement('div');
bar.className = 'color-bar';
bar.style.backgroundColor = GROUP_COLORS[sq.group];
// Bar on the inside edge
if (row === 10) { bar.style.top = '0'; bar.style.bottom = ''; }
else if (row === 0) { bar.style.top = ''; bar.style.bottom = '0'; bar.style.top = 'auto'; }
else if (col === 0) { bar.style.top = '0'; bar.style.left = ''; bar.style.right = '0'; bar.style.width = '6px'; bar.style.height = '100%'; }
@ -218,13 +331,11 @@ function renderBoard(state) {
el.appendChild(bar);
}
// Name
const nameEl = document.createElement('div');
nameEl.className = 'name';
nameEl.textContent = sq.name.replace(' Ave.', '').replace(' Place', ' Pl').replace(' Railroad', ' RR').replace('Community Chest', 'Comm. Chest').replace('Pennsylvania', 'Penn.');
el.appendChild(nameEl);
// Cost
if (sq.cost > 0) {
const costEl = document.createElement('div');
costEl.className = 'cost';
@ -232,7 +343,6 @@ function renderBoard(state) {
el.appendChild(costEl);
}
// Owner indicator
if (sq.owner >= 0 && sq.owner < players.length) {
const ownerEl = document.createElement('div');
ownerEl.className = 'owner-indicator';
@ -241,7 +351,6 @@ function renderBoard(state) {
el.appendChild(ownerEl);
}
// Houses/hotel
if (sq.houses > 0 && sq.houses < 5) {
const hd = document.createElement('div');
hd.className = 'houses-display';
@ -260,7 +369,6 @@ function renderBoard(state) {
el.appendChild(hd);
}
// Players on this square
const playersHere = players.filter(p => p.location === idx);
if (playersHere.length > 0) {
const ph = document.createElement('div');
@ -289,41 +397,38 @@ function renderPlayers(state) {
players.forEach((p, idx) => {
const panel = document.createElement('div');
panel.className = 'player-panel' + (idx === state.currentPlayer ? ' current-turn' : '');
panel.className = 'player-panel' + (p.number === state.currentPlayer ? ' current-turn' : '');
const color = PLAYER_COLORS[idx % PLAYER_COLORS.length];
let html = `<h3><span class="player-token" style="background:${color};width:20px;height:20px;font-size:11px">${p.name.charAt(0).toUpperCase()}</span> ${p.name}`;
if (idx === state.currentPlayer) html += ' 🎲';
if (p.number === state.currentPlayer) html += ' 🎲';
html += '</h3>';
html += `<div class="money ${p.money < 0 ? 'negative' : ''}">$${p.money.toLocaleString()}</div>`;
if (p.goJailFreeCards > 0) {
html += `<div style="font-size:0.8em;margin-top:4px">🃏 ${p.goJailFreeCards} GOJF card${p.goJailFreeCards > 1 ? 's' : ''}</div>`;
if (p.getOutOfJailFreeCards > 0) {
html += `<div style="font-size:0.8em;margin-top:4px">🃏 ${p.getOutOfJailFreeCards} GOJF card${p.getOutOfJailFreeCards > 1 ? 's' : ''}</div>`;
}
if (p.inJail) {
html += `<div style="font-size:0.8em;color:#e74c3c;margin-top:4px">🔒 In Jail</div>`;
html += `<div style="font-size:0.8em;color:#e74c3c;margin-top:4px">🔒 In Jail (turn ${p.jailTurns})</div>`;
}
// Location
const loc = squares[p.location];
const loc = p.location === 40 ? { name: 'JAIL' } : squares[p.location];
if (loc) {
html += `<div style="font-size:0.8em;margin-top:4px;color:#aaa">📍 ${loc.name}</div>`;
}
// Properties
if (p.properties && p.properties.length > 0) {
// Properties owned by this player
const owned = squares.filter(sq => sq.owner === p.number);
if (owned.length > 0) {
html += '<div class="props">';
p.properties.forEach(sid => {
const sq = squares[sid];
if (sq) {
owned.forEach(sq => {
const gc = sq.group ? GROUP_COLORS[sq.group] || '#666' : '#666';
html += `<div class="prop-item"><span class="prop-dot" style="background:${gc}"></span>${sq.name}`;
if (sq.houses > 0 && sq.houses < 5) html += ` 🏠×${sq.houses}`;
else if (sq.houses >= 5) html += ' 🏨';
if (sq.mortgaged) html += ' (M)';
html += '</div>';
}
});
html += '</div>';
}
@ -343,124 +448,317 @@ function renderLog(state) {
}).join('');
}
let lastUpdate = '';
async function fetchState() {
function renderGame(state) {
renderBoard(state);
renderPlayers(state);
renderLog(state);
}
// ===== CONFETTI =====
const confettiCanvas = document.getElementById('confetti-canvas');
const confettiCtx = confettiCanvas.getContext('2d');
let confettiPieces = [];
let confettiAnimFrame = null;
function resizeConfetti() {
confettiCanvas.width = window.innerWidth;
confettiCanvas.height = window.innerHeight;
}
window.addEventListener('resize', resizeConfetti);
resizeConfetti();
function startConfetti() {
if (confettiRunning) return;
confettiRunning = true;
confettiPieces = [];
const colors = ['#e74c3c','#3498db','#2ecc71','#f39c12','#9b59b6','#ffd700','#e91e63','#00bcd4'];
for (let i = 0; i < 200; i++) {
confettiPieces.push({
x: Math.random() * confettiCanvas.width,
y: Math.random() * confettiCanvas.height - confettiCanvas.height,
w: Math.random() * 10 + 5,
h: Math.random() * 6 + 3,
color: colors[Math.floor(Math.random() * colors.length)],
vx: (Math.random() - 0.5) * 3,
vy: Math.random() * 3 + 2,
spin: Math.random() * 0.2 - 0.1,
angle: Math.random() * Math.PI * 2,
opacity: 1,
});
}
animateConfetti();
}
function animateConfetti() {
confettiCtx.clearRect(0, 0, confettiCanvas.width, confettiCanvas.height);
let alive = 0;
confettiPieces.forEach(p => {
if (p.opacity <= 0) return;
alive++;
p.x += p.vx;
p.y += p.vy;
p.angle += p.spin;
p.vy += 0.05; // gravity
// Fade out when past bottom
if (p.y > confettiCanvas.height) {
p.opacity -= 0.02;
}
confettiCtx.save();
confettiCtx.translate(p.x, p.y);
confettiCtx.rotate(p.angle);
confettiCtx.globalAlpha = Math.max(0, p.opacity);
confettiCtx.fillStyle = p.color;
confettiCtx.fillRect(-p.w / 2, -p.h / 2, p.w, p.h);
confettiCtx.restore();
});
if (alive > 0) {
confettiAnimFrame = requestAnimationFrame(animateConfetti);
} else {
confettiRunning = false;
}
}
function stopConfetti() {
if (confettiAnimFrame) {
cancelAnimationFrame(confettiAnimFrame);
confettiAnimFrame = null;
}
confettiRunning = false;
confettiCtx.clearRect(0, 0, confettiCanvas.width, confettiCanvas.height);
}
// ===== GAME OVER DETECTION =====
function findWinner(state) {
const log = state.log || [];
for (let i = log.length - 1; i >= 0; i--) {
const m = log[i].text.match(/^(.+?) WINS!$/);
if (m) return m[1];
}
// If only one player left, they won
if (state.players && state.players.length === 1) {
return state.players[0].name;
}
return null;
}
function isGameOver(state) {
// Check log for WINS
const winner = findWinner(state);
if (winner) return true;
// Check if state explicitly says over (future-proofing)
if (state.phase === 'over') return true;
return false;
}
// ===== CONNECTION MONITORING =====
function showDisconnected() {
if (currentView === 'disconnected') return;
// Show the overlay on top of whatever was showing
document.getElementById('disconnect-overlay').style.display = 'flex';
document.body.classList.add('greyed-out');
startRetryCountdown();
}
function hideDisconnected() {
document.getElementById('disconnect-overlay').style.display = 'none';
document.body.classList.remove('greyed-out');
consecutiveFailures = 0;
if (retryTimer) { clearInterval(retryTimer); retryTimer = null; }
}
function startRetryCountdown() {
retryCountdown = 5;
document.getElementById('retry-countdown').textContent = retryCountdown;
if (retryTimer) clearInterval(retryTimer);
retryTimer = setInterval(() => {
retryCountdown--;
document.getElementById('retry-countdown').textContent = Math.max(0, retryCountdown);
}, 1000);
}
// ===== MAIN UPDATE LOOP =====
async function update() {
if (debugMode) return; // paused during debug
try {
const resp = await fetch('game-state.json?t=' + Date.now());
if (!resp.ok) throw new Error('HTTP ' + resp.status);
const state = await resp.json();
// Success — clear failure state
if (consecutiveFailures >= 3) hideDisconnected();
consecutiveFailures = 0;
// Determine what to show
if (!state.players || state.players.length === 0) {
// No players = no game
showView('zero');
lastUpdate = '';
gameOverShown = false;
return;
}
// Check for game over
if (isGameOver(state)) {
const updateStr = JSON.stringify(state);
if (updateStr !== lastUpdate) {
lastUpdate = updateStr;
renderBoard(state);
renderPlayers(state);
renderLog(state);
renderGame(state);
}
if (!gameOverShown) {
gameOverShown = true;
const winner = findWinner(state);
document.getElementById('winner-name').textContent = `🏆 ${winner || 'Someone'} WINS!`;
showView('gameover');
startConfetti();
}
document.getElementById('status').textContent =
`Last updated: ${state.lastUpdated ? new Date(state.lastUpdated).toLocaleString() : 'never'} · ${state.players?.length || 0} players`;
`Game Over · ${state.players.length} players · ${new Date(state.lastUpdated).toLocaleTimeString()}`;
return;
}
// Active game
gameOverShown = false;
const updateStr = JSON.stringify(state);
if (updateStr !== lastUpdate) {
lastUpdate = updateStr;
showView('playing');
renderGame(state);
}
document.getElementById('status').textContent =
`Live · ${state.players.length} players · Updated: ${new Date(state.lastUpdated).toLocaleTimeString()}`;
} catch (e) {
document.getElementById('status').textContent = 'Waiting for game-state.json... (' + e.message + ')';
consecutiveFailures++;
if (consecutiveFailures >= 3) {
showDisconnected();
}
}
}
// Generate demo state if no real game is running
function demoState() {
const squares = [
{id:0,name:"Go",type:"safe",owner:-1,cost:0,mortgaged:false,houses:0,monopoly:false,group:null,rent:[0]},
{id:1,name:"Mediterranean Ave.",type:"property",owner:0,cost:60,mortgaged:false,houses:2,monopoly:true,group:"purple",rent:[2,10,30,90,160,250]},
{id:2,name:"Community Chest",type:"cc",owner:-1,cost:0,mortgaged:false,houses:0,monopoly:false,group:null,rent:[0]},
{id:3,name:"Baltic Ave.",type:"property",owner:0,cost:60,mortgaged:false,houses:1,monopoly:true,group:"purple",rent:[4,20,60,180,320,450]},
{id:4,name:"Income Tax",type:"tax",owner:-1,cost:0,mortgaged:false,houses:0,monopoly:false,group:null,rent:[0]},
{id:5,name:"Reading Railroad",type:"railroad",owner:1,cost:200,mortgaged:false,houses:0,monopoly:false,group:"railroad",rent:[0]},
{id:6,name:"Oriental Ave.",type:"property",owner:-1,cost:100,mortgaged:false,houses:0,monopoly:false,group:"lightblue",rent:[6]},
{id:7,name:"Chance",type:"chance",owner:-1,cost:0,mortgaged:false,houses:0,monopoly:false,group:null,rent:[0]},
{id:8,name:"Vermont Ave.",type:"property",owner:-1,cost:100,mortgaged:false,houses:0,monopoly:false,group:"lightblue",rent:[6]},
{id:9,name:"Connecticut Ave.",type:"property",owner:2,cost:120,mortgaged:false,houses:0,monopoly:false,group:"lightblue",rent:[8]},
{id:10,name:"Just Visiting",type:"jail",owner:-1,cost:0,mortgaged:false,houses:0,monopoly:false,group:null,rent:[0]},
{id:11,name:"St. Charles Place",type:"property",owner:1,cost:140,mortgaged:false,houses:0,monopoly:false,group:"pink",rent:[10]},
{id:12,name:"Electric Company",type:"utility",owner:-1,cost:150,mortgaged:false,houses:0,monopoly:false,group:"utility",rent:[0]},
{id:13,name:"States Ave.",type:"property",owner:-1,cost:140,mortgaged:false,houses:0,monopoly:false,group:"pink",rent:[10]},
{id:14,name:"Virginia Ave.",type:"property",owner:-1,cost:160,mortgaged:false,houses:0,monopoly:false,group:"pink",rent:[12]},
{id:15,name:"Pennsylvania Railroad",type:"railroad",owner:-1,cost:200,mortgaged:false,houses:0,monopoly:false,group:"railroad",rent:[0]},
{id:16,name:"St. James Place",type:"property",owner:2,cost:180,mortgaged:false,houses:3,monopoly:true,group:"orange",rent:[14,70,200,550,750,950]},
{id:17,name:"Community Chest",type:"cc",owner:-1,cost:0,mortgaged:false,houses:0,monopoly:false,group:null,rent:[0]},
{id:18,name:"Tennessee Ave.",type:"property",owner:2,cost:180,mortgaged:false,houses:2,monopoly:true,group:"orange",rent:[14,70,200,550,750,950]},
{id:19,name:"New York Ave.",type:"property",owner:2,cost:200,mortgaged:false,houses:0,monopoly:true,group:"orange",rent:[16]},
{id:20,name:"Free Parking",type:"safe",owner:-1,cost:0,mortgaged:false,houses:0,monopoly:false,group:null,rent:[0]},
{id:21,name:"Kentucky Ave.",type:"property",owner:-1,cost:220,mortgaged:false,houses:0,monopoly:false,group:"red",rent:[18]},
{id:22,name:"Chance",type:"chance",owner:-1,cost:0,mortgaged:false,houses:0,monopoly:false,group:null,rent:[0]},
{id:23,name:"Indiana Ave.",type:"property",owner:-1,cost:220,mortgaged:false,houses:0,monopoly:false,group:"red",rent:[18]},
{id:24,name:"Illinois Ave.",type:"property",owner:0,cost:240,mortgaged:true,houses:0,monopoly:false,group:"red",rent:[20]},
{id:25,name:"B&O Railroad",type:"railroad",owner:-1,cost:200,mortgaged:false,houses:0,monopoly:false,group:"railroad",rent:[0]},
{id:26,name:"Atlantic Ave.",type:"property",owner:-1,cost:260,mortgaged:false,houses:0,monopoly:false,group:"yellow",rent:[22]},
{id:27,name:"Ventnor Ave.",type:"property",owner:-1,cost:260,mortgaged:false,houses:0,monopoly:false,group:"yellow",rent:[22]},
{id:28,name:"Water Works",type:"utility",owner:1,cost:150,mortgaged:false,houses:0,monopoly:false,group:"utility",rent:[0]},
{id:29,name:"Marvin Gardens",type:"property",owner:-1,cost:280,mortgaged:false,houses:0,monopoly:false,group:"yellow",rent:[24]},
{id:30,name:"Go to Jail",type:"gotojail",owner:-1,cost:0,mortgaged:false,houses:0,monopoly:false,group:null,rent:[0]},
{id:31,name:"Pacific Ave.",type:"property",owner:-1,cost:300,mortgaged:false,houses:0,monopoly:false,group:"green",rent:[26]},
{id:32,name:"North Carolina Ave.",type:"property",owner:-1,cost:300,mortgaged:false,houses:0,monopoly:false,group:"green",rent:[26]},
{id:33,name:"Community Chest",type:"cc",owner:-1,cost:0,mortgaged:false,houses:0,monopoly:false,group:null,rent:[0]},
{id:34,name:"Pennsylvania Ave.",type:"property",owner:-1,cost:320,mortgaged:false,houses:0,monopoly:false,group:"green",rent:[28]},
{id:35,name:"Short Line Railroad",type:"railroad",owner:-1,cost:200,mortgaged:false,houses:0,monopoly:false,group:"railroad",rent:[0]},
{id:36,name:"Chance",type:"chance",owner:-1,cost:0,mortgaged:false,houses:0,monopoly:false,group:null,rent:[0]},
{id:37,name:"Park Place",type:"property",owner:-1,cost:350,mortgaged:false,houses:0,monopoly:false,group:"darkblue",rent:[35]},
{id:38,name:"Luxury Tax",type:"tax",owner:-1,cost:0,mortgaged:false,houses:0,monopoly:false,group:null,rent:[0]},
{id:39,name:"Boardwalk",type:"property",owner:1,cost:400,mortgaged:false,houses:5,monopoly:false,group:"darkblue",rent:[50,200,600,1400,1700,2000]},
// ===== DEMO / TEST STATES =====
function demoSquares() {
const names = [
"=== GO ===","Mediterranean ave. (P)","Community Chest i","Baltic ave. (P)","Income Tax",
"Reading RR","Oriental ave. (L)","Chance i","Vermont ave. (L)","Connecticut ave. (L)",
"Just Visiting","St. Charles pl. (V)","Electric Co.","States ave. (V)","Virginia ave. (V)",
"Pennsylvania RR","St. James pl. (O)","Community Chest ii","Tennessee ave. (O)","New York ave. (O)",
"Free Parking","Kentucky ave. (R)","Chance ii","Indiana ave. (R)","Illinois ave. (R)",
"B&O RR","Atlantic ave. (Y)","Ventnor ave. (Y)","Water Works","Marvin Gardens (Y)",
"GO TO JAIL","Pacific ave. (G)","N. Carolina ave. (G)","Community Chest iii","Pennsylvania ave. (G)",
"Short Line RR","Chance iii","Park place (D)","Luxury Tax","Boardwalk (D)"
];
const groups = [
null,"purple",null,"purple",null,"railroad","lightblue",null,"lightblue","lightblue",
null,"violet","utility","violet","violet","railroad","orange",null,"orange","orange",
null,"red",null,"red","red","railroad","yellow","yellow","utility","yellow",
null,"green","green",null,"green","railroad",null,"darkblue",null,"darkblue"
];
const costs = [
0,60,0,60,0,200,100,0,100,120,0,140,150,140,160,200,180,0,180,200,
0,220,0,220,240,200,260,260,150,280,0,300,300,0,320,200,0,350,0,400
];
const types = [
"safe","property","cc","property","tax","railroad","property","chance","property","property",
"safe","property","utility","property","property","railroad","property","cc","property","property",
"safe","property","chance","property","property","railroad","property","property","utility","property",
"gotojail","property","property","cc","property","railroad","chance","property","tax","property"
];
return names.map((name, i) => ({
id: i, name, type: types[i], group: groups[i], cost: costs[i],
owner: null, mortgaged: false, houses: 0
}));
}
function demoPlayingState() {
const sq = demoSquares();
sq[1].owner = 1; sq[3].owner = 1; sq[1].houses = 2; sq[3].houses = 1;
sq[5].owner = 2; sq[39].owner = 2; sq[39].houses = 5;
sq[11].owner = 2; sq[28].owner = 2;
sq[9].owner = 3; sq[16].owner = 3; sq[18].owner = 3; sq[19].owner = 3;
sq[16].houses = 3; sq[18].houses = 2;
sq[24].owner = 1; sq[24].mortgaged = true;
return {
lastUpdated: new Date().toISOString(),
currentPlayer: 1,
currentPlayer: 2,
players: [
{name:"Alice",number:1,money:820,location:24,inJail:false,jailTurns:0,goJailFreeCards:1,properties:[1,3,24],numRailroads:0,numUtilities:0},
{name:"Bob",number:2,money:1350,location:39,inJail:false,jailTurns:0,goJailFreeCards:0,properties:[5,11,28,39],numRailroads:1,numUtilities:1},
{name:"Charlie",number:3,money:640,location:10,inJail:true,jailTurns:2,goJailFreeCards:0,properties:[9,16,18,19],numRailroads:0,numUtilities:0},
{name:"Alice",number:1,money:820,location:24,inJail:false,jailTurns:0,doublesCount:0,getOutOfJailFreeCards:1},
{name:"Bob",number:2,money:1350,location:39,inJail:false,jailTurns:0,doublesCount:0,getOutOfJailFreeCards:0},
{name:"Charlie",number:3,money:640,location:40,inJail:true,jailTurns:2,doublesCount:0,getOutOfJailFreeCards:0},
],
squares: squares,
squares: sq,
log: [
{timestamp:new Date(Date.now()-60000).toISOString(),text:"roll is 4, 3",player:"Alice"},
{timestamp:new Date(Date.now()-55000).toISOString(),text:"Landed on Illinois Ave.",player:"Alice"},
{timestamp:new Date(Date.now()-50000).toISOString(),text:"roll is 6, 6",player:"Bob"},
{timestamp:new Date(Date.now()-45000).toISOString(),text:"Passed Go, collected $200",player:"Bob"},
{timestamp:new Date(Date.now()-40000).toISOString(),text:"Landed on Boardwalk",player:"Bob"},
{timestamp:new Date(Date.now()-35000).toISOString(),text:"Bob rolled doubles",player:"Bob"},
{timestamp:new Date(Date.now()-30000).toISOString(),text:"roll is 2, 5",player:"Bob"},
{timestamp:new Date(Date.now()-25000).toISOString(),text:"Paid $550 rent (3 houses)",player:"Bob"},
{timestamp:new Date(Date.now()-20000).toISOString(),text:"Charlie's turn",player:"Charlie"},
{timestamp:new Date(Date.now()-50000).toISOString(),text:"Landed on Illinois ave. (R)",player:"Alice"},
{timestamp:new Date(Date.now()-40000).toISOString(),text:"roll is 6, 6",player:"Bob"},
{timestamp:new Date(Date.now()-30000).toISOString(),text:"Passed GO — collected $200",player:"Bob"},
{timestamp:new Date(Date.now()-20000).toISOString(),text:"Landed on Boardwalk (D)",player:"Bob"},
{timestamp:new Date(Date.now()-10000).toISOString(),text:"Still in jail (turn 2)",player:"Charlie"},
]
};
}
// Try to fetch real state, fall back to demo
async function update() {
try {
const resp = await fetch('game-state.json?t=' + Date.now());
if (!resp.ok) throw new Error('no file');
const state = await resp.json();
if (!state.players || state.players.length === 0) throw new Error('empty');
const updateStr = JSON.stringify(state);
if (updateStr !== lastUpdate) {
lastUpdate = updateStr;
renderBoard(state);
renderPlayers(state);
renderLog(state);
document.getElementById('status').textContent =
`Live · ${state.players.length} players · Updated: ${new Date(state.lastUpdated).toLocaleTimeString()}`;
}
} catch (e) {
// Use demo state
const demo = demoState();
const demoStr = 'demo';
if (lastUpdate !== demoStr) {
lastUpdate = demoStr;
renderBoard(demo);
renderPlayers(demo);
renderLog(demo);
document.getElementById('status').textContent = 'Demo mode (no live game detected)';
function demoGameOverState() {
const st = demoPlayingState();
st.players = [
{name:"Bob",number:1,money:4250,location:39,inJail:false,jailTurns:0,doublesCount:0,getOutOfJailFreeCards:2},
];
st.currentPlayer = 1;
st.log.push({timestamp:new Date().toISOString(), text:"Bob WINS!", player:"Bob"});
return st;
}
// ===== DEBUG MODE =====
function debugState(mode) {
stopConfetti();
gameOverShown = false;
if (mode === 'reset') {
debugMode = null;
lastUpdate = '';
hideDisconnected();
update();
return;
}
debugMode = mode;
if (mode === 'zero') {
showView('zero');
} else if (mode === 'playing') {
const st = demoPlayingState();
showView('playing');
renderGame(st);
document.getElementById('status').textContent = 'Debug: Playing (demo data)';
} else if (mode === 'gameover') {
const st = demoGameOverState();
showView('gameover');
renderGame(st);
const winner = findWinner(st);
document.getElementById('winner-name').textContent = `🏆 ${winner} WINS!`;
startConfetti();
document.getElementById('status').textContent = 'Debug: Game Over';
} else if (mode === 'disconnected') {
// Show game underneath the overlay
const st = demoPlayingState();
showView('playing');
renderGame(st);
showDisconnected();
}
}
// Ctrl+Shift+D to toggle debug panel
document.addEventListener('keydown', e => {
if (e.ctrlKey && e.shiftKey && e.key === 'D') {
e.preventDefault();
document.getElementById('debug-panel').classList.toggle('visible');
}
});
// ===== INIT =====
update();
setInterval(update, 2000);
</script>