From 6d055c68a22dab3ebe844130c112b9627f41c293 Mon Sep 17 00:00:00 2001 From: Jarvis Date: Sat, 21 Feb 2026 11:33:28 +0000 Subject: [PATCH] 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 --- __pycache__/monop_parser.cpython-310.pyc | Bin 26304 -> 26467 bytes monop_bridge.py | 19 ++ monop_parser.py | 17 +- plugins/monop/monop_parser.py | 17 +- run_game.py | 12 +- site/game-state.json | 372 +++-------------------- 6 files changed, 107 insertions(+), 330 deletions(-) diff --git a/__pycache__/monop_parser.cpython-310.pyc b/__pycache__/monop_parser.cpython-310.pyc index d5bad553634084b1a230e772fdfc7f0df2d1980d..ee65d839749bb05129f51e98adcd666eb0c82a86 100644 GIT binary patch delta 5105 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 delta 4842 zcmaJ_eQXrR72nz2^Vw$`<2#=Z`_4WaV`KASCA<>Gavr2rQ!;Qy zVTX_w<*HtnuIHf)ArdWzJ(qGxmGlN$)-B1B)*v(irToDBQs~ox z07_v-Ber!I$dnQvPSnyc54F)nH>255lp_x~}REr?YdG6MQl_;pH6|-_O!XtPDROWF~77;mu5|0Rta1dqr4tjWOn$p^SVvW z^R{xHvnn%ALm@-=fXYzW={(QX>2bS*sO*dtHmnq|o^~L67Cecj)c915IMApt_M3jtj#`#54t?+HUHY$ zU<`tBswN|Y&>rOPvH9ID{^48%fWOw_L(km+J=en+L@y_m^mVRvXY*5)tcTasdR{N; zO6VAZ6Z?>ho_6(wd30d+xY%zjRBmS4!hv@dYBAq!s|tG9&3nvz)mCMA*d5#Ljn)pS z0tOj_v20-$F_)V#?hh}FJADMgSistFHH=Wnh%jdx*NQh{3GKkjP;!-FXkGU9o1N`@ zQNd>Fkbq+?6CF*`w>ZoZWDa9lblFWYr?1yx#gR2_7f;R*OZymBr%QdGwGi(Y1?)Sv zdHs&M=TY4`TiqtxFUzz6*V;GC3+=&DJfF!1Ty@F3(jG9j+B&zn*q^&{x|G9RLfcL^ zb8=dOIQ|XS|0Q<<>|GV$pf zJB4u&d%#e$j{#$6u4*kdiwGIK6O=spV4f>p*6(s)Vx zuhi^H3j^~N&=M>ga@qWrS>F{TA@kGDnkp}Q&rZ>97w3-P7*;}?`EuSoDMOrsHuv#7 zZXp}Y^^Nhlq4jv z-0eUXeM7V^(*$X{G()QuF-`-+`4j^}q_Pe-GAZrxIF`N4&m;;4| zr?HS-6+6K$##L8^tv~}TrN82UU_xO9rkRnE|lIUwz> z`TYjk|1T8hEInW;O%*ZGq(;QAz1_GKQjpXn2$$&A85y2Kz?!gr(+9;DjKxmVyU+R| zXJrc>(Ft3lG3iVUKxGuMK9Osvj#7BbZ-c9B-isDa@nH_%2)PaPBi33BX%Rc(WOxm7;D@!lVNJ+Rn>viz`wZ!`N60)?ySEJ@h z9^zrRX!{sE!L?1&PFNL`@=Xs`ICedOthP2S17Uyn{8w zFFE)C;vYNMhgjrX;v%`lh%Y&K9Pux5*q|~Bev=c>3B+$X_#k4dzYB#F@pWhZ5aJsS zo<#hKgAXJA+`&f>e`#Y#%ux^&Gua{Nh5^|^tK9F zLQQ+2dqe6|*T@ z9it4{ae-+ZuOY?kX?s?o37ma){_z*{EvKHY*~LyAG@p6see?OAx8x-TixobY))}c}^xTW|!~(gpgOW{?S1~{$Wmx^^+^vpN>5v zlfRqo2YyCAG(R{nM{b(m9`BX^tD0BFM*-AM43YQE>4`rtz9F{zNDLo};Z}Cv!50WA Y%-*IULJG`+$sdrn&GVDjPx# diff --git a/monop_bridge.py b/monop_bridge.py index d995bf3..10a8085 100644 --- a/monop_bridge.py +++ b/monop_bridge.py @@ -90,8 +90,27 @@ class IRCBridge: print("[bridge] monop process died, restarting...") 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): self.connect_irc() + self._wait_for_players() self.start_monop() while True: diff --git a/monop_parser.py b/monop_parser.py index afa4255..9a610fe 100644 --- a/monop_parser.py +++ b/monop_parser.py @@ -408,7 +408,19 @@ class MonopParser: return 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) if m: self._new_game() @@ -471,6 +483,9 @@ class MonopParser: p = Player(f"Player {num}", num) g.players.append(p) 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 diff --git a/plugins/monop/monop_parser.py b/plugins/monop/monop_parser.py index afa4255..9a610fe 100644 --- a/plugins/monop/monop_parser.py +++ b/plugins/monop/monop_parser.py @@ -408,7 +408,19 @@ class MonopParser: return 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) if m: self._new_game() @@ -471,6 +483,9 @@ class MonopParser: p = Player(f"Player {num}", num) g.players.append(p) 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 diff --git a/run_game.py b/run_game.py index 93b5ee6..7cbcbd1 100644 --- a/run_game.py +++ b/run_game.py @@ -82,21 +82,23 @@ class ParserClient: def main(): player_names = ["alice", "bob", "charlie"] - # Start parser client + # 1. Start parser/observer client FIRST so it sees setup messages pc = ParserClient() parser_thread = threading.Thread(target=pc.run, daemon=True) parser_thread.start() - print(f"[game] Parser writing to {STATE_PATH}") - time.sleep(2) + print(f"[game] Observer connected, writing to {STATE_PATH}") - # 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 = [] for i, name in enumerate(player_names): bot = PlayerBot(name, CHANNEL, HOST, PORT, player_names, i) t = threading.Thread(target=bot.run, daemon=True) t.start() 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") diff --git a/site/game-state.json b/site/game-state.json index 08e36d1..ad1321a 100644 --- a/site/game-state.json +++ b/site/game-state.json @@ -1,19 +1,9 @@ { "players": [ { - "name": "bob", - "number": 2, - "money": 966, - "location": 2, - "inJail": false, - "jailTurns": 0, - "doublesCount": 0, - "getOutOfJailFreeCards": 0 - }, - { - "name": "charlie", - "number": 3, - "money": 349, + "name": "Player 1", + "number": 1, + "money": 1500, "location": 0, "inJail": false, "jailTurns": 0, @@ -21,17 +11,17 @@ "getOutOfJailFreeCards": 0 }, { - "name": "alice", - "number": 1, - "money": 276, - "location": 14, + "name": "Player 2", + "number": 2, + "money": 1500, + "location": 0, "inJail": false, "jailTurns": 0, "doublesCount": 0, - "getOutOfJailFreeCards": 1 + "getOutOfJailFreeCards": 0 } ], - "currentPlayer": 2, + "currentPlayer": null, "squares": [ { "id": 0, @@ -41,12 +31,7 @@ { "id": 1, "name": "Mediterranean ave. (P)", - "type": "property", - "owner": 3, - "mortgaged": false, - "group": "purple", - "cost": 60, - "houses": 0 + "type": "property" }, { "id": 2, @@ -56,12 +41,7 @@ { "id": 3, "name": "Baltic ave. (P)", - "type": "property", - "owner": 1, - "mortgaged": false, - "group": "purple", - "cost": 60, - "houses": 0 + "type": "property" }, { "id": 4, @@ -71,21 +51,12 @@ { "id": 5, "name": "Reading RR", - "type": "railroad", - "owner": 1, - "mortgaged": false, - "group": "railroad", - "cost": 200 + "type": "railroad" }, { "id": 6, "name": "Oriental ave. (L)", - "type": "property", - "owner": 3, - "mortgaged": false, - "group": "lightblue", - "cost": 100, - "houses": 0 + "type": "property" }, { "id": 7, @@ -95,22 +66,12 @@ { "id": 8, "name": "Vermont ave. (L)", - "type": "property", - "owner": null, - "mortgaged": false, - "group": "lightblue", - "cost": 100, - "houses": 0 + "type": "property" }, { "id": 9, "name": "Connecticut ave. (L)", - "type": "property", - "owner": null, - "mortgaged": false, - "group": "lightblue", - "cost": 120, - "houses": 0 + "type": "property" }, { "id": 10, @@ -120,60 +81,32 @@ { "id": 11, "name": "St. Charles pl. (V)", - "type": "property", - "owner": 2, - "mortgaged": false, - "group": "violet", - "cost": 140, - "houses": 0 + "type": "property" }, { "id": 12, "name": "Electric Co.", - "type": "utility", - "owner": 3, - "mortgaged": false, - "group": "utility", - "cost": 150 + "type": "utility" }, { "id": 13, "name": "States ave. (V)", - "type": "property", - "owner": 1, - "mortgaged": false, - "group": "violet", - "cost": 140, - "houses": 0 + "type": "property" }, { "id": 14, "name": "Virginia ave. (V)", - "type": "property", - "owner": 3, - "mortgaged": false, - "group": "violet", - "cost": 160, - "houses": 0 + "type": "property" }, { "id": 15, "name": "Pennsylvania RR", - "type": "railroad", - "owner": 3, - "mortgaged": false, - "group": "railroad", - "cost": 200 + "type": "railroad" }, { "id": 16, "name": "St. James pl. (O)", - "type": "property", - "owner": null, - "mortgaged": false, - "group": "orange", - "cost": 180, - "houses": 0 + "type": "property" }, { "id": 17, @@ -183,22 +116,12 @@ { "id": 18, "name": "Tennessee ave. (O)", - "type": "property", - "owner": 1, - "mortgaged": false, - "group": "orange", - "cost": 180, - "houses": 0 + "type": "property" }, { "id": 19, "name": "New York ave. (O)", - "type": "property", - "owner": 3, - "mortgaged": false, - "group": "orange", - "cost": 200, - "houses": 0 + "type": "property" }, { "id": 20, @@ -208,12 +131,7 @@ { "id": 21, "name": "Kentucky ave. (R)", - "type": "property", - "owner": null, - "mortgaged": false, - "group": "red", - "cost": 220, - "houses": 0 + "type": "property" }, { "id": 22, @@ -223,70 +141,37 @@ { "id": 23, "name": "Indiana ave. (R)", - "type": "property", - "owner": 3, - "mortgaged": false, - "group": "red", - "cost": 220, - "houses": 0 + "type": "property" }, { "id": 24, "name": "Illinois ave. (R)", - "type": "property", - "owner": null, - "mortgaged": false, - "group": "red", - "cost": 240, - "houses": 0 + "type": "property" }, { "id": 25, "name": "B&O RR", - "type": "railroad", - "owner": 2, - "mortgaged": false, - "group": "railroad", - "cost": 200 + "type": "railroad" }, { "id": 26, "name": "Atlantic ave. (Y)", - "type": "property", - "owner": null, - "mortgaged": false, - "group": "yellow", - "cost": 260, - "houses": 0 + "type": "property" }, { "id": 27, "name": "Ventnor ave. (Y)", - "type": "property", - "owner": 3, - "mortgaged": false, - "group": "yellow", - "cost": 260, - "houses": 0 + "type": "property" }, { "id": 28, "name": "Water Works", - "type": "utility", - "owner": null, - "mortgaged": false, - "group": "utility", - "cost": 150 + "type": "utility" }, { "id": 29, "name": "Marvin Gardens (Y)", - "type": "property", - "owner": null, - "mortgaged": false, - "group": "yellow", - "cost": 280, - "houses": 0 + "type": "property" }, { "id": 30, @@ -296,22 +181,12 @@ { "id": 31, "name": "Pacific ave. (G)", - "type": "property", - "owner": 3, - "mortgaged": false, - "group": "green", - "cost": 300, - "houses": 0 + "type": "property" }, { "id": 32, "name": "N. Carolina ave. (G)", - "type": "property", - "owner": 1, - "mortgaged": false, - "group": "green", - "cost": 300, - "houses": 0 + "type": "property" }, { "id": 33, @@ -321,21 +196,12 @@ { "id": 34, "name": "Pennsylvania ave. (G)", - "type": "property", - "owner": 1, - "mortgaged": false, - "group": "green", - "cost": 320, - "houses": 0 + "type": "property" }, { "id": 35, "name": "Short Line RR", - "type": "railroad", - "owner": 1, - "mortgaged": false, - "group": "railroad", - "cost": 200 + "type": "railroad" }, { "id": 36, @@ -345,12 +211,7 @@ { "id": 37, "name": "Park place (D)", - "type": "property", - "owner": 2, - "mortgaged": false, - "group": "darkblue", - "cost": 350, - "houses": 0 + "type": "property" }, { "id": 38, @@ -360,162 +221,27 @@ { "id": 39, "name": "Boardwalk (D)", - "type": "property", - "owner": 3, - "mortgaged": false, - "group": "darkblue", - "cost": 400, - "houses": 0 + "type": "property" } ], "log": [ { - "text": "bob's turn \u2014 $1138 on New York ave. (O)", - "player": "bob", - "timestamp": "2026-02-21 11:24:23" + "text": "Game for 3 players", + "player": null, + "timestamp": "2026-02-21 11:33:22" }, { - "text": "roll is 6, 2", - "player": "bob", - "timestamp": "2026-02-21 11:24:24" + "text": "Waiting for Player 1 to register...", + "player": null, + "timestamp": "2026-02-21 11:33:22" }, { - "text": "Landed on Ventnor ave. (Y)", - "player": "bob", - "timestamp": "2026-02-21 11:24:25" - }, - { - "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" + "text": "Waiting for Player 2 to register...", + "player": null, + "timestamp": "2026-02-21 11:33:23" } ], - "lastUpdated": "2026-02-21T11:24:48.953583+00:00" + "phase": "setup", + "numPlayersExpected": 3, + "lastUpdated": "2026-02-21T11:33:23.055895+00:00" } \ No newline at end of file