v0.8.0.10 — playtest fixes: clearance rulings, the log, the map, and a save file

From the first two multiplayer playtests of v0.8.0.9, each traced before fixing.

The engine:

- A train on a card BEHIND the one departing no longer triggers a clearance
  ruling or an opposite-direction bar (#26). Reproduced from the exported
  save: X15 was held over X18 behind it, and X18 then collided into the full
  Whistle Post. Games in progress holding a ruling the engine no longer asks
  for will not resume (28 of 40 recorded four-seat games); shipped as is at
  Jesse's call.
- `mainlineModified` carries the card's previous kind, so the log can say
  what a Realignment converted (#27).

The screen:

- The turn chart and the Division map name the player whose move is on
  screen while bot turns replay, not the live actor (#25).
- The owning player's name is no longer outlined by the turn arrow's stroke,
  which made it unreadable (#24).
- A Mainline card flashes on the map when a Realignment changes it (#28).
- The history is held back with the board and revealed step by step, instead
  of arriving whole while the board is still catching up (#29).
- A ruling made by holding the office reads "Superintendent Player X" (#30),
  and no line names a player twice (#31).
- A seated player can download their own game as a save file: the play
  page's Save replay button, fed by GET /api/save?token=… (#32). The StartOS
  action cannot do this — an action result is text only.

Closes #24
Closes #25
Closes #26
Closes #27
Closes #28
Closes #29
Closes #30
Closes #31
Closes #32

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017nnuCv8UodHucFfx3LWEoX
This commit is contained in:
Jesse.Markowitz
2026-09-15 22:44:20 -04:00
co-authored by Claude Opus 5
parent 76c6e103b3
commit 4adf149ba5
17 changed files with 607 additions and 41 deletions
+104
View File
@@ -19,6 +19,110 @@ page as `v0.1.0 · <sha> · <date>`, so what is deployed can always be identifie
---
## 0.8.0.10 — 2026-09-15
Three reports from the first multiplayer playtest of v0.8.0.9 — one player and three bots — each traced
to its cause before it was fixed.
### The Superintendent is no longer asked about a train BEHIND the one departing (Gitea#26)
*"Two westbound trains: X15 is further west on Mainline cards than X18. And I still get a Superintendent
must rule... held X15 and then collision happened. X18 destroyed, but X15 fine."*
**Reproduced by replaying the exported save.** At move 78, X15 was highballing west out of seat 3's
Office while X18, also westbound, was still crossing the card to its EAST — behind it. Every Office was a
Whistle Post, so the Subdivision ran the whole railroad, and `evaluateClearance` counted every train in it
without asking which side of the departing train it stood on. The ruling was meaningless; holding X15
kept the Whistle Post's only A/D track full, and X18 arrived into it and was destroyed. Unasked, X15 —
the lower number, so the first to move — would have left and freed the track.
§8.1 asks about a train the departing one would FOLLOW and one moving TOWARDS it, and both are ahead of
it. So a train on a card strictly behind the departing train's own card is no longer counted, in either
pass: a following train behind is no concern, and an oncoming one behind is moving away. A train on the
SAME card is still counted, exactly as before — which of two trains sharing a card is in front is
`entryConflict`'s region question. The other two rulings in the save (moves 37 and 530) were correct and
are unchanged.
A test had the fault written into it: its "train ahead" of an eastbound train was the first Mainline card
in the Division, which is west of the Office. It now stands on a card the train would follow, and three
new cases pin the rule: a same-direction train behind is not put to the Superintendent, an opposite-
direction train behind does not bar the departure, and a same-direction train ahead still is. The tally
test's seed moved from 42 to 44, because the collision seed 42 was chosen for was this bug.
### Games in progress — MANY WILL NOT RESUME
**This release changes when the engine asks for a ruling**, so a save holding a ruling it would no longer
ask for stops replaying at that move: `mainline.clearance` is refused with `NO_PENDING_DECISION`. The
server then refuses to resume that game and logs the move it stopped at, leaving the save untouched —
putting v0.8.0.9 back would resume it. **Measured:** 40 four-seat co-op games recorded by bots under
v0.8.0.9 and replayed under this release — 28 stop, usually 5–40% of the way in, most at a ruling on a
train behind (about ten rulings a game were being asked). The exported playtest game stops at move 78.
**Jesse's call (2026-09-15): no games in progress need keeping, so the fix ships as it is,** with no
per-game switch preserving the old rule.
### Six more from the same playtest (Gitea#27–#32)
**A Realignment says which card it converted (#27).** *"It stated Mainline card 3 converted to plains. It
should state that the mainline card 3 curves was converted to plains."* The event carried only what the
card became, so the line could not name what it had been; `mainlineModified` now carries `from` as well —
events are derived by replaying a save and never stored, so widening one strands nothing — and it reads
"Realignment: Mainline card 3, Curves, converted to Plains".
**And the map flashes it (#28).** *"Is it possible to flash the mainline card when it gets changed by
realignment?"* The one play that changes the Division itself was invisible on the map of it.
`changedDivisionCards` compares the two public boards the animation queue already holds — the same way a
pile is lit — so the pulse lands with the step that shows the change rather than when the intent arrived,
and it is shown to the player who made it too, unlike a lit pile. `prefers-reduced-motion` turns it off.
**The history stops running ahead of the board (#29).** *"Does history display immediately for all bot
turns... is it possible to stall history so it stays in sync with the number behind?"* A push carries its
narration and its steps together, so every line of a bot's turn was in the panel before the board had
drawn any of it. The queue now reports how many lines belong to steps not yet shown, and the panel holds
back exactly those, revealing each as its step goes up. Skip still shows everything at once.
**A ruling reads as the office's (#30).** *"If a player makes a move as a superintendent instead of as
themselves maybe it could say 'Superintendent Player Tom'."* The three moves made by holding the office
rather than in turn — §8.1's clearance, §11's Yard Office offer, §Q's Red Flag prompt — are prefixed that
way. `clearanceGiven` carries no player at all (the office made it, whoever holds it), so the acting seat
is what names it.
**And it names them once (#31).** *"It gives the player's name and then their player number together."*
`record()` prefixes the acting player's NAME for every event carrying `player`, and five narration lines
embedded `Player <index>` themselves — phase ended, actor changed, the Red Flag ruling, the extension vote
and the Yard Office ruling. The name is the log's job; the sentence is the narration's. A test now scans a
played game's whole log for a bare player index.
**A multiplayer game can be saved as a file (#32).** *"In the StartOS actions for the save game, I get a
string I can copy. Most of the time, I want to just save it as a JSON file."* Not possible in the action
itself: an action result member is text only — copyable, QR or masked, with nested groups — and the SDK
has no file member. So it is done where it can be: the play page's "Save replay" button was shown only in
solitaire, because a server-backed session has no local save to hand it. `GET /api/save?token=…` — the
same seat token `/api/stream` and `/api/intent` use, not the administrative secret — hands a seated player
their own game, and the button now appears in multiplayer.
### "Waiting on" follows the move on screen (Gitea#25)
*"Playing against 3 bots — waiting on always says me, even when it is someone else's turn."*
The server plays every bot move the moment a human's turn ends, so the LIVE game is almost always waiting
on the human — while the screen is still replaying those bots step by step. The turn chart and the
Division map's move marker read the live actor, and contradicted the playback row naming the bot actually
moving. `actorOnScreen` (`web/step-queue.ts`) answers with the player of the step on screen while the
board is catching up, and the live actor once it has; during playback the chart also leaves off a live
"asks … ruling" note that belongs to a position the screen has not reached. Five tests pin it.
### The name on the Division map is readable again (Gitea#24)
*"When it shows your player, the font is unreadable... the bold font makes it look fuzzy, and the letters
blur together."*
A name takes the class `bs-turn` while it is that player's move — and `.bs-turn` is also the Division
map's turn ARROW, which strokes its shape 2.4px grey with no fill. The name's own rule changed only the
fill, so every letter was outlined in grey. It showed for the viewing player almost constantly because of
Gitea#25: the map believed it was nearly always their move. `.bs-name` now sets `stroke:none`.
---
## 0.8.0.9 — 2026-09-15
**The developer bot, re-measured decision by decision — and an engine 2.8× faster.** Across the changes