Parse player commands and holdings displays for full state tracking
Player command tracking:
- .card: detect GOJF card usage in jail (previously invisible)
- .mortgage/.mor: set property_mortgaged flag using cost+name disambiguation
- .unmortgage/.unm: clear property_mortgaged flag using cost+name disambiguation
- Command context (_command_context) disambiguates 'That cost you $X' between
jail pay and unmortgage
Holdings display parsing:
- Parse 'NAME's (N) holdings' header, then printsq-format property lines
- Full resync of property_owner, property_mortgaged, property_houses
- Reuses resolve_trade_property() for name matching
- Holdings end on any non-property line (checkpoint, command prompt, etc.)
13 new tests in test_parser_commands.py:
- GOJF card (4): exit jail, no card, not in jail, log entry
- Unmortgage (2): unique property, disambiguation with user input
- Mortgage (1): flag set correctly
- Holdings (5): ownership, mortgage, houses, stale clearing, real log replay
- Real log (1): unmortgage at line 36-41
All 1551 checkpoints + 80 unit tests passing.
2026-02-21 19:30:21 +00:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
"""Tests for player command tracking and holdings display parsing."""
|
|
|
|
|
|
|
|
|
|
import sys
|
|
|
|
|
import os
|
|
|
|
|
import unittest
|
|
|
|
|
|
|
|
|
|
sys.path.insert(0, os.path.dirname(__file__))
|
|
|
|
|
from monop_parser import MonopParser
|
|
|
|
|
|
|
|
|
|
TS = "2026-01-01 00:00:00"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def feed(parser, lines):
|
|
|
|
|
for line in lines:
|
|
|
|
|
parser.parse_line(f"{TS}\t{line}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def setup_3player_game():
|
|
|
|
|
"""fbs goes first (turn order: fbs→merp→hiro)."""
|
|
|
|
|
p = MonopParser()
|
|
|
|
|
feed(p, [
|
|
|
|
|
"monop\tHow many players? ",
|
|
|
|
|
"monop\tPlayer 1, say 'me' please.",
|
|
|
|
|
"monop\tmerp (1) rolls 5",
|
|
|
|
|
"monop\tPlayer 2, say 'me' please.",
|
|
|
|
|
"monop\thiro (2) rolls 3",
|
|
|
|
|
"monop\tPlayer 3, say 'me' please.",
|
|
|
|
|
"monop\tfbs (3) rolls 8",
|
|
|
|
|
"monop\tfbs (3) goes first",
|
|
|
|
|
"monop\tfbs (3) (cash $1500) on === GO ===",
|
|
|
|
|
"monop\t-- Command: ",
|
|
|
|
|
])
|
|
|
|
|
return p
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def give_properties(game, player_num, sq_ids):
|
|
|
|
|
for sq_id in sq_ids:
|
|
|
|
|
game.property_owner[sq_id] = player_num
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =====================================================================
|
|
|
|
|
# GOJF card usage from player command
|
|
|
|
|
# =====================================================================
|
|
|
|
|
class TestGOJFCardCommand(unittest.TestCase):
|
|
|
|
|
|
|
|
|
|
def test_card_command_exits_jail(self):
|
|
|
|
|
"""Player types .card while in jail with GOJF card."""
|
|
|
|
|
p = setup_3player_game()
|
|
|
|
|
g = p.game
|
|
|
|
|
# Put fbs in jail with a GOJF card
|
|
|
|
|
fbs = g.get_player(name="fbs")
|
|
|
|
|
fbs.in_jail = True
|
|
|
|
|
fbs.jail_turns = 1
|
|
|
|
|
fbs.location = 40
|
|
|
|
|
fbs.get_out_of_jail_free_cards = 1
|
|
|
|
|
|
|
|
|
|
feed(p, ["fbs\t.card"])
|
|
|
|
|
|
|
|
|
|
self.assertFalse(fbs.in_jail)
|
|
|
|
|
self.assertEqual(fbs.location, 10) # Just Visiting
|
|
|
|
|
self.assertEqual(fbs.get_out_of_jail_free_cards, 0)
|
|
|
|
|
self.assertEqual(fbs.jail_turns, 0)
|
|
|
|
|
|
|
|
|
|
def test_card_command_no_gojf(self):
|
|
|
|
|
"""Player types .card but has no GOJF cards — nothing happens."""
|
|
|
|
|
p = setup_3player_game()
|
|
|
|
|
g = p.game
|
|
|
|
|
fbs = g.get_player(name="fbs")
|
|
|
|
|
fbs.in_jail = True
|
|
|
|
|
fbs.location = 40
|
|
|
|
|
fbs.get_out_of_jail_free_cards = 0
|
|
|
|
|
|
|
|
|
|
feed(p, ["fbs\t.card"])
|
|
|
|
|
|
|
|
|
|
# Should still be in jail
|
|
|
|
|
self.assertTrue(fbs.in_jail)
|
|
|
|
|
self.assertEqual(fbs.location, 40)
|
|
|
|
|
|
|
|
|
|
def test_card_command_not_in_jail(self):
|
|
|
|
|
"""Player types .card but isn't in jail — nothing happens."""
|
|
|
|
|
p = setup_3player_game()
|
|
|
|
|
g = p.game
|
|
|
|
|
fbs = g.get_player(name="fbs")
|
|
|
|
|
fbs.get_out_of_jail_free_cards = 1
|
|
|
|
|
|
|
|
|
|
feed(p, ["fbs\t.card"])
|
|
|
|
|
|
|
|
|
|
# GOJF card should not be consumed
|
|
|
|
|
self.assertEqual(fbs.get_out_of_jail_free_cards, 1)
|
|
|
|
|
|
|
|
|
|
def test_card_log_entry(self):
|
|
|
|
|
p = setup_3player_game()
|
|
|
|
|
g = p.game
|
|
|
|
|
fbs = g.get_player(name="fbs")
|
|
|
|
|
fbs.in_jail = True
|
|
|
|
|
fbs.location = 40
|
|
|
|
|
fbs.get_out_of_jail_free_cards = 2
|
|
|
|
|
|
|
|
|
|
feed(p, ["fbs\t.card"])
|
|
|
|
|
|
|
|
|
|
log_texts = [e["text"] for e in g.log]
|
|
|
|
|
self.assertTrue(any("Get Out of Jail" in t for t in log_texts))
|
|
|
|
|
self.assertEqual(fbs.get_out_of_jail_free_cards, 1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =====================================================================
|
|
|
|
|
# Unmortgage tracking via command context
|
|
|
|
|
# =====================================================================
|
|
|
|
|
class TestUnmortgageTracking(unittest.TestCase):
|
|
|
|
|
|
|
|
|
|
def test_unmortgage_clears_flag(self):
|
|
|
|
|
"""Player unmortgages a uniquely-identifiable property."""
|
|
|
|
|
p = setup_3player_game()
|
|
|
|
|
g = p.game
|
|
|
|
|
# fbs owns Boardwalk (39), mortgaged
|
|
|
|
|
give_properties(g, 3, [39])
|
|
|
|
|
g.property_mortgaged[39] = True
|
|
|
|
|
|
|
|
|
|
feed(p, [
|
|
|
|
|
"fbs\t.unm",
|
|
|
|
|
"monop\tYour only mortaged property is Boardwalk (D)",
|
|
|
|
|
"monop\tDo you want to unmortgage it? ",
|
|
|
|
|
"fbs\t.y",
|
|
|
|
|
"monop\tThat cost you $220", # 400/2 + 400/2/10 = 220
|
|
|
|
|
"monop\tfbs (3) (cash $1280) on === GO ===",
|
|
|
|
|
"monop\t-- Command: ",
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
self.assertNotIn(39, g.property_mortgaged)
|
|
|
|
|
|
|
|
|
|
def test_unmortgage_with_disambiguation(self):
|
|
|
|
|
"""Player unmortgages one of two same-cost properties."""
|
|
|
|
|
p = setup_3player_game()
|
|
|
|
|
g = p.game
|
|
|
|
|
# fbs owns both Kentucky (21) and Indiana (23), both mortgaged
|
|
|
|
|
# Both cost $220 → unmortgage cost $121 each
|
|
|
|
|
give_properties(g, 3, [21, 23])
|
|
|
|
|
g.property_mortgaged[21] = True
|
|
|
|
|
g.property_mortgaged[23] = True
|
|
|
|
|
|
|
|
|
|
feed(p, [
|
|
|
|
|
"fbs\t.unm",
|
|
|
|
|
"monop\tWhich property do you want to unmortgage? ",
|
|
|
|
|
"fbs\t.indiana", # user specifies Indiana
|
|
|
|
|
"monop\tThat cost you $121",
|
|
|
|
|
"monop\tfbs (3) (cash $1379) on === GO ===",
|
|
|
|
|
"monop\t-- Command: ",
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
# Indiana unmortgaged, Kentucky still mortgaged
|
|
|
|
|
self.assertNotIn(23, g.property_mortgaged)
|
|
|
|
|
self.assertTrue(g.property_mortgaged.get(21))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =====================================================================
|
|
|
|
|
# Mortgage tracking
|
|
|
|
|
# =====================================================================
|
|
|
|
|
class TestMortgageTracking(unittest.TestCase):
|
|
|
|
|
|
|
|
|
|
def test_mortgage_sets_flag(self):
|
|
|
|
|
"""Player mortgages a property — flag should be set."""
|
|
|
|
|
p = setup_3player_game()
|
|
|
|
|
g = p.game
|
|
|
|
|
give_properties(g, 3, [39]) # Boardwalk
|
|
|
|
|
|
|
|
|
|
feed(p, [
|
|
|
|
|
"fbs\t.mor",
|
|
|
|
|
"monop\tWhich property do you want to mortgage? ",
|
|
|
|
|
"fbs\t.boardwalk",
|
|
|
|
|
"monop\tThat got you $200", # 400/2
|
|
|
|
|
"monop\tfbs (3) (cash $1700) on === GO ===",
|
|
|
|
|
"monop\t-- Command: ",
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
self.assertTrue(g.property_mortgaged.get(39))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =====================================================================
|
|
|
|
|
# Holdings display parsing
|
|
|
|
|
# =====================================================================
|
|
|
|
|
class TestHoldingsDisplay(unittest.TestCase):
|
|
|
|
|
|
|
|
|
|
def test_holdings_syncs_properties(self):
|
|
|
|
|
"""Holdings display should resync property ownership."""
|
|
|
|
|
p = setup_3player_game()
|
|
|
|
|
g = p.game
|
|
|
|
|
|
|
|
|
|
feed(p, [
|
|
|
|
|
"monop\tmerp's (1) holdings (Total worth: $1806):",
|
|
|
|
|
"monop\t Name Own Price Mg # Rent",
|
|
|
|
|
"monop\t Electric C 1 150 1",
|
|
|
|
|
"monop\t New York a 1 Orange 200 16",
|
|
|
|
|
"monop\t Kentucky a 1 RED 220 0 36",
|
|
|
|
|
"monop\tmerp (1) (cash $496) on Community Chest ii",
|
|
|
|
|
"monop\t-- Command: ",
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
# Electric Co (12), New York ave (19), Kentucky ave (21) should be owned by merp (1)
|
|
|
|
|
self.assertEqual(g.property_owner.get(12), 1)
|
|
|
|
|
self.assertEqual(g.property_owner.get(19), 1)
|
|
|
|
|
self.assertEqual(g.property_owner.get(21), 1)
|
|
|
|
|
|
|
|
|
|
def test_holdings_syncs_mortgage_status(self):
|
|
|
|
|
"""Holdings display should set/clear mortgage flags."""
|
|
|
|
|
p = setup_3player_game()
|
|
|
|
|
g = p.game
|
|
|
|
|
|
|
|
|
|
feed(p, [
|
|
|
|
|
"monop\tmerp's (1) holdings (Total worth: $1806):",
|
|
|
|
|
"monop\t Name Own Price Mg # Rent",
|
|
|
|
|
"monop\t Indiana av 1 RED 220 * 0 36",
|
|
|
|
|
"monop\t Illinois a 1 RED 240 0 40",
|
|
|
|
|
"monop\tmerp (1) (cash $496) on Community Chest ii",
|
|
|
|
|
"monop\t-- Command: ",
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
self.assertTrue(g.property_mortgaged.get(23)) # Indiana mortgaged
|
|
|
|
|
self.assertNotIn(24, g.property_mortgaged) # Illinois not mortgaged
|
|
|
|
|
|
|
|
|
|
def test_holdings_syncs_house_counts(self):
|
|
|
|
|
"""Holdings display should update house counts."""
|
|
|
|
|
p = setup_3player_game()
|
|
|
|
|
g = p.game
|
|
|
|
|
|
|
|
|
|
feed(p, [
|
|
|
|
|
"monop\tfbs's (3) holdings (Total worth: $5000):",
|
|
|
|
|
"monop\t Name Own Price Mg # Rent",
|
|
|
|
|
"monop\t Kentucky a 3 RED 220 3 180",
|
|
|
|
|
"monop\t Indiana av 3 RED 220 3 180",
|
|
|
|
|
"monop\t Illinois a 3 RED 240 4 220",
|
|
|
|
|
"monop\tfbs (3) (cash $2000) on === GO ===",
|
|
|
|
|
"monop\t-- Command: ",
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
self.assertEqual(g.property_houses.get(21), 3) # Kentucky 3 houses
|
|
|
|
|
self.assertEqual(g.property_houses.get(23), 3) # Indiana 3 houses
|
|
|
|
|
self.assertEqual(g.property_houses.get(24), 4) # Illinois 4 houses
|
|
|
|
|
|
|
|
|
|
def test_holdings_clears_stale_houses(self):
|
|
|
|
|
"""If holdings shows 0 houses, clear any stale house count."""
|
|
|
|
|
p = setup_3player_game()
|
|
|
|
|
g = p.game
|
|
|
|
|
g.property_houses[21] = 3 # stale: Kentucky had 3 houses
|
|
|
|
|
|
|
|
|
|
feed(p, [
|
|
|
|
|
"monop\tmerp's (1) holdings (Total worth: $1000):",
|
|
|
|
|
"monop\t Name Own Price Mg # Rent",
|
|
|
|
|
"monop\t Kentucky a 1 RED 220 0 36",
|
|
|
|
|
"monop\tmerp (1) (cash $500) on === GO ===",
|
|
|
|
|
"monop\t-- Command: ",
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
self.assertNotIn(21, g.property_houses) # houses cleared
|
|
|
|
|
|
|
|
|
|
def test_real_log_holdings_line_26(self):
|
|
|
|
|
"""Replay real holdings display from line 26 of test log."""
|
|
|
|
|
p = MonopParser()
|
|
|
|
|
with open("test_data/monop.log") as f:
|
|
|
|
|
for i, line in enumerate(f, 1):
|
|
|
|
|
p.parse_line(line.rstrip('\n'))
|
|
|
|
|
if i >= 35:
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
g = p.game
|
|
|
|
|
self.assertIsNotNone(g)
|
|
|
|
|
# merp (1) should own Electric Co (12) and others from the holdings dump
|
|
|
|
|
self.assertEqual(g.property_owner.get(12), 1) # Electric Co
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =====================================================================
|
|
|
|
|
# Real log: unmortgage at line 36-41
|
|
|
|
|
# =====================================================================
|
|
|
|
|
class TestRealLogUnmortgage(unittest.TestCase):
|
|
|
|
|
|
|
|
|
|
def test_real_unmortgage(self):
|
|
|
|
|
"""Real log: merp unmortgages Indiana ave around line 36-41."""
|
|
|
|
|
p = MonopParser()
|
|
|
|
|
with open("test_data/monop.log") as f:
|
|
|
|
|
for i, line in enumerate(f, 1):
|
|
|
|
|
p.parse_line(line.rstrip('\n'))
|
|
|
|
|
if i >= 42:
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
g = p.game
|
|
|
|
|
# Indiana ave (23) should NOT be mortgaged after unmortgage
|
|
|
|
|
self.assertFalse(g.property_mortgaged.get(23, False),
|
|
|
|
|
"Indiana ave should be unmortgaged after .unm at line 36")
|
|
|
|
|
|
|
|
|
|
|
Add house buying/selling sub-parser with full integration
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.
2026-02-21 19:43:03 +00:00
|
|
|
# =====================================================================
|
|
|
|
|
# House buying/selling integration (via HouseParser sub-parser)
|
|
|
|
|
# =====================================================================
|
|
|
|
|
class TestHouseIntegration(unittest.TestCase):
|
|
|
|
|
|
|
|
|
|
def test_buy_houses_updates_state(self):
|
|
|
|
|
"""Full house buying flow updates property_houses in main parser."""
|
|
|
|
|
p = setup_3player_game()
|
|
|
|
|
g = p.game
|
|
|
|
|
give_properties(g, 3, [1, 3]) # fbs owns purple monopoly
|
|
|
|
|
|
|
|
|
|
feed(p, [
|
|
|
|
|
"monop\tMediterranean ave. (P) (0) Baltic ave. (P) (0) ",
|
|
|
|
|
"monop\tHouses will cost $50",
|
|
|
|
|
"monop\tHow many houses do you wish to buy for",
|
|
|
|
|
"monop\tMediterranean ave. (P) (0): ",
|
|
|
|
|
"fbs\t.2",
|
|
|
|
|
"monop\tBaltic ave. (P) (0): ",
|
|
|
|
|
"fbs\t.2",
|
|
|
|
|
"monop\tYou asked for 4 houses for $200",
|
|
|
|
|
"monop\tIs that ok? ",
|
|
|
|
|
"fbs\t.y",
|
|
|
|
|
"monop\tfbs (3) (cash $1300) on === GO ===",
|
|
|
|
|
"monop\t-- Command: ",
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
self.assertEqual(g.property_houses.get(1), 2) # Mediterranean
|
|
|
|
|
self.assertEqual(g.property_houses.get(3), 2) # Baltic
|
|
|
|
|
|
|
|
|
|
def test_sell_houses_updates_state(self):
|
|
|
|
|
"""Full house selling flow updates property_houses."""
|
|
|
|
|
p = setup_3player_game()
|
|
|
|
|
g = p.game
|
|
|
|
|
give_properties(g, 3, [21, 23, 24]) # fbs owns Red
|
|
|
|
|
g.property_houses[21] = 2
|
|
|
|
|
g.property_houses[23] = 2
|
|
|
|
|
g.property_houses[24] = 3
|
|
|
|
|
|
|
|
|
|
feed(p, [
|
|
|
|
|
"monop\tHouses will get you $75 apiece",
|
|
|
|
|
"monop\tKentucky ave. (R) (2) Indiana ave. (R) (2) Illinois ave. (R) (3) ",
|
|
|
|
|
"monop\tHow many houses do you wish to sell from",
|
|
|
|
|
"monop\tKentucky ave. (R) (2): ",
|
|
|
|
|
"fbs\t.2",
|
|
|
|
|
"monop\tIndiana ave. (R) (2): ",
|
|
|
|
|
"fbs\t.2",
|
|
|
|
|
"monop\tIllinois ave. (R) (3): ",
|
|
|
|
|
"fbs\t.3",
|
|
|
|
|
"monop\tYou asked to sell 7 houses for $525",
|
|
|
|
|
"monop\tIs that ok? ",
|
|
|
|
|
"fbs\t.y",
|
|
|
|
|
"monop\tfbs (3) (cash $2025) on === GO ===",
|
|
|
|
|
"monop\t-- Command: ",
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
self.assertNotIn(21, g.property_houses) # all gone
|
|
|
|
|
self.assertNotIn(23, g.property_houses)
|
|
|
|
|
self.assertNotIn(24, g.property_houses)
|
|
|
|
|
|
|
|
|
|
def test_buy_rejected_no_change(self):
|
|
|
|
|
"""Rejected house purchase doesn't change state."""
|
|
|
|
|
p = setup_3player_game()
|
|
|
|
|
g = p.game
|
|
|
|
|
give_properties(g, 3, [1, 3])
|
|
|
|
|
|
|
|
|
|
feed(p, [
|
|
|
|
|
"monop\tHouses will cost $50",
|
|
|
|
|
"monop\tHow many houses do you wish to buy for",
|
|
|
|
|
"monop\tMediterranean ave. (P) (0): ",
|
|
|
|
|
"fbs\t.2",
|
|
|
|
|
"monop\tBaltic ave. (P) (0): ",
|
|
|
|
|
"fbs\t.2",
|
|
|
|
|
"monop\tYou asked for 4 houses for $200",
|
|
|
|
|
"monop\tIs that ok? ",
|
|
|
|
|
"fbs\t.n",
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
self.assertNotIn(1, g.property_houses)
|
|
|
|
|
self.assertNotIn(3, g.property_houses)
|
|
|
|
|
|
|
|
|
|
def test_real_log_house_buy_line_59(self):
|
|
|
|
|
"""Real log: merp buys houses on Red at line 59-73."""
|
|
|
|
|
p = MonopParser()
|
|
|
|
|
with open("test_data/monop.log") as f:
|
|
|
|
|
for i, line in enumerate(f, 1):
|
|
|
|
|
p.parse_line(line.rstrip('\n'))
|
|
|
|
|
if i >= 73:
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
g = p.game
|
|
|
|
|
# Kentucky(21)=1, Indiana(23)=1, Illinois(24)=2
|
|
|
|
|
self.assertEqual(g.property_houses.get(21), 1)
|
|
|
|
|
self.assertEqual(g.property_houses.get(23), 1)
|
|
|
|
|
self.assertEqual(g.property_houses.get(24), 2)
|
|
|
|
|
|
|
|
|
|
|
Parse player commands and holdings displays for full state tracking
Player command tracking:
- .card: detect GOJF card usage in jail (previously invisible)
- .mortgage/.mor: set property_mortgaged flag using cost+name disambiguation
- .unmortgage/.unm: clear property_mortgaged flag using cost+name disambiguation
- Command context (_command_context) disambiguates 'That cost you $X' between
jail pay and unmortgage
Holdings display parsing:
- Parse 'NAME's (N) holdings' header, then printsq-format property lines
- Full resync of property_owner, property_mortgaged, property_houses
- Reuses resolve_trade_property() for name matching
- Holdings end on any non-property line (checkpoint, command prompt, etc.)
13 new tests in test_parser_commands.py:
- GOJF card (4): exit jail, no card, not in jail, log entry
- Unmortgage (2): unique property, disambiguation with user input
- Mortgage (1): flag set correctly
- Holdings (5): ownership, mortgage, houses, stale clearing, real log replay
- Real log (1): unmortgage at line 36-41
All 1551 checkpoints + 80 unit tests passing.
2026-02-21 19:30:21 +00:00
|
|
|
if __name__ == "__main__":
|
|
|
|
|
unittest.main(verbosity=2)
|