monop-state/monop_bridge.py
Jarvis 6d055c68a2 Fix setup visibility: bridge waits for players, observer sees registration
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
2026-02-21 11:33:28 +00:00

146 lines
4.9 KiB
Python

"""Bridge monop binary to IRC via subprocess pipes."""
import sys
sys.stdout.reconfigure(line_buffering=True)
import socket
import subprocess
import threading
import time
import sys
import os
import re
HOST = "127.0.0.1"
PORT = 6667
NICK = "monop"
CHANNEL = "#monop"
PREFIX = "."
MONOP_BIN = "/tmp/monop-irc/monop/monop"
CARDS_FILE = "/tmp/monop-irc/monop/cards.pck"
class IRCBridge:
def __init__(self):
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.proc = None
self.buffer = ""
def irc_send(self, line):
self.sock.sendall((line + "\r\n").encode())
def irc_say(self, msg):
self.irc_send(f"PRIVMSG {CHANNEL} :{msg}")
def connect_irc(self):
self.sock.connect((HOST, PORT))
self.irc_send(f"NICK {NICK}")
self.irc_send(f"USER {NICK} 0 * :{NICK}")
# Wait for registration
while True:
data = self.sock.recv(4096)
if not data:
raise ConnectionError("IRC connection closed during registration")
self.buffer += data.decode("utf-8", errors="replace")
for line in self.buffer.split("\r\n"):
if line.startswith("PING"):
self.irc_send("PONG" + line[4:])
if "ERROR" in line:
print(f"[bridge] Server error: {line}")
if " 001 " in self.buffer:
break
self.irc_send(f"JOIN {CHANNEL}")
time.sleep(0.5)
self.buffer = ""
self.irc_say("Shall We Play A Game?")
self.irc_say(f'Prefix your commands with a dot "."')
print(f"[bridge] Connected to IRC as {NICK} in {CHANNEL}")
def start_monop(self):
env = os.environ.copy()
self.proc = subprocess.Popen(
[MONOP_BIN],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
env=env,
cwd=os.path.dirname(MONOP_BIN),
)
# Read monop stdout -> IRC
t = threading.Thread(target=self._monop_reader, daemon=True)
t.start()
print("[bridge] monop process started")
def _monop_reader(self):
"""Read lines from monop stdout and send to IRC."""
for raw in self.proc.stdout:
line = raw.decode("utf-8", errors="replace").rstrip("\n\r")
if line:
self.irc_say(line)
print(f"[monop -> irc] {line}")
def _feed_monop(self, nick, text):
"""Feed input to monop stdin as 'nick text'."""
feed = f"{nick} {text}\n"
print(f"[irc -> monop] {feed.rstrip()}")
try:
self.proc.stdin.write(feed.encode())
self.proc.stdin.flush()
except BrokenPipeError:
print("[bridge] monop process died, restarting...")
self.start_monop()
def _wait_for_players(self):
"""Wait until at least one other user joins the channel before starting."""
print(f"[bridge] Waiting for players to join {CHANNEL}...")
while True:
data = self.sock.recv(4096)
if not data:
raise ConnectionError("IRC connection lost while waiting")
self.buffer += data.decode("utf-8", errors="replace")
while "\r\n" in self.buffer:
line, self.buffer = self.buffer.split("\r\n", 1)
if line.startswith("PING"):
self.irc_send("PONG" + line[4:])
# Detect JOIN from someone other than us
m = re.match(r":(\S+?)!\S+ JOIN", line)
if m and m.group(1) != NICK:
print(f"[bridge] {m.group(1)} joined — starting game")
return
def run(self):
self.connect_irc()
self._wait_for_players()
self.start_monop()
while True:
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)
if line.startswith("PING"):
self.irc_send("PONG" + line[4:])
continue
# Parse PRIVMSG
m = re.match(
r":(\S+?)!(\S+?) PRIVMSG (\S+) :(.+)", line
)
if m:
sender_nick = m.group(1)
target = m.group(3)
msg = m.group(4)
if target == CHANNEL and sender_nick != NICK:
if msg.startswith(PREFIX):
cmd = msg[len(PREFIX):]
self._feed_monop(sender_nick, cmd)
except Exception as e:
print(f"[bridge] error: {e}")
break
if __name__ == "__main__":
bridge = IRCBridge()
bridge.run()