Three rulings from the night games (rules rev 2)
Teleport wraps the maze's rim: its four counted squares now step through warp mouths like any doorway, so a wizard can blink off one side of the board and onto the other. A wizard at a door's threshold may pull it open and peek — lock removed, door unlocked this turn, or PICK LOCK / MASTER KEY in hand — without stepping through. The door still hangs shut to everyone down the hallway; only a HELD door is propped open for every eye. Gated behind rules rev 2 (now sourced from the engine's CURRENT_RULES_REV) so stored games replay with their doors dark; the client's sight mirror learns the same peek. Fear no longer nails a cornered wizard to the floor: a step closer to the dread is allowed when it walks the shortest way out of it, so escape can round a corner. Standing outside and stepping in is still refused. All 15 production ledgers verified before deploy; 269 engine tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015RCWSTnb1KYTPyL4GmhGnF
This commit is contained in:
co-authored by
Claude Fable 5
parent
2932d1e500
commit
154e00141c
@@ -500,6 +500,28 @@ describe("zero-damage utility attacks", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("teleport wraps the maze's rim", () => {
|
||||
it("four spaces counted through a warp mouth reach the far side", () => {
|
||||
let { state } = newGame();
|
||||
state = toRound2(state);
|
||||
// Stand the active wizard at a wraparound mouth; the paired cell is
|
||||
// ONE teleport step away, exactly as it is one walking step.
|
||||
const active = activePlayer(state);
|
||||
const warp = boardView(state).warps[0]!;
|
||||
active.position = { ...warp.from.cell };
|
||||
const tp = giveCard(state, active.id, "teleport", "TP", 0);
|
||||
const r = applyCommand(state, active.id, {
|
||||
type: "cast", instanceId: tp.instanceId,
|
||||
target: { kind: "cell", cell: { ...warp.to.cell } },
|
||||
});
|
||||
expect(r.ok).toBe(true);
|
||||
if (r.ok) {
|
||||
const after = r.state.players.find((p) => p.id === active.id)!;
|
||||
expect(cellKey(after.position)).toBe(cellKey(warp.to.cell));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("teleport as a counteraction", () => {
|
||||
it("the attack has no chance of hitting you — you are simply elsewhere", () => {
|
||||
let { state } = newGame();
|
||||
|
||||
@@ -720,4 +720,35 @@ describe("fear holds off monsters and unwilling feet alike", () => {
|
||||
expect(r.ok).toBe(false);
|
||||
if (!r.ok) expect(r.error).toContain("dread");
|
||||
});
|
||||
|
||||
it("a wizard caged inside the dread may round a corner to escape", () => {
|
||||
let { state } = createGame({ playerIds: ["a", "b"], seed: 42, sets: ["basic", "expansion1"] });
|
||||
const a = state.players.find((p) => p.id === "a")!;
|
||||
const b = state.players.find((p) => p.id === "b")!;
|
||||
pushSustained(state, {
|
||||
id: "fx-fear", cardId: "fear", casterId: "b", targetId: "b",
|
||||
remainingTurns: 5, data: {},
|
||||
});
|
||||
// b radiates dread from (2,5). a stands in a dead-end pocket at
|
||||
// (2,3): walls on three sides, so the only way out steps SOUTH —
|
||||
// closer to b — before the corridor east leads clear of the dread.
|
||||
b.position = { x: 2, y: 5 };
|
||||
a.position = { x: 2, y: 3 };
|
||||
for (const side of ["N", "E", "W"] as const) {
|
||||
state.edgeOverrides[edgeKey({ x: 2, y: 3 }, side)] = "wall";
|
||||
}
|
||||
state.edgeOverrides[edgeKey({ x: 2, y: 3 }, "S")] = "open";
|
||||
state.edgeOverrides[edgeKey({ x: 2, y: 4 }, "E")] = "open";
|
||||
state.edgeOverrides[edgeKey({ x: 3, y: 4 }, "E")] = "open";
|
||||
state.edgeOverrides[edgeKey({ x: 4, y: 4 }, "N")] = "open";
|
||||
while (state.players[state.turn.activeIndex]!.id !== "a") {
|
||||
const r0 = applyCommand(state, state.players[state.turn.activeIndex]!.id, { type: "endTurn", draw: 0 });
|
||||
if (!r0.ok) throw new Error(r0.error);
|
||||
state = r0.state;
|
||||
}
|
||||
// The closer step is the only way out: permitted.
|
||||
const r = applyCommand(state, "a", { type: "move", direction: "S" });
|
||||
if (!r.ok) throw new Error("escape step refused: " + r.error);
|
||||
expect(r.ok).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -445,7 +445,7 @@ describe("a held door is an open doorway to the eye", () => {
|
||||
expect(state.players.find((p) => p.id === pursuer)!.life).toBeLessThan(lifeBefore);
|
||||
});
|
||||
|
||||
it("an unheld unlocked door still blocks sight", () => {
|
||||
it("an unlocked door is an open doorway to the wizard at its threshold", () => {
|
||||
let { state, cell, side, holder, pursuer } = sightRig();
|
||||
const pick = giveCard(state, holder, "pick-lock");
|
||||
state = must(state, holder, {
|
||||
@@ -453,9 +453,56 @@ describe("a held door is an open doorway to the eye", () => {
|
||||
target: { kind: "edge", cell, side },
|
||||
});
|
||||
const fb = giveCard(state, holder, "fireball", "FB", 1);
|
||||
const refused = applyCommand(state, holder, {
|
||||
const cast = applyCommand(state, holder, {
|
||||
type: "cast", instanceId: fb.instanceId, target: { kind: "player", playerId: pursuer },
|
||||
});
|
||||
expect(cast.ok).toBe(true);
|
||||
});
|
||||
|
||||
it("a lock REMOVED lets any wizard beside the door peek through", () => {
|
||||
let { state, cell, side, holder, pursuer } = sightRig();
|
||||
const rm = giveCard(state, holder, "remove-lock");
|
||||
state = must(state, holder, {
|
||||
type: "cast", instanceId: rm.instanceId,
|
||||
target: { kind: "edge", cell, side },
|
||||
});
|
||||
const fb = giveCard(state, holder, "fireball", "FB", 1);
|
||||
const cast = applyCommand(state, holder, {
|
||||
type: "cast", instanceId: fb.instanceId, target: { kind: "player", playerId: pursuer },
|
||||
});
|
||||
expect(cast.ok).toBe(true);
|
||||
});
|
||||
|
||||
it("PICK LOCK in hand is enough to ease a locked door open a crack", () => {
|
||||
let { state, cell, side, holder, pursuer } = sightRig();
|
||||
giveCard(state, holder, "pick-lock");
|
||||
const fb = giveCard(state, holder, "fireball", "FB", 1);
|
||||
const cast = applyCommand(state, holder, {
|
||||
type: "cast", instanceId: fb.instanceId, target: { kind: "player", playerId: pursuer },
|
||||
});
|
||||
expect(cast.ok).toBe(true);
|
||||
});
|
||||
|
||||
it("the wizard down the hallway sees no further than the shut door", () => {
|
||||
let { state, cell, side, holder, pursuer } = sightRig();
|
||||
// Pull the pursuer one square further down the hall, so the door is
|
||||
// no longer on their threshold; clear their path to the doorway.
|
||||
const near = neighbor(cell, side);
|
||||
const far = neighbor(near, side);
|
||||
if (!boardView(state).cells[cellKey(far)]) return; // the maze ends here; geometry unavailable
|
||||
state.players.find((p) => p.id === pursuer)!.position = far;
|
||||
state.edgeOverrides[edgeKey(near, side)] = "open";
|
||||
const rm = giveCard(state, holder, "remove-lock");
|
||||
state = must(state, holder, {
|
||||
type: "cast", instanceId: rm.instanceId,
|
||||
target: { kind: "edge", cell, side },
|
||||
});
|
||||
state = must(state, holder, { type: "endTurn", draw: 0 });
|
||||
const fb = giveCard(state, pursuer, "fireball", "FB", 1);
|
||||
const refused = applyCommand(state, pursuer, {
|
||||
type: "cast", instanceId: fb.instanceId, target: { kind: "player", playerId: holder },
|
||||
});
|
||||
expect(refused.ok).toBe(false);
|
||||
if (!refused.ok) expect(refused.error).toContain("line of sight");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user