Eight releases of fixes since v0.7.9, most of them multiplayer, which is
what this package is for. Submodule pin and `startos/versions/current.ts`
only — no Dockerfile, main.ts or manifest change, as an ordinary bump
should be.
Bumped in place rather than spun off: v0.7.9's migration body is empty,
which is `versions.md`'s common case, and git history keeps its release
notes.
FOUR-COMPONENT VERSION, CHECKED. Every example in `versions.md` has
three, so `0.7.9.8:0` was parsed with the SDK's own ExVer parser before
being written down: upstream `[0,7,9,8]`, which sorts above `[0,7,9]`, so
a box on 0.7.9:0 takes this as an update rather than refusing it as a
downgrade.
GAMES IN PROGRESS ON THE BOX RESUME, and that was established rather than
hoped for. v0.7.9 broke solitaire saves because it changed a RULE, and
the rule behind that is that a save is a seed plus the moves played,
replayed through the CURRENT rules — so the question for any bump is
whether something became illegal. `git diff v0.7.9..HEAD -- src/engine/`
is, in full: `isExpedited` exported and widened to a structural parameter
type, `isFreight` exported, one new read-only helper
(`freightRuleSpentHere`, which only the blocked panel asks), and
`check('draw.end')`'s inline hand-limit test replaced by a call to
`overHandLimit(state, player)` holding the identical expression. No
predicate changed its answer. The release notes say so in all five
locales rather than leaving it to be discovered.
UPDATING.md CORRECTED: it said a running server's saves "are stamped with
`engineVersion` and refuse to resume under a mismatched one". They are
stamped with it and it is reported, but `src/server/index.ts` decides by
attempting the replay and names the versions only inside the failure
message — its own comment says why, that "some version differs" was never
enough to act on. A doc that sends the next person to schedule downtime
for a bump that needs none costs about as much as the reverse. It now
carries the diff command to ask the question of the code instead.
What players get, briefly: two multiplayer information leaks closed (the
seed in the shared log, and a blind Home Office draw naming its card); a
history panel that came back EMPTY after a mid-game browser reload; the
extended-play vote no longer naming an arbitrary player when the vote is
open to everyone at once; a train held at your Limits by an Interlocking
no longer vanishing off the board until a track frees; trains queued for
a Crew Tray saying so with the free count; a Red Flag drawn on the map; a
spent Telegraph/Telephone/Radio struck through; the Campaign Train saying
whether its speeches are made; and two bugs from the last playtest — the
Express's one-freight-car-per-location rule explained where it refuses
you, and westbound trains drawn in the correct half of a card.
README.md and instructions.md updated in the same change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y5boPxP6JHRYMm8adXaF5R
383 lines
26 KiB
Markdown
383 lines
26 KiB
Markdown
<p align="center">
|
|
<img src="icon.png" alt="Station Master Logo" width="21%">
|
|
</p>
|
|
|
|
# Station Master on StartOS
|
|
|
|
> Everything not listed in this document should behave the same as upstream
|
|
> Station Master. If a feature, setting, or behavior is not mentioned here,
|
|
> the upstream documentation is accurate and fully applicable — see the
|
|
> Documentation section of `instructions.md` for links.
|
|
|
|
Station Master is a railroad operations board game with an authoritative multiplayer server;
|
|
solitaire needs no server and is not what this package is for. This package runs that server —
|
|
the browser client, the lobby, and the intent/SSE API — as a single StartOS service.
|
|
|
|
**Bundled version: 0.7.9.8.** Eight releases of fixes since 0.7.9, most of them multiplayer, and
|
|
**games in progress on this server resume normally** — which was checked rather than assumed.
|
|
|
|
**Why saves are safe this time, and how that was established.** 0.7.9's breakage came from a rules
|
|
change, and the general rule behind it is that a save is a seed plus the moves played, replayed
|
|
through the *current* rules — so any change that makes a once-legal move illegal stops an older save.
|
|
The question for this bump is therefore narrow: did anything become illegal? `git diff v0.7.9..HEAD
|
|
-- src/engine/` is, in its entirety, four things — `isExpedited` exported and widened to a structural
|
|
parameter type, `isFreight` exported, one new read-only helper (`freightRuleSpentHere`, which only
|
|
the blocked panel asks), and `check('draw.end')`'s inline hand-limit test replaced by a call to
|
|
`overHandLimit(state, player)` holding the identical expression. **No predicate changed its answer.**
|
|
Eight releases, none of them in the rules.
|
|
|
|
Note that `engineVersion` is *not* a gate. `src/server/index.ts` attempts the replay and reports the
|
|
stored and running versions only in the failure message; a game is refused because a move no longer
|
|
replays, never because the version string differs.
|
|
|
|
**Two multiplayer information leaks.** The random seed was announced in the shared narration log at
|
|
game creation, and a blind Home Office draw resolved the drawn card to its real name for every seat.
|
|
Both were found by reading a plan rather than by a test, which is why 0.7.9.4 added a systematic
|
|
redaction net: a seat's Frame, the spectator projection and the narration are serialised and searched
|
|
for every opponent card id, every card name unique to one hand, the seed, and any private decision
|
|
data — with an allow-list of every public property that fails the suite when a field is added.
|
|
|
|
**The history panel came back empty after a mid-game reload.** `Frame.lines` carried the whole
|
|
narration log on every push to every seat and nothing read it — `RemoteSession` accumulates from
|
|
`Push.lines` alone. The waste was masking the fault: `connect()` cleared the frame cache but not the
|
|
narration watermark, so a reconnecting seat was told "nothing new since your last push" while the
|
|
browser it was answering had just reloaded from an empty accumulator.
|
|
|
|
**Whose turn it is, during the extended-play vote.** The §3.3 vote is parallel — every un-voted seat
|
|
may vote at once — so there is no actor to be, and the engine says so. The turn chart named the last
|
|
seat to move anyway, beside a tally correctly showing three seats outstanding. Cause: two functions
|
|
answering one question, one carrying a status guard and one not; there is one now, and it is the same
|
|
function that *refuses* an intent, so the screen can no longer name somebody the server would turn
|
|
away.
|
|
|
|
**A train held at the Limits was drawn nowhere.** An Interlocking stops an inbound train on the Limit
|
|
Track instead of colliding with a full Office. `arriveAtOffice` removes the tray from the Mainline
|
|
node's `transits` and the Interlocking branch never assigns `tray.position`, so with the map drawing
|
|
mainline nodes from `transits` and squares from `position.at === 'grid'`, the train was in neither —
|
|
it vanished off the board until an A/D track freed. Fixed in the view; the engine state was right.
|
|
|
|
**Also in this range.** Trains queued for a Crew Tray (a Timetabled departure, a played Extra, an
|
|
ordered second section) now report themselves with the free-tray count, where two of the three had
|
|
been waiting invisibly behind an exhausted pool. A Red Flag standing at an Office's Limits is drawn
|
|
on the map. A spent Telegraph/Telephone/Radio is struck through and says when another district holds
|
|
the Fedora — it is the Superintendent's own devices that get spent, and the Fedora moves every three
|
|
Stages. The Campaign Train says whether its speeches are made, which is what decides whether leaving
|
|
it off the Office square is a fault. Two bugs from the last playtest session: the Express's one
|
|
freight car per location rule is explained where it refuses you (the panel had been describing the
|
|
industry instead), and westbound trains are drawn in the correct half of a Mainline card.
|
|
|
|
**Bundled version: 0.7.9.** Enforces solitaire's collision limits, which had been offered as
|
|
settings and never checked — and that is a rules change, so **a solitaire game in progress will not
|
|
resume if it has had collisions**.
|
|
|
|
`advance.ts` gated §3.4's check on `mode === 'competitive' || mode === 'coop'`, while `SOLO_CONFIG`
|
|
carried both limits (3 a Day, 5 total) and the setup screen offered them as live settings with "the
|
|
game ends in a loss" printed beside them. A solitaire player could set a limit of 1 and crash all
|
|
game. Jesse's ruling: the settings do what they say, so the gate went rather than the controls.
|
|
|
|
**Why saves break, and how it was measured.** A solitaire save is `{ seed, history, rules? }` where
|
|
`rules` is the house rules only — **the collision caps are not in the save**, so a restore takes
|
|
today's defaults. A 0.7.8 save therefore replays under live caps and stops at the move that crossed
|
|
one. Measured with a control, because two earlier attempts were wrong: with the caps disabled 1200
|
|
of 1200 generated saves replay every intent; of those, 74 crossed the 5-collision total and **70 of
|
|
the 74 truncate** once the caps are enforced, one at 141 of its 532 intents. An earlier run that
|
|
reported no breakage had in fact been failing every replay at intent 2 on an unrelated house-rules
|
|
mismatch — the control is what caught that. It fails safe: the save is untouched and the game
|
|
declines rather than loading a position the rules could not have produced. **Multiplayer games are
|
|
unaffected** — competitive and coop were always inside that gate.
|
|
|
|
**Also in 0.7.9.** The end-of-game dialog now asks whether to play one more Day; the buttons existed
|
|
but were written into `#actions` _underneath_ a modal whose only control was Close, so a solitaire
|
|
player reaching the end was never offered the extension — and the Gitea#11 verification missed it
|
|
because it drove the HTTP API, which renders no dialog. `Frame.actor` carried `clock.currentActor`,
|
|
null for the whole Mainline Phase, so all three interruptions reported that nobody was holding the
|
|
game up; it carries `actingPlayer` and an `awaiting` field now. The game settings moved off the top
|
|
line into a **This Game** card drawn by the same renderer as the lobby's join preview, with the
|
|
running collision counts taking their place on the top line. The Office Area's auto-hide button was
|
|
a cycle that could not reach every state and is now three controls. The history reads newest first.
|
|
A Heavy Grade card draws which way it climbs.
|
|
|
|
**Bundled version: 0.7.8.** Made the solitaire setup screen reachable at all. 0.7.5 skipped it
|
|
whenever `load()` found a saved game — reasoned as "a saved game is a game to resume" — and a browser
|
|
that has ever played solitaire always has one, so the door could never reach the screen again. The
|
|
door (`?solitaire`) outranks a save now; a bare reload still resumes. Since dealing calls
|
|
`clearSave()`, the screen carries a **Continue saved game** button and states what Deal costs, so the
|
|
door cannot destroy a game in progress. A solitaire save is browser-side, so nothing on this server
|
|
is involved either way. The splash footer also now names both ways to play.
|
|
|
|
**This was reported three times before it was found, and the first two fixes were real bugs that were
|
|
not it** — a routing fault (0.7.6) and a caching fault (0.7.7). Both were reported as verified, and
|
|
both verifications read what the SERVER returned rather than exercising the path with the state a
|
|
returning player actually has. What found it was a failing test written before the fix. Worth knowing
|
|
when the next "that didn't work" arrives: reproduce the reporter's state first.
|
|
|
|
**0.7.7 fixed the client caching** that stopped the two releases before it from ever reaching a
|
|
browser. `build-web.ts` stamps a build tag onto every module URL as a cache key, and
|
|
its fallback when `git rev-parse` fails was the literal `nogit` — which is precisely the `.s9pk`
|
|
case, since the Dockerfile copies the working tree in without `.git`. So every packaged release
|
|
published `./web/main.js?v=nogit`, byte-identical to the one before, and a returning browser refetched
|
|
nothing. `serveStatic` also sent no `Cache-Control` at all, so the pages that carry those tags were
|
|
themselves served from cache. The tag is now the package version plus the build timestamp, and a
|
|
request carrying `?v=` is `immutable` for a year while everything else is `no-cache`.
|
|
|
|
**Diagnosing "my fix did not ship".** A hard reload is NOT a sufficient check — confirmed in the
|
|
field on 0.7.6: the document refetches but ES module sub-imports keep their cached `?v=` URLs, so the
|
|
module graph stays stale. A fresh private window is the reliable test. Reading what the server
|
|
returns (`curl` inside the container) proves what was installed, never what a browser is running.
|
|
|
|
**0.7.6 fixed the solitaire door 0.7.5 introduced**: a browser that had ever held a multiplayer seat
|
|
could not reach the new setup screen at all — a bare page load could not tell "clicked Play
|
|
solitaire" apart from "reloaded mid multiplayer game", so the door lost to whatever game or lobby
|
|
that browser last touched. The door marks its intent explicitly now (`?solitaire`), the same fix
|
|
`?lobby` already carries for the door on the other side.
|
|
|
|
**0.7.5 was a client-side flow change**: a genuinely fresh visit to the solitaire page opens a setup
|
|
screen and asks for the game's options before dealing, the same question the multiplayer lobby has
|
|
asked before a game starts since 0.6.0.
|
|
|
|
**0.7.4 bundled three rules corrections** off the tracker (Gitea#13, #5, #19), all of them places
|
|
where the code and the cards disagreed.
|
|
|
|
**The Yard Office is offered rather than imposed (Gitea#5).** It was implemented in a form missing
|
|
all three of its conditions: a qualifying train was teleported onto the card, so nobody was asked, no
|
|
route was walked — the card's printed "that can reach the yard office in one move" was unenforced —
|
|
and nothing was ever met on the way in. The arrival now interrupts the Mainline Phase to ask the
|
|
district's owner, reachability is the engine's own move walk, and cars on the lead collide. Where no
|
|
route exists the offer is withheld and the history says why.
|
|
|
|
**Some Extras must run loaded (Gitea#13).** Circus, Campaign and Military trains take a loaded car
|
|
while the Division Yard can supply one, an empty once it cannot, and may depart short. The per-stop
|
|
point is earned once per Office Area rather than once per game, and only by a fully loaded train.
|
|
Two long-standing bugs went with it: `emptiesOnly` was rendered to the player and enforced nowhere,
|
|
and a set-up out on the Mainline paid its point to player 0 whoever was playing.
|
|
|
|
**Red Flags is a different card (Gitea#19).** The old rule protected a stopped train on the Mainline
|
|
and was played 4 times in 4,212 offers across 600 games. It is now a directional flag on your own
|
|
Limits, spent on the train it stops, playable either in phase or at the moment the engine sees a
|
|
certain collision.
|
|
|
|
**GAMES IN PROGRESS MAY NOT SURVIVE AN UPDATE FROM BEFORE 0.7.4.** Unlike 0.7.3, which changed no
|
|
rules, each of the three above can stop an older history replaying: the Red Flags intent changed
|
|
shape, a make-up that was legal may now be refused, and a Yard Office arrival asks a question no
|
|
older history has an answer for. It fails safe — `src/server/index.ts` refuses to resume a save the
|
|
rules reject, logs which move it stopped at, and **leaves the file untouched**, so an operator can
|
|
put 0.7.3 back on to finish a game that matters. A game that never meets one of the three carries on
|
|
normally, which is why this is "may not" rather than 0.7.2's "will not". **0.7.5 carries every game
|
|
forward without exception** — see above.
|
|
|
|
**Diagnosing the interruptions.** The Mainline Phase can now stop and ask three different questions,
|
|
of three different players: §8.1's clearance goes to the Superintendent, the Yard Office offer and
|
|
the Red Flag prompt to the owner of the district a train is arriving at. A game sitting on one is
|
|
waiting on a person, not stuck. `Games in Progress` shows it as active with nobody to wait for,
|
|
because an interruption is not a turn — the same reading as an extension vote.
|
|
|
|
0.7.0 remains the shape of the service: the lobby sets a game up from one of four game types
|
|
(Solitaire, Co-op, Competitive, Cutthroat) plus Custom, offers the whole rule set for reading before
|
|
a seat is taken, and lets a player leave a lobby or a running game and come back to it. None of this
|
|
changes anything the package itself does: the submodule pin is the version, and the service is the
|
|
same server it always was.
|
|
|
|
---
|
|
|
|
## Table of Contents
|
|
|
|
- [Image and Container Runtime](#image-and-container-runtime)
|
|
- [Volume and Data Layout](#volume-and-data-layout)
|
|
- [File Models](#file-models)
|
|
- [Dependencies](#dependencies)
|
|
- [Network Access and Interfaces](#network-access-and-interfaces)
|
|
- [Installation and First-Run Flow](#installation-and-first-run-flow)
|
|
- [Actions](#actions)
|
|
- [Tasks](#tasks)
|
|
- [Health Checks](#health-checks)
|
|
- [Backups and Restore](#backups-and-restore)
|
|
- [Limitations and Differences](#limitations-and-differences)
|
|
- [Quick Reference for AI Consumers](#quick-reference-for-ai-consumers)
|
|
|
|
---
|
|
|
|
## Image and Container Runtime
|
|
|
|
Built from source with a custom `Dockerfile` — there is no published Station Master image.
|
|
|
|
| What to Document | Value |
|
|
| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
| Image source | Custom multi-stage `Dockerfile`: `node:*-slim` builder runs `npm run build:web` to produce the static client, then a second `node:*-slim` stage copies only `package.json`, `src/`, and the built `dist/` — no `node_modules` in the runtime stage, since the server has zero runtime dependencies |
|
|
| Architectures | x86_64, aarch64 |
|
|
| Entrypoint | `node src/server/index.ts` — runs straight from TypeScript source; no compile step, see Limitations |
|
|
|
|
One subcontainer: `station-master-sub`, running the single daemon `server`.
|
|
|
|
## Volume and Data Layout
|
|
|
|
One volume, `data`, mounted at `/data` (`DATA_DIR`).
|
|
|
|
| What to Document | Value |
|
|
| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
| Volume names | `data` |
|
|
| Mount points | `/data` |
|
|
| StartOS files | `store.json` — holds the join and admin secrets (see File Models) |
|
|
| Database | None — flat files. The server holds any number of games at once: each is `games/<gameId>/game.json` (`{ engineVersion, seed, config, playerNames, history, status, createdAt, lastMoveAt, botSeats }`) plus `turn-timings.json`, with a top-level `index.json` naming every game and lobby state for those not yet started |
|
|
|
|
## File Models
|
|
|
|
One StartOS-managed file, `store.json`, on the `data` volume.
|
|
|
|
- **`store.json`** — JSON, holding two independent secrets, both seeded on install by
|
|
`init/generateSecrets.ts` and both read reactively by `main.ts`, so rewriting either restarts the
|
|
daemon with the new value.
|
|
- `joinSecret` — 24 characters. What players need to create or join a game. The daemon will not
|
|
start without one, so it is never left unset. Rewritten only by the **Get Join Secret**
|
|
action, which mints a new value on every run.
|
|
- `adminSecret` — 32 characters. Gates the server's `/api/games` routes, which the two game
|
|
actions use. **Deliberately not the join secret**: every player holds that one, so gating a
|
|
delete with it would let anyone at the table destroy anyone else's game. It is never shown to
|
|
a player and never leaves the package except as the daemon's `ADMIN_SECRET`. Backfilled on
|
|
update for a volume written before this field existed, and otherwise never rewritten.
|
|
|
|
Neither key is re-asserted on start, so a hand edit to either survives until the relevant action
|
|
is next run.
|
|
|
|
Everything else under `/data` — `games/`, `index.json`, lobby and session state — is the
|
|
application's own persistence, written directly by the server process, not by a StartOS file
|
|
model.
|
|
|
|
## Dependencies
|
|
|
|
None.
|
|
|
|
## Network Access and Interfaces
|
|
|
|
One interface, `ui`, on the single port the daemon listens on. It serves the browser client, the
|
|
lobby and intent HTTP API, and the per-seat SSE game stream — all from the same origin, which is
|
|
what lets a player reach the server from a LAN address and another player reach it from a
|
|
different one in the same game.
|
|
|
|
## Installation and First-Run Flow
|
|
|
|
No setup wizard. On install, both secrets are generated and stored immediately (see File Models),
|
|
and a critical task is raised pointing at **Get Join Secret** — the service starts and is usable
|
|
the moment the daemon is healthy, but a player cannot create or join a game until the join secret
|
|
has been retrieved and shared with them.
|
|
|
|
**Updating the package keeps games in progress, unless the rules actually changed.** On boot the
|
|
server replays each saved game's moves through the current engine and resumes it if they all still
|
|
apply — the version that wrote the file is recorded and reported but decides nothing. When a move
|
|
_is_ rejected, that game is refused and the log names it: `move 3 of 8 (localOps.choose) is
|
|
rejected by the current rules with OPTION_ALREADY_CHOSEN`. A refused game is never modified or
|
|
deleted, so reinstalling the previous version makes it loadable again and it can be played out.
|
|
|
|
Earlier versions of this package compared version strings instead, which destroyed every game in
|
|
progress on every update, including updates that changed only how the board is drawn.
|
|
|
|
## Actions
|
|
|
|
Two of the three read or change the games on the server, and both are `only-running`: what they
|
|
report exists only inside the live server process. A save is a seed plus a list of moves, so
|
|
"whose turn is it" is answerable only by replaying the game through the engine — which lives in
|
|
the game repo, not in this package. The server has already done that work and is asked for the
|
|
answer.
|
|
|
|
- **Get Join Secret** (`get-join-secret`) — run this any time you want to read the current join
|
|
secret, or to invalidate it and issue a new one. Every run **generates a fresh secret,
|
|
overwrites the stored one, and restarts the daemon** with it — there is no read-only mode.
|
|
Rotating does not disconnect players already seated in a running game (D14's join secret gates
|
|
the lobby door, not an in-progress session); it only invalidates the old value for anyone who
|
|
has not yet joined or created a game. Completes in a few seconds, safe to repeat.
|
|
|
|
- **Games in Progress** (`games-in-progress`) — read-only, changes nothing, safe to run at any
|
|
time. Lists every game and lobby on the server with its code, players, Day/Stage/phase, who it
|
|
is waiting on, when it started and when it last moved. Run it to find a game that has stalled —
|
|
a `Last move` days old with a named player under `Waiting on` is someone who is not coming back.
|
|
Returns quickly; the server answers from memory.
|
|
|
|
- **Manage Game** (`manage-game`) — pick a game from a dropdown built live from the server, then
|
|
either **Export** it (returns the complete save as copyable text and changes nothing) or **End**
|
|
it (deletes it from the server, disconnects anyone still watching, and removes its files and
|
|
index entry). **Ending cannot be undone and is not idempotent** — a second attempt reports that
|
|
the game no longer exists. It always returns the deleted game's save, so nothing is destroyed
|
|
without being handed back first; that text is the only remaining copy, so keep it if the game is
|
|
worth replaying. This is the only way a game ends other than being played to a finish: an
|
|
abandoned game otherwise stays active and is resumed on every restart indefinitely.
|
|
|
|
## Tasks
|
|
|
|
- **Get the join secret to share with players** — raised on install, severity `critical`. Points
|
|
at the **Get Join Secret** action. Clears the moment that action is run for the first time; it
|
|
does not return afterward (running it again to rotate does not re-raise it).
|
|
|
|
## Health Checks
|
|
|
|
- **Multiplayer Server** — fetches the server's own `/api/health` and reports what it says:
|
|
_"Multiplayer server is ready — 3 games in progress"_, with _", 1 waiting to start"_ appended
|
|
only when a lobby exists, and _"no games in progress"_ on an idle server. So the check that
|
|
proves the server is answering also says how much is going on.
|
|
|
|
A server that does not answer is reported as **starting, never failed**. The server replays its
|
|
saved games before binding its port, so a boot legitimately looks like nothing is listening, and
|
|
calling that a failure would make an ordinary restart look like a crash. Replaying measures
|
|
around 100 ms per game and only unfinished games are replayed, so in practice this window is a
|
|
fraction of a second. A check stuck on "starting" for much longer means the daemon is failing to
|
|
come up — read the service logs, where a refused resume names the game and the version that
|
|
wrote it.
|
|
|
|
## Backups and Restore
|
|
|
|
Strategy: the entire `data` volume is backed up wholesale (`ofVolumes`) — every file copied
|
|
exactly as it sits on disk, nothing dumped and replayed.
|
|
|
|
That includes every game's full intent history, so a restore can resume any in-progress game
|
|
exactly where it left off, and the join secret in `store.json`, so previously shared join links
|
|
keep working after a restore. Nothing needs to be rebuilt or re-entered after a restore; the
|
|
server resumes every saved game on boot the same way it does after an ordinary restart.
|
|
|
|
## Limitations and Differences
|
|
|
|
1. **No compile step.** The image runs the server directly from TypeScript source using Node's
|
|
native type stripping, rather than building a `dist/` for the server the way the browser
|
|
client is built. This is upstream's own deployment shape (`docs/architecture/deployment.md` in
|
|
the game repo), not something this package changed.
|
|
2. **The join secret has no per-player identity.** It is a single server-wide value (D14) that
|
|
gates who may create or join a game — it is not a username or password, and StartOS has no
|
|
visibility into who a player is once they've joined.
|
|
3. **No accounts, and one game at a time per person is expected but not enforced** — a deliberate
|
|
upstream design decision (`docs/architecture/multiplayer.md` §11 D13), not a StartOS-specific
|
|
limitation. Since 0.7.0 a browser can hold seats in several games at once and picks between them
|
|
in the lobby, which is a client convenience rather than a change to that decision.
|
|
4. **A player's identity lives in their browser.** The session token issued at join is the only
|
|
proof of who a player is; it is kept in that browser's `localStorage` and nowhere else. A player
|
|
may leave a running game and rejoin it (the lobby lists every game the browser is in), but a
|
|
token lost with the browser — cleared site data, a different device — cannot be recovered from
|
|
this package, and the seat stays in the game waiting. Upstream `TODO.md` tracks an
|
|
administrator-issued rejoin link; it does not exist yet.
|
|
|
|
---
|
|
|
|
## Quick Reference for AI Consumers
|
|
|
|
```yaml
|
|
package_id: station-master
|
|
image: built from source (Dockerfile)
|
|
architectures: [x86_64, aarch64]
|
|
subcontainers: [station-master-sub]
|
|
volumes:
|
|
data: /data
|
|
file_models:
|
|
- store.json
|
|
startos_managed_env_vars:
|
|
- DATA_DIR
|
|
- JOIN_SECRET
|
|
- ADMIN_SECRET
|
|
dependencies: none
|
|
interfaces:
|
|
ui: { type: ui, port: 8081 }
|
|
actions:
|
|
- get-join-secret
|
|
- games-in-progress
|
|
- manage-game
|
|
tasks:
|
|
- { action: get-join-secret, severity: critical }
|
|
health_checks:
|
|
- Multiplayer Server
|
|
```
|