v0.4.8 — the Limits bound the whole district, the nine spots really are nine, and an action points at its square

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YQAJ4dND7enLj54eLyiF2C
This commit is contained in:
Jesse
2026-08-19 13:38:06 -04:00
co-authored by Claude Opus 5
parent 9f3b92d08e
commit 461c4d9bac
25 changed files with 3264 additions and 4524 deletions
+108
View File
@@ -19,6 +19,114 @@ page as `v0.1.0 · <sha> · <date>`, so what is deployed can always be identifie
---
## 0.4.8 — 2026-08-19
Two reports from the same game, both about squares: which ones a card may go on, and which one a
button in the action list means.
### The Limits bound the district, not just the Running Track
**Reported:**
> "Sidings should not be allowed to be built outside the limits."
The Limits sign "denotes the limit of your control area" (§2.1) and §3 defines Secondary Track as
"all tracks **in your limits** that are not the Running Track" — but the bound was only ever enforced
on the Running Track row itself, with a comment in `canPlaceAt` stating outright that "the district
below the Running Track is unbounded". So a siding could run east past a player's own sign, take
industries with it, and put cars on track that §8.1 and §10 do not consider his territory at all: the
rules reason about "the track between the train and the Limits", and running past another player's
Limits is what makes a collision his fault.
`withinLimits` is now the district's rule at **every row**, and a Facility is bounded by it too —
§11.2 is explicit that "Facility cards carry their own rails: placing a Facility places track". The
refusal is its own code, `OUTSIDE_LIMITS`, rather than `NOT_CONNECTED`, for the same reason
`BREAKS_RUNNING_TRACK` is: the card would join perfectly well, and is refused for a different reason.
**Inclusive of the sign's own column**, which is the half that keeps the game playable. Jesse's call.
The sign stands *on* the boundary rather than beyond it, and a district opens with signs at ±1 around
the Office — so the strict reading would leave exactly one buildable column and break §11.3's promise
that both Secondary rows, and the nine-spot Modifier neighbourhood, are usable from the first Stage.
A siding may run under the sign; nothing may go past it.
**What it costs the bot: nothing measurable.** Out-of-limits building was rare for it to begin with —
5 cards across 100 games, in 4 of them, never more than two columns over — and paired over 200 seeds
the bound moved 4 games and −0.015 Revenue (t = −0.26). It is a human building deliberately who hits
this, which is exactly how it was found.
### The nine spots really are nine — the four diagonals were legal and unofferable
**Reported:**
> "Modifiers should be able to go on any diagonal — we were unable to place it to the south-east."
§9 places a Modifier "adjacent to a Facility, on any of the nine nearby spots", and `check` has
always accepted all eight neighbours. The fault was upstream in `placementCandidates`, which walks
north, south, east and west from every occupied square: a diagonal square with no *orthogonal*
occupied neighbour was never generated, so it was never offered. Measured on a facility below the
Office: `check` says legal at all four diagonals, the menu offered the three orthogonal ones. At a
stub industry — the case in the report — every diagonal is in that position.
Generated in a **second pass, only around a Facility**. Track has to *join* and a diagonal shares no
edge, so a Modifier is the only card those squares can ever take; generating diagonals around every
card instead costs 54% more simulation time producing candidates `check` then rejects. Appending
rather than interleaving leaves the existing candidate order untouched, which matters because the bot
breaks ties by first-best — measured over 200 seeded games, **0 played differently**, so every figure
in `TODO.md` still stands.
### A Modifier may hang outside the Limits — but not in the Running Track's row
Jesse's call, both halves. A Modifier is not track (§9), so unlike a siding it keeps all nine of its
spots when its host stands at the limit: refusing the outer three would make the card unplayable
exactly where the district ends. What it may not do is stand in the row the Running Track grows
along. Inside the Limits that row is always full, so the rule bites only beyond the sign — and that
is the ground the main extends onto, where a parked Modifier would block the player's own sign from
moving outward (§2.1) with nothing on screen to warn him. Industries have been barred from the
Running Track since the "Placed" column was read properly; this is the same rule for the same reason.
### The board draws where the district ends
Two signs on one row cannot say that nothing may be built outside their columns at *any* row: a
player looking at open ground beyond a sign had no way to know it was unbuildable until the square
failed to light up. `officeSvg` now draws a quiet dashed edge down the whole canvas at each limit,
labelled, and just **outside** the sign's own column so the picture agrees with the rule — a siding
may run under the sign. It moves outward on its own as the Running Track is extended, which is the
reward for extending made visible. The Frame carries `limits` for it, resolved server-side like
everything else a remote client cannot look up (`multiplayer.md` §5).
The margin on the viewBox is not cosmetic: the west sign is usually the leftmost card on the canvas,
so the line outside its column lands at x = −1.5 and simply is not there on a canvas starting at 0.
Caught by drawing one, not by reading the code.
### Hovering an action points at the square it means
**Reported:**
> "When I have my mouse over the action for a particular square, could the corresponding square in
> the office area be highlighted to minimize my mistakes of selecting the wrong square — 1,3 versus
> −1,3? In a solitaire game with Undo it is not fatal. In a multiplayer game that could be a major
> disaster."
The action list is a column of near-identical sentences separated by a coordinate, and the board
beside it said nothing about which was which. Every action that names a square now carries it as
`data-square`, and hovering — or focusing, so the keyboard route is not a lesser one — lights that
square on the board, ghost targets included, which is precisely the moment a player is choosing
between coordinates.
The coordinate rides on the **menu**, resolved from the intent by `coordOf`, rather than being parsed
back out of the label: a remote client holds no `GameState` and cannot look up where a tray is
standing, which is the same reason the menu already carries card descriptions. Checked over 12 bot
games: 870 direct actions name a square, and all 870 carry it.
### Also
- The three published replays were re-recorded. A save is a seed and its intents, so tightening a
placement rule kills every replay containing one — `harness.test.ts` catches it rather than letting
them go quietly dead, which has happened twice before. Old saves are not preserved across rule
changes and are not meant to be yet.
- `node_modules` was a **tracked symlink pointing at a path that does not exist**, so `tsc` was
missing and 20 tests failed before any of this was written. Replaced with the real dependencies.
## 0.4.7 — 2026-08-19
Eight play reports and one design that had been written up and not built. The through-line is the