diff --git a/packages/engine/src/automaton.ts b/packages/engine/src/automaton.ts index 0953b23..7097216 100644 --- a/packages/engine/src/automaton.ts +++ b/packages/engine/src/automaton.ts @@ -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) { diff --git a/packages/engine/test/automaton.test.ts b/packages/engine/test/automaton.test.ts index 60c6b91..65a51e9 100644 --- a/packages/engine/test/automaton.test.ts +++ b/packages/engine/test/automaton.test.ts @@ -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); + } + }); +}); diff --git a/packages/web/src/App.svelte b/packages/web/src/App.svelte index 30906fe..465ecd5 100644 --- a/packages/web/src/App.svelte +++ b/packages/web/src/App.svelte @@ -1767,6 +1767,16 @@ {/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)} +
+ 😱 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.) +
+ {/if} {#if actionsSpent} {#if view.turn.drawForbidden}
Your own bolt has left you reeling — the rest of this turn is lost, and no cards come with it.