v0.7.1 — a caboose is not a load, a Day that says it ended, and a train you may throw away
Four issues off the Gitea tracker, all of them things a player saw at the board. Reasoning for every item, and what was verified how: CHANGELOG.md. - Gitea#8: X22 Pee-Dee refused every caboose, including the one it was made up with, so setting it out stranded the train. All six cabooses are minted loaded because §2.2's "coloured is loaded, white is empty" doubles as a piece count in the supply table; one read of the flag took that literally. A caboose carries the crew, not freight, so it is never a load. - Gitea#10: a Day turns over inside the phases that run themselves, so it passes between one click and the next — and both transient signals fade before a player reading the board notices. A modal stops and waits, carrying the standings, the Days left and the combined target. Suppressed on the first frame, on Undo stepping back across a rollover, and on the Day the game ends. - Gitea#9, which SUPERSEDES Gitea#6 from three days ago: a Timetabled train may be tossed face-up to a Department slot, where a rival may pick it up — the second half of the ruling needed no code, since that is where every discard already goes. An Extra still may not. A New Game setting on this line (discardTimetabled, on by default), the plain rule on the 0.4.9 line. - Gitea#2 is not an engine bug: the rules are implemented exactly, and running the coach pool dry is Jesse's ruling to keep — "part of the strategy". What was wrong is that the game said nothing. A blocked platform now gives its reason, from the engine's own predicate, including how many coaches are stranded in Classification and what brings them back. The same four ship as v0.4.9g on the playtest line. Closes #2 Closes #8 Closes #9 Closes #10 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FLnYR4XtXQNamYJXGYT8oC
This commit is contained in:
co-authored by
Claude Opus 5
parent
603d38602c
commit
441447648d
+139
@@ -19,6 +19,145 @@ page as `v0.1.0 · <sha> · <date>`, so what is deployed can always be identifie
|
||||
|
||||
---
|
||||
|
||||
## 0.7.1 — 2026-08-25
|
||||
|
||||
Four issues off the Gitea tracker, all of them things a player saw at the board. Two are engine or
|
||||
page bugs, one is a rules ruling that supersedes a ruling from three days earlier, and the fourth
|
||||
turned out not to be a bug at all — the fix there is that the game now says so. The same four ship
|
||||
as **0.4.9g** on the 0.4.9 line.
|
||||
|
||||
### A caboose is not a load (Gitea#8)
|
||||
|
||||
**REPORTED:** X22 Pee-Dee could not couple a caboose — including the one it was made up with. Drop
|
||||
it at the end of a sweep and it was stranded there, which makes a train whose whole card is a
|
||||
restriction ("may only pick up MTs") unplayable rather than merely restricted.
|
||||
|
||||
`ROLLING_STOCK_SUPPLY` mints all six cabooses as `{ loaded: 6, empty: 0 }`, because §2.2's "a
|
||||
coloured car is loaded, a white car is empty" is doing double duty there as a PIECE COUNT and there
|
||||
is no white caboose to make a train up from. So `.loaded` carries two meanings, and the second one
|
||||
escaped in exactly one place: `pickUpEmptiesOnly`'s `fresh.some((c) => c.loaded)`. Every other read
|
||||
of the flag in `apply.ts` is already scoped to a coach or to a named car type.
|
||||
|
||||
`carriesLoad` now answers the question the rule is actually asking — a caboose carries the crew, not
|
||||
freight, so it is never a load — and the restriction itself is untouched: a loaded car alongside the
|
||||
caboose still refuses. `trainRules` says so on the card ("A caboose is not a load"), because a player
|
||||
reading "EMPTIES ONLY" has no way to know which reading the game took.
|
||||
|
||||
### The Day rolls over and says so (Gitea#10)
|
||||
|
||||
**REPORTED:** "As the game rolls off the end of the day, you get a dialog saying such. Hard to keep
|
||||
track of time."
|
||||
|
||||
Nothing on screen was wrong. The clock, the turn chart and the timetable all said which Day it was.
|
||||
What is wrong is WHEN it changes: a Day turns over inside the phases that run themselves, so it
|
||||
happens between one click and the next, while the player is watching the board and waiting for their
|
||||
turn. The two transient signals the page already had are both gone before that — the phase banner at
|
||||
2.6s, the announcement flash at 4.2s. A modal is the whole request: it stops, and it waits.
|
||||
|
||||
`dayEndHtml` writes it from the frame AFTER the rollover, so the Day that ended is `f.day - 1`.
|
||||
It carries the standings in **Revenue order** rather than seat order (the question at the end of a
|
||||
Day is who is ahead), the Days left to run, and the combined target — reported against the whole
|
||||
table's Revenue, because `minCombinedRevenue` is a combined floor and one player's score against a
|
||||
four-player target reads as hopeless when the table is comfortably ahead. Collisions appear only
|
||||
where §3.4 actually scores them: competitive and co-op, and only when a dial is non-zero. A solitaire
|
||||
game carries the default dials and enforces neither, so printing a collision budget there would put a
|
||||
rule on screen that this game does not have.
|
||||
|
||||
Three suppressions, each of them a way it would otherwise lie:
|
||||
|
||||
- **the first frame** — arriving in a game already on Day 3 is not Day 2 ending, and a page reloaded
|
||||
mid-game would announce a rollover that happened before it was watching;
|
||||
- **the Day going DOWN** — that is Undo stepping back across the rollover, not a Day passing. Undo
|
||||
also clears `lastDay`, so replaying forward through the same rollover does not announce it twice;
|
||||
- **the Day the game ends on** — the outcome panel is the thing to read then. Verified by playing a
|
||||
full solitaire game through: the dialog fires four times in a 5-Day game, not five.
|
||||
|
||||
### A Timetabled train may be thrown away (Gitea#9)
|
||||
|
||||
**REPORTED:** "Timetabled trains are at the choice of the player — they can either play or discard.
|
||||
If someone else wants to pick it up, they are more than able to. The reason: I don't want, if you
|
||||
decide to play a game longer than five days, to decide that maybe there are too many trains, the
|
||||
stations are jammed, and the railroad doesn't need any more. You can toss it. Someone else might
|
||||
disagree and pick it up."
|
||||
|
||||
This **supersedes Gitea#6**, shipped three days earlier in 0.6.2, which made every train card
|
||||
unconditionally undiscardable. Two things narrow it:
|
||||
|
||||
- an **Extra** is still never discardable. It never joins the timetable, so it can never be what
|
||||
jams it, and the only rule it would dodge by being thrown away is the hand limit;
|
||||
- the Timetabled half is a **New Game setting** — `discardTimetabled`, on by default — because
|
||||
Jesse's reasoning is explicitly about games run LONGER than five days, and a five-Day game may well
|
||||
want Gitea#6's pressure back. Jesse asked for it as a setting on this line and as the plain rule on
|
||||
the 0.4.9 line, which has no scaffolding for one; both play the same game at main's default.
|
||||
|
||||
**The second half of the ruling needed no code at all.** "Someone else might pick it up" — a discard
|
||||
already goes face-up onto a Department pile, and a Department pile is exactly what a rival draws
|
||||
from. Only the first half was a change.
|
||||
|
||||
**One place decides, and the card says which rule refused.** `keepReason` returns the sentence a
|
||||
player should read, or `null` if the card may go; `check`, the hand panel and the blocked "End Local
|
||||
Operations" button all ask it. It returns a SENTENCE rather than a boolean because there are now two
|
||||
distinct reasons — "an Extra is never discarded" and "not in this game" — and the panel that used to
|
||||
hard-code one of them would now tell half the players the wrong thing. It reaches the page as the
|
||||
Frame's `handKeepWhy`, replacing text `panels.ts` and `main.ts` each wrote for themselves.
|
||||
|
||||
**Two places would have dropped the setting silently**, both found by looking rather than by `tsc` —
|
||||
`HouseRuleOverrides`' fields are all optional, so omitting one compiles and falls back to the
|
||||
default. The New Game dialog's close handler builds its own `houseRules` object (the comment
|
||||
directly above it warns of exactly this: "a setting missing from here is a setting the dialog
|
||||
silently discards"), and `presets.ts`'s Frame-to-config path, which is how a JOINER is shown someone
|
||||
else's game — a setting dropped there shows them a rule the table is not playing.
|
||||
|
||||
**Gitea#6's corner survives, narrowed.** A hand of four undiscardable trains still has exactly one
|
||||
legal way on — play one — with nothing in the engine computing "you must play a train". With the
|
||||
setting on, the only hand that reaches it is four Extras; with it off, any four trains, as before.
|
||||
Both are pinned by tests.
|
||||
|
||||
### Why nothing is moving on the platform (Gitea#2)
|
||||
|
||||
**REPORTED:** "The Sparrow pulled into the station with two loaded coaches. There are two passengers
|
||||
on the platform. Four porters. My thought was to unload two and load two. I never get the chance to
|
||||
load the last two."
|
||||
|
||||
**The engine is not deviating from the rules**, and this was checked step by step against the
|
||||
reported save. §9.2 discards the white coach into the Classification Yard on boarding and draws one
|
||||
from the Division Yard on de-training; §2.2 returns the Classification Yard only when the Division
|
||||
Yard is empty. All three are implemented exactly. What bites is the interaction: both halves of every
|
||||
passenger cycle consume coaches one-way, and a single global refill condition over a pile of six
|
||||
commodities means they do not come back. Traced over the reported game the coach pool goes 8+/8− on
|
||||
Day 1 to 0+/1− by Day 5, with eight coaches stranded in Classification behind ~60 other cars.
|
||||
|
||||
**Jesse's ruling is that the shortage stays** — "it is possible to run out, that's part of the
|
||||
strategy" — so the three balance options written up in `TODO.md` are declined rather than deferred.
|
||||
|
||||
What was unambiguously a bug is that the game said **nothing**. A Porter action that cannot be taken
|
||||
is simply absent from the menu, and `impediments()` — the panel whose entire job is "why is nothing
|
||||
moving?" — opened with `if (!f || f.kind !== 'freight') continue`, so a platform had never had
|
||||
anything to say for itself. The player was not merely blocked; he was given no reason.
|
||||
|
||||
A Passenger Facility now reports both directions: passengers standing with no train to take them, a
|
||||
train whose card bars Porters from working it, a Terminals-only train at a lesser Office, every coach
|
||||
already full, the red slots full, the same-district rule, and the coach shortage itself — that last
|
||||
one naming how many coaches are sitting in Classification and the condition that brings them back,
|
||||
because a yard visibly full of cars that will not yield one coach is the state that looks like a
|
||||
broken game. The reason text comes from `passengerRefusal`, the engine's own predicate (exported for
|
||||
this), so what is on screen is the rule that actually refused rather than a second guess at it.
|
||||
|
||||
**A second defect fell out of fixing it.** The row's name is read off `card.geometry.facility`, which
|
||||
a Passenger Facility does not have — it rides on the `office` card — so every passenger impediment
|
||||
would have read `facility 0,0` beside a freight row saying `mineTipple 1,-3`. It is named by the
|
||||
Office Area's tier now (`terminal 0,0`), and there is exactly one Office per Area, so that tier is
|
||||
the card's own.
|
||||
|
||||
Verified by replaying the reported save (`playtests/station-master-seed947338225-day5(1).json`)
|
||||
through `fromSave` and printing the panel. Worth noting for anyone who tries it: that save is a
|
||||
v0.4.9-line recording and stops at intent 250 of 323 on the 0.7 engine, because Gitea#4, #6 and #7
|
||||
changed the rules its later intents were recorded against. That is expected divergence, not a
|
||||
save-format bug — a replay reproduces a game from decisions, and the decisions no longer mean the
|
||||
same thing.
|
||||
|
||||
---
|
||||
|
||||
## 0.7.0 — 2026-08-23
|
||||
|
||||
The multiplayer set-up, the lobby, the start of a game, and four things a remote client had never
|
||||
|
||||
Reference in New Issue
Block a user