From 72d996cb4b72c5a790a83962085a2fae044bd643 Mon Sep 17 00:00:00 2001 From: Jarvis Date: Sat, 21 Feb 2026 11:24:49 +0000 Subject: [PATCH] Show players during setup: empty slots and registration progress MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Parser changes: - Track num_players_expected from user input after 'How many players?' - Create placeholder players ('Player N') on 'say me please' prompts - Replace placeholder names when real names appear in roll lines - Emit state during setup phase (was returning None) - Include phase and numPlayersExpected in state JSON UI changes: - Empty slots shown as dashed/dimmed panels with '?' token - Placeholder players shown as 'Registering...' - Registered players shown with checkmark and ,500 - Status bar shows 'Setting up ยท 2/3 players registered' --- __pycache__/monop_parser.cpython-310.pyc | Bin 25095 -> 26304 bytes monop_parser.py | 52 ++++- plugins/monop/monop_parser.py | 52 ++++- site/game-state.json | 275 ++++++++++++----------- site/index.html | 110 ++++++--- 5 files changed, 310 insertions(+), 179 deletions(-) diff --git a/__pycache__/monop_parser.cpython-310.pyc b/__pycache__/monop_parser.cpython-310.pyc index 9e0778afc62508eb0b64bd47464ca47c4b07e088..d5bad553634084b1a230e772fdfc7f0df2d1980d 100644 GIT binary patch delta 7811 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 ZWB_3p0nLlBDh~fDaQ*rDWLy3t{|AYA19kua delta 6486 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 R-i-X%zj?v8g{!aU{{cnA%aZ^A diff --git a/monop_parser.py b/monop_parser.py index 5bb6701..afa4255 100644 --- a/monop_parser.py +++ b/monop_parser.py @@ -107,6 +107,7 @@ class GameState: self.card_lines = [] self.setup_names = [] self.setup_rolls = [] + self.num_players_expected = 0 # from "How many players?" self.game_active = False # Track spec flag (chance card: nearest RR/utility) self.spec = False @@ -238,6 +239,23 @@ class MonopParser: def _new_game(self): self.game = GameState() self.games.append(self.game) + self._awaiting_player_count = True + + def _handle_setup_input(self, sender, msg, timestamp): + """Handle user input during setup phase.""" + g = self.game + if not g: + return + # Capture player count (first numeric input after "How many players?") + if hasattr(self, '_awaiting_player_count') and self._awaiting_player_count: + m = re.match(r'^(\d+)$', msg.strip()) + if m: + count = int(m.group(1)) + if 1 <= count <= 9: + g.num_players_expected = count + self._awaiting_player_count = False + g.add_log(f"Game for {count} players", timestamp=timestamp) + return def parse_line(self, line): """Parse a single IRC log line. Returns any events generated.""" @@ -254,12 +272,14 @@ class MonopParser: message_raw = message_full.rstrip() message = message_full.strip() - # Track user input for resign target detection + # Track user input if sender != "monop": - # Store last user input (strip bot prefix '.') user_msg = message.lstrip('.') if user_msg: self._last_user_input = user_msg + # During setup, capture player count and registrations + if self.game and self.game.phase == "setup": + self._handle_setup_input(sender, user_msg, timestamp) return self._process_bot_line(message, timestamp, message_raw) @@ -414,10 +434,16 @@ class MonopParser: m = self.PLAYER_ROLLS_RE.match(msg) if m: name, num, roll_val = m.group(1), int(m.group(2)), int(m.group(3)) - # Ensure player exists - if not g.get_player(name=name): + existing = g.get_player(number=num) + if existing: + # Update placeholder name with real name + if existing.name.startswith("Player "): + existing.name = name + g.add_log(f"{name} registered!", player=name, timestamp=timestamp) + elif not g.get_player(name=name): p = Player(name, num) g.players.append(p) + g.add_log(f"{name} registered!", player=name, timestamp=timestamp) return m = self.GOES_FIRST_RE.match(msg) @@ -437,9 +463,14 @@ class MonopParser: g.add_log(f"Game started! {name} goes first", timestamp=timestamp) return - # "Player N, say 'me' please" - just note it + # "Player N, say 'me' please" - create placeholder m = self.SAY_ME_RE.match(msg) if m: + num = int(m.group(1)) + if not g.get_player(number=num): + p = Player(f"Player {num}", num) + g.players.append(p) + g.add_log(f"Waiting for Player {num} to register...", timestamp=timestamp) return return @@ -1209,6 +1240,17 @@ class MonopParser: if not self.game: return None g = self.game + + # During setup, emit partial state so the UI can show registering players + if g.phase == "setup": + return { + "players": [p.to_dict() for p in g.players], + "currentPlayer": None, + "squares": [{"id": sq["id"], "name": sq["name"], "type": sq["type"]} for sq in g.squares], + "log": g.log[-30:], + "phase": "setup", + "numPlayersExpected": g.num_players_expected, + } squares = [] for sq in g.squares: sq_out = {"id": sq["id"], "name": sq["name"], "type": sq["type"]} diff --git a/plugins/monop/monop_parser.py b/plugins/monop/monop_parser.py index 5bb6701..afa4255 100644 --- a/plugins/monop/monop_parser.py +++ b/plugins/monop/monop_parser.py @@ -107,6 +107,7 @@ class GameState: self.card_lines = [] self.setup_names = [] self.setup_rolls = [] + self.num_players_expected = 0 # from "How many players?" self.game_active = False # Track spec flag (chance card: nearest RR/utility) self.spec = False @@ -238,6 +239,23 @@ class MonopParser: def _new_game(self): self.game = GameState() self.games.append(self.game) + self._awaiting_player_count = True + + def _handle_setup_input(self, sender, msg, timestamp): + """Handle user input during setup phase.""" + g = self.game + if not g: + return + # Capture player count (first numeric input after "How many players?") + if hasattr(self, '_awaiting_player_count') and self._awaiting_player_count: + m = re.match(r'^(\d+)$', msg.strip()) + if m: + count = int(m.group(1)) + if 1 <= count <= 9: + g.num_players_expected = count + self._awaiting_player_count = False + g.add_log(f"Game for {count} players", timestamp=timestamp) + return def parse_line(self, line): """Parse a single IRC log line. Returns any events generated.""" @@ -254,12 +272,14 @@ class MonopParser: message_raw = message_full.rstrip() message = message_full.strip() - # Track user input for resign target detection + # Track user input if sender != "monop": - # Store last user input (strip bot prefix '.') user_msg = message.lstrip('.') if user_msg: self._last_user_input = user_msg + # During setup, capture player count and registrations + if self.game and self.game.phase == "setup": + self._handle_setup_input(sender, user_msg, timestamp) return self._process_bot_line(message, timestamp, message_raw) @@ -414,10 +434,16 @@ class MonopParser: m = self.PLAYER_ROLLS_RE.match(msg) if m: name, num, roll_val = m.group(1), int(m.group(2)), int(m.group(3)) - # Ensure player exists - if not g.get_player(name=name): + existing = g.get_player(number=num) + if existing: + # Update placeholder name with real name + if existing.name.startswith("Player "): + existing.name = name + g.add_log(f"{name} registered!", player=name, timestamp=timestamp) + elif not g.get_player(name=name): p = Player(name, num) g.players.append(p) + g.add_log(f"{name} registered!", player=name, timestamp=timestamp) return m = self.GOES_FIRST_RE.match(msg) @@ -437,9 +463,14 @@ class MonopParser: g.add_log(f"Game started! {name} goes first", timestamp=timestamp) return - # "Player N, say 'me' please" - just note it + # "Player N, say 'me' please" - create placeholder m = self.SAY_ME_RE.match(msg) if m: + num = int(m.group(1)) + if not g.get_player(number=num): + p = Player(f"Player {num}", num) + g.players.append(p) + g.add_log(f"Waiting for Player {num} to register...", timestamp=timestamp) return return @@ -1209,6 +1240,17 @@ class MonopParser: if not self.game: return None g = self.game + + # During setup, emit partial state so the UI can show registering players + if g.phase == "setup": + return { + "players": [p.to_dict() for p in g.players], + "currentPlayer": None, + "squares": [{"id": sq["id"], "name": sq["name"], "type": sq["type"]} for sq in g.squares], + "log": g.log[-30:], + "phase": "setup", + "numPlayersExpected": g.num_players_expected, + } squares = [] for sq in g.squares: sq_out = {"id": sq["id"], "name": sq["name"], "type": sq["type"]} diff --git a/site/game-state.json b/site/game-state.json index 04e3428..08e36d1 100644 --- a/site/game-state.json +++ b/site/game-state.json @@ -1,20 +1,10 @@ { "players": [ - { - "name": "alice", - "number": 1, - "money": 919, - "location": 25, - "inJail": false, - "jailTurns": 0, - "doublesCount": 0, - "getOutOfJailFreeCards": 0 - }, { "name": "bob", "number": 2, - "money": 1292, - "location": 37, + "money": 966, + "location": 2, "inJail": false, "jailTurns": 0, "doublesCount": 0, @@ -23,9 +13,19 @@ { "name": "charlie", "number": 3, - "money": 320, - "location": 40, - "inJail": true, + "money": 349, + "location": 0, + "inJail": false, + "jailTurns": 0, + "doublesCount": 0, + "getOutOfJailFreeCards": 0 + }, + { + "name": "alice", + "number": 1, + "money": 276, + "location": 14, + "inJail": false, "jailTurns": 0, "doublesCount": 0, "getOutOfJailFreeCards": 1 @@ -42,7 +42,7 @@ "id": 1, "name": "Mediterranean ave. (P)", "type": "property", - "owner": 2, + "owner": 3, "mortgaged": false, "group": "purple", "cost": 60, @@ -57,7 +57,7 @@ "id": 3, "name": "Baltic ave. (P)", "type": "property", - "owner": 3, + "owner": 1, "mortgaged": false, "group": "purple", "cost": 60, @@ -72,7 +72,7 @@ "id": 5, "name": "Reading RR", "type": "railroad", - "owner": 2, + "owner": 1, "mortgaged": false, "group": "railroad", "cost": 200 @@ -96,7 +96,7 @@ "id": 8, "name": "Vermont ave. (L)", "type": "property", - "owner": 3, + "owner": null, "mortgaged": false, "group": "lightblue", "cost": 100, @@ -106,7 +106,7 @@ "id": 9, "name": "Connecticut ave. (L)", "type": "property", - "owner": 3, + "owner": null, "mortgaged": false, "group": "lightblue", "cost": 120, @@ -121,7 +121,7 @@ "id": 11, "name": "St. Charles pl. (V)", "type": "property", - "owner": 3, + "owner": 2, "mortgaged": false, "group": "violet", "cost": 140, @@ -131,7 +131,7 @@ "id": 12, "name": "Electric Co.", "type": "utility", - "owner": 2, + "owner": 3, "mortgaged": false, "group": "utility", "cost": 150 @@ -140,7 +140,7 @@ "id": 13, "name": "States ave. (V)", "type": "property", - "owner": 3, + "owner": 1, "mortgaged": false, "group": "violet", "cost": 140, @@ -150,7 +150,7 @@ "id": 14, "name": "Virginia ave. (V)", "type": "property", - "owner": 2, + "owner": 3, "mortgaged": false, "group": "violet", "cost": 160, @@ -160,7 +160,7 @@ "id": 15, "name": "Pennsylvania RR", "type": "railroad", - "owner": 1, + "owner": 3, "mortgaged": false, "group": "railroad", "cost": 200 @@ -169,7 +169,7 @@ "id": 16, "name": "St. James pl. (O)", "type": "property", - "owner": 2, + "owner": null, "mortgaged": false, "group": "orange", "cost": 180, @@ -209,7 +209,7 @@ "id": 21, "name": "Kentucky ave. (R)", "type": "property", - "owner": 1, + "owner": null, "mortgaged": false, "group": "red", "cost": 220, @@ -224,7 +224,7 @@ "id": 23, "name": "Indiana ave. (R)", "type": "property", - "owner": 1, + "owner": 3, "mortgaged": false, "group": "red", "cost": 220, @@ -234,7 +234,7 @@ "id": 24, "name": "Illinois ave. (R)", "type": "property", - "owner": 1, + "owner": null, "mortgaged": false, "group": "red", "cost": 240, @@ -263,7 +263,7 @@ "id": 27, "name": "Ventnor ave. (Y)", "type": "property", - "owner": 1, + "owner": 3, "mortgaged": false, "group": "yellow", "cost": 260, @@ -273,7 +273,7 @@ "id": 28, "name": "Water Works", "type": "utility", - "owner": 2, + "owner": null, "mortgaged": false, "group": "utility", "cost": 150 @@ -282,7 +282,7 @@ "id": 29, "name": "Marvin Gardens (Y)", "type": "property", - "owner": 3, + "owner": null, "mortgaged": false, "group": "yellow", "cost": 280, @@ -307,7 +307,7 @@ "id": 32, "name": "N. Carolina ave. (G)", "type": "property", - "owner": 2, + "owner": 1, "mortgaged": false, "group": "green", "cost": 300, @@ -322,7 +322,7 @@ "id": 34, "name": "Pennsylvania ave. (G)", "type": "property", - "owner": 3, + "owner": 1, "mortgaged": false, "group": "green", "cost": 320, @@ -346,7 +346,7 @@ "id": 37, "name": "Park place (D)", "type": "property", - "owner": 1, + "owner": 2, "mortgaged": false, "group": "darkblue", "cost": 350, @@ -370,149 +370,152 @@ ], "log": [ { - "text": "charlie's turn \u2014 $320 on Electric Co.", - "player": "charlie", - "timestamp": "2026-02-21 11:03:37" + "text": "bob's turn \u2014 $1138 on New York ave. (O)", + "player": "bob", + "timestamp": "2026-02-21 11:24:23" }, { - "text": "roll is 4, 6", - "player": "charlie", - "timestamp": "2026-02-21 11:03:38" + "text": "roll is 6, 2", + "player": "bob", + "timestamp": "2026-02-21 11:24:24" }, { - "text": "Landed on Chance ii", - "player": "charlie", - "timestamp": "2026-02-21 11:03:39" + "text": "Landed on Ventnor ave. (Y)", + "player": "bob", + "timestamp": "2026-02-21 11:24:25" }, { - "text": "Go Back 3 Spaces", + "text": "Paid $22 rent to charlie", + "player": "bob" + }, + { + "text": "charlie's turn \u2014 $163 on Ventnor ave. (Y)", + "player": "charlie", + "timestamp": "2026-02-21 11:24:26" + }, + { + "text": "roll is 2, 3", + "player": "charlie", + "timestamp": "2026-02-21 11:24:27" + }, + { + "text": "Landed on N. Carolina ave. (G)", + "player": "charlie", + "timestamp": "2026-02-21 11:24:28" + }, + { + "text": "Paid $26 rent to alice", "player": "charlie" }, { - "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)", + "text": "alice's turn \u2014 $88 on Pennsylvania ave. (G)", "player": "alice", - "timestamp": "2026-02-21 11:03:42" + "timestamp": "2026-02-21 11:24:29" }, { - "text": "roll is 1, 2", + "text": "roll is 3, 3", "player": "alice", - "timestamp": "2026-02-21 11:03:43" + "timestamp": "2026-02-21 11:24:31" }, { - "text": "Landed on St. James pl. (O)", + "text": "Passed GO \u2014 collected $200", "player": "alice", - "timestamp": "2026-02-21 11:03:43" + "timestamp": "2026-02-21 11:24:31" }, { - "text": "Paid $14 rent to bob", - "player": "alice" - }, - { - "text": "bob's turn \u2014 $1113 on Pennsylvania RR", - "player": "bob", - "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 $320 on New York ave. (O)", - "player": "charlie", - "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)", + "text": "Landed on === GO ===", "player": "alice", - "timestamp": "2026-02-21 11:03:51" + "timestamp": "2026-02-21 11:24:32" }, { - "text": "roll is 5, 4", + "text": "alice's turn \u2014 $288 on === GO ===", "player": "alice", - "timestamp": "2026-02-21 11:03:52" + "timestamp": "2026-02-21 11:24:33" }, { - "text": "Landed on B&O RR", + "text": "roll is 2, 1", "player": "alice", - "timestamp": "2026-02-21 11:03:52" + "timestamp": "2026-02-21 11:24:34" }, { - "text": "Paid $50 rent to bob", - "player": "alice" + "text": "Landed on Baltic ave. (P)", + "player": "alice", + "timestamp": "2026-02-21 11:24:35" }, { - "text": "bob's turn \u2014 $1127 on Kentucky ave. (R)", + "text": "bob's turn \u2014 $1116 on Ventnor ave. (Y)", "player": "bob", - "timestamp": "2026-02-21 11:03:54" + "timestamp": "2026-02-21 11:24:36" }, { - "text": "bob's turn \u2014 $1127 on Kentucky ave. (R)", + "text": "roll is 4, 6", "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" + "timestamp": "2026-02-21 11:24:37" }, { "text": "Landed on Park place (D)", "player": "bob", - "timestamp": "2026-02-21 11:04:10" + "timestamp": "2026-02-21 11:24:38" }, { - "text": "Paid $35 rent to alice", - "player": "bob" + "text": "charlie's turn \u2014 $137 on N. Carolina ave. (G)", + "player": "charlie", + "timestamp": "2026-02-21 11:24:40" }, { - "text": "bob's turn \u2014 $1292 on Park place (D)", + "text": "roll is 3, 5", + "player": "charlie", + "timestamp": "2026-02-21 11:24:41" + }, + { + "text": "Passed GO \u2014 collected $200", + "player": "charlie", + "timestamp": "2026-02-21 11:24:41" + }, + { + "text": "Landed on === GO ===", + "player": "charlie", + "timestamp": "2026-02-21 11:24:42" + }, + { + "text": "alice's turn \u2014 $288 on Baltic ave. (P)", + "player": "alice", + "timestamp": "2026-02-21 11:24:43" + }, + { + "text": "roll is 6, 5", + "player": "alice", + "timestamp": "2026-02-21 11:24:44" + }, + { + "text": "Landed on Virginia ave. (V)", + "player": "alice", + "timestamp": "2026-02-21 11:24:45" + }, + { + "text": "Paid $12 rent to charlie", + "player": "alice" + }, + { + "text": "bob's turn \u2014 $766 on Park place (D)", "player": "bob", - "timestamp": "2026-02-21 11:04:12" + "timestamp": "2026-02-21 11:24:46" + }, + { + "text": "roll is 3, 2", + "player": "bob", + "timestamp": "2026-02-21 11:24:47" + }, + { + "text": "Passed GO \u2014 collected $200", + "player": "bob", + "timestamp": "2026-02-21 11:24:48" + }, + { + "text": "Landed on Community Chest i", + "player": "bob", + "timestamp": "2026-02-21 11:24:48" } ], - "lastUpdated": "2026-02-21T11:04:14.993563+00:00" + "lastUpdated": "2026-02-21T11:24:48.953583+00:00" } \ No newline at end of file diff --git a/site/index.html b/site/index.html index f4ad372..7935957 100644 --- a/site/index.html +++ b/site/index.html @@ -406,48 +406,77 @@ function renderPlayers(state) { container.innerHTML = ''; const players = state.players || []; const squares = state.squares || []; + const isSetup = state.phase === 'setup'; + const expected = state.numPlayersExpected || 0; - players.forEach((p, idx) => { + // During setup, show slots for all expected players + const slotCount = isSetup ? Math.max(players.length, expected) : players.length; + + for (let idx = 0; idx < slotCount; idx++) { + const p = players[idx]; // may be undefined for empty slots const panel = document.createElement('div'); - panel.className = 'player-panel' + (p.number === state.currentPlayer ? ' current-turn' : ''); - const color = PLAYER_COLORS[idx % PLAYER_COLORS.length]; - let html = `

${p.name.charAt(0).toUpperCase()} ${p.name}`; - if (p.number === state.currentPlayer) html += ' ๐ŸŽฒ'; - html += '

'; - html += `
$${p.money.toLocaleString()}
`; + if (!p) { + // Empty slot โ€” waiting for registration + panel.className = 'player-panel'; + panel.style.opacity = '0.4'; + panel.style.borderStyle = 'dashed'; + panel.innerHTML = `

? Player ${idx + 1}

+
Waiting to join...
`; + } else if (isSetup && p.name.startsWith('Player ')) { + // Placeholder โ€” registered slot but no name yet + panel.className = 'player-panel'; + panel.style.opacity = '0.6'; + panel.style.borderStyle = 'dashed'; + panel.innerHTML = `

? ${p.name}

+
Registering...
`; + } else if (isSetup) { + // Registered player during setup + panel.className = 'player-panel'; + panel.innerHTML = `

${p.name.charAt(0).toUpperCase()} ${p.name} โœ“

+
$1,500
+
Ready to play
`; + } else { + // Normal playing state + panel.className = 'player-panel' + (p.number === state.currentPlayer ? ' current-turn' : ''); + let html = `

${p.name.charAt(0).toUpperCase()} ${p.name}`; + if (p.number === state.currentPlayer) html += ' ๐ŸŽฒ'; + html += '

'; - if (p.getOutOfJailFreeCards > 0) { - html += `
๐Ÿƒ ${p.getOutOfJailFreeCards} GOJF card${p.getOutOfJailFreeCards > 1 ? 's' : ''}
`; - } - if (p.inJail) { - html += `
๐Ÿ”’ In Jail (turn ${p.jailTurns})
`; - } + html += `
$${p.money.toLocaleString()}
`; - const loc = p.location === 40 ? { name: 'JAIL' } : squares[p.location]; - if (loc) { - html += `
๐Ÿ“ ${loc.name}
`; - } + if (p.getOutOfJailFreeCards > 0) { + html += `
๐Ÿƒ ${p.getOutOfJailFreeCards} GOJF card${p.getOutOfJailFreeCards > 1 ? 's' : ''}
`; + } + if (p.inJail) { + html += `
๐Ÿ”’ In Jail (turn ${p.jailTurns})
`; + } - // Properties owned by this player - const owned = squares.filter(sq => sq.owner === p.number); - if (owned.length > 0) { - html += '
'; - owned.forEach(sq => { - const gc = sq.group ? GROUP_COLORS[sq.group] || '#666' : '#666'; - html += `
${sq.name}`; - if (sq.houses > 0 && sq.houses < 5) html += ` ๐Ÿ ร—${sq.houses}`; - else if (sq.houses >= 5) html += ' ๐Ÿจ'; - if (sq.mortgaged) html += ' (M)'; + const loc = p.location === 40 ? { name: 'JAIL' } : squares[p.location]; + if (loc) { + html += `
๐Ÿ“ ${loc.name}
`; + } + + const owned = squares.filter(sq => sq.owner === p.number); + if (owned.length > 0) { + html += '
'; + owned.forEach(sq => { + const gc = sq.group ? GROUP_COLORS[sq.group] || '#666' : '#666'; + html += `
${sq.name}`; + if (sq.houses > 0 && sq.houses < 5) html += ` ๐Ÿ ร—${sq.houses}`; + else if (sq.houses >= 5) html += ' ๐Ÿจ'; + if (sq.mortgaged) html += ' (M)'; + html += '
'; + }); html += '
'; - }); - html += '
'; + } + + panel.innerHTML = html; } - panel.innerHTML = html; container.appendChild(panel); - }); + } } function renderLog(state) { @@ -626,14 +655,29 @@ async function update() { checkStale(); // Determine what to show - if (!state.players || state.players.length === 0) { - // No players = no game + if (!state.players || (state.players.length === 0 && !state.phase)) { + // No players and no phase = no game showView('zero'); lastUpdate = ''; gameOverShown = false; return; } + // Setup phase โ€” show registration + if (state.phase === 'setup') { + const updateStr = JSON.stringify(state); + if (updateStr !== lastUpdate) { + lastUpdate = updateStr; + showView('playing'); + renderGame(state); + } + const registered = (state.players || []).filter(p => !p.name.startsWith('Player ')).length; + const expected = state.numPlayersExpected || '?'; + document.getElementById('status').textContent = + `Setting up ยท ${registered}/${expected} players registered`; + return; + } + // Check for game over if (isGameOver(state)) { const updateStr = JSON.stringify(state);