The stone pierces walls for creations too (rules rev 3)
VISIONSTONE only ever lit the attack path: casterLos knew it, but every sight check routed through gameLos — creations like THORNBUSH, dispels, CREATE DOOR, STONE TO WATER — refused "no line of sight" through the bearer's one wall. gameLos now honors the stone whenever the viewer bears it, at every revision: widening what a cast may target never changes how a recorded command replays, so games already in flight get the fix at once. Only the ambush's mid-move eye is frozen per game (rev 3) — springing lands in the ledger's flow, and stored games must replay their blindness. All 18 production ledgers verified; 272 engine tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015RCWSTnb1KYTPyL4GmhGnF
This commit is contained in:
co-authored by
Claude Fable 5
parent
7adf4b6cd6
commit
71a3c81670
@@ -226,7 +226,7 @@ export interface CastParams {
|
||||
}
|
||||
|
||||
/** The revision new games are dealt under; GameConfig.deckRev pins it per game. */
|
||||
export const CURRENT_RULES_REV = 2;
|
||||
export const CURRENT_RULES_REV = 3;
|
||||
|
||||
export interface GameConfig {
|
||||
playerIds: PlayerId[];
|
||||
@@ -242,6 +242,9 @@ export interface GameConfig {
|
||||
* Rev 2: a wizard beside a door they can open (lock removed, door
|
||||
* unlocked this turn, or PICK LOCK / MASTER KEY in hand) sees through
|
||||
* the doorway; the hallway behind them still cannot.
|
||||
* Rev 3: VISIONSTONE pierces its one wall for every sight the game
|
||||
* asks of its bearer — creations and utility spells included — not
|
||||
* only direct attacks.
|
||||
*/
|
||||
deckRev?: number;
|
||||
}
|
||||
@@ -378,8 +381,37 @@ function doorsAjar(state: GameState, viewerId: PlayerId | undefined, board: Asse
|
||||
}
|
||||
|
||||
/** LOS including square-filling blockers. */
|
||||
function losWith(state: GameState, from: Cell, to: Cell, viewerId: PlayerId | undefined, stone: boolean): boolean {
|
||||
const board = doorsAjar(state, viewerId, openedDoors(state, boardView(state)));
|
||||
const blockers = losBlockers(state);
|
||||
if (sightBetween(board, from, to, blockers)) return true;
|
||||
if (!stone) return false;
|
||||
const viewer = viewerId ? state.players.find((p) => p.id === viewerId) : undefined;
|
||||
if (!viewer || !viewer.alive || !displays(viewer, "visionstone")) return false;
|
||||
for (const key of Object.keys(board.edges)) {
|
||||
if ((board.edges[key] ?? "open") === "open") continue;
|
||||
const edges = { ...board.edges };
|
||||
delete edges[key];
|
||||
if (sightBetween({ ...board, edges }, from, to, blockers)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** LOS including square-filling blockers. VISIONSTONE is the bearer's
|
||||
* sight wherever sight is asked of them — creations, dispels, utility
|
||||
* spells — not just attacks: one wall or door, any type, falls away.
|
||||
* Safe at every revision: widening what a cast may target never changes
|
||||
* how a recorded command replays. */
|
||||
export function gameLos(state: GameState, from: Cell, to: Cell, viewerId?: PlayerId): boolean {
|
||||
return sightBetween(doorsAjar(state, viewerId, openedDoors(state, boardView(state))), from, to, losBlockers(state));
|
||||
return losWith(state, from, to, viewerId, true);
|
||||
}
|
||||
|
||||
/** The eye of an armed ambush. Springing is decided mid-move and lands
|
||||
* in the ledger's flow, so this sight is frozen per game: before rev 3
|
||||
* an ambush never looked through its owner's VISIONSTONE, and stored
|
||||
* games must replay that blindness. */
|
||||
function ambushLos(state: GameState, owner: PlayerState, from: Cell, to: Cell): boolean {
|
||||
return losWith(state, from, to, owner.id, (state.config.deckRev ?? 1) >= 3);
|
||||
}
|
||||
|
||||
/** Parse an edge key back into its north/west cell and side. */
|
||||
@@ -5167,8 +5199,8 @@ function checkAmbushes(
|
||||
sprung = context.pickedUpTreasure === true;
|
||||
} else if (context.movedFrom) {
|
||||
if (ambush.trigger.kind === "los") {
|
||||
const before = gameLos(state, owner.position, context.movedFrom, owner.id);
|
||||
const now = gameLos(state, owner.position, actor.position, owner.id);
|
||||
const before = ambushLos(state, owner, owner.position, context.movedFrom);
|
||||
const now = ambushLos(state, owner, owner.position, actor.position);
|
||||
sprung = now && !before;
|
||||
} else if (ambush.trigger.kind === "near") {
|
||||
const dist = (c: Cell) =>
|
||||
@@ -5180,7 +5212,7 @@ function checkAmbushes(
|
||||
|
||||
// The committed spell must be legal right now, or the ambush stays armed.
|
||||
const fx = CARD_EFFECTS[ambush.spell.cardId] as AttackEffect;
|
||||
if (fx.requiresLos && !gameLos(state, owner.position, actor.position, owner.id)) continue;
|
||||
if (fx.requiresLos && !ambushLos(state, owner, owner.position, actor.position)) continue;
|
||||
|
||||
state.ambushes = state.ambushes.filter((a) => a.id !== ambush.id);
|
||||
state.discard.push(ambush.via, ambush.spell, ...ambush.numbers);
|
||||
|
||||
Reference in New Issue
Block a user