user card Hand becomes the action surface, the board shows the train, and a reverse stops turning it around, other bug fixes too.

This commit is contained in:
Jesse
2026-08-05 19:18:26 -04:00
parent 1c321d4cea
commit 543584a2ca
20 changed files with 1851 additions and 320 deletions
+192
View File
@@ -10,6 +10,198 @@ The target is 20 Revenue over 5 Days.
## Unreleased
### A playtest report, worked through
**Backing up turned the train around.** `facing` is which way the ENGINE points, and it was reset to
the direction of travel on every move — so one reverse move silently spun the train about, everything
read "forward" again, and a run-around became pointless: you could change ends for free by backing up
twice. Running forward the engine leads and points the way it went; backing up it trails, still
pointing the way it came, which is the port it arrived through. Both hold around a curve.
**"drop 1 car(s)" hid an option entirely.** It never said which car, and it read identically for a
nose drop and a tail drop — so the action list's duplicate-label filter discarded one outright, and
setting out from the front of the train could not be chosen at all. The same failure as the turnout
rotation earlier, in a feature added two commits ago to fix the make-up deadlock. Now: *"set out the
caboose off the back"*.
**ABS Signals offered the Office Area.** The card says "any Mainline card", and
`checkEnhancementPlacement` reads `placement.col` as a Division NODE index for it — while the
candidate list handed it occupied grid cells. So "(0, 1)" was accepted because node 1 happened to be
a Mainline card: the label and the meaning were different things. It now offers the Mainline cards by
name, *"on the Uncontrolled Siding, out on the Mainline"*.
**Two T10 chips on one Office.** A train standing at the Office is on the Office grid card AND on an
A/D track, so it arrived in both lists and was drawn twice.
**The train was invisible on the board**, which is what made the switching game unplayable: every
decision is about car ORDER — which car comes off next, which end a cut couples onto — and the card
showed a name badge. The train is now drawn as it sits in the tray: engine in its place with an arrow
for which way it points, cars in order, loaded solid and empty hollow. That arrow is also what makes
"(reverse)" mean something.
**Modifier effects were applied and invisible.** The Ice House and Local Small Groceries both worked
— `capacity.outbound` and `laborers` went up — but the card draws `Math.max(1, greenCap)` green
boxes, so 0 → 1 looked identical, and laborers were never drawn at all. The Facilities panel now
shows laborers, outbound and inbound as numbers with what a Modifier added (`2 +1`), names the
Modifier cards beside the industry, and says in the tooltip what the card itself prints. Laborers had
been in a hover tooltip only, which is the wrong place for the number that decides every Cargo phase.
**Coupling was silent and left no trace.** A car simply vanished from the board with only a line of
history to say where it went. There are now two new sounds — a knuckle-coupler clank for coupling and
a quieter one for setting out — alongside the whistle, bell and conductor.
**Moves left were reported only in the history panel**, which is the one place a player is not
looking while switching. Now beside the buttons, and struck red at zero.
**A/D tracks were a tooltip.** "3 A/D tracks" with nothing on the card — the number that decides
whether the next arrival is an automatic collision. Drawn as pips, filled for taken; there is no room
on the card for more rails and the count is what matters.
**The phase changed under you.** Local Operations ends the moment the last Move is spent and the
automatic phases then run themselves, so the page could change between two clicks with no notice. A
banner now names the phase it moved to.
#### What was already right, from the same report
The car type and colour on the board (read as "cab in red" without being told), the Blocked panel
explaining a full industry track, the Facilities panel's accepted car types and spotted cars, and the
Division chip's `T10 (2)` with its consist on hover. All four were reported as useful, and none of
them changed.
### Softlock: a train being made up with no visible way to make it up
Reported at Stage 10 of seed 775569289 — Train 10 at the West Division Point, history saying "now
taking cars", and **nothing at all under Your Move**.
Moving train make-up onto the Division Yard chips took the "Making up …" group out of the action
list, and everything that was not a car went with it: the heading naming the train and what its card
calls for, and the "no more cars" button. The nine cars the train could take WERE clickable on the
yard chips the whole time — nothing on screen said so, and the panel a player looks at was empty.
The panel is back, and now says where to click: *"Click a car in the Division Yard below to add it —
9 kinds it may take are highlighted there."* It carries the pass button when passing is legal, which
§7 allows only when the Division Yard is bare — "must make every effort to find a suitable car".
**The engine was never at fault.** 2065 New Train decisions across 60 campaign games, and not one
offered zero options; the pass/place pair covers the phase. The panel still words the third case
honestly rather than implying a button that is not there.
**Two guards, because the obvious one would not have caught it.** A menu-level invariant — every
option the engine offers must be reachable through something the menu exposes — passes on this bug,
because `makeUp.pass` was in the menu and correct all along. What failed was the page never reading
it. So there is also a coarse check that `main.ts` references every field the menu offers: a field
nothing reads is either dead or a control that has gone missing. Verified by putting the regression
back and watching it fail.
### Four reports from playing seed 775569289
**A curve was described as a turnout.** Both read "east-west track with a 45° leg", which is a
turnout — a road straight across the card plus a leg off it. A curve has ONE road: in from the east
or west edge, along the centre line to the frog, out at 45° through the middle of a north or south
edge, and **nothing runs past it**. Which is precisely why it may not be laid in the Running Track,
so describing it as though it had a through track contradicted the rule that stops you.
**A curve in hand showed no preview.** The shapes were read off the card's legal PLACEMENTS, so a
card with nowhere legal to go had nothing to draw — and that is exactly when a player most wants to
see what the piece is. They now come from the card itself, which is where they belong: what a piece
looks like does not depend on whether there is currently a square for it.
**"Realignment on Mainline card 3"** named a raw node index. It said nothing about which stretch of
the Division it meant or what it would do, and it had no tooltip either — because the action list
attaches one only when a label happens to contain an em-dash, which this one did not. It now reads
> Realignment on the Uncontrolled Siding — the second Mainline card west to east; converts it to
> Double Track
and any action naming a card falls back to that card's own description when its label carries no
explanation of its own. That was the actual complaint: the same card explained itself perfectly in
hand and said nothing in the action list.
**It offered only one Mainline card, and that was correct.** `REALIGNMENTS` converts Plains, Curves,
Uncontrolled Siding and Trestle; the Division on that seed is a Heavy Grade and an Uncontrolled
Siding, so only the second could be converted. The engine was right and the label was hiding it —
"Mainline card 3" gave no way to tell a considered restriction from a bug. Naming the card fixes the
report without changing the rule.
### The hand is the action surface, and the yard makes up the train
The action list reached **22 buttons**, and most of it was a cross-product. A card appeared in two
panels under two different models: as a *subject* under "Play a card from my hand", which then
highlighted squares on the board, and as one flat button per Department under "Discard a card from
my hand". Four cards times three Departments was **twelve buttons repeating the same three choices
four times**, about 290px of the list. Separately, making up a train offered up to ten buttons
reading "add loaded hopper", "add empty boxcar" — while the Division Yard sat on screen already
showing exactly those cars by type and load state.
Both are now on the objects already being looked at, using the pattern board placement always had:
**pick the thing, then pick where it goes.**
- **Every card in hand carries its own verbs.** `play` highlights the squares it may go on, exactly
as before; a card needing no square (an Office upgrade, a train, a maneuver) goes down in one
click. `discard` lights up the three Department piles as targets — they already show their top
card and their depth, which is precisely what you choose between.
- **A make-up car is picked off the Division Yard chip** that shows it. The loaded and empty counts
are separate targets, because a car of a type and a load state is exactly what the choice is.
- The action list keeps what is not about a card or a car: the Local Operations choice, drawing,
switching moves, the Freight Agent, and finishing.
**Measured over a full game of seed 430: the widest action list went from 22 buttons to 5.** A test
now walks the same game and fails if it climbs back above 8.
The rotation step stays where it was and is now the only thing the placement panel shows — the card
is picked in the hand and the square on the board, so a rotation is the one question neither of those
can ask. Its hover previews, added earlier, are unchanged.
### A note on the two replay viewers
`TODO.md` now carries an item to decide between them. The standalone `node src/sim/replay.ts` writes
a self-contained HTML file that nothing links to and that `.gitignore` excludes; the site reads JSON
saves from `public/replays/`. The standalone one carries the bot's decision trace and a timetable
panel, which is debugging material rather than something a player wants. No action taken.
### A replay now looks like the game it is a replay of
**"Extra slow" was there and did nothing.** The site's replay viewer builds its interval with
whatever the speed select held when play started, and nothing re-read it — so changing pace
mid-replay had no effect at all and the pace looked stuck. The standalone replay had always
restarted its timer on change; this viewer was missed. Both offer the same five paces, extra slow
through very fast, and a test now asserts they stay in step.
**The turn chart lived on one screen out of three.** Where you are in the Day — the five phases with
the violet "you are here" — was in `main.ts` alone, so both replays reported the Day and the phase as
two plain strings. The same position looked like a different game depending on which screen you were
on. It is now `sim/turnchart.ts`, shared exactly as the board renderers are: the playable page and
the site viewer import it, and the standalone replay embeds it by `Function.toString()` because it
is a single file with an inline script and cannot import anything.
**And the side panels were three against eight.** The viewer showed the Division, the Office Area and
the log; the play page shows those plus cards in hand, the Department decks, the yards, the blockers
and the facilities. A replay could not answer *"why is nothing moving?"* — which is most of what a
replay is for. `web/panels.ts` now renders all of them for both pages, and the duplicated CSS is gone
from `play.html`.
Three tests hold it there: both screens must call the same panel renderers, all three must use the
shared turn chart and none may keep a private copy of the phase table, and the two viewers must offer
the same paces.
### You could not rotate a turnout at all, and now you can see what you are laying
**The rotation was being thrown away before it reached the menu.** `actionGroups` drops duplicate
labels, and `describeIntent` for a card play said only `play right-hand turnout at (0, 1)` — no
rotation in it. So a turnout's two orientations produced the same label and the second was silently
discarded. The "choose a rotation" step existed and worked; it was never given more than one
rotation to choose between. The label now names the orientation, and both survive.
**And the buttons are pictures now.** Hovering a rotation draws the piece as it will land on the
board, and hovering the card itself draws every shape it could be laid as — which is how a player
sees a turnout has two orientations before picking a square at all. Rendered by `officeSvg`, the
board's own renderer, on a one-card board: the preview and the board cannot disagree about what the
piece looks like, and the rails come from the engine's `connectionsFor`, so a preview cannot promise
a shape the placement will not produce.
`Placeable.spots` carries the links for this. `data-tip-html` on the tooltip renders a figure above
the caption; it is only ever set from markup this app builds.
### Turnouts say what they do, and a curve may not break the Running Track
**"Right-hand turnout, stem east, through west, diverges north at 45°"** is three pieces of jargon