#!/usr/bin/env python3 """ Comprehensive integration test: replay a synthetic game that exercises every fixed bug, then verify final state + take screenshots. Bugs exercised: #1 Trade property transfer #2 UI owner color (visual — screenshot) #5 Resign-to-bank clears properties #6 Resign-to-player transfers properties #7 Property owner renumbering after resign #8 spec flag cleared after rent #9 phase field always emitted #10 Resign/trade log timestamps #11 House counts from rent lines New: House buying via sub-parser New: Holdings display resync New: GOJF card command New: Mortgage/unmortgage tracking """ import sys import os import json import unittest sys.path.insert(0, os.path.dirname(__file__)) from monop_parser import MonopParser, BOARD TS = "2026-01-01 00:{:02d}:{:02d}" _t = [0] def ts(): _t[0] += 1 return TS.format(_t[0] // 60, _t[0] % 60) def feed(p, lines): for line in lines: p.parse_line(f"{ts()}\t{line}") def setup_game(): """3-player game: alice, bob, charlie. charlie goes first.""" p = MonopParser() feed(p, [ "monop\tHow many players? ", "monop\tPlayer 1, say 'me' please.", "monop\talice (1) rolls 3", "monop\tPlayer 2, say 'me' please.", "monop\tbob (2) rolls 5", "monop\tPlayer 3, say 'me' please.", "monop\tcharlie (3) rolls 9", "monop\tcharlie (3) goes first", "monop\tcharlie (3) (cash $1500) on === GO ===", "monop\t-- Command: ", ]) return p class TestFullGameIntegration(unittest.TestCase): def test_full_game_flow(self): p = setup_game() g = p.game # === Phase field (Bug #9) === state = p.get_state() self.assertIn("phase", state) self.assertEqual(state["phase"], "playing") # === charlie's turn: buy Mediterranean === feed(p, [ "charlie\t.", "monop\troll is 1, 0", "monop\tThat puts you on Mediterranean ave. (P)", "monop\tThat would cost $60", "monop\tDo you want to buy? ", "charlie\t.y", "monop\talice (1) (cash $1500) on === GO ===", "monop\t-- Command: ", ]) self.assertEqual(g.property_owner.get(1), 3) # charlie is #3 # === alice's turn: buy Baltic === feed(p, [ "alice\t.", "monop\troll is 1, 2", "monop\tThat puts you on Baltic ave. (P)", "monop\tThat would cost $60", "monop\tDo you want to buy? ", "alice\t.y", "monop\tbob (2) (cash $1500) on === GO ===", "monop\t-- Command: ", ]) self.assertEqual(g.property_owner.get(3), 1) # alice is #1 # === bob's turn: buy Reading RR === feed(p, [ "bob\t.", "monop\troll is 2, 3", "monop\tThat puts you on Reading RR", "monop\tThat would cost $200", "monop\tDo you want to buy? ", "bob\t.y", "monop\tcharlie (3) (cash $1440) on Mediterranean ave. (P)", "monop\t-- Command: ", ]) self.assertEqual(g.property_owner.get(5), 2) # bob is #2 # === charlie buys Oriental, Vermont, Connecticut (full lightblue) === feed(p, [ "charlie\t.", "monop\troll is 3, 2", "monop\tThat puts you on Oriental ave. (L)", "monop\tThat would cost $100", "monop\tDo you want to buy? ", "charlie\t.y", "monop\talice (1) (cash $1440) on Baltic ave. (P)", "monop\t-- Command: ", ]) g.property_owner[8] = 3 # Vermont (simulate) g.property_owner[9] = 3 # Connecticut (simulate) # === charlie buys houses on lightblue (Bug: house sub-parser) === feed(p, [ "charlie\t.buy", "monop\tOriental ave. (L) (0) Vermont ave. (L) (0) Connecticut ave. (L) (0) ", "monop\tHouses will cost $50", "monop\tHow many houses do you wish to buy for", "monop\tOriental ave. (L) (0): ", "charlie\t.2", "monop\tVermont ave. (L) (0): ", "charlie\t.2", "monop\tConnecticut ave. (L) (0): ", "charlie\t.2", "monop\tYou asked for 6 houses for $300", "monop\tIs that ok? ", "charlie\t.y", "monop\tcharlie (3) (cash $1040) on Oriental ave. (L)", "monop\t-- Command: ", ]) self.assertEqual(g.property_houses.get(6), 2) # Oriental 2 houses self.assertEqual(g.property_houses.get(8), 2) # Vermont 2 houses self.assertEqual(g.property_houses.get(9), 2) # Connecticut 2 houses # === alice lands on Oriental, pays rent with houses (Bug #11) === feed(p, [ "alice\t.", "monop\troll is 2, 1", "monop\tThat puts you on Oriental ave. (L)", "monop\tOwned by charlie", "monop\twith 2 houses, rent is 30", "monop\tbob (2) (cash $1300) on Reading RR", "monop\t-- Command: ", ]) # Rent paid — house count confirmed from rent line (Bug #11) self.assertEqual(g.property_houses.get(6), 2) # still 2 houses # === Trade: bob gives Reading RR to charlie for $300 (Bug #1) === feed(p, [ "monop\tPlayer bob (2) gives:", "monop\t Reading RR 2 200 1", "monop\tPlayer charlie (3) gives:", "monop\t $300", "monop\tcharlie, is the trade ok? ", "charlie\t.y", "monop\tTrade is done!", "monop\tbob (2) (cash $1600) on Reading RR", "monop\t-- Command: ", ]) # Reading RR should be charlie's now self.assertEqual(g.property_owner.get(5), 3, "Bug #1: Trade should transfer property") bob = g.get_player(name="bob") self.assertEqual(bob.money, 1600) # Verify trade log has timestamp (Bug #10) trade_logs = [e for e in g.log if "Trade completed" in e.get("text", "")] self.assertTrue(len(trade_logs) > 0) self.assertIn("timestamp", trade_logs[-1], "Bug #10: Trade log should have timestamp") # === Advance to alice's turn === feed(p, [ "bob\t.", "monop\troll is 1, 1", "monop\tThat puts you on Vermont ave. (L)", "monop\tOwned by charlie", "monop\twith 2 houses, rent is 30", "monop\tcharlie (3) (cash $800) on Oriental ave. (L)", "monop\t-- Command: ", # Bob rolled doubles, goes again "monop\tbob rolled doubles. Goes again", "bob\t.", "monop\troll is 3, 2", "monop\tThat puts you on States ave. (V)", "monop\tThat is a safe place", "monop\talice (1) (cash $1140) on Oriental ave. (L)", "monop\t-- Command: ", ]) # === alice mortgages Baltic (New: mortgage tracking) === feed(p, [ "alice\t.mor", "monop\tWhich property do you want to mortgage? ", "alice\t.baltic", "monop\tThat got you $30", "monop\talice (1) (cash $1170) on Oriental ave. (L)", "monop\t-- Command: ", ]) self.assertTrue(g.property_mortgaged.get(3), "Mortgage flag should be set") # === alice unmortgages Baltic (New: unmortgage tracking) === feed(p, [ "alice\t.unm", "monop\tYour only mortaged property is Baltic ave. (P)", "monop\tDo you want to unmortgage it? ", "alice\t.y", "monop\tThat cost you $33", "monop\talice (1) (cash $1137) on Oriental ave. (L)", "monop\t-- Command: ", ]) self.assertFalse(g.property_mortgaged.get(3, False), "Unmortgage should clear flag") # === charlie shows holdings (New: holdings resync) === feed(p, [ "monop\tcharlie's (3) holdings (Total worth: $3000):", "monop\t Name Own Price Mg # Rent", "monop\t Oriental a 3 LtBlue 100 2 30", "monop\t Vermont av 3 LtBlue 100 2 30", "monop\t Connecticu 3 LtBlue 120 2 30", "monop\t Reading RR 3 200 1", "monop\t Mediterran 3 60 4", "monop\tcharlie (3) (cash $740) on Oriental ave. (L)", "monop\t-- Command: ", ]) # Verify ownership resync self.assertEqual(g.property_owner.get(6), 3) # Oriental self.assertEqual(g.property_owner.get(8), 3) # Vermont self.assertEqual(g.property_owner.get(9), 3) # Connecticut self.assertEqual(g.property_owner.get(5), 3) # Reading RR self.assertEqual(g.property_owner.get(1), 3) # Mediterranean # Verify house resync self.assertEqual(g.property_houses.get(6), 2) self.assertEqual(g.property_houses.get(8), 2) self.assertEqual(g.property_houses.get(9), 2) # === Advance: alice rolls, then bob's turn === feed(p, [ "alice\t.", "monop\troll is 4, 2", "monop\tThat puts you on Connecticut ave. (L)", "monop\tOwned by charlie", "monop\twith 2 houses, rent is 30", "monop\tbob (2) (cash $1570) on States ave. (V)", "monop\t-- Command: ", ]) # === bob resigns to bank (Bug #5) === feed(p, [ "bob\t.resign", "monop\tWho do you wish to resign to? ", "bob\t.bank", "monop\tDo you really want to resign? ", "bob\t.y", "monop\tresigning to bank", "monop\tcharlie (1) (cash $770) on Oriental ave. (L)", "monop\t-- Command: ", ]) # Bob should be bankrupt state = p.get_state() bankrupt = [pl for pl in state["players"] if pl.get("bankrupt")] active = [pl for pl in state["players"] if not pl.get("bankrupt")] self.assertEqual(len(bankrupt), 1) self.assertEqual(bankrupt[0]["name"], "bob") # Players renumbered (Bug #7) self.assertEqual(len(active), 2) # Property ownership renumbered correctly # charlie was 3, after bob (2) removed, charlie becomes 2 charlie = g.get_player(name="charlie") self.assertIsNotNone(charlie) for sq_id in [1, 5, 6, 8, 9]: self.assertEqual(g.property_owner.get(sq_id), charlie.number, f"Bug #7: Property {sq_id} should be renumbered to charlie's new number") # === GOJF card: charlie goes to jail, uses card (New) === charlie = g.get_player(name="charlie") charlie.in_jail = True charlie.location = 40 charlie.get_out_of_jail_free_cards = 1 feed(p, [ "charlie\t.card", ]) self.assertFalse(charlie.in_jail, "GOJF card should free from jail") self.assertEqual(charlie.get_out_of_jail_free_cards, 0) self.assertEqual(charlie.location, 10) # === Advance to alice's turn === feed(p, [ "charlie\t.", "monop\troll is 3, 4", "monop\tThat puts you on Mediterranean ave. (P)", "monop\tYou own it.", "monop\talice (1) (cash $1107) on Connecticut ave. (L)", "monop\t-- Command: ", ]) # === alice resigns to charlie (Bug #6) — game over === alice = g.get_player(name="alice") g.property_owner[3] = alice.number # give alice Baltic back for the test feed(p, [ "monop\tYou would resign to charlie", "monop\tDo you really want to resign? ", "alice\t.y", "monop\tresigning to player", "monop\tTrade is done!", "monop\tThen charlie WINS!!!!!", ]) state = p.get_state() self.assertEqual(state["phase"], "over", "Bug #9: phase should be '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"], "charlie") # Baltic should have transferred to charlie (Bug #6) charlie = g.get_player(name="charlie") self.assertEqual(g.property_owner.get(3), charlie.number, "Bug #6: Resign-to-player should transfer properties") # Resign log should have timestamp (Bug #10) resign_logs = [e for e in g.log if "resigned" in e.get("text", "")] for entry in resign_logs: self.assertIn("timestamp", entry, "Bug #10: Resign log should have timestamp") # === Write state for screenshot === return p class TestStateOutput(unittest.TestCase): """Verify the full state JSON is well-formed for the UI.""" def test_state_structure(self): p = setup_game() g = p.game # Give charlie some properties and houses g.property_owner[6] = 3 g.property_owner[8] = 3 g.property_owner[9] = 3 g.property_houses[6] = 3 g.property_houses[8] = 2 g.property_houses[9] = 2 g.property_owner[1] = 1 g.property_mortgaged[1] = True state = p.get_state() # Verify structure self.assertIn("players", state) self.assertIn("squares", state) self.assertIn("log", state) self.assertIn("currentPlayer", state) self.assertIn("phase", state) # Verify squares have correct owner/house info oriental = next(sq for sq in state["squares"] if sq["id"] == 6) self.assertEqual(oriental["owner"], 3) self.assertEqual(oriental["houses"], 3) mediterranean = next(sq for sq in state["squares"] if sq["id"] == 1) self.assertEqual(mediterranean["owner"], 1) self.assertTrue(mediterranean["mortgaged"]) # Verify all ownable squares have owner field for sq in state["squares"]: if sq["type"] in ("property", "railroad", "utility"): self.assertIn("owner", sq) self.assertIn("mortgaged", sq) if __name__ == "__main__": unittest.main(verbosity=2)