Rev 17: a warp mouth on a pit's rim is a way off it (M4Q7)

Kestrel stood beside a pit dug against the board's west edge, walls
north and south of it, and was told there was nothing to land on — the
rim check counted only neighbouring squares, and the warp mouth beyond
the pit is not one. From rev 17 a mouth on the rim is an exit like any
square, named at a fork like any other; every older game takes the
mouth when no square offers, so the refused crossing goes through as
it stands. The jump through a mouth carries `via: "warp"`: the board
shimmers both mouths instead of streaking across the maze, and the
reel cuts rather than gliding. The automaton leaves a pit only by its
exits. Three tests walk the rim under both readings.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jm2auWk6RP71CjaAb4FMoG
This commit is contained in:
Eric Wagoner
2026-09-05 22:20:18 -04:00
co-authored by Claude Fable 5.1
parent dc4d2a1221
commit 461c1a5c83
6 changed files with 124 additions and 22 deletions
+23 -11
View File
@@ -238,7 +238,7 @@ export interface CastParams {
}
/** The revision new games are dealt under; GameConfig.deckRev pins it per game. */
export const CURRENT_RULES_REV = 16;
export const CURRENT_RULES_REV = 17;
/** Every rulings revision since the baseline, newest last — the entries a
* game's deckRev freezes it before or after. Shown to players as the house
@@ -259,6 +259,7 @@ export const RULES_REVISIONS: { rev: number; note: string }[] = [
{ rev: 14, note: "Crossing a pit on a 2, 3, or 4 means edging around its rim: the walker lands on an open square beside the pit — the only one if there is one, otherwise the one they name by clicking it — and a pit with no way off cannot be entered. Older games bounced the walker back and charged the stride." },
{ rev: 15, note: "BIG MAN at a fork: a pushed wizard leaves by any open side but the way the giant came — the only one if there is one, otherwise the side they choose, the giant's stride hanging until they do (FAQ: at an intersection the other player decides). Older games shove straight ahead whenever that way is open; rounding a corner was never possible before and is allowed in every game." },
{ rev: 16, note: "BUTT-HEAD's charge is measured as far as the goat's legs reach. Older games searched only six steps of corridor and refused a longer ram as unreachable, even with the legs to make it." },
{ rev: 17, note: "A warp mouth on a pit's rim is a way off it like any other square, and a fork with one is the walker's to name. Older games take the mouth only when no square offers — before that, a pit against the board's edge with walls either side could not be crossed at all." },
];
export interface GameConfig {
@@ -693,7 +694,7 @@ export type GameEvent =
| { type: "slippedInOoze"; player: PlayerId; at: Cell }
| { type: "struggledInOoze"; player: PlayerId; stood: boolean }
| { type: "steppedOnTacks"; player: PlayerId; at: Cell }
| { type: "jumpedPit"; player: PlayerId; from: Cell; to: Cell }
| { type: "jumpedPit"; player: PlayerId; from: Cell; to: Cell; via?: "warp" }
| { type: "fellInPit"; player: PlayerId; at: Cell }
| { type: "climbedFromPit"; player: PlayerId; success: boolean }
| { type: "stuckInSlime"; player: PlayerId; at: Cell }
@@ -4414,15 +4415,23 @@ function doMove(prev: GameState, direction: Side, over = false, exit?: Side, sho
// named side — or the pit cannot be entered from here at all.
const ledge = (state.config.deckRev ?? 1) >= 14;
let landing: Side | null = null;
let landingCell: Cell | null = null;
let landingWarp = false;
if (ledge) {
const pitCell = p.position;
const open = (d: Side) => {
const b = neighbor(pitCell, d);
return !!view.cells[cellKey(b)] &&
(view.edges[edgeKey(pitCell, d)] ?? "open") === "open" &&
state.squareContents[cellKey(b)]?.kind !== "stone";
const footing = (d: Side): { kind: "step" | "warp"; to: Cell } | null => {
const t = stepTarget(view, pitCell, d);
if (t.kind === "blocked" || state.squareContents[cellKey(t.to)]?.kind === "stone") return null;
return t;
};
const exits = SIDES.filter((d) => d !== opposite(direction) && open(d));
const ways = SIDES.filter((d) => d !== opposite(direction));
const floor = ways.filter((d) => footing(d)?.kind === "step");
const mouths = ways.filter((d) => footing(d)?.kind === "warp");
// Rev 17: a warp mouth on the rim is a way off it like any other.
// Before it only a neighbouring square counted, so a pit against
// the board's edge with walls either side could not be crossed at
// all; older games take the mouth only when no square offers.
const exits = (state.config.deckRev ?? 1) >= 17 || floor.length === 0 ? [...floor, ...mouths] : floor;
if (exits.length === 0) {
p.position = from;
return err("the pit cannot be crossed from here — nothing to land on beside it");
@@ -4436,6 +4445,9 @@ function doMove(prev: GameState, direction: Side, over = false, exit?: Side, sho
p.position = from;
return err(`the rim leads more than one way — click the square to land on: ${exits.join(" or ")}`);
}
const foot = footing(landing)!;
landingCell = foot.to;
landingWarp = foot.kind === "warp";
}
const roll = rollD4(state, events, p.id, "leaping the pit — a 1 falls in");
if (roll === 1) {
@@ -4447,15 +4459,15 @@ function doMove(prev: GameState, direction: Side, over = false, exit?: Side, sho
events.unshift({ type: "moved", player: p.id, from, to: p.position, direction, via });
return { ok: true, state, events };
}
const beyond = neighbor(p.position, landing ?? direction);
const beyondOk = landing !== null || (
const beyond = landingCell ?? neighbor(p.position, direction);
const beyondOk = landingCell !== null || (
view.cells[cellKey(beyond)] &&
(view.edges[edgeKey(p.position, direction)] ?? "open") === "open" &&
state.squareContents[cellKey(beyond)]?.kind !== "stone");
if (beyondOk) {
const pitCell = p.position;
p.position = beyond;
events.push({ type: "jumpedPit", player: p.id, from: pitCell, to: beyond });
events.push({ type: "jumpedPit", player: p.id, from: pitCell, to: beyond, ...(landingWarp ? { via: "warp" as const } : {}) });
content = state.squareContents[cellKey(p.position)];
} else {
// Nowhere to land: teeter back where you started.