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