Credibility pass: session residue swept from the rev-10 run
War-story comments become timeless constraints (carrier rule, boobytrap tell, remove-curse prescription); the rev changelog reunites under GameConfig.deckRev; three hand-copied cardless stack literals fold into openCardlessStack; the visionstone one-edge loop is shared via throughOneEdge with the edge-target hairpin exclusion stated; drain() moves to test helpers and replaces four inline copies; the butt-head sweep test loses its dead scaffolding and its expect(refused || true) tautology and now asserts both branches; a process-named suite is renamed for the behavior it pins; dead .bezel-spacer CSS and the fx effect's fossilized cleanup story go. No behavior changes: 304 engine tests pass, 36/36 ledgers replay, web typecheck clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015RCWSTnb1KYTPyL4GmhGnF
This commit is contained in:
co-authored by
Claude Fable 5
parent
f2af1e26f8
commit
e93bcc15a2
@@ -1070,9 +1070,8 @@ function selfCare(view: GameView, style: AutomatonStyle, tier: TierTraits): Comm
|
||||
}
|
||||
}
|
||||
if (!tier.buffs) return null; // the apprentice's book ends at the stones
|
||||
// A curse on the clockwork gets scrubbed off.
|
||||
// The engine wants the whole prescription — patient and ailment both —
|
||||
// or the cast is refused and the fallback reads as an idle bot.
|
||||
// A curse on the clockwork gets scrubbed off. The engine wants the
|
||||
// whole prescription — patient and ailment both — or it refuses the cast.
|
||||
const myCurse = view.sustained.find(
|
||||
(e) => e.targetId === view.you && e.casterId !== view.you && AFFLICTIONS[e.cardId] != null,
|
||||
);
|
||||
@@ -1594,8 +1593,7 @@ export function automatonCommand(
|
||||
const strikeLive = !view.turn.attackUsed && !view.turn.attackForbidden &&
|
||||
!view.turn.actionsEnded && view.yourHand.some((c) => ATTACKS[c.cardId] != null);
|
||||
// A carrier never chases: the delivery in hand outranks the gold in
|
||||
// someone else's. Chasing while laden once marched a bot across the
|
||||
// whole board, four steps from a winning drop.
|
||||
// someone else's.
|
||||
const chasing = thief !== null && strikeLive && !self.carriedTreasureId;
|
||||
// BANK GUARD: enemy gold delivered to my home is my scoreboard, and
|
||||
// anyone may snatch it off the floor. A raider nearer my bank than I
|
||||
|
||||
+56
-60
@@ -232,17 +232,7 @@ export interface CastParams {
|
||||
hold?: boolean;
|
||||
}
|
||||
|
||||
/** The revision new games are dealt under; GameConfig.deckRev pins it per game.
|
||||
* Rev 6: STRENGTH's treasure-tear opens a counteraction window ("this would
|
||||
* be an attack") instead of resolving instantly.
|
||||
* Rev 7: DISEASE is a self-cast plague ("You're the carrier!") — the caster
|
||||
* carries it, sharing a square bites both directions, no counteraction.
|
||||
* Rev 8: the fire imp's scorch is a SPELL (FAQ) — counteractable, magical —
|
||||
* and SOULSTONE's floor holds even at 3 life or below.
|
||||
* Rev 9: the WARD's bite is counteractable ("COUNTERACTIONs ... otherwise
|
||||
* work as written"), though nothing a counter does touches its caster.
|
||||
* Rev 10: BUTT-HEAD's ram is MOVEMENT — the charge walks real corridors on
|
||||
* the turn's legal legs (three plus numbers) and spends them. */
|
||||
/** The revision new games are dealt under; GameConfig.deckRev pins it per game. */
|
||||
export const CURRENT_RULES_REV = 10;
|
||||
|
||||
export interface GameConfig {
|
||||
@@ -267,6 +257,19 @@ export interface GameConfig {
|
||||
* it; unpassed, it relocks at turn's end as before.
|
||||
* Rev 5: MENTAL FORCE refuses a destination the victim cannot walk to
|
||||
* in three spaces, instead of eating the card silently at resolution.
|
||||
* Rev 6: STRENGTH's treasure-tear opens a counteraction window ("this
|
||||
* would be an attack") instead of resolving instantly.
|
||||
* Rev 7: DISEASE is a self-cast plague ("You're the carrier!") — the
|
||||
* caster carries it, sharing a square bites both directions, no
|
||||
* counteraction.
|
||||
* Rev 8: the fire imp's scorch is a SPELL (FAQ) — counteractable,
|
||||
* magical — and SOULSTONE's floor holds even at 3 life or below.
|
||||
* Rev 9: the WARD's bite is counteractable ("COUNTERACTIONs ...
|
||||
* otherwise work as written"), though nothing a counter does touches
|
||||
* its caster.
|
||||
* Rev 10: BUTT-HEAD's ram is MOVEMENT — the charge walks real
|
||||
* corridors on the turn's legal legs (three plus numbers) and spends
|
||||
* them.
|
||||
*/
|
||||
deckRev?: number;
|
||||
}
|
||||
@@ -402,21 +405,25 @@ function doorsAjar(state: GameState, viewerId: PlayerId | undefined, board: Asse
|
||||
return edges ? { ...board, edges } : board;
|
||||
}
|
||||
|
||||
/** LOS including square-filling blockers. */
|
||||
/** Sight granted by removing any ONE closed edge — the VISIONSTONE's
|
||||
* whole power, shared by every path that honors it. */
|
||||
function sightThroughOneEdge(
|
||||
board: AssembledBoard, from: Cell, to: Cell, blockers: Record<string, true>,
|
||||
): boolean {
|
||||
* whole power, shared by every path that honors it. `sees` judges the
|
||||
* board with the candidate edge gone. */
|
||||
function throughOneEdge(board: AssembledBoard, sees: (b: AssembledBoard) => boolean): boolean {
|
||||
for (const key of Object.keys(board.edges)) {
|
||||
if ((board.edges[key] ?? "open") === "open") continue;
|
||||
const edges = { ...board.edges };
|
||||
delete edges[key];
|
||||
if (sightBetween({ ...board, edges }, from, to, blockers)) return true;
|
||||
if (sees({ ...board, edges })) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function sightThroughOneEdge(
|
||||
board: AssembledBoard, from: Cell, to: Cell, blockers: Record<string, true>,
|
||||
): boolean {
|
||||
return throughOneEdge(board, (b) => sightBetween(b, from, to, blockers));
|
||||
}
|
||||
|
||||
function losWith(state: GameState, from: Cell, to: Cell, viewerId: PlayerId | undefined, stone: boolean): boolean {
|
||||
const board = doorsAjar(state, viewerId, openedDoors(state, boardView(state)));
|
||||
const blockers = losBlockers(state);
|
||||
@@ -539,15 +546,13 @@ function castSightEdge(
|
||||
side: Side,
|
||||
): boolean {
|
||||
if (losToEdge(board, caster.position, cell, side)) return true;
|
||||
if (displays(caster, "visionstone")) {
|
||||
for (const [key, edge] of Object.entries(board.edges)) {
|
||||
if (edge === "open") continue;
|
||||
const edges = { ...board.edges };
|
||||
delete edges[key];
|
||||
if (losToEdge({ ...board, edges }, caster.position, cell, side)) return true;
|
||||
}
|
||||
if (displays(caster, "visionstone") &&
|
||||
throughOneEdge(board, (b) => losToEdge(b, caster.position, cell, side))) {
|
||||
return true;
|
||||
}
|
||||
if (!cmd.aroundCornerInstanceId) return false;
|
||||
// The bend pivots in a seen middle SQUARE; the gap-pivot hairpin is
|
||||
// square-to-square geometry, so an edge target gets no 180-degree bend.
|
||||
for (const key of Object.keys(board.cells)) {
|
||||
const [mx, my] = key.split(",").map(Number) as [number, number];
|
||||
const mid = { x: mx, y: my };
|
||||
@@ -2016,7 +2021,7 @@ const CARD_EFFECTS: Record<string, AttackEffect | NeutralEffect | CounterEffect>
|
||||
// The FIRST cell in the command is the real trap — but that truth
|
||||
// lives only in realKey. Stored and broadcast in canonical order,
|
||||
// the tokens carry no tell: position must never whisper which one
|
||||
// bites (the placement order once did, to anyone reading events).
|
||||
// bites.
|
||||
const laid = [...cells].sort((a, b) => cellKey(a).localeCompare(cellKey(b)));
|
||||
state.boobytraps.push({ casterId: caster.id, cells: laid, realKey: cellKey(cells[0]!) });
|
||||
events.push({ type: "boobytrapPlaced", caster: caster.id, cells: laid.map((c) => ({ ...c })) });
|
||||
@@ -3220,21 +3225,34 @@ function openCreatureStack(
|
||||
damage: number,
|
||||
touch?: "wraith" | "claw",
|
||||
kind: "physical" | "spell" = "physical",
|
||||
): void {
|
||||
openCardlessStack(state, creature.controllerId, victim.id, {
|
||||
params: { damage },
|
||||
kind,
|
||||
creatureId: creature.id,
|
||||
...(touch ? { creatureTouch: touch } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
/** A counteraction stack with no attack card behind it — a creature's
|
||||
* blow, the WARD's bite, STRENGTH's tear. Counters ride it as usual. */
|
||||
function openCardlessStack(
|
||||
state: GameState,
|
||||
attackerId: PlayerId,
|
||||
defenderId: PlayerId,
|
||||
opts: Pick<CastStack, "params" | "kind"> & Partial<CastStack>,
|
||||
): void {
|
||||
state.stack = {
|
||||
attackerId: creature.controllerId,
|
||||
defenderId: victim.id,
|
||||
attackerId,
|
||||
defenderId,
|
||||
attackCard: null,
|
||||
numberValue: null,
|
||||
amplifyFactor: 1,
|
||||
extendFactor: 1,
|
||||
powerAttackPoints: 0,
|
||||
params: { damage },
|
||||
kind,
|
||||
counters: [],
|
||||
waitingOn: victim.id,
|
||||
creatureId: creature.id,
|
||||
...(touch ? { creatureTouch: touch } : {}),
|
||||
waitingOn: defenderId,
|
||||
...opts,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4608,20 +4626,9 @@ function doWardChoice(prev: GameState, play: boolean): CommandResult {
|
||||
// nothing a counter does can touch the Ward's caster. Older games
|
||||
// recorded the instant bite and replay it.
|
||||
if ((state.config.deckRev ?? 1) >= 9) {
|
||||
state.stack = {
|
||||
attackerId: owner.id,
|
||||
defenderId: taker.id,
|
||||
attackCard: null,
|
||||
numberValue: null,
|
||||
amplifyFactor: 1,
|
||||
extendFactor: 1,
|
||||
powerAttackPoints: 0,
|
||||
params: { damage: 3 },
|
||||
kind: "spell",
|
||||
counters: [],
|
||||
waitingOn: taker.id,
|
||||
trapped: true,
|
||||
};
|
||||
openCardlessStack(state, owner.id, taker.id, {
|
||||
params: { damage: 3 }, kind: "spell", trapped: true,
|
||||
});
|
||||
} else {
|
||||
applyDamage(state, events, taker, 3, "warded treasure", null);
|
||||
checkVictory(state, events);
|
||||
@@ -4746,20 +4753,9 @@ function doTearTreasure(prev: GameState, targetId: PlayerId): CommandResult {
|
||||
// Rev 6: "this would be an attack" — the grab opens a counteraction
|
||||
// window like any punch; the clutch roll waits for the counters.
|
||||
if ((state.config.deckRev ?? 1) >= 6) {
|
||||
state.stack = {
|
||||
attackerId: attacker.id,
|
||||
defenderId: target.id,
|
||||
attackCard: null,
|
||||
numberValue: null,
|
||||
amplifyFactor: 1,
|
||||
extendFactor: 1,
|
||||
powerAttackPoints: 0,
|
||||
params: null,
|
||||
kind: "physical",
|
||||
counters: [],
|
||||
waitingOn: target.id,
|
||||
tearTreasure: true,
|
||||
};
|
||||
openCardlessStack(state, attacker.id, target.id, {
|
||||
params: null, kind: "physical", tearTreasure: true,
|
||||
});
|
||||
events.push({ type: "treasureTearAttempted", attacker: attacker.id, defender: target.id });
|
||||
return { ok: true, state, events };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user