monop-board/test/gen_demo_state.py

268 lines
8.4 KiB
Python
Raw Permalink Normal View History

#!/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()