The clockwork minds the pit, the curse, and the shadow (GDCN)
Kestrel's archmage berserker dug a pit at the far mouth of a warp, then tried to walk through that mouth seven times: each stride landed on its own pit with a thornbush beyond, the maze bounced it back and charged the stride, and on the seventh it rolled the 1 and fell in. Cursed with SLOW DEATH at seven life, it kept drawing two cards a turn at a point apiece, raised a SHADOW that drinks a point a turn, and bled to death in three turns. The path search now refuses a pit it cannot leap beyond — the same test the maze applies — whether the pit is a waypoint or the goal. Under SLOW DEATH the end-of-turn draw keeps four life in hand and draws nothing when it cannot; while bleeding, or at six life or less, no SHADOW is raised. Three tests pin them. The chronicle tells the bounce as what it was: "leaps the pit — nothing to land on beyond it — and teeters back", not a blind blunder into a wall; and the shadow's last drink no longer trails a zero-damage line. Brain and chronicle changes only, ungated; 83 ledgers replay. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jm2auWk6RP71CjaAb4FMoG
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
b92016db51
commit
0750fe3f21
@@ -8,7 +8,7 @@ import {
|
||||
} from "../src/game";
|
||||
import { cellKey, edgeKey } from "../src/board";
|
||||
import { sightedCellsFor, viewFor } from "../src/view";
|
||||
import { automatonCommand, automatonFallback, type AutomatonStyle, type AutomatonTier } from "../src/automaton";
|
||||
import { automatonCommand, automatonFallback, drawUnderCurses, pathToward, type AutomatonStyle, type AutomatonTier } from "../src/automaton";
|
||||
import { pushSustained } from "./helpers";
|
||||
|
||||
/** Whose input does the maze want right now? */
|
||||
@@ -1110,3 +1110,83 @@ describe("interception, escape, and hazard sense", () => {
|
||||
expect(applyCommand(state, "bot", cmd!).ok).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("the clockwork under a curse, and before a pit", () => {
|
||||
/** A two-seat game advanced to the bot's own turn in round 2. */
|
||||
function botsTurn() {
|
||||
let { state } = createGame({ playerIds: ["human", "bot"], seed: 7, sets: ["basic", "expansion1"] });
|
||||
for (let guard = 0; guard < 8; guard++) {
|
||||
if (state.turn.round >= 2 && actingSeat(state) === "bot") break;
|
||||
const r = applyCommand(state, actingSeat(state), { type: "endTurn", draw: 0 });
|
||||
if (!r.ok) throw new Error(`setup: ${r.error}`);
|
||||
state = r.state;
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
it("draws only what SLOW DEATH lets it afford", () => {
|
||||
const state = botsTurn();
|
||||
const bot = state.players.find((p) => p.id === "bot")!;
|
||||
const plain = viewFor(state, "bot");
|
||||
expect(drawUnderCurses(plain, 2)).toBe(2);
|
||||
pushSustained(state, { id: "sd", cardId: "slow-death", casterId: "human", targetId: "bot", remainingTurns: 1e9 });
|
||||
bot.life = 7;
|
||||
expect(drawUnderCurses(viewFor(state, "bot"), 2)).toBe(2);
|
||||
bot.life = 5;
|
||||
expect(drawUnderCurses(viewFor(state, "bot"), 2)).toBe(1);
|
||||
bot.life = 4;
|
||||
expect(drawUnderCurses(viewFor(state, "bot"), 2)).toBe(0);
|
||||
});
|
||||
|
||||
it("raises no SHADOW while bleeding, nor with little blood to spare", () => {
|
||||
for (const rig of [
|
||||
(s: GameState) => pushSustained(s, { id: "sd", cardId: "slow-death", casterId: "human", targetId: "bot", remainingTurns: 1e9 }),
|
||||
(s: GameState) => { s.players.find((p) => p.id === "bot")!.life = 5; },
|
||||
]) {
|
||||
let state = botsTurn();
|
||||
rig(state);
|
||||
const bot = state.players.find((p) => p.id === "bot")!;
|
||||
bot.hand = [{ instanceId: "shadow#T", cardId: "shadow" }];
|
||||
// Play the bot's whole turn: nothing it does may be the shadow.
|
||||
for (let guard = 0; guard < 30 && actingSeat(state) === "bot"; guard++) {
|
||||
const view = viewFor(state, "bot");
|
||||
const cmd = automatonCommand(view, "berserker", "archmage") ?? automatonFallback(view, "archmage");
|
||||
expect(JSON.stringify(cmd)).not.toContain("shadow#T");
|
||||
const r = applyCommand(state, "bot", cmd);
|
||||
if (!r.ok) break;
|
||||
state = r.state;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("never plans a stride into a pit it cannot leap beyond", () => {
|
||||
const state = botsTurn();
|
||||
const bot = state.players.find((p) => p.id === "bot")!;
|
||||
const view0 = viewFor(state, "bot");
|
||||
// Stand the bot at the head of a straight three-square run, dig a pit
|
||||
// in the middle square, and wall the far one with stone: the route in
|
||||
// from here is no route. With the stone gone, the leap is a road again.
|
||||
const delta = { N: { x: 0, y: -1 }, E: { x: 1, y: 0 }, S: { x: 0, y: 1 }, W: { x: -1, y: 0 } } as const;
|
||||
for (const k of Object.keys(view0.board.cells)) {
|
||||
const [x, y] = k.split(",").map(Number) as [number, number];
|
||||
for (const dir of ["N", "E", "S", "W"] as const) {
|
||||
const pit = { x: x + delta[dir].x, y: y + delta[dir].y };
|
||||
const beyond = { x: pit.x + delta[dir].x, y: pit.y + delta[dir].y };
|
||||
if (!view0.board.cells[cellKey(pit)] || !view0.board.cells[cellKey(beyond)]) continue;
|
||||
if ((view0.board.edges[edgeKey({ x, y }, dir)] ?? "open") !== "open") continue;
|
||||
if ((view0.board.edges[edgeKey(pit, dir)] ?? "open") !== "open") continue;
|
||||
if (state.squareContents[k] || state.squareContents[cellKey(pit)] || state.squareContents[cellKey(beyond)]) continue;
|
||||
bot.position = { x, y };
|
||||
state.squareContents[cellKey(pit)] = { kind: "pit", damage: 0, createdBy: "human" };
|
||||
state.squareContents[cellKey(beyond)] = { kind: "stone", damage: 0, createdBy: "human" };
|
||||
const blocked = pathToward(viewFor(state, "bot"), bot.position, new Set([cellKey(pit)]), { throughHazards: true });
|
||||
expect(blocked === null || blocked.dir !== dir).toBe(true);
|
||||
delete state.squareContents[cellKey(beyond)];
|
||||
const open = pathToward(viewFor(state, "bot"), bot.position, new Set([cellKey(pit)]), { throughHazards: true });
|
||||
expect(open?.dir).toBe(dir);
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw new Error("no straight three-square run on this board");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user