The great simplification: every vintage gate collapses, the count restarts at 1

All 50 rooms were retired (backed up on the droplet and in Spaces), so no
ledger needs an old branch to replay: forty revisions of deckRev gates
fold into one canonical ruleset — the newest behavior everywhere. The
ward's arming, the idiot's steering, the lazy illusion roll, the legacy
redirection swap, and the flat wave all leave with their gates; RULES_REV
restarts at 1 for whenever the rules fork again. Old-vintage test pins go
with them. The opening screen now quietly drops seats whose rooms the
server no longer knows, instead of listing them "unreachable" forever.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Eric Wagoner
2026-08-21 12:45:35 -04:00
co-authored by Claude Fable 5
parent e8df6efcde
commit 8a50ff7f36
19 changed files with 383 additions and 685 deletions
+11 -16
View File
@@ -42,7 +42,8 @@ export interface GameView {
winReason: "treasures" | "lastStanding" | null;
turn: TurnState;
activePlayerId: PlayerId;
/** The game's rules revision — the client mirrors rev-gated legality. */
/** The game's rules revision (a fresh count from 1; gates return if the
* rules ever fork again while games are live). */
deckRev: number;
/** Board with dynamic wall changes already merged in. */
board: AssembledBoard;
@@ -70,7 +71,7 @@ export interface GameView {
createdEdges: string[];
/** Illusion edges YOU know are fake (creator or saw through); others see walls. */
knownIllusionEdges: string[];
/** Every illusion edge and YOUR verdict on it (rules rev 29+): untested
/** Every illusion edge and YOUR verdict on it: untested
* shimmers, believes renders solid, mine/seesThrough are ghosts. */
illusionEdges: Record<string, "untested" | "believes" | "seesThrough" | "mine">;
creatures: CreatureState[];
@@ -82,9 +83,7 @@ export interface GameView {
outOfTurnWindow: { playerId: PlayerId; kind: "interrupt" | "opportunity-fire" } | null;
/** CHAOS shield windows in progress (public: everyone sees it coming). */
chaosPending: { casterId: PlayerId; queue: PlayerId[] } | null;
/** Whether YOUR ward is set to spring. */
yourWardArmed: boolean;
/** A grab hangs while the treasure's owner decides their Ward (rev 31). */
/** A grab hangs while the treasure's owner decides their Ward. */
wardPending: { ownerId: PlayerId; takerId: PlayerId } | null;
/** YOUR armed ambushes. Other players' ambushes are invisible. */
yourAmbushes: AmbushState[];
@@ -95,20 +94,17 @@ export interface GameView {
export function viewFor(state: GameState, playerId: PlayerId): GameView {
const you = state.players.find((p) => p.id === playerId);
// Illusion walls render as real walls unless this viewer knows better.
// From rules rev 29 their locations are open knowledge (the cast is
// public at a table) — what stays personal is each player's verdict.
// Their locations are open knowledge (the cast is public at a table) —
// what stays personal is each player's verdict.
const base = boardView(state);
const rev29 = (state.config.deckRev ?? 1) >= 29;
const knownIllusionEdges: string[] = [];
const illusionEdges: Record<string, "untested" | "believes" | "seesThrough" | "mine"> = {};
let edges = base.edges;
for (const [key, wall] of Object.entries(state.illusionWalls)) {
const knows = wall.createdBy === playerId || wall.belief[playerId] === "seesThrough";
if (rev29) {
illusionEdges[key] =
wall.createdBy === playerId ? "mine"
: wall.belief[playerId] ?? "untested";
}
illusionEdges[key] =
wall.createdBy === playerId ? "mine"
: wall.belief[playerId] ?? "untested";
if (knows) {
knownIllusionEdges.push(key);
} else {
@@ -169,7 +165,6 @@ export function viewFor(state: GameState, playerId: PlayerId): GameView {
chaosPending: state.chaosPending
? { casterId: state.chaosPending.casterId, queue: [...state.chaosPending.queue] }
: null,
yourWardArmed: state.wardArmed.includes(playerId),
wardPending: state.wardPending ? { ...state.wardPending } : null,
yourAmbushes: state.ambushes
.filter((a) => a.ownerId === playerId)
@@ -205,9 +200,9 @@ export function sightedCellsFor(view: GameView): Set<string> {
/** The board-as-seen and sight blockers this view's sight rules run against. */
function sightBasis(view: GameView): { board: GameView["board"]; blockers: Record<string, true> } {
// Held-open doors are open doorways to the eye (rules rev 15).
// Held-open doors are open doorways to the eye.
let board = view.board;
if (view.deckRev >= 15 && view.heldDoorEdges.length > 0) {
if (view.heldDoorEdges.length > 0) {
const edges = { ...board.edges };
for (const k of view.heldDoorEdges) delete edges[k];
board = { ...board, edges };