monop-state/docs/ARCHITECTURE.md

231 lines
10 KiB
Markdown
Raw Normal View History

# 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](https://github.com/JohnMaguire/Cardinal) (a Python IRC bot) watches game messages in an IRC channel and writes `game-state.json` on every state change.
2. **Standalone bridge mode**`monop_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 viewer**`site/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:**
```bash
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:**
```bash
# 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:**
```bash
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.
```bash
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.
```bash
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.
```bash
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`)
```bash
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.