Rules rev 3: Ward is a choice, Chaos honors the shield
WARD's text says "you may play at that time" — under rules revision 3 the choice is made by setting the trap: an armWard command toggles it on your own turn (your secret; a private wardSet event and a rail note), and the spring fires only while set, consuming the card and the arming together. Async games keep their agency without an interrupt window, exactly as the ambush system solved this before. CHAOS gains its printed interactions: "FULL SHIELD removes a player from participation" — a defender's shield sits them out instead of stopping the spell, and after the counter chain every bystander gets a shield window in seat order (chaosPending; queue head owes a response in lobby summaries, the rail says whose hand hangs in the balance) before the pile forms among the unshielded. "REFLECTIONS have no effect" — both reflections are refused as counters. ABSORB SPELL still eats the whole thing through the existing absorb path. Both changes are frozen behind deckRev 3 so every stored game — and the live one — replays byte-for-byte under its own rules. Five new tests cover armed/unarmed wards, shielded defenders and bystanders, the reflection refusal, and the legacy auto-ward. Not deployed — a live game is in progress. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
41c384274d
commit
7e3e00bf97
@@ -387,3 +387,103 @@ describe("attacking walls and doors", () => {
|
||||
expect(r.ok).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("rules revision 3", () => {
|
||||
function rev3Game(seed = 42) {
|
||||
return createGame({ playerIds: ["alice", "bob", "cara"], seed, sets: ["basic", "expansion1"], deckRev: 3 });
|
||||
}
|
||||
|
||||
it("ward springs only when its owner armed it", () => {
|
||||
let { state } = rev3Game();
|
||||
state = toRound2(state);
|
||||
state = must(state, activePlayer(state).id, { type: "endTurn", draw: 0 });
|
||||
const thief = activePlayer(state);
|
||||
const owner = state.players.find((p) => p.id !== thief.id)!;
|
||||
giveCard(state, owner.id, "ward", "W", 0);
|
||||
const treasure = state.treasures.find((t) => t.owner === owner.id)!;
|
||||
|
||||
// Unarmed: the grab goes unpunished.
|
||||
thief.position = { ...treasure.position! };
|
||||
let s2 = must(state, thief.id, { type: "pickUpTreasure" });
|
||||
expect(s2.players.find((p) => p.id === thief.id)!.life).toBe(15);
|
||||
expect(s2.players.find((p) => p.id === owner.id)!.hand.some((c) => c.cardId === "ward")).toBe(true);
|
||||
|
||||
// Armed (on the owner's own turn): the trap bites for 3.
|
||||
state.players[state.turn.activeIndex] = state.players[state.turn.activeIndex]!;
|
||||
const ownerTurnState = (() => {
|
||||
let s = state;
|
||||
while (activePlayer(s).id !== owner.id) s = must(s, activePlayer(s).id, { type: "endTurn", draw: 0 });
|
||||
return s;
|
||||
})();
|
||||
let s3 = must(ownerTurnState, owner.id, { type: "armWard", armed: true });
|
||||
while (activePlayer(s3).id !== thief.id) s3 = must(s3, activePlayer(s3).id, { type: "endTurn", draw: 0 });
|
||||
s3.players.find((p) => p.id === thief.id)!.position = { ...treasure.position! };
|
||||
s3 = must(s3, thief.id, { type: "pickUpTreasure" });
|
||||
expect(s3.players.find((p) => p.id === thief.id)!.life).toBe(12);
|
||||
expect(s3.wardArmed).not.toContain(owner.id);
|
||||
});
|
||||
|
||||
it("chaos: bystanders may shield out, reflections are refused", () => {
|
||||
let { state } = rev3Game();
|
||||
state = toRound2(state);
|
||||
state = must(state, activePlayer(state).id, { type: "endTurn", draw: 0 });
|
||||
const caster = activePlayer(state);
|
||||
const defender = state.players.find((p) => p.id !== caster.id)!;
|
||||
const bystander = state.players.find((p) => p.id !== caster.id && p.id !== defender.id)!;
|
||||
const chaos = giveCard(state, caster.id, "chaos", "C", 0);
|
||||
giveCard(state, defender.id, "full-reflection", "R", 0);
|
||||
giveCard(state, bystander.id, "full-shield", "S", 0);
|
||||
const bystanderHand = bystander.hand.map((c) => c.instanceId).sort();
|
||||
|
||||
state = must(state, caster.id, {
|
||||
type: "cast", instanceId: chaos.instanceId, target: { kind: "player", playerId: defender.id },
|
||||
});
|
||||
// "REFLECTIONS have no effect."
|
||||
expect(applyCommand(state, defender.id, { type: "counteract", instanceId: "full-reflection#R" }).ok).toBe(false);
|
||||
state = must(state, defender.id, { type: "pass" });
|
||||
// Now the bystander's window: they shield out and keep their hand.
|
||||
expect(state.chaosPending?.queue[0]).toBe(bystander.id);
|
||||
state = must(state, bystander.id, { type: "counteract", instanceId: "full-shield#S" });
|
||||
expect(state.chaosPending).toBeNull();
|
||||
const after = state.players.find((p) => p.id === bystander.id)!;
|
||||
expect(after.hand.map((c) => c.instanceId).sort()).toEqual(
|
||||
bystanderHand.filter((id) => id !== "full-shield#S").sort());
|
||||
});
|
||||
|
||||
it("chaos: the defender's full shield sits them out without stopping it", () => {
|
||||
let { state } = rev3Game();
|
||||
state = toRound2(state);
|
||||
state = must(state, activePlayer(state).id, { type: "endTurn", draw: 0 });
|
||||
const caster = activePlayer(state);
|
||||
const defender = state.players.find((p) => p.id !== caster.id)!;
|
||||
const bystander = state.players.find((p) => p.id !== caster.id && p.id !== defender.id)!;
|
||||
const chaos = giveCard(state, caster.id, "chaos", "C", 0);
|
||||
giveCard(state, defender.id, "full-shield", "S", 0);
|
||||
const defenderHand = () => state.players.find((p) => p.id === defender.id)!.hand.map((c) => c.instanceId).sort();
|
||||
const kept = defenderHand().filter((id) => id !== "full-shield#S").sort();
|
||||
|
||||
state = must(state, caster.id, {
|
||||
type: "cast", instanceId: chaos.instanceId, target: { kind: "player", playerId: defender.id },
|
||||
});
|
||||
state = must(state, defender.id, { type: "counteract", instanceId: "full-shield#S" });
|
||||
state = must(state, caster.id, { type: "pass" }); // caster declines to anti-anti
|
||||
state = must(state, defender.id, { type: "pass" }); // defender rests on the shield
|
||||
// Bystander declines; the scramble happens without the defender.
|
||||
expect(state.chaosPending?.queue[0]).toBe(bystander.id);
|
||||
state = must(state, bystander.id, { type: "pass" });
|
||||
expect(state.chaosPending).toBeNull();
|
||||
expect(defenderHand()).toEqual(kept);
|
||||
});
|
||||
|
||||
it("legacy games (rev < 3) keep the automatic ward", () => {
|
||||
let { state } = createGame({ playerIds: ["alice", "bob"], seed: 42, sets: ["basic"] });
|
||||
state = toRound2(state);
|
||||
const thief = activePlayer(state);
|
||||
const owner = state.players.find((p) => p.id !== thief.id)!;
|
||||
giveCard(state, owner.id, "ward", "W", 0);
|
||||
const treasure = state.treasures.find((t) => t.owner === owner.id)!;
|
||||
thief.position = { ...treasure.position! };
|
||||
state = must(state, thief.id, { type: "pickUpTreasure" });
|
||||
expect(state.players.find((p) => p.id === thief.id)!.life).toBe(12);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user