v0.1.0 — undo, the crew's reach on the board, workers on the card, and four engine bugs

Fifteen items from two playtest sessions. Three that read as drawing faults were engine
bugs: cars could be added to a train that was not being made up (50 offers in 8 games),
the make-up panel merged two trains and could couple a car to the wrong one, and an
Office upgrade silently deleted what a Modifier had added. A fourth was a sentinel
inside a coordinate's own value range — a Mainline placement travelling as row -1, which
is an ordinary district row.

Trains are now drawn the way they stand: west on the left, nose toward the way the engine
faces, on both the district card and the Division chip. Undo steps back through the game
by replaying the save without its last intent. The switching walk keeps its rejections, so
the board can say why a square is not offered. Laborers and Porters are on the card, and
the rule that a district only grows outwards is finally written down.

Versions start here: third digit for fixes, second for a feature set, 1.0 for a release.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GgtkX8JnvKa8y2tuJ8aQf4
This commit is contained in:
Jesse
2026-08-08 05:23:17 -04:00
co-authored by Claude Opus 5
parent f4c0f49604
commit 5d825b97d2
20 changed files with 1604 additions and 191 deletions
+157
View File
@@ -6,10 +6,167 @@ 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.
## Versions
Every commit carries a bump, decided with Jesse rather than assumed:
- **third digit** — bug fixes to what is already there.
- **second digit** — a new set of features.
- **1.0** — the first release we think is solid enough to call one.
The number lives in `package.json` and nowhere else; `scripts/build-web.ts` stamps it into every
page as `v0.1.0 · <sha> · <date>`, so what is deployed can always be identified from the page itself.
---
## Unreleased
## 0.1.0 — 2026-08-08
The first numbered build. Everything below the "second playtest pass" heading was made under
`0.0.1`, across twenty commits, which is exactly the problem the version convention above exists to
fix: "the current build" was only ever answerable by a commit hash.
### A third playtest pass — and three of the "display problems" were engine bugs
Fifteen items came back from two playtest sessions. Several of the ones that read like drawing
faults were not: the board was reporting the game accurately and the game was wrong.
**Cars could be added to a train that was not being made up.** `newTrain.placeCar` accepted any tray
with room in its consist, while the New Train Phase's own idea of the train it is waiting on —
`trainNeedingCars` — requires the tray to be **at a Division Point** and short of its card. So during
any New Train Phase the Division Yard would hand cars to a train standing on a siding in your own
district, or one halfway across the Division. Measured before the fix: **50 such offers across 8
solitaire games**, including Train 9 mid-crossing with three cars already aboard. That is the
"cars magically appeared on my train" report, and the answer to it.
The two questions — "is this the train being assembled?" and "may this car be added?" — now come from
one predicate (`isBeingMadeUp`, in apply.ts beside `check`), which is what stopped them disagreeing.
`newTrain.passCar` takes the same guard.
**The make-up panel merged two trains into one.** Two trains can be built in the same Stage — a
timetabled train and a Second Section, or an Extra — and the panel collected every option from every
tray, titling the result with whichever tray came first out of the map. Reproduced at seed 99,
Day 3 Stage 12: eighteen car chips under "Making up Train 8", covering two different trains. Worse
than the wrong caption: the Division Yard chip binds to the FIRST matching option, so clicking a
hopper could couple it to the other train. The panel is now scoped to the one tray the engine is
waiting on. This was reported as the history announcing Train 9 while the panel said Train 10.
**Upgrading the Office deleted Modifier bonuses.** `officeUpgraded` wrote the new tier's printed
numbers straight over the facility, so a Restaurant beside a Depot — +1 passenger out, +1 porter —
was silently erased by the next upgrade, after the card had been spent. The tier is applied as a
**difference** now, so the upgrade raises the Office by exactly what it is worth and leaves what is
standing beside it alone.
**A sentinel inside a coordinate's own value range.** ABS Signals goes on a Mainline card, and the
node index travelled as the fake coordinate `{ row: -1, col: node }` — but row −1 is an ordinary
district row, the first one below the Running Track, where most districts start. So ABS Signals
highlighted whichever district card sat at that column, and an ordinary Enhancement laid one row down
was described as being "out on the Mainline, node −2". `card.play` now carries `node` as its own
field and a Mainline placement has no coordinate at all; the board simply does not light up for it.
This was the other half of "why is my Depot highlighted?" — the first half being that Enhancements
legitimately attach to played cards, which nothing on screen said.
### The train, drawn the way it stands
**`consist` is ordered nose first, and both renderers drew index 0 leftmost whatever the train was
doing.** A westbound train therefore came out right and an eastbound one came out mirrored —
reported from seed 270861860, Train 10 running east and drawn engine-first at the WEST end, which
reads as an engine shoving its whole train ahead of it. The board is a map, so the drawing obeys the
map: west on the left, nose toward the way the engine faces. A crew on a north-south spur keeps
nose-left and lets its ▲/▼ say the rest.
**The Division chip draws the train now.** It was a name and a figure, and the figure was Stages left
to cross — read once as the car count, and once it was labelled `· 2⧗`, read as redundant beside the
position the card already draws. It is: the region position is DERIVED from that countdown. So the
chip carries the consist instead, in the same vocabulary the district card uses, and the countdown
moved into the tooltip where there is room to say what it is. Stages are not regions — a card is two
regions of fixed distance, and how many Stages a train needs over them depends on the card speed and
the train (`crossingStages`); they coincide only for a 30 card and a normal train.
**Loaded or empty, told the same way in both places.** An occupied slot on a district card took a
generic blue fill and only a LOADED car overrode it, so at 26×15px empty read as blue-ish and loaded
as brown-ish — while the tray beside it made the same distinction unmistakable. Reported as "I left a
car in a siding and now I can't remember if it was loaded". The card now matches the tray, and every
slot and every car in a consist carries its own words as a tooltip.
### Saying what the game already knew
**Why a switching move is not offered.** The switching game was played off a list of coordinates —
`move to (0, -2)` as a button, nothing on the board, and no account at all of the squares missing
from the list. `exploreMoves` returns the walk's REJECTIONS alongside its destinations, out of the
same traversal, so a reason on screen is the rule that actually refused the square rather than a
second guess at it. Five conditions: the rails do not meet, another train is on the card, the
industry is locked by MEN AT WORK (§9.3 — no train may enter it *or cross it*), the coupling would
overfill the consist, or it is a turnout you may run through but not stop on.
The board now shows where the crew is, where it may go, and marks the rest with its reason on the
card's own tooltip. A turnout is drawn differently from an obstruction, because it is not one — and
it is kept out of the Blocked panel, where every turnout in the district would otherwise appear.
**Mandatory coupling is named on the button.** "Move to (0, -2)" becomes "move to (0, -2) — couples
loaded hopper, caboose on the way (onto the nose)". Coupling is compulsory (§A.4) and was announced
only in the history panel, which is the one place a player is not looking while switching. 70 move
buttons in five games now say what they will pick up.
**The workers are on the card.** "How do I see the number of laborers in an industry card?" — you
could not; the number that decides every Cargo phase was in a side panel. Porters were worse:
`porters` had been on the view-model all along and **no renderer had ever drawn it**, which is why a
Restaurant's +1 porter had "no indication anywhere". Both are on the card now as free-over-total, and
the Facilities panel shows porters beside laborers.
**Ships-out and receives, in words.** A Refinery and a Grocer's Warehouse were distinguished by the
stroke colour of one 13×12px box and the direction of a 10px chevron. Rendered both and diffed the
SVG to check the complaint: identical shape, identical sign, same box count in the same place. The
card says `SHIPS OUT` or `RECEIVES` now.
**And the Office told the truth about itself.** `baseOf` returned zeros for a passenger facility
despite a comment claiming it read the Office tier, so a plain Depot displayed `out 1 +1` — crediting
a Modifier that was never played. The card tooltip had the mirror-image bug, reading the printed tier
rather than the Office as it stands, so a Modifier's effect never showed there either.
### Undo, and the chrome around it
**Undo, as far back as you like.** The save is the seed plus the intents, so undo is a replay without
the last one: no undo stack, no inverse of each action, and no way to reach a position the rules could
not have produced. Solitaire only. It is a deliberate take-back rather than a rewind — the RNG
advances with the replay, so a train card re-rolls the same Stage, but you can see that roll and then
spend the turn differently. `TODO.md` carries the question of whether a Stage boundary should become
a commit point.
**Both Division end labels were clipped.** They hung off the outside of the end cells and needed
padding wider than the caption to survive, which the board did not have — and giving it to them would
have spent that width on two captions instead of on the map. Centred under their own Division Point
they cost nothing, and `PAD` drops from 92 to 22.
**The turn chart scrolls away no more.** The title bar was sticky and the row under it — Day, Stage,
phase, waiting-on — was not, so scrolling the board took away the thing most worth glancing at. They
are one sticky block now.
**The timetable has a key.** Blue is a train due, violet is the Stage you are in, dimmed is gone, and
the green is a *flash* marking the slot the die just filled — which is exactly why it needed saying.
**And the rule that shapes every district is finally written down.** A district only grows outwards:
the Limits signs are the growth point on the Running Track and move outward with the card, and no
card anywhere can be inserted between two cards already down. Enforced since the beginning, stated
nowhere. It now sits beside the district, and a legal square that already carries a card says what
playing there would do — `EXTEND THE RUNNING TRACK HERE`, `ATTACH TO THIS CARD` — instead of lighting
up blue and saying nothing.
### Measurements
The two engine fixes take things away, so both were measured rather than assumed: 200 paired games
before and after come to **revenue 1.4 either way**, cards played 24.4 → 24.2. Neither was paying for
anything the bot relied on.
Worth stating plainly, since the changelog above still carries older numbers: the developer bot now
averages **1.4 revenue against a target of 20**, with 1 win in 200. That is not a regression from this
work — it is the same at `f4c0f49` — and it is consistent with what `TODO.md` already records about
barring curves from the Running Track and making industries stub-only. The bot has not been retaught
since those rules tightened.
Tests 386 → 402.
### A second playtest pass
**The arrival message told you to do work you could not do.** Train 4 is the *Express*, which carries