Ambushed teleports carry their destination; room 59UN unstuck (rev 35)
An ambush springing TELEPORT OPPONENT opened a stack with params null —
the spring bypasses cast validation and nothing ever asked where the
victim goes — and resolution crashed on the missing cell, wedging the
room 'waiting on Automaton'. Three layers:
- Crash guards: teleport-opponent and mental-force fizzle gracefully on
a destination-less stack (ungated: no stored command had resolved one).
- The trap commits its destination when laid: setAmbush carries a cell
('wherever you say', said in advance), the spring passes it into the
stack, and the client's arming flow asks for the click.
- Rev 35 refuses arming those spells without a destination; older
ledgers armed blind and their springs fizzle.
All ledgers verified before deploy.
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
e8c25ddc19
commit
76dd019d5d
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { giveCard } from "./helpers";
|
||||
import { edgeKey } from "../src/board";
|
||||
import {
|
||||
applyCommand,
|
||||
activePlayer,
|
||||
@@ -312,3 +313,50 @@ describe("the Ward is played in the moment (rules rev 31)", () => {
|
||||
expect(r.ok).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("an ambushed teleport carries its destination (rules rev 35)", () => {
|
||||
it("the trap springs and the victim lands where the trapper said", () => {
|
||||
let { state } = createGame({ playerIds: ["trapper", "prey"], seed: 42, sets: ["basic", "expansion1"], deckRev: 35 });
|
||||
for (let guard = 0; guard < 10 && !(state.players[state.turn.activeIndex]!.id === "trapper" && state.turn.round > 1); guard++) {
|
||||
const r = applyCommand(state, state.players[state.turn.activeIndex]!.id, { type: "endTurn", draw: 0 });
|
||||
if (!r.ok) throw new Error(r.error);
|
||||
state = r.state;
|
||||
}
|
||||
const trapper = state.players.find((p) => p.id === "trapper")!;
|
||||
const prey = state.players.find((p) => p.id === "prey")!;
|
||||
giveCard(state, "trapper", "interrupt", "I", 0);
|
||||
giveCard(state, "trapper", "teleport-opponent", "TO", 1);
|
||||
// Arming without a destination is refused; with one it is stored.
|
||||
const bad = applyCommand(state, "trapper", {
|
||||
type: "setAmbush", instanceId: "interrupt#I", trigger: { kind: "near" },
|
||||
spellInstanceId: "teleport-opponent#TO",
|
||||
});
|
||||
expect(bad.ok).toBe(false);
|
||||
const dest = { x: trapper.home.x, y: trapper.home.y === 0 ? 1 : trapper.home.y - 1 };
|
||||
let r = applyCommand(state, "trapper", {
|
||||
type: "setAmbush", instanceId: "interrupt#I", trigger: { kind: "near" },
|
||||
spellInstanceId: "teleport-opponent#TO", cell: dest,
|
||||
});
|
||||
if (!r.ok) throw new Error(r.error);
|
||||
state = r.state;
|
||||
state = applyCommand(state, "trapper", { type: "endTurn", draw: 0 }).ok
|
||||
? (applyCommand(state, "trapper", { type: "endTurn", draw: 0 }) as { state: typeof state }).state : state;
|
||||
// The prey walks adjacent; the trap springs; the prey passes; they land
|
||||
// at dest. (Re-find both: applyCommand clones made the handles stale.)
|
||||
const trapperNow = state.players.find((p) => p.id === "trapper")!;
|
||||
const preyNow = state.players.find((p) => p.id === "prey")!;
|
||||
const below = trapperNow.position.y >= 2;
|
||||
preyNow.position = { x: trapperNow.position.x, y: trapperNow.position.y + (below ? -2 : 2) };
|
||||
const dir = below ? "S" : "N";
|
||||
state.edgeOverrides[edgeKey(preyNow.position, dir)] = "open";
|
||||
r = applyCommand(state, "prey", { type: "move", direction: dir });
|
||||
if (!r.ok) throw new Error(r.error);
|
||||
state = r.state;
|
||||
expect(state.stack?.attackCard?.cardId).toBe("teleport-opponent");
|
||||
expect(state.stack?.params?.cell).toEqual(dest);
|
||||
r = applyCommand(state, "prey", { type: "pass" });
|
||||
if (!r.ok) throw new Error(r.error);
|
||||
state = r.state;
|
||||
expect(state.players.find((p) => p.id === "prey")!.position).toEqual(dest);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user