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.
This commit is contained in:
Jarvis 2026-02-21 10:30:53 +00:00
parent cceec64a7c
commit d2bd66ba78
4 changed files with 74 additions and 4 deletions

View file

@ -97,6 +97,8 @@ class GameState:
self.property_owner = {} # square_id -> player_number (1-based) self.property_owner = {} # square_id -> player_number (1-based)
self.property_mortgaged = {} # square_id -> bool self.property_mortgaged = {} # square_id -> bool
self.property_houses = {} # square_id -> int (5 = hotel) 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 = (0, 0)
self.last_roll_total = 0 self.last_roll_total = 0
self.pending_buy_cost = None # cost of property being offered self.pending_buy_cost = None # cost of property being offered
@ -111,6 +113,15 @@ class GameState:
# Track current player's location before card movement # Track current player's location before card movement
self.pending_rent_owner = None # name of rent owner 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): def get_player(self, name=None, number=None):
"""Find player by name or number (1-based).""" """Find player by name or number (1-based)."""
for p in self.players: for p in self.players:
@ -422,6 +433,7 @@ class MonopParser:
break break
g.phase = "playing" g.phase = "playing"
g.game_active = True g.game_active = True
g.add_log(f"Game started! {name} goes first", timestamp=timestamp)
return return
# "Player N, say 'me' please" - just note it # "Player N, say 'me' please" - just note it
@ -454,6 +466,8 @@ class MonopParser:
) )
return return
g.add_log(f"{name}'s turn — ${money} on {sq_name}", player=name, timestamp=timestamp)
player = g.get_player(name=name, number=num) player = g.get_player(name=name, number=num)
if player is None: if player is None:
# New player we haven't seen (mid-game join) # 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)) d1, d2 = int(m.group(1)), int(m.group(2))
g.last_roll = (d1, d2) g.last_roll = (d1, d2)
g.last_roll_total = 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 return
# ===== MOVEMENT ===== # ===== MOVEMENT =====
@ -523,12 +538,16 @@ class MonopParser:
cp.location = 40 # JAIL cp.location = 40 # JAIL
cp.in_jail = True cp.in_jail = True
cp.jail_turns = 0 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 return
# ===== PASS GO ===== # ===== PASS GO =====
if self.PASS_GO_RE.match(msg): if self.PASS_GO_RE.match(msg):
if cp: if cp:
cp.money += 200 cp.money += 200
g.add_log("Passed GO — collected $200", player=cp.name, timestamp=timestamp)
return return
# ===== SAFE PLACE ===== # ===== SAFE PLACE =====
@ -602,6 +621,7 @@ class MonopParser:
cp.in_jail = True cp.in_jail = True
cp.jail_turns = 0 cp.jail_turns = 0
cp.doubles_count = 0 cp.doubles_count = 0
g.add_log("3 doubles — go to jail!", player=cp.name, timestamp=timestamp)
return return
# ===== GO TO JAIL (landing on square) ===== # ===== GO TO JAIL (landing on square) =====
@ -774,6 +794,8 @@ class MonopParser:
sq_id = cp.location sq_id = cp.location
if 0 <= sq_id < 40: if 0 <= sq_id < 40:
g.property_owner[sq_id] = num 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 return
if self.NOBODY_RE.match(msg): if self.NOBODY_RE.match(msg):
@ -834,11 +856,14 @@ class MonopParser:
if self.RESIGN_TO_BANK_RE.match(msg): if self.RESIGN_TO_BANK_RE.match(msg):
# Player resigns to bank - remove them # Player resigns to bank - remove them
if cp: if cp:
g.add_log(f"{cp.name} resigned to the bank", player=cp.name, timestamp=timestamp)
self._remove_player(cp) self._remove_player(cp)
return return
m = self.WINS_RE.match(msg) m = self.WINS_RE.match(msg)
if m: if m:
winner = m.group(1)
g.add_log(f"{winner} WINS!", player=winner, timestamp=timestamp)
g.phase = "over" g.phase = "over"
g.game_active = False g.game_active = False
return return
@ -1006,10 +1031,14 @@ class MonopParser:
return return
cp.money -= amount cp.money -= amount
# Pay to owner # Pay to owner
if g.pending_rent_owner: owner_name = g.pending_rent_owner
owner = g.get_player(name=g.pending_rent_owner) if owner_name:
owner = g.get_player(name=owner_name)
if owner: if owner:
owner.money += amount 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 g.pending_rent_owner = None
def _process_card(self, lines): def _process_card(self, lines):
@ -1022,6 +1051,9 @@ class MonopParser:
return return
text = "\n".join(lines) 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 # GET OUT OF JAIL FREE
if "GET OUT OF JAIL FREE" in text: 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_gojf1
p2.get_out_of_jail_free_cards -= self._trade_gojf2 p2.get_out_of_jail_free_cards -= self._trade_gojf2
p1.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_state = None
self._trade_player1 = None self._trade_player1 = None
self._trade_player2 = None self._trade_player2 = None
@ -1146,6 +1179,7 @@ class MonopParser:
target.money += cp.money target.money += cp.money
if target: if target:
target.get_out_of_jail_free_cards += cp.get_out_of_jail_free_cards 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) self._remove_player(cp)
def _remove_player(self, player): def _remove_player(self, player):
@ -1193,6 +1227,7 @@ class MonopParser:
"players": [p.to_dict() for p in g.players], "players": [p.to_dict() for p in g.players],
"currentPlayer": g.current_player.number if g.current_player else None, "currentPlayer": g.current_player.number if g.current_player else None,
"squares": squares, "squares": squares,
"log": g.log[-30:],
} }

View file

@ -97,6 +97,8 @@ class GameState:
self.property_owner = {} # square_id -> player_number (1-based) self.property_owner = {} # square_id -> player_number (1-based)
self.property_mortgaged = {} # square_id -> bool self.property_mortgaged = {} # square_id -> bool
self.property_houses = {} # square_id -> int (5 = hotel) 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 = (0, 0)
self.last_roll_total = 0 self.last_roll_total = 0
self.pending_buy_cost = None # cost of property being offered self.pending_buy_cost = None # cost of property being offered
@ -111,6 +113,15 @@ class GameState:
# Track current player's location before card movement # Track current player's location before card movement
self.pending_rent_owner = None # name of rent owner 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): def get_player(self, name=None, number=None):
"""Find player by name or number (1-based).""" """Find player by name or number (1-based)."""
for p in self.players: for p in self.players:
@ -422,6 +433,7 @@ class MonopParser:
break break
g.phase = "playing" g.phase = "playing"
g.game_active = True g.game_active = True
g.add_log(f"Game started! {name} goes first", timestamp=timestamp)
return return
# "Player N, say 'me' please" - just note it # "Player N, say 'me' please" - just note it
@ -454,6 +466,8 @@ class MonopParser:
) )
return return
g.add_log(f"{name}'s turn — ${money} on {sq_name}", player=name, timestamp=timestamp)
player = g.get_player(name=name, number=num) player = g.get_player(name=name, number=num)
if player is None: if player is None:
# New player we haven't seen (mid-game join) # 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)) d1, d2 = int(m.group(1)), int(m.group(2))
g.last_roll = (d1, d2) g.last_roll = (d1, d2)
g.last_roll_total = 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 return
# ===== MOVEMENT ===== # ===== MOVEMENT =====
@ -523,12 +538,16 @@ class MonopParser:
cp.location = 40 # JAIL cp.location = 40 # JAIL
cp.in_jail = True cp.in_jail = True
cp.jail_turns = 0 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 return
# ===== PASS GO ===== # ===== PASS GO =====
if self.PASS_GO_RE.match(msg): if self.PASS_GO_RE.match(msg):
if cp: if cp:
cp.money += 200 cp.money += 200
g.add_log("Passed GO — collected $200", player=cp.name, timestamp=timestamp)
return return
# ===== SAFE PLACE ===== # ===== SAFE PLACE =====
@ -602,6 +621,7 @@ class MonopParser:
cp.in_jail = True cp.in_jail = True
cp.jail_turns = 0 cp.jail_turns = 0
cp.doubles_count = 0 cp.doubles_count = 0
g.add_log("3 doubles — go to jail!", player=cp.name, timestamp=timestamp)
return return
# ===== GO TO JAIL (landing on square) ===== # ===== GO TO JAIL (landing on square) =====
@ -774,6 +794,8 @@ class MonopParser:
sq_id = cp.location sq_id = cp.location
if 0 <= sq_id < 40: if 0 <= sq_id < 40:
g.property_owner[sq_id] = num 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 return
if self.NOBODY_RE.match(msg): if self.NOBODY_RE.match(msg):
@ -834,11 +856,14 @@ class MonopParser:
if self.RESIGN_TO_BANK_RE.match(msg): if self.RESIGN_TO_BANK_RE.match(msg):
# Player resigns to bank - remove them # Player resigns to bank - remove them
if cp: if cp:
g.add_log(f"{cp.name} resigned to the bank", player=cp.name, timestamp=timestamp)
self._remove_player(cp) self._remove_player(cp)
return return
m = self.WINS_RE.match(msg) m = self.WINS_RE.match(msg)
if m: if m:
winner = m.group(1)
g.add_log(f"{winner} WINS!", player=winner, timestamp=timestamp)
g.phase = "over" g.phase = "over"
g.game_active = False g.game_active = False
return return
@ -1006,10 +1031,14 @@ class MonopParser:
return return
cp.money -= amount cp.money -= amount
# Pay to owner # Pay to owner
if g.pending_rent_owner: owner_name = g.pending_rent_owner
owner = g.get_player(name=g.pending_rent_owner) if owner_name:
owner = g.get_player(name=owner_name)
if owner: if owner:
owner.money += amount 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 g.pending_rent_owner = None
def _process_card(self, lines): def _process_card(self, lines):
@ -1022,6 +1051,9 @@ class MonopParser:
return return
text = "\n".join(lines) 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 # GET OUT OF JAIL FREE
if "GET OUT OF JAIL FREE" in text: 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_gojf1
p2.get_out_of_jail_free_cards -= self._trade_gojf2 p2.get_out_of_jail_free_cards -= self._trade_gojf2
p1.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_state = None
self._trade_player1 = None self._trade_player1 = None
self._trade_player2 = None self._trade_player2 = None
@ -1146,6 +1179,7 @@ class MonopParser:
target.money += cp.money target.money += cp.money
if target: if target:
target.get_out_of_jail_free_cards += cp.get_out_of_jail_free_cards 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) self._remove_player(cp)
def _remove_player(self, player): def _remove_player(self, player):
@ -1193,6 +1227,7 @@ class MonopParser:
"players": [p.to_dict() for p in g.players], "players": [p.to_dict() for p in g.players],
"currentPlayer": g.current_player.number if g.current_player else None, "currentPlayer": g.current_player.number if g.current_player else None,
"squares": squares, "squares": squares,
"log": g.log[-30:],
} }