v0.8.0.16 — the guide a tester can reach, and a flag that named the card instead of the gate

v0.8.0.15 wrote a Quickstart for a tester who has never played and then left it
in docs/, where a tester does not look — reachable only by somebody who already
has the repository. Nobody handed the box had it. build-web.ts now copies it to
dist/quickstart.md and the splash page offers it under the three doors, as a
line rather than a fourth door: reading the guide is not a way to play, and
giving it equal weight in that grid would say it is.

COPIED, NEVER RE-WRITTEN. The Markdown document stays the one copy. A
hand-written HTML twin drifts from it on the first edit, which is the failure
#15a was raised about and precisely what the v0.8.0.15 pass spent itself
undoing. It is served as PLAIN TEXT, which is honest rather than good — tables
render as pipes and the links do not click. Rendering it into a styled page
wants a small Markdown converter and is filed as TODO #109; build-web.ts's
comment names that number rather than gesturing at "the next step", so the file
and the worklist cannot drift the way the references just did.

Two things had to be true and tsc checks neither, so both are tests. The href on
the splash page and the filename the build writes are two strings with nothing
connecting them: rename the document and the build quietly publishes nothing
while the page keeps offering a link that 404s. And a .md file must not arrive
as a download — the server's MIME fallback is application/octet-stream, which a
browser saves instead of displaying, so the link would have handed a tester a
file to save rather than a page to read. '.md' is in http.ts's table now, and
the test reads that table out of the source rather than asserting on a copy of
it, which would pass while the real one was wrong.

VERIFIED AGAINST A RUNNING SERVER, not only compiled: 200,
text/plain; charset=utf-8, the guide's own first lines, and the splash link
resolving.

THE sortsCars COMMENT. Asked after v0.8.0.15 whether everything now agreed, and
the audit turned up one place that did not — the field's own doc comment named
the card's printed text as though it were the flag's meaning. Nothing reads it
to permit a sort; its two readers, resolveExtraStart in apply.ts and the
enumeration in legal.ts, both ask whether this is the one Mainline card with a
Yard Limit and therefore the one an Extra may be made up and started on. Comment
only, and worth the bump because of where it is: it is what a developer reads
before using the flag, and it is the likeliest source of the sentence v0.8.0.15
had to correct off the board. The name is kept for its link to the card face and
the comment now says outright that the name is not the meaning.

The five references and docs/design.md read v0.8.0.16. They describe this build
because the audit re-checked them against it, not because the number was swept
forward — a stamp bumped without a reading is worth less than none.

1001 fast tests and 35 sim tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MUizFYCMHRWhbWwXhp7WPR
This commit is contained in:
Jesse.Markowitz
2026-09-21 01:28:51 -04:00
co-authored by Claude Opus 5
parent f308a2d94d
commit 92bea930ec
14 changed files with 203 additions and 9 deletions
+85
View File
@@ -19,6 +19,91 @@ page as `v0.1.0 · <sha> · <date>`, so what is deployed can always be identifie
---
## 0.8.0.16 — 2026-09-20
The Quickstart put where a tester can actually reach it, the last place that still told the old
story about the Interchange, and the release that carries v0.8.0.15's corrected card text into a
package.
### The Quickstart is published beside the game
v0.8.0.15 wrote a Quickstart for a tester who has never played, and then left it in `docs/`, where
a tester does not look — reachable only by someone who already has the repository. Nobody being
handed the box has it.
`build-web.ts` now copies `docs/StationMaster-Quickstart.md` into `dist/quickstart.md`, and the
splash page offers it under the three doors: *New to Station Master? Read the Quickstart guide.*
**Not a fourth door** — reading the guide is not a way to play, and giving it equal weight in that
grid would say it is.
**Copied, never re-written.** The Markdown document stays the one copy. A hand-written HTML twin
would drift from it on the first edit, which is exactly the failure #15a was raised about and
exactly what the v0.8.0.15 pass spent its time undoing: four references a month and two minor
versions out of date.
**Served as plain text, which is honest rather than good.** Tables render as rows of pipes and the
links do not click. Rendering it into a styled page wants a small Markdown converter and is filed
as **TODO #109**; this is the fifteen-minute version that gets the guide in front of testers for
this round instead of leaving them without one.
Two things had to be true and neither is checked by `tsc`, so both are tests:
- **The link has to find the file.** The href on the splash page and the filename the build writes
are two strings with nothing connecting them — rename the document and the build quietly
publishes nothing while the page keeps offering a link that 404s. The test asserts the guide is
in `dist`, is the guide, and is the file the splash page names. The build also warns loudly
rather than silently skipping a missing document.
- **A `.md` file must not arrive as a download.** The server's MIME fallback is
`application/octet-stream`, which a browser saves instead of displaying, so the link would hand a
tester a file rather than a page. `'.md': 'text/plain; charset=utf-8'` was added to the table in
`http.ts`, and the test reads that table out of the source rather than asserting on a copy of it,
which would pass while the real one was wrong.
### `sortsCars` says what it gates, not what the card prints
Asked directly after v0.8.0.15 — does everything now agree? — and the audit turned up one place that
did not. The field's own doc comment read:
```ts
/** Interchange: "Sort cars in new order". */
sortsCars: boolean;
```
which names the printed text as though it were the flag's meaning. Nothing reads it to permit a
sort. Its two readers — `resolveExtraStart` in `apply.ts` and the enumeration in `legal.ts` — both
ask the same question, which is whether this is the one Mainline card with a Yard Limit and
therefore the one an Extra may be made up and started on.
**Comment only, and worth the bump because of where it is.** This is what a developer reads before
using the flag, and it is the most likely source of the sentence v0.8.0.15 had to correct — the one
telling players, on the board, that cars could be sorted at an Interchange. The name is kept for its
link to the card face, and the comment now says outright that the name is not the meaning.
Everything else already agreed, and was checked rather than assumed: `mainlineDescription`, the
generated `rules/as-built.md`, the Mainline deck reference and the Rules FAQ all say the printed
sorting is unimplemented and that a consist is re-ordered at a Small Yard. `rules/implications.md`
is the deliberate exception and was already correct — it transcribes what the card face prints and
then records that the concept is "still unimplemented".
### The reference stamps, and what was filed rather than done
The five documents v0.8.0.15 stamped — the Quickstart, the Rules, Components, the Home deck and the
Mainline deck — plus `docs/design.md` now read **v0.8.0.16**, and the Rules book's implementation
note about `card.discard` says the same. They describe this build because the audit above re-checked
them against it, not because the number was swept forward: the point of the stamp is that it was
earned, and a stamp bumped without a reading is worth less than none.
**TODO #109** holds the part deliberately not done — rendering the guide into a styled page instead
of serving it as plain text. `build-web.ts`'s comment names that number rather than gesturing at
"the next step", so the file and the worklist cannot drift apart the way the references just did.
### Why this is a release at all
One player-visible change, the link to the guide, and one that only a developer reads. The
Interchange correction a player actually sees on the board is v0.8.0.15's; this is the version the
wrapper bundles, so that correction reaches the box rather than only the repository — and a tag
with an uncommitted comment sitting on top of it is not a thing to package.
## 0.8.0.15 — 2026-09-20
The reference documentation brought up to the game as it actually runs, ahead of the next round of
+17
View File
@@ -582,6 +582,23 @@ What the project says about itself, and what it ships alongside the code.
- [ ] **#88** — `card-reference.md`'s industry table may still be stale beyond Grocer's Warehouse and
the Oil Refinery. See **Reference · #88**.
- [ ] **#109** — **Render the published Quickstart instead of serving it as plain text.** v0.8.0.16
publishes `docs/StationMaster-Quickstart.md` to `dist/quickstart.md` and links it from the
splash page, served as `text/plain` — so a tester reads the guide's tables as rows of pipes and
its links do not click. That was the fifteen-minute version, taken deliberately to get the
guide in front of testers for this round rather than to leave them without one.
**Copy the document, do not re-write it.** A hand-written HTML twin drifts from the Markdown on
the first edit, which is the whole argument of #15a. The step is a small Markdown-to-HTML
converter in `scripts/build-web.ts` writing `quickstart.html` beside the game, styled like the
splash page — headings, lists, tables, links and code spans are the whole of what the guide
uses. The `.md` MIME entry in `src/server/http.ts` and the two assertions in
`test/web.test.ts` (`the Quickstart guide reaches the site`) move to the rendered file with it.
**Cost:** an afternoon, most of it in the converter's table and list handling. No dependency —
a Markdown library would be the only runtime dependency this project has, and the guide uses a
small enough subset that it is not worth becoming the first.
---
## Reference — measurements, rulings and rejected approaches
+1 -1
View File
@@ -1,6 +1,6 @@
# Station Master — Components and Markers
**Describes the game as built at v0.8.0.15** (2026-09-20). Previously stamped "v0.4.5", the
**Describes the game as built at v0.8.0.16** (2026-09-20). Previously stamped "v0.4.5", the
prototype rules edition these references were first written against; the filename keeps that stamp
because `src/`, `CHANGELOG.md` and `docs/rules/` all cite this file by name.
+1 -1
View File
@@ -1,6 +1,6 @@
# Station Master — Home Deck
**Describes the game as built at v0.8.0.15** (2026-09-20). Previously stamped "v0.4.5", the
**Describes the game as built at v0.8.0.16** (2026-09-20). Previously stamped "v0.4.5", the
prototype rules edition these references were first written against; the filename keeps that stamp
because `src/`, `CHANGELOG.md` and `docs/rules/` all cite this file by name.
+1 -1
View File
@@ -1,6 +1,6 @@
# Station Master — Mainline Deck
**Describes the game as built at v0.8.0.15** (2026-09-20). Previously stamped "v0.4.5", the
**Describes the game as built at v0.8.0.16** (2026-09-20). Previously stamped "v0.4.5", the
prototype rules edition these references were first written against; the filename keeps that stamp
because `src/`, `CHANGELOG.md` and `docs/rules/` all cite this file by name.
+1 -1
View File
@@ -1,6 +1,6 @@
# Station Master — Quickstart
**For a tester who has never played. Describes the game as built at v0.8.0.15** (2026-09-20).
**For a tester who has never played. Describes the game as built at v0.8.0.16** (2026-09-20).
Read this once before you sit down. It is about twenty minutes of reading and will save you an hour
of confusion. The deeper references are listed at the end.
+2 -2
View File
@@ -1,6 +1,6 @@
# Station Master — Rules
**Describes the game as built at v0.8.0.15** (2026-09-20). Previously stamped "v0.4.5", the
**Describes the game as built at v0.8.0.16** (2026-09-20). Previously stamped "v0.4.5", the
prototype rules edition this reference was first written against; the filename keeps that stamp
because `src/`, `CHANGELOG.md` and `docs/rules/` all cite this file by name.
@@ -206,7 +206,7 @@ roll a tail cut into a connected Freight Facility.
**Freight Agent.** Make one of these operations, then the turn ends: stock one green outbound box from a matching loaded Division Yard car; clear one red inbound box to the Classification Yard; unjam one outbound, inbound, or MEN | AT | WORK load to the Classification Yard; or explicitly end without acting. Freight can be stocked only when an unclaimed empty matching car is already spotted at that industry. Passengers may wait in a green Office box without a train present.
Implementation note, still true at v0.8.0.15: `card.discard` is accepted during Local Operations
Implementation note, still true at v0.8.0.16: `card.discard` is accepted during Local Operations
without checking that the Draw option was chosen — unlike `card.play`, which does check. This is an
implementation quirk rather than a fourth published turn option.
+1 -1
View File
@@ -66,7 +66,7 @@ must do.
## Current status
**v0.8.0.15.** Rules formalized, card faces specified, architecture documented, and the game
**v0.8.0.16.** Rules formalized, card faces specified, architecture documented, and the game
playable **solitaire and multiplayer** in a browser against an authoritative server. See
[`../CHANGELOG.md`](../CHANGELOG.md) for what each version changed and [`../TODO.md`](../TODO.md) for
what is open; this section is the shape of the project, not a running tally, because a
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "station-master",
"version": "0.8.0.15",
"version": "0.8.0.16",
"private": true,
"type": "module",
"description": "Station Master — a railroad operations game",
+21
View File
@@ -188,6 +188,27 @@ if (existsSync(imageSrc)) {
for (const f of readdirSync(imageSrc)) copyFileSync(join(imageSrc, f), join(imageOut, f));
}
/**
* The Quickstart, published beside the game so a tester can reach it from the box.
*
* COPIED, NOT RE-WRITTEN. `docs/StationMaster-Quickstart.md` is the one copy; a hand-written HTML
* twin would drift from it on the first edit, which is the whole lesson of TODO #15a and of the
* 2026-09-20 documentation pass that found four references a month out of date.
*
* SERVED AS PLAIN TEXT for now, which is honest rather than good: tables render as pipes and the
* links do not click. Rendering it into a styled page needs a small Markdown converter and is
* filed as TODO #109 — this is the fifteen-minute version that gets the guide in front of testers
* for this round rather than leaving them without one.
*/
const guideSrc = join(root, 'docs/StationMaster-Quickstart.md');
if (existsSync(guideSrc)) {
copyFileSync(guideSrc, join(dist, 'quickstart.md'));
} else {
// Loud rather than silent: a missing guide is a broken link on the splash page, and the build is
// the only place that can still notice.
console.error('WARNING: docs/StationMaster-Quickstart.md is missing — the splash link will 404');
}
// A tiny note for whoever unzips this later and wonders what it needs.
writeFileSync(
join(dist, 'README.txt'),
+14 -1
View File
@@ -589,7 +589,20 @@ export type MainlineProfile = {
speedStarts?: { fast: number; slow: number };
/** Double Track: "Trains may pass". */
trainsMayPass: boolean;
/** Interchange: "Sort cars in new order". */
/**
* Interchange only. The card prints "Sort cars in new order" — **and that is not what this flag
* does**, which is why it is worth spelling out where the field is declared.
*
* The printed sorting has never been implemented: nothing reads this to permit a sort, and a
* consist is re-ordered at a Small Yard in a district (`switch.sortConsist`). What this actually
* marks is the one Mainline card with a Yard Limit, and therefore the one an Extra may be made up
* and started on (`apply.ts` § resolveExtraStart, `legal.ts`).
*
* Named for the printed text, and kept that way deliberately — renaming it would lose the link to
* the card face — but the name has already misled once: `mainlineDescription` grew a sentence
* telling players cars could be sorted here, which reached the board and the generated card
* reference before it was caught on 2026-09-20.
*/
sortsCars: boolean;
/** Named entry points printed on the card; some are unlocked by modifier cards. */
entryPoints: readonly string[];
+8
View File
@@ -85,6 +85,14 @@ const MIME: Record<string, string> = {
'.json': 'application/json; charset=utf-8',
'.png': 'image/png',
'.svg': 'image/svg+xml',
/**
* The Quickstart guide, published by `build-web.ts` as `quickstart.md`.
*
* text/plain ON PURPOSE. The fallback below is `application/octet-stream`, which makes a browser
* DOWNLOAD the file instead of showing it — so without this line the splash page's "read the
* guide" link hands a tester a file to save rather than a page to read.
*/
'.md': 'text/plain; charset=utf-8',
};
const HEARTBEAT_MS = 20_000;
+10
View File
@@ -27,6 +27,11 @@ h1{font-size:32px;margin:0 0 2px;letter-spacing:.02em}
a.door:hover{border-color:#4d6fa8;background:#1f2733;transform:translateY(-1px)}
.door h2{font-size:17px;margin:0 0 5px;color:#9fb6d8}
.door p{margin:0;color:var(--dim);font-size:13px;line-height:1.5}
/* Not a fourth door: reading the guide is not a way to play, and giving it equal weight in the
grid would say it is. A line under the doors, where somebody who does not know what to click
will already be looking. */
.newhere{margin:16px 2px 0;color:var(--dim);font-size:13px;line-height:1.55}
.newhere a{color:#9fb6d8}
.door .go{display:inline-block;margin-top:11px;font-size:12px;color:#5aa9e6}
.door.disabled .go{color:var(--dim)}
a.door.disabled{pointer-events:none}
@@ -96,6 +101,11 @@ footer{margin-top:26px;color:var(--dim);font-size:11px;display:flex;gap:18px;fle
</a>
</div>
<p class="newhere">New to Station Master?
<a href="./quickstart.md">Read the Quickstart guide</a> &mdash; what the game is, how you win,
how a Stage runs, what is on the screen, and a first twenty minutes. About twenty minutes to
read, and it will save you an hour of guessing.</p>
<div class="rule"></div>
<footer>
+40
View File
@@ -5509,3 +5509,43 @@ describe('a Division Yard with no coaches is a reportable condition', () => {
assert.equal(waiting.loaded + waiting.empty, 2, 'the waiting coaches were miscounted');
});
});
// ---------------------------------------------------------------------------
/**
* The Quickstart is published beside the game, so a tester on the box can reach it.
*
* WHY THIS IS A TEST. The link on the splash page is a plain href to a file the BUILD copies out of
* `docs/`. Nothing else connects the two: rename the document, or move it, and the build quietly
* publishes nothing while the splash page keeps offering a link that 404s. Neither `tsc` nor any
* other test would notice — the whole failure lives between a file name and a string.
*/
describe('the Quickstart guide reaches the site', () => {
it('is published into dist and linked from the splash page', () => {
const guide = join(dist, 'quickstart.md');
assert.ok(existsSync(guide), 'the build did not publish quickstart.md');
const text = readFileSync(guide, 'utf8');
assert.match(text, /^# Station Master — Quickstart/, 'quickstart.md is not the guide');
assert.match(
text,
/Describes the game as built at v/,
'the guide does not say which build it describes',
);
const splash = readFileSync(join(dist, 'index.html'), 'utf8');
assert.match(splash, /href="\.\/quickstart\.md"/, 'the splash page does not link the guide');
});
it('is served as text rather than handed over as a download', () => {
/**
* The server's MIME fallback is `application/octet-stream`, which a browser downloads instead of
* displaying — so the link would hand a tester a file to save rather than a page to read. The
* table is read straight out of the source: asserting on a copy of it would pass while the real
* one was wrong.
*/
const http = readFileSync(join(root, 'src/server/http.ts'), 'utf8');
const table = http.slice(http.indexOf('const MIME'), http.indexOf('const HEARTBEAT_MS'));
assert.match(table, /'\.md':\s*'text\/plain/, 'a .md file would be served as a download');
});
});