diff --git a/__pycache__/monop_players.cpython-310.pyc b/__pycache__/monop_players.cpython-310.pyc new file mode 100644 index 0000000..d4c77b4 Binary files /dev/null and b/__pycache__/monop_players.cpython-310.pyc differ 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)