Illusion walls shimmer until YOU test them (rules rev 29)

Per the owner's design — and truer to a real table, where the cast is
public: every illusion wall's location is open knowledge, rendered as a
shimmering wall to any player whose eyes have not yet ruled on it.
Testing is an explicit act: tap the shimmer (or bump into it) and a
modal explains the 50/50 and offers 'Roll to test your eyes' — the new
testIllusion command rolls once, then the wall either hardens or
dissolves for you alone. Nobody's verdict is moved by watching someone
else stroll through (the Q3WZ mystery: an automaton walking through
its own round-1 fake). Untested walls block movement and sight without
consuming RNG; the pre-29 lazy belief roll is preserved for stored
games. Automatons doubt free shimmers on their best crossing before
spending any card on that wall.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0138A8CjeQRpvzKxuMfz1Bqc
This commit is contained in:
Eric Wagoner
2026-08-18 01:54:53 -04:00
co-authored by Claude Fable 5
parent 500d9a7154
commit d5b2b92f2c
8 changed files with 215 additions and 8 deletions
+13
View File
@@ -70,6 +70,9 @@ 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
* shimmers, believes renders solid, mine/seesThrough are ghosts. */
illusionEdges: Record<string, "untested" | "believes" | "seesThrough" | "mine">;
creatures: CreatureState[];
/** Charges left on displayed wands (public), by card instance id. */
wandCharges: Record<string, number>;
@@ -90,11 +93,20 @@ 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.
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";
}
if (knows) {
knownIllusionEdges.push(key);
} else {
@@ -147,6 +159,7 @@ export function viewFor(state: GameState, playerId: PlayerId): GameView {
heldDoorEdges: state.heldDoors.map((h) => h.key),
createdEdges: Object.keys(state.createdEdges),
knownIllusionEdges,
illusionEdges,
creatures: state.creatures.map((c) => ({ ...c, scorchedThisTurn: [...c.scorchedThisTurn] })),
wandCharges: { ...state.wandCharges },
dimWarps: state.dimWarps.map((w) => ({ a: { ...w.a }, b: { ...w.b } })),