392 lines
13 KiB
Python
392 lines
13 KiB
Python
"""Parser for monop-irc game output messages."""
|
|
|
|
import re
|
|
from board_data import NAME_TO_ID, SQUARES
|
|
|
|
|
|
def find_square_id(name):
|
|
"""Look up a square ID from a name string."""
|
|
name = name.strip().lower()
|
|
if name in NAME_TO_ID:
|
|
return NAME_TO_ID[name]
|
|
# Try partial match
|
|
for key, sid in NAME_TO_ID.items():
|
|
if name in key or key in name:
|
|
return sid
|
|
return None
|
|
|
|
|
|
class MonopParser:
|
|
"""Parses monop-irc game messages and updates game state."""
|
|
|
|
def __init__(self):
|
|
self.reset()
|
|
|
|
def reset(self):
|
|
self.players = []
|
|
self.current_player_idx = 0
|
|
self.squares = [self._init_square(sq) for sq in SQUARES]
|
|
self.log = []
|
|
self.game_started = False
|
|
self._parsing_holdings = False
|
|
self._holdings_player = None
|
|
self._parsing_board = False
|
|
self._awaiting_buy = False
|
|
self._last_roll = None
|
|
|
|
def _init_square(self, sq):
|
|
return {
|
|
"id": sq["id"],
|
|
"name": sq["name"],
|
|
"type": sq["type"],
|
|
"owner": -1,
|
|
"cost": sq["cost"],
|
|
"mortgaged": False,
|
|
"houses": 0,
|
|
"monopoly": False,
|
|
"group": sq.get("group"),
|
|
"rent": sq.get("rent", [0]),
|
|
}
|
|
|
|
def get_state(self):
|
|
return {
|
|
"players": [
|
|
{
|
|
"name": p["name"],
|
|
"number": p["number"],
|
|
"money": p["money"],
|
|
"location": p["location"],
|
|
"inJail": p["in_jail"],
|
|
"jailTurns": p["jail_turns"],
|
|
"goJailFreeCards": p["gojf"],
|
|
"properties": p["properties"],
|
|
"numRailroads": p["num_rr"],
|
|
"numUtilities": p["num_util"],
|
|
}
|
|
for p in self.players
|
|
],
|
|
"currentPlayer": self.current_player_idx,
|
|
"squares": self.squares,
|
|
"log": self.log[-100:], # keep last 100 log entries
|
|
}
|
|
|
|
def _cur_player(self):
|
|
if self.players:
|
|
return self.players[self.current_player_idx]
|
|
return None
|
|
|
|
def _find_player_by_name(self, name):
|
|
for i, p in enumerate(self.players):
|
|
if p["name"].lower() == name.lower():
|
|
return i, p
|
|
return None, None
|
|
|
|
def _add_log(self, text, player=None):
|
|
from datetime import datetime, timezone
|
|
self.log.append({
|
|
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
"text": text,
|
|
"player": player,
|
|
})
|
|
|
|
def add_player(self, name, number):
|
|
self.players.append({
|
|
"name": name,
|
|
"number": number,
|
|
"money": 1500,
|
|
"location": 0,
|
|
"in_jail": False,
|
|
"jail_turns": 0,
|
|
"gojf": 0,
|
|
"properties": [],
|
|
"num_rr": 0,
|
|
"num_util": 0,
|
|
})
|
|
self._add_log(f"{name} joined as player {number}", name)
|
|
|
|
def parse_line(self, line):
|
|
"""Parse a single line of monop-irc output. Returns True if state changed."""
|
|
line = line.strip()
|
|
if not line:
|
|
return False
|
|
|
|
changed = False
|
|
|
|
# --- Player setup ---
|
|
# "How many players? " -> game init
|
|
m = re.match(r"How many players\?", line)
|
|
if m:
|
|
self.reset()
|
|
self.game_started = True
|
|
return True
|
|
|
|
# Player name prompt: "Player N's name:"
|
|
m = re.match(r"Player (\d+)'s name:", line)
|
|
if m:
|
|
return False # the actual name comes as input
|
|
|
|
# --- Roll ---
|
|
m = re.match(r"roll is (\d+), (\d+)", line)
|
|
if m:
|
|
d1, d2 = int(m.group(1)), int(m.group(2))
|
|
self._last_roll = (d1, d2)
|
|
cp = self._cur_player()
|
|
if cp:
|
|
self._add_log(f"roll is {d1}, {d2}", cp["name"])
|
|
return True
|
|
|
|
# --- Movement ---
|
|
m = re.match(r"That puts you on (.+)", line)
|
|
if m:
|
|
sq_name = m.group(1)
|
|
sq_id = find_square_id(sq_name)
|
|
cp = self._cur_player()
|
|
if cp and sq_id is not None:
|
|
cp["location"] = sq_id
|
|
self._add_log(f"Landed on {sq_name}", cp["name"])
|
|
changed = True
|
|
return changed
|
|
|
|
# --- Pass Go ---
|
|
m = re.match(r"You pass .+ and get \$200", line)
|
|
if m:
|
|
cp = self._cur_player()
|
|
if cp:
|
|
cp["money"] += 200
|
|
self._add_log("Passed Go, collected $200", cp["name"])
|
|
return True
|
|
|
|
# --- Doubles ---
|
|
m = re.match(r"(.+) rolled doubles\.\s+Goes again", line)
|
|
if m:
|
|
self._add_log(f"{m.group(1)} rolled doubles", m.group(1))
|
|
return True
|
|
|
|
# --- 3 doubles -> jail ---
|
|
m = re.match(r"That's 3 doubles\.\s+You go to jail", line)
|
|
if m:
|
|
cp = self._cur_player()
|
|
if cp:
|
|
cp["location"] = 10
|
|
cp["in_jail"] = True
|
|
cp["jail_turns"] = 0
|
|
self._add_log("3 doubles! Go to jail", cp["name"])
|
|
return True
|
|
|
|
# --- Go to jail (from square) ---
|
|
if line == "Go directly to Jail":
|
|
cp = self._cur_player()
|
|
if cp:
|
|
cp["location"] = 10
|
|
cp["in_jail"] = True
|
|
cp["jail_turns"] = 0
|
|
self._add_log("Go to Jail!", cp["name"])
|
|
return True
|
|
|
|
# --- Property cost ---
|
|
m = re.match(r"That would cost \$(\d+)", line)
|
|
if m:
|
|
self._awaiting_buy = True
|
|
return False
|
|
|
|
# --- Buy prompt ---
|
|
m = re.match(r"Do you want to buy\?", line)
|
|
if m:
|
|
self._awaiting_buy = True
|
|
return False
|
|
|
|
# --- Rent ---
|
|
m = re.match(r"Owned by (.+)", line)
|
|
if m:
|
|
return False # rent line follows
|
|
|
|
m = re.match(r"rent is (\d+)", line)
|
|
if m:
|
|
rent_amt = int(m.group(1))
|
|
cp = self._cur_player()
|
|
if cp:
|
|
cp["money"] -= rent_amt
|
|
sq_id = cp["location"]
|
|
owner_idx = self.squares[sq_id]["owner"]
|
|
if 0 <= owner_idx < len(self.players):
|
|
self.players[owner_idx]["money"] += rent_amt
|
|
self._add_log(f"Paid ${rent_amt} rent to {self.players[owner_idx]['name']}", cp["name"])
|
|
return True
|
|
|
|
m = re.match(r"with (\d+) houses?, rent is (\d+)", line)
|
|
if m:
|
|
rent_amt = int(m.group(2))
|
|
cp = self._cur_player()
|
|
if cp:
|
|
cp["money"] -= rent_amt
|
|
sq_id = cp["location"]
|
|
owner_idx = self.squares[sq_id]["owner"]
|
|
if 0 <= owner_idx < len(self.players):
|
|
self.players[owner_idx]["money"] += rent_amt
|
|
self._add_log(f"Paid ${rent_amt} rent ({m.group(1)} houses)", cp["name"])
|
|
return True
|
|
|
|
m = re.match(r"with a hotel, rent is (\d+)", line)
|
|
if m:
|
|
rent_amt = int(m.group(1))
|
|
cp = self._cur_player()
|
|
if cp:
|
|
cp["money"] -= rent_amt
|
|
sq_id = cp["location"]
|
|
owner_idx = self.squares[sq_id]["owner"]
|
|
if 0 <= owner_idx < len(self.players):
|
|
self.players[owner_idx]["money"] += rent_amt
|
|
self._add_log(f"Paid ${rent_amt} rent (hotel)", cp["name"])
|
|
return True
|
|
|
|
# Utility rent: "rent is 4 * roll (N) = N" or "rent is 10 * roll (N) = N"
|
|
m = re.match(r"rent is (\d+) \* roll \((\d+)\) = (\d+)", line)
|
|
if m:
|
|
rent_amt = int(m.group(3))
|
|
cp = self._cur_player()
|
|
if cp:
|
|
cp["money"] -= rent_amt
|
|
sq_id = cp["location"]
|
|
owner_idx = self.squares[sq_id]["owner"]
|
|
if 0 <= owner_idx < len(self.players):
|
|
self.players[owner_idx]["money"] += rent_amt
|
|
self._add_log(f"Paid ${rent_amt} utility rent", cp["name"])
|
|
return True
|
|
|
|
# --- Safe place ---
|
|
if line == "That is a safe place":
|
|
return False
|
|
|
|
# --- Income tax ---
|
|
m = re.match(r"You pay \$(\d+)", line)
|
|
if m:
|
|
cp = self._cur_player()
|
|
if cp:
|
|
cp["money"] -= int(m.group(1))
|
|
self._add_log(f"Paid ${m.group(1)} tax", cp["name"])
|
|
return True
|
|
|
|
# --- Luxury tax ---
|
|
if "Luxury tax" in line and "$75" in line:
|
|
cp = self._cur_player()
|
|
if cp:
|
|
cp["money"] -= 75
|
|
self._add_log("Paid $75 luxury tax", cp["name"])
|
|
return True
|
|
|
|
# --- You own it ---
|
|
if line == "You own it.":
|
|
return False
|
|
|
|
# --- Holdings ---
|
|
m = re.match(r"(.+)'s \((\d+)\) holdings \(Total worth: \$(\d+)\):", line)
|
|
if m:
|
|
name = m.group(1)
|
|
self._parsing_holdings = True
|
|
idx, player = self._find_player_by_name(name)
|
|
self._holdings_player = idx
|
|
return False
|
|
|
|
# Holdings cash line
|
|
if self._parsing_holdings and self._holdings_player is not None:
|
|
m = re.match(r"\s+\$(\d+)", line)
|
|
if m:
|
|
self.players[self._holdings_player]["money"] = int(m.group(1))
|
|
# Check for GOJF cards
|
|
gojf_m = re.search(r"(\d+) get-out-of-jail-free card", line)
|
|
if gojf_m:
|
|
self.players[self._holdings_player]["gojf"] = int(gojf_m.group(1))
|
|
return True
|
|
|
|
# End holdings parsing on blank or non-indented line
|
|
if self._parsing_holdings and not line.startswith(" ") and not line.startswith("Name"):
|
|
self._parsing_holdings = False
|
|
self._holdings_player = None
|
|
|
|
# --- Board header ---
|
|
if "Name Own Price Mg # Rent" in line:
|
|
self._parsing_board = True
|
|
return False
|
|
|
|
# --- Mortgage ---
|
|
m = re.match(r"(.+) is mortgaged", line)
|
|
if m:
|
|
sq_id = find_square_id(m.group(1))
|
|
if sq_id is not None:
|
|
self.squares[sq_id]["mortgaged"] = True
|
|
self._add_log(f"{m.group(1)} mortgaged", self._cur_player()["name"] if self._cur_player() else None)
|
|
return True
|
|
|
|
m = re.match(r"(.+) is unmortgaged", line)
|
|
if m:
|
|
sq_id = find_square_id(m.group(1))
|
|
if sq_id is not None:
|
|
self.squares[sq_id]["mortgaged"] = False
|
|
return True
|
|
|
|
# --- Houses bought/sold ---
|
|
m = re.match(r"(\d+) house(?:s)? (?:bought|added) (?:on|to) (.+)", line)
|
|
if m:
|
|
num = int(m.group(1))
|
|
sq_id = find_square_id(m.group(2))
|
|
if sq_id is not None:
|
|
self.squares[sq_id]["houses"] += num
|
|
self._add_log(f"{num} house(s) added to {m.group(2)}")
|
|
return True
|
|
|
|
# --- Next player turn ---
|
|
m = re.match(r"(.+)'s turn", line)
|
|
if m:
|
|
name = m.group(1)
|
|
idx, _ = self._find_player_by_name(name)
|
|
if idx is not None:
|
|
self.current_player_idx = idx
|
|
self._add_log(f"{name}'s turn", name)
|
|
return True
|
|
|
|
# --- Player leaves jail ---
|
|
if "out of jail" in line.lower():
|
|
cp = self._cur_player()
|
|
if cp:
|
|
cp["in_jail"] = False
|
|
cp["jail_turns"] = 0
|
|
self._add_log("Got out of jail", cp["name"])
|
|
return True
|
|
|
|
# --- Property bought (inferred from game flow) ---
|
|
# When a player buys, the game deducts money. We track via
|
|
# the "That would cost $N" + purchase flow. The game prints
|
|
# ownership changes in the board/holdings output.
|
|
|
|
# --- Debt message ---
|
|
m = re.match(r"That leaves you \$(\d+) in debt", line)
|
|
if m:
|
|
cp = self._cur_player()
|
|
if cp:
|
|
cp["money"] = -int(m.group(1))
|
|
return True
|
|
|
|
if line == "that leaves you broke":
|
|
cp = self._cur_player()
|
|
if cp:
|
|
cp["money"] = 0
|
|
return True
|
|
|
|
# --- Solvent ---
|
|
if "You are now Solvent" in line:
|
|
return False
|
|
|
|
return changed
|
|
|
|
def process_buy(self, player_idx, square_id):
|
|
"""Record a property purchase."""
|
|
p = self.players[player_idx]
|
|
sq = self.squares[square_id]
|
|
sq["owner"] = player_idx
|
|
p["properties"].append(square_id)
|
|
p["money"] -= sq["cost"]
|
|
if sq["type"] == "railroad":
|
|
p["num_rr"] += 1
|
|
elif sq["type"] == "utility":
|
|
p["num_util"] += 1
|
|
self._add_log(f"Bought {sq['name']} for ${sq['cost']}", p["name"])
|