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
@@ -980,7 +980,7 @@ describe("the denial planner offers only castable blocks", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("game-six lessons from the table", () => {
|
||||
describe("interception, escape, and hazard sense", () => {
|
||||
function toBot(state: GameState): GameState {
|
||||
while (state.turn.round < 2 || state.players[state.turn.activeIndex]!.id !== "bot") {
|
||||
const r = applyCommand(state, actingSeat(state), { type: "endTurn", draw: 0 });
|
||||
|
||||
@@ -3,7 +3,7 @@ import { applyCommand, activePlayer, boardView, createGame, type GameState } fro
|
||||
import { cellKey, edgeKey, hasLineOfSight, neighbor, type Cell, type Side, SIDES, stepTarget } from "../src/board";
|
||||
import type { CardInstance } from "../src/cards";
|
||||
import { sightedCellsFor, stackSightTrace, viewFor } from "../src/view";
|
||||
import { newGame, must, giveCard, toRound2, faceOff } from "./helpers";
|
||||
import { newGame, must, drain, giveCard, toRound2, faceOff } from "./helpers";
|
||||
|
||||
/** Test surgery: put a specific card into a player's hand (swapping one out). */
|
||||
/** Advance past round 1 (both players just end their turns). */
|
||||
@@ -398,7 +398,7 @@ describe("the ward window and chaos shields", () => {
|
||||
expect(state.wardPending).toEqual({ ownerId: owner.id, takerId: thief.id });
|
||||
// Spring it: the thief bleeds 3 and the ward is spent.
|
||||
state = must(state, owner.id, { type: "wardChoice", play: true });
|
||||
while (state.stack) state = must(state, state.stack.waitingOn, { type: "pass" });
|
||||
state = drain(state);
|
||||
expect(state.players.find((p) => p.id === thief.id)!.life).toBe(12);
|
||||
expect(state.players.find((p) => p.id === owner.id)!.hand.some((c) => c.cardId === "ward")).toBe(false);
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
type CreatureState, applyCommand, activePlayer, boardView, creatureAt, type GameState, type PlayerId, createGame } from "../src/game";
|
||||
import { cellKey, edgeKey, SIDES, stepTarget, type Cell, type Side } from "../src/board";
|
||||
import type { CardInstance } from "../src/cards";
|
||||
import { newExpansionGame as newGame, must, giveCard, toRound2, emptyNeighborCell, pushSustained } from "./helpers";
|
||||
import { newExpansionGame as newGame, must, drain, giveCard, toRound2, emptyNeighborCell, pushSustained } from "./helpers";
|
||||
|
||||
/** Summon a creature next to its creator (round 2+, consumes the attack). */
|
||||
function summon(state: GameState, playerId: PlayerId, kind: string, tag = "S") {
|
||||
@@ -146,14 +146,6 @@ describe("monsters", () => {
|
||||
expect(bitten.hand.length).toBe(6);
|
||||
});
|
||||
|
||||
/** Rev 8: scorches open counteraction windows — wave them through. */
|
||||
function drain(state: GameState): GameState {
|
||||
while (state.stack) {
|
||||
state = must(state, state.stack.waitingOn, { type: "pass" });
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
it("the fire imp scorches on sight and dies only to water", () => {
|
||||
let { state } = newGame();
|
||||
state = toRound2(state);
|
||||
@@ -824,7 +816,7 @@ describe("the imp's fire is a spell (rev 8)", () => {
|
||||
expect(state.stack?.kind).toBe("spell");
|
||||
giveCard(state, "mark", "full-shield", "FS", 0);
|
||||
state = must(state, "mark", { type: "counteract", instanceId: "full-shield#FS" });
|
||||
while (state.stack) state = must(state, state.stack.waitingOn, { type: "pass" });
|
||||
state = drain(state);
|
||||
expect(state.players.find((p) => p.id === "mark")!.life).toBe(15);
|
||||
});
|
||||
|
||||
@@ -845,7 +837,7 @@ describe("the imp's fire is a spell (rev 8)", () => {
|
||||
state = r.state;
|
||||
if (!state.stack) state = must(state, "imp-owner", { type: "endTurn", draw: 0 });
|
||||
expect(state.stack?.kind).toBe("spell");
|
||||
while (state.stack) state = must(state, state.stack.waitingOn, { type: "pass" });
|
||||
state = drain(state);
|
||||
const after = state.players.find((p) => p.id === "mark")!;
|
||||
expect(after.life).toBe(2);
|
||||
expect(after.alive).toBe(true);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest";
|
||||
import { applyCommand, activePlayer, boardView, createGame, gameLos, sustainedOn } from "../src/game";
|
||||
import { cellKey, edgeKey, stepTarget } from "../src/board";
|
||||
import type { CardInstance } from "../src/cards";
|
||||
import { newExpansionGame as newGame, must, giveCard, toRound2, faceOff, castAt } from "./helpers";
|
||||
import { newExpansionGame as newGame, must, drain, giveCard, toRound2, faceOff, castAt } from "./helpers";
|
||||
|
||||
describe("expansion combat cards", () => {
|
||||
it("power attack burns life for extra damage", () => {
|
||||
@@ -116,7 +116,7 @@ describe("expansion combat cards", () => {
|
||||
state = must(state, me.id, { type: "pickUpTreasure" });
|
||||
expect(state.wardPending).toEqual({ ownerId: enemy.id, takerId: me.id });
|
||||
state = must(state, enemy.id, { type: "wardChoice", play: true });
|
||||
while (state.stack) state = must(state, state.stack.waitingOn, { type: "pass" });
|
||||
state = drain(state);
|
||||
expect(state.players.find((p) => p.id === me.id)!.life).toBe(12);
|
||||
expect(state.players.find((p) => p.id === enemy.id)!.hand.some((c) => c.cardId === "ward")).toBe(false);
|
||||
});
|
||||
@@ -684,52 +684,34 @@ describe("go away routs around walls", () => {
|
||||
});
|
||||
|
||||
describe("the goat charges on legs, not wings (rev 10)", () => {
|
||||
it("a ram beyond the turn's legal movement is refused", () => {
|
||||
it("the charge walks real corridors: in reach it spends legs, beyond them it is refused", () => {
|
||||
let { state } = newGame();
|
||||
state = toRound2(state);
|
||||
const attacker = activePlayer(state);
|
||||
const defender = state.players.find((p) => p.id !== attacker.id)!;
|
||||
// Find a cell more than 3 walked steps away and park the victim there.
|
||||
const view = boardView(state);
|
||||
let farCell: { x: number; y: number } | null = null;
|
||||
for (const key of Object.keys(view.cells)) {
|
||||
const [x, y] = key.split(",").map(Number) as [number, number];
|
||||
const r0 = applyCommand(state, attacker.id, { type: "endTurn", draw: 0 });
|
||||
void r0;
|
||||
break;
|
||||
}
|
||||
// Use the engine itself as the oracle: try every cell until one refuses.
|
||||
const bh = giveCard(state, attacker.id, "butt-head");
|
||||
let refused = false;
|
||||
for (const key of Object.keys(view.cells)) {
|
||||
// The engine is the oracle: every other square is either within the
|
||||
// turn's legal movement or beyond it, and this maze holds both kinds.
|
||||
let refusals = 0;
|
||||
let rams = 0;
|
||||
for (const key of Object.keys(boardView(state).cells)) {
|
||||
if (key === cellKey(attacker.position)) continue;
|
||||
const [x, y] = key.split(",").map(Number) as [number, number];
|
||||
defender.position = { x, y };
|
||||
const r = applyCommand(state, attacker.id, {
|
||||
type: "cast", instanceId: bh.instanceId, target: { kind: "player", playerId: defender.id },
|
||||
});
|
||||
if (!r.ok && /legs, not wings/.test(r.error)) { refused = true; break; }
|
||||
if (r.ok) {
|
||||
// In reach: the charge spends movement equal to the walked distance.
|
||||
let st = r.state;
|
||||
while (st.stack) {
|
||||
const rr = applyCommand(st, st.stack.waitingOn, { type: "pass" });
|
||||
if (!rr.ok) throw new Error(rr.error);
|
||||
st = rr.state;
|
||||
}
|
||||
const a = st.players.find((p) => p.id === attacker.id)!;
|
||||
expect(a.position).toEqual(st.players.find((p) => p.id === defender.id)!.position);
|
||||
expect(st.turn.movementUsed).toBeGreaterThan(0);
|
||||
break;
|
||||
if (!r.ok) {
|
||||
if (/legs, not wings/.test(r.error)) refusals += 1;
|
||||
continue;
|
||||
}
|
||||
const st = drain(r.state);
|
||||
const a = st.players.find((p) => p.id === attacker.id)!;
|
||||
expect(a.position).toEqual(st.players.find((p) => p.id === defender.id)!.position);
|
||||
expect(st.turn.movementUsed).toBeGreaterThan(0);
|
||||
rams += 1;
|
||||
}
|
||||
// Whichever branch ran, exercise the other with the far corner.
|
||||
if (!refused) {
|
||||
defender.position = { x: 0, y: 9 };
|
||||
const r = applyCommand(state, attacker.id, {
|
||||
type: "cast", instanceId: bh.instanceId, target: { kind: "player", playerId: defender.id },
|
||||
});
|
||||
if (!r.ok) refused = /legs, not wings/.test(r.error);
|
||||
}
|
||||
expect(refused || true).toBe(true);
|
||||
expect(refusals).toBeGreaterThan(0);
|
||||
expect(rams).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { giveCard } from "./helpers";
|
||||
import { drain, giveCard } from "./helpers";
|
||||
import { edgeKey } from "../src/board";
|
||||
import {
|
||||
applyCommand,
|
||||
@@ -288,11 +288,11 @@ describe("the Ward is played in the moment", () => {
|
||||
expect(state.wardPending).toEqual({ ownerId: "owner", takerId: "thief" });
|
||||
// Nobody else may act while it hangs.
|
||||
expect(applyCommand(state, "thief", { type: "endTurn", draw: 0 }).ok).toBe(false);
|
||||
let r = applyCommand(state, "owner", { type: "wardChoice", play: true });
|
||||
// Rev 9: the bite rides the stack — wave the counteraction window through.
|
||||
if (r.ok && r.state.stack) r = applyCommand(r.state, r.state.stack.waitingOn, { type: "pass" });
|
||||
const r = applyCommand(state, "owner", { type: "wardChoice", play: true });
|
||||
if (!r.ok) throw new Error(r.error);
|
||||
state = r.state;
|
||||
// The bite rides the stack: a counteraction window opens for the thief.
|
||||
expect(r.state.stack).toBeTruthy();
|
||||
state = drain(r.state);
|
||||
expect(state.wardPending).toBeNull();
|
||||
expect(state.players.find((p) => p.id === "thief")!.life).toBe(12);
|
||||
expect(state.players.find((p) => p.id === "owner")!.hand.some((c) => c.cardId === "ward")).toBe(false);
|
||||
|
||||
@@ -29,6 +29,14 @@ export function must(state: GameState, player: PlayerId, command: Command): Game
|
||||
return result.state;
|
||||
}
|
||||
|
||||
/** Wave every pending counteraction window through unanswered. */
|
||||
export function drain(state: GameState): GameState {
|
||||
while (state.stack) {
|
||||
state = must(state, state.stack.waitingOn, { type: "pass" });
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
export function giveCard(state: GameState, playerId: PlayerId, cardId: string, tag = "T", slot = 0): CardInstance {
|
||||
const p = state.players.find((p) => p.id === playerId)!;
|
||||
const instance = { instanceId: `${cardId}#${tag}`, cardId };
|
||||
|
||||
Reference in New Issue
Block a user