switching paths: two routes to the same square
Builds docs/plans/switching-paths.md. A passing loop can offer two legal routes between the same two squares, coupling different cars — the engine only ever found one, an artifact of search order (Reported by Jesse, undo 379). exploreMoves now enumerates every simple route (per-path visited set, capped at 4000 frontier nodes) and dedupes on outcome — destination, entry side, and origin-tagged cars — rather than on reaching the square at all. switch.move gains an optional `via: GridCoord` naming one intermediate square on the chosen route; absent, it resolves exactly as before, so every existing save and bot decision replays identically (575/575, then 579/579 with the new tests). Threaded through the label, the action-list dedupe, the hover highlight (data-route), and the history (trayMoved.via). Ruling recorded as Gap 14 in docs/rules/open-questions.md: the player may choose the path; §A.4's "may not go around" a car does not reach a different track the player declined to enter. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SAt2YCXgd5qCjBcF2x34aK
This commit is contained in:
+52
-2
@@ -21,8 +21,9 @@ 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.
|
||||
Three reports from the same game, all about squares: which ones a card may go on, which one a button
|
||||
in the action list means, and — when there is more than one legal road to the same square — which
|
||||
one a train actually takes.
|
||||
|
||||
### The Limits bound the district, not just the Running Track
|
||||
|
||||
@@ -118,6 +119,55 @@ back out of the label: a remote client holds no `GameState` and cannot look up w
|
||||
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.
|
||||
|
||||
### Two routes to the same square
|
||||
|
||||
**Reported:**
|
||||
|
||||
> "There are times when a train can take two different paths to get to a destination. See game at
|
||||
> undo 379. Train 11 can go from Eastern limits to the straight at 1,1 two different ways. It can go
|
||||
> through the refinery and pick up the tanker on its nose, or it could go straight and then make the
|
||||
> curve and skip the refinery and not pick up the car. The player should have the two different
|
||||
> options."
|
||||
|
||||
A district with a passing loop — turnout off the main, curve down to an industry, curve back up —
|
||||
offers two legal routes between the same two squares, and `exploreMoves` (`track.ts`) only ever found
|
||||
one: it keyed its visited set on destination and entry port, both routes rejoin through the same
|
||||
port, and the shorter one won the race before the longer one could be recorded. Which route survived
|
||||
was an artifact of search order, not a choice.
|
||||
|
||||
**Ruling (Jesse, docs/rules/open-questions.md Gap 14):** the player may choose the path. §A.4 says
|
||||
"cars **on your track**" — the industry spur is a different track, and declining to enter it is not
|
||||
going around the car standing there. The mandatory half still bites in full once a route is chosen:
|
||||
every car standing on it couples.
|
||||
|
||||
**The walk now enumerates every simple route** — a per-path visited set replaces the old global one,
|
||||
so a card may be revisited across different routes but never twice within one — capped at 4,000
|
||||
frontier nodes so a dense district cannot blow the walk up combinatorially; BFS order means the cap
|
||||
loses only the longest routes first. Routes are deduped on **outcome**, not on reaching the square:
|
||||
`(destination, entry side, [(origin card, car)…])`, so two routes coupling identical car *types* off
|
||||
*different* cards are both offered and each sweeps its own card, while two routes with nothing to
|
||||
tell apart collapse to one.
|
||||
|
||||
**`switch.move` gained an optional `via: GridCoord`** — one intermediate square on the chosen route,
|
||||
never the start or the destination. `legal.ts` emits one intent per distinct route with a `via` that
|
||||
distinguishes it from its siblings; `via` absent resolves exactly as before (the first route
|
||||
enumerated), so every existing save and every bot decision replays identically. Checked against
|
||||
`public/replays/*.json` and the full suite: 575 pass, 0 fail, unchanged.
|
||||
|
||||
Threaded through the label (`describeIntent` finds the route `via` names, so "couples 1 tanker"
|
||||
reports the cars *that* route actually lifts), the action-list dedupe (two routes to one square would
|
||||
otherwise print the identical button twice), the hover highlight (`data-route` lights every square a
|
||||
route runs over, not just its destination), and the history (`trayMoved.via`, when the move was
|
||||
ambiguous, names which road the crew took).
|
||||
|
||||
**Balance impact: none expected, and none measured able to be attributed to this.** Instrumented over
|
||||
60 developer-bot games (17,884 destinations enumerated): squares reachable by 2+ distinct paths are
|
||||
~1.2% (208), and in every one of those 208 the routes coupled identically — the bot has never yet
|
||||
built a district where the discarded route would have mattered. 400 full games (200 developer bot,
|
||||
200 random bot) completed 200/200 with no exceptions on the reworked walk. `compare.ts` needs an
|
||||
ablation flag to run at all and this is not a bot heuristic, so paired A/B was not applicable here;
|
||||
the enumeration-based argument above is the actual evidence.
|
||||
|
||||
### Also
|
||||
|
||||
- The three published replays were re-recorded. A save is a seed and its intents, so tightening a
|
||||
|
||||
Reference in New Issue
Block a user