From c8e5a830103a3dafe9a37d1f560a31f04f676550 Mon Sep 17 00:00:00 2001 From: Jarvis Date: Sat, 21 Feb 2026 09:53:09 +0000 Subject: [PATCH] Add unit tests for autopilot players (32 tests), fix trade force=True and duplicate handler --- __pycache__/monop_players.cpython-310.pyc | Bin 0 -> 12665 bytes monop_players.py | 17 +- test_players.py | 337 ++++++++++++++++++++++ 3 files changed, 345 insertions(+), 9 deletions(-) create mode 100644 __pycache__/monop_players.cpython-310.pyc create mode 100644 test_players.py diff --git a/__pycache__/monop_players.cpython-310.pyc b/__pycache__/monop_players.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d4c77b4b217f780baea9c9e1484f9d1912b1aeda GIT binary patch literal 12665 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 Bq{09I literal 0 HcmV?d00001 diff --git a/monop_players.py b/monop_players.py index 8feaaec..823bebf 100644 --- a/monop_players.py +++ b/monop_players.py @@ -361,8 +361,7 @@ class PlayerBot: # We don't trade proactively — shouldn't hit this return - if msg.startswith("Which property do you wish to trade?"): - return + # (Trade property prompt handled in TRADING section below) # ============================================================ # DEBT / FORCED MORTGAGE @@ -528,9 +527,9 @@ class PlayerBot: if self.trade_props_offered == 0 and random.random() < 0.5: # Offer one property then done self.trade_props_offered = 1 - self.say_delayed("?") # get the list + self.say_delayed("?", force=True) # get the list else: - self.say_delayed("done") + self.say_delayed("done", force=True) return # "You have $X. How much are you trading?" — offer some cash @@ -540,20 +539,20 @@ class PlayerBot: cash = int(m.group(1)) # Offer 0-25% of cash randomly offer = random.randint(0, max(1, cash // 4)) - self.say_delayed(str(offer)) + self.say_delayed(str(offer), force=True) return # "You have N get-out-of-jail-free cards. How many are you trading?" m = re.match(r'^You have (\d+) get-out-of-jail-free card', msg) if m: if self.in_trade: - self.say_delayed("0") + self.say_delayed("0", force=True) return # "You've already allocated that." if msg == "You've already allocated that.": if self.in_trade: - self.say_delayed("done") + self.say_delayed("done", force=True) return # "{name}, is the trade ok?" — 50/50 accept or reject @@ -563,10 +562,10 @@ class PlayerBot: if name.lower() == self.nick.lower(): if random.random() < 0.5: self.log("Accepting trade!") - self.say_delayed("yes") + self.say_delayed("yes", force=True) else: self.log("Rejecting trade!") - self.say_delayed("no") + self.say_delayed("no", force=True) return # "Trade is done!" — trade completed diff --git a/test_players.py b/test_players.py new file mode 100644 index 0000000..112f21e --- /dev/null +++ b/test_players.py @@ -0,0 +1,337 @@ +#!/usr/bin/env python3 +"""Unit tests for monop_players.py — test bot responses to monop messages.""" + +import sys +import unittest +from unittest.mock import MagicMock, patch +import time + +# Import PlayerBot directly +sys.path.insert(0, "/tmp/monop-state") +from monop_players import PlayerBot + + +class FakePlayerBot(PlayerBot): + """PlayerBot that captures messages instead of sending to IRC.""" + + def __init__(self, nick, player_names, player_index): + # Don't call super().__init__ — skip socket stuff + self.nick = nick + self.channel = "#monop" + self.host = "127.0.0.1" + self.port = 6667 + self.player_names = player_names + self.player_index = player_index + self.num_players = len(player_names) + + self.sock = None + self.buffer = "" + self.lock = __import__("threading").Lock() + + # Game state + self.setup_phase = True + self.setup_registrations_seen = 0 + self.current_player = None + self.my_money = 1500 + self.in_jail = False + self.jail_turns = 0 + self.in_debt = False + self.in_auction = False + self.auction_bid = 0 + self.awaiting_prompt = None + self.game_started = False + self.game_over = False + self.rolled_this_turn = False + self.my_properties = [] + self.mortgaged = set() + self._prompt_answered = False + self._first_player_announced = False + self.in_trade = False + self.trade_props_offered = 0 + self.turns_played = 0 + + # Capture sent messages instead of IRC + self.sent_messages = [] + + def say(self, msg): + self.sent_messages.append(msg) + + def say_delayed(self, msg, delay=None, force=False): + """Immediate version — no threading, no delay.""" + if force or self.is_my_turn(): + self.sent_messages.append(msg) + + def feed(self, msg): + """Simulate receiving a message from the monop bot.""" + self.sent_messages.clear() + self._handle_bot_msg(msg) + return list(self.sent_messages) + + def feed_setup(self, msg): + """Feed a setup-phase message.""" + self.sent_messages.clear() + result = self._handle_setup(msg) + return list(self.sent_messages), result + + +class TestSetup(unittest.TestCase): + def test_player_count(self): + bot = FakePlayerBot("alice", ["alice", "bob"], 0) + msgs, handled = bot.feed_setup("How many players?") + self.assertTrue(handled) + self.assertEqual(msgs, ["2"]) + + def test_player_count_only_first(self): + bot = FakePlayerBot("bob", ["alice", "bob"], 1) + msgs, handled = bot.feed_setup("How many players?") + self.assertTrue(handled) + self.assertEqual(msgs, []) # bob doesn't send count + + def test_register_correct_player(self): + bot = FakePlayerBot("alice", ["alice", "bob"], 0) + msgs, handled = bot.feed_setup("Player 1, say ''me'' please.") + self.assertTrue(handled) + self.assertEqual(msgs, ["alice"]) + + def test_register_wrong_player(self): + bot = FakePlayerBot("alice", ["alice", "bob"], 0) + msgs, handled = bot.feed_setup("Player 2, say ''me'' please.") + self.assertTrue(handled) + self.assertEqual(msgs, []) # alice doesn't respond to player 2 + + def test_goes_first(self): + bot = FakePlayerBot("alice", ["alice", "bob"], 0) + msgs, handled = bot.feed_setup("alice (1) goes first") + self.assertTrue(handled) + self.assertFalse(bot.setup_phase) + self.assertEqual(bot.current_player, "alice") + + +class TestTurns(unittest.TestCase): + def _make_bot(self, nick="alice", current_player=None): + bot = FakePlayerBot(nick, ["alice", "bob"], 0 if nick == "alice" else 1) + bot.setup_phase = False + bot.current_player = current_player + return bot + + def test_checkpoint_triggers_roll(self): + bot = self._make_bot("alice") + msgs = bot.feed("alice (1) (cash $1500) on === GO ===") + self.assertIn("roll", msgs) + self.assertTrue(bot.rolled_this_turn) + + def test_checkpoint_other_player_no_roll(self): + bot = self._make_bot("alice") + msgs = bot.feed("bob (2) (cash $1500) on === GO ===") + self.assertEqual(msgs, []) + self.assertEqual(bot.current_player, "bob") + + def test_doubles_roll_again(self): + bot = self._make_bot("alice", "alice") + msgs = bot.feed("alice rolled doubles. Goes again") + self.assertIn("roll", msgs) + + def test_buy_prompt(self): + bot = self._make_bot("alice", "alice") + msgs = bot.feed("Do you want to buy?") + self.assertIn("yes", msgs) + + def test_buy_prompt_not_my_turn(self): + bot = self._make_bot("alice", "bob") + msgs = bot.feed("Do you want to buy?") + self.assertEqual(msgs, []) + + def test_command_prompt_rolls_if_needed(self): + bot = self._make_bot("alice", "alice") + bot.rolled_this_turn = False + msgs = bot.feed("-- Command:") + self.assertIn("roll", msgs) + + def test_command_prompt_no_double_roll(self): + bot = self._make_bot("alice", "alice") + bot.rolled_this_turn = True + msgs = bot.feed("-- Command:") + self.assertEqual(msgs, []) + + +class TestJail(unittest.TestCase): + def test_jail_turn_rolls(self): + bot = FakePlayerBot("alice", ["alice", "bob"], 0) + bot.setup_phase = False + bot.current_player = "alice" + msgs = bot.feed("(This is your 1st turn in JAIL)") + self.assertIn("roll", msgs) + self.assertTrue(bot.in_jail) + + +class TestDebt(unittest.TestCase): + def test_debt_mortgages(self): + bot = FakePlayerBot("alice", ["alice", "bob"], 0) + bot.setup_phase = False + bot.current_player = "alice" + msgs = bot.feed("How are you going to fix it up?") + self.assertIn("mortgage", msgs) + self.assertTrue(bot.in_debt) + + def test_mortgage_prompt_sends_question(self): + bot = FakePlayerBot("alice", ["alice", "bob"], 0) + bot.setup_phase = False + bot.current_player = "alice" + bot.in_debt = True + msgs = bot.feed("Which property do you want to mortgage?") + self.assertIn("?", msgs) + + def test_mortgage_prompt_done_when_solvent(self): + bot = FakePlayerBot("alice", ["alice", "bob"], 0) + bot.setup_phase = False + bot.current_player = "alice" + bot.in_debt = False + msgs = bot.feed("Which property do you want to mortgage?") + self.assertIn("done", msgs) + + def test_no_property_sells_houses(self): + bot = FakePlayerBot("alice", ["alice", "bob"], 0) + bot.setup_phase = False + bot.current_player = "alice" + bot.in_debt = True + msgs = bot.feed("You don't have any un-mortgaged property.") + self.assertIn("sell houses", msgs) + + def test_no_houses_resigns(self): + bot = FakePlayerBot("alice", ["alice", "bob"], 0) + bot.setup_phase = False + bot.current_player = "alice" + bot.in_debt = True + msgs = bot.feed("You don't have any houses to sell!!") + self.assertIn("resign", msgs) + + +class TestTax(unittest.TestCase): + def test_tax_choice(self): + bot = FakePlayerBot("alice", ["alice", "bob"], 0) + bot.setup_phase = False + bot.current_player = "alice" + msgs = bot.feed("Do you wish to lose 10%% of your total worth or $200? ") + self.assertIn("10%", msgs) + + +class TestTrading(unittest.TestCase): + def _make_bot(self, nick="alice"): + bot = FakePlayerBot(nick, ["alice", "bob"], 0 if nick == "alice" else 1) + bot.setup_phase = False + bot.current_player = "alice" + return bot + + @patch("monop_players.random") + def test_trade_initiated(self, mock_random): + """With random < 0.10, bot initiates trade instead of rolling.""" + 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 ===") + self.assertIn("trade", msgs) + self.assertTrue(bot.in_trade) + self.assertNotIn("roll", msgs) + + @patch("monop_players.random") + def test_trade_not_initiated_early(self, mock_random): + """No trades before turn 5.""" + 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 ===") + self.assertIn("roll", msgs) + self.assertFalse(bot.in_trade) + + def test_trade_property_prompt_sends_done(self): + bot = self._make_bot("alice") + bot.in_trade = True + bot.trade_props_offered = 1 # already offered one + msgs = bot.feed("Which property do you wish to trade?") + self.assertIn("done", msgs) + + def test_trade_cash_prompt(self): + bot = self._make_bot("alice") + bot.in_trade = True + msgs = bot.feed("You have $1500. How much are you trading?") + self.assertEqual(len(msgs), 1) + amount = int(msgs[0]) + self.assertGreaterEqual(amount, 0) + self.assertLessEqual(amount, 375) # max 25% of 1500 + + def test_trade_gojf_prompt(self): + bot = self._make_bot("alice") + bot.in_trade = True + msgs = bot.feed("You have 2 get-out-of-jail-free cards. How many are you trading?") + self.assertEqual(msgs, ["0"]) + + @patch("monop_players.random") + def test_trade_accepted(self, mock_random): + mock_random.random.return_value = 0.3 # < 0.5 → accept + bot = self._make_bot("alice") + bot.in_trade = True + # alice is asked to confirm (she's the tradee) + bot.current_player = "bob" # bob initiated + msgs = bot.feed("alice, is the trade ok?") + self.assertIn("yes", msgs) + + @patch("monop_players.random") + def test_trade_rejected(self, mock_random): + mock_random.random.return_value = 0.7 # >= 0.5 → reject + bot = self._make_bot("alice") + bot.in_trade = True + bot.current_player = "bob" + msgs = bot.feed("alice, is the trade ok?") + self.assertIn("no", msgs) + + def test_trade_done_resets(self): + bot = self._make_bot("alice") + bot.in_trade = True + msgs = bot.feed("Trade is done!") + self.assertFalse(bot.in_trade) + + def test_trade_nobody_around(self): + bot = self._make_bot("alice") + bot.in_trade = True + msgs = bot.feed("There ain't no-one around to trade WITH!!") + self.assertFalse(bot.in_trade) + self.assertIn("roll", msgs) + + def test_command_prompt_resets_trade(self): + bot = self._make_bot("alice") + bot.in_trade = True + bot.rolled_this_turn = False + msgs = bot.feed("-- Command:") + self.assertFalse(bot.in_trade) + self.assertIn("roll", msgs) + + +class TestValidInputs(unittest.TestCase): + def test_picks_first_option(self): + bot = FakePlayerBot("alice", ["alice", "bob"], 0) + bot.setup_phase = False + bot.current_player = "alice" + msgs = bot.feed("Valid inputs are: Mediterranean ave. (P), Baltic ave. (P), done") + self.assertIn("Mediterranean ave. (P)", msgs) + + def test_picks_done(self): + bot = FakePlayerBot("alice", ["alice", "bob"], 0) + bot.setup_phase = False + bot.current_player = "alice" + msgs = bot.feed("Valid inputs are: done") + self.assertIn("done", msgs) + + +class TestBadPlayer(unittest.TestCase): + def test_turn_correction(self): + bot = FakePlayerBot("alice", ["alice", "bob"], 0) + bot.setup_phase = False + bot.current_player = "bob" + msgs = bot.feed("Illegal action: bad player (alice's turn, not bob)") + self.assertEqual(bot.current_player, "alice") + # alice should roll + self.assertIn("roll", msgs) + + +if __name__ == "__main__": + unittest.main(verbosity=2)