Four things the game counted and never said, and two it said wrong

Stays in the unshipped v0.7.9. Prompted by Jesse asking the general
question after two v0.7.9 fixes turned out to be the same shape:
actingPlayer existed and the Frame threw it away, and collisionsToday /
collisionsTotal rode the Frame for three releases with nothing drawing
them. So what else is computed, serialised and sent to nobody?

THE AUDIT, done rather than guessed. Every one of Frame's 59 top-level
fields grepped for a read across the seven renderers, then the same for
Tally's 26 members. 55 of 59 are read. Four are not.

tally.unloadsBegun was visible rather than merely unused. §9.1 makes
loading and unloading the same shape — begun, then carried through —
and the results screen printed "Loads still in the pipeline" for one
side and nothing for the other, reporting half of a symmetric
mechanism. tally.cardsDiscarded was counted by the engine and listed
beside "Cards drawn" and "Cards played" without it, though Gitea#9 made
throwing a Timetabled train away a deliberate move — a player CHOICE
the game counted and never reported. Both are reported now.

viewerSeat and overHandLimit are deferred by Jesse. The second is the
fullest version of the shape: engine computes it, view.ts puts it on
the Frame, session.ts declares it on the Session interface AND
implements it twice, and the only caller in the repo is its own test.
Four layers of plumbing, no consumer. The decision when it comes is
delete-or-document, not a patch.

Fixing the two turned up a third thing: resultsHtml draws
tallyHtml(report?.tally ?? f.tally), and report is f.official, so a
finished game reports the tally frozen at the official ending rather
than the live one. The first attempt at a test overrode f.tally alone,
changed nothing on screen, and failed for a reason unrelated to the
fix.

A SHOUTED KEYWORD IS NOT A SENTENCE. `EXTRA X18 started…` attributed to
a player rendered as `Player Solitaire eXTRA X18 started…`, and the
same happened to TRAIN 1 MADE UP and COLLISION. `record` folds a
narration's opening word into the middle of a sentence and did it with
a flat charAt(0).toLowerCase(). It now folds only a sentence-cased word
— ^[A-Z][a-z], a capital followed by a lower-case letter — which also
leaves X22 Pee-Dee alone, where a naive uppercase test gets it wrong
because '2'.toUpperCase() is '2'. It had been filed under Play Balance,
where it has no business being, which is how it survived a session that
had ruled balance work out of scope.

A REPLAYED SAVE NOW NARRATES WHAT THE LIVE GAME NARRATED. fromSave's
loop called record(game, result.events) with no actor, so every
restored save, every undo (which rebuilds through fromSave) and the
replay viewer stripped the "Player X" prefix off every attributed line.
submit attributes and fromMultiplayerSave attributes; this was the one
path of three that did not. One argument, with actor already computed
on the line above.

Why it survived: nothing ever compared a fromSave-built log against a
LIVE-played one. The single log-comparing test compares undo's rebuilt
log against another fromSave-built log — and undo itself rebuilds
through fromSave — so the gap cancelled out on both sides. The suite
was green with the bug in and green with it out. The new test plays a
game, saves it, restores it and asserts the two logs are identical:
the missing direction, not a new requirement.

All three fixes were confirmed to go RED with the fix reverted before
being called done.

884 tests pass, seventeen new.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YTaNBL1jVxNqgFdjHkHoo3
This commit is contained in:
Jesse.Markowitz
2026-08-30 19:44:25 -04:00
co-authored by Claude Opus 5
parent d267f89a82
commit e62ea54259
4 changed files with 212 additions and 10 deletions
+65 -1
View File
@@ -341,6 +341,70 @@ read `#houserules`/`#gametype` were rewritten to open the card rather than delet
not verified** — there is no browser on this box, so nothing has confirmed the segmented control, the
card, or the reversed panel actually look right on screen. That wants a play session.
### Four things the game counted and never said
Prompted by Jesse asking the general question after two separate v0.7.9 fixes turned out to have the
same shape — `actingPlayer` existed and the Frame threw it away, and `collisionsToday`/`collisionsTotal`
rode the Frame for three releases with nothing drawing them. So: **what else is being computed,
serialised and sent to nobody?**
Audited rather than guessed. Every one of `Frame`'s 59 top-level fields grepped for a read across the
seven renderers, then the same for `Tally`'s 26 members. **55 of 59 are read.** The four that are not:
- **`tally.unloadsBegun`** — and this one was visible rather than merely unused. §9.1 makes loading
and unloading the same shape, begun then carried through, and the results screen printed "Loads
still in the pipeline" for one side and nothing for the other. It reported half of a symmetric
mechanism. There is an "Unloads still in the pipeline" line now.
- **`tally.cardsDiscarded`** — counted by the engine, listed beside "Cards drawn" and "Cards played"
without it. Gitea#9 made throwing a Timetabled train away a legal, deliberate move, so a discard is
a player CHOICE the game counted and never reported. It reports it now.
- **`viewerSeat`** and **`overHandLimit`** — deferred by Jesse. The second is the fullest version of
the shape: the engine computes it, `view.ts` puts it on the Frame, `session.ts` declares it on the
`Session` interface and implements it twice, and the only caller in the repo is its own test. Four
layers of plumbing with no consumer. Left alone for now; the decision when it comes is
delete-or-document, not a patch.
**Fixing the two turned up a third thing worth knowing.** `resultsHtml` draws
`tallyHtml(report?.tally ?? f.tally)`, and `report` is `f.official`, which the engine writes the
moment any game ends — so a finished game reports the tally frozen at the official ending, not the
live one. The first attempt at a test overrode `f.tally` alone, changed nothing on screen, and failed
for a reason that had nothing to do with the fix.
### A shouted keyword is not a sentence
`EXTRA X18 started…`, attributed to a player, rendered as `Player Solitaire eXTRA X18 started…`. The
same happened to `TRAIN 1 MADE UP` and `COLLISION`, which are shouted deliberately.
`record` folds a narration's opening word into the middle of a sentence — "Chose to draw" has to read
"Player Bob chose to draw" — and did it with a flat `charAt(0).toLowerCase()`. It now folds **only a
sentence-cased word**: `^[A-Z][a-z]`, a capital followed by a lower-case letter, which is an ordinary
word capitalised because it began a sentence and nothing else is. That also leaves `X22 Pee-Dee`
alone, which a naive "is the first letter uppercase?" test gets wrong, because `'2'.toUpperCase()`
is `'2'`.
**It had been filed under Play Balance**, where it has no business being — which is how it survived a
session that had explicitly ruled balance work out of scope. Found by reading the section it did not
belong to.
### A replayed save now narrates what the live game narrated
`fromSave`'s loop called `record(game, result.events)` with no `actor`, so every restored save, every
undo (which rebuilds through `fromSave`) and the replay viewer stripped the "Player X" prefix off
every attributed line. Live play attributes — `submit` passes `actor` — and so does multiplayer's
replay; this was the one path of the three that did not, which meant the same moves were described in
different words from the game that produced them. The fix is one argument, with `actor` already
computed on the line above.
**Why it survived two years of green tests, which is the part worth keeping.** Nothing ever compared a
`fromSave`-built log against a LIVE-played one. The single log-comparing test compares `undo`'s
rebuilt log against another `fromSave`-built log — and since `undo` itself rebuilds through
`fromSave`, the missing attribution cancelled out on both sides. The whole suite stayed green with the
bug in place, and stayed green after the fix too.
The new test plays a game, saves it, restores it, and asserts the two logs are identical. That is the
**missing direction**, not a new requirement. All three of this batch's fixes were confirmed to fail
with the fix reverted before being called done — the discipline the v0.7.5→v0.7.8 sequence bought.
### Three wording and layout fixes
- **The collision entries** on all three screens now read "The game ends immediately and results in
@@ -355,7 +419,7 @@ card, or the reversed panel actually look right on screen. That wants a play ses
cost, not a danger, and red would outrank the actual rules above it. The buttons say **Continue
Existing Saved Game** and **Deal New Game** rather than "Continue saved game" and "Deal".
881 tests pass, fourteen of them new; one existing test asserted the opposite of the collision ruling
884 tests pass, seventeen of them new; one existing test asserted the opposite of the collision ruling
above and says so where it was reversed.
---