#!/usr/bin/env python3 """Unit tests for parser resignation handling — both to-player and to-bank.""" import sys import os import unittest sys.path.insert(0, os.path.dirname(__file__)) from monop_parser import MonopParser TS = "2026-01-01 00:00:00" def feed(parser, lines): """Feed tab-delimited lines (sender\\tmessage) into the parser.""" for line in lines: parser.parse_line(f"{TS}\t{line}") def setup_3player_game(): """Set up a 3-player game (merp, hiro, fbs) in playing phase. Uses the DarkScience-style setup that the parser actually handles.""" p = MonopParser() feed(p, [ # Start a new game "monop\tHow many players? ", # Setup: say me + rolls "monop\tPlayer 1, say 'me' please.", "monop\tmerp (1) rolls 5", "monop\tPlayer 2, say 'me' please.", "monop\thiro (2) rolls 3", "monop\tPlayer 3, say 'me' please.", "monop\tfbs (3) rolls 8", # fbs rolls highest, goes first "monop\tfbs (3) goes first", # First checkpoint to confirm playing state "monop\tfbs (3) (cash $1500) on === GO ===", "monop\t-- Command: ", ]) return p def give_properties(game, player_num, sq_ids): """Assign properties to a player in game state.""" for sq_id in sq_ids: game.property_owner[sq_id] = player_num class TestResignToBank(unittest.TestCase): """Test resignation to bank — properties become unowned.""" def _resign_merp_to_bank(self): """Set up game, give merp properties, resign to bank. Note: fbs goes first (highest roll). Turn order is fbs→merp→hiro. We need it to be merp's turn for the resign to work.""" p = setup_3player_game() g = p.game # Give merp some properties and houses/mortgages give_properties(g, 1, [1, 3, 5]) # Mediterranean, Baltic, Reading RR g.property_houses[1] = 2 g.property_mortgaged[3] = True # fbs rolls, then it's merp's turn feed(p, [ "fbs\t.", "monop\troll is 3, 4", "monop\tThat puts you on Oriental ave. (L)", "monop\tThat would cost $100, do you want it? ", "fbs\t.n", "monop\tmerp (1) (cash $1500) on === GO ===", "monop\t-- Command: ", # Now merp resigns to bank "merp\t.resign", "monop\tWho do you wish to resign to? ", "merp\t.bank", "monop\tDo you really want to resign? ", "merp\t.y", "monop\tresigning to bank", "monop\thiro (1) (cash $1500) on === GO ===", "monop\t-- Command: ", ]) return p def test_player_removed_from_active(self): p = self._resign_merp_to_bank() state = p.get_state() active_names = [pl["name"] for pl in state["players"] if not pl.get("bankrupt")] self.assertNotIn("merp", active_names) self.assertEqual(len(active_names), 2) def test_player_marked_bankrupt(self): p = self._resign_merp_to_bank() state = p.get_state() bankrupt = [pl for pl in state["players"] if pl.get("bankrupt")] self.assertEqual(len(bankrupt), 1) self.assertEqual(bankrupt[0]["name"], "merp") def test_properties_cleared(self): p = self._resign_merp_to_bank() g = p.game for sq_id in [1, 3, 5]: self.assertNotIn(sq_id, g.property_owner, f"Property {sq_id} should be unowned after resign to bank") def test_houses_cleared(self): p = self._resign_merp_to_bank() self.assertNotIn(1, p.game.property_houses) def test_mortgages_cleared(self): p = self._resign_merp_to_bank() self.assertNotIn(3, p.game.property_mortgaged) def test_remaining_players_renumbered(self): p = self._resign_merp_to_bank() state = p.get_state() active = [pl for pl in state["players"] if not pl.get("bankrupt")] names = {pl["name"]: pl["number"] for pl in active} # After merp removed, remaining get renumbered 1, 2 self.assertIn("hiro", names) self.assertIn("fbs", names) def test_log_entry(self): p = self._resign_merp_to_bank() state = p.get_state() log_texts = [e["text"] for e in state["log"]] self.assertTrue(any("merp" in t and "bank" in t for t in log_texts), f"Expected resign-to-bank log entry, got: {log_texts}") class TestResignToPlayer(unittest.TestCase): """Test resignation to another player — assets transferred.""" def _resign_merp_to_fbs(self, extra_setup=None): """merp resigns to fbs. Optional extra_setup callback for custom state.""" p = setup_3player_game() g = p.game # Give merp properties and money give_properties(g, 1, [1, 3, 5]) merp = g.get_player(name="merp") merp.money = 800 merp.get_out_of_jail_free_cards = 1 if extra_setup: extra_setup(p) # fbs turn, then merp's turn feed(p, [ "fbs\t.", "monop\troll is 3, 4", "monop\tThat puts you on Oriental ave. (L)", "monop\tThat would cost $100, do you want it? ", "fbs\t.n", "monop\tmerp (1) (cash $800) on === GO ===", "monop\t-- Command: ", # merp resigns — auto-target since negative money scenario uses "You would resign to" "monop\tYou would resign to fbs", "monop\tDo you really want to resign? ", "merp\t.y", "monop\tresigning to player", "monop\tTrade is done!", "monop\thiro (1) (cash $1500) on === GO ===", "monop\t-- Command: ", ]) return p def test_player_marked_bankrupt(self): p = self._resign_merp_to_fbs() state = p.get_state() bankrupt = [pl for pl in state["players"] if pl.get("bankrupt")] self.assertEqual(len(bankrupt), 1) self.assertEqual(bankrupt[0]["name"], "merp") def test_money_transferred(self): p = self._resign_merp_to_fbs() fbs = p.game.get_player(name="fbs") self.assertIsNotNone(fbs) # fbs started with 1500, gets merp's 800 self.assertEqual(fbs.money, 1500 + 800) def test_gojf_transferred(self): p = self._resign_merp_to_fbs() fbs = p.game.get_player(name="fbs") self.assertEqual(fbs.get_out_of_jail_free_cards, 1) def test_properties_transferred(self): p = self._resign_merp_to_fbs() g = p.game fbs = g.get_player(name="fbs") for sq_id in [1, 3, 5]: self.assertEqual(g.property_owner.get(sq_id), fbs.number, f"Property {sq_id} should belong to fbs (#{fbs.number})") def test_other_player_properties_preserved(self): """hiro's properties survive with correct renumbered owner.""" def setup(parser): give_properties(parser.game, 2, [11, 13]) # hiro owns St Charles, States p = self._resign_merp_to_fbs(extra_setup=setup) g = p.game hiro = g.get_player(name="hiro") self.assertIsNotNone(hiro, "hiro should still be in the game") for sq_id in [11, 13]: self.assertEqual(g.property_owner.get(sq_id), hiro.number, f"Property {sq_id} should belong to hiro (#{hiro.number})") def test_log_entry(self): p = self._resign_merp_to_fbs() state = p.get_state() log_texts = [e["text"] for e in state["log"]] self.assertTrue(any("merp" in t and "fbs" in t for t in log_texts), f"Expected resign-to-fbs log entry, got: {log_texts}") class TestResignWithChosenTarget(unittest.TestCase): """Player with positive money chooses who to resign to via 'Who do you wish to resign to?'""" def test_resign_choose_hiro(self): p = setup_3player_game() give_properties(p.game, 1, [1, 3]) feed(p, [ # fbs turn "fbs\t.", "monop\troll is 3, 4", "monop\tThat puts you on Oriental ave. (L)", "monop\tThat would cost $100, do you want it? ", "fbs\t.n", "monop\tmerp (1) (cash $1500) on === GO ===", "monop\t-- Command: ", # merp chooses to resign to hiro "merp\t.resign", "monop\tWho do you wish to resign to? ", "merp\t.hiro", "monop\tDo you really want to resign? ", "merp\t.y", "monop\tresigning to player", "monop\tTrade is done!", "monop\thiro (1) (cash $3000) on === GO ===", "monop\t-- Command: ", ]) g = p.game hiro = g.get_player(name="hiro") self.assertIsNotNone(hiro) # Properties transferred to hiro for sq_id in [1, 3]: self.assertEqual(g.property_owner.get(sq_id), hiro.number) # Money transferred (1500 + 1500) self.assertEqual(hiro.money, 3000) class TestResignGameOver(unittest.TestCase): """Resignations leading to game over.""" def test_last_two_players_one_resigns(self): """In 3-player game: merp resigns, then hiro resigns → fbs wins.""" p = setup_3player_game() feed(p, [ # fbs turn, then merp's "fbs\t.", "monop\troll is 3, 4", "monop\tThat puts you on Oriental ave. (L)", "monop\tThat would cost $100, do you want it? ", "fbs\t.n", "monop\tmerp (1) (cash $1500) on === GO ===", "monop\t-- Command: ", # merp resigns to bank "merp\t.resign", "monop\tWho do you wish to resign to? ", "merp\t.bank", "monop\tDo you really want to resign? ", "merp\t.y", "monop\tresigning to bank", "monop\thiro (1) (cash $1500) on === GO ===", "monop\t-- Command: ", # hiro's turn, rolls "hiro\t.", "monop\troll is 2, 3", "monop\tThat puts you on Reading RR", "monop\tThat would cost $200, do you want it? ", "hiro\t.n", "monop\tfbs (2) (cash $1500) on Oriental ave. (L)", "monop\t-- Command: ", # fbs turn, then hiro again "fbs\t.", "monop\troll is 1, 2", "monop\tThat puts you on Connecticut ave. (L)", "monop\tThat would cost $120, do you want it? ", "fbs\t.n", "monop\thiro (1) (cash $1500) on Reading RR", "monop\t-- Command: ", # hiro resigns to fbs "monop\tYou would resign to fbs", "monop\tDo you really want to resign? ", "hiro\t.y", "monop\tresigning to player", "monop\tTrade is done!", "monop\tThen fbs WINS!!!!!", ]) state = p.get_state() self.assertEqual(state.get("phase"), "over") bankrupt = [pl for pl in state["players"] if pl.get("bankrupt")] self.assertEqual(len(bankrupt), 2) active = [pl for pl in state["players"] if not pl.get("bankrupt")] self.assertEqual(len(active), 1) self.assertEqual(active[0]["name"], "fbs") class TestLogReplayResignations(unittest.TestCase): """Replay real log segments involving resignations.""" def _replay_to_line(self, end_line): p = MonopParser() with open("test_data/monop.log") as f: for i, line in enumerate(f, 1): p.parse_line(line.rstrip('\n')) if i >= end_line: break return p def test_merp_resigns_to_fbs_line_1012(self): """Real log: merp resigns to fbs around line 1012.""" p = self._replay_to_line(1020) state = p.get_state() self.assertIsNotNone(state) bankrupt_names = [pl["name"] for pl in state["players"] if pl.get("bankrupt")] self.assertIn("merp", bankrupt_names) active_names = [pl["name"] for pl in state["players"] if not pl.get("bankrupt")] self.assertIn("hiro", active_names) self.assertIn("fbs", active_names) def test_xlink_resigns_to_fbs_line_1245(self): """Real log: xLink resigns to fbs around line 1245.""" p = self._replay_to_line(1250) state = p.get_state() self.assertIsNotNone(state) bankrupt_names = [pl["name"] for pl in state["players"] if pl.get("bankrupt")] self.assertIn("xLink", bankrupt_names) def test_derecho_resigns_to_bank_line_6874(self): """Real log: Derecho resigns to bank around line 6874.""" p = self._replay_to_line(6880) state = p.get_state() self.assertIsNotNone(state) # Should not crash — that's the main assertion # Check bankrupt list includes someone bankrupt_names = [pl["name"] for pl in state["players"] if pl.get("bankrupt")] self.assertTrue(len(bankrupt_names) > 0, "Expected at least one bankrupt player by line 6880") def test_whoami_resigns_to_bank_line_10729(self): """Real log: whoami resigns to bank around line 10729.""" p = self._replay_to_line(10735) state = p.get_state() self.assertIsNotNone(state) if __name__ == "__main__": unittest.main(verbosity=2)