monop-state/test_house_parser.py

347 lines
13 KiB
Python
Raw Permalink Normal View History

#!/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)