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
+43 -4
View File
@@ -360,8 +360,10 @@ function parseEdgeKey(key: string): { cell: Cell; side: Side } {
}
/**
* What does this player believe about an illusion wall? Rolls the 50% chance
* lazily the first time it matters ("when they gain L.O.S. to it").
* What does this player believe about an illusion wall? Before rules rev 29
* the 50% chance rolled lazily the first time it mattered; from rev 29 the
* test is an explicit act (the testIllusion command) and an untested wall
* simply IS a wall to this player until they choose to doubt it.
*/
function illusionBelief(
state: GameState,
@@ -373,6 +375,7 @@ function illusionBelief(
if (wall.createdBy === playerId) return "seesThrough";
const known = wall.belief[playerId];
if (known) return known;
if ((state.config.deckRev ?? 1) >= 29) return "believes"; // untested blocks; no roll
const [roll, rngNext] = rollDie(state.rng);
state.rng = rngNext;
const result = roll <= 2 ? "seesThrough" : "believes";
@@ -649,6 +652,7 @@ export type Command =
| { type: "playNumberForMovement"; instanceId: string; addInstanceId?: string }
| { type: "punch"; targetId: PlayerId }
| { type: "punchWall"; cell: Cell; side: Side }
| { type: "testIllusion"; cell: Cell; side: Side }
| { type: "armWard"; armed: boolean }
| { type: "warpStep" }
| { type: "moveCreature"; creatureId: string; direction: Side }
@@ -3631,6 +3635,7 @@ function applyCommandInner(state: GameState, playerId: PlayerId, command: Comman
case "playNumberForMovement": return doPlayNumberForMovement(state, command.instanceId, command.addInstanceId);
case "punch": return doPunch(state, command.targetId);
case "punchWall": return doPunchWall(state, command.cell, command.side);
case "testIllusion": return doTestIllusion(state, command.cell, command.side);
case "armWard": return doArmWard(state, command.armed);
case "warpStep": return doWarpStep(state);
case "moveCreature": return doMoveCreature(state, command.creatureId, command.direction);
@@ -3721,8 +3726,13 @@ function doMove(prev: GameState, direction: Side, over = false): CommandResult {
// A believed (or untested, when bumped) ILLUSION WALL blocks the believer.
{
const key = edgeKey(p.position, direction);
if (state.illusionWalls[key] &&
illusionBelief(state, events, p.id, key) === "believes") {
const shimmer = state.illusionWalls[key];
if (shimmer && (state.config.deckRev ?? 1) >= 29 &&
shimmer.createdBy !== p.id && !shimmer.belief[p.id]) {
if (isBlinded(state, p)) return blindBump(state, events, p, direction);
return err("the wall shimmers oddly — test your eyes before you can pass");
}
if (shimmer && illusionBelief(state, events, p.id, key) === "believes") {
if (isBlinded(state, p)) return blindBump(state, events, p, direction);
return err("blocked by wall");
}
@@ -4232,6 +4242,35 @@ function finishChaosIfReady(state: GameState, events: GameEvent[]): void {
scrambleHands(state, events, pending.casterId, pending.excluded);
}
/**
* TEST AN ILLUSION (rules rev 29): a free act on your turn. Standing beside
* the shimmer or seeing it, you roll your 50% doubt the wall then either
* hardens (for you) or dissolves (for you). Nobody else's eyes are moved by
* your verdict, or by anyone strolling through it.
*/
function doTestIllusion(prev: GameState, cell: Cell, side: Side): CommandResult {
if ((prev.config.deckRev ?? 1) < 29) return err("illusions test themselves in this vintage of the rules");
const state = clone(prev);
const p = activePlayer(state);
const key = edgeKey(cell, side);
const wall = state.illusionWalls[key];
if (!wall) return err("there is no illusion there");
if (wall.createdBy === p.id) return err("you made it — you know exactly what it is");
if (wall.belief[p.id]) return err("your eyes have already ruled on that wall");
const events: GameEvent[] = [];
const board = openHeldDoors(state, perceivedBoard(state, events, p.id));
if (!isAdjacentToEdge(p.position, cell, side) &&
!losToEdge(board, p.position, cell, side)) {
return err("you cannot see that wall from here");
}
const [roll, rngNext] = rollDie(state.rng);
state.rng = rngNext;
const result = roll <= 2 ? "seesThrough" : "believes";
state.illusionWalls[key]!.belief[p.id] = result;
events.push({ type: "illusionTested", player: p.id, edge: key, result });
return { ok: true, state, events };
}
function doPunchWall(prev: GameState, cell: Cell, side: Side): CommandResult {
const pre = attackPreconditions(prev);
if (pre) return err(pre);