monop-state/monop_bridge.py
Jarvis 9c47bac33a Fix setup registration: names appear as each player joins
Parser: detect name registration from user input during setup
(sender == name constraint prevents false matches on gameplay messages).

Bridge: extract _process_buffer() to handle leftover lines after
_wait_for_players returns, preventing messages from being stuck.

Player bots: proactively send name on join (not just first player
sending count) so they respond even if they missed the 'say me' prompt.

All tests pass (1551/1553 parser checkpoints, 38 player unit tests).
2026-02-21 11:54:53 +00:00

150 lines
5 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 _process_buffer(self):
"""Process any complete lines in the buffer."""
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
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)
def run(self):
self.connect_irc()
self._wait_for_players()
self.start_monop()
# Process any lines buffered during _wait_for_players
self._process_buffer()
while True:
try:
data = self.sock.recv(4096)
if not data:
break
self.buffer += data.decode("utf-8", errors="replace")
self._process_buffer()
except Exception as e:
print(f"[bridge] error: {e}")
break
if __name__ == "__main__":
bridge = IRCBridge()
bridge.run()