Rev 19: a dust cloud blinds whoever stands in it (CPU7)
The automaton stood in Kestrel's DUST CLOUD and filled a square with stone three rows away. The cloud blocked only sight lines passing through it; the square a line began or ended in was never asked. The FAQ is plain: no LOS spell may be cast by a wizard in a cloud, nor at one, though a spell upon oneself still works, and VISIONSTONE does not see through dust. From rev 19 the engine's sight and the view's — the targeting aid the client and the automaton both read — refuse a line with dust at either end. Older games keep their sight. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jm2auWk6RP71CjaAb4FMoG
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
3d4bf1fa6d
commit
f859d90af8
@@ -238,7 +238,7 @@ export interface CastParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** The revision new games are dealt under; GameConfig.deckRev pins it per game. */
|
/** The revision new games are dealt under; GameConfig.deckRev pins it per game. */
|
||||||
export const CURRENT_RULES_REV = 18;
|
export const CURRENT_RULES_REV = 19;
|
||||||
|
|
||||||
/** Every rulings revision since the baseline, newest last — the entries a
|
/** Every rulings revision since the baseline, newest last — the entries a
|
||||||
* game's deckRev freezes it before or after. Shown to players as the house
|
* game's deckRev freezes it before or after. Shown to players as the house
|
||||||
@@ -261,6 +261,7 @@ export const RULES_REVISIONS: { rev: number; note: string }[] = [
|
|||||||
{ rev: 16, note: "BUTT-HEAD's charge is measured as far as the goat's legs reach. Older games searched only six steps of corridor and refused a longer ram as unreachable, even with the legs to make it." },
|
{ rev: 16, note: "BUTT-HEAD's charge is measured as far as the goat's legs reach. Older games searched only six steps of corridor and refused a longer ram as unreachable, even with the legs to make it." },
|
||||||
{ rev: 17, note: "A warp mouth on a pit's rim is a way off it like any square: a bare step beside one square and one mouth is a fork the walker names. In every game the mouth may be named, and is taken when no square offers; only an older game's bare step still lands on the square unasked." },
|
{ rev: 17, note: "A warp mouth on a pit's rim is a way off it like any square: a bare step beside one square and one mouth is a fork the walker names. In every game the mouth may be named, and is taken when no square offers; only an older game's bare step still lands on the square unasked." },
|
||||||
{ rev: 18, note: "REFLECTION's returning half is an attack on the caster in its own right, with the caster's own counteraction window — an ABSORB or a BLUNT meets it as it would any blow. Before, the half landed the instant the spell resolved." },
|
{ rev: 18, note: "REFLECTION's returning half is an attack on the caster in its own right, with the caster's own counteraction window — an ABSORB or a BLUNT meets it as it would any blow. Before, the half landed the instant the spell resolved." },
|
||||||
|
{ rev: 19, note: "DUST CLOUD blinds whoever stands in it: no LOS spell may be cast from inside a cloud, nor at anyone standing in one, and VISIONSTONE does not see through it. Spells cast on oneself still work. Before, the cloud blocked only sight lines passing through it." },
|
||||||
];
|
];
|
||||||
|
|
||||||
export interface GameConfig {
|
export interface GameConfig {
|
||||||
@@ -438,7 +439,18 @@ function sightThroughOneEdge(
|
|||||||
return throughOneEdge(board, (b) => sightBetween(b, from, to, blockers));
|
return throughOneEdge(board, (b) => sightBetween(b, from, to, blockers));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Rev 19: DUST CLOUD blinds whoever stands in it and hides them — "an
|
||||||
|
* LOS spell cannot be cast upon a player in DUST CLOUD, or by a player
|
||||||
|
* who is in a DUST CLOUD" (official FAQ); a spell cast upon oneself
|
||||||
|
* still works, and VISIONSTONE does not see through dust either. Before
|
||||||
|
* it the cloud blocked only sight lines passing through. */
|
||||||
|
function dustAtEnd(state: GameState, from: Cell, to: Cell): boolean {
|
||||||
|
if ((state.config.deckRev ?? 1) < 19 || cellKey(from) === cellKey(to)) return false;
|
||||||
|
return state.squareContents[cellKey(from)]?.kind === "dust" || state.squareContents[cellKey(to)]?.kind === "dust";
|
||||||
|
}
|
||||||
|
|
||||||
function losWith(state: GameState, from: Cell, to: Cell, viewerId: PlayerId | undefined, stone: boolean): boolean {
|
function losWith(state: GameState, from: Cell, to: Cell, viewerId: PlayerId | undefined, stone: boolean): boolean {
|
||||||
|
if (dustAtEnd(state, from, to)) return false;
|
||||||
const board = doorsAjar(state, viewerId, openedDoors(state, boardView(state)));
|
const board = doorsAjar(state, viewerId, openedDoors(state, boardView(state)));
|
||||||
const blockers = losBlockers(state);
|
const blockers = losBlockers(state);
|
||||||
if (sightBetween(board, from, to, blockers)) return true;
|
if (sightBetween(board, from, to, blockers)) return true;
|
||||||
|
|||||||
@@ -220,8 +220,14 @@ export function sightedCellsFor(view: GameView): Set<string> {
|
|||||||
const me = view.players.find((p) => p.id === view.you);
|
const me = view.players.find((p) => p.id === view.you);
|
||||||
if (!me) return out;
|
if (!me) return out;
|
||||||
const { board, blockers } = sightBasis(view);
|
const { board, blockers } = sightBasis(view);
|
||||||
|
// In a dust cloud a wizard sees only their own square (rev 19).
|
||||||
|
if (dustAtEnd(view, me.position, me.position, true)) {
|
||||||
|
out.add(`${me.position.x},${me.position.y}`);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
for (const key of Object.keys(board.cells)) {
|
for (const key of Object.keys(board.cells)) {
|
||||||
const [x, y] = key.split(",").map(Number) as [number, number];
|
const [x, y] = key.split(",").map(Number) as [number, number];
|
||||||
|
if (dustAtEnd(view, me.position, { x, y })) continue;
|
||||||
if (sightBetween(board, me.position, { x, y }, blockers)) out.add(key);
|
if (sightBetween(board, me.position, { x, y }, blockers)) out.add(key);
|
||||||
}
|
}
|
||||||
// VISIONSTONE lets its bearer see through exactly one wall or door —
|
// VISIONSTONE lets its bearer see through exactly one wall or door —
|
||||||
@@ -314,16 +320,29 @@ function sightBasis(view: GameView): { board: GameView["board"]; blockers: Recor
|
|||||||
* exists — the renderer's material for drawing the line an attack traveled.
|
* exists — the renderer's material for drawing the line an attack traveled.
|
||||||
*/
|
*/
|
||||||
export function traceSightFor(view: GameView, from: Cell, to: Cell): SightTrace | null {
|
export function traceSightFor(view: GameView, from: Cell, to: Cell): SightTrace | null {
|
||||||
|
if (dustAtEnd(view, from, to)) return null;
|
||||||
const { board, blockers } = sightBasis(view);
|
const { board, blockers } = sightBasis(view);
|
||||||
return traceSight(board, from, to, blockers);
|
return traceSight(board, from, to, blockers);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Rev 19: DUST CLOUD blinds its occupant and hides them from LOS spells;
|
||||||
|
* a square sees itself still. `standing` asks only whether the viewer's
|
||||||
|
* own square is dust. */
|
||||||
|
function dustAtEnd(view: GameView, from: Cell, to: Cell, standing = false): boolean {
|
||||||
|
if (view.deckRev < 19) return false;
|
||||||
|
const dust = (c: Cell) => view.squareContents[`${c.x},${c.y}`]?.kind === "dust";
|
||||||
|
if (standing) return dust(from);
|
||||||
|
if (from.x === to.x && from.y === to.y) return false;
|
||||||
|
return dust(from) || dust(to);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AROUND THE CORNER's bent sight, from this viewer's knowledge: the caster
|
* AROUND THE CORNER's bent sight, from this viewer's knowledge: the caster
|
||||||
* sees a middle square which sees the target — mirroring the engine's
|
* sees a middle square which sees the target — mirroring the engine's
|
||||||
* bentLos so a client can predict whether the modifier will land.
|
* bentLos so a client can predict whether the modifier will land.
|
||||||
*/
|
*/
|
||||||
export function bentSightFor(view: GameView, from: Cell, to: Cell): boolean {
|
export function bentSightFor(view: GameView, from: Cell, to: Cell): boolean {
|
||||||
|
if (dustAtEnd(view, from, to)) return false;
|
||||||
const { board, blockers } = sightBasis(view);
|
const { board, blockers } = sightBasis(view);
|
||||||
if (sightBetween(board, from, to, blockers)) return true;
|
if (sightBetween(board, from, to, blockers)) return true;
|
||||||
for (const key of Object.keys(board.cells)) {
|
for (const key of Object.keys(board.cells)) {
|
||||||
|
|||||||
@@ -145,6 +145,29 @@ describe("expansion terrain", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("a wizard in a dust cloud casts no LOS spell out, and none reaches them (rev 19)", () => {
|
||||||
|
for (const deckRev of [19, 18]) {
|
||||||
|
let { state } = createGame({ playerIds: ["alice", "bob"], seed: 42, sets: ["basic", "expansion1"], deckRev });
|
||||||
|
const me = activePlayer(state);
|
||||||
|
const spot = emptyNeighborCell(state, me.position);
|
||||||
|
state.squareContents[cellKey(spot.cell)] = { kind: "dust", damage: 0, createdBy: me.id };
|
||||||
|
me.position = { ...spot.cell };
|
||||||
|
// An empty square beside the cloud, in plain sight but for the dust.
|
||||||
|
const outside = emptyNeighborCell(state, spot.cell).cell;
|
||||||
|
const blind = deckRev >= 19;
|
||||||
|
expect(gameLos(state, me.position, outside, me.id)).toBe(!blind);
|
||||||
|
expect(gameLos(state, outside, me.position)).toBe(!blind);
|
||||||
|
expect(gameLos(state, me.position, me.position, me.id)).toBe(true);
|
||||||
|
const sighted = sightedCellsFor(viewFor(state, me.id));
|
||||||
|
expect(sighted.has(cellKey(outside))).toBe(!blind);
|
||||||
|
expect(sighted.has(cellKey(me.position))).toBe(true);
|
||||||
|
// FILL SQUARE WITH STONE from inside the cloud: refused when blind.
|
||||||
|
const stone = giveCard(state, me.id, "fill-square-with-stone");
|
||||||
|
const r = applyCommand(state, me.id, { type: "cast", instanceId: stone.instanceId, target: { kind: "cell", cell: outside } });
|
||||||
|
expect(r.ok).toBe(!blind);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("glue pins a treasure to the floor until it wears off", () => {
|
it("glue pins a treasure to the floor until it wears off", () => {
|
||||||
let { state } = newGame();
|
let { state } = newGame();
|
||||||
const me = activePlayer(state);
|
const me = activePlayer(state);
|
||||||
|
|||||||
Reference in New Issue
Block a user