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.
This commit is contained in:
parent
8bbadba7d9
commit
7a4346a53f
5 changed files with 119 additions and 153 deletions
Binary file not shown.
BIN
__pycache__/test_players.cpython-310.pyc
Normal file
BIN
__pycache__/test_players.cpython-310.pyc
Normal file
Binary file not shown.
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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):
|
||||
|
|
|
|||
Loading…
Reference in a new issue