""" Sub-parser for the interactive house buying/selling dialog in BSD monop. The dialog flow (buying): 1. list_cur: "Kentucky ave. (R) (0) Indiana ave. (R) (0) Illinois ave. (R) (0)" 2. "Houses will cost $N" 3. "How many houses do you wish to buy for" 4. Prompts: "Kentucky ave. (R) (0): " → user enters number 5. Repeat for each property in monopoly 6. "You asked for N houses for $X" 7. "Is that ok? " → y/n Selling is identical except: - "Houses will get you $N apiece" (step 2) - "How many houses do you wish to sell from" (step 3) - "You asked to sell N houses for $X" (step 6) This parser tracks the prompts (which tell us property name + current houses) and the user responses (how many to add/remove), then applies the changes when confirmed. Usage: hp = HouseParser() for each line: result = hp.feed(sender, message) if result is not None: # result is a dict: {"action": "buy"|"sell", "changes": {sq_id: new_count}} # or {"action": "cancel"} if rejected """ import re # Lazy import to avoid circular dependency with monop_parser _SQUARE_BY_NAME = None def _get_square_by_name(): global _SQUARE_BY_NAME if _SQUARE_BY_NAME is None: from monop_parser import SQUARE_BY_NAME _SQUARE_BY_NAME = SQUARE_BY_NAME return _SQUARE_BY_NAME # Property prompt: "PropName (N): " where N is digit or H PROMPT_RE = re.compile(r'^(.+?) \((\d|H)\):\s*$') # list_cur line: "PropName (N) PropName (N) ..." (space-separated, trailing space) LIST_CUR_RE = re.compile(r'^((?:.+? \(\d|H\) )+)\s*$') HOUSES_COST_RE = re.compile(r'^Houses will cost \$(\d+)$') HOUSES_GET_RE = re.compile(r'^Houses will get you \$(\d+) apiece$') HOW_MANY_BUY_RE = re.compile(r'^How many houses do you wish to buy for$') HOW_MANY_SELL_RE = re.compile(r'^How many houses do you wish to sell from$') ASKED_BUY_RE = re.compile(r'^You asked for (\d+) houses for \$(\d+)$') ASKED_SELL_RE = re.compile(r'^You asked to sell (\d+) houses for \$(\d+)$') IS_THAT_OK_RE = re.compile(r'^Is that ok\?') SPREAD_TOO_WIDE_RE = re.compile(r"^That makes the spread too wide") TOO_MANY_RE = re.compile(r"^That's too many") def _resolve_property_name(name): """Resolve a full property name from a prompt to a square ID. Prompts use the full name like 'Kentucky ave. (R)' — this is the square name from the C board array. """ return _get_square_by_name().get(name) class HouseParser: """State machine for parsing house buy/sell dialogs.""" def __init__(self): self.reset() def reset(self): """Reset to idle state.""" self.state = "idle" # idle, prompting, confirming self.action = None # "buy" or "sell" self.price_per = 0 # cost per house (buy) or refund per house (sell) self.prompts = [] # list of (sq_id, current_houses) in prompt order self.responses = [] # list of int (user's count per prompt) self._current_prompt = None # (sq_id, current_houses) waiting for response @property def active(self): """True if we're in the middle of a house dialog.""" return self.state != "idle" def feed(self, sender, msg): """Feed a line. Returns a result dict when the dialog completes, else None. sender: "monop" for bot lines, anything else for player input msg: the message text (stripped of IRC prefix) Returns: None — dialog still in progress (or not in a dialog) {"action": "buy"|"sell", "changes": {sq_id: new_house_count}, "cost": int} {"action": "cancel"} — player rejected or dialog reset """ msg = msg.strip() is_bot = (sender == "monop") if self.state == "idle": return self._handle_idle(msg, is_bot) elif self.state == "prompting": return self._handle_prompting(msg, is_bot) elif self.state == "confirming": return self._handle_confirming(msg, is_bot) return None def _handle_idle(self, msg, is_bot): if not is_bot: return None m = HOUSES_COST_RE.match(msg) if m: self.action = "buy" self.price_per = int(m.group(1)) return None m = HOUSES_GET_RE.match(msg) if m: self.action = "sell" self.price_per = int(m.group(1)) return None if HOW_MANY_BUY_RE.match(msg): self.state = "prompting" self.action = "buy" self.prompts = [] self.responses = [] return None if HOW_MANY_SELL_RE.match(msg): self.state = "prompting" self.action = "sell" self.prompts = [] self.responses = [] return None return None def _handle_prompting(self, msg, is_bot): if is_bot: # Property prompt from monop m = PROMPT_RE.match(msg) if m: name = m.group(1) houses_str = m.group(2) houses = 5 if houses_str == 'H' else int(houses_str) sq_id = _resolve_property_name(name) if sq_id is not None: self._current_prompt = (sq_id, houses) self.prompts.append((sq_id, houses)) else: # Can't resolve — still track position self._current_prompt = (None, houses) self.prompts.append((None, houses)) # Hotel prompt during buying: C auto-sets input to 0, no user input if self.action == "buy" and houses == 5: self.responses.append(0) self._current_prompt = None # Zero houses prompt during selling: C auto-sets input to 0 elif self.action == "sell" and houses == 0: self.responses.append(0) self._current_prompt = None return None # Confirmation line — all prompts done m = ASKED_BUY_RE.match(msg) or ASKED_SELL_RE.match(msg) if m: self.state = "confirming" self._current_prompt = None return None # Error: spread too wide or too many — retry if SPREAD_TOO_WIDE_RE.match(msg) or TOO_MANY_RE.match(msg): # C code retries — for "too many" it re-prompts same property, # for "spread too wide" it restarts all prompts if SPREAD_TOO_WIDE_RE.match(msg): self.prompts = [] self.responses = [] self._current_prompt = None elif TOO_MANY_RE.match(msg): # Re-prompt the same property (pop last response and prompt; # C will re-output the prompt line which re-adds it) if self.responses: self.responses.pop() if self.prompts: self.prompts.pop() self._current_prompt = None return None return None else: # Player response to a prompt if self._current_prompt is not None: try: count = int(msg.lstrip('.')) self.responses.append(count) except ValueError: pass # invalid input, C will re-prompt self._current_prompt = None return None def _handle_confirming(self, msg, is_bot): if is_bot: # "Is that ok?" — wait for player response return None # Player says yes or no answer = msg.lstrip('.').lower() if answer in ('y', 'yes'): result = self._compute_result() self.reset() return result else: # Rejected self.reset() return {"action": "cancel"} def _compute_result(self): """Compute the final house counts after confirmation.""" changes = {} for i, (sq_id, current) in enumerate(self.prompts): if sq_id is None: continue if i >= len(self.responses): continue delta = self.responses[i] if delta == 0: continue if self.action == "buy": new_count = current + delta else: # sell new_count = current - delta changes[sq_id] = max(0, min(5, new_count)) total_houses = sum(self.responses[:len(self.prompts)]) cost = total_houses * self.price_per return { "action": self.action, "changes": changes, "cost": cost, }