Add static Monopoly board viewer with demo mode
This commit is contained in:
parent
d3e7ed9375
commit
c85ace5d7f
2 changed files with 494 additions and 0 deletions
26
site/README.md
Normal file
26
site/README.md
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# monop-board Static Site
|
||||
|
||||
Visual Monopoly board viewer that reads `game-state.json` and displays the board.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# Serve from the site directory (game-state.json should be in this dir or parent)
|
||||
cd site/
|
||||
python3 -m http.server 8080
|
||||
```
|
||||
|
||||
Then open http://localhost:8080 in your browser.
|
||||
|
||||
## Features
|
||||
|
||||
- Classic Monopoly board layout
|
||||
- Player tokens with colors and initials
|
||||
- Property ownership indicators
|
||||
- Houses (green) and hotels (red)
|
||||
- Color-coded property groups
|
||||
- Player info panels with money, properties, cards
|
||||
- Game log with recent events
|
||||
- Auto-refreshes every 2 seconds
|
||||
- Demo mode when no live game is running
|
||||
- Mobile-responsive dark theme
|
||||
468
site/index.html
Normal file
468
site/index.html
Normal file
|
|
@ -0,0 +1,468 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Monopoly Board Viewer</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { background: #1a1a2e; color: #e0e0e0; font-family: 'Segoe UI', system-ui, sans-serif; min-height: 100vh; }
|
||||
|
||||
.container { display: flex; flex-direction: column; align-items: center; padding: 20px; gap: 20px; }
|
||||
h1 { color: #fff; font-size: 1.5em; text-align: center; }
|
||||
.status { font-size: 0.8em; color: #888; }
|
||||
|
||||
/* Board layout */
|
||||
.board-wrapper { position: relative; width: min(90vw, 700px); height: min(90vw, 700px); }
|
||||
.board { position: absolute; inset: 0; }
|
||||
|
||||
/* Squares */
|
||||
.square {
|
||||
position: absolute;
|
||||
border: 1px solid #333;
|
||||
background: #16213e;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.55em;
|
||||
overflow: hidden;
|
||||
cursor: default;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.square:hover { background: #1a2a50; z-index: 10; }
|
||||
.square .name { text-align: center; line-height: 1.1; padding: 1px 2px; font-weight: 500; }
|
||||
.square .cost { font-size: 0.85em; color: #aaa; }
|
||||
.square .color-bar {
|
||||
position: absolute; top: 0; left: 0; right: 0; height: 6px;
|
||||
}
|
||||
.square .players-here {
|
||||
display: flex; flex-wrap: wrap; gap: 1px; margin-top: 2px;
|
||||
}
|
||||
.player-token {
|
||||
width: 12px; height: 12px; border-radius: 50%; display: flex;
|
||||
align-items: center; justify-content: center; font-size: 7px;
|
||||
font-weight: bold; color: #fff; border: 1px solid rgba(255,255,255,0.3);
|
||||
}
|
||||
.square .houses-display { display: flex; gap: 1px; margin-top: 1px; }
|
||||
.house { width: 6px; height: 6px; background: #2ecc71; }
|
||||
.hotel { width: 8px; height: 8px; background: #e74c3c; }
|
||||
.mortgaged { opacity: 0.4; }
|
||||
.owner-indicator { font-size: 0.7em; color: #ffd700; }
|
||||
|
||||
/* Corner squares are bigger */
|
||||
.corner { width: calc(100% / 8.5); height: calc(100% / 8.5); font-size: 0.65em; }
|
||||
|
||||
/* Group colors */
|
||||
.group-purple { background-color: #6a0dad; }
|
||||
.group-lightblue { background-color: #87ceeb; }
|
||||
.group-pink { background-color: #d63384; }
|
||||
.group-orange { background-color: #e67e22; }
|
||||
.group-red { background-color: #e74c3c; }
|
||||
.group-yellow { background-color: #f1c40f; }
|
||||
.group-green { background-color: #27ae60; }
|
||||
.group-darkblue { background-color: #2c3e8a; }
|
||||
.group-railroad { background-color: #555; }
|
||||
.group-utility { background-color: #777; }
|
||||
|
||||
/* Player info panel */
|
||||
.info-panels { display: flex; flex-wrap: wrap; gap: 15px; justify-content: center; max-width: 900px; width: 100%; }
|
||||
.player-panel {
|
||||
background: #16213e; border: 2px solid #333; border-radius: 8px;
|
||||
padding: 12px; min-width: 200px; flex: 1; max-width: 280px;
|
||||
}
|
||||
.player-panel h3 { font-size: 1em; margin-bottom: 8px; display: flex; align-items: center; gap: 6px; }
|
||||
.player-panel .money { font-size: 1.2em; color: #2ecc71; font-weight: bold; }
|
||||
.player-panel .money.negative { color: #e74c3c; }
|
||||
.player-panel .props { font-size: 0.8em; margin-top: 6px; }
|
||||
.player-panel .prop-item { padding: 2px 0; display: flex; align-items: center; gap: 4px; }
|
||||
.prop-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
|
||||
.current-turn { border-color: #ffd700 !important; box-shadow: 0 0 10px rgba(255, 215, 0, 0.3); }
|
||||
|
||||
/* Game log */
|
||||
.game-log {
|
||||
background: #16213e; border: 1px solid #333; border-radius: 8px;
|
||||
padding: 12px; max-width: 900px; width: 100%; max-height: 200px;
|
||||
overflow-y: auto; font-size: 0.85em;
|
||||
}
|
||||
.game-log h3 { margin-bottom: 8px; }
|
||||
.log-entry { padding: 2px 0; border-bottom: 1px solid #1a1a2e; color: #ccc; }
|
||||
.log-entry .log-player { color: #ffd700; font-weight: 500; }
|
||||
.log-entry .log-time { color: #666; font-size: 0.8em; }
|
||||
|
||||
/* Center area of board */
|
||||
.board-center {
|
||||
position: absolute;
|
||||
top: calc(100% / 8.5);
|
||||
left: calc(100% / 8.5);
|
||||
right: calc(100% / 8.5);
|
||||
bottom: calc(100% / 8.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
background: #0f3460;
|
||||
border: 2px solid #333;
|
||||
font-size: 1.8em;
|
||||
font-weight: bold;
|
||||
color: #e74c3c;
|
||||
text-align: center;
|
||||
letter-spacing: 3px;
|
||||
}
|
||||
.board-center .subtitle { font-size: 0.35em; color: #888; letter-spacing: 0; margin-top: 8px; font-weight: normal; }
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.board-wrapper { width: 95vw; height: 95vw; }
|
||||
.square { font-size: 0.4em; }
|
||||
.player-token { width: 8px; height: 8px; font-size: 5px; }
|
||||
.info-panels { flex-direction: column; }
|
||||
.player-panel { max-width: 100%; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div>
|
||||
<h1>🎲 Monopoly Board</h1>
|
||||
<div class="status" id="status">Loading game state...</div>
|
||||
</div>
|
||||
<div class="board-wrapper">
|
||||
<div class="board" id="board"></div>
|
||||
<div class="board-center">
|
||||
MONOPOLY
|
||||
<div class="subtitle">IRC Edition</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-panels" id="players"></div>
|
||||
<div class="game-log">
|
||||
<h3>📜 Game Log</h3>
|
||||
<div id="log"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const PLAYER_COLORS = ['#e74c3c','#3498db','#2ecc71','#f39c12','#9b59b6','#1abc9c','#e91e63','#00bcd4','#ff5722'];
|
||||
|
||||
const GROUP_COLORS = {
|
||||
purple: '#6a0dad', lightblue: '#87ceeb', pink: '#d63384',
|
||||
orange: '#e67e22', red: '#e74c3c', yellow: '#f1c40f',
|
||||
green: '#27ae60', darkblue: '#2c3e8a', railroad: '#555', utility: '#777'
|
||||
};
|
||||
|
||||
// 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];
|
||||
|
||||
function renderBoard(state) {
|
||||
const board = document.getElementById('board');
|
||||
board.innerHTML = '';
|
||||
const squares = state.squares || [];
|
||||
const players = state.players || [];
|
||||
|
||||
const bw = board.offsetWidth || 700;
|
||||
const cornerSize = bw / 8.5;
|
||||
const sideSize = (bw - 2 * cornerSize) / 9;
|
||||
|
||||
squares.forEach((sq, idx) => {
|
||||
const [row, col] = POSITIONS[idx];
|
||||
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);
|
||||
|
||||
if (isCorner) {
|
||||
w = cornerSize; h = cornerSize;
|
||||
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;
|
||||
}
|
||||
|
||||
el.style.left = x + 'px';
|
||||
el.style.top = y + 'px';
|
||||
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%'; }
|
||||
else if (col === 10) { bar.style.top = '0'; bar.style.left = '0'; bar.style.right = ''; bar.style.width = '6px'; bar.style.height = '100%'; }
|
||||
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';
|
||||
costEl.textContent = '$' + sq.cost;
|
||||
el.appendChild(costEl);
|
||||
}
|
||||
|
||||
// Owner indicator
|
||||
if (sq.owner >= 0 && sq.owner < players.length) {
|
||||
const ownerEl = document.createElement('div');
|
||||
ownerEl.className = 'owner-indicator';
|
||||
ownerEl.textContent = '⬤';
|
||||
ownerEl.style.color = PLAYER_COLORS[sq.owner % PLAYER_COLORS.length];
|
||||
el.appendChild(ownerEl);
|
||||
}
|
||||
|
||||
// Houses/hotel
|
||||
if (sq.houses > 0 && sq.houses < 5) {
|
||||
const hd = document.createElement('div');
|
||||
hd.className = 'houses-display';
|
||||
for (let h = 0; h < sq.houses; h++) {
|
||||
const house = document.createElement('div');
|
||||
house.className = 'house';
|
||||
hd.appendChild(house);
|
||||
}
|
||||
el.appendChild(hd);
|
||||
} else if (sq.houses >= 5) {
|
||||
const hd = document.createElement('div');
|
||||
hd.className = 'houses-display';
|
||||
const hotel = document.createElement('div');
|
||||
hotel.className = 'hotel';
|
||||
hd.appendChild(hotel);
|
||||
el.appendChild(hd);
|
||||
}
|
||||
|
||||
// Players on this square
|
||||
const playersHere = players.filter(p => p.location === idx);
|
||||
if (playersHere.length > 0) {
|
||||
const ph = document.createElement('div');
|
||||
ph.className = 'players-here';
|
||||
playersHere.forEach(p => {
|
||||
const pidx = players.indexOf(p);
|
||||
const tok = document.createElement('div');
|
||||
tok.className = 'player-token';
|
||||
tok.style.backgroundColor = PLAYER_COLORS[pidx % PLAYER_COLORS.length];
|
||||
tok.textContent = p.name.charAt(0).toUpperCase();
|
||||
tok.title = p.name;
|
||||
ph.appendChild(tok);
|
||||
});
|
||||
el.appendChild(ph);
|
||||
}
|
||||
|
||||
board.appendChild(el);
|
||||
});
|
||||
}
|
||||
|
||||
function renderPlayers(state) {
|
||||
const container = document.getElementById('players');
|
||||
container.innerHTML = '';
|
||||
const players = state.players || [];
|
||||
const squares = state.squares || [];
|
||||
|
||||
players.forEach((p, idx) => {
|
||||
const panel = document.createElement('div');
|
||||
panel.className = 'player-panel' + (idx === 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 += ' 🎲';
|
||||
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.inJail) {
|
||||
html += `<div style="font-size:0.8em;color:#e74c3c;margin-top:4px">🔒 In Jail</div>`;
|
||||
}
|
||||
|
||||
// Location
|
||||
const loc = 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) {
|
||||
html += '<div class="props">';
|
||||
p.properties.forEach(sid => {
|
||||
const sq = squares[sid];
|
||||
if (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>';
|
||||
}
|
||||
|
||||
panel.innerHTML = html;
|
||||
container.appendChild(panel);
|
||||
});
|
||||
}
|
||||
|
||||
function renderLog(state) {
|
||||
const container = document.getElementById('log');
|
||||
const entries = (state.log || []).slice(-30).reverse();
|
||||
container.innerHTML = entries.map(e => {
|
||||
const time = e.timestamp ? new Date(e.timestamp).toLocaleTimeString() : '';
|
||||
const player = e.player ? `<span class="log-player">${e.player}</span>: ` : '';
|
||||
return `<div class="log-entry"><span class="log-time">${time}</span> ${player}${e.text}</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
let lastUpdate = '';
|
||||
async function fetchState() {
|
||||
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();
|
||||
const updateStr = JSON.stringify(state);
|
||||
if (updateStr !== lastUpdate) {
|
||||
lastUpdate = updateStr;
|
||||
renderBoard(state);
|
||||
renderPlayers(state);
|
||||
renderLog(state);
|
||||
}
|
||||
document.getElementById('status').textContent =
|
||||
`Last updated: ${state.lastUpdated ? new Date(state.lastUpdated).toLocaleString() : 'never'} · ${state.players?.length || 0} players`;
|
||||
} catch (e) {
|
||||
document.getElementById('status').textContent = 'Waiting for game-state.json... (' + e.message + ')';
|
||||
}
|
||||
}
|
||||
|
||||
// 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]},
|
||||
];
|
||||
return {
|
||||
lastUpdated: new Date().toISOString(),
|
||||
currentPlayer: 1,
|
||||
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},
|
||||
],
|
||||
squares: squares,
|
||||
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()-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)';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
update();
|
||||
setInterval(update, 2000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in a new issue