From 7a4346a53f84dcc352c7c91d41ae9ca2a601f765 Mon Sep 17 00:00:00 2001 From: Jarvis Date: Sat, 21 Feb 2026 10:55:33 +0000 Subject: [PATCH] Fix trade race condition: move trade/roll decision to -- Command: handler The root cause: checkpoint handler queued .trade via say_delayed, but monop sends '-- Command:' before processing the trade. The old handler reset in_trade and queued .roll, so both commands got sent. Fix: checkpoint handler no longer sends commands. The -- Command: prompt is where the bot decides to trade or roll, matching how monop actually works (-- Command: is the interactive prompt). Also: no trades while in jail (jail has no -- Command: before roll). Updated all tests to reflect the new flow. --- __pycache__/monop_players.cpython-310.pyc | Bin 12899 -> 12887 bytes __pycache__/test_players.cpython-310.pyc | Bin 0 -> 16213 bytes monop_players.py | 27 ++-- site/game-state.json | 188 ++++++++-------------- test_players.py | 57 ++++--- 5 files changed, 119 insertions(+), 153 deletions(-) create mode 100644 __pycache__/test_players.cpython-310.pyc diff --git a/__pycache__/monop_players.cpython-310.pyc b/__pycache__/monop_players.cpython-310.pyc index c403b2eb79996297c663e256087e6a8f092f1353..1933d65c0256b5a612b6720428f0ab8cf0ad4ad9 100644 GIT binary patch delta 2265 zcmZ8jX=q$k6n^)MnMa)O={ZIt(2TdGN}pP85HCV z(=dshB9$qx3&m}o{_saZE%QVCBlx=tq5&~h#kPt_5wZUSiRYeq?`4t%a__n4yXSmo zx##6Bd=G*7K)|Qq&+WIa&wO<=PzkaD z7i`j*$#Y!i;%ZSTZ#ETi8MLBux>4t4H+W2zbLlSd&vLNSYK&csgwBJcIT5K9plZ`?Y*9NGM zDg{2P;Eho_kCo#PgLvNL5hdrA7qbc7U0`dMG2;naB4VvI#AdS|n_7Wk92ADmF!@_$ zjG5PH-$wE3l;Mb#xAs>hiHp_DmGVryxjuPV+zyK7JqyCbI~x(`rjty6>R>)5%r=`a zxla-Xi<1ta%li)Y{(0pk1}W%T^yX37`nXbHIgjo^(4B0oyj7R|iCtM%VY}{qTX~mD zvxp4qSUc#x$g6!QPoT#+kxdw~qU^;GxZqT2JeO^XGTOkb_N|eGd_MvGl%hK8@&KLD zW5R3x9{}W(6^O39|@!dtf}`G#3}wovYXAs?&x@U>_0q zo$?iOaQ7_w^3~TQ7*G8ro{`!h#AY?5b-Rkp$hEP=^O@UN@tCQ=etaCLriz8=ZMLB? z6%I(=WcHvP0ZG+qR5L3uszVr+DvqLF)>{=LI?J}hp#lT8#9I`4#C7l}HlCu_l{?_D zX?)j`nv!qZulwOKd>X1~Evc_Gpwxqrs-l;iQ37B@!8Cqv5}nglyNzSJY2tazlL{xQErz)dOE<>3c=&}d46k#^V1hg@ zt;@6~3h3Ei(lWSW=Wob3O~F%GAx#*#-v-ap{)+KW{D$#$Vz+xm6Z2=!8fxo3=BnIn z%`gU)lWjjR%4XYlO4K%5UAk*m&hgubZ*9%4pvO&dwQ4eL4rf$2cNr{L$)v*#zCSqT(FY|#2DbeNs9Vzo!jg?U9+QX%l{R#asyG%wcG zX`SbA<(;1%+P{{>-tUq2}G-DX^@|aKYBJ%9o)EO}9=}qr45}6+l`jYv2)2 zBo>~zL}5#TFoeON7LniCt}lZZkwFpMVEK9o7Z_i&byv$eBxbsxPBOb8sc43l4-_z) z)E4nX*Z-24zap&64}(Y2>LTOSr4YFCQmt$PP*2)Msg04BsHl=D3lwu`p!s&myrIBQ z9w2v7Dt%1okXRJ=m#MUk6_#^O3i*bDlD#Djv=NPsfsZIXJ8z2aHI=)Uie(z186h_*|CnvvT=tM1r!hyN}q*n-Lf#NuGa}UVbEux!qw* z;cr8b${R4>Fg~L;7*}s7&ok(Ru2~$-RLfo^$Fd&7gP0nuORFKq7d3b$x{azOJZQFj@ zyU&*HAnB0wF^WXI=nJ3NqMa4K?o68^Xi7ql+faNWbf5ydi=4hLk+U|^B;;-fNm7p^ z;mNp*?B11j?`%YvRQ5eo_GJmWK%l|08|>fRgjo?e(r^vQ;ZpfG((-Gvlw*hl?jZty zQ@z3xcipCc?s_`!bJuBJJW?n&`VT^pG^t!Mo^D#=|nyvel;+Btt)7ky9vpNqD zR_E1~*GE_`^#nm}90qV~*1U6;9i2r)@-_wGxX1Gm0d2 zK44>Z66RKt2P^!YkmYqQTvNU{(AAXv5}=xtnZtCFJ5e(W^A&UY+YkkNBn7a%6ctc=bA` zm7w^h^=I$(_$geiv&<&Ix_9V>@u~4wPVnK6i~sZ;U~JZUu>T}`+xoVD-ZN3vi`;Jh E4`3sOX#fBK diff --git a/__pycache__/test_players.cpython-310.pyc b/__pycache__/test_players.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bc86aa2679a65a206c796dedf85cc1fd09b92bb1 GIT binary patch literal 16213 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 5 and not self.in_debt - and random.random() < 0.10): - self.in_trade = True - self.trade_props_offered = 0 - self.log("Initiating a trade!") - self.say_delayed("trade") - else: - self.say_delayed("roll") - self.rolled_this_turn = True + # Trade decision is deferred to -- Command: handler + # to avoid race conditions with delayed messages return # ============================================================ @@ -444,13 +436,22 @@ class PlayerBot: # COMMAND PROMPT # ============================================================ if msg == "-- Command:": - self.in_trade = False # trade ended (accepted, rejected, or cancelled) + self.in_trade = False # any active trade is over if self.is_my_turn(): if self.in_debt: self.say_delayed("mortgage") elif not self.rolled_this_turn: - self.say_delayed("roll") - self.rolled_this_turn = True + # ~10% chance to initiate a trade (after turn 5, not in debt/jail) + if (self.turns_played > 5 and not self.in_debt + and not self.in_jail + and random.random() < 0.10): + self.in_trade = True + self.trade_props_offered = 0 + self.log("Initiating a trade!") + self.say_delayed("trade") + else: + self.say_delayed("roll") + self.rolled_this_turn = True # else: already rolled, waiting for prompts to resolve return diff --git a/site/game-state.json b/site/game-state.json index cd5904f..1d38170 100644 --- a/site/game-state.json +++ b/site/game-state.json @@ -1,21 +1,11 @@ { "players": [ - { - "name": "charlie", - "number": 3, - "money": 985, - "location": 40, - "inJail": true, - "jailTurns": 1, - "doublesCount": 0, - "getOutOfJailFreeCards": 0 - }, { "name": "alice", "number": 1, - "money": 1375, - "location": 40, - "inJail": true, + "money": 1280, + "location": 21, + "inJail": false, "jailTurns": 0, "doublesCount": 0, "getOutOfJailFreeCards": 0 @@ -23,8 +13,18 @@ { "name": "bob", "number": 2, - "money": 800, - "location": 31, + "money": 1305, + "location": 16, + "inJail": false, + "jailTurns": 0, + "doublesCount": 0, + "getOutOfJailFreeCards": 0 + }, + { + "name": "charlie", + "number": 3, + "money": 1260, + "location": 13, "inJail": false, "jailTurns": 0, "doublesCount": 0, @@ -81,7 +81,7 @@ "id": 6, "name": "Oriental ave. (L)", "type": "property", - "owner": 2, + "owner": 3, "mortgaged": false, "group": "lightblue", "cost": 100, @@ -96,7 +96,7 @@ "id": 8, "name": "Vermont ave. (L)", "type": "property", - "owner": 1, + "owner": null, "mortgaged": false, "group": "lightblue", "cost": 100, @@ -121,7 +121,7 @@ "id": 11, "name": "St. Charles pl. (V)", "type": "property", - "owner": 3, + "owner": null, "mortgaged": false, "group": "violet", "cost": 140, @@ -131,7 +131,7 @@ "id": 12, "name": "Electric Co.", "type": "utility", - "owner": 2, + "owner": null, "mortgaged": false, "group": "utility", "cost": 150 @@ -140,7 +140,7 @@ "id": 13, "name": "States ave. (V)", "type": "property", - "owner": null, + "owner": 3, "mortgaged": false, "group": "violet", "cost": 140, @@ -169,7 +169,7 @@ "id": 16, "name": "St. James pl. (O)", "type": "property", - "owner": null, + "owner": 2, "mortgaged": false, "group": "orange", "cost": 180, @@ -194,7 +194,7 @@ "id": 19, "name": "New York ave. (O)", "type": "property", - "owner": 3, + "owner": null, "mortgaged": false, "group": "orange", "cost": 200, @@ -209,7 +209,7 @@ "id": 21, "name": "Kentucky ave. (R)", "type": "property", - "owner": null, + "owner": 1, "mortgaged": false, "group": "red", "cost": 220, @@ -244,7 +244,7 @@ "id": 25, "name": "B&O RR", "type": "railroad", - "owner": 3, + "owner": null, "mortgaged": false, "group": "railroad", "cost": 200 @@ -273,7 +273,7 @@ "id": 28, "name": "Water Works", "type": "utility", - "owner": 2, + "owner": null, "mortgaged": false, "group": "utility", "cost": 150 @@ -297,7 +297,7 @@ "id": 31, "name": "Pacific ave. (G)", "type": "property", - "owner": 2, + "owner": null, "mortgaged": false, "group": "green", "cost": 300, @@ -370,153 +370,99 @@ ], "log": [ { - "text": "Landed on Community Chest ii", + "text": "roll is 4, 6", "player": "alice", - "timestamp": "2026-02-21 10:41:53" + "timestamp": "2026-02-21 10:55:12" }, { - "text": "You are Assessed for street repairs.", - "player": "alice" + "text": "Landed on Just Visiting", + "player": "alice", + "timestamp": "2026-02-21 10:55:13" }, { - "text": "bob's turn \u2014 $1400 on Oriental ave. (L)", + "text": "bob's turn \u2014 $1500 on === GO ===", "player": "bob", - "timestamp": "2026-02-21 10:41:57" + "timestamp": "2026-02-21 10:55:14" }, { - "text": "roll is 1, 5", + "text": "roll is 5, 2", "player": "bob", - "timestamp": "2026-02-21 10:41:58" + "timestamp": "2026-02-21 10:55:15" }, { - "text": "Landed on Electric Co.", + "text": "Landed on Chance i", "player": "bob", - "timestamp": "2026-02-21 10:41:58" + "timestamp": "2026-02-21 10:55:15" }, { - "text": "charlie's turn \u2014 $1160 on New York ave. (O)", + "text": "Pay Poor Tax of $15", + "player": "bob" + }, + { + "text": "charlie's turn \u2014 $1500 on === GO ===", "player": "charlie", - "timestamp": "2026-02-21 10:42:00" + "timestamp": "2026-02-21 10:55:17" }, { "text": "roll is 3, 3", "player": "charlie", - "timestamp": "2026-02-21 10:42:01" + "timestamp": "2026-02-21 10:55:19" }, { - "text": "Landed on B&O RR", + "text": "Landed on Oriental ave. (L)", "player": "charlie", - "timestamp": "2026-02-21 10:42:01" + "timestamp": "2026-02-21 10:55:19" }, { - "text": "charlie's turn \u2014 $960 on B&O RR", + "text": "charlie's turn \u2014 $1400 on Oriental ave. (L)", "player": "charlie", - "timestamp": "2026-02-21 10:42:04" + "timestamp": "2026-02-21 10:55:22" }, { - "text": "roll is 4, 1", + "text": "roll is 1, 6", "player": "charlie", - "timestamp": "2026-02-21 10:42:05" + "timestamp": "2026-02-21 10:55:23" }, { - "text": "Landed on GO TO JAIL!", + "text": "Landed on States ave. (V)", "player": "charlie", - "timestamp": "2026-02-21 10:42:05" + "timestamp": "2026-02-21 10:55:23" }, { - "text": "alice's turn \u2014 $1400 on Community Chest ii", + "text": "alice's turn \u2014 $1500 on Just Visiting", "player": "alice", - "timestamp": "2026-02-21 10:42:06" + "timestamp": "2026-02-21 10:55:25" }, { - "text": "roll is 6, 2", + "text": "roll is 5, 6", "player": "alice", - "timestamp": "2026-02-21 10:42:07" + "timestamp": "2026-02-21 10:55:26" }, { - "text": "Landed on B&O RR", + "text": "Landed on Kentucky ave. (R)", "player": "alice", - "timestamp": "2026-02-21 10:42:08" + "timestamp": "2026-02-21 10:55:27" }, { - "text": "Paid $25 rent to charlie", - "player": "alice" - }, - { - "text": "bob's turn \u2014 $1250 on Electric Co.", + "text": "bob's turn \u2014 $1485 on Chance i", "player": "bob", - "timestamp": "2026-02-21 10:42:09" + "timestamp": "2026-02-21 10:55:29" }, { - "text": "roll is 4, 4", + "text": "roll is 3, 6", "player": "bob", - "timestamp": "2026-02-21 10:42:10" + "timestamp": "2026-02-21 10:55:30" }, { - "text": "Landed on Free Parking", + "text": "Landed on St. James pl. (O)", "player": "bob", - "timestamp": "2026-02-21 10:42:11" + "timestamp": "2026-02-21 10:55:30" }, { - "text": "bob's turn \u2014 $1250 on Free Parking", - "player": "bob", - "timestamp": "2026-02-21 10:42:12" - }, - { - "text": "roll is 6, 2", - "player": "bob", - "timestamp": "2026-02-21 10:42:13" - }, - { - "text": "Landed on Water Works", - "player": "bob", - "timestamp": "2026-02-21 10:42:14" - }, - { - "text": "charlie's turn \u2014 $985 on JAIL", + "text": "charlie's turn \u2014 $1260 on States ave. (V)", "player": "charlie", - "timestamp": "2026-02-21 10:42:16" - }, - { - "text": "roll is 6, 5", - "player": "charlie", - "timestamp": "2026-02-21 10:42:17" - }, - { - "text": "alice's turn \u2014 $1375 on B&O RR", - "player": "alice", - "timestamp": "2026-02-21 10:42:18" - }, - { - "text": "roll is 2, 3", - "player": "alice", - "timestamp": "2026-02-21 10:42:20" - }, - { - "text": "Landed on GO TO JAIL!", - "player": "alice", - "timestamp": "2026-02-21 10:42:20" - }, - { - "text": "bob's turn \u2014 $1100 on Water Works", - "player": "bob", - "timestamp": "2026-02-21 10:42:21" - }, - { - "text": "roll is 1, 2", - "player": "bob", - "timestamp": "2026-02-21 10:42:22" - }, - { - "text": "Landed on Pacific ave. (G)", - "player": "bob", - "timestamp": "2026-02-21 10:42:22" - }, - { - "text": "charlie's turn \u2014 $985 on JAIL", - "player": "charlie", - "timestamp": "2026-02-21 10:42:24" + "timestamp": "2026-02-21 10:55:32" } ], - "lastUpdated": "2026-02-21T10:42:27.015680+00:00" + "lastUpdated": "2026-02-21T10:55:32.750315+00:00" } \ No newline at end of file diff --git a/test_players.py b/test_players.py index 63fde51..7cfc8aa 100644 --- a/test_players.py +++ b/test_players.py @@ -115,8 +115,13 @@ class TestTurns(unittest.TestCase): return bot def test_checkpoint_triggers_roll(self): + """Checkpoint sets up turn, -- Command: triggers the roll.""" bot = self._make_bot("alice") msgs = bot.feed("alice (1) (cash $1500) on === GO ===") + self.assertEqual(msgs, [], "Checkpoint should not send commands") + # Roll happens at -- Command: + with patch("random.random", return_value=0.99): # no trade + msgs = bot.feed("-- Command:") self.assertIn("roll", msgs) self.assertTrue(bot.rolled_this_turn) @@ -224,11 +229,12 @@ class TestTrading(unittest.TestCase): @patch("monop_players.random") def test_trade_initiated(self, mock_random): - """With random < 0.10, bot initiates trade instead of rolling.""" + """With random < 0.10, bot initiates trade at -- Command:.""" mock_random.random.return_value = 0.05 # < 0.10 bot = self._make_bot("alice") bot.turns_played = 10 # past turn 5 - msgs = bot.feed("alice (1) (cash $1500) on === GO ===") + bot.feed("alice (1) (cash $1500) on === GO ===") + msgs = bot.feed("-- Command:") self.assertIn("trade", msgs) self.assertTrue(bot.in_trade) self.assertNotIn("roll", msgs) @@ -239,7 +245,8 @@ class TestTrading(unittest.TestCase): mock_random.random.return_value = 0.05 bot = self._make_bot("alice") bot.turns_played = 2 - msgs = bot.feed("alice (1) (cash $1500) on === GO ===") + bot.feed("alice (1) (cash $1500) on === GO ===") + msgs = bot.feed("-- Command:") self.assertIn("roll", msgs) self.assertFalse(bot.in_trade) @@ -332,21 +339,18 @@ class TestTradeInJail(unittest.TestCase): self.assertNotIn("roll", msgs, "Should not roll while in_trade is True") - def test_trade_then_jail_turn_sequence(self): - """Simulate the exact sequence: checkpoint initiates trade, then jail prompt arrives.""" + def test_no_trade_from_jail(self): + """Can't trade while in jail — must roll first.""" bot = self._make_bot() - # Force trade to trigger (patch random) - with patch("random.random", return_value=0.01): # < 0.10 → triggers trade - msgs = bot.feed("charlie (3) (cash $985) on JAIL") - self.assertTrue(bot.in_trade, "Trade should be initiated") - self.assertIn("trade", msgs, "Should send .trade") - self.assertNotIn("roll", msgs, "Should not also roll") - - # Now jail turn prompt arrives + msgs = bot.feed("charlie (3) (cash $985) on JAIL") msgs = bot.feed("(This is your 2nd turn in JAIL)") - self.assertNotIn("roll", msgs, - "Jail handler must not roll during active trade") - self.assertTrue(bot.in_trade, "Trade should still be active") + self.assertIn("roll", msgs, "Should roll in jail") + + # Even if random triggers, no trade while in_jail + with patch("random.random", return_value=0.01): + msgs = bot.feed("-- Command:") + self.assertFalse(bot.in_trade, "Should not trade while in jail") + self.assertIn("roll", msgs) def test_trade_which_player_prompt(self): """Bot should pick a trade partner when asked.""" @@ -371,16 +375,31 @@ class TestTradeInJail(unittest.TestCase): msgs = bot.feed("(This is your 2nd turn in JAIL)") self.assertIn("roll", msgs, "Should roll when not trading") - def test_command_prompt_after_trade_allows_roll(self): - """After trade ends (-- Command:), bot should roll normally.""" + def test_trade_done_triggers_roll(self): + """Trade is done! resets in_trade and triggers roll.""" bot = self._make_bot() bot.current_player = "charlie" bot.in_trade = True bot.rolled_this_turn = False - msgs = bot.feed("-- Command:") + msgs = bot.feed("Trade is done!") self.assertFalse(bot.in_trade) self.assertIn("roll", msgs) + def test_trade_decision_at_command_prompt(self): + """Trade/roll decision happens at -- Command:, not at checkpoint.""" + bot = self._make_bot() + # Checkpoint just records the turn + msgs = bot.feed("charlie (3) (cash $1500) on === GO ===") + self.assertNotIn("trade", msgs, "Checkpoint must not send trade") + self.assertNotIn("roll", msgs, "Checkpoint must not send roll") + + # -- Command: is where the decision happens + with patch("random.random", return_value=0.01): # triggers trade + msgs = bot.feed("-- Command:") + self.assertTrue(bot.in_trade) + self.assertIn("trade", msgs) + self.assertNotIn("roll", msgs) + class TestValidInputs(unittest.TestCase): def test_picks_first_option(self):