Bridge changes: - Wait for at least one user to JOIN before starting monop - Ensures observer is in channel to see all setup messages Parser changes: - Handle 'Player N, say me' even without prior 'How many players?' - Infer num_players_expected from highest player number seen - Emit state during setup phase run_game.py changes: - 3s stagger between bot joins so setup is visible in web UI - Observer connects before bots to catch all registration messages
114 lines
3.7 KiB
Python
114 lines
3.7 KiB
Python
#!/usr/bin/env python3
|
|
"""Run a simulated game: player bots + parser writing game-state.json to site/."""
|
|
import sys
|
|
import os
|
|
import json
|
|
import socket
|
|
import threading
|
|
import time
|
|
|
|
sys.path.insert(0, os.path.dirname(__file__))
|
|
from monop_parser import MonopParser
|
|
from monop_players import PlayerBot
|
|
|
|
STATE_PATH = os.path.join(os.path.dirname(__file__), "site", "game-state.json")
|
|
HOST = "127.0.0.1"
|
|
PORT = 6667
|
|
CHANNEL = "#monop"
|
|
BOT_NICK = "monop"
|
|
|
|
class ParserClient:
|
|
"""IRC client that watches the game and writes game-state.json."""
|
|
def __init__(self):
|
|
self.parser = MonopParser()
|
|
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
self.buffer = ""
|
|
self.running = True
|
|
|
|
def connect(self):
|
|
self.sock.connect((HOST, PORT))
|
|
self.sock.sendall(b"NICK observer\r\nUSER observer 0 * :observer\r\n")
|
|
time.sleep(1)
|
|
self.sock.sendall(f"JOIN {CHANNEL}\r\n".encode())
|
|
|
|
def run(self):
|
|
self.connect()
|
|
while self.running:
|
|
try:
|
|
data = self.sock.recv(4096)
|
|
if not data:
|
|
break
|
|
self.buffer += data.decode("utf-8", errors="replace")
|
|
while "\r\n" in self.buffer:
|
|
line, self.buffer = self.buffer.split("\r\n", 1)
|
|
self._handle(line)
|
|
except Exception as e:
|
|
print(f"[parser] Error: {e}")
|
|
break
|
|
|
|
def _handle(self, line):
|
|
if line.startswith("PING"):
|
|
self.sock.sendall(f"PONG {line[5:]}\r\n".encode())
|
|
return
|
|
# Parse PRIVMSG from bot
|
|
if "PRIVMSG" in line and CHANNEL in line:
|
|
# :nick!user@host PRIVMSG #chan :message
|
|
parts = line.split(f"PRIVMSG {CHANNEL} :", 1)
|
|
if len(parts) == 2:
|
|
prefix = parts[0].strip()
|
|
sender = prefix.split("!")[0].lstrip(":")
|
|
message = parts[1]
|
|
ts = time.strftime("%Y-%m-%d %H:%M:%S")
|
|
log_line = f"{ts}\t{sender}\t{message}"
|
|
self.parser.parse_line(log_line)
|
|
state = self.parser.get_state()
|
|
if state:
|
|
from datetime import datetime, timezone
|
|
state["lastUpdated"] = datetime.now(timezone.utc).isoformat()
|
|
try:
|
|
with open(STATE_PATH, "w") as f:
|
|
json.dump(state, f, indent=2)
|
|
except Exception as e:
|
|
print(f"[parser] Write error: {e}")
|
|
|
|
def stop(self):
|
|
self.running = False
|
|
try:
|
|
self.sock.close()
|
|
except:
|
|
pass
|
|
|
|
|
|
def main():
|
|
player_names = ["alice", "bob", "charlie"]
|
|
|
|
# 1. Start parser/observer client FIRST so it sees setup messages
|
|
pc = ParserClient()
|
|
parser_thread = threading.Thread(target=pc.run, daemon=True)
|
|
parser_thread.start()
|
|
print(f"[game] Observer connected, writing to {STATE_PATH}")
|
|
|
|
# 2. Wait for observer to be fully joined before bots trigger setup
|
|
time.sleep(3)
|
|
|
|
# 3. Start player bots (they'll trigger setup by sending player count)
|
|
bots = []
|
|
for i, name in enumerate(player_names):
|
|
bot = PlayerBot(name, CHANNEL, HOST, PORT, player_names, i)
|
|
t = threading.Thread(target=bot.run, daemon=True)
|
|
t.start()
|
|
bots.append(bot)
|
|
time.sleep(3.0) # stagger joins so setup is visible in the web UI
|
|
|
|
print(f"[game] {len(bots)} player bots started")
|
|
|
|
# Run until killed
|
|
try:
|
|
while True:
|
|
time.sleep(1)
|
|
except KeyboardInterrupt:
|
|
print("[game] Shutting down...")
|
|
pc.stop()
|
|
|
|
if __name__ == "__main__":
|
|
main()
|