diff --git a/packages/engine/src/game.ts b/packages/engine/src/game.ts index 67baafb..231a63c 100644 --- a/packages/engine/src/game.ts +++ b/packages/engine/src/game.ts @@ -226,7 +226,7 @@ export interface CastParams { } /** The revision new games are dealt under; GameConfig.deckRev pins it per game. */ -export const CURRENT_RULES_REV = 3; +export const CURRENT_RULES_REV = 4; export interface GameConfig { playerIds: PlayerId[]; @@ -245,6 +245,9 @@ export interface GameConfig { * Rev 3: VISIONSTONE pierces its one wall for every sight the game * asks of its bearer — creations and utility spells included — not * only direct attacks. + * Rev 4: "the door will relock behind you" means it — passing through + * an unlocked door shuts it at the walker's back unless a hand holds + * it; unpassed, it relocks at turn's end as before. */ deckRev?: number; } @@ -3950,14 +3953,24 @@ function doMove(prev: GameState, direction: Side, over = false): CommandResult { } else if (edge === "door" && (doorIsOpen(state, key) || (displays(p, "master-key") && state.doorStates[key] !== "jammed"))) { // The displayed MASTER KEY turns in every lock it meets: the walker - // passes without another cast, and the door relocks behind them at - // turn's end like any picked lock. A JAMmed LOCK still refuses it. + // passes without another cast, and the door RELOCKS BEHIND THEM at + // once — "door relocks behind you" grants no lingering opening, so + // bystanders see only a shut door (the bearer's own threshold peek + // rides the key itself). A JAMmed LOCK still refuses it. To hold a + // door open, cast the key at it as ever. if (!doorIsOpen(state, key)) { - state.openDoorEdges.push(key); events.push({ type: "doorUnlocked", player: p.id, edge: parseEdgeKey(key), withCardId: "master-key" }); } p.position = dest; via = "step"; + // "The door will relock behind you": stepping through an unlocked + // door shuts it at the walker's back unless a hand holds it open. + // (Rev 4; earlier games kept their turn-long openings.) + if ((state.config.deckRev ?? 1) >= 4 && + state.openDoorEdges.includes(key) && !state.heldDoors.some((h) => h.key === key)) { + state.openDoorEdges = state.openDoorEdges.filter((k) => k !== key); + events.push({ type: "doorsRelocked", count: 1 }); + } } else if (edge === "firewall") { // "Passing through it does four points of magical damage." Firewalls // burn even a MIST-BODY. diff --git a/packages/engine/test/durations-doors-cards.test.ts b/packages/engine/test/durations-doors-cards.test.ts index 641f032..0157608 100644 --- a/packages/engine/test/durations-doors-cards.test.ts +++ b/packages/engine/test/durations-doors-cards.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; import { applyCommand, activePlayer, boardView, createGame, sustainedOn, type GameState } from "../src/game"; -import { cellKey, edgeKey, neighbor, type Side } from "../src/board"; +import { cellKey, edgeKey, neighbor, opposite, type Side } from "../src/board"; import type { CardInstance } from "../src/cards"; import { newGame, must, giveCard, toRound2, faceOff, castAt } from "./helpers"; @@ -111,7 +111,7 @@ describe("doors", () => { throw new Error("no door on this board"); } - it("pick lock opens an adjacent door until end of turn", () => { + it("pick lock opens an adjacent door — and it relocks behind the walker", () => { let { state } = newGame(); const me = activePlayer(state); const door = findDoor(state); @@ -130,9 +130,8 @@ describe("doors", () => { state = must(state, me.id, { type: "move", direction: dir }); expect(cellKey(activePlayer(state).position)).toBe(cellKey(other)); - // Relocks when the turn ends. - state = must(state, me.id, { type: "endTurn", draw: 0 }); - state = must(state, activePlayer(state).id, { type: "endTurn", draw: 0 }); + // "It will relock behind you": shut at the walker's back at once — + // the way back is barred without another pick. expect(state.openDoorEdges.length).toBe(0); expect(applyCommand(state, me.id, { type: "move", direction: dir === "E" ? "W" : "N" }).ok).toBe(false); }); @@ -539,7 +538,7 @@ describe("the displayed MASTER KEY", () => { } }); - it("its bearer walks through locked doors, which relock behind them", () => { + it("its bearer walks through locked doors, which relock at their back", () => { const { state, cell, side, key, keeper } = keyRig(); const mk = giveCard(state, keeper.id, "master-key", "MK"); keeper.displayed.push(mk.instanceId); @@ -548,9 +547,12 @@ describe("the displayed MASTER KEY", () => { if (r.ok) { const after = r.state.players.find((p) => p.id === keeper.id)!; expect(cellKey(after.position)).toBe(cellKey(neighbor(cell, side))); - // Unlocked for the turn — the relock rides the usual end-of-turn sweep. - expect(r.state.openDoorEdges).toContain(key); + // "Door relocks behind you": no lingering opening for bystanders. + expect(r.state.openDoorEdges).not.toContain(key); expect(r.events.some((e) => e.type === "doorUnlocked")).toBe(true); + // The key turns again on the way back. + const back = applyCommand(r.state, keeper.id, { type: "move", direction: opposite(side) }); + expect(back.ok).toBe(true); } });