FEAR's rout: bots flee it, humans are told their duty

'Players or monsters must move away from you on their turn if within
range, if they can.' Automatons caught in the dread now spend their
legs escaping before any other marching — the step that most increases
distance, a true dead end excusing them per the card. Humans keep their
freedom but get the compulsion spelled out in an urgent slip (move
away, even by card or through danger; only a dead end excuses you),
with the maze trusting the table to honor it — the owner's chosen
depth of enforcement.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0138A8CjeQRpvzKxuMfz1Bqc
This commit is contained in:
Eric Wagoner
2026-08-19 21:51:22 -04:00
co-authored by Claude Fable 5
parent fdb44ab09f
commit 6ec9d6ab55
3 changed files with 60 additions and 0 deletions
+22
View File
@@ -1298,6 +1298,28 @@ export function automatonCommand(
}
}
// FEAR's compulsion: "must move away from you on their turn if within
// range, if they can." Caught in a dread bubble, the clockwork spends
// its legs fleeing before any other marching.
if (view.turn.movementUsed < view.turn.movementAllowance) {
const dreadful = livingEnemies(view).filter((p) =>
view.sustained.some((e) => e.cardId === "fear" && e.targetId === p.id) &&
Math.abs(p.position.x - self.position.x) + Math.abs(p.position.y - self.position.y) <= 3);
for (const source of dreadful) {
const here = Math.abs(source.position.x - self.position.x) + Math.abs(source.position.y - self.position.y);
let best: { dir: Side; d: number } | null = null;
for (const dir of SIDES) {
const t = stepTarget(view.board, self.position, dir);
if (t.kind === "blocked") continue;
if (view.squareContents[cellKey(t.to)]?.kind === "stone") continue;
const d = Math.abs(source.position.x - t.to.x) + Math.abs(source.position.y - t.to.y);
if (d > here && (best === null || d > best.d)) best = { dir, d };
}
if (best) return { type: "move", direction: best.dir };
// Dead end: the card excuses what cannot be done; march on normally.
}
}
// March. The thief-chase outranks everything; the berserker hunts wizards
// over gold; the worrier keeps its distance; the hunter goes to the gold.
if (view.turn.movementUsed < view.turn.movementAllowance) {
+28
View File
@@ -520,3 +520,31 @@ describe("no number is wasted on a turn that ends at a grab", () => {
expect(cmd?.type).not.toBe("playNumberForMovement");
});
});
describe("the clockwork flees the dread", () => {
it("caught in FEAR's bubble, it spends its legs moving away", () => {
let { state } = createGame({ playerIds: ["bot", "grim"], seed: 42, sets: ["basic"], deckRev: 32 });
for (let guard = 0; guard < 10 && !(actingSeat(state) === "bot" && state.turn.round > 1); guard++) {
const r = applyCommand(state, actingSeat(state), { type: "endTurn", draw: 0 });
if (!r.ok) throw new Error(r.error);
state = r.state;
}
const bot = state.players.find((p) => p.id === "bot")!;
const grim = state.players.find((p) => p.id === "grim")!;
grim.position = { x: 2, y: 5 };
bot.position = { x: 2, y: 7 }; // two spaces inside the dread
state.sustained.push({
id: "fx-fear", cardId: "fear", casterId: "grim", targetId: "grim",
remainingTurns: 5, data: {},
} as never);
const cmd = automatonCommand(viewFor(state, "bot"), "berserker", "archmage");
expect(cmd?.type).toBe("move");
if (cmd?.type === "move") {
const r = applyCommand(state, "bot", cmd);
if (!r.ok) throw new Error(r.error);
const after = r.state.players.find((p) => p.id === "bot")!;
const d = Math.abs(after.position.x - grim.position.x) + Math.abs(after.position.y - grim.position.y);
expect(d).toBeGreaterThan(2);
}
});
});
+10
View File
@@ -1767,6 +1767,16 @@
<button class="stamp tiny" onclick={askCouncil}>ask again</button>
</div>
{/if}
{#if isYourTurn && view.players.some((p) => p.alive && p.id !== view.you &&
view.sustained.some((e) => e.cardId === "fear" && e.targetId === p.id) && me &&
Math.abs(p.position.x - me.position.x) + Math.abs(p.position.y - me.position.y) <= 3)}
<div class="slip urgent">
😱 An unnatural dread grips you — FEAR requires you to move away
this turn if you can, even if your only escape is a card
(Teleport, Pass Through Wall) or a dangerous square. Only a true
dead end excuses you. (The maze trusts you to honor it.)
</div>
{/if}
{#if actionsSpent}
{#if view.turn.drawForbidden}
<div class="slip urgent">Your own bolt has left you reeling — the rest of this turn is lost, and no cards come with it.</div>