Chaos aims at no one
The client always offered CHAOS from the hint bar without a target, and the engine refused it: "attack spells target a player". Not this one — cast bare, it sweeps every other living wizard into the pile, each offered their FULL SHIELD sit-out in turn order from the caster. No stack forms; full shields only ever excluded a player from the storm, so the queue is the whole defense. The older player-targeted form stays accepted, so stored games replay unchanged. The scramble joins the chronicle's notable events — a turn that redeals every hand earns its eye. 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
168e57ab91
commit
805fb09782
@@ -4933,6 +4933,28 @@ function doCast(prev: GameState, cmd: Extract<Command, { type: "cast" }>): Comma
|
|||||||
}
|
}
|
||||||
return { ok: true, state, events: events2 };
|
return { ok: true, state, events: events2 };
|
||||||
}
|
}
|
||||||
|
// CHAOS aims at no one: cast bare, it sweeps every other living
|
||||||
|
// wizard into the pile, each offered their FULL SHIELD sit-out in
|
||||||
|
// turn order. (Full shields only exclude; nothing in the set cancels
|
||||||
|
// a zero-point storm, so the queue is the whole defense.) The older
|
||||||
|
// player-targeted form is still accepted below, so stored games
|
||||||
|
// replay unchanged.
|
||||||
|
if (inHand.cardId === "chaos" && (!cmd.target || cmd.target.kind !== "player")) {
|
||||||
|
consumeCast(state, caster, inHand, mods, false);
|
||||||
|
if (state.turn.attackUsed) state.turn.secondAttackUsed = true;
|
||||||
|
state.turn.attackUsed = true;
|
||||||
|
state.lastSpellUsed[caster.id] = inHand.cardId;
|
||||||
|
const events: GameEvent[] = [{
|
||||||
|
type: "spellCast", caster: caster.id, card: inHand, cardId: inHand.cardId,
|
||||||
|
numberCards: mods.numbers, numberValue: mods.magnitude.numberValue,
|
||||||
|
from: caster.position, target: null, targetCell: null,
|
||||||
|
}];
|
||||||
|
const queue = turnOrderFrom(state, caster.id).filter((id) =>
|
||||||
|
id !== caster.id && state.players.find((p) => p.id === id)!.alive);
|
||||||
|
state.chaosPending = { casterId: caster.id, excluded: [], queue };
|
||||||
|
finishChaosIfReady(state, events);
|
||||||
|
return { ok: true, state, events };
|
||||||
|
}
|
||||||
if (!cmd.target || cmd.target.kind !== "player") return err("attack spells target a player");
|
if (!cmd.target || cmd.target.kind !== "player") return err("attack spells target a player");
|
||||||
if (cmd.target.playerId === caster.id) return err("you cannot attack yourself");
|
if (cmd.target.playerId === caster.id) return err("you cannot attack yourself");
|
||||||
const target = state.players.find((p) => p.id === (cmd.target as { playerId: PlayerId }).playerId);
|
const target = state.players.find((p) => p.id === (cmd.target as { playerId: PlayerId }).playerId);
|
||||||
|
|||||||
@@ -402,6 +402,35 @@ describe("the ward window and chaos shields", () => {
|
|||||||
expect(state.players.find((p) => p.id === owner.id)!.hand.some((c) => c.cardId === "ward")).toBe(false);
|
expect(state.players.find((p) => p.id === owner.id)!.hand.some((c) => c.cardId === "ward")).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("chaos casts bare — no target, every other wizard queued for a shield", () => {
|
||||||
|
let { state } = threeGame();
|
||||||
|
state = toRound2(state);
|
||||||
|
state = must(state, activePlayer(state).id, { type: "endTurn", draw: 0 });
|
||||||
|
const caster = activePlayer(state);
|
||||||
|
const others = state.players.filter((p) => p.id !== caster.id).map((p) => p.id);
|
||||||
|
const chaos = giveCard(state, caster.id, "chaos", "C", 0);
|
||||||
|
const shielder = others[0]!;
|
||||||
|
giveCard(state, shielder, "full-shield", "S", 0);
|
||||||
|
const shielderHand = () => state.players.find((p) => p.id === shielder)!.hand.map((c) => c.instanceId).sort();
|
||||||
|
const kept = shielderHand().filter((id) => id !== "full-shield#S").sort();
|
||||||
|
|
||||||
|
state = must(state, caster.id, { type: "cast", instanceId: chaos.instanceId });
|
||||||
|
// No stack: the queue opens at once, in turn order from the caster.
|
||||||
|
expect(state.stack).toBeNull();
|
||||||
|
expect(state.chaosPending?.queue.length).toBe(2);
|
||||||
|
const first = state.chaosPending!.queue[0]!;
|
||||||
|
state = first === shielder
|
||||||
|
? must(state, first, { type: "counteract", instanceId: "full-shield#S" })
|
||||||
|
: must(state, first, { type: "pass" });
|
||||||
|
const second = state.chaosPending!.queue[0]!;
|
||||||
|
state = second === shielder
|
||||||
|
? must(state, second, { type: "counteract", instanceId: "full-shield#S" })
|
||||||
|
: must(state, second, { type: "pass" });
|
||||||
|
expect(state.chaosPending).toBeNull();
|
||||||
|
// The shielded hand rode out the storm untouched.
|
||||||
|
expect(shielderHand()).toEqual(kept);
|
||||||
|
});
|
||||||
|
|
||||||
it("chaos: bystanders may shield out, reflections are refused", () => {
|
it("chaos: bystanders may shield out, reflections are refused", () => {
|
||||||
let { state } = threeGame();
|
let { state } = threeGame();
|
||||||
state = toRound2(state);
|
state = toRound2(state);
|
||||||
|
|||||||
@@ -226,6 +226,7 @@ const NOTABLE_EVENTS = new Set([
|
|||||||
"firewallBurned", "objectThrown", "gameWon", "positionsSwapped",
|
"firewallBurned", "objectThrown", "gameWon", "positionsSwapped",
|
||||||
"teleported", "stonesDestroyed", "thumbOfGod", "stoneTurnedToWater",
|
"teleported", "stonesDestroyed", "thumbOfGod", "stoneTurnedToWater",
|
||||||
"waterwallCrashes", "sectorRotated", "sectorRelocated", "homeBasesSwapped",
|
"waterwallCrashes", "sectorRotated", "sectorRelocated", "homeBasesSwapped",
|
||||||
|
"handsScrambled",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const SEAT_KEY = "wizwar-seat";
|
const SEAT_KEY = "wizwar-seat";
|
||||||
|
|||||||
Reference in New Issue
Block a user