Add game simulation script for testing
This commit is contained in:
parent
0d8c16d2f5
commit
6ce682645f
1 changed files with 226 additions and 0 deletions
226
test/simulate_game.py
Normal file
226
test/simulate_game.py
Normal file
|
|
@ -0,0 +1,226 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Simulate a monop game on IRC for testing the board viewer bot.
|
||||||
|
|
||||||
|
Runs monop in a subprocess, connects to IRC, and plays the game
|
||||||
|
with scripted AI players that make simple decisions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import select
|
||||||
|
import ssl
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import irc.client
|
||||||
|
import irc.connection
|
||||||
|
|
||||||
|
CONFIG = {
|
||||||
|
"server": "irc.darkscience.net",
|
||||||
|
"port": 6697,
|
||||||
|
"tls": True,
|
||||||
|
"nick": "monop",
|
||||||
|
"channel": "#monop-dev",
|
||||||
|
"num_players": 3,
|
||||||
|
"player_names": ["Alice", "Bob", "Charlie"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class MonopSimulator:
|
||||||
|
def __init__(self, config):
|
||||||
|
self.config = config
|
||||||
|
self.connection = None
|
||||||
|
self.channel = config["channel"]
|
||||||
|
self.proc = None
|
||||||
|
self.game_phase = "init" # init, setup, playing
|
||||||
|
self.setup_step = 0
|
||||||
|
self.current_prompt = ""
|
||||||
|
self.output_buffer = ""
|
||||||
|
self.turn_count = 0
|
||||||
|
self.max_turns = 60 # stop after this many turns
|
||||||
|
|
||||||
|
def start_monop(self):
|
||||||
|
"""Start the monop game process."""
|
||||||
|
self.proc = subprocess.Popen(
|
||||||
|
["/usr/games/monop"],
|
||||||
|
stdin=subprocess.PIPE,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
text=True,
|
||||||
|
bufsize=1,
|
||||||
|
)
|
||||||
|
# Set stdout to non-blocking
|
||||||
|
import fcntl
|
||||||
|
fd = self.proc.stdout.fileno()
|
||||||
|
fl = fcntl.fcntl(fd, fcntl.F_GETFL)
|
||||||
|
fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
|
||||||
|
print("monop process started", flush=True)
|
||||||
|
|
||||||
|
def read_monop_output(self):
|
||||||
|
"""Read available output from monop."""
|
||||||
|
lines = []
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
data = self.proc.stdout.read(4096)
|
||||||
|
if not data:
|
||||||
|
break
|
||||||
|
self.output_buffer += data
|
||||||
|
except (BlockingIOError, IOError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
while "\n" in self.output_buffer:
|
||||||
|
line, self.output_buffer = self.output_buffer.split("\n", 1)
|
||||||
|
lines.append(line)
|
||||||
|
|
||||||
|
# Check for prompts (lines without newline at end)
|
||||||
|
if self.output_buffer:
|
||||||
|
self.current_prompt = self.output_buffer
|
||||||
|
|
||||||
|
return lines
|
||||||
|
|
||||||
|
def send_to_monop(self, text):
|
||||||
|
"""Send input to monop process."""
|
||||||
|
if self.proc and self.proc.poll() is None:
|
||||||
|
print(f" -> monop input: {text.strip()}", flush=True)
|
||||||
|
self.proc.stdin.write(text + "\n")
|
||||||
|
self.proc.stdin.flush()
|
||||||
|
time.sleep(0.3) # give monop time to process
|
||||||
|
|
||||||
|
def send_to_irc(self, text):
|
||||||
|
"""Send a message to the IRC channel."""
|
||||||
|
if self.connection and text.strip():
|
||||||
|
# Split long lines
|
||||||
|
for line in text.split("\n"):
|
||||||
|
line = line.rstrip()
|
||||||
|
if line:
|
||||||
|
self.connection.privmsg(self.channel, line)
|
||||||
|
time.sleep(0.15) # rate limit
|
||||||
|
|
||||||
|
def decide_action(self, prompt, output_lines):
|
||||||
|
"""AI decision making for the scripted players."""
|
||||||
|
full_text = "\n".join(output_lines) + "\n" + prompt
|
||||||
|
|
||||||
|
# "How many players?"
|
||||||
|
if "How many players?" in full_text:
|
||||||
|
return str(self.config["num_players"])
|
||||||
|
|
||||||
|
# Player name prompts
|
||||||
|
for i, name in enumerate(self.config["player_names"]):
|
||||||
|
if f"Player {i+1}'s name:" in full_text:
|
||||||
|
return name
|
||||||
|
|
||||||
|
# "Do you want to buy?"
|
||||||
|
if "Do you want to buy?" in prompt or "Do you want to buy?" in full_text:
|
||||||
|
return "yes"
|
||||||
|
|
||||||
|
# yes/no questions - default yes
|
||||||
|
if prompt.strip().endswith("?") and ("yes" in prompt.lower() or "no" in prompt.lower()):
|
||||||
|
return "yes"
|
||||||
|
|
||||||
|
# Command prompt (the game's main input) - just roll
|
||||||
|
if "-- Loss of $" in full_text:
|
||||||
|
return "" # just continue
|
||||||
|
|
||||||
|
# If we see a command prompt, roll
|
||||||
|
return "" # empty = roll (default action)
|
||||||
|
|
||||||
|
def on_connect(self, connection, event):
|
||||||
|
print(f"Connected to IRC. Joining {self.channel}", flush=True)
|
||||||
|
self.connection = connection
|
||||||
|
connection.join(self.channel)
|
||||||
|
|
||||||
|
def on_join(self, connection, event):
|
||||||
|
if event.source.nick == self.config["nick"]:
|
||||||
|
print(f"Joined {self.channel}. Starting game in 3s...", flush=True)
|
||||||
|
time.sleep(3)
|
||||||
|
self.start_monop()
|
||||||
|
self.game_loop()
|
||||||
|
|
||||||
|
def game_loop(self):
|
||||||
|
"""Main game loop - read monop output, send to IRC, make decisions."""
|
||||||
|
print("Starting game loop...", flush=True)
|
||||||
|
idle_count = 0
|
||||||
|
|
||||||
|
while self.proc and self.proc.poll() is None and self.turn_count < self.max_turns:
|
||||||
|
lines = self.read_monop_output()
|
||||||
|
|
||||||
|
if lines:
|
||||||
|
idle_count = 0
|
||||||
|
for line in lines:
|
||||||
|
print(f" monop: {line}", flush=True)
|
||||||
|
self.send_to_irc(line)
|
||||||
|
|
||||||
|
# Track turns
|
||||||
|
if "rolled doubles" in line or "'s turn" in line:
|
||||||
|
self.turn_count += 1
|
||||||
|
|
||||||
|
# Check if monop is waiting for input
|
||||||
|
time.sleep(0.5)
|
||||||
|
more_lines = self.read_monop_output()
|
||||||
|
for line in more_lines:
|
||||||
|
lines.append(line)
|
||||||
|
print(f" monop: {line}", flush=True)
|
||||||
|
self.send_to_irc(line)
|
||||||
|
|
||||||
|
# Make a decision if there's a prompt
|
||||||
|
if self.current_prompt or any("?" in l for l in lines):
|
||||||
|
decision = self.decide_action(self.current_prompt, lines)
|
||||||
|
if decision is not None:
|
||||||
|
time.sleep(0.5)
|
||||||
|
self.send_to_monop(decision)
|
||||||
|
self.current_prompt = ""
|
||||||
|
else:
|
||||||
|
# Game might be waiting for default input (roll)
|
||||||
|
time.sleep(1)
|
||||||
|
self.send_to_monop("")
|
||||||
|
else:
|
||||||
|
idle_count += 1
|
||||||
|
if idle_count > 10:
|
||||||
|
# Monop might be waiting for input
|
||||||
|
time.sleep(0.5)
|
||||||
|
self.send_to_monop("")
|
||||||
|
idle_count = 0
|
||||||
|
time.sleep(0.3)
|
||||||
|
|
||||||
|
# Process IRC events
|
||||||
|
try:
|
||||||
|
self.reactor.process_once(timeout=0.1)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
print(f"Game loop ended. Turns: {self.turn_count}", flush=True)
|
||||||
|
self.send_to_irc(f"--- Game simulation ended after {self.turn_count} turns ---")
|
||||||
|
if self.proc:
|
||||||
|
self.proc.terminate()
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
config = self.config
|
||||||
|
self.reactor = irc.client.Reactor()
|
||||||
|
|
||||||
|
connect_params = {}
|
||||||
|
if config.get("tls", False):
|
||||||
|
ssl_ctx = ssl.create_default_context()
|
||||||
|
hostname = config["server"]
|
||||||
|
ssl_factory = irc.connection.Factory(
|
||||||
|
wrapper=lambda s: ssl_ctx.wrap_socket(s, server_hostname=hostname))
|
||||||
|
connect_params["connect_factory"] = ssl_factory
|
||||||
|
|
||||||
|
server = self.reactor.server()
|
||||||
|
server.connect(
|
||||||
|
config["server"],
|
||||||
|
config["port"],
|
||||||
|
config["nick"],
|
||||||
|
**connect_params,
|
||||||
|
)
|
||||||
|
|
||||||
|
server.add_global_handler("welcome", self.on_connect)
|
||||||
|
server.add_global_handler("join", self.on_join)
|
||||||
|
|
||||||
|
print(f"Connecting to {config['server']}:{config['port']}...", flush=True)
|
||||||
|
self.reactor.process_forever()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sim = MonopSimulator(CONFIG)
|
||||||
|
sim.run()
|
||||||
Loading…
Reference in a new issue