Disease makes its caster the carrier (rules rev 7)

The card says it plainly — "You're the carrier! Disease caused by
spell does not affect you, only others" — but the engine cast it AT an
adjacent victim, making them the plague rat. At rev 7 it is a
self-cast on fear's pattern: no target, no counteraction window
("REFLECTIONs have no effect against this"), duration equals the
number, and sharing a square bites in both directions — the carrier
walking in, or anyone walking onto the carrier. Older ledgers hold
targeted disease casts with full stack exchanges, so the legacy attack
path survives for rev ≤6 games and all 33 ledgers replay clean. The
client offers the plain Cast button for it in rev-7 games.

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-08-28 12:35:35 -04:00
co-authored by Claude Fable 5
parent db3b14fd51
commit b99c22f14a
3 changed files with 97 additions and 3 deletions
+55
View File
@@ -1296,3 +1296,58 @@ describe("a forced drop lands like any drop", () => {
if (drop?.type === "treasureDropped") expect(drop.onHomeOf).toBe("def");
});
});
describe("disease makes the caster the carrier (rev 7)", () => {
it("self-casts with no target; sharing a square bites both directions", () => {
let { state } = createGame({ playerIds: ["carrier", "mark"], seed: 42, sets: ["basic", "expansion1"] });
state = toRound2(state);
while (activePlayer(state).id !== "carrier") {
state = must(state, activePlayer(state).id, { type: "endTurn", draw: 0 });
}
const carrier = state.players.find((p) => p.id === "carrier")!;
const mark = state.players.find((p) => p.id === "mark")!;
const dz = giveCard(state, "carrier", "disease", "D", 0);
giveCard(state, "carrier", "number-3", "N", 1);
let r = applyCommand(state, "carrier", {
type: "cast", instanceId: dz.instanceId, numberInstanceIds: ["number-3#N"],
});
if (!r.ok) throw new Error(r.error);
state = r.state;
expect(state.stack).toBeNull();
expect(state.sustained.some((s) => s.cardId === "disease" && s.targetId === "carrier")).toBe(true);
// The carrier walks INTO the mark's square: the mark takes 3.
const view = boardView(state);
const carrierNow = state.players.find((p) => p.id === "carrier")!;
const markNow = state.players.find((p) => p.id === "mark")!;
for (const side of SIDES) {
const t = stepTarget(view, carrierNow.position, side);
if (t.kind !== "step") continue;
markNow.position = { ...t.to };
const before = markNow.life;
r = applyCommand(state, "carrier", { type: "move", direction: side });
if (!r.ok) throw new Error(r.error);
state = r.state;
expect(state.players.find((p) => p.id === "mark")!.life).toBe(before - 3);
break;
}
state = must(state, "carrier", { type: "endTurn", draw: 0 });
// The mark walks INTO the carrier's square: the mark takes 3 again.
const m2 = state.players.find((p) => p.id === "mark")!;
const c2 = state.players.find((p) => p.id === "carrier")!;
const view2 = boardView(state);
for (const side of SIDES) {
const t = stepTarget(view2, c2.position, side);
if (t.kind !== "step") continue;
m2.position = { ...t.to };
const back = side === "N" ? "S" : side === "S" ? "N" : side === "E" ? "W" : "E";
const before = m2.life;
r = applyCommand(state, "mark", { type: "move", direction: back as "N" });
if (!r.ok) throw new Error(r.error);
expect(r.state.players.find((p) => p.id === "mark")!.life).toBe(before - 3);
return;
}
throw new Error("no adjacent step for the return walk");
});
});