v0.7.9.7 — a device that said it was available all day after it was gone

The last item off 0.7.9.6's sweep, parked there as the one genuine maybe.
It had a second half worth more than the first.

#101 — Telegraph (+4), Telephone (+8) and Radio (+12) are "once a day,
when dispatching facing trains, add +N to the other train's number".
`enhancementText(key)` takes only the KEY, so the tooltip could not vary
with anything: a spent Radio read "Once a day, add +12..." for the rest
of the Day, advertising a bonus that was not there. That is `trainRules`
before #100, in another corner of the same view.

THE HALF THAT ACTUALLY SURPRISES. `spendDispatchBonus` reads
`areaOf(s, s.clock.superintendent)` — the SUPERINTENDENT's own devices,
not the train owner's — and the Fedora moves every STAGES_PER_SHIFT (3)
Stages, four times a Day. So a player's Radio does nothing at all for
three-quarters of the Day, and is spent automatically, without its owner
being asked, during the quarter it is theirs. Neither half was anywhere
on the board.

The card now reads as one of three states — available and dispatching,
unspent but idle while somebody else holds the Fedora, or spent until the
next Day — and names the shift length, because "not now" without "for how
long" is half an answer. A spent device is struck through on the board.
Shown on EVERY district, not only the viewer's (Jesse's call): it is
public, and a rival's spent Radio is what you want to know before forcing
a meet.

What counts as a device is `enhancementRule(key)?.dispatchBonus` rather
than three keys written out in the view — the ladder lives in
ENHANCEMENT_RULES and a fourth rung would otherwise be silently exempt.

A STALE COMMENT CORRECTED, AND PINNED. `advance.ts` warned that indexing
a SEAT-keyed area with the PLAYER holding the Fedora "is right only while
seating is the identity map". It read as a live Employee Rotation bug and
was not one: `areaOf(s, p)` IS `areaAtSeat(s, seatOf(s, p))`. A comment
that sends the next reader chasing a bug that does not exist costs about
what the bug would. Rewritten, and the claim is now a test — seating set
to a real permutation, and the Superintendent's own district rather than
the seat with the same index is the one that reads as dispatching.

TWO THINGS MUTATION CAUGHT THAT PASSING DID NOT. A test asserted the
ABSENCE of /spent|Fedora/ with the Fedora held, and a mutant with the
`dispatchBonus` guard deleted PASSED it — the leaked text in that case
says "Available today, and this district is dispatching", which contains
neither word. A test that something was left alone has to compare it
against what it should be, so it asserts equality with `enhancementText`
now, in both Fedora states. And the replay wire format needed the field:
cells pack positionally, so the flag is index 9 and reads `?? []`, the
same tolerance `standingWest` uses — older recordings report no device
spent, which is what they drew at the time, so every published replay is
unchanged.

943 tests pass, up from 934.

NOT VERIFIED AT A TABLE, like 0.7.9.6. #39 and #35 still stand.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y5boPxP6JHRYMm8adXaF5R
This commit is contained in:
Jesse.Markowitz
2026-09-07 21:54:18 -04:00
co-authored by Claude Opus 5
parent 7f4e027258
commit 88a42ae9e2
8 changed files with 332 additions and 11 deletions
+133 -1
View File
@@ -20,7 +20,7 @@ import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { createGame } from '../src/engine/setup.ts';
import { crewTrayCount } from '../src/engine/content.ts';
import { crewTrayCount, enhancementText } from '../src/engine/content.ts';
import { areaOf } from '../src/engine/apply.ts';
import { impediments } from '../src/sim/narrate.ts';
import { projectDistrict, projectSharedTable, publicSnapshot, trainRules } from '../src/sim/view.ts';
@@ -256,3 +256,135 @@ describe('the Campaign Train says whether its speeches are made (#100)', () => {
assert.doesNotMatch(trainRules({ trainNumber: 5, trainIsExtra: false }), /SPEECHES/i);
});
});
/**
* A DISPATCH DEVICE SAYS WHETHER IT IS STILL AVAILABLE TODAY (#101).
*
* Telegraph (+4), Telephone (+8) and Radio (+12) are "once a day, when dispatching facing trains,
* add +N to the other train's number". The device is drawn on its card and its effect text is in the
* tooltip — but `enhancementText(key)` takes only the KEY, so it could not vary with anything, and
* the Radio read "once a day, add +12…" all Day after it had been spent. That is `trainRules` before
* #100, in a different corner of the same view.
*
* THE SECOND HALF IS THE ONE THAT SURPRISES. `spendDispatchBonus` reads
* `areaOf(s, s.clock.superintendent)` — the SUPERINTENDENT's own devices, not the train owner's —
* and the Fedora moves every `STAGES_PER_SHIFT` Stages, four times a Day. So a player's Radio does
* nothing at all for three-quarters of the Day, and is spent automatically, without being asked,
* during the quarter it is theirs to use. The board said neither half.
*
* Shown on EVERY district (Jesse, 2026-09-07), not only the viewer's: it is public, and a rival's
* spent Radio is exactly what you want to know before forcing a meet. `projectDistrict` serves both
* the player's own cells and the common board's `districts`, so one change covers both.
*/
describe('a dispatch device says whether it is still available today (#101)', () => {
/** Puts `key` on the first card of seat 0's district and returns the pieces to assert on. */
const withDevice = (key: string, opts: { spent?: boolean; fedora?: boolean } = {}) => {
const s = game();
const area = areaOf(s, 0 as PlayerIndex);
const card = [...area.grid.values()][0]!;
card.enhancements.push(key);
if (opts.spent) area.dispatchUsedToday.push(key);
// The Fedora is a PLAYER; give it to somebody whose seat is not this district's.
s.clock.superintendent = (opts.fedora ? 0 : 1) as PlayerIndex;
const seat = seatOf(s, 0 as PlayerIndex);
const cells = projectDistrict(s, seat as SeatIndex).cells;
const cell = cells.find((c) => c.enhancements.length > 0)!;
const i = cell.enhancementsWhat.findIndex((w) => new RegExp(key, 'i').test(w));
return { s, area, cell, what: cell.enhancementsWhat[i] ?? '', spent: cell.enhancementsSpent[i] };
};
it('reads as available before it is used, and says what it is worth', () => {
const { what, spent } = withDevice('radio', { fedora: true });
assert.equal(spent, false, 'an unused device reported itself spent');
assert.match(what, /\+12/, 'the device no longer says what it is worth');
assert.doesNotMatch(what, /spent/i, 'an unused device claimed it had been spent');
});
it('says so once it has been spent, and says when it comes back', () => {
const { what, spent } = withDevice('radio', { spent: true, fedora: true });
assert.equal(spent, true, 'a spent device still reported itself available');
assert.match(what, /spent/i, 'a spent device read exactly as it did before it was spent');
assert.match(what, /next Day|tomorrow/i, 'it does not say the device comes back');
});
it('reads differently spent and unspent — the whole of the bug was that it did not', () => {
assert.notEqual(
withDevice('telegraph', { fedora: true }).what,
withDevice('telegraph', { spent: true, fedora: true }).what,
);
});
it('says a device is idle while somebody else holds the Fedora', () => {
const { what } = withDevice('telephone');
assert.match(what, /Fedora|Superintendent/i, 'nothing said the device is only used while dispatching');
});
it('does not say that when this district IS the Superintendent', () => {
const { what } = withDevice('telephone', { fedora: true });
assert.doesNotMatch(what, /while .*holds? the Fedora/i);
});
/**
* EXACT equality, not "does not mention the Fedora". The first draft asserted the absence of
* /spent|Fedora/i with the Fedora held, and a mutation that removed the `dispatchBonus` guard
* altogether PASSED it — because the leaked text in that case reads "Available today, and this
* district is dispatching", which contains neither word. A test for a field being left alone has
* to compare it to what it should be.
*/
it('leaves an enhancement that is not a dispatch device exactly as it was', () => {
for (const fedora of [true, false]) {
const { what, spent } = withDevice('interlocking', { fedora });
assert.equal(spent, false, 'a non-dispatch enhancement was marked spendable');
assert.equal(what, enhancementText('interlocking'), 'an Interlocking was given a dispatch caveat');
}
});
it('comes back when the Day turns, which is what clears the record', () => {
const { s, area } = withDevice('radio', { spent: true, fedora: true });
area.dispatchUsedToday = [];
const cell = projectDistrict(s, seatOf(s, 0 as PlayerIndex) as SeatIndex).cells.find(
(c) => c.enhancements.length > 0,
)!;
assert.equal(cell.enhancementsSpent.some((x) => x), false, 'a new Day did not restore the device');
});
/**
* SEAT IS NOT PLAYER INDEX, and this is the one place the two are joined: the Fedora is held by a
* PLAYER and the devices sit in an Office Area keyed by SEAT. `advance.ts` carried a comment
* warning that indexing one with the other was safe only while seating was the identity map — it
* read as a live bug and was not one, because `areaOf` resolves through `seatOf`. The comment is
* corrected; this is the guard, because the next reader deserves better than a claim.
*
* §4.4's D12 makes seating a real permutation, so a two-player game where player 1 sits in seat 0
* is ordinary rather than contrived.
*/
it("marks the SUPERINTENDENT's own district as dispatching under non-identity seating", () => {
const s = game();
s.seating = [1, 0] as PlayerIndex[];
assert.notEqual(seatOf(s, 0 as PlayerIndex), 0, 'the premise is gone: seating is still identity');
s.clock.superintendent = 0 as PlayerIndex;
const mine = areaOf(s, 0 as PlayerIndex);
[...mine.grid.values()][0]!.enhancements.push('radio');
const other = areaOf(s, 1 as PlayerIndex);
[...other.grid.values()][0]!.enhancements.push('radio');
const whatAt = (player: PlayerIndex): string => {
const cells = projectDistrict(s, seatOf(s, player) as SeatIndex).cells;
return cells.find((c) => c.enhancements.length > 0)!.enhancementsWhat.join(' ');
};
// The Fedora is player 0's, whatever seat that is.
assert.doesNotMatch(whatAt(0 as PlayerIndex), /IDLE/, "the Superintendent's own device read as idle");
assert.match(whatAt(1 as PlayerIndex), /IDLE/, "somebody else's device read as dispatching");
});
it('is on every district of the common board, not only the viewer own', () => {
const { s } = withDevice('radio', { spent: true, fedora: true });
const pub = publicSnapshot(s);
const all = pub.districts.flatMap((d) => d.cells);
assert.ok(
all.some((c) => c.enhancementsSpent.some((x) => x)),
"a spectator cannot see which devices are spent in a player's district",
);
});
});