diff --git a/__pycache__/monop_players.cpython-310.pyc b/__pycache__/monop_players.cpython-310.pyc index c403b2e..1933d65 100644 Binary files a/__pycache__/monop_players.cpython-310.pyc and b/__pycache__/monop_players.cpython-310.pyc differ diff --git a/__pycache__/test_players.cpython-310.pyc b/__pycache__/test_players.cpython-310.pyc new file mode 100644 index 0000000..bc86aa2 Binary files /dev/null and b/__pycache__/test_players.cpython-310.pyc differ diff --git a/monop_players.py b/monop_players.py index 825b488..2de5acc 100644 --- a/monop_players.py +++ b/monop_players.py @@ -224,16 +224,8 @@ class PlayerBot: if self.is_my_turn(): self.turns_played += 1 - # ~10% chance to initiate a trade (after turn 5, not in debt) - if (self.turns_played > 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):