Ops hardening plus two rules gaps close (rev 13)

Operational: auto-reboot for kernel patches (09:30 UTC), Caddy access
logs at /var/lib/caddy (self-rotating; the sandbox denies /var/log),
logrotate for the backup log, a restore drill proving a Spaces snapshot
boots 56/56 rooms clean, the about page's plain sentence on permanent
recording, and /wizwar-pulse to read it all weekly.

Rules: safes now smash — attacks aimed at the box accumulate on it and
the fifteenth point bursts it (widening, ungated). And SLOW DEATH's
bites pause for a victim holding an ABSORB — the card named by the FAQ,
not Absorb Spell — each soaking exactly one point, rev-gated at 13 with
the instant-bite legacy pinned. Bots weigh the soak against their life.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015RCWSTnb1KYTPyL4GmhGnF
This commit is contained in:
Eric Wagoner
2026-09-01 10:35:52 -04:00
co-authored by Claude Fable 5
parent a66b058c68
commit 92613d7386
11 changed files with 281 additions and 2 deletions
+1
View File
@@ -15,6 +15,7 @@ import { pushSustained } from "./helpers";
function actingSeat(state: GameState): PlayerId {
return (
state.wardPending?.ownerId ??
state.slowDeathPending?.playerId ??
state.stack?.waitingOn ??
state.pendingDiscard ??
state.chaosPending?.queue[0] ??
+39
View File
@@ -147,6 +147,45 @@ describe("slow death", () => {
expect(sustainedOn(state, defender, "slow-death").length).toBe(1);
});
it("an absorb in hand pauses the bites; each soaks one point (rev 13)", () => {
let { state } = newGame();
state = toRound2(state);
const { attacker, defender } = faceOff(state);
const sd = giveCard(state, attacker, "slow-death");
state = castAt(state, attacker, defender, sd);
state = must(state, attacker, { type: "endTurn", draw: 0 });
const d = state.players.find((p) => p.id === defender)!;
const toDiscard = d.hand.slice(0, 2).map((c) => c.instanceId);
state = must(state, defender, { type: "discard", instanceIds: toDiscard });
giveCard(state, defender, "absorb", "AB", 0);
state = must(state, defender, { type: "endTurn", draw: 2 });
// Two bites hang; nobody else may act.
expect(state.slowDeathPending).toEqual({ playerId: defender, points: 2 });
expect(applyCommand(state, attacker, { type: "endTurn", draw: 0 }).ok).toBe(false);
// One absorb soaks one point; the other lands.
state = must(state, defender, { type: "slowDeathChoice", absorbInstanceId: "absorb#AB" });
expect(state.slowDeathPending).toBeNull();
expect(state.players.find((p) => p.id === defender)!.life).toBe(14);
expect(state.players.find((p) => p.id === defender)!.hand.some((c) => c.cardId === "absorb")).toBe(false);
});
it("without an absorb the bites land instantly, as in older decks", () => {
let { state } = createGame({ playerIds: ["alice", "bob"], seed: 42, sets: ["basic"], deckRev: 12 });
state = toRound2(state);
const { attacker, defender } = faceOff(state);
const sd = giveCard(state, attacker, "slow-death");
state = castAt(state, attacker, defender, sd);
state = must(state, attacker, { type: "endTurn", draw: 0 });
const d = state.players.find((p) => p.id === defender)!;
giveCard(state, defender, "absorb", "AB", 0);
const toDiscard = d.hand.filter((c) => c.cardId !== "absorb").slice(0, 2).map((c) => c.instanceId);
state = must(state, defender, { type: "discard", instanceIds: toDiscard });
state = must(state, defender, { type: "endTurn", draw: 2 });
// Rev 12: even with an absorb in hand, the old decks bite instantly.
expect(state.slowDeathPending ?? null).toBeNull();
expect(state.players.find((p) => p.id === defender)!.life).toBe(13);
});
it("a reverse at the casting turns the curse: a point gained per draw (rev 11)", () => {
let { state } = newGame();
state = toRound2(state);
@@ -136,6 +136,27 @@ describe("safes and lock cards", () => {
expect(state.openSafes).toEqual([]);
});
it("fifteen points of battering bursts the safe open", () => {
let { state, raider, chest } = safeRig();
const box = state.players.find((p) => p.id === raider)!.position;
// Two 5-point fireballs dent it; the third bursts it.
for (let i = 0; i < 3; i++) {
const fb = giveCard(state, raider, "fireball", `F${i}`, 0);
state = must(state, raider, {
type: "cast", instanceId: fb.instanceId, target: { kind: "cell", cell: { ...box } },
});
if (i < 2) {
expect(state.squareContents[cellKey(box)]?.kind).toBe("safe");
state = must(state, raider, { type: "endTurn", draw: 0 });
const other = state.players.find((p) => p.id !== raider)!;
state = must(state, other.id, { type: "endTurn", draw: 0 });
}
}
expect(state.squareContents[cellKey(box)]).toBeUndefined();
state = must(state, raider, { type: "pickUpTreasure" });
expect(state.treasures.find((t) => t.id === chest.id)!.carriedBy).toBe(raider);
});
it("the key must be at or beside the safe", () => {
let { state, raider } = safeRig();
const p = state.players.find((q) => q.id === raider)!;