# Track order for standing cars, and the cut you left on your own card > **BUILT IN v0.4.7.** Kept as the record of why the model is what it is — the reproductions below > are the evidence for west-to-east, and none of them is re-derivable from the code that fixed them. > > Two departures from the plan as written, both recorded here so the document does not read as a > description of the code: > > - **Step 3 does not flip the card's cut.** The plan asked for `board-svg` to apply the tray strip's > `facing === 'e' ? reverse : as-is` to the standing cars too. Once `standing` is defined west to > east that flip would be wrong — the board is a map and draws west on the left, so the array is > already in drawing order. What the renderer got instead is the half that actually answers the > report: the row is split at the train, west cars left and east cars right, with the engine in the > gap, and each car's tooltip says whether it is ahead of or behind the engine. > - **One new field after all.** The plan argued a cut's end is *derivable* from position in the > array. It is not, in the one case that matters: a train may set out off both ends on the same > square, and nothing in the array records where the train itself stands among them. That is > `CrewTray.standingWest`, kept on the tray rather than the card because it describes a > relationship — with no train there, a cut has no near or far side. > > **Step 5 was decided (i)**: taking your own cut back on the square you are standing on is undoing > the drop, exempt from the printed pick-up restrictions, and it refunds the freight budget. Plan for the next session. Two reported problems that turned out to be one problem. --- ## The reports 1. **Drops.** "If I put cars off the nose on a given track, and my next move is go forward, I need to couple those cars right back on. If I drop cars off the back and I move back, then I will automatically recouple the cars onto the back of my train. Right after dropping my cars I need to be able to see if those cars are ahead or behind the train." 2. **Ordering.** "When dropping all 4 cars, order was reversed. It worked properly if we dropped cars individually. Also, when adding four cars, again the order was reversed." --- ## The single root cause `TrackCard.standing` is a bare `RollingStock[]` whose doc comment claims "in track order (§A.3)" and which in fact has **no defined orientation at all**. `CrewTray.consist` *is* oriented — nose first, relative to `facing` — so every transfer between the two is an orientation change that nothing performs. §A.3 states the requirement plainly: > It is critical that cars are loaded into and unloaded from the Crew Tray, and occupy the track, in > the same order they originally held, **left-to-right**. Left-to-right is west-to-east. That is the orientation `standing` is missing. ### Reproduced, against the working tree Scratch scripts only; reproduce with a tray on a plain straight, consist `[boxcar hopper reefer tank]`, `engineAt = 0`. **(a) Batch size changes the parked order.** Tail drops: | drops | `card.standing` afterwards | | --- | --- | | `count: 4` | `[boxcar hopper reefer tank]` | | `count: 1` ×4 | `[tank reefer hopper boxcar]` | | `count: 2` ×2 | `[reefer tank boxcar hopper]` — physically impossible | `carsDropped` always does `carsOn(card).push(...stock)` (`apply.ts:1657`). Each successive tail cut is left *outboard* of the previous one on the track, so it belongs at the other end of the array. Nose drops are batch-invariant by luck, so only tail drops show it. **(b) Coupling is orientation-blind.** A train running **east** onto a parked cut and a train running **west** onto the same cut yield the **identical** consist. They must mirror. `couples` is accumulated in path order in `exploreMoves` (`track.ts:448`) and `unshift`ed wholesale in `carsCoupled` (`apply.ts:1618`), with no reference to which way the engine met the cars. **(c) The display mismatch — what was actually seen on screen.** `board-svg.ts:736` draws the tray strip as `t.facing === 'e' ? [...items].reverse() : items`, so the nose lands on the right for an east-facing train (a deliberate fix, documented in the comment above it). The card's standing cars at `board-svg.ts:~633` are drawn in raw array order with no such flip. The same physical cut therefore reads one way in the tray and the other way on the card — and which one looks "reversed" flips with batch size *and* with train facing. The round trip (drop, run away, back up, collect) is internally self-consistent today, which is why this survived: whatever order got parked comes back in that order. Only the picture disagreed. ### Why this settles the earlier design question The previous session offered two models for recording which end a cut sits at: - **Option A** — one cut per card plus a `standingEnd?: Port` marker. Cheap. - **Option B** — give `standing` a real orientation. Option A leaves (a), (b) and (c) in place. **Do Option B.** With orientation defined, "which end is the cut at" is *derivable* from position in the array and needs no new field — so B is also the smaller change once the ordering bugs are counted as in scope, which they now are. --- ## The model **`TrackCard.standing` (and `Facility.industryTrack.cars`, via `carsOn`) is ordered WEST to EAST.** That is the printed rule, it is the convention the board already draws in, and it is the only orientation that is a property of the board rather than of whichever train last touched the card. Two derived quantities, both cheap: - **A cut's end.** With a train on the card, cars before the train in track order are at its west end, cars after it are at its east end. Combined with `railFacing`, that is "ahead" or "behind". - **Approach order.** A train travelling east meets a card's cars in array order; travelling west, in reverse array order. The nose/tail blocking rule then reads: **a move that leaves the card through the end the cut sits at must couple that cut first** — onto the nose when running forward, onto the tail when reversing, which is what the existing `toNose: !i.reverse` already gives. ### The two conversions, stated once - **Consist → track.** `consist` is nose-first; nose is `facing`. So an east-facing train's consist reads east-to-west and must be **reversed** on the way onto the card; a west-facing train's reads west-to-east and goes on **as-is**. - **Track → consist.** Build `couples` **near-to-far along the direction of travel** (reverse each card's array when travelling west, and visit cards in path order). Then a nose coupling is `unshift(...[...couples].reverse())` and a tail coupling is `push(...couples)` unchanged. Check the tail case by hand: backing east into a cut, the nearest car couples closest to the existing tail, and `consist` runs nose-to-tail, so near-before-far pushed in order is right. Check the nose case: running east into a cut, the *farthest* car ends up nose-most, so the reverse is right. --- ## Work, in order Each step should land green on its own. ### 1. Define the orientation and fix the drop - `state.ts` — rewrite the `standing` doc comment to state west-to-east outright, and say `carsOn` inherits it. That comment is currently the thing that is wrong. - `apply.ts` `carsDropped` reducer (~1649) — insert at the correct end for the dropping train's `facing`, reversing the cut where the conversion above says to. A tail cut from an east-facing train goes at the **west** end of whatever is already on the card; a nose cut from the same train goes at the **east** end. - Tests: batch-invariance. `count: 4`, `1×4` and `2×2` must all produce the same `standing`, from both ends, at both facings. Direct regression for report (2). ### 2. Fix the coupling - `track.ts` `exploreMoves` (~448) — build `couples` near-to-far along travel, per the conversion above. Needs the exit port at each hop, which the walk already has in `MoveStep`. - `apply.ts` `carsCoupled` (~1612) — reverse on `toNose`. - Tests: the mirror property. Approaching the same parked cut eastbound and westbound must give mirrored consists, never identical ones. - Tests: round trip at both facings and all batch sizes — drop, run clear, back up, collect; the consist must come back identical. ### 3. Fix the picture - `view.ts` `CellView` — `cars: string[]` is flat and carries no relation to `train`. It needs enough for the renderer to place the cut relative to the engine. Preferred: keep `cars` in track order (west-to-east, matching state) and let the renderer flip, exactly as it already does for the tray strip. - `board-svg.ts` (~633) — apply the same `facing === 'e' ? reverse : as-is` flip to the cut that line 736 applies to the tray, and lay the cut on the correct **side** of the train chip rather than in the fixed slot row. This is the real UI work and answers "I need to see if those cars are ahead or behind". - Tooltips: "standing here" becomes "standing ahead of the engine" / "behind the engine" when a train shares the card. ### 4. Layer on the blocking rule - `track.ts` `exploreMoves` — seed the walk's `couples` with the start card's cut when the cut sits at the exit end. Today the walk begins at `neighbour(start, initialExit)` and never reads `carsOn(startCard)` at all, so a train drives through its own cut in both directions. Regularise the loop-back case at the same time: `couples` already picks up the start card if the walk circles back to it, because only `results.push`/`block` are guarded by `!sameCoord(node.coord, start)`. - `apply.ts` `switch.move` build (~1230) — `lifted` is `[...dest.path.map(s => s.coord), i.to]` and can never contain `start`. It must, or the recoupled cut is not cleared off the card. - `ctx.consistSize` — the start-card cut must count toward `MAX_CONSIST`, so `tooManyCars` bites at the start square. Its message should name the cut on your own card or it will read as nonsense. - Tests: encode §A.5's two worked examples move-for-move. Both must remain playable; the trailing example's step 1 ("drop the back two on Card F, move train to Card B") is the specific case that must *not* recouple, and it is the strongest single check that the rule is right. ### 5. Decide the printed-rule interaction — Jesse's call Dropping a freight car spends the square's budget for trains 3/4 (`oneFreightPerLocation`). Recoupling it on the way out would then be refused with `FREIGHT_WORKED_HERE`, so the train could only ever back away from its own cut. Same shape for X13 `dropOnly` and X22 `pickUpEmptiesOnly`. Two defensible answers: - **(i)** Recoupling your own cut on the square you are standing on is *undoing the drop* — exempt from those restrictions, and it refunds the freight budget. - **(ii)** It is a pickup like any other; a train that sets out off its nose is committed to reversing. Harsher, but never strands a train permanently, since backing up stays legal. Leaning (i): (ii) makes a legal-looking drop silently one-way, which is exactly the kind of trap the "why can't I move" panel exists to prevent. ### 6. Bot and fixtures - `bot.ts` — the "drop the nose cut to un-bury the engine" heuristic (~1268) and "spot the back car at this facility" (~1352) now have direction consequences. The bot cannot produce an illegal move (legality is enforced upstream) but it will silently get worse until its scoring knows about the cut. - `public/replays/seed-*.json` will diverge — legality changes, so bot play changes. Regenerate, as was done last release. - **`docs/station-master-seed58228926-day6.json` no longer replays at all.** It stops at intent #2, `mainline.modify c108` → `NO_SUCH_CARD`, because uncommitted deck changes shifted card ids. It cannot serve as the regression case for these reports. Capture a fresh save against current code reproducing the same switching sequence, and keep it. --- ## Files expected to change | File | Why | | --- | --- | | `src/engine/state.ts` | `standing` orientation, doc comment | | `src/engine/track.ts` | direction-aware `couples`; start-card cut; consist limit | | `src/engine/apply.ts` | `carsDropped`, `carsCoupled`, `switch.move` `lifted`, budget decision | | `src/sim/view.ts` | `CellView.cars` orientation and its relation to `train` | | `src/sim/board-svg.ts` | flip the cut, place it beside the chip, tooltips | | `src/sim/narrate.ts` | say which end was set out; distinguish recoupling own cut (~138) | | `src/sim/bot.ts` | direction-aware set-out heuristics | | `test/apply.test.ts`, `test/train-rules.test.ts`, `test/web.test.ts`, `test/replay.test.ts` | drop/couple batteries, §A.5 examples | | `public/replays/seed-*.json` | regenerate | ## Not in scope - Leaving a coach at the Office (§A.4 / trains 7/8) — separate open item in `TODO.md`. - Any change to `maneuver.flyingSwitch`'s target rules. It drops onto a card with no train on it, so the cut has no "end" to sit at; it just needs to land in west-to-east order like any other cut.