382 lines
14 KiB
Python
382 lines
14 KiB
Python
|
|
#!/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)
|