The great simplification: every vintage gate collapses, the count restarts at 1
All 50 rooms were retired (backed up on the droplet and in Spaces), so no ledger needs an old branch to replay: forty revisions of deckRev gates fold into one canonical ruleset — the newest behavior everywhere. The ward's arming, the idiot's steering, the lazy illusion roll, the legacy redirection swap, and the flat wave all leave with their gates; RULES_REV restarts at 1 for whenever the rules fork again. Old-vintage test pins go with them. The opening screen now quietly drops seats whose rooms the server no longer knows, instead of listing them "unreachable" forever. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
e8df6efcde
commit
8a50ff7f36
@@ -17,7 +17,8 @@ function summon(state: GameState, playerId: PlayerId, kind: string, tag = "S") {
|
||||
|
||||
describe("expansion deck", () => {
|
||||
it("basic + expansion1 builds the full 200-card game", () => {
|
||||
const { state } = newGame();
|
||||
// Three seats: two-player games shed LIFESAVER from the build.
|
||||
const { state } = newGame(42, ["a", "b", "c"]);
|
||||
const total = state.deck.length + state.discard.length +
|
||||
state.players.reduce((s, p) => s + p.hand.length, 0);
|
||||
expect(total).toBe(200);
|
||||
@@ -59,6 +60,8 @@ describe("monsters", () => {
|
||||
state = must(state, me, {
|
||||
type: "creatureAttack", creatureId: state.creatures[0]!.id, targetId: victim.id,
|
||||
});
|
||||
// The blow opens the victim's counteraction window; they take it raw.
|
||||
state = must(state, victim.id, { type: "pass" });
|
||||
expect(state.players.find((p) => p.id !== me)!.life).toBe(13);
|
||||
});
|
||||
|
||||
@@ -136,6 +139,8 @@ describe("monsters", () => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// The touch opens the victim's counteraction window; they take it raw.
|
||||
state = must(state, enemy2.id, { type: "pass" });
|
||||
const bitten = state.players.find((p) => p.id !== me)!;
|
||||
expect(bitten.life).toBe(13);
|
||||
expect(bitten.hand.length).toBe(6);
|
||||
@@ -293,9 +298,9 @@ describe("expansion support cards", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("counteracting a creature's blow (rules rev 4)", () => {
|
||||
describe("counteracting a creature's blow", () => {
|
||||
function rev4() {
|
||||
return createGame({ playerIds: ["alice", "bob"], seed: 42, sets: ["basic", "expansion1"], deckRev: 4 });
|
||||
return createGame({ playerIds: ["alice", "bob"], seed: 42, sets: ["basic", "expansion1"] });
|
||||
}
|
||||
function wraithOnVictim(state: GameState) {
|
||||
state = toRound2(state);
|
||||
@@ -341,20 +346,11 @@ describe("counteracting a creature's blow (rules rev 4)", () => {
|
||||
const wraith = state.creatures.find((c) => c.id === "w1")!;
|
||||
expect(wraith.damage).toBe(2);
|
||||
});
|
||||
|
||||
it("legacy games (rev < 4) keep the instant touch", () => {
|
||||
let { state } = createGame({ playerIds: ["alice", "bob"], seed: 42, sets: ["basic", "expansion1"] });
|
||||
const rig = wraithOnVictim(state);
|
||||
state = rig.state;
|
||||
state = must(state, rig.controller, { type: "moveCreature", creatureId: "w1", direction: rig.side });
|
||||
expect(state.stack).toBeNull();
|
||||
expect(state.players.find((p) => p.id === rig.victim)!.life).toBe(13);
|
||||
});
|
||||
});
|
||||
|
||||
describe("big man (rules rev 5)", () => {
|
||||
function bigRig() {
|
||||
let { state } = createGame({ playerIds: ["alice", "bob"], seed: 42, sets: ["basic", "expansion1"], deckRev: 5 });
|
||||
let { state } = createGame({ playerIds: ["alice", "bob"], seed: 42, sets: ["basic", "expansion1"] });
|
||||
state = toRound2(state);
|
||||
const giant = activePlayer(state);
|
||||
state.sustained.push({
|
||||
@@ -450,9 +446,9 @@ describe("big man (rules rev 5)", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("monsters roll to hit the hidden (rules rev 13)", () => {
|
||||
function creatureVsInvisible(deckRev: number) {
|
||||
let { state } = createGame({ playerIds: ["alice", "bob"], seed: 42, sets: ["basic", "expansion1"], deckRev });
|
||||
describe("monsters roll to hit the hidden", () => {
|
||||
function creatureVsInvisible() {
|
||||
let { state } = createGame({ playerIds: ["alice", "bob"], seed: 42, sets: ["basic", "expansion1"] });
|
||||
state = toRound2(state);
|
||||
const me = activePlayer(state).id;
|
||||
const victim = state.players.find((p) => p.id !== me)!;
|
||||
@@ -470,29 +466,18 @@ describe("monsters roll to hit the hidden (rules rev 13)", () => {
|
||||
}
|
||||
|
||||
it("rev 13: the skeleton's blow takes the 1-in-4 roll — 'any attack' means any", () => {
|
||||
let { state, me, victim, skeletonId } = creatureVsInvisible(13);
|
||||
let { state, me, victim, skeletonId } = creatureVsInvisible();
|
||||
const rngBefore = JSON.stringify(state.rng);
|
||||
state = must(state, me, { type: "creatureAttack", creatureId: skeletonId, targetId: victim.id });
|
||||
state = must(state, victim.id, { type: "pass" });
|
||||
// The die was consumed, whichever way it landed.
|
||||
expect(JSON.stringify(state.rng)).not.toBe(rngBefore);
|
||||
});
|
||||
|
||||
it("earlier revisions keep the old certainty: no roll, the blow just lands", () => {
|
||||
let { state, me, victim, skeletonId } = creatureVsInvisible(12);
|
||||
const lifeBefore = state.players.find((p) => p.id === victim.id)!.life;
|
||||
const rngBefore = JSON.stringify(state.rng);
|
||||
state = must(state, me, { type: "creatureAttack", creatureId: skeletonId, targetId: victim.id });
|
||||
state = must(state, victim.id, { type: "pass" });
|
||||
expect(state.players.find((p) => p.id === victim.id)!.life).toBeLessThan(lifeBefore);
|
||||
// No die was consumed on the way.
|
||||
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 });
|
||||
let { state } = createGame({ playerIds: ["a", "b", "c"], seed: 42, sets: ["basic", "expansion1"] });
|
||||
const victim = state.players.find((p) => p.id === "c")!;
|
||||
state.creatures.push({
|
||||
id: "imp1", kind: "fire-imp", controllerId: "c",
|
||||
@@ -524,7 +509,7 @@ describe("elimination sweeps the board either way (rules rev 14)", () => {
|
||||
|
||||
describe("creatures walk open doorways", () => {
|
||||
it("a skeleton passes a door whose lock was removed", () => {
|
||||
let { state } = createGame({ playerIds: ["a", "b"], seed: 42, sets: ["basic", "expansion1"], deckRev: 16 });
|
||||
let { state } = createGame({ playerIds: ["a", "b"], seed: 42, sets: ["basic", "expansion1"] });
|
||||
state = toRound2(state);
|
||||
const view = boardView(state);
|
||||
for (const [key, edge] of Object.entries(view.edges)) {
|
||||
@@ -553,7 +538,7 @@ describe("creatures walk open doorways", () => {
|
||||
|
||||
describe("the wave carries monsters (rules rev 17)", () => {
|
||||
it("a cornered skeleton takes the waterwall crush", () => {
|
||||
let { state } = createGame({ playerIds: ["a", "b"], seed: 42, sets: ["basic", "expansion1"], deckRev: 17 });
|
||||
let { state } = createGame({ playerIds: ["a", "b"], seed: 42, sets: ["basic", "expansion1"] });
|
||||
state = toRound2(state);
|
||||
const caster = activePlayer(state);
|
||||
// A skeleton pinned against solid stone, one square from the wave's edge.
|
||||
@@ -592,7 +577,7 @@ describe("the wave carries monsters (rules rev 17)", () => {
|
||||
|
||||
describe("the democratic monster's claw survives the first wizard's death (rules rev 18)", () => {
|
||||
it("refreshes each round even with the roll-off winner dead", () => {
|
||||
let { state } = createGame({ playerIds: ["a", "b", "c"], seed: 42, sets: ["basic", "expansion1"], deckRev: 18 });
|
||||
let { state } = createGame({ playerIds: ["a", "b", "c"], seed: 42, sets: ["basic", "expansion1"] });
|
||||
state = toRound2(state);
|
||||
const firstId = state.players[state.turn.firstIndex]!.id;
|
||||
const dm = {
|
||||
@@ -619,7 +604,7 @@ describe("the democratic monster's claw survives the first wizard's death (rules
|
||||
|
||||
describe("a mid-round democratic monster claws on the next turn (rules rev 18)", () => {
|
||||
it("creation spends the turn, not the round", () => {
|
||||
let { state } = createGame({ playerIds: ["a", "b"], seed: 42, sets: ["basic", "expansion1"], deckRev: 18 });
|
||||
let { state } = createGame({ playerIds: ["a", "b"], seed: 42, sets: ["basic", "expansion1"] });
|
||||
state = toRound2(state);
|
||||
const creator = activePlayer(state);
|
||||
const r0 = summon(state, creator.id, "democratic-monster");
|
||||
@@ -641,9 +626,9 @@ describe("a mid-round democratic monster claws on the next turn (rules rev 18)",
|
||||
});
|
||||
});
|
||||
|
||||
describe("collapsing walls crush monsters too (rules rev 21)", () => {
|
||||
function wallRig(deckRev: number) {
|
||||
let { state } = createGame({ playerIds: ["a", "b"], seed: 42, sets: ["basic", "expansion1"], deckRev });
|
||||
describe("collapsing walls crush monsters too", () => {
|
||||
function wallRig() {
|
||||
let { state } = createGame({ playerIds: ["a", "b"], seed: 42, sets: ["basic", "expansion1"] });
|
||||
state = toRound2(state);
|
||||
const view = boardView(state);
|
||||
for (const [key, edge] of Object.entries(view.edges)) {
|
||||
@@ -672,21 +657,16 @@ describe("collapsing walls crush monsters too (rules rev 21)", () => {
|
||||
}
|
||||
|
||||
it("rev 21: the adjacent troll takes the four points", () => {
|
||||
const state = wallRig(21);
|
||||
const state = wallRig();
|
||||
const troll = state.creatures.find((c) => c.id === "tr1");
|
||||
// Four points on a six-point troll: hurt but standing (or regenerating).
|
||||
expect(troll?.damage).toBe(4);
|
||||
});
|
||||
|
||||
it("earlier revisions leave the troll dusty but unharmed", () => {
|
||||
const state = wallRig(20);
|
||||
expect(state.creatures.find((c) => c.id === "tr1")!.damage).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("the self-stack resolves on a pass (rules rev 22)", () => {
|
||||
function selfTouch(deckRev: number) {
|
||||
let { state } = createGame({ playerIds: ["a", "b"], seed: 42, sets: ["basic", "expansion1"], deckRev });
|
||||
describe("the self-stack resolves on a pass", () => {
|
||||
function selfTouch() {
|
||||
let { state } = createGame({ playerIds: ["a", "b"], seed: 42, sets: ["basic", "expansion1"] });
|
||||
state = toRound2(state);
|
||||
const creator = activePlayer(state);
|
||||
state.creatures.push({
|
||||
@@ -702,7 +682,7 @@ describe("the self-stack resolves on a pass (rules rev 22)", () => {
|
||||
}
|
||||
|
||||
it("rev 22: passing your own monster's touch takes the claw and moves on", () => {
|
||||
const { state, creator } = selfTouch(22);
|
||||
const { state, creator } = selfTouch();
|
||||
if (!state.stack) return; // a wall between: the touch never opened
|
||||
expect(state.stack.attackerId).toBe(creator);
|
||||
expect(state.stack.defenderId).toBe(creator);
|
||||
@@ -711,19 +691,11 @@ describe("the self-stack resolves on a pass (rules rev 22)", () => {
|
||||
expect(r.state.stack).toBeNull();
|
||||
expect(r.state.players.find((p) => p.id === creator)!.life).toBe(13);
|
||||
});
|
||||
|
||||
it("rev 21 keeps its recorded bounce, no-op as it was", () => {
|
||||
const { state, creator } = selfTouch(21);
|
||||
if (!state.stack) return;
|
||||
const r = applyCommand(state, creator, { type: "pass" });
|
||||
if (!r.ok) throw new Error(r.error);
|
||||
expect(r.state.stack).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("fear holds off monsters and unwilling feet alike (rules rev 32)", () => {
|
||||
it("a commanded monster cannot close within three of the fearsome", () => {
|
||||
let { state } = createGame({ playerIds: ["a", "b"], seed: 42, sets: ["basic", "expansion1"], deckRev: 32 });
|
||||
let { state } = createGame({ playerIds: ["a", "b"], seed: 42, sets: ["basic", "expansion1"] });
|
||||
const a = state.players.find((p) => p.id === "a")!;
|
||||
const b = state.players.find((p) => p.id === "b")!;
|
||||
// b radiates fear; a's troll stands exactly four away, aimed straight at b.
|
||||
|
||||
Reference in New Issue
Block a user