Stone to Water melts doors too (rules rev 38)

"Small entryways in a stone wall" are stone: a door melts exactly as a
wall does — edge opens, lock state washes away, the two-point wave
crashes. Older ledgers had the refusal and replay so.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Eric Wagoner
2026-08-21 11:43:27 -04:00
co-authored by Claude Fable 5
parent 118bfd236b
commit 2afc4e3973
4 changed files with 44 additions and 4 deletions
+7 -1
View File
@@ -1937,9 +1937,15 @@ const CARD_EFFECTS: Record<string, AttackEffect | NeutralEffect | CounterEffect>
if (cmd.target?.kind === "edge") { if (cmd.target?.kind === "edge") {
const { cell, side } = cmd.target; const { cell, side } = cmd.target;
const key = edgeKey(cell, side); const key = edgeKey(cell, side);
if (view.edges[key] !== "wall") return "that is not a stone wall"; // Doors are "small entryways in a stone wall" — stone melts as
// stone (rev 38; older ledgers had refused doors and replay so).
const current = view.edges[key];
const meltable = current === "wall" ||
((state.config.deckRev ?? 1) >= 38 && current === "door");
if (!meltable) return "that is not a stone wall";
if (!losToEdge(view, caster.position, cell, side)) return "no line of sight"; if (!losToEdge(view, caster.position, cell, side)) return "no line of sight";
state.edgeOverrides[key] = "open"; state.edgeOverrides[key] = "open";
delete state.doorStates[key];
delete state.createdEdges[key]; delete state.createdEdges[key];
events.push({ type: "stoneTurnedToWater", caster: caster.id, at: null, edge: { cell, side } }); events.push({ type: "stoneTurnedToWater", caster: caster.id, at: null, edge: { cell, side } });
// "Wall turns into a WATERWALL with a range and damage of 2." // "Wall turns into a WATERWALL with a range and damage of 2."
+35 -1
View File
@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { applyCommand, activePlayer, boardView, createGame, gameLos } from "../src/game"; import { applyCommand, activePlayer, boardView, createGame, gameLos } from "../src/game";
import { cellKey, edgeKey, SIDES, stepTarget, type Cell } from "../src/board"; import { cellKey, edgeKey, SIDES, stepTarget, type Cell, type Side } from "../src/board";
import type { CardInstance } from "../src/cards"; import type { CardInstance } from "../src/cards";
import { eligibleCellsFor, sightedCellsFor, viewFor } from "../src/view"; import { eligibleCellsFor, sightedCellsFor, viewFor } from "../src/view";
import { newExpansionGame as newGame, must, giveCard, emptyNeighborCell } from "./helpers"; import { newExpansionGame as newGame, must, giveCard, emptyNeighborCell } from "./helpers";
@@ -161,6 +161,40 @@ describe("expansion terrain", () => {
expect(state.squareContents[cellKey(spot.cell)]).toBeUndefined(); expect(state.squareContents[cellKey(spot.cell)]).toBeUndefined();
// Wave side effects vary by geometry; the melt itself is the pinned behavior. // Wave side effects vary by geometry; the melt itself is the pinned behavior.
}); });
it("stone to water melts a door from rev 38 — a small entryway in a stone wall", () => {
for (const deckRev of [37, 38]) {
const { state } = createGame({
playerIds: ["alice", "bob"], seed: 42, sets: ["basic", "expansion1"], deckRev,
});
const me = activePlayer(state);
const board = boardView(state);
// Stand the caster at some door and melt it point-blank.
let doorAt: { cell: Cell; side: Side } | null = null;
outer: for (const k of Object.keys(board.cells)) {
const [x, y] = k.split(",").map(Number) as [number, number];
for (const side of SIDES) {
if (board.edges[edgeKey({ x, y }, side)] === "door") {
doorAt = { cell: { x, y }, side };
break outer;
}
}
}
if (!doorAt) throw new Error("setup: no door on this board");
me.position = { ...doorAt.cell };
const stw = giveCard(state, me.id, "stone-to-water", "SW", 0);
const r = applyCommand(state, me.id, {
type: "cast", instanceId: stw.instanceId,
target: { kind: "edge", cell: doorAt.cell, side: doorAt.side },
});
expect(r.ok).toBe(deckRev >= 38);
if (r.ok) {
const key = edgeKey(doorAt.cell, doorAt.side);
expect(boardView(r.state).edges[key] ?? "open").toBe("open");
expect(r.state.doorStates[key]).toBeUndefined();
}
}
});
}); });
describe("eligibility dimming mirrors the engine", () => { describe("eligibility dimming mirrors the engine", () => {
+1 -1
View File
@@ -54,7 +54,7 @@ export interface Room {
const rooms = new Map<string, Room>(); const rooms = new Map<string, Room>();
/** Rules revision new games are dealt under (stored games keep their own). */ /** Rules revision new games are dealt under (stored games keep their own). */
const RULES_REV = 37; const RULES_REV = 38;
const ROOM_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; const ROOM_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
+1 -1
View File
@@ -282,7 +282,7 @@
"remove-lock": "click a door to strip its lock for good", "remove-lock": "click a door to strip its lock for good",
"create-door": "click a wall for the new door", "create-door": "click a wall for the new door",
"warp-wand": "click a wall to warp it open", "warp-wand": "click a wall to warp it open",
"stone-to-water": "click a stone wall, or a stone-filled square", "stone-to-water": "click a stone wall or door, or a stone-filled square",
"thumb-of-god": "aim the die at a square in sight — it may drift on impact", "thumb-of-god": "aim the die at a square in sight — it may drift on impact",
"dispel-creation": "click the creation — a square, a creature, or a conjured wall", "dispel-creation": "click the creation — a square, a creature, or a conjured wall",
}; };