monop-state/docs/ARCHITECTURE.md
Jarvis cceec64a7c Add ARCHITECTURE.md developer guide
Covers system goals, component design, directory structure,
testing strategies, and key design decisions for future agents.
2026-02-21 10:20:01 +00:00

10 KiB

Monop-State Architecture & Developer Guide

Goals

This project tracks a game of BSD monop (terminal Monopoly) being played over IRC and produces a live visual board that can be viewed in a browser. There are three modes of operation:

  1. Cardinal plugin mode — A plugin for Cardinal (a Python IRC bot) watches game messages in an IRC channel and writes game-state.json on every state change.
  2. Standalone bridge modemonop_bridge.py spawns the monop binary as a subprocess and bridges its stdin/stdout to IRC, while monop_players.py provides autopilot player bots.
  3. Web viewersite/index.html reads game-state.json and renders a visual Monopoly board with player tokens, property ownership, houses/hotels, and a game log. Auto-refreshes every 2 seconds.

The core challenge is parsing unstructured terminal output from a 1980s C program and reconstructing full game state from it.


Directory Structure

monop-state/
├── monop_parser.py          # Core: parses monop output → game state
├── monop_players.py         # Autopilot player bots (connect via IRC)
├── monop_bridge.py          # Bridges monop binary stdin/stdout to IRC
├── irc_client.py            # Minimal IRC client (used by integration tests)
├── test_parser.py           # Log replay test for the parser
├── test_players.py          # Unit tests for player bot logic
├── test_integration.py      # Full integration test (needs live IRC + monop)
├── test_data/               # Recorded game logs for replay testing
│   ├── monop.log            # Real game log
│   ├── autopilot_bridge.log # Bridge-side log from an autopilot game
│   └── autopilot_players.log# Player-side log from an autopilot game
├── plugins/                 # Cardinal IRC bot plugins
│   ├── monop/               # Game observer plugin
│   │   ├── plugin.py        # Cardinal plugin entry point
│   │   ├── monop_parser.py  # Bundled copy of the parser
│   │   ├── config.example.json
│   │   └── __init__.py
│   ├── monop-player/        # Autopilot player as Cardinal plugin
│   │   ├── plugin.py
│   │   ├── config.example.json
│   │   └── __init__.py
│   └── test_plugin.py       # Smoke test for the Cardinal plugin
├── site/                    # Web board viewer
│   ├── index.html           # Self-contained HTML/CSS/JS board
│   └── game-state.json      # State file (written by plugin or bridge)
├── reference/               # BSD monop C source code
├── docs/
│   ├── ARCHITECTURE.md      # This file
│   └── OUTPUT_CATALOG.md    # Every possible monop output line
└── README.md

Component Details

monop_parser.py — The Parser (~1200 lines)

The heart of the system. Parses raw monop output lines and maintains full game state including:

  • Player positions, money, jail status, doubles tracking
  • Property ownership, houses, hotels, mortgages
  • Get Out of Jail Free cards
  • Current player turn
  • Game log

Key concepts:

  • Checkpoint lines — Every turn starts with a line like alice (1) (cash $1500) on === GO ===. These are ground truth and the parser reconciles its tracked state against them.
  • Incremental parsing — Between checkpoints, the parser tracks state changes (buys, rent payments, card draws, etc.) line by line using regex matching.
  • Input format — Expects tab-separated log lines: TIMESTAMP\tSENDER\tMESSAGE. The parser only processes lines from the monop bot nick.

Important exports:

  • MonopParser — Main class. Call parse_line(log_line) and get_state().
  • BOARD — List of all 40 board squares with names, types, groups, and costs.
  • SQUARE_BY_NAME — Dict mapping square name → square ID.

monop_players.py — Autopilot Bots (~685 lines)

Connects to IRC as individual players and responds to monop prompts automatically. Each bot:

  • Rolls when it's their turn
  • Buys properties when affordable
  • Handles jail (pays to get out)
  • Responds to auction prompts
  • Handles debt resolution (mortgaging, selling houses)

Usage:

python3 monop_players.py --host 127.0.0.1 --port 6667 --channel '#monop' --players alice,bob,charlie

Useful for testing the parser against a full game without manual play.

monop_bridge.py — IRC Bridge (~127 lines)

Spawns the actual monop binary and bridges it to IRC:

  • Reads monop's stdout and sends lines to IRC channel
  • Reads IRC messages prefixed with . and pipes them to monop's stdin
  • Expects the monop binary at /tmp/monop-irc/monop/monop

irc_client.py — Test IRC Client (~85 lines)

Minimal IRC client used by integration tests. Connects, joins channels, sends messages, and collects responses. Not used in production.

Cardinal Plugins (plugins/)

plugins/monop/ — Watches IRC messages, feeds them to MonopParser, writes game-state.json. Provides IRC commands:

  • .monop or .monop status — Current game summary
  • .monop players — Detailed player info with properties
  • .monop owned — Properties grouped by owner

plugins/monop-player/ — The autopilot player logic packaged as a Cardinal plugin (alternative to standalone monop_players.py).

Installing plugins into Cardinal:

# Symlink from this repo into your Cardinal instance
ln -s /path/to/monop-state/plugins/monop /path/to/cardinal/plugins/monop
ln -s /path/to/monop-state/plugins/monop-player /path/to/cardinal/plugins/monop-player

The plugins/monop/ directory contains its own copy of monop_parser.py (loaded via importlib.util from the same directory). This means the plugin is self-contained — it doesn't need the root-level parser on sys.path.

Web Viewer (site/)

A single index.html that fetches game-state.json every 2 seconds and renders:

  • Classic Monopoly board layout with color-coded property groups
  • Player tokens with colors and initials
  • Property ownership indicators, houses (green), hotels (red)
  • Player info panels with money, properties, cards
  • Game log with recent events
  • Mobile-responsive dark theme
  • Demo mode when no live game data exists

Serving:

cd site/
python3 -m http.server 9998

The game-state.json must be in the same directory as index.html (it fetches it with a relative path).

Reference Source (reference/)

The original BSD monop C source code. Use this to understand what output the binary produces — essential when adding new parser patterns. Key files:

  • execute.c — Movement, buying, rolling logic
  • rent.c — Rent calculation and messages
  • houses.c — House/hotel buying
  • jail.c — Jail mechanics
  • cards.c / cards.inp — Chance and Community Chest cards
  • print.c — Board/player printing (checkpoint format)
  • brd.dat — Board square definitions
  • prop.dat — Property data (costs, rents)

Testing

Unit Tests — Parser (test_parser.py)

Replays a recorded game log through the parser and validates that checkpoint lines match the parser's tracked state.

python3 test_parser.py

Uses test_data/monop.log by default. This is the fastest way to verify parser correctness. If you fix a parsing bug, capture the failing log lines and add them to the test data.

Unit Tests — Player Bots (test_players.py)

Tests the PlayerBot response logic in isolation using mocked IRC connections.

python3 -m pytest test_players.py -v

Uses FakePlayerBot — a subclass that captures outgoing messages instead of sending to IRC. Tests verify that bots respond correctly to prompts (rolling, buying, jail, auctions, debt).

Plugin Smoke Test (plugins/test_plugin.py)

Tests the Cardinal plugin without a running Cardinal instance by mocking the cardinal module.

python3 plugins/test_plugin.py

Integration Tests (test_integration.py)

Runs a real game against a live monop-irc setup. Requires:

  1. An IRC server (e.g., InspIRCd) running on localhost:6667
  2. The monop bridge running (monop_bridge.py)
python3 test_integration.py

Plays a game using irc_client.py, periodically dumps state via .print and .own commands, and compares against the parser's tracked state. This is the most thorough test but requires infrastructure.

Manual Testing with Autopilot

To watch a full automated game and observe the parser + web viewer working together:

  1. Start an IRC server
  2. Start the bridge: python3 monop_bridge.py
  3. Start player bots: python3 monop_players.py --players alice,bob,charlie
  4. Start the web viewer: cd site && python3 -m http.server 9998
  5. Watch the board update in your browser

The test_data/autopilot_bridge.log and test_data/autopilot_players.log are recorded outputs from such a session — useful for debugging.


Adding New Parser Patterns

When the parser fails to handle a monop output line:

  1. Check docs/OUTPUT_CATALOG.md — it catalogs every possible output line from the C source
  2. Check the relevant C file in reference/ for exact format strings
  3. Add regex patterns to monop_parser.py
  4. Capture the failing game log and use it as test data
  5. Important: If you modify the root monop_parser.py, also update plugins/monop/monop_parser.py (it's a separate copy)

Key Design Decisions

  • Flat Python layout — Core files live at the repo root so imports work without packaging. Tests use sys.path.insert(0, '.') which is pragmatic for a project of this scope.
  • Parser has its own copy in the plugin — The Cardinal plugin loads monop_parser.py via importlib.util from its own directory, making it self-contained and deployable via symlink. The tradeoff is keeping two copies in sync.
  • Tab-separated log format — The parser expects TIMESTAMP\tSENDER\tMESSAGE. This matches how Cardinal and the bridge produce log lines.
  • game-state.json as the interface — The parser writes JSON, the web viewer reads JSON. They're decoupled — you could replace either side independently.
  • Checkpoint reconciliation — Rather than trusting incremental state tracking alone, the parser validates against checkpoint lines every turn. This makes it self-correcting.