Files
station-master/README.md
T
JesseandClaude Opus 5 5d825b97d2 v0.1.0 — undo, the crew's reach on the board, workers on the card, and four engine bugs
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
2026-08-08 05:23:17 -04:00

88 lines
4.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`](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`](docs/design.md) — it indexes everything. Commit messages stay high
level; [`CHANGELOG.md`](CHANGELOG.md) carries the reasoning and the measurements, and
[`TODO.md`](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.
```sh
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, and
`advance(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 is `joins()` in `src/engine/track.ts`, never a bare
pair of `hasPort()` 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.