Rev 14: elimination sweeps the board by either door
Two ways out of the game, one cleanup. A wizard killed by damage took their monsters, sustained spells, and armed ambushes with them; a wizard eliminated by treasure loss left everything standing — a fire imp scorching for a dead master, a Medusa still gazing at a corpse. Both paths now share one sweep, gated at rev 14 so stored games keep their orphans acting as recorded. Also: Lifesaver lasts until used, carried as a near-billion-turn sentinel — which the scoresheet printed. Durations that mean forever no longer show a number. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
a60467c6fc
commit
d4565006bc
@@ -3335,6 +3335,17 @@ function clone(state: GameState): GameState {
|
|||||||
return structuredClone(state);
|
return structuredClone(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Everything an eliminated wizard leaves behind goes with them.
|
||||||
|
* "If you die, any monster controlled by you immediately disappears." */
|
||||||
|
function sweepEliminated(state: GameState, playerId: PlayerId): void {
|
||||||
|
state.sustained = state.sustained.filter((s) => s.targetId !== playerId && s.casterId !== playerId);
|
||||||
|
state.creatures = state.creatures.filter((c) => c.controllerId !== playerId);
|
||||||
|
for (const a of state.ambushes.filter((a) => a.ownerId === playerId)) {
|
||||||
|
state.discard.push(a.via, a.spell, ...a.numbers);
|
||||||
|
}
|
||||||
|
state.ambushes = state.ambushes.filter((a) => a.ownerId !== playerId);
|
||||||
|
}
|
||||||
|
|
||||||
function requireActionsAvailable(state: GameState): string | null {
|
function requireActionsAvailable(state: GameState): string | null {
|
||||||
if (state.turn.actionsEnded) return "your turn's actions ended when you picked up an object";
|
if (state.turn.actionsEnded) return "your turn's actions ended when you picked up an object";
|
||||||
return null;
|
return null;
|
||||||
@@ -5094,13 +5105,7 @@ function applyDamage(
|
|||||||
target.finalHand = [...target.hand];
|
target.finalHand = [...target.hand];
|
||||||
events.push({ type: "died", player: target.id, killedBy: attackerId });
|
events.push({ type: "died", player: target.id, killedBy: attackerId });
|
||||||
events.push({ type: "playerEliminated", player: target.id, reason: "killed" });
|
events.push({ type: "playerEliminated", player: target.id, reason: "killed" });
|
||||||
state.sustained = state.sustained.filter((s) => s.targetId !== target.id && s.casterId !== target.id);
|
sweepEliminated(state, target.id);
|
||||||
// "If you die, any monster controlled by you immediately disappears."
|
|
||||||
state.creatures = state.creatures.filter((c) => c.controllerId !== target.id);
|
|
||||||
for (const a of state.ambushes.filter((a) => a.ownerId === target.id)) {
|
|
||||||
state.discard.push(a.via, a.spell, ...a.numbers);
|
|
||||||
}
|
|
||||||
state.ambushes = state.ambushes.filter((a) => a.ownerId !== target.id);
|
|
||||||
|
|
||||||
if (target.carriedTreasureId) {
|
if (target.carriedTreasureId) {
|
||||||
const t = state.treasures.find((t) => t.id === target.carriedTreasureId)!;
|
const t = state.treasures.find((t) => t.id === target.carriedTreasureId)!;
|
||||||
@@ -5281,6 +5286,7 @@ function checkVictory(state: GameState, events: GameEvent[]): void {
|
|||||||
state.discard.push(...p.hand.splice(0));
|
state.discard.push(...p.hand.splice(0));
|
||||||
p.displayed = [];
|
p.displayed = [];
|
||||||
events.push({ type: "playerEliminated", player: p.id, reason: "treasuresLost" });
|
events.push({ type: "playerEliminated", player: p.id, reason: "treasuresLost" });
|
||||||
|
if ((state.config.deckRev ?? 1) >= 14) sweepEliminated(state, p.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -494,3 +494,35 @@ describe("monsters roll to hit the hidden (rules rev 13)", () => {
|
|||||||
expect(JSON.stringify(state.rng)).toBe(rngBefore);
|
expect(JSON.stringify(state.rng)).toBe(rngBefore);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("elimination sweeps the board either way (rules rev 14)", () => {
|
||||||
|
it("a treasure-eliminated wizard's fire imp vanishes with them", () => {
|
||||||
|
let { state } = createGame({ playerIds: ["a", "b", "c"], seed: 42, sets: ["basic", "expansion1"], deckRev: 14 });
|
||||||
|
const victim = state.players.find((p) => p.id === "c")!;
|
||||||
|
state.creatures.push({
|
||||||
|
id: "imp1", kind: "fire-imp", controllerId: "c",
|
||||||
|
position: { ...victim.position },
|
||||||
|
damage: 0, maxDamage: 5, movesPerTurn: 0, movementUsed: 0,
|
||||||
|
attackUsed: false, justCreated: false,
|
||||||
|
wallPassesPerTurn: 0, wallPassUsed: 0, scorchedThisTurn: [],
|
||||||
|
});
|
||||||
|
state.sustained.push({
|
||||||
|
id: "fx-med", cardId: "medusa", casterId: "a", targetId: "c",
|
||||||
|
remainingTurns: 3, data: {},
|
||||||
|
});
|
||||||
|
// Both of c's treasures sit captured on living enemies' home bases.
|
||||||
|
const mine = state.treasures.filter((t) => t.owner === "c");
|
||||||
|
expect(mine.length).toBe(2);
|
||||||
|
mine[0]!.position = { ...state.players.find((p) => p.id === "a")!.home };
|
||||||
|
mine[0]!.carriedBy = null;
|
||||||
|
mine[1]!.position = { ...state.players.find((p) => p.id === "b")!.home };
|
||||||
|
mine[1]!.carriedBy = null;
|
||||||
|
const active = activePlayer(state).id;
|
||||||
|
const r = applyCommand(state, active, { type: "endTurn", draw: 0 });
|
||||||
|
if (!r.ok) throw new Error(r.error);
|
||||||
|
state = r.state;
|
||||||
|
expect(state.players.find((p) => p.id === "c")!.alive).toBe(false);
|
||||||
|
expect(state.creatures.some((c) => c.controllerId === "c")).toBe(false);
|
||||||
|
expect(state.sustained.some((s) => s.targetId === "c")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export interface Room {
|
|||||||
const rooms = new Map<string, Room>();
|
const rooms = new Map<string, Room>();
|
||||||
|
|
||||||
/** Rules revision new games are dealt under (stored games keep their own). */
|
/** Rules revision new games are dealt under (stored games keep their own). */
|
||||||
const RULES_REV = 13;
|
const RULES_REV = 14;
|
||||||
|
|
||||||
const ROOM_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
const ROOM_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
||||||
|
|
||||||
|
|||||||
@@ -1367,7 +1367,7 @@
|
|||||||
peekCard = { instanceId: `peek-${e.id}`, cardId: e.cardId };
|
peekCard = { instanceId: `peek-${e.id}`, cardId: e.cardId };
|
||||||
peekCreatureId = null;
|
peekCreatureId = null;
|
||||||
}}>
|
}}>
|
||||||
✦ {spellName(e.cardId)} · {e.remainingTurns}
|
✦ {spellName(e.cardId)}{e.remainingTurns < 9000 ? ` · ${e.remainingTurns}` : ""}
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ class LocalGame {
|
|||||||
seed,
|
seed,
|
||||||
sets: expansion ? ["basic", "expansion1"] : ["basic"],
|
sets: expansion ? ["basic", "expansion1"] : ["basic"],
|
||||||
...(colors ? { colors } : {}),
|
...(colors ? { colors } : {}),
|
||||||
deckRev: 13,
|
deckRev: 14,
|
||||||
};
|
};
|
||||||
const { state, events } = createGame(config);
|
const { state, events } = createGame(config);
|
||||||
for (const e of events) {
|
for (const e of events) {
|
||||||
|
|||||||
Reference in New Issue
Block a user