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.
This commit is contained in:
+147
@@ -0,0 +1,147 @@
|
||||
# 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 3–4) 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.
|
||||
Reference in New Issue
Block a user