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:
co-authored by
Claude Fable 5
parent
fdb44ab09f
commit
6ec9d6ab55
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user