The Alter Ego casts from its own square; the first-stride cue leaves the maze
Randy raised his double, selected it, and could cast nothing through it: the card's whole point — "it may use any of the spells in your hand, and you need not be in its L.O.S." — had never been wired. A cast may now name the double, and the double's square becomes the spell's origin: sight, reach, the launch point the reel films, and the direction a knockback or rout pushes. The hand, the attack, and every answer to it stay the caster's. Spells that move the caster's own body — a goat's charge, a swap — do not travel through it. On the board, the double's sight lights the eligible squares; selecting the double and then a card aims from where it stands. A test hides the caster and fires through the double. The first-stride caption sat on the maze and hid the walls beside the wizard; it now stands above the board with a color chip for "you." Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jm2auWk6RP71CjaAb4FMoG
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
5f58a0955a
commit
10d5c5815d
@@ -1,9 +1,10 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
type CreatureState, applyCommand, activePlayer, boardView, creatureAt, type GameState, type PlayerId, createGame } from "../src/game";
|
||||
type CreatureState, applyCommand, activePlayer, boardView, creatureAt, type GameState, type PlayerId, createGame, gameLos,
|
||||
} from "../src/game";
|
||||
import { cellKey, edgeKey, neighbor, opposite, SIDES, stepTarget, type Cell, type Side } from "../src/board";
|
||||
import type { CardInstance } from "../src/cards";
|
||||
import { newExpansionGame as newGame, must, drain, giveCard, toRound2, emptyNeighborCell, pushSustained } from "./helpers";
|
||||
import { newExpansionGame as newGame, must, drain, giveCard, toRound2, emptyNeighborCell, pushSustained, faceOff } from "./helpers";
|
||||
|
||||
/** Summon a creature next to its creator (round 2+, consumes the attack). */
|
||||
function summon(state: GameState, playerId: PlayerId, kind: string, tag = "S") {
|
||||
@@ -961,3 +962,38 @@ describe("the imp's fire is a spell (rev 8)", () => {
|
||||
expect(after.alive).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("the alter ego casts from its own square", () => {
|
||||
it("a fireball leaves from the double, by the double's sight, and answers to the caster", () => {
|
||||
let { state } = newGame(42);
|
||||
state = toRound2(state);
|
||||
const { attacker, defender } = faceOff(state);
|
||||
const a = state.players.find((p) => p.id === attacker)!;
|
||||
const d = state.players.find((p) => p.id === defender)!;
|
||||
const ego = giveCard(state, attacker, "alter-ego");
|
||||
state = must(state, attacker, { type: "cast", instanceId: ego.instanceId });
|
||||
const double = state.creatures.find((c) => c.kind === "alter-ego" && c.controllerId === attacker)!;
|
||||
expect(cellKey(double.position)).toBe(cellKey(a.position));
|
||||
// The caster steps out of the defender's sight; the double keeps it.
|
||||
const view = boardView(state);
|
||||
const hidden = Object.keys(view.cells).map((k) => { const [x, y] = k.split(",").map(Number) as [number, number]; return { x, y }; })
|
||||
.find((c) => !gameLos(state, c, d.position) && !state.squareContents[cellKey(c)] && !view.homes.some((h) => cellKey(h) === cellKey(c)));
|
||||
expect(hidden).toBeDefined();
|
||||
const me = state.players.find((p) => p.id === attacker)!;
|
||||
me.position = { ...hidden! };
|
||||
const fb = giveCard(state, attacker, "fireball");
|
||||
const blind = applyCommand(state, attacker, { type: "cast", instanceId: fb.instanceId, target: { kind: "player", playerId: defender } });
|
||||
expect(blind.ok).toBe(false);
|
||||
const wrong = applyCommand(state, attacker, { type: "cast", instanceId: fb.instanceId, target: { kind: "player", playerId: defender }, via: "no-such-double" });
|
||||
expect(wrong.ok).toBe(false);
|
||||
const viaEgo = applyCommand(state, attacker, { type: "cast", instanceId: fb.instanceId, target: { kind: "player", playerId: defender }, via: double.id });
|
||||
expect(viaEgo.ok).toBe(true);
|
||||
if (!viaEgo.ok) return;
|
||||
expect(viaEgo.state.stack?.attackerId).toBe(attacker);
|
||||
expect(cellKey(viaEgo.state.stack!.origin!)).toBe(cellKey(double.position));
|
||||
const cast = viaEgo.events.find((e) => e.type === "spellCast");
|
||||
expect(cast && cast.type === "spellCast" ? cellKey(cast.from) : null).toBe(cellKey(double.position));
|
||||
const done = must(viaEgo.state, defender, { type: "pass" });
|
||||
expect(done.players.find((p) => p.id === defender)!.life).toBe(10);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user