#!/usr/bin/env python3 """Generate a realistic game-state.json from a scripted monop session using pexpect.""" import os import sys import json import re import time import pexpect from datetime import datetime, timezone sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'bot')) from board_data import SQUARES, NAME_TO_ID STATE_FILE = os.path.join(os.path.dirname(__file__), '..', 'site', 'game-state.json') MONOP = "/usr/games/monop" PLAYER_NAMES = ["Alice", "Bob", "Charlie"] def find_square(name): name = name.strip().lower().rstrip(".") # Try various lookups for key in [name, name.split("(")[0].strip()]: key = key.strip() if key in NAME_TO_ID: return NAME_TO_ID[key] for k, v in NAME_TO_ID.items(): if key in k or k in key: return v return None class GameState: def __init__(self, names): self.players = [] for i, name in enumerate(names): self.players.append({ "name": name, "number": i + 1, "money": 1500, "location": 0, "inJail": False, "jailTurns": 0, "goJailFreeCards": 0, "properties": [], "numRailroads": 0, "numUtilities": 0, }) self.squares = [] for sq in SQUARES: self.squares.append({ "id": sq["id"], "name": sq["name"], "type": sq.get("type", "safe"), "owner": -1, "cost": sq.get("cost", 0), "mortgaged": False, "houses": 0, "monopoly": False, "group": sq.get("group"), "rent": sq.get("rent", [0]), }) self.current_player = 0 self.log = [] def add_log(self, text, player=None): self.log.append({ "timestamp": datetime.now(timezone.utc).isoformat(), "text": text, "player": player, }) def find_player(self, name): for i, p in enumerate(self.players): if p["name"].lower() == name.lower(): return i return None def cur(self): return self.players[self.current_player] def to_dict(self): return { "lastUpdated": datetime.now(timezone.utc).isoformat(), "currentPlayer": self.current_player, "players": self.players, "squares": self.squares, "log": self.log[-100:], } def save(self): d = self.to_dict() tmp = STATE_FILE + ".tmp" with open(tmp, "w") as f: json.dump(d, f, indent=2) os.replace(tmp, STATE_FILE) def main(): gs = GameState(PLAYER_NAMES) child = pexpect.spawn(MONOP, encoding='utf-8', timeout=5) all_output = [] def interact(expect_pat, response, timeout=3): try: child.expect(expect_pat, timeout=timeout) text = (child.before or "") + (child.after or "") all_output.append(text) process_text(text) child.sendline(response) time.sleep(0.15) return True except (pexpect.TIMEOUT, pexpect.EOF): try: text = child.before or "" if text: all_output.append(text) process_text(text) except: pass return False def process_text(text): for line in text.split("\n"): line = line.strip().replace("\r", "") if not line: continue cp = gs.cur() # Roll m = re.match(r"roll is (\d+), (\d+)", line) if m: gs.add_log(f"roll is {m.group(1)}, {m.group(2)}", cp["name"]) # Movement m = re.match(r"That puts you on (.+)", line) if m: sq_name = m.group(1).strip() sq_id = find_square(sq_name) if sq_id is not None: cp["location"] = sq_id gs.add_log(f"Landed on {sq_name}", cp["name"]) # Pass Go if "pass" in line.lower() and "$200" in line: cp["money"] += 200 gs.add_log("Passed Go, collected $200", cp["name"]) # Doubles m = re.match(r"(.+) rolled doubles", line) if m: gs.add_log(f"{m.group(1)} rolled doubles", m.group(1)) # Jail if "GO DIRECTLY TO JAIL" in line or "go to jail" in line.lower(): cp["location"] = 10 cp["inJail"] = True gs.add_log("Go to Jail!", cp["name"]) # Rent m = re.match(r"rent is (\d+)", line) if m: rent = int(m.group(1)) cp["money"] -= rent sq_id = cp["location"] owner = gs.squares[sq_id]["owner"] if 0 <= owner < len(gs.players): gs.players[owner]["money"] += rent gs.add_log(f"Paid ${rent} rent", cp["name"]) # Tax if "Luxury tax" in line: cp["money"] -= 75 gs.add_log("Paid $75 luxury tax", cp["name"]) if "Income tax" in line and "%" in line: cp["money"] -= 200 gs.add_log("Paid $200 income tax", cp["name"]) # Services $25 if "Receive for Services $25" in line: cp["money"] += 25 gs.add_log("Received $25 for services", cp["name"]) # Player turn indicator m = re.match(r"(\w+) \(\d+\) \(cash \$(\d+)\) on (.+)", line) if m: name = m.group(1) money = int(m.group(2)) idx = gs.find_player(name) if idx is not None: gs.current_player = idx gs.players[idx]["money"] = money # Bought detection (after "yes" to buy) if "-- Loss of $" in line: m2 = re.search(r"Loss of \$(\d+)", line) if m2: cp["money"] -= int(m2.group(1)) gs.save() # Setup print("=== Setup ===", flush=True) interact("How many players\\?", str(len(PLAYER_NAMES))) for i, name in enumerate(PLAYER_NAMES): interact(f"Player {i+1}'s name:", name) # Handle initial rolls for order for _ in range(10): if not interact([r"\(\d+\) rolls \d+", "goes first"], "", timeout=2): break # Play the game print("\n=== Playing ===", flush=True) turns = 0 max_turns = 80 while turns < max_turns: # Try to match common prompts matched = False # Buy prompt if interact("Do you want to buy\\?", "yes", timeout=2): # Record the purchase cp = gs.cur() sq_id = cp["location"] if sq_id is not None and gs.squares[sq_id]["owner"] == -1: sq = gs.squares[sq_id] if sq["cost"] > 0: sq["owner"] = gs.current_player cp["properties"].append(sq_id) cp["money"] -= sq["cost"] if sq["type"] == "railroad": cp["numRailroads"] += 1 elif sq["type"] == "utility": cp["numUtilities"] += 1 gs.add_log(f"Bought {sq['name']} for ${sq['cost']}", cp["name"]) gs.save() matched = True turns += 1 continue # Command prompt - roll if interact("-- Command:", "", timeout=2): matched = True turns += 1 continue # Any other prompt if interact("\\? ", "", timeout=2): matched = True turns += 1 continue if not matched: # Try just reading output try: child.expect(r".+", timeout=1) text = (child.before or "") + (child.after or "") if text: process_text(text) except (pexpect.TIMEOUT, pexpect.EOF): child.sendline("") turns += 1 print(f"\n=== Done: {turns} turns ===", flush=True) gs.save() child.close() for p in gs.players: props = [gs.squares[pid]["name"] for pid in p["properties"]] print(f" {p['name']}: ${p['money']}, loc={p['location']}, props={props}") print(f" Log: {len(gs.log)} entries") if __name__ == "__main__": main()