Fix setup visibility: bridge waits for players, observer sees registration

Bridge changes:
- Wait for at least one user to JOIN before starting monop
- Ensures observer is in channel to see all setup messages

Parser changes:
- Handle 'Player N, say me' even without prior 'How many players?'
- Infer num_players_expected from highest player number seen
- Emit state during setup phase

run_game.py changes:
- 3s stagger between bot joins so setup is visible in web UI
- Observer connects before bots to catch all registration messages
This commit is contained in:
Jarvis 2026-02-21 11:33:28 +00:00
parent 72d996cb4b
commit 6d055c68a2
6 changed files with 107 additions and 330 deletions

View file

@ -90,8 +90,27 @@ class IRCBridge:
print("[bridge] monop process died, restarting...") print("[bridge] monop process died, restarting...")
self.start_monop() self.start_monop()
def _wait_for_players(self):
"""Wait until at least one other user joins the channel before starting."""
print(f"[bridge] Waiting for players to join {CHANNEL}...")
while True:
data = self.sock.recv(4096)
if not data:
raise ConnectionError("IRC connection lost while waiting")
self.buffer += data.decode("utf-8", errors="replace")
while "\r\n" in self.buffer:
line, self.buffer = self.buffer.split("\r\n", 1)
if line.startswith("PING"):
self.irc_send("PONG" + line[4:])
# Detect JOIN from someone other than us
m = re.match(r":(\S+?)!\S+ JOIN", line)
if m and m.group(1) != NICK:
print(f"[bridge] {m.group(1)} joined — starting game")
return
def run(self): def run(self):
self.connect_irc() self.connect_irc()
self._wait_for_players()
self.start_monop() self.start_monop()
while True: while True:

View file

@ -408,7 +408,19 @@ class MonopParser:
return return
if g is None: if g is None:
# No game yet - try to pick up from checkpoint # No game yet - try to pick up from setup or checkpoint
m = self.SAY_ME_RE.match(msg)
if m:
self._new_game()
g = self.game
g.phase = "setup"
num = int(m.group(1))
g.num_players_expected = num
p = Player(f"Player {num}", num)
g.players.append(p)
g.add_log(f"Waiting for Player {num} to register...", timestamp=timestamp)
return
m = self.CHECKPOINT_RE.match(msg) m = self.CHECKPOINT_RE.match(msg)
if m: if m:
self._new_game() self._new_game()
@ -471,6 +483,9 @@ class MonopParser:
p = Player(f"Player {num}", num) p = Player(f"Player {num}", num)
g.players.append(p) g.players.append(p)
g.add_log(f"Waiting for Player {num} to register...", timestamp=timestamp) g.add_log(f"Waiting for Player {num} to register...", timestamp=timestamp)
# Update expected count (in case we missed "How many players?")
if num > g.num_players_expected:
g.num_players_expected = num
return return
return return

View file

@ -408,7 +408,19 @@ class MonopParser:
return return
if g is None: if g is None:
# No game yet - try to pick up from checkpoint # No game yet - try to pick up from setup or checkpoint
m = self.SAY_ME_RE.match(msg)
if m:
self._new_game()
g = self.game
g.phase = "setup"
num = int(m.group(1))
g.num_players_expected = num
p = Player(f"Player {num}", num)
g.players.append(p)
g.add_log(f"Waiting for Player {num} to register...", timestamp=timestamp)
return
m = self.CHECKPOINT_RE.match(msg) m = self.CHECKPOINT_RE.match(msg)
if m: if m:
self._new_game() self._new_game()
@ -471,6 +483,9 @@ class MonopParser:
p = Player(f"Player {num}", num) p = Player(f"Player {num}", num)
g.players.append(p) g.players.append(p)
g.add_log(f"Waiting for Player {num} to register...", timestamp=timestamp) g.add_log(f"Waiting for Player {num} to register...", timestamp=timestamp)
# Update expected count (in case we missed "How many players?")
if num > g.num_players_expected:
g.num_players_expected = num
return return
return return

View file

@ -82,21 +82,23 @@ class ParserClient:
def main(): def main():
player_names = ["alice", "bob", "charlie"] player_names = ["alice", "bob", "charlie"]
# Start parser client # 1. Start parser/observer client FIRST so it sees setup messages
pc = ParserClient() pc = ParserClient()
parser_thread = threading.Thread(target=pc.run, daemon=True) parser_thread = threading.Thread(target=pc.run, daemon=True)
parser_thread.start() parser_thread.start()
print(f"[game] Parser writing to {STATE_PATH}") print(f"[game] Observer connected, writing to {STATE_PATH}")
time.sleep(2)
# Start player bots # 2. Wait for observer to be fully joined before bots trigger setup
time.sleep(3)
# 3. Start player bots (they'll trigger setup by sending player count)
bots = [] bots = []
for i, name in enumerate(player_names): for i, name in enumerate(player_names):
bot = PlayerBot(name, CHANNEL, HOST, PORT, player_names, i) bot = PlayerBot(name, CHANNEL, HOST, PORT, player_names, i)
t = threading.Thread(target=bot.run, daemon=True) t = threading.Thread(target=bot.run, daemon=True)
t.start() t.start()
bots.append(bot) bots.append(bot)
time.sleep(0.5) time.sleep(3.0) # stagger joins so setup is visible in the web UI
print(f"[game] {len(bots)} player bots started") print(f"[game] {len(bots)} player bots started")

View file

@ -1,19 +1,9 @@
{ {
"players": [ "players": [
{ {
"name": "bob", "name": "Player 1",
"number": 2, "number": 1,
"money": 966, "money": 1500,
"location": 2,
"inJail": false,
"jailTurns": 0,
"doublesCount": 0,
"getOutOfJailFreeCards": 0
},
{
"name": "charlie",
"number": 3,
"money": 349,
"location": 0, "location": 0,
"inJail": false, "inJail": false,
"jailTurns": 0, "jailTurns": 0,
@ -21,17 +11,17 @@
"getOutOfJailFreeCards": 0 "getOutOfJailFreeCards": 0
}, },
{ {
"name": "alice", "name": "Player 2",
"number": 1, "number": 2,
"money": 276, "money": 1500,
"location": 14, "location": 0,
"inJail": false, "inJail": false,
"jailTurns": 0, "jailTurns": 0,
"doublesCount": 0, "doublesCount": 0,
"getOutOfJailFreeCards": 1 "getOutOfJailFreeCards": 0
} }
], ],
"currentPlayer": 2, "currentPlayer": null,
"squares": [ "squares": [
{ {
"id": 0, "id": 0,
@ -41,12 +31,7 @@
{ {
"id": 1, "id": 1,
"name": "Mediterranean ave. (P)", "name": "Mediterranean ave. (P)",
"type": "property", "type": "property"
"owner": 3,
"mortgaged": false,
"group": "purple",
"cost": 60,
"houses": 0
}, },
{ {
"id": 2, "id": 2,
@ -56,12 +41,7 @@
{ {
"id": 3, "id": 3,
"name": "Baltic ave. (P)", "name": "Baltic ave. (P)",
"type": "property", "type": "property"
"owner": 1,
"mortgaged": false,
"group": "purple",
"cost": 60,
"houses": 0
}, },
{ {
"id": 4, "id": 4,
@ -71,21 +51,12 @@
{ {
"id": 5, "id": 5,
"name": "Reading RR", "name": "Reading RR",
"type": "railroad", "type": "railroad"
"owner": 1,
"mortgaged": false,
"group": "railroad",
"cost": 200
}, },
{ {
"id": 6, "id": 6,
"name": "Oriental ave. (L)", "name": "Oriental ave. (L)",
"type": "property", "type": "property"
"owner": 3,
"mortgaged": false,
"group": "lightblue",
"cost": 100,
"houses": 0
}, },
{ {
"id": 7, "id": 7,
@ -95,22 +66,12 @@
{ {
"id": 8, "id": 8,
"name": "Vermont ave. (L)", "name": "Vermont ave. (L)",
"type": "property", "type": "property"
"owner": null,
"mortgaged": false,
"group": "lightblue",
"cost": 100,
"houses": 0
}, },
{ {
"id": 9, "id": 9,
"name": "Connecticut ave. (L)", "name": "Connecticut ave. (L)",
"type": "property", "type": "property"
"owner": null,
"mortgaged": false,
"group": "lightblue",
"cost": 120,
"houses": 0
}, },
{ {
"id": 10, "id": 10,
@ -120,60 +81,32 @@
{ {
"id": 11, "id": 11,
"name": "St. Charles pl. (V)", "name": "St. Charles pl. (V)",
"type": "property", "type": "property"
"owner": 2,
"mortgaged": false,
"group": "violet",
"cost": 140,
"houses": 0
}, },
{ {
"id": 12, "id": 12,
"name": "Electric Co.", "name": "Electric Co.",
"type": "utility", "type": "utility"
"owner": 3,
"mortgaged": false,
"group": "utility",
"cost": 150
}, },
{ {
"id": 13, "id": 13,
"name": "States ave. (V)", "name": "States ave. (V)",
"type": "property", "type": "property"
"owner": 1,
"mortgaged": false,
"group": "violet",
"cost": 140,
"houses": 0
}, },
{ {
"id": 14, "id": 14,
"name": "Virginia ave. (V)", "name": "Virginia ave. (V)",
"type": "property", "type": "property"
"owner": 3,
"mortgaged": false,
"group": "violet",
"cost": 160,
"houses": 0
}, },
{ {
"id": 15, "id": 15,
"name": "Pennsylvania RR", "name": "Pennsylvania RR",
"type": "railroad", "type": "railroad"
"owner": 3,
"mortgaged": false,
"group": "railroad",
"cost": 200
}, },
{ {
"id": 16, "id": 16,
"name": "St. James pl. (O)", "name": "St. James pl. (O)",
"type": "property", "type": "property"
"owner": null,
"mortgaged": false,
"group": "orange",
"cost": 180,
"houses": 0
}, },
{ {
"id": 17, "id": 17,
@ -183,22 +116,12 @@
{ {
"id": 18, "id": 18,
"name": "Tennessee ave. (O)", "name": "Tennessee ave. (O)",
"type": "property", "type": "property"
"owner": 1,
"mortgaged": false,
"group": "orange",
"cost": 180,
"houses": 0
}, },
{ {
"id": 19, "id": 19,
"name": "New York ave. (O)", "name": "New York ave. (O)",
"type": "property", "type": "property"
"owner": 3,
"mortgaged": false,
"group": "orange",
"cost": 200,
"houses": 0
}, },
{ {
"id": 20, "id": 20,
@ -208,12 +131,7 @@
{ {
"id": 21, "id": 21,
"name": "Kentucky ave. (R)", "name": "Kentucky ave. (R)",
"type": "property", "type": "property"
"owner": null,
"mortgaged": false,
"group": "red",
"cost": 220,
"houses": 0
}, },
{ {
"id": 22, "id": 22,
@ -223,70 +141,37 @@
{ {
"id": 23, "id": 23,
"name": "Indiana ave. (R)", "name": "Indiana ave. (R)",
"type": "property", "type": "property"
"owner": 3,
"mortgaged": false,
"group": "red",
"cost": 220,
"houses": 0
}, },
{ {
"id": 24, "id": 24,
"name": "Illinois ave. (R)", "name": "Illinois ave. (R)",
"type": "property", "type": "property"
"owner": null,
"mortgaged": false,
"group": "red",
"cost": 240,
"houses": 0
}, },
{ {
"id": 25, "id": 25,
"name": "B&O RR", "name": "B&O RR",
"type": "railroad", "type": "railroad"
"owner": 2,
"mortgaged": false,
"group": "railroad",
"cost": 200
}, },
{ {
"id": 26, "id": 26,
"name": "Atlantic ave. (Y)", "name": "Atlantic ave. (Y)",
"type": "property", "type": "property"
"owner": null,
"mortgaged": false,
"group": "yellow",
"cost": 260,
"houses": 0
}, },
{ {
"id": 27, "id": 27,
"name": "Ventnor ave. (Y)", "name": "Ventnor ave. (Y)",
"type": "property", "type": "property"
"owner": 3,
"mortgaged": false,
"group": "yellow",
"cost": 260,
"houses": 0
}, },
{ {
"id": 28, "id": 28,
"name": "Water Works", "name": "Water Works",
"type": "utility", "type": "utility"
"owner": null,
"mortgaged": false,
"group": "utility",
"cost": 150
}, },
{ {
"id": 29, "id": 29,
"name": "Marvin Gardens (Y)", "name": "Marvin Gardens (Y)",
"type": "property", "type": "property"
"owner": null,
"mortgaged": false,
"group": "yellow",
"cost": 280,
"houses": 0
}, },
{ {
"id": 30, "id": 30,
@ -296,22 +181,12 @@
{ {
"id": 31, "id": 31,
"name": "Pacific ave. (G)", "name": "Pacific ave. (G)",
"type": "property", "type": "property"
"owner": 3,
"mortgaged": false,
"group": "green",
"cost": 300,
"houses": 0
}, },
{ {
"id": 32, "id": 32,
"name": "N. Carolina ave. (G)", "name": "N. Carolina ave. (G)",
"type": "property", "type": "property"
"owner": 1,
"mortgaged": false,
"group": "green",
"cost": 300,
"houses": 0
}, },
{ {
"id": 33, "id": 33,
@ -321,21 +196,12 @@
{ {
"id": 34, "id": 34,
"name": "Pennsylvania ave. (G)", "name": "Pennsylvania ave. (G)",
"type": "property", "type": "property"
"owner": 1,
"mortgaged": false,
"group": "green",
"cost": 320,
"houses": 0
}, },
{ {
"id": 35, "id": 35,
"name": "Short Line RR", "name": "Short Line RR",
"type": "railroad", "type": "railroad"
"owner": 1,
"mortgaged": false,
"group": "railroad",
"cost": 200
}, },
{ {
"id": 36, "id": 36,
@ -345,12 +211,7 @@
{ {
"id": 37, "id": 37,
"name": "Park place (D)", "name": "Park place (D)",
"type": "property", "type": "property"
"owner": 2,
"mortgaged": false,
"group": "darkblue",
"cost": 350,
"houses": 0
}, },
{ {
"id": 38, "id": 38,
@ -360,162 +221,27 @@
{ {
"id": 39, "id": 39,
"name": "Boardwalk (D)", "name": "Boardwalk (D)",
"type": "property", "type": "property"
"owner": 3,
"mortgaged": false,
"group": "darkblue",
"cost": 400,
"houses": 0
} }
], ],
"log": [ "log": [
{ {
"text": "bob's turn \u2014 $1138 on New York ave. (O)", "text": "Game for 3 players",
"player": "bob", "player": null,
"timestamp": "2026-02-21 11:24:23" "timestamp": "2026-02-21 11:33:22"
}, },
{ {
"text": "roll is 6, 2", "text": "Waiting for Player 1 to register...",
"player": "bob", "player": null,
"timestamp": "2026-02-21 11:24:24" "timestamp": "2026-02-21 11:33:22"
}, },
{ {
"text": "Landed on Ventnor ave. (Y)", "text": "Waiting for Player 2 to register...",
"player": "bob", "player": null,
"timestamp": "2026-02-21 11:24:25" "timestamp": "2026-02-21 11:33:23"
},
{
"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": "alice's turn \u2014 $88 on Pennsylvania ave. (G)",
"player": "alice",
"timestamp": "2026-02-21 11:24:29"
},
{
"text": "roll is 3, 3",
"player": "alice",
"timestamp": "2026-02-21 11:24:31"
},
{
"text": "Passed GO \u2014 collected $200",
"player": "alice",
"timestamp": "2026-02-21 11:24:31"
},
{
"text": "Landed on === GO ===",
"player": "alice",
"timestamp": "2026-02-21 11:24:32"
},
{
"text": "alice's turn \u2014 $288 on === GO ===",
"player": "alice",
"timestamp": "2026-02-21 11:24:33"
},
{
"text": "roll is 2, 1",
"player": "alice",
"timestamp": "2026-02-21 11:24:34"
},
{
"text": "Landed on Baltic ave. (P)",
"player": "alice",
"timestamp": "2026-02-21 11:24:35"
},
{
"text": "bob's turn \u2014 $1116 on Ventnor ave. (Y)",
"player": "bob",
"timestamp": "2026-02-21 11:24:36"
},
{
"text": "roll is 4, 6",
"player": "bob",
"timestamp": "2026-02-21 11:24:37"
},
{
"text": "Landed on Park place (D)",
"player": "bob",
"timestamp": "2026-02-21 11:24:38"
},
{
"text": "charlie's turn \u2014 $137 on N. Carolina ave. (G)",
"player": "charlie",
"timestamp": "2026-02-21 11:24:40"
},
{
"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: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:24:48.953583+00:00" "phase": "setup",
"numPlayersExpected": 3,
"lastUpdated": "2026-02-21T11:33:23.055895+00:00"
} }