Rev 22: TELEPORT crosses the maze's outer edge anywhere

A teleporter ignores walls, and the outer edge is no more than a wall
to it: a line leaving the maze at any square's edge re-enters at the
opposite edge on the same row or column, one space on. The lettered
openings connect as they always did. Older games crossed the edge only
at the openings, and replay so; U3U2, dealt at rev 21 with no teleport
yet cast, was re-stamped to 22 at its table's request.

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-16 23:56:13 -04:00
co-authored by Claude Fable 5.1
parent 7a6aa3d2a1
commit 803c51415e
4 changed files with 77 additions and 10 deletions
+36 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { applyCommand, activePlayer, boardView, createGame, gameLos, type GameState } from "../src/game";
import { applyCommand, activePlayer, boardView, createGame, gameLos, wallIgnoringDistance, type GameState } from "../src/game";
import { cellKey, edgeKey, neighbor, opposite, SIDES, stepTarget, type Cell, type Side } from "../src/board";
import type { CardInstance } from "../src/cards";
import { eligibleCellsFor, sightedCellsFor, viewFor } from "../src/view";
@@ -786,3 +786,38 @@ describe("teleport's reach wraps through the board's openings", () => {
expect(r.ok).toBe(true);
});
});
describe("teleport across the maze's outer edge (rev 22)", () => {
function atTheTopEdge(deckRev?: number) {
const config = { playerIds: ["alice", "bob"], seed: 42, sets: ["basic", "expansion1"] as ("basic" | "expansion1")[], ...(deckRev ? { deckRev } : {}) };
const state = toRound2(createGame(config).state);
const me = activePlayer(state);
const view = boardView(state);
// A square on the top row whose north side is plain edge, not a lettered mouth.
const top = Object.keys(view.cells).map((k) => k.split(",").map(Number) as [number, number])
.filter(([x, y]) => y === 0 && !view.warps.some((w) => w.from.cell.x === x && w.from.cell.y === y && w.from.side === "N"))
.find(([x]) => !state.squareContents[`${x},0`])!;
me.position = { x: top[0], y: 0 };
const column = Object.keys(view.cells).map((k) => k.split(",").map(Number) as [number, number]).filter(([x]) => x === top[0]).map(([, y]) => y);
const bottom = { x: top[0], y: Math.max(...column) };
giveCard(state, me.id, "teleport");
return { state, me, bottom };
}
it("re-enters at the bottom of the same column, one space on", () => {
const { state, me, bottom } = atTheTopEdge();
expect(wallIgnoringDistance(boardView(state), me.position, bottom, true)).toBe(1);
expect(eligibleCellsFor(viewFor(state, me.id), "teleport")!.has(cellKey(bottom))).toBe(true);
const r = applyCommand(state, me.id, { type: "cast", instanceId: "teleport#T", target: { kind: "cell", cell: bottom } });
expect(r.ok).toBe(true);
if (r.ok) expect(r.state.players.find((p) => p.id === me.id)!.position).toEqual(bottom);
});
it("older games cross only at the lettered openings", () => {
const { state, me, bottom } = atTheTopEdge(21);
expect(wallIgnoringDistance(boardView(state), me.position, bottom)).toBeGreaterThan(4);
expect(eligibleCellsFor(viewFor(state, me.id), "teleport")!.has(cellKey(bottom))).toBe(false);
const r = applyCommand(state, me.id, { type: "cast", instanceId: "teleport#T", target: { kind: "cell", cell: bottom } });
expect(r.ok).toBe(false);
});
});