231 lines
10 KiB
Markdown
231 lines
10 KiB
Markdown
|
|
# 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.
|