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:
parent
19fc56d982
commit
34a4f47402
2 changed files with 547 additions and 250 deletions
|
|
@ -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"
|
||||
}
|
||||
542
site/index.html
542
site/index.html
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue