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
@@ -43,6 +43,9 @@ export interface Room {
|
||||
|
||||
const rooms = new Map<string, Room>();
|
||||
|
||||
/** Rules revision new games are dealt under (stored games keep their own). */
|
||||
const RULES_REV = 3;
|
||||
|
||||
const ROOM_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
||||
|
||||
/** Tokens live hashed at rest (memory and disk); clients hold the raw form. */
|
||||
@@ -176,9 +179,9 @@ function startInMemory(room: Room, expansion: boolean, colors?: number[], deckRe
|
||||
export function startGame(room: Room, expansion: boolean): { events: GameEvent[] } | { error: string } {
|
||||
if (room.state) return { error: "already started" };
|
||||
const colors = resolveColors(room);
|
||||
const result = startInMemory(room, expansion, colors, 2);
|
||||
const result = startInMemory(room, expansion, colors, RULES_REV);
|
||||
if ("error" in result) return result;
|
||||
appendLine(room.id, { kind: "start", expansion, colors, deckRev: 2 });
|
||||
appendLine(room.id, { kind: "start", expansion, colors, deckRev: RULES_REV });
|
||||
recordRoom(room);
|
||||
return result;
|
||||
}
|
||||
@@ -224,12 +227,13 @@ export interface GameSummary {
|
||||
export function summarize(room: Room, playerId: PlayerId): GameSummary {
|
||||
const s = room.state;
|
||||
const active = s && s.phase === "playing" ? s.players[s.turn.activeIndex]!.id : null;
|
||||
const waitingOn = s?.stack?.waitingOn ?? s?.pendingDiscard ?? s?.outOfTurnWindow?.playerId ?? null;
|
||||
const waitingOn = s?.stack?.waitingOn ?? s?.pendingDiscard ?? s?.chaosPending?.queue[0] ?? s?.outOfTurnWindow?.playerId ?? null;
|
||||
const turnHolder = waitingOn ?? active;
|
||||
let attention: "turn" | "counteract" | "discard" | "interrupt" | null = null;
|
||||
if (s?.phase === "playing" && turnHolder === playerId) {
|
||||
attention =
|
||||
s.stack?.waitingOn === playerId ? "counteract"
|
||||
: s.chaosPending?.queue[0] === playerId ? "counteract"
|
||||
: s.pendingDiscard === playerId ? "discard"
|
||||
: s.outOfTurnWindow?.playerId === playerId ? "interrupt"
|
||||
: "turn";
|
||||
|
||||
Reference in New Issue
Block a user