Add test scripts and realistic demo game state
This commit is contained in:
parent
6ce682645f
commit
a02632e2a2
3 changed files with 508 additions and 0 deletions
103
site/game-state.json
Normal file
103
site/game-state.json
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
{
|
||||||
|
"lastUpdated": "2026-02-20T21:30:00Z",
|
||||||
|
"currentPlayer": 1,
|
||||||
|
"players": [
|
||||||
|
{
|
||||||
|
"name": "Alice",
|
||||||
|
"number": 1,
|
||||||
|
"money": 920,
|
||||||
|
"location": 24,
|
||||||
|
"inJail": false,
|
||||||
|
"jailTurns": 0,
|
||||||
|
"goJailFreeCards": 1,
|
||||||
|
"properties": [1, 3, 6, 8, 9, 24],
|
||||||
|
"numRailroads": 0,
|
||||||
|
"numUtilities": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Bob",
|
||||||
|
"number": 2,
|
||||||
|
"money": 1150,
|
||||||
|
"location": 39,
|
||||||
|
"inJail": false,
|
||||||
|
"jailTurns": 0,
|
||||||
|
"goJailFreeCards": 0,
|
||||||
|
"properties": [5, 11, 15, 28, 37, 39],
|
||||||
|
"numRailroads": 2,
|
||||||
|
"numUtilities": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Charlie",
|
||||||
|
"number": 3,
|
||||||
|
"money": 480,
|
||||||
|
"location": 10,
|
||||||
|
"inJail": true,
|
||||||
|
"jailTurns": 2,
|
||||||
|
"goJailFreeCards": 0,
|
||||||
|
"properties": [16, 18, 19, 25, 31, 32],
|
||||||
|
"numRailroads": 1,
|
||||||
|
"numUtilities": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"squares": [
|
||||||
|
{"id":0,"name":"Go","type":"safe","owner":-1,"cost":0,"mortgaged":false,"houses":0,"monopoly":false,"group":null,"rent":[0]},
|
||||||
|
{"id":1,"name":"Mediterranean Ave.","type":"property","owner":0,"cost":60,"mortgaged":false,"houses":3,"monopoly":true,"group":"purple","rent":[2,10,30,90,160,250]},
|
||||||
|
{"id":2,"name":"Community Chest","type":"cc","owner":-1,"cost":0,"mortgaged":false,"houses":0,"monopoly":false,"group":null,"rent":[0]},
|
||||||
|
{"id":3,"name":"Baltic Ave.","type":"property","owner":0,"cost":60,"mortgaged":false,"houses":2,"monopoly":true,"group":"purple","rent":[4,20,60,180,320,450]},
|
||||||
|
{"id":4,"name":"Income Tax","type":"tax","owner":-1,"cost":0,"mortgaged":false,"houses":0,"monopoly":false,"group":null,"rent":[0]},
|
||||||
|
{"id":5,"name":"Reading Railroad","type":"railroad","owner":1,"cost":200,"mortgaged":false,"houses":0,"monopoly":false,"group":"railroad","rent":[0]},
|
||||||
|
{"id":6,"name":"Oriental Ave.","type":"property","owner":0,"cost":100,"mortgaged":false,"houses":0,"monopoly":true,"group":"lightblue","rent":[6,30,90,270,400,550]},
|
||||||
|
{"id":7,"name":"Chance","type":"chance","owner":-1,"cost":0,"mortgaged":false,"houses":0,"monopoly":false,"group":null,"rent":[0]},
|
||||||
|
{"id":8,"name":"Vermont Ave.","type":"property","owner":0,"cost":100,"mortgaged":false,"houses":0,"monopoly":true,"group":"lightblue","rent":[6,30,90,270,400,550]},
|
||||||
|
{"id":9,"name":"Connecticut Ave.","type":"property","owner":0,"cost":120,"mortgaged":false,"houses":1,"monopoly":true,"group":"lightblue","rent":[8,40,100,300,450,600]},
|
||||||
|
{"id":10,"name":"Just Visiting","type":"jail","owner":-1,"cost":0,"mortgaged":false,"houses":0,"monopoly":false,"group":null,"rent":[0]},
|
||||||
|
{"id":11,"name":"St. Charles Place","type":"property","owner":1,"cost":140,"mortgaged":false,"houses":0,"monopoly":false,"group":"pink","rent":[10,50,150,450,625,750]},
|
||||||
|
{"id":12,"name":"Electric Company","type":"utility","owner":-1,"cost":150,"mortgaged":false,"houses":0,"monopoly":false,"group":"utility","rent":[0]},
|
||||||
|
{"id":13,"name":"States Ave.","type":"property","owner":-1,"cost":140,"mortgaged":false,"houses":0,"monopoly":false,"group":"pink","rent":[10,50,150,450,625,750]},
|
||||||
|
{"id":14,"name":"Virginia Ave.","type":"property","owner":-1,"cost":160,"mortgaged":false,"houses":0,"monopoly":false,"group":"pink","rent":[12,60,180,500,700,900]},
|
||||||
|
{"id":15,"name":"Pennsylvania Railroad","type":"railroad","owner":1,"cost":200,"mortgaged":false,"houses":0,"monopoly":false,"group":"railroad","rent":[0]},
|
||||||
|
{"id":16,"name":"St. James Place","type":"property","owner":2,"cost":180,"mortgaged":false,"houses":4,"monopoly":true,"group":"orange","rent":[14,70,200,550,750,950]},
|
||||||
|
{"id":17,"name":"Community Chest","type":"cc","owner":-1,"cost":0,"mortgaged":false,"houses":0,"monopoly":false,"group":null,"rent":[0]},
|
||||||
|
{"id":18,"name":"Tennessee Ave.","type":"property","owner":2,"cost":180,"mortgaged":false,"houses":3,"monopoly":true,"group":"orange","rent":[14,70,200,550,750,950]},
|
||||||
|
{"id":19,"name":"New York Ave.","type":"property","owner":2,"cost":200,"mortgaged":false,"houses":2,"monopoly":true,"group":"orange","rent":[16,80,220,600,800,1000]},
|
||||||
|
{"id":20,"name":"Free Parking","type":"safe","owner":-1,"cost":0,"mortgaged":false,"houses":0,"monopoly":false,"group":null,"rent":[0]},
|
||||||
|
{"id":21,"name":"Kentucky Ave.","type":"property","owner":-1,"cost":220,"mortgaged":false,"houses":0,"monopoly":false,"group":"red","rent":[18,90,250,700,875,1050]},
|
||||||
|
{"id":22,"name":"Chance","type":"chance","owner":-1,"cost":0,"mortgaged":false,"houses":0,"monopoly":false,"group":null,"rent":[0]},
|
||||||
|
{"id":23,"name":"Indiana Ave.","type":"property","owner":-1,"cost":220,"mortgaged":false,"houses":0,"monopoly":false,"group":"red","rent":[18,90,250,700,875,1050]},
|
||||||
|
{"id":24,"name":"Illinois Ave.","type":"property","owner":0,"cost":240,"mortgaged":true,"houses":0,"monopoly":false,"group":"red","rent":[20,100,300,750,925,1100]},
|
||||||
|
{"id":25,"name":"B&O Railroad","type":"railroad","owner":2,"cost":200,"mortgaged":false,"houses":0,"monopoly":false,"group":"railroad","rent":[0]},
|
||||||
|
{"id":26,"name":"Atlantic Ave.","type":"property","owner":-1,"cost":260,"mortgaged":false,"houses":0,"monopoly":false,"group":"yellow","rent":[22,110,330,800,975,1150]},
|
||||||
|
{"id":27,"name":"Ventnor Ave.","type":"property","owner":-1,"cost":260,"mortgaged":false,"houses":0,"monopoly":false,"group":"yellow","rent":[22,110,330,800,975,1150]},
|
||||||
|
{"id":28,"name":"Water Works","type":"utility","owner":1,"cost":150,"mortgaged":false,"houses":0,"monopoly":false,"group":"utility","rent":[0]},
|
||||||
|
{"id":29,"name":"Marvin Gardens","type":"property","owner":-1,"cost":280,"mortgaged":false,"houses":0,"monopoly":false,"group":"yellow","rent":[24,120,360,850,1025,1200]},
|
||||||
|
{"id":30,"name":"Go to Jail","type":"gotojail","owner":-1,"cost":0,"mortgaged":false,"houses":0,"monopoly":false,"group":null,"rent":[0]},
|
||||||
|
{"id":31,"name":"Pacific Ave.","type":"property","owner":2,"cost":300,"mortgaged":false,"houses":0,"monopoly":false,"group":"green","rent":[26,130,390,900,1100,1275]},
|
||||||
|
{"id":32,"name":"North Carolina Ave.","type":"property","owner":2,"cost":300,"mortgaged":false,"houses":0,"monopoly":false,"group":"green","rent":[26,130,390,900,1100,1275]},
|
||||||
|
{"id":33,"name":"Community Chest","type":"cc","owner":-1,"cost":0,"mortgaged":false,"houses":0,"monopoly":false,"group":null,"rent":[0]},
|
||||||
|
{"id":34,"name":"Pennsylvania Ave.","type":"property","owner":-1,"cost":320,"mortgaged":false,"houses":0,"monopoly":false,"group":"green","rent":[28,150,450,1000,1200,1400]},
|
||||||
|
{"id":35,"name":"Short Line Railroad","type":"railroad","owner":-1,"cost":200,"mortgaged":false,"houses":0,"monopoly":false,"group":"railroad","rent":[0]},
|
||||||
|
{"id":36,"name":"Chance","type":"chance","owner":-1,"cost":0,"mortgaged":false,"houses":0,"monopoly":false,"group":null,"rent":[0]},
|
||||||
|
{"id":37,"name":"Park Place","type":"property","owner":1,"cost":350,"mortgaged":false,"houses":0,"monopoly":false,"group":"darkblue","rent":[35,175,500,1100,1300,1500]},
|
||||||
|
{"id":38,"name":"Luxury Tax","type":"tax","owner":-1,"cost":0,"mortgaged":false,"houses":0,"monopoly":false,"group":null,"rent":[0]},
|
||||||
|
{"id":39,"name":"Boardwalk","type":"property","owner":1,"cost":400,"mortgaged":false,"houses":5,"monopoly":true,"group":"darkblue","rent":[50,200,600,1400,1700,2000]}
|
||||||
|
],
|
||||||
|
"log": [
|
||||||
|
{"timestamp":"2026-02-20T21:20:00Z","text":"roll is 5, 6","player":"Alice"},
|
||||||
|
{"timestamp":"2026-02-20T21:20:05Z","text":"Passed Go, collected $200","player":"Alice"},
|
||||||
|
{"timestamp":"2026-02-20T21:20:06Z","text":"Landed on Illinois Ave.","player":"Alice"},
|
||||||
|
{"timestamp":"2026-02-20T21:20:10Z","text":"Bought Illinois Ave. for $240","player":"Alice"},
|
||||||
|
{"timestamp":"2026-02-20T21:21:00Z","text":"roll is 6, 6","player":"Bob"},
|
||||||
|
{"timestamp":"2026-02-20T21:21:02Z","text":"Bob rolled doubles","player":"Bob"},
|
||||||
|
{"timestamp":"2026-02-20T21:21:05Z","text":"Landed on Boardwalk","player":"Bob"},
|
||||||
|
{"timestamp":"2026-02-20T21:21:10Z","text":"roll is 3, 5","player":"Bob"},
|
||||||
|
{"timestamp":"2026-02-20T21:21:15Z","text":"Paid $750 rent (4 houses)","player":"Bob"},
|
||||||
|
{"timestamp":"2026-02-20T21:22:00Z","text":"Charlie's turn","player":"Charlie"},
|
||||||
|
{"timestamp":"2026-02-20T21:22:05Z","text":"Still in jail (turn 2)","player":"Charlie"},
|
||||||
|
{"timestamp":"2026-02-20T21:23:00Z","text":"roll is 4, 2","player":"Alice"},
|
||||||
|
{"timestamp":"2026-02-20T21:23:05Z","text":"Landed on Atlantic Ave.","player":"Alice"},
|
||||||
|
{"timestamp":"2026-02-20T21:24:00Z","text":"roll is 1, 5","player":"Bob"},
|
||||||
|
{"timestamp":"2026-02-20T21:24:05Z","text":"Passed Go, collected $200","player":"Bob"},
|
||||||
|
{"timestamp":"2026-02-20T21:24:06Z","text":"Landed on Oriental Ave.","player":"Bob"},
|
||||||
|
{"timestamp":"2026-02-20T21:24:10Z","text":"Paid $12 rent","player":"Bob"}
|
||||||
|
]
|
||||||
|
}
|
||||||
267
test/gen_demo_state.py
Normal file
267
test/gen_demo_state.py
Normal file
|
|
@ -0,0 +1,267 @@
|
||||||
|
#!/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()
|
||||||
138
test/local_sim.py
Normal file
138
test/local_sim.py
Normal file
|
|
@ -0,0 +1,138 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Run monop locally with scripted players via pexpect, feed output to parser."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
import pexpect
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'bot'))
|
||||||
|
from parser import MonopParser
|
||||||
|
|
||||||
|
STATE_FILE = os.path.join(os.path.dirname(__file__), '..', 'site', 'game-state.json')
|
||||||
|
MONOP = "/usr/games/monop"
|
||||||
|
PLAYERS = ["Alice", "Bob", "Charlie"]
|
||||||
|
MAX_TURNS = 80
|
||||||
|
|
||||||
|
|
||||||
|
def write_state(parser):
|
||||||
|
state = parser.get_state()
|
||||||
|
state["lastUpdated"] = datetime.now(timezone.utc).isoformat()
|
||||||
|
tmp = STATE_FILE + ".tmp"
|
||||||
|
with open(tmp, "w") as f:
|
||||||
|
json.dump(state, f, indent=2)
|
||||||
|
os.replace(tmp, STATE_FILE)
|
||||||
|
|
||||||
|
|
||||||
|
def process_output(parser, text):
|
||||||
|
"""Feed text lines to parser."""
|
||||||
|
for line in text.split("\r\n"):
|
||||||
|
line = line.strip()
|
||||||
|
if line:
|
||||||
|
print(f" monop: {line}", flush=True)
|
||||||
|
parser.parse_line(line)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = MonopParser()
|
||||||
|
for i, name in enumerate(PLAYERS):
|
||||||
|
parser.add_player(name, i + 1)
|
||||||
|
|
||||||
|
child = pexpect.spawn(MONOP, encoding='utf-8', timeout=5)
|
||||||
|
child.logfile_read = None # we'll handle output ourselves
|
||||||
|
|
||||||
|
turn_count = 0
|
||||||
|
|
||||||
|
def expect_and_respond(patterns_responses, default=""):
|
||||||
|
"""Wait for patterns and respond."""
|
||||||
|
nonlocal turn_count
|
||||||
|
patterns = [p for p, _ in patterns_responses]
|
||||||
|
try:
|
||||||
|
idx = child.expect(patterns, timeout=3)
|
||||||
|
output = child.before + child.after
|
||||||
|
process_output(parser, output)
|
||||||
|
write_state(parser)
|
||||||
|
resp = patterns_responses[idx][1]
|
||||||
|
if callable(resp):
|
||||||
|
resp = resp()
|
||||||
|
print(f" -> {resp!r}", flush=True)
|
||||||
|
child.sendline(resp)
|
||||||
|
return True
|
||||||
|
except pexpect.TIMEOUT:
|
||||||
|
output = child.before or ""
|
||||||
|
if output:
|
||||||
|
process_output(parser, output)
|
||||||
|
write_state(parser)
|
||||||
|
return False
|
||||||
|
except pexpect.EOF:
|
||||||
|
output = child.before or ""
|
||||||
|
if output:
|
||||||
|
process_output(parser, output)
|
||||||
|
write_state(parser)
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Phase 1: Setup
|
||||||
|
print("=== Game Setup ===", flush=True)
|
||||||
|
expect_and_respond([("How many players\\?", str(len(PLAYERS)))])
|
||||||
|
for i, name in enumerate(PLAYERS):
|
||||||
|
expect_and_respond([(f"Player {i+1}'s name:", name)])
|
||||||
|
|
||||||
|
# Phase 2: Playing
|
||||||
|
print("\n=== Game Play ===", flush=True)
|
||||||
|
consecutive_timeouts = 0
|
||||||
|
|
||||||
|
while turn_count < MAX_TURNS:
|
||||||
|
result = expect_and_respond([
|
||||||
|
("Do you want to buy\\?", "yes"),
|
||||||
|
("do you wish to ", "yes"),
|
||||||
|
("Bid for ", "0"),
|
||||||
|
("How much do you", "0"),
|
||||||
|
("mortgage\\?", "yes"),
|
||||||
|
("Which file", "/dev/null"),
|
||||||
|
("do you wish to sell", "no"),
|
||||||
|
("Will you buy", "no"),
|
||||||
|
("rolled doubles", ""),
|
||||||
|
("'s turn", ""),
|
||||||
|
# The main prompt - just hit enter to roll
|
||||||
|
("\\? ", ""),
|
||||||
|
("\n", ""),
|
||||||
|
])
|
||||||
|
|
||||||
|
if result is None:
|
||||||
|
print("Game ended (EOF)", flush=True)
|
||||||
|
break
|
||||||
|
elif result is False:
|
||||||
|
consecutive_timeouts += 1
|
||||||
|
if consecutive_timeouts > 5:
|
||||||
|
# Try sending empty line to nudge
|
||||||
|
child.sendline("")
|
||||||
|
consecutive_timeouts = 0
|
||||||
|
else:
|
||||||
|
consecutive_timeouts = 0
|
||||||
|
turn_count += 1
|
||||||
|
|
||||||
|
# Read any remaining output
|
||||||
|
try:
|
||||||
|
extra = child.read_nonblocking(4096, timeout=0.3)
|
||||||
|
if extra:
|
||||||
|
process_output(parser, extra)
|
||||||
|
write_state(parser)
|
||||||
|
except (pexpect.TIMEOUT, pexpect.EOF):
|
||||||
|
pass
|
||||||
|
|
||||||
|
print(f"\n=== Done: {turn_count} turns ===", flush=True)
|
||||||
|
write_state(parser)
|
||||||
|
child.close()
|
||||||
|
|
||||||
|
state = parser.get_state()
|
||||||
|
for p in state["players"]:
|
||||||
|
props = [state["squares"][pid]["name"] for pid in p["properties"]]
|
||||||
|
print(f" {p['name']}: ${p['money']}, loc={p['location']}, props={props}")
|
||||||
|
print(f" Log entries: {len(state['log'])}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Loading…
Reference in a new issue