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
@@ -582,7 +582,16 @@ function wallBlastTarget(
|
||||
* when the clockwork can unlock them; the first such door is reported so the
|
||||
* key gets used before the boot.
|
||||
*/
|
||||
function pathToward(
|
||||
/** Can a wizard entering the pit at `pit` heading `dir` land beyond it —
|
||||
* the same test the maze applies: a cell there, no wall between, no stone. */
|
||||
function pitLandable(view: GameView, pit: Cell, dir: Side): boolean {
|
||||
const beyond = neighbor(pit, dir);
|
||||
return !!view.board.cells[cellKey(beyond)] &&
|
||||
(view.board.edges[edgeKey(pit, dir)] ?? "open") === "open" &&
|
||||
view.squareContents[cellKey(beyond)]?.kind !== "stone";
|
||||
}
|
||||
|
||||
export function pathToward(
|
||||
view: GameView,
|
||||
from: Cell,
|
||||
goals: Set<string>,
|
||||
@@ -607,6 +616,11 @@ function pathToward(
|
||||
const k = cellKey(to);
|
||||
if (seen.has(k)) continue;
|
||||
if (view.squareContents[k]?.kind === "stone") continue;
|
||||
const hazard = view.squareContents[k]?.kind;
|
||||
// A pit is entered by leaping to the square beyond it in the same
|
||||
// direction; with nothing to land on, the maze bounces the leaper
|
||||
// back and charges the stride. Goal or waypoint, that is no road.
|
||||
if (hazard === "pit" && !pitLandable(view, to, dir)) continue;
|
||||
seen.add(k);
|
||||
cameBy.set(k, { prev: cellKey(c), dir, viaDoor });
|
||||
if (goals.has(k)) { found = k; break; }
|
||||
@@ -615,7 +629,6 @@ function pathToward(
|
||||
// turn, and stings — a jail with foliage. Only a wizard with no
|
||||
// road at all (throughJails) dives in. "Few will dive into one
|
||||
// unless they are desperate."
|
||||
const hazard = view.squareContents[k]?.kind;
|
||||
if (hazard === "thornbush" && !opts.throughJails) continue;
|
||||
if (!opts.throughHazards &&
|
||||
(hazard === "pit" || hazard === "ooze" ||
|
||||
@@ -725,7 +738,8 @@ function overLimit(view: GameView): number {
|
||||
* clogged hand never refreshes; only cards the brain has no play for are
|
||||
* shed (good counters and attacks are hoarded, as a human would). */
|
||||
function endTurnDrawing(view: GameView, tier: TierTraits, style?: AutomatonStyle): Command {
|
||||
const deficit = tier.draw - (handLimitOf(view) - view.yourHand.length);
|
||||
const draw = drawUnderCurses(view, tier.draw);
|
||||
const deficit = draw - (handLimitOf(view) - view.yourHand.length);
|
||||
if (tier.sheds && deficit > 0) {
|
||||
const shed = [...view.yourHand]
|
||||
.filter((c) => discardValue(c, view, style) <= 3)
|
||||
@@ -734,7 +748,16 @@ function endTurnDrawing(view: GameView, tier: TierTraits, style?: AutomatonStyle
|
||||
.map((c) => c.instanceId);
|
||||
if (shed.length > 0) return { type: "discard", instanceIds: shed };
|
||||
}
|
||||
return { type: "endTurn", draw: tier.draw };
|
||||
return { type: "endTurn", draw };
|
||||
}
|
||||
|
||||
/** SLOW DEATH bleeds a point per card drawn: the clockwork draws only what
|
||||
* it can afford, keeping four life in hand, and nothing at all when it
|
||||
* cannot — a bare hand beats a bare grave. */
|
||||
export function drawUnderCurses(view: GameView, draw: number): number {
|
||||
const slow = view.sustained.some((e) => e.cardId === "slow-death" && e.targetId === view.you && !e.data?.reversed);
|
||||
if (!slow) return draw;
|
||||
return Math.max(0, Math.min(draw, me(view).life - 4));
|
||||
}
|
||||
|
||||
function worstCards(view: GameView, n: number, style?: AutomatonStyle): string[] {
|
||||
@@ -1522,8 +1545,13 @@ export function automatonCommand(
|
||||
}
|
||||
}
|
||||
}
|
||||
// No shot at a wizard: raise a creature to do the walking.
|
||||
const summon = view.yourHand.find((c) => SUMMONS.has(c.cardId));
|
||||
// No shot at a wizard: raise a creature to do the walking. A SHADOW
|
||||
// drinks a life a turn from its master — no pet for a wizard already
|
||||
// bleeding, or with little blood to spare.
|
||||
const bleeding = view.sustained.some((e) =>
|
||||
(e.cardId === "slow-death" || e.cardId === "walking-dead") && e.targetId === you && !e.data?.reversed);
|
||||
const summon = view.yourHand.find((c) => SUMMONS.has(c.cardId) &&
|
||||
!(c.cardId === "shadow" && (bleeding || self.life <= 6)));
|
||||
if (summon && livingEnemies(view).length > 0) {
|
||||
const near = style === "worrier" ? self.position : livingEnemies(view)[0]!.position;
|
||||
const spot = summonSpot(view, near);
|
||||
|
||||
@@ -648,7 +648,9 @@ export type GameEvent =
|
||||
| { type: "objectDragged"; caster: PlayerId; what: string; from: Cell; to: Cell }
|
||||
| { type: "spellReused"; player: PlayerId; card: CardInstance }
|
||||
| { type: "castAroundCorner"; caster: PlayerId }
|
||||
| { type: "moveBumped"; player: PlayerId; direction: Side }
|
||||
/** A stride that went nowhere: into a wall while blind, or into a pit
|
||||
* with nothing to land on beyond it. */
|
||||
| { type: "moveBumped"; player: PlayerId; direction: Side; why?: "pit" }
|
||||
| { type: "attackMisdirected"; attacker: PlayerId; intended: PlayerId; rolledDirection: Side; newTarget: PlayerId | null }
|
||||
| { type: "dieRolled"; player: PlayerId | null; roll: number; purpose: string }
|
||||
| { type: "tableTalk"; player: PlayerId; text: string }
|
||||
@@ -4352,7 +4354,7 @@ function doMove(prev: GameState, direction: Side, over = false): CommandResult {
|
||||
// Nowhere to land: teeter back where you started.
|
||||
p.position = from;
|
||||
state.turn.movementUsed++;
|
||||
events.push({ type: "moveBumped", player: p.id, direction });
|
||||
events.push({ type: "moveBumped", player: p.id, direction, why: "pit" });
|
||||
return { ok: true, state, events };
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user