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:
co-authored by
Claude Opus 5
parent
7f4e027258
commit
88a42ae9e2
+58
-2
@@ -35,6 +35,7 @@ import {
|
||||
REALIGNMENTS,
|
||||
OFFICE_ORDER,
|
||||
SPACE_USE_CARDS,
|
||||
STAGES_PER_SHIFT,
|
||||
crewTrayCount,
|
||||
enhancementRule,
|
||||
enhancementText,
|
||||
@@ -47,7 +48,7 @@ import {
|
||||
mainlineDescription,
|
||||
} from '../engine/content.ts';
|
||||
import type { Intent } from '../engine/intents.ts';
|
||||
import type { Facility, GameConfig, GameState, PlayerIndex, SeatIndex, TrackCard, TurnoutOrientation } from '../engine/state.ts';
|
||||
import type { Facility, GameConfig, GameState, OfficeArea, PlayerIndex, SeatIndex, TrackCard, TurnoutOrientation } from '../engine/state.ts';
|
||||
import { actingPlayer, carsOn, overHandLimit, playerAtSeat, railFacingOf, seatOf, turnOf } from '../engine/state.ts';
|
||||
import type { Direction, Hand, HouseRules, TrackGeometry } from '../engine/content.ts';
|
||||
import type { Port } from '../engine/track.ts';
|
||||
@@ -74,6 +75,14 @@ export type CellView = {
|
||||
* `toString()`), so it cannot reach the card catalogue itself.
|
||||
*/
|
||||
enhancementsWhat: string[];
|
||||
/**
|
||||
* Which of those enhancements is SPENT for today, in the same order (#101).
|
||||
*
|
||||
* Only ever true of a dispatch device — Telegraph, Telephone, Radio — which is "once a day". The
|
||||
* reason and the Fedora caveat are already written into `enhancementsWhat`; this is the flag the
|
||||
* board styles from, because `board-svg.ts` imports nothing and cannot work it out for itself.
|
||||
*/
|
||||
enhancementsSpent: boolean[];
|
||||
/**
|
||||
* EVERY TRAIN STANDING HERE, in order, each with the engine in it and which way it points.
|
||||
*
|
||||
@@ -1253,6 +1262,52 @@ export function describeIntent(s: GameState, i: Intent): string {
|
||||
* made it. For a player's own view that seat is theirs; for a spectator's view of district N it is
|
||||
* N, which is the same fact asked from outside.
|
||||
*/
|
||||
/**
|
||||
* WHAT AN ENHANCEMENT DOES — AND WHETHER IT CAN DO IT RIGHT NOW (#101).
|
||||
*
|
||||
* `enhancementText(key)` takes only the key, so it says the same thing for ever. That is right for
|
||||
* every enhancement except the three dispatch devices, which are "once a day": a spent Radio read
|
||||
* "Once a day, add +12…" all Day after it was gone, which is `trainRules` before #100 in a different
|
||||
* corner of the same view.
|
||||
*
|
||||
* AND THE FEDORA, which is the half that actually surprises. `spendDispatchBonus` (advance.ts) reads
|
||||
* the SUPERINTENDENT's own devices, not the train owner's, and the Fedora moves every
|
||||
* `STAGES_PER_SHIFT` Stages — so a device does nothing at all while somebody else is dispatching,
|
||||
* and is spent automatically, without its owner being asked, while they are.
|
||||
*
|
||||
* `dispatchBonus` decides what counts as a device, rather than a list of three keys written out
|
||||
* here: the ladder lives in `ENHANCEMENT_RULES` and a fourth rung would otherwise be silently
|
||||
* exempt.
|
||||
*/
|
||||
function enhancementState(
|
||||
s: GameState,
|
||||
area: OfficeArea,
|
||||
seat: SeatIndex,
|
||||
key: string,
|
||||
): { what: string; spent: boolean } {
|
||||
const base = enhancementText(key) ?? prettyKey(key);
|
||||
if (enhancementRule(key)?.dispatchBonus === undefined) return { what: base, spent: false };
|
||||
|
||||
const spent = area.dispatchUsedToday.includes(key);
|
||||
if (spent) {
|
||||
return {
|
||||
what: `${base} SPENT for today — it comes back at the start of the next Day.`,
|
||||
spent: true,
|
||||
};
|
||||
}
|
||||
// Available, but only to whoever is dispatching. Naming the shift length is the difference
|
||||
// between "not now" and knowing how long "not now" lasts.
|
||||
if (seatOf(s, s.clock.superintendent) !== seat) {
|
||||
return {
|
||||
what:
|
||||
`${base} Unspent, but IDLE: a device is only used by the district holding the Fedora, ` +
|
||||
`which moves every ${STAGES_PER_SHIFT} Stages.`,
|
||||
spent: false,
|
||||
};
|
||||
}
|
||||
return { what: `${base} Available today, and this district is dispatching.`, spent: false };
|
||||
}
|
||||
|
||||
export function projectDistrict(
|
||||
s: GameState,
|
||||
seat: SeatIndex,
|
||||
@@ -1287,7 +1342,8 @@ export function projectDistrict(
|
||||
what: cellDescription(card, officeProfile(area.tier).name, row === area.runningRow),
|
||||
links: connectionsFor(card).map(([a, b]) => `${a}${b}`),
|
||||
enhancements: card.enhancements.map(prettyKey),
|
||||
enhancementsWhat: card.enhancements.map((k) => enhancementText(k) ?? prettyKey(k)),
|
||||
enhancementsWhat: card.enhancements.map((k) => enhancementState(s, area, seat, k).what),
|
||||
enhancementsSpent: card.enhancements.map((k) => enhancementState(s, area, seat, k).spent),
|
||||
trains: trainsOnCard(s, seat, key),
|
||||
adTracks: card.geometry.kind === 'office' ? officeProfile(area.tier).adTracks : null,
|
||||
cars: carsOn(card).map((c) => carLabel(c, seat)),
|
||||
|
||||
Reference in New Issue
Block a user