diff --git a/packages/engine/src/game.ts b/packages/engine/src/game.ts index 16910df..3062cf4 100644 --- a/packages/engine/src/game.ts +++ b/packages/engine/src/game.ts @@ -5071,7 +5071,13 @@ function doPass(prev: GameState, playerId: PlayerId): CommandResult { const state = clone(prev); const stack = state.stack!; const events: GameEvent[] = []; - if (playerId === stack.attackerId) { + // A self-stack (a democratic monster touching its own creator) has one + // party wearing both hats: their pass RESOLVES — bouncing it "to the + // defender" would hand the exchange back to themselves forever + // (rules rev 22; older ledgers hold those no-op bounces and replay so). + const selfStack = + stack.attackerId === stack.defenderId && (state.config.deckRev ?? 1) >= 22; + if (playerId === stack.attackerId && !selfStack) { // The attacker declining to answer bounces the exchange back so the // defender may stack further counters — but not past a total stop: // re-prompting then reads as "your shield failed" and goads the table diff --git a/packages/engine/test/creatures.test.ts b/packages/engine/test/creatures.test.ts index 3022954..9057945 100644 --- a/packages/engine/test/creatures.test.ts +++ b/packages/engine/test/creatures.test.ts @@ -683,3 +683,40 @@ describe("collapsing walls crush monsters too (rules rev 21)", () => { 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 }); + state = toRound2(state); + const creator = activePlayer(state); + state.creatures.push({ + id: "dm1", kind: "democratic-monster", controllerId: creator.id, + position: { x: creator.position.x - 1, y: creator.position.y }, + damage: 0, maxDamage: 5, movesPerTurn: 3, movementUsed: 0, + attackUsed: false, justCreated: false, + wallPassesPerTurn: 0, wallPassUsed: 0, scorchedThisTurn: [], + }); + const r = applyCommand(state, creator.id, { type: "moveCreature", creatureId: "dm1", direction: "E" }); + if (!r.ok) throw new Error(r.error); + return { state: r.state, creator: creator.id }; + } + + it("rev 22: passing your own monster's touch takes the claw and moves on", () => { + const { state, creator } = selfTouch(22); + if (!state.stack) return; // a wall between: the touch never opened + expect(state.stack.attackerId).toBe(creator); + expect(state.stack.defenderId).toBe(creator); + const r = applyCommand(state, creator, { type: "pass" }); + if (!r.ok) throw new Error(r.error); + 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(); + }); +}); diff --git a/packages/server/src/rooms.ts b/packages/server/src/rooms.ts index 1f88fdb..104dcc7 100644 --- a/packages/server/src/rooms.ts +++ b/packages/server/src/rooms.ts @@ -54,7 +54,7 @@ export interface Room { const rooms = new Map(); /** Rules revision new games are dealt under (stored games keep their own). */ -const RULES_REV = 21; +const RULES_REV = 22; const ROOM_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; diff --git a/packages/web/src/local.svelte.ts b/packages/web/src/local.svelte.ts index 3faf6dd..5efc996 100644 --- a/packages/web/src/local.svelte.ts +++ b/packages/web/src/local.svelte.ts @@ -136,7 +136,7 @@ class LocalGame { seed, sets: expansion ? ["basic", "expansion1"] : ["basic"], ...(colors ? { colors } : {}), - deckRev: 21, + deckRev: 22, }; const { state, events } = createGame(config); for (const e of events) {