fix tracks to all be EW. no more NS tracks

This commit is contained in:
Jesse
2026-08-05 01:00:11 -04:00
parent 821d3cdffa
commit 2d348e1ecf
16 changed files with 1157 additions and 355 deletions
+81
View File
@@ -10,6 +10,87 @@ The target is 20 Revenue over 5 Days.
## Unreleased
### The track is 45° geometry, drawn from the printed cards
The prototype designer's feedback: **there are no north–south tracks.** Straight runs are always
east–west, the east–west line sits dead centre on the card rather than biased to the top, and a
turnout is an east–west through track plus a curved leg meeting the north or south edge at 45°, which
must line up with the corresponding leg on the card it abuts.
Measured off `docs/tracks.png` rather than inferred. Card grid lines at y = 15/164/314/464/614/764/
914/1064/1213 on a 2136×1397 sheet; rail centres at 89.5/236/–/–/689.5/839/989/1135.5/1288.5 — the
card's exact vertical middle, within a pixel, on every one of the nine rows. Cards are 259×150
(aspect ≈ 1.73). Every leg crosses the middle of its edge. Four shapes exist: turnout with the leg
off the west end, turnout with the leg off the east end, curve west↔south, curve east↔south, plus the
plain straight. Depot, Station, Terminal, Refinery, Freighthouse, Coal Tipple, Manufacturing and Town
are all plain east–west straights with **no diverging leg at all**.
**Slope is now part of adjacency.** Name the two diagonals after the pair of arcs that mate across a
horizontal card edge: `ne_sw` (port pairs {n,e} and {s,w}) and `nw_se` ({n,w} and {s,e}). An `sw` card
above an `ne` card is one unbroken rail; `sw` above `nw` is a V — both cards have the port, both legs
meet the same point on the edge, and they still do not connect. `joins(a, p, b)` in
`src/engine/track.ts` is the single adjacency test, and every `hasPort(nb, opposite(p))` in the
engine, the bot and the tests now goes through it. Leaving one behind reintroduces the V.
**Handedness is that diagonal.** A printed card has a back: it turns 180° but never flips. So a
straight has ONE orientation, and a curve or turnout has two — 0° sends the 45° leg south, 180° sends
it north, and neither changes diagonal. Left-hand reaches `sw`/`ne`, right-hand `se`/`nw`. That is
what makes the 4-left/4-right split of the curve and turnout supply mean something: a run-around needs
one card of each hand — a left turnout down, its matching left curve, straights along, a right curve
back up into a right turnout. `variantsFor` takes a `hand` argument for this reason.
**What went, and what opened.** `TrackAxis` and every `axis` field are deleted outright — there is
one axis now, and a one-valued field invites the old branching back; deleting it turned `tsc` into
the migration checklist. The Office's `e-s`/`w-s` stubs are gone, as are north–south facility
placements. Q7's "a district hangs below the Running Track" is lifted: a turnout turned 180° reaches
north, so `placementCandidates` and `adjacentFacilityCoord` no longer reject the rows above, and §9's
"nine nearby spots" really is nine. `legalIntents` offered six variants per placement per card; the
widest set is now two.
**The renderer draws the card rather than approximating it.** `RAIL` moves from 30 to `H/2`, and the
card widens from 132×96 to 166×96 to match the sheet's 1.73 — the aspect is not decoration, since the
frog where a 45° leg meets the through track sits `H/2` from the centre and a squarer card pushes it
almost to the edge. The fixed elbow at `(W/2, RAIL + (H-RAIL)/2)` is replaced by the real frog at
`(W/2 ± H/2, H/2)` and a segment at exactly 45° to the edge midpoint. That elbow sat *below* the rail
on the assumption everything diverged downward, so a leg reaching north was drawn as a hook that
dropped past the rail and came back up. Four tests in `test/web.test.ts` now measure the emitted SVG:
the through rail level and centred and spanning the full card, every leg at 45°, `ne`/`sw` on one
diagonal and `nw`/`se` on the other, and the leg crossing the edge at its midpoint. The enhancement
label moved above the rail, where it used to print along it.
**Measured, 100 solitaire Standard games, against the same run before the change:**
| | before | after |
| --- | ---: | ---: |
| mismatched 45° joints on finished boards | — | **0** |
| district size | 25.4 cards | 28.5 |
| mean max depth off the main | 2.38 rows | 1.45 |
| built above the Running Track | 0/100 games | 93/100 |
| straights laid | 1.01/game | 3.44 |
| districts with a run-around | 99/100 | 70/100 |
| facilities on a run-around | 0.82/game | 0.51 |
| revenue | 7.34 mean | 5.80 |
The shallower districts are intrinsic: no card joins north to south, so descending a row costs at
least two cards — the leg out, then a curve turning the run back east–west. Districts are now wide
and shallow, which is what the printed sheet and a real yard both look like, and the run along the
siding is straights, which is why the bot lays three times as many (13 of the 18 enhancement cards
need one).
**The revenue and run-around drops are honest, not a regression to chase.** A run-around costs more
pieces than it did, and the Office no longer offers a free way back up onto the main — it used to
carry `e-s`/`w-s` stubs, so every district had one guaranteed climbing point. Two bot heuristics were
tried against it and both rejected on measurement: preferring one side of the main changed nothing at
−4 and made things worse at −100 (62/100 run-arounds), and paying more for the second turnout that
closes a siding bought 86/100 run-arounds and 1.08 facilities on a loop but cost 9% of revenue
(5.80 → 5.25). Revenue is the game's own measure, and both structural floors in `test/sim.test.ts`
still pass, so neither shipped.
The bot is otherwise rewritten around the new geometry: `descendFrom`/`waysOff`/`reachableOffMain`/
`runAroundCells` take a side rather than assuming "below", the arc score asks `joins` instead of
reading the arc name, and `arcsLeft` is asked per hand — a left-hand turnout's leg can only be
continued by a left-hand curve, and a supply full of right-hand ones is no help to it.
### Q13 — a train that catches the one ahead runs into it
Answered, and implemented as option B: **collide on catching up**, which is the version that rewards