# 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.