Files
station-master/CHANGELOG.md
T
Jesse f52ff0e9ac Draw the board as track, split the site into three pages, and fix curve geometry
Adds SVG board rendering shared by the game and the replay, a splash page with a
shareable replay directory, hover tooltips for reference detail, and makes curves
two-port arcs so sidings and run-arounds can finally be built.
2026-08-01 13:35:53 -04:00

7.2 KiB
Raw Blame History

Changelog

Detail behind each commit. Commit messages stay high level; the reasoning, the measurements, and the things that turned out to be wrong live here.

Measured figures are 100 solitaire Standard games with the developer bot unless stated otherwise. The target is 20 Revenue over 5 Days.


Unreleased

Nothing yet.


Board rendering, three-page site, curve geometry

Drawing the board as track, splitting the site, tooltips, and the curve fix.

Board rendering

Two renderers in src/sim/board-svg.ts, chosen because they fail in opposite places:

  • Office Area — the district stays a map. Cards on a grid with the rails drawn edge to edge, so a join is rail meeting rail rather than two descriptions that happen to agree. The through rail sits at a constant height on every card, which is the alignment the printed cards use.
  • The Division — not a map but a queue of sections with hard capacities, so it is a dispatcher's diagram: one line per track, 1 of 2 free under each section, no limit — trains queue at the Division Points.

Both are self-contained — no imports, no module-level helpers — because the playable app imports them normally while the replay is a single HTML file with an inline script that cannot import anything, and embeds them via Function.toString(). One implementation either way; a second copy would eventually draw a different board from the same state.

Rails come from the engine's own connectionsFor, now exported. A drawn rail cannot claim a connection the rules do not have — visible in that Modifier cards draw no rails at all.

Capacity, confirmed from the rules and now shown: Division Points unlimited, most Mainline cards 1, Double Track and Uncontrolled Siding 2, Offices 1/2/3/4 by tier.

Three pages

index.html is now a splash with two doors. The game moved to play.html; replays.html is a directory.

A replay is a save, and a save is {seed, history}. The engine is deterministic and runs in the browser, so re-submitting the same moves rebuilds the position exactly — 18 KB instead of 3.4 MB, about 190× smaller, small enough to email. A save that would describe an impossible position cannot be replayed at all, because every step goes back through applyIntent; the viewer stops and says so rather than showing a board the rules could not produce.

Static hosting cannot list a directory, so replays/manifest.json is generated at build time from whatever is in public/replays/, with each file validated first.

Tooltips

Reference detail is read once and printing it costs the space the board and action list need. A single delegated tooltip (src/web/tooltip.ts) now carries card effects, action reasoning, and facility state. Not the native title: that waits a second, cannot be styled, and never appears for keyboard users.

Curve geometry — the significant fix

A curve was modelled as a through track plus a diverging leg, which is a turnout. Consequences:

  • Curves were topologically identical duplicates of turnouts — same connections, no distinction beyond the sharp curve's 2-Move cost.
  • Every diverging leg went south, so no piece anywhere reached north except an n-s straight, which connects n↔s and nothing else.
  • Therefore a district could only ever be a vertical column: no siding, no parallel track, no run-around, no second turnout back to the main.

The printed cards (docs/tracks.png, rows 34) show a curve as a single arc from edge to edge with no through track. A curve is now a two-port arc, rotatable to ne/nw/se/sw. A sharp curve is geometrically identical and costs 2 Moves. Turnouts keep §A.1 unchanged — a two-port arc has no third port, so the absent-edge rule cannot apply to it.

Verified through the engine, not the types: a crew leaves the main at a turnout, runs a siding parallel, and rejoins at the far end.

Measurements

revenue freight sidings built
before 4.1 0.5 impossible
geometry only 3.9 0.9 possible, not sought
bot builds sidings 3.3 1.1 59/60 games

Freight more than doubled and the harness recorded its first win, but overall revenue is down from geometry-alone and the bad tail worsened (29 → 59).

Capping turnouts at one or two measured worse (revenue 2.5, freight 0.6) — more ways off the main means more industries the crew can reach, and that beats a tidy Running Track. The uncapped version stands, with that measurement recorded at the code.

The bot builds sidings but does not exploit them. It pays about 20 of its 26 track pieces for them while its switching logic still sets out dead weight on a spur rather than planning a run-around. That is the next piece of work and where the revenue should appear.


Earlier commits

Recorded from memory of the work rather than written at the time; detail thins going back.

f00255c — more fixes and tweaks

Card descriptions on every card in hand and every face-up slot, the three Local Operations options explained, the objective and pace in the header, and rotations named in words rather than "rotation 2". Build stamp added (version, git SHA, -dirty for an uncommitted tree) because nothing tracked what was deployed. Extra X22 was not a bug — a per-diem train whose card calls for a caboose and nothing else — but "loaded caboose" was.

d1f689d — name the caboose, the train and the Running Track hazard

maneuver.redFlags was labelled "Red Flags on tray3": the earlier tray-id fix checked the history log, and the action list was a surface it missed. An industry on the Running Track does not block traffic (§11.2 gives it rails) but a car left standing there is hit by the next arrival (§10).

dd300ac — fix caching, Limits placement, and explain the board better

The first deploy served a fresh index.html against a cached main.js, which threw missing element: target and never started. Every module import now carries the build tag. The Limits fix was half-done: the sign could move, but nothing forbade building past it, so one straight at (0,2) produced limits · Whistle Post · limits · straight · limits.

2eca9de — playable browser build

The solitaire game as a static site, proven to need no server: full games run with every Node global replaced by a throwing stub. Train cards stopped accepting a board placement — seed 555 had offered Extra X15 at six squares with six rotations, all identical.

160190d — the bot's reasoning in the replay

Decision panel showing what the bot chose, why, and every option it passed over, with the reasons reported by the bot itself rather than re-derived by the viewer.

d261ad7 — parking trains, freight deadlock, Whistle Post lock-in

Three faults found by measurement rather than failing tests. Trains parked because destinationsFor computed reverse as facing === 'e' ? 'w' : 'e', so a crew facing south reversed to east — a port a north-south card does not have. Freight ran at a 3% load completion rate because a load started with no spotted car parks on WORK and locks the industry track, blocking the very car that would clear it. Office density doubled after 25 of 100 games never drew a Depot and never escaped a one-track Whistle Post.