Add unit tests for autopilot players (32 tests), fix trade force=True and duplicate handler
This commit is contained in:
parent
0af3184590
commit
c8e5a83010
3 changed files with 345 additions and 9 deletions
BIN
__pycache__/monop_players.cpython-310.pyc
Normal file
BIN
__pycache__/monop_players.cpython-310.pyc
Normal file
Binary file not shown.
|
|
@ -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
|
||||
|
|
|
|||
337
test_players.py
Normal file
337
test_players.py
Normal file
|
|
@ -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)
|
||||
Loading…
Reference in a new issue