# Architecture Overview

Station Master as a multiplayer digital game: players connect from a web browser, an authoritative
server manages lobbies and game state. Target scale is **small self-hosted** — a handful of
concurrent games, friends-and-family — hosted on a website or packaged as a StartOS service.

**No stack is chosen.** These documents describe what any implementation must do. Where a concrete
shape helps, it is written as pseudo-structure, not as a language.

Written against [`../rules/rules-v0.2.md`](../rules/rules-v0.2.md).

---

## The server is authoritative

The server owns all game state and is the sole party that may change it. Clients render state and
submit *intents*; the server validates, applies, and broadcasts.

This is not a default choice — the rules force it:

**The game has hidden information.** Each player holds up to three cards (four with the Emergency
Toolbox) that no other player may see. The Home Office deck is face-down and its order must not be
knowable. A client holding full state could read both.

**The game has randomness that decides outcomes.** D12 rolls set the Eastern Division Point, the
opening Superintendent, and — critically — the timetable slot a newly played Timetabled Train lands
in (§7). Deck shuffles decide everything else. A client-side RNG is a client-side cheat.

**Turn legality is global, not local.** After Gap 1, phases resolve phase-major with one player
acting at a time. Whether *you* may act depends on where every other player is. Only a global
observer can answer that. The Mainline Phase adds a wrinkle: it is automatic except when §8.1's
fourth condition prompts the Superintendent to rule on a following-train clearance — an input that
arrives mid-phase, out of the normal turn order, and can be triggered by another player's train.

**Collision evaluation must be identical for everyone.** After Gap 2, collisions are automatic — a
pure function of board state at each movement step. Two clients disagreeing about a wreck is a
divergent game, and in Competitive mode a wreck can end the game for everyone (§3.4).

### What that means in practice

| Concern | Where it lives |
| --- | --- |
| Deck order, shuffles, all RNG | Server only. Seeded, so a game is replayable for debugging. |
| Player hands | Server. Sent only to their owner. |
| Board state (track, pieces, facilities, yards) | Server. Public — broadcast to everyone. |
| Whose turn it is | Server. Derived, never client-asserted. |
| Rules validation | Server. The client may pre-validate for responsiveness, but the server's answer is the only one that counts. |
| Rendering, animation, local UI state | Client. |

The client is allowed to know the rules and grey out illegal moves — that is good UX. It is never
allowed to *decide* one.

---

## State, intents, events

Three flows, and keeping them distinct is what keeps the implementation tractable:

```
 client  ──── intent ────►  server        "I want to move Crew Tray 3 to card B4"
                              │
                              ├─ validate against rules + current phase + current actor
                              ├─ apply to authoritative state
                              │
 client  ◄──── event ─────────┘           "Crew Tray 3 moved to B4; 4 Moves remain"
 client  ◄──── view  ─────────┘           per-player redacted snapshot
```

- **Intents** are what a player wants to do. They are proposals; they can be rejected.
- **Events** are what happened. They are facts, ordered, and form the game's history.
- **Views** are per-player projections of state, with other players' hands redacted.

An event log that fully determines state is worth building even at this scale. It gives
reconnection (replay to catch up), persistence (store the log, not a snapshot), and debugging (replay
a reported bug exactly) for one design decision.

### Post-game replay — a desired future capability

**Not to be implemented yet.** Recorded here so it is not accidentally designed out, because the
architecture above already produces almost everything it needs.

The goal: when a game finishes, players can watch it back at speed — the Division filling with
traffic, the wrecks, who was Superintendent when. For a game whose drama is largely invisible while
you are playing your own Office, this is worth more than it would be in most games. You spend the
game watching your own station; the replay is where you find out what the railroad was doing.

State is already `fold(events)` over an ordered, append-only log, and all randomness derives from a
stored seed. Replay is therefore a **read-only projection over the existing log** — no new rules-engine
surface, no second code path, nothing the server has to do differently during play.

Three constraints keep it cheap, and all three are free if honoured from the start:

1. **Events must render standalone.** A `TrainMoved` should carry its from/to, not just an id the
   renderer has to resolve against live state. An event stream that only makes sense when applied in
   order to a mutable world is replayable but not *displayable* — the renderer would need to
   reconstruct the whole board to draw one frame.
2. **Finished games keep their log, seed and config.** See the retention note in
   [`lobby-and-sessions.md`](lobby-and-sessions.md).
3. **Timing is a presentation concern.** The log has no wall-clock pacing and does not need any —
   the replay UI decides how fast to advance. Do not record timestamps for the benefit of replay;
   Stage and phase boundaries are the natural beats.

The one genuine open question is **whose view a replay shows**. Hands were private during play, and
revealing them afterwards is a design choice rather than a technical one — it changes what players
learn about each other between games. Worth deciding before building, not during.

---

## Transport

The game is turn-based with exactly one actor at a time, so the traffic pattern is bursty and small —
but every player needs to *watch* the acting player, and above all to watch the Mainline Phase, where
trains move and wrecks happen while the Superintendent rules on clearances.

That means the server must push. A request/response API alone would leave four players polling to
watch a fifth switch cars.

**WebSocket, with the HTTP endpoints alongside it** for lobby operations (list games, create, join)
where a request/response shape is the natural fit. Server-Sent Events would also serve, since the
push is nearly one-directional and intents are rare enough to send over HTTP — worth keeping in mind
if the eventual stack makes SSE materially simpler.

What matters more than the choice: **every state change reaches clients as an event on one ordered
stream**. Clients apply events in order and never mutate state locally in a way that could drift.

---

## Timing and the absence of clocks

Nothing in the rules is real-time. A Stage advances when the phase sequence completes, not when a
timer fires. There are no reflexes to test and no reason to rush a player.

The one place real time may need to intrude is a player who walks away mid-game. That is a session
concern, not a rules concern — see [`lobby-and-sessions.md`](lobby-and-sessions.md). Any turn timer
is a lobby setting layered on top of the rules, never part of them.

---

## What the server does not need

Worth stating, because small self-hosted scale makes several standard concerns disappear:

- **No horizontal scaling.** A handful of concurrent games fits in one process. Game state lives in
  memory; the event log is persisted for durability, not for coordination.
- **No matchmaking service.** Players share a game code (see `lobby-and-sessions.md`).
- **No accounts system, initially.** A display name plus a session token is enough to join and
  reconnect. Real accounts can be layered on later without touching the game engine.
- **No anti-cheat beyond authority.** Server authority plus redacted views is the whole model.

---

## The one boundary that matters

Keep the **rules engine** free of any knowledge of networking, storage, or players-as-connections:

```
   rules engine        pure. state + intent → events, or a rejection.
        ▲              no I/O, no clock, no sockets. deterministic given a seed.
        │
   game session        owns one game's state, applies intents, emits events
        ▲
   lobby / transport   connections, reconnection, persistence, HTTP + WebSocket
```

The rules engine being pure and deterministic is what makes the whole thing testable — you can drive
a full game through it in a unit test with no server at all, which matters a great deal for a game
with this much rules surface. It is also what would make a future Solitaire offline mode or an AI
opponent cheap rather than a rewrite.

Everything in [`game-state.md`](game-state.md) belongs to the rules engine. Everything in
[`lobby-and-sessions.md`](lobby-and-sessions.md) belongs outside it.

**The engine is reactive *and* active.** It exposes two entry points, not one:

```
apply(state, intent)  → events | rejection      a player did something
advance(state)        → events | needsInput     the game moves on its own
```

The second matters more than it looks. Mainline movement, collision resolution, Stage and Day
advance, Fedora rotation and train make-up all happen with no player acting — at a table the players
do this collectively, and in software something must. Keeping that **phase driver** inside the engine
rather than in the session layer means all rules knowledge stays in one pure deterministic place, and
a balance simulation becomes a loop over these two functions with no server involved.

For the full list of buildable pieces — where each runs, when, and how big — see
[`components.md`](components.md).
