diff --git a/__pycache__/monop_players.cpython-310.pyc b/__pycache__/monop_players.cpython-310.pyc index d4c77b4..2a8f06a 100644 Binary files a/__pycache__/monop_players.cpython-310.pyc and b/__pycache__/monop_players.cpython-310.pyc differ diff --git a/monop_players.py b/monop_players.py index 823bebf..a400ad8 100644 --- a/monop_players.py +++ b/monop_players.py @@ -358,7 +358,16 @@ class PlayerBot: return if msg.startswith("Which player do you wish to trade with?"): - # We don't trade proactively — shouldn't hit this + if self.in_trade and self.is_my_turn(): + # Pick a random other player to trade with + others = [n for n in self.player_names if n.lower() != self.nick.lower()] + if others: + target = random.choice(others) + self.log(f"Trading with {target}") + self.say_delayed(target, force=True) + else: + self.say_delayed("done", force=True) + self.in_trade = False return # (Trade property prompt handled in TRADING section below) @@ -524,12 +533,8 @@ class PlayerBot: # "Which property do you wish to trade?" — pick a property or done if msg == "Which property do you wish to trade?": if self.in_trade: - if self.trade_props_offered == 0 and random.random() < 0.5: - # Offer one property then done - self.trade_props_offered = 1 - self.say_delayed("?", force=True) # get the list - else: - self.say_delayed("done", force=True) + # Keep it simple — cash-only trades, skip property offers + self.say_delayed("done", force=True) return # "You have $X. How much are you trading?" — offer some cash diff --git a/run_game.py b/run_game.py new file mode 100644 index 0000000..93b5ee6 --- /dev/null +++ b/run_game.py @@ -0,0 +1,112 @@ +#!/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"] + + # Start parser client + pc = ParserClient() + parser_thread = threading.Thread(target=pc.run, daemon=True) + parser_thread.start() + print(f"[game] Parser writing to {STATE_PATH}") + time.sleep(2) + + # Start player bots + 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(0.5) + + 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() diff --git a/site/game-state.json b/site/game-state.json index 99c3934..cd5904f 100644 --- a/site/game-state.json +++ b/site/game-state.json @@ -1,91 +1,522 @@ { "players": [ { - "name": "Alice", - "number": 1, - "money": 1500, - "location": 0, - "inJail": false, - "jailTurns": 0, - "doublesCount": 0, - "getOutOfJailFreeCards": 0 - }, - { - "name": "Bob", - "number": 2, - "money": 1500, - "location": 0, - "inJail": false, - "jailTurns": 0, - "doublesCount": 0, - "getOutOfJailFreeCards": 0 - }, - { - "name": "Charlie", + "name": "charlie", "number": 3, - "money": 1500, - "location": 0, + "money": 985, + "location": 40, + "inJail": true, + "jailTurns": 1, + "doublesCount": 0, + "getOutOfJailFreeCards": 0 + }, + { + "name": "alice", + "number": 1, + "money": 1375, + "location": 40, + "inJail": true, + "jailTurns": 0, + "doublesCount": 0, + "getOutOfJailFreeCards": 0 + }, + { + "name": "bob", + "number": 2, + "money": 800, + "location": 31, "inJail": false, "jailTurns": 0, "doublesCount": 0, "getOutOfJailFreeCards": 0 } ], - "currentPlayer": 1, + "currentPlayer": 3, "squares": [ - { "id": 0, "name": "=== GO ===", "type": "safe" }, - { "id": 1, "name": "Mediterranean ave. (P)", "type": "property", "group": "purple", "cost": 60, "owner": null, "mortgaged": false, "houses": 0, "rent": [2, 10, 30, 90, 160, 250] }, - { "id": 2, "name": "Community Chest i", "type": "cc" }, - { "id": 3, "name": "Baltic ave. (P)", "type": "property", "group": "purple", "cost": 60, "owner": null, "mortgaged": false, "houses": 0, "rent": [4, 20, 60, 180, 320, 450] }, - { "id": 4, "name": "Income Tax", "type": "tax" }, - { "id": 5, "name": "Reading RR", "type": "railroad", "group": "railroad", "cost": 200, "owner": null, "mortgaged": false }, - { "id": 6, "name": "Oriental ave. (L)", "type": "property", "group": "lightblue", "cost": 100, "owner": null, "mortgaged": false, "houses": 0, "rent": [6, 30, 90, 270, 400, 550] }, - { "id": 7, "name": "Chance i", "type": "chance" }, - { "id": 8, "name": "Vermont ave. (L)", "type": "property", "group": "lightblue", "cost": 100, "owner": null, "mortgaged": false, "houses": 0, "rent": [6, 30, 90, 270, 400, 550] }, - { "id": 9, "name": "Connecticut ave. (L)", "type": "property", "group": "lightblue", "cost": 120, "owner": null, "mortgaged": false, "houses": 0, "rent": [8, 40, 100, 300, 450, 600] }, - { "id": 10, "name": "Just Visiting", "type": "safe" }, - { "id": 11, "name": "St. Charles pl. (V)", "type": "property", "group": "violet", "cost": 140, "owner": null, "mortgaged": false, "houses": 0, "rent": [10, 50, 150, 450, 625, 750] }, - { "id": 12, "name": "Electric Co.", "type": "utility", "group": "utility", "cost": 150, "owner": null, "mortgaged": false }, - { "id": 13, "name": "States ave. (V)", "type": "property", "group": "violet", "cost": 140, "owner": null, "mortgaged": false, "houses": 0, "rent": [10, 50, 150, 450, 625, 750] }, - { "id": 14, "name": "Virginia ave. (V)", "type": "property", "group": "violet", "cost": 160, "owner": null, "mortgaged": false, "houses": 0, "rent": [12, 60, 180, 500, 700, 900] }, - { "id": 15, "name": "Pennsylvania RR", "type": "railroad", "group": "railroad", "cost": 200, "owner": null, "mortgaged": false }, - { "id": 16, "name": "St. James pl. (O)", "type": "property", "group": "orange", "cost": 180, "owner": null, "mortgaged": false, "houses": 0, "rent": [14, 70, 200, 550, 750, 950] }, - { "id": 17, "name": "Community Chest ii", "type": "cc" }, - { "id": 18, "name": "Tennessee ave. (O)", "type": "property", "group": "orange", "cost": 180, "owner": null, "mortgaged": false, "houses": 0, "rent": [14, 70, 200, 550, 750, 950] }, - { "id": 19, "name": "New York ave. (O)", "type": "property", "group": "orange", "cost": 200, "owner": null, "mortgaged": false, "houses": 0, "rent": [16, 80, 220, 600, 800, 1000] }, - { "id": 20, "name": "Free Parking", "type": "safe" }, - { "id": 21, "name": "Kentucky ave. (R)", "type": "property", "group": "red", "cost": 220, "owner": null, "mortgaged": false, "houses": 0, "rent": [18, 90, 250, 700, 875, 1050] }, - { "id": 22, "name": "Chance ii", "type": "chance" }, - { "id": 23, "name": "Indiana ave. (R)", "type": "property", "group": "red", "cost": 220, "owner": null, "mortgaged": false, "houses": 0, "rent": [18, 90, 250, 700, 875, 1050] }, - { "id": 24, "name": "Illinois ave. (R)", "type": "property", "group": "red", "cost": 240, "owner": null, "mortgaged": false, "houses": 0, "rent": [20, 100, 300, 750, 925, 1100] }, - { "id": 25, "name": "B&O RR", "type": "railroad", "group": "railroad", "cost": 200, "owner": null, "mortgaged": false }, - { "id": 26, "name": "Atlantic ave. (Y)", "type": "property", "group": "yellow", "cost": 260, "owner": null, "mortgaged": false, "houses": 0, "rent": [22, 110, 330, 800, 975, 1150] }, - { "id": 27, "name": "Ventnor ave. (Y)", "type": "property", "group": "yellow", "cost": 260, "owner": null, "mortgaged": false, "houses": 0, "rent": [22, 110, 330, 800, 975, 1150] }, - { "id": 28, "name": "Water Works", "type": "utility", "group": "utility", "cost": 150, "owner": null, "mortgaged": false }, - { "id": 29, "name": "Marvin Gardens (Y)", "type": "property", "group": "yellow", "cost": 280, "owner": null, "mortgaged": false, "houses": 0, "rent": [24, 120, 360, 850, 1025, 1200] }, - { "id": 30, "name": "GO TO JAIL", "type": "gotojail" }, - { "id": 31, "name": "Pacific ave. (G)", "type": "property", "group": "green", "cost": 300, "owner": null, "mortgaged": false, "houses": 0, "rent": [26, 130, 390, 900, 1100, 1275] }, - { "id": 32, "name": "N. Carolina ave. (G)", "type": "property", "group": "green", "cost": 300, "owner": null, "mortgaged": false, "houses": 0, "rent": [26, 130, 390, 900, 1100, 1275] }, - { "id": 33, "name": "Community Chest iii", "type": "cc" }, - { "id": 34, "name": "Pennsylvania ave. (G)", "type": "property", "group": "green", "cost": 320, "owner": null, "mortgaged": false, "houses": 0, "rent": [28, 150, 450, 1000, 1200, 1400] }, - { "id": 35, "name": "Short Line RR", "type": "railroad", "group": "railroad", "cost": 200, "owner": null, "mortgaged": false }, - { "id": 36, "name": "Chance iii", "type": "chance" }, - { "id": 37, "name": "Park place (D)", "type": "property", "group": "darkblue", "cost": 350, "owner": null, "mortgaged": false, "houses": 0, "rent": [35, 175, 500, 1100, 1300, 1500] }, - { "id": 38, "name": "Luxury Tax", "type": "tax" }, - { "id": 39, "name": "Boardwalk (D)", "type": "property", "group": "darkblue", "cost": 400, "owner": null, "mortgaged": false, "houses": 0, "rent": [50, 200, 600, 1400, 1700, 2000] } + { + "id": 0, + "name": "=== GO ===", + "type": "safe" + }, + { + "id": 1, + "name": "Mediterranean ave. (P)", + "type": "property", + "owner": null, + "mortgaged": false, + "group": "purple", + "cost": 60, + "houses": 0 + }, + { + "id": 2, + "name": "Community Chest i", + "type": "cc" + }, + { + "id": 3, + "name": "Baltic ave. (P)", + "type": "property", + "owner": null, + "mortgaged": false, + "group": "purple", + "cost": 60, + "houses": 0 + }, + { + "id": 4, + "name": "Income Tax", + "type": "tax" + }, + { + "id": 5, + "name": "Reading RR", + "type": "railroad", + "owner": null, + "mortgaged": false, + "group": "railroad", + "cost": 200 + }, + { + "id": 6, + "name": "Oriental ave. (L)", + "type": "property", + "owner": 2, + "mortgaged": false, + "group": "lightblue", + "cost": 100, + "houses": 0 + }, + { + "id": 7, + "name": "Chance i", + "type": "chance" + }, + { + "id": 8, + "name": "Vermont ave. (L)", + "type": "property", + "owner": 1, + "mortgaged": false, + "group": "lightblue", + "cost": 100, + "houses": 0 + }, + { + "id": 9, + "name": "Connecticut ave. (L)", + "type": "property", + "owner": null, + "mortgaged": false, + "group": "lightblue", + "cost": 120, + "houses": 0 + }, + { + "id": 10, + "name": "Just Visiting", + "type": "safe" + }, + { + "id": 11, + "name": "St. Charles pl. (V)", + "type": "property", + "owner": 3, + "mortgaged": false, + "group": "violet", + "cost": 140, + "houses": 0 + }, + { + "id": 12, + "name": "Electric Co.", + "type": "utility", + "owner": 2, + "mortgaged": false, + "group": "utility", + "cost": 150 + }, + { + "id": 13, + "name": "States ave. (V)", + "type": "property", + "owner": null, + "mortgaged": false, + "group": "violet", + "cost": 140, + "houses": 0 + }, + { + "id": 14, + "name": "Virginia ave. (V)", + "type": "property", + "owner": null, + "mortgaged": false, + "group": "violet", + "cost": 160, + "houses": 0 + }, + { + "id": 15, + "name": "Pennsylvania RR", + "type": "railroad", + "owner": null, + "mortgaged": false, + "group": "railroad", + "cost": 200 + }, + { + "id": 16, + "name": "St. James pl. (O)", + "type": "property", + "owner": null, + "mortgaged": false, + "group": "orange", + "cost": 180, + "houses": 0 + }, + { + "id": 17, + "name": "Community Chest ii", + "type": "cc" + }, + { + "id": 18, + "name": "Tennessee ave. (O)", + "type": "property", + "owner": null, + "mortgaged": false, + "group": "orange", + "cost": 180, + "houses": 0 + }, + { + "id": 19, + "name": "New York ave. (O)", + "type": "property", + "owner": 3, + "mortgaged": false, + "group": "orange", + "cost": 200, + "houses": 0 + }, + { + "id": 20, + "name": "Free Parking", + "type": "safe" + }, + { + "id": 21, + "name": "Kentucky ave. (R)", + "type": "property", + "owner": null, + "mortgaged": false, + "group": "red", + "cost": 220, + "houses": 0 + }, + { + "id": 22, + "name": "Chance ii", + "type": "chance" + }, + { + "id": 23, + "name": "Indiana ave. (R)", + "type": "property", + "owner": null, + "mortgaged": false, + "group": "red", + "cost": 220, + "houses": 0 + }, + { + "id": 24, + "name": "Illinois ave. (R)", + "type": "property", + "owner": null, + "mortgaged": false, + "group": "red", + "cost": 240, + "houses": 0 + }, + { + "id": 25, + "name": "B&O RR", + "type": "railroad", + "owner": 3, + "mortgaged": false, + "group": "railroad", + "cost": 200 + }, + { + "id": 26, + "name": "Atlantic ave. (Y)", + "type": "property", + "owner": null, + "mortgaged": false, + "group": "yellow", + "cost": 260, + "houses": 0 + }, + { + "id": 27, + "name": "Ventnor ave. (Y)", + "type": "property", + "owner": null, + "mortgaged": false, + "group": "yellow", + "cost": 260, + "houses": 0 + }, + { + "id": 28, + "name": "Water Works", + "type": "utility", + "owner": 2, + "mortgaged": false, + "group": "utility", + "cost": 150 + }, + { + "id": 29, + "name": "Marvin Gardens (Y)", + "type": "property", + "owner": null, + "mortgaged": false, + "group": "yellow", + "cost": 280, + "houses": 0 + }, + { + "id": 30, + "name": "GO TO JAIL", + "type": "gotojail" + }, + { + "id": 31, + "name": "Pacific ave. (G)", + "type": "property", + "owner": 2, + "mortgaged": false, + "group": "green", + "cost": 300, + "houses": 0 + }, + { + "id": 32, + "name": "N. Carolina ave. (G)", + "type": "property", + "owner": null, + "mortgaged": false, + "group": "green", + "cost": 300, + "houses": 0 + }, + { + "id": 33, + "name": "Community Chest iii", + "type": "cc" + }, + { + "id": 34, + "name": "Pennsylvania ave. (G)", + "type": "property", + "owner": null, + "mortgaged": false, + "group": "green", + "cost": 320, + "houses": 0 + }, + { + "id": 35, + "name": "Short Line RR", + "type": "railroad", + "owner": null, + "mortgaged": false, + "group": "railroad", + "cost": 200 + }, + { + "id": 36, + "name": "Chance iii", + "type": "chance" + }, + { + "id": 37, + "name": "Park place (D)", + "type": "property", + "owner": null, + "mortgaged": false, + "group": "darkblue", + "cost": 350, + "houses": 0 + }, + { + "id": 38, + "name": "Luxury Tax", + "type": "tax" + }, + { + "id": 39, + "name": "Boardwalk (D)", + "type": "property", + "owner": null, + "mortgaged": false, + "group": "darkblue", + "cost": 400, + "houses": 0 + } ], - "groups": { - "purple": { "letter": "P", "size": 2, "houseCost": 50, "squares": [1, 3] }, - "lightblue": { "letter": "L", "size": 3, "houseCost": 50, "squares": [6, 8, 9] }, - "violet": { "letter": "V", "size": 3, "houseCost": 100, "squares": [11, 13, 14] }, - "orange": { "letter": "O", "size": 3, "houseCost": 100, "squares": [16, 18, 19] }, - "red": { "letter": "R", "size": 3, "houseCost": 150, "squares": [21, 23, 24] }, - "yellow": { "letter": "Y", "size": 3, "houseCost": 150, "squares": [26, 27, 29] }, - "green": { "letter": "G", "size": 3, "houseCost": 200, "squares": [31, 32, 34] }, - "darkblue": { "letter": "D", "size": 2, "houseCost": 200, "squares": [37, 39] }, - "railroad": { "size": 4, "squares": [5, 15, 25, 35], "rentByCount": [25, 50, 100, 200] }, - "utility": { "size": 2, "squares": [12, 28], "rollMultiplierByCount": [4, 10] } - }, - "log": [], - "lastUpdated": null -} + "log": [ + { + "text": "Landed on Community Chest ii", + "player": "alice", + "timestamp": "2026-02-21 10:41:53" + }, + { + "text": "You are Assessed for street repairs.", + "player": "alice" + }, + { + "text": "bob's turn \u2014 $1400 on Oriental ave. (L)", + "player": "bob", + "timestamp": "2026-02-21 10:41:57" + }, + { + "text": "roll is 1, 5", + "player": "bob", + "timestamp": "2026-02-21 10:41:58" + }, + { + "text": "Landed on Electric Co.", + "player": "bob", + "timestamp": "2026-02-21 10:41:58" + }, + { + "text": "charlie's turn \u2014 $1160 on New York ave. (O)", + "player": "charlie", + "timestamp": "2026-02-21 10:42:00" + }, + { + "text": "roll is 3, 3", + "player": "charlie", + "timestamp": "2026-02-21 10:42:01" + }, + { + "text": "Landed on B&O RR", + "player": "charlie", + "timestamp": "2026-02-21 10:42:01" + }, + { + "text": "charlie's turn \u2014 $960 on B&O RR", + "player": "charlie", + "timestamp": "2026-02-21 10:42:04" + }, + { + "text": "roll is 4, 1", + "player": "charlie", + "timestamp": "2026-02-21 10:42:05" + }, + { + "text": "Landed on GO TO JAIL!", + "player": "charlie", + "timestamp": "2026-02-21 10:42:05" + }, + { + "text": "alice's turn \u2014 $1400 on Community Chest ii", + "player": "alice", + "timestamp": "2026-02-21 10:42:06" + }, + { + "text": "roll is 6, 2", + "player": "alice", + "timestamp": "2026-02-21 10:42:07" + }, + { + "text": "Landed on B&O RR", + "player": "alice", + "timestamp": "2026-02-21 10:42:08" + }, + { + "text": "Paid $25 rent to charlie", + "player": "alice" + }, + { + "text": "bob's turn \u2014 $1250 on Electric Co.", + "player": "bob", + "timestamp": "2026-02-21 10:42:09" + }, + { + "text": "roll is 4, 4", + "player": "bob", + "timestamp": "2026-02-21 10:42:10" + }, + { + "text": "Landed on Free Parking", + "player": "bob", + "timestamp": "2026-02-21 10:42:11" + }, + { + "text": "bob's turn \u2014 $1250 on Free Parking", + "player": "bob", + "timestamp": "2026-02-21 10:42:12" + }, + { + "text": "roll is 6, 2", + "player": "bob", + "timestamp": "2026-02-21 10:42:13" + }, + { + "text": "Landed on Water Works", + "player": "bob", + "timestamp": "2026-02-21 10:42:14" + }, + { + "text": "charlie's turn \u2014 $985 on JAIL", + "player": "charlie", + "timestamp": "2026-02-21 10:42:16" + }, + { + "text": "roll is 6, 5", + "player": "charlie", + "timestamp": "2026-02-21 10:42:17" + }, + { + "text": "alice's turn \u2014 $1375 on B&O RR", + "player": "alice", + "timestamp": "2026-02-21 10:42:18" + }, + { + "text": "roll is 2, 3", + "player": "alice", + "timestamp": "2026-02-21 10:42:20" + }, + { + "text": "Landed on GO TO JAIL!", + "player": "alice", + "timestamp": "2026-02-21 10:42:20" + }, + { + "text": "bob's turn \u2014 $1100 on Water Works", + "player": "bob", + "timestamp": "2026-02-21 10:42:21" + }, + { + "text": "roll is 1, 2", + "player": "bob", + "timestamp": "2026-02-21 10:42:22" + }, + { + "text": "Landed on Pacific ave. (G)", + "player": "bob", + "timestamp": "2026-02-21 10:42:22" + }, + { + "text": "charlie's turn \u2014 $985 on JAIL", + "player": "charlie", + "timestamp": "2026-02-21 10:42:24" + } + ], + "lastUpdated": "2026-02-21T10:42:27.015680+00:00" +} \ No newline at end of file