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

148 lines
7.2 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.
# 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.