Track becomes a deck card, Departments become decks, and trains must be made up to leave, industry restrictions enforced, update display and add a new game button
This commit is contained in:
+286
@@ -10,6 +10,292 @@ The target is 20 Revenue over 5 Days.
|
||||
|
||||
## Unreleased
|
||||
|
||||
### The Crew Tray is a train, and a train must be made up to leave
|
||||
|
||||
**§8.2 was not checked at all.** A train could highball onto the Mainline engine-last with its
|
||||
caboose in the middle. Now a train held at the Office is held until it is made up: the engine at an
|
||||
end of the tray — pulling or pushing, both are real — and the caboose at the far end from it. The
|
||||
check is deliberately direction-free, because what a train may not be is *broken-backed*, with the
|
||||
engine buried among its own cars and some ahead of it and some behind. That state is only reachable
|
||||
through switching: a train arrives made up and comes apart because the player took a cut onto the
|
||||
nose or picked cars up in a run-around.
|
||||
|
||||
**`engineAt` was written and never maintained.** Cars taken onto the nose go AHEAD of the engine —
|
||||
Appendix A: *"a train can pick up two cars and add them to the Crew Tray in order that they were in,
|
||||
pushing them into the Facility"* — so the engine stops leading, and its recorded index did not
|
||||
follow. It does now, and drops adjust it the other way.
|
||||
|
||||
**Setting out from the nose was missing entirely.** Appendix A uses the move in its own worked
|
||||
example — *"Back up and drop off everything on the nose of your train (red and blue) on Card B"* —
|
||||
and `switch.dropCars` could only ever take from the tail. Without it, cars taken onto the nose could
|
||||
never come off, so an engine buried in its own train had **no way back to an end**: the first version
|
||||
of the make-up rule stranded a train permanently in 6 games of 40, holding an A/D track for the rest
|
||||
of the game. `fromNose` fixes it, and a cut is now guarded to come off an OUTER end only — lifting
|
||||
cars from beside the engine would leave the far end of the train coupled to nothing.
|
||||
|
||||
The bot learned both remedies: dig the engine out when it is buried, and shed a misplaced caboose
|
||||
when it is at a reachable end. Measured over 40 games: **106 make-up holds across 6 games → 13 across
|
||||
1**, revenue 1.55 → 2.05. The one that remains is a caboose stuck mid-train, which genuinely needs a
|
||||
run-around or a Small Yard — the game working, not a defect.
|
||||
|
||||
**A knock-on worth recording:** with cars now set out properly, the freight pipeline runs clean — 55
|
||||
loads started and 54 completed across the 40-game sweep, with **zero jams of either kind**. The
|
||||
regression test that asserted the bot clears a MEN|AT|WORK jam had become untestable, so §6.3's
|
||||
unjam is now asserted directly against a constructed jam instead of hoping the bot stumbles into one.
|
||||
It also checks that clearing the jam reopens the track, which is the point of it.
|
||||
|
||||
**You can see which car is where.** The board printed the first three characters of each car's
|
||||
label, which for "loaded hopper" and "loaded boxcar" alike is `loa` — every car on the map looked
|
||||
identical. The switching game is entirely about getting the RIGHT car to the right industry, so type
|
||||
now reads by colour and by three letters (`box` `hop` `tnk` `rfr` `cch` `cab`), and loaded shows as a
|
||||
filled slot against an empty one's outline — the same distinction the printed game makes with
|
||||
coloured tokens.
|
||||
|
||||
### Operational Rail — a locked industry could be driven straight over
|
||||
|
||||
Checked the whole rule against Appendix A of `StationMasterPrototypeRules.pdf`, which defines it
|
||||
outright: *"Operational Rail is any track card that a train can stop and leave Rolling Stock
|
||||
(uncouple) on. Operational Rail is any track card with a train wheel icon on it."*
|
||||
|
||||
Most of it was already right, and the wheel icons in the rules diagrams confirm which cards carry
|
||||
one. Straights, curves, industries and the Limits cards all do. The turnout does not, and the page
|
||||
says so in words — *"Since there is no Operational Rail wheel icon, the train may not stop on this
|
||||
card"* — so a train runs through one and may not stop or uncouple on it. Both already held.
|
||||
|
||||
**The Office is Operational Rail.** The Depot card in the diagrams carries a wheel icon and is drawn
|
||||
as one of the green squares a Crew Tray may move to, and the Special Rules say *"While your Office
|
||||
Track is considered Operational Rail, Rolling Stock may not be dropped off here."* So a train may
|
||||
stop there and may not leave cars — which is what `canDropCarsAt` already did. Worth stating plainly
|
||||
because it is easy to read the "no cars here" half as "not Operational Rail", and the A/D track
|
||||
mechanic depends on trains being able to hold at the Office.
|
||||
|
||||
**The real gap was §9.3's lockout.** *"While ANY loads are in the MEN | AT | WORK track, the
|
||||
industry's track is locked down... It loses its status as Operational Rail. No cars can be picked up
|
||||
or dropped off, and no trains may occupy **or move on** it."* Losing Operational Rail status only
|
||||
stops a train FINISHING somewhere — a turnout is not Operational Rail either and trains run through
|
||||
one all day. Passage was never blocked, so a crew rolled straight over a locked industry and, because
|
||||
coupling is automatic and mandatory, picked up the cars spotted on it on the way past. Those are
|
||||
precisely the two things the safety lockout exists to prevent.
|
||||
|
||||
`isLockedByWork` is now separate from `isOperationalRail` for that reason: "cannot stop here" and
|
||||
"cannot pass through here" are different properties and only a locked industry has both.
|
||||
|
||||
Covered by a new suite in `track.test.ts` that walks each card type against the wheel-icon rule,
|
||||
asserts the Office may be stopped at but not unloaded on, asserts a turnout may be passed but not
|
||||
stopped on, and asserts a locked industry blocks both stopping and passage — then reopens when the
|
||||
work clears. It also checks the supply catalogue's `isOperationalRail` column against the rule, so
|
||||
the data and the diagrams cannot drift apart.
|
||||
|
||||
### Industry cards: on a stub, one of a kind, and never both ends of a chain
|
||||
|
||||
From the sheet, the "Placed" column reading identically for all six industries — **"Straight, Stub
|
||||
(not on Running Track)"** — and the "Lockouts" column beside it.
|
||||
|
||||
**An industry may no longer be built on the Running Track.** It was allowed and merely *warned*
|
||||
about: the card text noted that a car left standing there would be hit by the next arrival. That is
|
||||
a hazard, not a rule, and it let a player skip the district entirely and spot cars on the main line —
|
||||
which removes the whole switching puzzle, since the point of a siding is getting a car down off the
|
||||
main and back. `check` now returns `ON_RUNNING_TRACK`.
|
||||
|
||||
**No two of the same industry in one Office Area.** The sheet states this in the Freight House row,
|
||||
which lists Freight House among its own lockouts; the catalogue had dropped that self-reference as if
|
||||
it were a typo. It is a general rule, so it is enforced for every kind in `isLockedOut` rather than
|
||||
repeated in all six entries.
|
||||
|
||||
**Producer and consumer of the same commodity stay apart.** Mine Tipple makes the coal a Power Plant
|
||||
burns; the Refinery makes the oil it also burns; Packing Sheds fill the reefers a Grocer's Warehouse
|
||||
empties. Build one end of a chain or the other, never both — which is what pushes freight to run
|
||||
between districts instead of circling inside one. The pairs were already right; they had **no test
|
||||
coverage at all**, and now have a suite that checks each pair in both directions, checks that
|
||||
industries sharing no commodity may stand together, and checks the catalogue against the sheet
|
||||
column so a change to it has to be deliberate.
|
||||
|
||||
**Measured, 60 solitaire Standard games:** 0 industries on the Running Track, 0 duplicates. Industry
|
||||
placements fell from 3.84 a game to 2.23 and revenue from 2.87 to 1.35, because the bot builds
|
||||
shallow districts and there are now far fewer legal squares. Not rebalanced — deliberately. The
|
||||
counts, the industries and the track mix are all due a pass together once the rules are right.
|
||||
|
||||
One consequence worth naming: `flyingSwitch` stopped firing in the 60-game reachability sweep. The
|
||||
rule is fine — `mainline-cards.test.ts` exercises it end to end on a hand-built siding — but the bot
|
||||
no longer gets a crew next to an industry. It is exempted **by name** in that test, with the reason
|
||||
written next to it, so the other forty-odd event checks stay live and deleting the line is what
|
||||
proves the bot has been fixed.
|
||||
|
||||
### §6.2's reshuffle, and the Departments and Salvage Yard on screen
|
||||
|
||||
**The reshuffle existed as a fiction.** `events.ts` declared `{ type: 'deckReshuffled' }` and
|
||||
`narrate.ts` had a line of prose ready for it — "Home Office deck ran out — Salvage Yard reshuffled
|
||||
back in" — and nothing anywhere emitted or reduced it. `check` just returned `DECK_EMPTY`. A declared
|
||||
event with narration written for it reads as an implemented feature to anyone grepping for one, which
|
||||
is worse than an obvious gap.
|
||||
|
||||
Now real: when a draw takes the last card, the Salvage Yard and all three Department decks are
|
||||
collected, reshuffled, and §4.6-4.7's opening is re-run — three cards turned face up as the
|
||||
Departments, the rest face down as the deck. Cards played onto the board are **not** recovered; they
|
||||
are on the table, which is where they belong. A game that has genuinely used everything still ends on
|
||||
`DECK_EMPTY` rather than reshuffling an empty sweep.
|
||||
|
||||
The full shuffled order rides the event rather than being recomputed from `rngState`. A save is a
|
||||
seed plus the intents, so events are never serialised and the size costs nothing — and an event that
|
||||
states the outcome outright cannot drift from the reducer the way a re-derivation can. Covered by a
|
||||
test that builds the same position twice and asserts the two decks come out identical.
|
||||
|
||||
**It has not fired in play yet, which is worth knowing.** Solitaire Short/Standard/Campaign end with
|
||||
186.8 / 176.2 / 168.8 cards left of 243 and never ran dry across 60 games each; four-player Campaign
|
||||
ends with 86.6 across 25 games. It is a safety net, not a live mechanic. It is not the case that the Departments
|
||||
only grow — a player takes the top card of one as their draw as readily as discarding onto it, so a
|
||||
Department can be drawn down and refilled from the Home Office deck. What the reshuffle guards is the
|
||||
Home Office deck itself running out, which is possible whichever way the piles happen to be moving.
|
||||
|
||||
**The Salvage Yard is a pile like the others**, and now shown like them. It was already modelled as a
|
||||
list; what it lacked was any presence on screen, which mattered the moment it became the thing that
|
||||
comes back in a reshuffle. Watching it fill is the only warning a player gets that the deck is about
|
||||
to turn over.
|
||||
|
||||
**All four piles now show their top card and their depth.** Each is drawn as a card with the pile's
|
||||
name and a count badge on it, then the face-up card underneath. The depth is a count and not a hint:
|
||||
only the top card may ever be drawn, so everything below it is out of reach, and choosing where to
|
||||
discard is choosing what to put there.
|
||||
|
||||
### The Departments are decks, and you choose which one to discard onto
|
||||
|
||||
From the designer: *"when discarding from their hand, the player can select which department card
|
||||
deck they want to place the discard on top of. Department card decks are shared across all players.
|
||||
When pulling from a department card deck players may only pull the top card."*
|
||||
|
||||
The rules already said so and the code had read them the other way. §6.2: a discard is placed "face
|
||||
up **on top of** one of the three Department slots", and a draw takes "the **top** face-up card".
|
||||
Both phrases only mean something over a pile. Gap 4a had concluded the Departments were "three
|
||||
face-up market slots fed from the one deck, not decks with their own contents"; that finding is now
|
||||
marked corrected in `open-questions.md`.
|
||||
|
||||
**It was destroying cards.** `cardDiscarded` did `departments[toSlot] = cardId` — assignment, not a
|
||||
push — so discarding onto an occupied Department annihilated the card already face up there. A closed
|
||||
deck was quietly leaking. It survived because the only card-conservation test ran at setup and never
|
||||
again; there is now one that counts after a full game, and one that asserts no id is ever in two
|
||||
places at once.
|
||||
|
||||
**And the choice was invisible.** All three discards described themselves as `discard X`, and the
|
||||
action list drops duplicate labels — so three genuinely different decisions collapsed into a single
|
||||
button and the Department could not be picked at all. Discards now read `discard Freight House onto
|
||||
Department 2, burying Brakeman`, and a draw reads `take Depot from Department 1, 3 buried beneath
|
||||
it`. The browser shows each pile's top card with a `+n under` count, because a deep pile is where
|
||||
cards have been put beyond reach and that is what a discarding player is choosing between.
|
||||
|
||||
**Refill timing changed with it.** §6.2 refills an *empty* Department from the Home Office deck. The
|
||||
old code refilled after **every** Department draw, which was harmless when a slot held one card and
|
||||
would now drain the deck into the piles. It refills only when taking the last card empties one.
|
||||
|
||||
**The bot got the strategy this opens up.** It used to take the first discard option, always
|
||||
Department 1, burying whatever sat there — including the Depot it was waiting on. It now covers the
|
||||
face-up card least worth keeping reachable, never one it would take, and breaks ties toward the
|
||||
shallowest pile. Measured over 100 games: spreading discards **2.87** revenue, concentrating them on
|
||||
the deepest pile 2.67, indifferent 2.67. Three piles offer three face-up cards, and piling onto one
|
||||
of them leaves the other two showing whatever they started with.
|
||||
|
||||
In a competitive game the same call reads the other way round — burying a card a rival wants is an
|
||||
attack rather than housekeeping — which is exactly why the choice belongs to the discarding player.
|
||||
|
||||
**Measured, 100 solitaire Standard games:** Departments hold 8.3 cards between them at game end,
|
||||
deepest single pile 17. The Home Office deck ended with 176.6 of 243 and **never ran dry**, which
|
||||
matters because §6.2's reshuffle — collect the Salvage Yard and all three Departments, reshuffle,
|
||||
re-establish the deck — **is not implemented**. It has never been reachable in a 5-Day solitaire
|
||||
game; it will matter for Campaign length and for four players. *(Implemented in a later entry.)*
|
||||
|
||||
### Track is a deck card, not a private supply
|
||||
|
||||
Reported by the designer: *"all track cards are included in the home office deck and are played from
|
||||
there like any other card."*
|
||||
|
||||
The error is visible in the spreadsheet. `docs/Deck cards2.xlsx` has a column B headed **"Number in
|
||||
Deck"** — 32 straights, 16+16 curves, 4+4 sharp curves, 16+16 turnouts, **104 cards** — and a LAST
|
||||
column headed **"Track Per Player"** reading 8/4/4/1/1/4/4 = 26. The code took the last column as a
|
||||
separate physical stack and wrote *"Track is NOT in the Home Office deck — this is the single biggest
|
||||
structural change from the placeholder."* It is 104 shared among four players, not a second pile. The
|
||||
sheet's own totals settle it: "Sum other 115", "Total track 104", grand total 231 — and 115 + 104 +
|
||||
12 start cards is exactly 231.
|
||||
|
||||
**What went.** `TRACK_SUPPLY`/`TRACK_PER_PLAYER`, `OfficeArea.trackSupply`, the `track.lay` intent,
|
||||
the `trackLaid` event, `protoTrackCard`, `TurnState.laidThisTurn` (the one-piece-a-turn cap, which
|
||||
only existed because a private supply had nothing else bounding it), and the "Your Track Supply"
|
||||
panel. `CardKind` for track gained a `hand`, because handedness is the diagonal and a track card
|
||||
without one cannot say what it may be joined to.
|
||||
|
||||
Most of the plumbing was already there and dead: `checkPlay` had a `track` branch, `protoCard` had a
|
||||
`track` branch, and the `cardPlayed` reducer already placed a track card with a rotation. Track had
|
||||
been a card once, and moving it back was largely deleting the parallel path.
|
||||
|
||||
**The deck is 243 cards, of which 104 are track** — the largest category by some way, and the point
|
||||
of the change: building a district is now paid for in the industry or train you did not draw, and a
|
||||
three-card hand is the real constraint on how fast a railroad grows.
|
||||
|
||||
**Measured over 100 solitaire Standard games, against the same run with track as a private supply:**
|
||||
|
||||
| | private supply | in the deck |
|
||||
| --- | ---: | ---: |
|
||||
| deck size | 139 | 243 |
|
||||
| district size | 28.5 cards | 28.0 |
|
||||
| mean max depth off the main | 1.45 rows | 1.94 |
|
||||
| turnouts / curves / straights per game | 7.6 / 7.2 / 3.4 | 5.6 / 7.4 / 5.6 |
|
||||
| facilities placed | 4.56 | 3.84 |
|
||||
| districts with a run-around | 70/100 | **29/100** |
|
||||
| facilities on a run-around | 0.51/game | **0.18** |
|
||||
| revenue | 5.80 mean | **3.05** |
|
||||
|
||||
**Revenue nearly halved, and that is the headline for the next decision, not a defect to paper
|
||||
over.** A run-around needs a turnout, a matching curve, straights, a second curve and a second
|
||||
turnout — all of the right hand, arriving in a three-card hand in a usable order. It used to be a
|
||||
shopping list; it is now a draw. Two test floors were re-baselined against the measurement with the
|
||||
old figure recorded beside them, deliberately set BELOW what was measured so they detect the loop
|
||||
machinery breaking rather than endorsing 29%.
|
||||
|
||||
**Two bot fixes fell out of it, both real.** Track became an ordinary `card.play`, so the generic
|
||||
"play anything placeable" fallback started dumping track on whatever square was legal — bypassing
|
||||
`bestTrackLay`, which had already looked at the same piece and declined it. Measured: 26 of 60
|
||||
districts ran the siding past the last column with a way up. A track card the scorer will not use is
|
||||
a card to discard. And `arcsLeft`, which asked a supply that no longer exists, became `arcInHand`:
|
||||
"have I got a curve of this hand?" is now a question about the hand, not a certainty.
|
||||
|
||||
**Still open, and now urgent.** The office counts are doubled (Depot 4→8, Station 2→4, Terminal 1→2,
|
||||
Q12) and the industries tripled (Gap 12), both tuned by measuring a deck with **no track in it** —
|
||||
25 of 100 games never drew a Depot and never escaped Whistle Post. Adding 104 cards dilutes every
|
||||
draw by 43%, which is exactly what those multipliers were compensating for, so they are now either
|
||||
badly needed or badly wrong and only a measurement will say which. `test/setup.test.ts` records both
|
||||
departures and flags them for re-measurement.
|
||||
|
||||
### The placement labels described the mirror of the card they would lay
|
||||
|
||||
Reported as "I can't play a turnout north or south of an existing turnout". It was always legal — a
|
||||
turnout under a turnout is a **crossover**, and it is how a siding gets a track running parallel to
|
||||
the Running Track. The engine accepts left-over-left and right-over-right today, and the square was
|
||||
offered and clickable. What was wrong was the words on it.
|
||||
|
||||
`rotationNote` in `src/web/game.ts` called `variantsFor(geometry)` **without the hand**. Hand is the
|
||||
diagonal, so without it `variantsFor` answers for the left-hand card whatever you are holding. Every
|
||||
right-hand turnout was offered as *"stem west, through east, diverges south"* — the exact mirror of
|
||||
the card it would lay — and every right-hand curve named the wrong edge. The placement was always
|
||||
correct and only the description lied, which is the kind of bug that survives a green test suite and
|
||||
makes a working feature feel broken.
|
||||
|
||||
Fixed by threading `hand` through `rotationNote` → `variantLabel`, and covered by a test that walks
|
||||
every geometry × hand × rotation and checks the label against `variantsFor`'s own answer.
|
||||
|
||||
**And a placement now says what it would connect to.** Two cards meeting at an edge is not a rail —
|
||||
on a north or south edge their 45° legs must also share a diagonal — so "is this square legal" and
|
||||
"does this piece meet the one I am aiming at" are different questions and only the first was on
|
||||
screen. Spots now read `(-1, 1) — stem east, through west, diverges north at 45° · joins the track
|
||||
above`, which is the crossover named outright.
|
||||
|
||||
### A New game button, instead of finishing the one you have
|
||||
|
||||
`start()` restores from localStorage on every load and the only "new game" button lived on the
|
||||
game-over screen, so a game you no longer wanted followed you across reloads with no way out.
|
||||
|
||||
`New game` sits beside `Save replay` in the header. It confirms once past the opening Stage — the
|
||||
save *is* the game, there is no undo, and the replay download is right there — then clears the save
|
||||
and reloads. It drops any `?seed=` from the URL as well: leaving it would deal the same game again
|
||||
and look like the button had done nothing.
|
||||
|
||||
### 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
|
||||
|
||||
Reference in New Issue
Block a user