From d2bd66ba780e278eb17cdc947138a0ea832a8b5b Mon Sep 17 00:00:00 2001 From: Jarvis Date: Sat, 21 Feb 2026 10:30:53 +0000 Subject: [PATCH] Add game log to parser output for web viewer Parser now accumulates log entries for key game events: - Turn starts (checkpoint lines) - Rolls, movement, passing GO - Rent payments - Card draws (with card text) - Auctions, trades, resignations - Jail (triple doubles, GO TO JAIL) - Game start and winner Log is capped at 100 entries in GameState, last 30 emitted in get_state() to match what index.html expects. Also synced plugin's monop_parser.py copy. --- __pycache__/monop_parser.cpython-310.pyc | Bin 23546 -> 24976 bytes monop_parser.py | 39 +++++++++++++++++- .../__pycache__/monop_parser.cpython-310.pyc | Bin 23394 -> 23560 bytes plugins/monop/monop_parser.py | 39 +++++++++++++++++- 4 files changed, 74 insertions(+), 4 deletions(-) diff --git a/__pycache__/monop_parser.cpython-310.pyc b/__pycache__/monop_parser.cpython-310.pyc index 38d2321b17cca2bef111b5b3870e9765f9528545..22960d4a0bf84bfb7b54297593d5dc6869072553 100644 GIT binary patch delta 8184 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 O(A)VfijnyjX!swKYW3^@ diff --git a/monop_parser.py b/monop_parser.py index 7fab73e..cabbfde 100644 --- a/monop_parser.py +++ b/monop_parser.py @@ -97,6 +97,8 @@ class GameState: self.property_owner = {} # square_id -> player_number (1-based) self.property_mortgaged = {} # square_id -> bool self.property_houses = {} # square_id -> int (5 = hotel) + # Game log for the web viewer + self.log = [] # list of {"timestamp": str, "text": str, "player": str|None} self.last_roll = (0, 0) self.last_roll_total = 0 self.pending_buy_cost = None # cost of property being offered @@ -111,6 +113,15 @@ class GameState: # Track current player's location before card movement self.pending_rent_owner = None # name of rent owner + def add_log(self, text, player=None, timestamp=None): + """Append an entry to the game log (kept to last 100 entries).""" + entry = {"text": text, "player": player} + if timestamp: + entry["timestamp"] = timestamp + self.log.append(entry) + if len(self.log) > 100: + self.log = self.log[-100:] + def get_player(self, name=None, number=None): """Find player by name or number (1-based).""" for p in self.players: @@ -422,6 +433,7 @@ class MonopParser: break g.phase = "playing" g.game_active = True + g.add_log(f"Game started! {name} goes first", timestamp=timestamp) return # "Player N, say 'me' please" - just note it @@ -454,6 +466,8 @@ class MonopParser: ) return + g.add_log(f"{name}'s turn — ${money} on {sq_name}", player=name, timestamp=timestamp) + player = g.get_player(name=name, number=num) if player is None: # New player we haven't seen (mid-game join) @@ -509,6 +523,7 @@ class MonopParser: d1, d2 = int(m.group(1)), int(m.group(2)) g.last_roll = (d1, d2) g.last_roll_total = d1 + d2 + g.add_log(f"roll is {d1}, {d2}", player=cp.name if cp else None, timestamp=timestamp) return # ===== MOVEMENT ===== @@ -523,12 +538,16 @@ class MonopParser: cp.location = 40 # JAIL cp.in_jail = True cp.jail_turns = 0 + g.add_log(f"Landed on GO TO JAIL!", player=cp.name, timestamp=timestamp) + else: + g.add_log(f"Landed on {sq_name}", player=cp.name, timestamp=timestamp) return # ===== PASS GO ===== if self.PASS_GO_RE.match(msg): if cp: cp.money += 200 + g.add_log("Passed GO — collected $200", player=cp.name, timestamp=timestamp) return # ===== SAFE PLACE ===== @@ -602,6 +621,7 @@ class MonopParser: cp.in_jail = True cp.jail_turns = 0 cp.doubles_count = 0 + g.add_log("3 doubles — go to jail!", player=cp.name, timestamp=timestamp) return # ===== GO TO JAIL (landing on square) ===== @@ -774,6 +794,8 @@ class MonopParser: sq_id = cp.location if 0 <= sq_id < 40: g.property_owner[sq_id] = num + sq_name = g.location_name(sq_id) + g.add_log(f"Won auction for {sq_name} at ${price}", player=name, timestamp=timestamp) return if self.NOBODY_RE.match(msg): @@ -834,11 +856,14 @@ class MonopParser: if self.RESIGN_TO_BANK_RE.match(msg): # Player resigns to bank - remove them if cp: + g.add_log(f"{cp.name} resigned to the bank", player=cp.name, timestamp=timestamp) self._remove_player(cp) return m = self.WINS_RE.match(msg) if m: + winner = m.group(1) + g.add_log(f"{winner} WINS!", player=winner, timestamp=timestamp) g.phase = "over" g.game_active = False return @@ -1006,10 +1031,14 @@ class MonopParser: return cp.money -= amount # Pay to owner - if g.pending_rent_owner: - owner = g.get_player(name=g.pending_rent_owner) + owner_name = g.pending_rent_owner + if owner_name: + owner = g.get_player(name=owner_name) if owner: owner.money += amount + g.add_log(f"Paid ${amount} rent to {owner_name}", player=cp.name) + else: + g.add_log(f"Paid ${amount} rent", player=cp.name) g.pending_rent_owner = None def _process_card(self, lines): @@ -1022,6 +1051,9 @@ class MonopParser: return text = "\n".join(lines) + # Log the card draw (use first non-empty line as summary) + card_summary = next((l.strip() for l in lines if l.strip()), "Drew a card") + g.add_log(card_summary, player=cp.name) # GET OUT OF JAIL FREE if "GET OUT OF JAIL FREE" in text: @@ -1124,6 +1156,7 @@ class MonopParser: p2.get_out_of_jail_free_cards += self._trade_gojf1 p2.get_out_of_jail_free_cards -= self._trade_gojf2 p1.get_out_of_jail_free_cards += self._trade_gojf2 + g.add_log(f"Trade completed between {p1.name} and {p2.name}") self._trade_state = None self._trade_player1 = None self._trade_player2 = None @@ -1146,6 +1179,7 @@ class MonopParser: target.money += cp.money if target: target.get_out_of_jail_free_cards += cp.get_out_of_jail_free_cards + g.add_log(f"{cp.name} resigned to {target.name if target else 'bank'}", player=cp.name) self._remove_player(cp) def _remove_player(self, player): @@ -1193,6 +1227,7 @@ class MonopParser: "players": [p.to_dict() for p in g.players], "currentPlayer": g.current_player.number if g.current_player else None, "squares": squares, + "log": g.log[-30:], } diff --git a/plugins/monop/__pycache__/monop_parser.cpython-310.pyc b/plugins/monop/__pycache__/monop_parser.cpython-310.pyc index aa515f0927265c72ce7d6bc4fd2c34f0c219855c..a59290ac0cbb0efe370eff775044e071615f99a1 100644 GIT binary patch delta 5835 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 diff --git a/plugins/monop/monop_parser.py b/plugins/monop/monop_parser.py index 7fab73e..cabbfde 100644 --- a/plugins/monop/monop_parser.py +++ b/plugins/monop/monop_parser.py @@ -97,6 +97,8 @@ class GameState: self.property_owner = {} # square_id -> player_number (1-based) self.property_mortgaged = {} # square_id -> bool self.property_houses = {} # square_id -> int (5 = hotel) + # Game log for the web viewer + self.log = [] # list of {"timestamp": str, "text": str, "player": str|None} self.last_roll = (0, 0) self.last_roll_total = 0 self.pending_buy_cost = None # cost of property being offered @@ -111,6 +113,15 @@ class GameState: # Track current player's location before card movement self.pending_rent_owner = None # name of rent owner + def add_log(self, text, player=None, timestamp=None): + """Append an entry to the game log (kept to last 100 entries).""" + entry = {"text": text, "player": player} + if timestamp: + entry["timestamp"] = timestamp + self.log.append(entry) + if len(self.log) > 100: + self.log = self.log[-100:] + def get_player(self, name=None, number=None): """Find player by name or number (1-based).""" for p in self.players: @@ -422,6 +433,7 @@ class MonopParser: break g.phase = "playing" g.game_active = True + g.add_log(f"Game started! {name} goes first", timestamp=timestamp) return # "Player N, say 'me' please" - just note it @@ -454,6 +466,8 @@ class MonopParser: ) return + g.add_log(f"{name}'s turn — ${money} on {sq_name}", player=name, timestamp=timestamp) + player = g.get_player(name=name, number=num) if player is None: # New player we haven't seen (mid-game join) @@ -509,6 +523,7 @@ class MonopParser: d1, d2 = int(m.group(1)), int(m.group(2)) g.last_roll = (d1, d2) g.last_roll_total = d1 + d2 + g.add_log(f"roll is {d1}, {d2}", player=cp.name if cp else None, timestamp=timestamp) return # ===== MOVEMENT ===== @@ -523,12 +538,16 @@ class MonopParser: cp.location = 40 # JAIL cp.in_jail = True cp.jail_turns = 0 + g.add_log(f"Landed on GO TO JAIL!", player=cp.name, timestamp=timestamp) + else: + g.add_log(f"Landed on {sq_name}", player=cp.name, timestamp=timestamp) return # ===== PASS GO ===== if self.PASS_GO_RE.match(msg): if cp: cp.money += 200 + g.add_log("Passed GO — collected $200", player=cp.name, timestamp=timestamp) return # ===== SAFE PLACE ===== @@ -602,6 +621,7 @@ class MonopParser: cp.in_jail = True cp.jail_turns = 0 cp.doubles_count = 0 + g.add_log("3 doubles — go to jail!", player=cp.name, timestamp=timestamp) return # ===== GO TO JAIL (landing on square) ===== @@ -774,6 +794,8 @@ class MonopParser: sq_id = cp.location if 0 <= sq_id < 40: g.property_owner[sq_id] = num + sq_name = g.location_name(sq_id) + g.add_log(f"Won auction for {sq_name} at ${price}", player=name, timestamp=timestamp) return if self.NOBODY_RE.match(msg): @@ -834,11 +856,14 @@ class MonopParser: if self.RESIGN_TO_BANK_RE.match(msg): # Player resigns to bank - remove them if cp: + g.add_log(f"{cp.name} resigned to the bank", player=cp.name, timestamp=timestamp) self._remove_player(cp) return m = self.WINS_RE.match(msg) if m: + winner = m.group(1) + g.add_log(f"{winner} WINS!", player=winner, timestamp=timestamp) g.phase = "over" g.game_active = False return @@ -1006,10 +1031,14 @@ class MonopParser: return cp.money -= amount # Pay to owner - if g.pending_rent_owner: - owner = g.get_player(name=g.pending_rent_owner) + owner_name = g.pending_rent_owner + if owner_name: + owner = g.get_player(name=owner_name) if owner: owner.money += amount + g.add_log(f"Paid ${amount} rent to {owner_name}", player=cp.name) + else: + g.add_log(f"Paid ${amount} rent", player=cp.name) g.pending_rent_owner = None def _process_card(self, lines): @@ -1022,6 +1051,9 @@ class MonopParser: return text = "\n".join(lines) + # Log the card draw (use first non-empty line as summary) + card_summary = next((l.strip() for l in lines if l.strip()), "Drew a card") + g.add_log(card_summary, player=cp.name) # GET OUT OF JAIL FREE if "GET OUT OF JAIL FREE" in text: @@ -1124,6 +1156,7 @@ class MonopParser: p2.get_out_of_jail_free_cards += self._trade_gojf1 p2.get_out_of_jail_free_cards -= self._trade_gojf2 p1.get_out_of_jail_free_cards += self._trade_gojf2 + g.add_log(f"Trade completed between {p1.name} and {p2.name}") self._trade_state = None self._trade_player1 = None self._trade_player2 = None @@ -1146,6 +1179,7 @@ class MonopParser: target.money += cp.money if target: target.get_out_of_jail_free_cards += cp.get_out_of_jail_free_cards + g.add_log(f"{cp.name} resigned to {target.name if target else 'bank'}", player=cp.name) self._remove_player(cp) def _remove_player(self, player): @@ -1193,6 +1227,7 @@ class MonopParser: "players": [p.to_dict() for p in g.players], "currentPlayer": g.current_player.number if g.current_player else None, "squares": squares, + "log": g.log[-30:], }