Fifteen items from two playtest sessions. Three that read as drawing faults were engine bugs: cars could be added to a train that was not being made up (50 offers in 8 games), the make-up panel merged two trains and could couple a car to the wrong one, and an Office upgrade silently deleted what a Modifier had added. A fourth was a sentinel inside a coordinate's own value range — a Mainline placement travelling as row -1, which is an ordinary district row. Trains are now drawn the way they stand: west on the left, nose toward the way the engine faces, on both the district card and the Division chip. Undo steps back through the game by replaying the save without its last intent. The switching walk keeps its rejections, so the board can say why a square is not offered. Laborers and Porters are on the card, and the rule that a district only grows outwards is finally written down. Versions start here: third digit for fixes, second for a feature set, 1.0 for a release. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GgtkX8JnvKa8y2tuJ8aQf4
4.9 KiB
Station Master
A railroad operations game set in the era of timetable-and-train-order railroading (1840–1950), being built as a multiplayer browser game with an authoritative server.
You are the Station Master of a lineside Office on a shared east–west Division. Trains run to a timetable with no radios — just pocket watches and written orders. You switch cars, work freight and passengers, and take your turn as Superintendent deciding whether it is safe to clear a following train into an occupied Subdivision. Get that wrong and two trains meet at speed.
Status
v0.1.0 — solitaire is playable in a browser. The whole game runs client-side: the engine is pure,
imports nothing outside itself, and never touches Math.random, so a static host is all it needs.
- Rules — fully specified. Ten gaps in the original prototype rules found and resolved.
- Card faces — every card's printed values specified.
- Architecture — six documents, including a 20-component build plan.
- Code — the engine, the bot, the balance harness, the replay viewer and the playable page. A game can be saved, shared, replayed and stepped back through.
- Not built — multiplayer (the engine runs 2–5 player games and the bot plays them, but there is no server, no turn submission and no per-player view), the 22 opponent-directed cards, and real audio.
Balance is not where it should be: the developer bot averages 1.4 Revenue against a target of 20.
TODO.md says why, and says which of it is the bot and which is the deck.
Versions follow the convention at the top of CHANGELOG.md: third digit for fixes,
second for a set of features, 1.0 for the first release that deserves the name.
Layout
station-master/
├── CHANGELOG.md ← what changed and why, in detail, commit to commit
├── TODO.md ← open questions, provisional numbers, things to come back to
├── docs/
│ ├── rules/ ← the ruleset, card reference, glossary, decision record
│ ├── architecture/ ← how it is built, and what the pieces are
│ └── design/ ← board layout studies and rendering samples
├── public/replays/ ← saved games published to the site's replay directory
├── scripts/ ← build and deploy the static site
├── src/
│ ├── engine/ ← pure rules engine: no I/O, no clock, deterministic from a seed
│ ├── sim/ ← bot, harness, replay, board rendering
│ └── web/ ← the playable site: splash, game, replay viewer
└── test/
Start with docs/design.md — it indexes everything. Commit messages stay high
level; CHANGELOG.md carries the reasoning and the measurements, and
TODO.md is what we have decided not to forget.
Development
Requires Node 22.18+, which runs TypeScript directly by type stripping. There is no build step.
npm install
npm test # node --test
npm run typecheck # tsc --noEmit
Because Node strips types rather than compiling them, the codebase is restricted to erasable
syntax: no enum, no parameter properties, no namespaces. tsconfig.json enforces this.
Design notes worth knowing
- The rules engine is pure. No I/O, no clock, no sockets, and all randomness derives from one stored seed — so any game is exactly replayable, and a full game can be driven in a unit test with no server at all.
- It has two entry points, not one.
apply(state, intent)for player actions, andadvance(state)for everything the game does on its own — Mainline movement, collisions, the Stage clock, Superintendent rotation. - State is
fold(events). The event log is the source of truth, which is what gives reconnection, restart recovery and post-game replay from a single decision. - Never call
Math.random(). One ambient random call silently breaks replay. - Track is a deck card. 104 of the 243 cards in the Home Office deck are track — the largest category — so a district is built from what you draw, and building it costs you the industry or train you drew instead. There is no separate supply and no one-piece-a-turn cap.
- The track is 45° geometry, not a graph on a grid. Measured off
docs/tracks.png: the through rail runs east–west across the exact vertical middle of every card, there is no north–south track anywhere, and everything that leaves through the north or south edge does so at 45°, through the middle of that edge. So two ports meeting is not enough to make a rail — two 45° legs can meet at the same point and still form a V. Adjacency isjoins()insrc/engine/track.ts, never a bare pair ofhasPort()calls. A printed card turns 180° but never flips, so its handedness fixes which diagonal its leg lies on for good, and a run-around needs one card of each hand.