v0.7.9.4 — Gitea#20 step 1, and a Red Flag you can see
Step 1 of the common board done as its own release rather than as the first hour of 0.8.0, since both halves of it are worth having whether or not anything is ever published to a call. #95 — the public projection helpers. `projectDistrict(state, seat)`, `projectDivision(state)`, `projectSharedTable(state)`, `publicSnapshot(state)` and `currentActorOfState(state)`, with `snapshot()` REBUILT to compose from the same helpers rather than keeping a second copy of the shared table, so a player's frame and a spectator's cannot come to disagree about the clock, the phase, whose turn it is or the score. Behaviour-neutral; the 897 existing tests passing unchanged is the proof. The public view is composed UPWARD, never by calling `snapshot()` once per seat. That shortcut is the trap the plan names: `snapshot` assembles one player's view, so a public view made of player views builds every private field and then has to remember to strip it — and it defaults its viewer to player zero, so a careless spectator call would have served seat 0's hand. Districts are keyed by SEAT with the player resolved through `playerAtSeat`, because Employee Rotation moves players between districts and a board that treated seat and player index as interchangeable would relabel every district the first time anybody rotated. One plan finding is struck off rather than fixed: it warns a display reading `clock.currentActor` could highlight the wrong district during a decision. Measured over six seeds and 3,600 decision points, that field and `actingPlayer` never disagreed. `currentActorOfState` exists anyway, as one place for the next reader to ask. #91 — the redaction net, systematically. v0.7.9.2's two leaks were found by reading a plan, not by a test, which is the whole argument for this: a suite made of the leaks somebody happened to notice proves nothing about the next one. Serialise a seat's Frame, the PublicFrame a spectator gets and the narration they receive, then search all three for every opponent card id, every card name unique to one opponent's hand, the seed and any private decision or menu data — across a fresh game, a blind draw, mid-game, a pending decision, Employee Rotation before and after the seating moves, a reconnect push (a full Frame, and its own opportunity to leak) and a played-out game. And the allow-list, which is the plan's stated acceptance bar rather than the tests: every property of `publicSnapshot` is written down with its reason and compared on every run, so adding a field fails the suite until somebody has said out loud that a spectator may see it. Both v0.7.9.2 leaks were fields nobody had ever asked that question about. Proved by mutation rather than by passing: restoring the seed line fails 6 tests, restoring the blind-draw card name fails 1, adding a private field to the public projection fails 7, and making `players[]` carry hand contents instead of a count fails 5. Two false failures were worth the lesson. A card NAME is a type, not an identity — "right-hand curve" names a dozen cards and one is legitimately a cell label the moment anybody lays track, so searching for it fails on correct code, which is worse than not searching; a name is evidence only when every card bearing it is in the one hand. And a one-digit seed makes the seed check meaningless: seed 7 matched "Train 7". One item on the plan's list has no test because it has no referent — there is no secret objective in this game, `objectiveOf` deriving from `config.minCombinedRevenue` and the player's own Revenue, both public. #94 — a Red Flag standing at an Office's Limits is on the map. It is a token set out ON the board that holds the next train arriving from that side, and it was announced once in the log and drawn nowhere, so a train stops short three Stages later with its only explanation scrolled out of the panel. `DivisionView`'s office node carries `redFlag` and the map draws a staff and pennant AT THE END IT GUARDS — west on the left, east on the right — because which approach it covers is the whole of the information; a flag in the middle of the cell would say one is out and leave the reader to hover for the half that decides whether to run a train. The tooltip leads with it, ahead of everything that merely describes the cell. The third of these in a row after Gitea#21 and #22: when the engine gains something that changes what a train may do, the question to ask is where it is drawn, not whether it works. 909 tests pass, up from 897. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ss2y7FyhxkHjGj7xnUPCgY
This commit is contained in:
co-authored by
Claude Opus 5
parent
e734481d65
commit
ebd16983e2
@@ -3254,6 +3254,72 @@ describe('the Division map shows the whole route', () => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* #94 — A RED FLAG IS A THING STANDING ON THE BOARD, AND IT WAS DRAWN NOWHERE.
|
||||
*
|
||||
* `maneuver.redFlags` sets `node.redFlag` on an Office's Division node, and from then on
|
||||
* `redFlagStop` holds the next train arriving from that side until the flag is spent. It is a
|
||||
* standing token that stops trains — the same kind of object as a train or an A/D track, not a
|
||||
* transient event.
|
||||
*
|
||||
* It was announced once in the log and then existed only in the engine. The office node in
|
||||
* `DivisionView` carried no field for it and `board-svg.ts` never mentioned one, so a player who
|
||||
* set a flag out three Stages ago had nothing on screen saying it was still there, and an opponent
|
||||
* who missed the line never knew at all. Then a train stops short and the only explanation has
|
||||
* scrolled away.
|
||||
*
|
||||
* The same failure as Gitea#21 and #22: the engine is right and the screen is silent about what it
|
||||
* is acting on. Asserted on both halves, because either alone would have looked fixed — the
|
||||
* projection has to carry it AND the map has to draw it.
|
||||
*/
|
||||
describe('#94 — a Red Flag standing at the Limits is on the board and on the map', () => {
|
||||
const withFlag = (side: 'east' | 'west' | null): ReturnType<typeof snapshot> => {
|
||||
const s = createEngineGame({
|
||||
id: 'redflag',
|
||||
seed: 11,
|
||||
config: {
|
||||
mode: 'solitaire', days: 5, minCombinedRevenue: 0, maxCollisionsPerDay: 0,
|
||||
maxCollisionsTotal: 0, pvpCardsAllowed: false,
|
||||
optionalRules: { reducedVisibility: false, employeeRotation: false, emergencyToolbox: false },
|
||||
},
|
||||
playerNames: ['Solitaire'],
|
||||
});
|
||||
const office = s.division.nodes.find((n) => n.kind === 'office');
|
||||
assert.ok(office && office.kind === 'office', 'no Office node in the Division');
|
||||
// Exactly what `redFlagsSet`'s reducer does (`apply.ts`).
|
||||
if (side) (office as { redFlag?: string }).redFlag = side;
|
||||
return snapshot(s, [], null);
|
||||
};
|
||||
|
||||
it('carries the flag on the office node, and its side', () => {
|
||||
const node = withFlag('east').division.find((n) => n.kind === 'office');
|
||||
assert.ok(node, 'no office node in the Division view');
|
||||
assert.equal(
|
||||
(node as { redFlag?: string | null }).redFlag,
|
||||
'east',
|
||||
'the Division view does not carry the Red Flag standing at this Office',
|
||||
);
|
||||
});
|
||||
|
||||
it('carries nothing when no flag is out — it must not draw one by default', () => {
|
||||
const node = withFlag(null).division.find((n) => n.kind === 'office');
|
||||
const flag = (node as { redFlag?: string | null }).redFlag;
|
||||
assert.ok(flag === null || flag === undefined, `an Office with no flag reported "${flag}"`);
|
||||
});
|
||||
|
||||
it('draws it on the map, and says which side it guards', () => {
|
||||
const svg = divisionSvg(withFlag('west').division);
|
||||
assert.match(svg, /bs-flag/, 'the Red Flag is not drawn on the Division map');
|
||||
// The side is the whole of the information: a flag guards ONE approach, and a player deciding
|
||||
// whether to run a train needs to know which.
|
||||
assert.match(svg, /RED FLAG[^"]*[Ww]est/, 'the map does not say which side the flag guards');
|
||||
});
|
||||
|
||||
it('draws none when none is out', () => {
|
||||
assert.ok(!/bs-flag/.test(divisionSvg(withFlag(null).division)), 'a flag was drawn with none set');
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* GITEA#22 — and the same invariant, applied to the trains standing on a card.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user