diff --git a/__pycache__/monop_parser.cpython-310.pyc b/__pycache__/monop_parser.cpython-310.pyc index 9e0778a..d5bad55 100644 Binary files a/__pycache__/monop_parser.cpython-310.pyc and b/__pycache__/monop_parser.cpython-310.pyc differ 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);