184 lines
9.3 KiB
Markdown
184 lines
9.3 KiB
Markdown
# 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
|
||
|
||
### Teaching the bot to use a siding
|
||
|
||
**Nose coupling, which the rules had and the engine did not.** §A.3: "engines also have couplers on
|
||
the front end, so a train can pick cars up onto its nose". Coupling always appended to the back, so
|
||
which end cars landed on did not exist — and that is precisely what a run-around is for. Cars met
|
||
running FORWARD now couple in front, cars met BACKING couple behind, so the approach decides which
|
||
car is next off the tail:
|
||
|
||
running forward -> reefer, boxcar, hopper next off: hopper
|
||
backing up -> boxcar, hopper, reefer next off: reefer
|
||
|
||
The bot prefers a run-around to setting a car down, since it keeps the car — but only when the drop
|
||
can actually follow. Without that guard it ran the loop for its own sake (8 a game, 63 Moves), which
|
||
the shuttling regression correctly failed.
|
||
|
||
**A serious bug found on the way.** The `carsCoupled` reducer cleared **every card in the Office
|
||
Area**, not the ones the crew ran over — its own comment said "every card along the path" while the
|
||
code iterated the whole grid. One coupling anywhere deleted every standing car and every industry
|
||
track in the district, so loads worked over several Stages vanished when a crew picked up an
|
||
unrelated boxcar somewhere else. The event now names the cards it lifted from.
|
||
|
||
**A test replaced rather than relaxed.** "Under 60 Moves a game" started failing. That threshold was
|
||
calibrated when the crew coupled ~0.4 cars a game; it now couples ~8 and drops ~11, and a run-around
|
||
is *supposed* to cost several Moves. Counting Moves was always a proxy for aimlessness, so the test
|
||
now measures the thing itself — Moves per drop-or-coupling — which still fails when the bot is made
|
||
to wander.
|
||
|
||
| | before sidings | now |
|
||
| --- | --- | --- |
|
||
| revenue | 3.9 | 3.2 |
|
||
| freight | 0.9 | 1.0 |
|
||
| drops per game | 3.3 | **11.1** |
|
||
| couplings per game | ~0.4 | **7.9** |
|
||
|
||
Switching is transformed; revenue is not. The crew now does real work — roughly 4 Moves per
|
||
productive act, which is what running a loop costs — but that work is not yet converting into
|
||
Revenue. Where it goes next is an open question rather than a known fix.
|
||
|
||
---
|
||
|
||
## 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.
|