New file: house_parser.py — self-contained state machine for the interactive
house buy/sell dialog. Parses:
- Property prompts ('PropName (N): ') to capture current house count
- Player responses (how many to buy/sell)
- Auto-skipped prompts (hotel during buy, 0 houses during sell)
- Error retries ('spread too wide', 'too many')
- Confirmation ('Is that ok?' → y/n)
Returns a result dict with {action, changes: {sq_id: new_count}, cost}
when the dialog completes.
Integration: monop_parser feeds every line (bot + player) to HouseParser.
On result, applies house count changes to property_houses.
Tests:
- test_house_parser.py (18 tests): buy/sell basics, hotel, error retry,
real log replay (lines 59, 354, 4386)
- test_parser_commands.py: 4 new integration tests (buy, sell, reject,
real log verification)
This closes the last tracking gap — house counts are now accurate at
purchase time, not just when rent is later charged.
346 lines
13 KiB
Python
346 lines
13 KiB
Python
#!/usr/bin/env python3
|
|
"""Tests for the house buying/selling sub-parser."""
|
|
|
|
import sys
|
|
import os
|
|
import unittest
|
|
|
|
sys.path.insert(0, os.path.dirname(__file__))
|
|
from house_parser import HouseParser
|
|
|
|
|
|
def feed_lines(hp, lines):
|
|
"""Feed lines, return the first non-None result."""
|
|
for sender, msg in lines:
|
|
result = hp.feed(sender, msg)
|
|
if result is not None:
|
|
return result
|
|
return None
|
|
|
|
|
|
class TestBuyBasic(unittest.TestCase):
|
|
"""Basic house buying flow."""
|
|
|
|
def test_buy_3_properties(self):
|
|
"""Buy 1+1+2 houses on Red monopoly."""
|
|
hp = HouseParser()
|
|
result = feed_lines(hp, [
|
|
("monop", "Kentucky ave. (R) (0) Indiana ave. (R) (0) Illinois ave. (R) (0) "),
|
|
("monop", "Houses will cost $150"),
|
|
("monop", "How many houses do you wish to buy for"),
|
|
("monop", "Kentucky ave. (R) (0): "),
|
|
("merp", ".1"),
|
|
("monop", "Indiana ave. (R) (0): "),
|
|
("merp", ".1"),
|
|
("monop", "Illinois ave. (R) (0): "),
|
|
("merp", ".2"),
|
|
("monop", "You asked for 4 houses for $600"),
|
|
("monop", "Is that ok? "),
|
|
("merp", ".y"),
|
|
])
|
|
self.assertIsNotNone(result)
|
|
self.assertEqual(result["action"], "buy")
|
|
self.assertEqual(result["changes"], {21: 1, 23: 1, 24: 2})
|
|
self.assertEqual(result["cost"], 600)
|
|
|
|
def test_buy_2_property_monopoly(self):
|
|
"""Buy houses on a 2-property monopoly (Purple)."""
|
|
hp = HouseParser()
|
|
result = feed_lines(hp, [
|
|
("monop", "Mediterranean ave. (P) (0) Baltic ave. (P) (0) "),
|
|
("monop", "Houses will cost $50"),
|
|
("monop", "How many houses do you wish to buy for"),
|
|
("monop", "Mediterranean ave. (P) (0): "),
|
|
("fbs", ".2"),
|
|
("monop", "Baltic ave. (P) (0): "),
|
|
("fbs", ".2"),
|
|
("monop", "You asked for 4 houses for $200"),
|
|
("monop", "Is that ok? "),
|
|
("fbs", ".y"),
|
|
])
|
|
self.assertEqual(result["changes"], {1: 2, 3: 2})
|
|
self.assertEqual(result["cost"], 200)
|
|
|
|
def test_buy_with_existing_houses(self):
|
|
"""Buy more houses on properties that already have some."""
|
|
hp = HouseParser()
|
|
result = feed_lines(hp, [
|
|
("monop", "Houses will cost $100"),
|
|
("monop", "How many houses do you wish to buy for"),
|
|
("monop", "St. James pl. (O) (2): "),
|
|
("merp", ".1"),
|
|
("monop", "Tennessee ave. (O) (1): "),
|
|
("merp", ".2"),
|
|
("monop", "New York ave. (O) (1): "),
|
|
("merp", ".2"),
|
|
("monop", "You asked for 5 houses for $500"),
|
|
("monop", "Is that ok? "),
|
|
("merp", ".y"),
|
|
])
|
|
self.assertEqual(result["changes"], {16: 3, 18: 3, 19: 3})
|
|
|
|
def test_buy_rejected(self):
|
|
"""Player says no to 'Is that ok?'."""
|
|
hp = HouseParser()
|
|
result = feed_lines(hp, [
|
|
("monop", "Houses will cost $150"),
|
|
("monop", "How many houses do you wish to buy for"),
|
|
("monop", "Kentucky ave. (R) (0): "),
|
|
("merp", ".1"),
|
|
("monop", "Indiana ave. (R) (0): "),
|
|
("merp", ".1"),
|
|
("monop", "Illinois ave. (R) (0): "),
|
|
("merp", ".1"),
|
|
("monop", "You asked for 3 houses for $450"),
|
|
("monop", "Is that ok? "),
|
|
("merp", ".n"),
|
|
])
|
|
self.assertEqual(result, {"action": "cancel"})
|
|
self.assertFalse(hp.active)
|
|
|
|
def test_buy_to_hotel(self):
|
|
"""Buy up to hotel (5 houses)."""
|
|
hp = HouseParser()
|
|
result = feed_lines(hp, [
|
|
("monop", "Houses will cost $200"),
|
|
("monop", "How many houses do you wish to buy for"),
|
|
("monop", "Park place (D) (4): "),
|
|
("fbs", ".1"),
|
|
("monop", "Boardwalk (D) (4): "),
|
|
("fbs", ".1"),
|
|
("monop", "You asked for 2 houses for $400"),
|
|
("monop", "Is that ok? "),
|
|
("fbs", ".y"),
|
|
])
|
|
self.assertEqual(result["changes"], {37: 5, 39: 5})
|
|
|
|
def test_buy_skip_hotel(self):
|
|
"""Property already has hotel — prompt shows (H), auto-skipped."""
|
|
hp = HouseParser()
|
|
result = feed_lines(hp, [
|
|
("monop", "Houses will cost $50"),
|
|
("monop", "How many houses do you wish to buy for"),
|
|
("monop", "Oriental ave. (L) (3): "),
|
|
("fbs", ".2"),
|
|
("monop", "Vermont ave. (L) (H):"), # auto-skipped
|
|
("monop", "Connecticut ave. (L) (4): "),
|
|
("fbs", ".1"),
|
|
("monop", "You asked for 3 houses for $150"),
|
|
("monop", "Is that ok? "),
|
|
("fbs", ".y"),
|
|
])
|
|
self.assertEqual(result["changes"], {6: 5, 9: 5})
|
|
# Vermont (8) not in changes — already at hotel, no change
|
|
self.assertNotIn(8, result["changes"])
|
|
|
|
|
|
class TestSellBasic(unittest.TestCase):
|
|
"""Basic house selling flow."""
|
|
|
|
def test_sell_houses(self):
|
|
"""Sell houses from Red monopoly."""
|
|
hp = HouseParser()
|
|
result = feed_lines(hp, [
|
|
("monop", "Houses will get you $75 apiece"),
|
|
("monop", "Kentucky ave. (R) (1) Indiana ave. (R) (1) Illinois ave. (R) (2) "),
|
|
("monop", "How many houses do you wish to sell from"),
|
|
("monop", "Kentucky ave. (R) (1): "),
|
|
("merp", ".1"),
|
|
("monop", "Indiana ave. (R) (1): "),
|
|
("merp", ".1"),
|
|
("monop", "Illinois ave. (R) (2): "),
|
|
("merp", ".2"),
|
|
("monop", "You asked to sell 4 houses for $300"),
|
|
("monop", "Is that ok? "),
|
|
("merp", ".y"),
|
|
])
|
|
self.assertEqual(result["action"], "sell")
|
|
self.assertEqual(result["changes"], {21: 0, 23: 0, 24: 0})
|
|
self.assertEqual(result["cost"], 300)
|
|
|
|
def test_sell_from_hotel(self):
|
|
"""Sell 1 house from a hotel."""
|
|
hp = HouseParser()
|
|
result = feed_lines(hp, [
|
|
("monop", "Houses will get you $25 apiece"),
|
|
("monop", "How many houses do you wish to sell from"),
|
|
("monop", "Oriental ave. (L) (H): "),
|
|
("fbs", ".1"),
|
|
("monop", "Vermont ave. (L) (H): "),
|
|
("fbs", ".0"),
|
|
("monop", "Connecticut ave. (L) (H): "),
|
|
("fbs", ".0"),
|
|
("monop", "You asked to sell 1 houses for $25"),
|
|
("monop", "Is that ok? "),
|
|
("fbs", ".y"),
|
|
])
|
|
self.assertEqual(result["changes"], {6: 4}) # hotel(5) - 1 = 4
|
|
|
|
def test_sell_skip_zero(self):
|
|
"""Property with 0 houses — auto-skipped during sell."""
|
|
hp = HouseParser()
|
|
result = feed_lines(hp, [
|
|
("monop", "Houses will get you $100 apiece"),
|
|
("monop", "How many houses do you wish to sell from"),
|
|
("monop", "St. James pl. (O) (0):"), # auto-skipped
|
|
("monop", "Tennessee ave. (O) (2): "),
|
|
("merp", ".1"),
|
|
("monop", "New York ave. (O) (3): "),
|
|
("merp", ".2"),
|
|
("monop", "You asked to sell 3 houses for $300"),
|
|
("monop", "Is that ok? "),
|
|
("merp", ".y"),
|
|
])
|
|
self.assertEqual(result["changes"], {18: 1, 19: 1})
|
|
self.assertNotIn(16, result["changes"]) # St. James had 0
|
|
|
|
|
|
class TestErrorHandling(unittest.TestCase):
|
|
"""Error cases and retries."""
|
|
|
|
def test_spread_too_wide_retry(self):
|
|
"""'Spread too wide' resets all prompts."""
|
|
hp = HouseParser()
|
|
result = feed_lines(hp, [
|
|
("monop", "Houses will cost $150"),
|
|
("monop", "How many houses do you wish to buy for"),
|
|
("monop", "Kentucky ave. (R) (0): "),
|
|
("merp", ".3"),
|
|
("monop", "Indiana ave. (R) (0): "),
|
|
("merp", ".0"),
|
|
("monop", "Illinois ave. (R) (0): "),
|
|
("merp", ".0"),
|
|
("monop", "That makes the spread too wide. Try again"),
|
|
# Retry
|
|
("monop", "Kentucky ave. (R) (0): "),
|
|
("merp", ".1"),
|
|
("monop", "Indiana ave. (R) (0): "),
|
|
("merp", ".1"),
|
|
("monop", "Illinois ave. (R) (0): "),
|
|
("merp", ".1"),
|
|
("monop", "You asked for 3 houses for $450"),
|
|
("monop", "Is that ok? "),
|
|
("merp", ".y"),
|
|
])
|
|
self.assertIsNotNone(result)
|
|
self.assertEqual(result["changes"], {21: 1, 23: 1, 24: 1})
|
|
|
|
def test_too_many_retry(self):
|
|
"""'Too many' re-prompts same property."""
|
|
hp = HouseParser()
|
|
result = feed_lines(hp, [
|
|
("monop", "Houses will cost $150"),
|
|
("monop", "How many houses do you wish to buy for"),
|
|
("monop", "Kentucky ave. (R) (3): "),
|
|
("merp", ".5"),
|
|
("monop", "That's too many. The most you can buy is 2"),
|
|
("monop", "Kentucky ave. (R) (3): "),
|
|
("merp", ".2"),
|
|
("monop", "Indiana ave. (R) (3): "),
|
|
("merp", ".2"),
|
|
("monop", "Illinois ave. (R) (3): "),
|
|
("merp", ".2"),
|
|
("monop", "You asked for 6 houses for $900"),
|
|
("monop", "Is that ok? "),
|
|
("merp", ".y"),
|
|
])
|
|
self.assertEqual(result["changes"], {21: 5, 23: 5, 24: 5})
|
|
|
|
def test_resets_after_completion(self):
|
|
"""Parser resets to idle after completion."""
|
|
hp = HouseParser()
|
|
feed_lines(hp, [
|
|
("monop", "Houses will cost $50"),
|
|
("monop", "How many houses do you wish to buy for"),
|
|
("monop", "Mediterranean ave. (P) (0): "),
|
|
("fbs", ".1"),
|
|
("monop", "Baltic ave. (P) (0): "),
|
|
("fbs", ".1"),
|
|
("monop", "You asked for 2 houses for $100"),
|
|
("monop", "Is that ok? "),
|
|
("fbs", ".y"),
|
|
])
|
|
self.assertFalse(hp.active)
|
|
self.assertEqual(hp.state, "idle")
|
|
|
|
|
|
class TestRealLog(unittest.TestCase):
|
|
"""Replay real log segments through the sub-parser."""
|
|
|
|
def _replay_segment(self, start, end):
|
|
"""Replay lines start..end from test log through HouseParser."""
|
|
hp = HouseParser()
|
|
results = []
|
|
with open("test_data/monop.log") as f:
|
|
for i, line in enumerate(f, 1):
|
|
if i < start:
|
|
continue
|
|
if i > end:
|
|
break
|
|
parts = line.rstrip('\n').split('\t', 2)
|
|
if len(parts) < 3:
|
|
continue
|
|
sender = parts[1].strip().lstrip('@')
|
|
msg = parts[2]
|
|
r = hp.feed(sender, msg)
|
|
if r is not None:
|
|
results.append(r)
|
|
return results, hp
|
|
|
|
def test_real_buy_line_59(self):
|
|
"""Real log: merp buys 1+1+2 houses on Red (lines 59-73)."""
|
|
results, hp = self._replay_segment(59, 73)
|
|
self.assertEqual(len(results), 1)
|
|
r = results[0]
|
|
self.assertEqual(r["action"], "buy")
|
|
# Kentucky=21, Indiana=23, Illinois=24
|
|
self.assertEqual(r["changes"], {21: 1, 23: 1, 24: 2})
|
|
self.assertEqual(r["cost"], 600)
|
|
|
|
def test_real_sell_line_354(self):
|
|
"""Real log: merp sells 1+1+2 houses from Red (lines 354-366)."""
|
|
results, hp = self._replay_segment(354, 366)
|
|
self.assertEqual(len(results), 1)
|
|
r = results[0]
|
|
self.assertEqual(r["action"], "sell")
|
|
self.assertEqual(r["changes"], {21: 0, 23: 0, 24: 0})
|
|
|
|
def test_real_sell_hotel_line_4386(self):
|
|
"""Real log: Derecho sells 1 hotel from Oriental (lines 4386-4399)."""
|
|
results, hp = self._replay_segment(4386, 4399)
|
|
self.assertEqual(len(results), 1)
|
|
r = results[0]
|
|
self.assertEqual(r["action"], "sell")
|
|
# Oriental (6): H(5) - 1 = 4
|
|
self.assertEqual(r["changes"][6], 4)
|
|
|
|
|
|
class TestActiveState(unittest.TestCase):
|
|
"""Test the active property."""
|
|
|
|
def test_not_active_initially(self):
|
|
hp = HouseParser()
|
|
self.assertFalse(hp.active)
|
|
|
|
def test_active_during_prompts(self):
|
|
hp = HouseParser()
|
|
hp.feed("monop", "Houses will cost $150")
|
|
hp.feed("monop", "How many houses do you wish to buy for")
|
|
self.assertTrue(hp.active)
|
|
|
|
def test_active_during_confirm(self):
|
|
hp = HouseParser()
|
|
feed_lines(hp, [
|
|
("monop", "Houses will cost $50"),
|
|
("monop", "How many houses do you wish to buy for"),
|
|
("monop", "Mediterranean ave. (P) (0): "),
|
|
("fbs", ".1"),
|
|
("monop", "Baltic ave. (P) (0): "),
|
|
("fbs", ".1"),
|
|
("monop", "You asked for 2 houses for $100"),
|
|
])
|
|
self.assertTrue(hp.active)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main(verbosity=2)
|