v0.4.4 — New Game dialog for the opening hand and the three revenue rates, Yard renamed Interchange, and two movement bugs behind a mirrored consist
This commit is contained in:
+199
@@ -21,6 +21,205 @@ page as `v0.1.0 · <sha> · <date>`, so what is deployed can always be identifie
|
||||
|
||||
## Unreleased
|
||||
|
||||
## 0.4.4 — 2026-08-14
|
||||
|
||||
Out of a play session: engines that pointed north, a card whose name collided with five other things,
|
||||
a New Game dialog that only asked for a seed — and then, chasing a mirrored consist, two movement
|
||||
bugs that had been there all along.
|
||||
|
||||
Both playtest reports were confirmed against Jesse's own saved game,
|
||||
`docs/station-master-seed493290760-day2.json` (seed 493290760, two Days, 127 intents), which is kept
|
||||
as the evidence for what follows.
|
||||
|
||||
### A curve is not a straight, and a train that rounds one knows it
|
||||
|
||||
**Reported:** "a train reversed into a siding and the display of the cars was reversed."
|
||||
|
||||
`facing` is the port the engine would leave by, and a Move recorded it as `opposite(entry)`. That is
|
||||
the far end of a **straight** and of nothing else: a curve is an arc between two ADJACENT edges, so a
|
||||
train entering a north-west curve through its west port comes out facing **north**, not east. Every
|
||||
train that rounded a curve was left facing a port its own card does not have.
|
||||
|
||||
Two things went wrong with that, and only the second was visible:
|
||||
|
||||
- **Movement.** `movesFor` explores from `facing`, and a port the card lacks yields no destinations
|
||||
at all — so a crew that rounded a curve could only ever back out the way it came. It could not
|
||||
continue round the corner it had just taken.
|
||||
- **Display.** The east-west sense the board draws is carried from `facing`, so a curve that had
|
||||
really turned the engine west could leave the board still drawing it east — the consist mirrored,
|
||||
which is what was seen.
|
||||
|
||||
The forward case now asks the **card** for its far end (`farPort`). The reverse case is unchanged and
|
||||
was already right: backing up, the engine trails and points out through the port the train came in
|
||||
by, whatever the track does underneath — there is a test pinning that so the fix cannot drift into it.
|
||||
|
||||
**The reported moment, out of the save.** Three times in two Days the crew backs off the Running
|
||||
Track into the curved siding at (1,-2) — a `sw` arc, so entering it from the south leaves the engine
|
||||
facing south. Intent #109, drawn both ways:
|
||||
|
||||
```
|
||||
came from (0,0) office [ew] west cab tnk [>] east
|
||||
BEFORE the fix west [v] box tnk cab east
|
||||
AFTER the fix west cab tnk box [>] east
|
||||
```
|
||||
|
||||
The engine was on the east end before the move and is on the east end after it — backing up does not
|
||||
turn a train around. The old renderer flipped the whole strip to nose-left the moment `facing` stopped
|
||||
being `'e'`, which is precisely the mirroring that was reported.
|
||||
|
||||
### A train leaving a district drops its spur port
|
||||
|
||||
The same family, found while fixing the above. Nothing reset `facing` when a train left an Office, so
|
||||
a crew that had been shunted onto a north-south spur carried a compass port out onto a Division that
|
||||
runs east and west — and then into the next Office, whose card has no north or south edge at all.
|
||||
With neither `facing` nor its opposite on the card, `movesFor` returned nothing in either direction:
|
||||
**the train arrived at the next Office unable to make a single Move.** It also drew a ▲ on the
|
||||
Division map, where there is no north to point at.
|
||||
|
||||
A train out there is running one way along an east-west railroad with its engine at one end, so
|
||||
`enterMainline` now says so. This is the same stale port that made the ▲ on the Division map — the
|
||||
`railFacing` change above stopped it being *drawn*, and this stops it existing.
|
||||
|
||||
### Departures after Cargo: investigated, not a bug — but one card contradicts itself
|
||||
|
||||
Also reported: "a train left at the end of the Cargo phase — shouldn't it wait for the next Mainline
|
||||
phase?" It should not, and it did not: **only Expedite trains do this**, which is Q3 working as
|
||||
agreed. In the save it is **6 The Sparrow**, twice — arriving in Stage 9's Mainline and highballing
|
||||
in Stage 9's Supervisor Shift, then the same in Stage 10 of Day 2. The one non-Expedite train in the
|
||||
game, 12 Drag Freight, arrived in Stage 8 and left three Stages later in a **Mainline** phase, exactly
|
||||
as it should. Measured more widely, over 40 bot games, the split is perfect: every departure with no
|
||||
Local Operations turn was an Expedite train, and every non-Expedite train got at least one.
|
||||
|
||||
Two things are worth writing down because they read as bugs and are not:
|
||||
|
||||
- **Expedite departs in Supervisor Shift, not in Cargo.** Q3 as recorded says the train "gets a
|
||||
second `moveTrain` in the same Mainline Phase"; the code deliberately does not, because that had
|
||||
expedited trains gone before a single Porter could reach them. Supervisor Shift is the last phase
|
||||
of the Stage, so it is still the Stage the train arrived in — it just looks like "I finished Cargo
|
||||
and it left", because Supervisor Shift needs no input and runs itself.
|
||||
- **Expedite costs the train its Local Operations turn**, since Local Ops is phase 1 and the train
|
||||
arrives in phase 3 and leaves in phase 5. Harmless for four of the five — Crack Limited, The
|
||||
Sparrow, the Military train and the Light Engine all print "no switching" and would decline it.
|
||||
|
||||
**Except for 3/4 Express**, which prints *"may drop or pick up one freight car at every location"*
|
||||
and is also Expedite. That budget is spent by coupling and setting out, which happen only in Local
|
||||
Operations — so the Express has a printed ability it can never use: 31 Office visits in 40 games, 31
|
||||
with no turn to use it in. X14 Fruit Growers Express is in the same position, though its extra-reefer
|
||||
line is a note today with no mechanics behind it. Left alone pending Jesse's call and recorded in
|
||||
`TODO.md`; it is a rules contradiction on the card, not a fault in the timing.
|
||||
|
||||
### Replays are replaced rather than piled up
|
||||
|
||||
Both fixes change which Moves are legal, so the published replays stopped replaying — correctly, and
|
||||
the liveness test caught it. Re-recording used to write the new set **beside** the old one, and the
|
||||
old set is precisely the one whose rules have just moved, so dead files accumulated and the test
|
||||
failed on them forever. `save-replay.ts` now retires what it replaces (only once a replacement has
|
||||
verified), and writes the `rules` block into each file so a replay can never again be silently
|
||||
re-dealt. `harness.test.ts` was passing `{ seed, history }` and dropping `rules` on the floor, which
|
||||
would have replayed every published file under the pre-dialog defaults whatever it said.
|
||||
|
||||
### The engine points east or west, always
|
||||
|
||||
**Reported:** a crew that turned onto a north-south spur was drawn with a ▲ over it, and the change
|
||||
of convention was harder to read than no arrow at all.
|
||||
|
||||
`facing` on a Crew Tray is a **port** — 'n', 's', 'e' or 'w' — because movement needs one: a crew
|
||||
standing on a north-south spur has to be able to leave by 'n' or 's', and the last attempt to derive
|
||||
east/west from the direction of the run stranded 29 of 62 leftover crews on north-south track with no
|
||||
legal move. So the port stays exactly as it is, and what changed is the **drawing**.
|
||||
|
||||
A new `railFacing` on the tray carries the east-west sense across north-south track: it updates
|
||||
whenever `facing` becomes 'e' or 'w' and holds its value in between. That is the railroad's own
|
||||
convention, where compass north on a branch is still timetable east. It follows the engine around
|
||||
180° of curves, because a train that runs forward through two curves really has turned around — and
|
||||
it does *not* move when a train backs up, because a train that backs up has not.
|
||||
|
||||
**It also fixes a bug nobody had reported yet.** Nothing resets `facing` when a train leaves a
|
||||
district, so a crew that shunted onto a north-south spur and then departed carried its 'n' out onto
|
||||
the Division map — where there is no north or south — and drew ▲ there too.
|
||||
|
||||
The Frame's `facing` is now typed `'e' | 'w'`, so this is enforced rather than merely observed, and
|
||||
the Office card lays a north-south crew's consist east-west like every other train instead of pinning
|
||||
it nose-left.
|
||||
|
||||
### The Yard mainline card is now the Interchange
|
||||
|
||||
Same 60, same "sort cars into any new order", same entry points, same art. What it did not have was a
|
||||
name of its own: **Division Yard, Classification Yard, Salvage Yard, Yard Office and Small Yard** are
|
||||
five other things in this game, and none of them is this card. The internal key is renamed with it
|
||||
(`MainlineKind` `'yard'` → `'interchange'`) so the two cannot drift. The other five are deliberately
|
||||
untouched — they merely shared a word.
|
||||
|
||||
### New game asks for the rules, not just the seed
|
||||
|
||||
It was a `prompt()` asking for a seed. Two of the three things that decide what kind of game you are
|
||||
about to play had no way in at all: the opening hand had been changed twice with no way back to the
|
||||
earlier rule, and the three revenue rates were constants in the source. Balance is the open question
|
||||
this game has, and settling it means dealing several games at different settings — which needs a
|
||||
dialog, not a rebuild.
|
||||
|
||||
**Starting hand**, three options, all of which have been the rule at some point:
|
||||
|
||||
- **Three random cards** *(new default)* — the prototype rule. At the hand limit already.
|
||||
- **Six random cards** — twice the choice, still no guaranteed track.
|
||||
- **Three random track and three random non-track** — the v0.4.x deal, from two shuffled piles.
|
||||
|
||||
**Revenue**, three rates, each 0–5:
|
||||
|
||||
- **Passenger revenue per coach**, default **1**. Paid when a coach is boarded and again when it is
|
||||
detrained — both halves of the movement, as it has always worked.
|
||||
- **Freight revenue per load**, default **1**. Paid when a load is made up outbound and again when it
|
||||
is broken inbound.
|
||||
- **Train revenue per transit**, default **0** — *changed from 1*. Paid to every player when a train
|
||||
runs off the end of the Division. At 1 it was worth ~5.4 Revenue against a bot mean of 7.0: the
|
||||
railroad was earning most of its money from the one thing nobody has to work for, and the freight
|
||||
and passenger economies the game is about could not be read through it.
|
||||
|
||||
Zero is a real setting rather than "one, suppressed" — no revenue event is emitted at all, so the
|
||||
history does not fill with "+0 Revenue" for work that did not pay.
|
||||
|
||||
**A seed no longer names a game**, so the settings ride in the URL beside it
|
||||
(`?seed=430&hand=sixRandom&passenger=1&freight=1&transit=0`) and the header carries a readout of what
|
||||
is in force. A playtest note reading "scored 4" is worthless without it.
|
||||
|
||||
**Saves carry the rules they were dealt under.** A save is a seed and a list of intents: replay it
|
||||
under different rules and it is a different game, and the symptom is not an error but a replay that
|
||||
quietly stops early — which `TODO.md` records happening twice unnoticed, one of them 42 intents into
|
||||
360. `Save.rules` is written from now on, and a save without it replays under the pre-dialog rules
|
||||
(3+3, and 1 per transit) rather than today's defaults, so the three published replays still run.
|
||||
|
||||
**Under the hood.** The settings live on `GameConfig.houseRules` as a partial, resolved in one place
|
||||
by `houseRules()`, which clamps and rounds — so a hand-edited URL cannot deal a game at 900 Revenue a
|
||||
coach. They reach the page on the `Frame` rather than off the config, because a remote client holds
|
||||
no `GameState` and still has to be able to answer "what does a load pay here?"; `createLocalSession`
|
||||
takes house rules rather than a whole `GameConfig` for the same reason.
|
||||
|
||||
**Exercised, not just compiled.** Ten developer-bot games per setting, solitaire Standard — far too
|
||||
small a sample to conclude anything about balance, and enough to show the dials are wired to the
|
||||
game rather than to the dialog:
|
||||
|
||||
| Setting | Dealt | Mean Revenue |
|
||||
| --- | --- | --- |
|
||||
| `threeRandom` (default) | 3 | 4.00 |
|
||||
| `sixRandom` | 6 | 2.00 |
|
||||
| `threeTrackThreeOther` | 6 | 0.50 |
|
||||
| pax 1 · frt 1 · **trn 0** (default) | — | 4.00 |
|
||||
| pax 1 · frt 1 · **trn 1** (the old rule) | — | 9.30 |
|
||||
| pax 0 · frt 0 · trn 0 | — | −0.20 |
|
||||
| pax 5 · frt 5 · trn 0 | — | 20.80 |
|
||||
|
||||
The transit rule is worth **+5.30** here against the +5.4 measured over 200 games before it became a
|
||||
setting, which is the number this change was made to get out from underneath. All zeroes lands just
|
||||
below zero — collision penalties, with nothing left paying — which is the right shape for an economy
|
||||
switched off. **Read no balance conclusion from the two random-deal rows**: ten seeds is noise, and
|
||||
the hand rows are three different games rather than the same game dealt differently.
|
||||
|
||||
**Tests.** 513 passing, six of them driving the dialog through the *emitted bundle* — the failure
|
||||
mode here is wiring, not logic. Four existing tests had to be re-pinned rather than merely updated:
|
||||
the opening deal changes the RNG stream, so `advance.test.ts` was relying on whichever mainline card
|
||||
seed 1 happened to lay down and now pins its own terrain, and the widest action list was re-measured
|
||||
over five seeds in all three deals (13 under the random deals, 10 under 3+3).
|
||||
|
||||
## 0.4.3 — 2026-08-14
|
||||
|
||||
### A load has to have somewhere to go
|
||||
|
||||
Reference in New Issue
Block a user