#!/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 TestTradeInJail(unittest.TestCase): """Regression tests for trade-while-in-jail bug. Sequence that caused the hang: 1. Checkpoint "charlie (3) (cash $985) on JAIL" → trade initiated 2. "(This is your 2nd turn in JAIL)" → roll queued (ignoring in_trade) 3. ".trade" sent, then ".roll" sent into trade prompt → hang """ def _make_bot(self, nick="charlie"): bot = FakePlayerBot(nick, ["alice", "bob", "charlie"], 2) bot.setup_phase = False bot.game_started = True bot.turns_played = 10 # past trade threshold return bot def test_jail_handler_no_roll_during_trade(self): """Jail turn handler must not queue a roll when in_trade is True.""" bot = self._make_bot() bot.current_player = "charlie" bot.in_trade = True bot.in_jail = True msgs = bot.feed("(This is your 2nd turn in JAIL)") 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.""" 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("(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") def test_trade_which_player_prompt(self): """Bot should pick a trade partner when asked.""" bot = self._make_bot() bot.current_player = "charlie" bot.in_trade = True msgs = bot.feed("Which player do you wish to trade with?") self.assertTrue(len(msgs) > 0, "Should respond to trade partner prompt") self.assertNotIn("roll", msgs) # Should pick one of the other players self.assertTrue( msgs[0] in ["alice", "bob"], f"Expected a player name, got: {msgs[0]}" ) def test_no_trade_in_jail_still_rolls(self): """When NOT in a trade, jail handler should still queue a roll.""" bot = self._make_bot() bot.current_player = "charlie" bot.in_trade = False bot.in_jail = True 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.""" bot = self._make_bot() bot.current_player = "charlie" 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)