The client's eye learns the VISIONSTONE
The engine always honored the stone — its bearer casts through exactly one wall or door — but the targeting shadow never did: sightedCellsFor kept every square behind a wall dimmed, so the aid told the bearer the stone wasn't working. The sighted set now retries each unseen square with a single edge removed, mirroring casterLos; the automaton shares the function, so bots see through their stones too. Two tests pin the engine ruling and the client parity. 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
b16bbe82a6
commit
7adf4b6cd6
@@ -200,6 +200,26 @@ export function sightedCellsFor(view: GameView): Set<string> {
|
||||
const [x, y] = key.split(",").map(Number) as [number, number];
|
||||
if (sightBetween(board, me.position, { x, y }, blockers)) out.add(key);
|
||||
}
|
||||
// VISIONSTONE lets its bearer see through exactly one wall or door —
|
||||
// any one — so a square is also sighted if removing a single edge
|
||||
// reveals it, mirroring the engine's casterLos.
|
||||
if (me.displayed.some((c) => c.cardId === "visionstone")) {
|
||||
const unseen = Object.keys(board.cells).filter((k) => !out.has(k));
|
||||
for (const edgeK of Object.keys(board.edges)) {
|
||||
if (unseen.length === 0) break;
|
||||
if ((board.edges[edgeK] ?? "open") === "open") continue;
|
||||
const edges = { ...board.edges };
|
||||
delete edges[edgeK];
|
||||
const opened = { ...board, edges };
|
||||
for (let i = unseen.length - 1; i >= 0; i--) {
|
||||
const [x, y] = unseen[i]!.split(",").map(Number) as [number, number];
|
||||
if (sightBetween(opened, me.position, { x, y }, blockers)) {
|
||||
out.add(unseen[i]!);
|
||||
unseen.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest";
|
||||
import { applyCommand, activePlayer, boardView, createGame, type GameState } from "../src/game";
|
||||
import { cellKey, edgeKey, hasLineOfSight, neighbor, type Cell, type Side, SIDES, stepTarget } from "../src/board";
|
||||
import type { CardInstance } from "../src/cards";
|
||||
import { viewFor } from "../src/view";
|
||||
import { sightedCellsFor, viewFor } from "../src/view";
|
||||
import { newGame, must, giveCard, toRound2, faceOff } from "./helpers";
|
||||
|
||||
/** Test surgery: put a specific card into a player's hand (swapping one out). */
|
||||
@@ -500,6 +500,42 @@ describe("zero-damage utility attacks", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("the VISIONSTONE pierces one wall", () => {
|
||||
function stoneRig() {
|
||||
let { state } = newGame();
|
||||
state = toRound2(state);
|
||||
const a = activePlayer(state);
|
||||
const b = state.players.find((p) => p.id !== a.id)!;
|
||||
a.position = { x: 2, y: 4 };
|
||||
b.position = { x: 2, y: 5 };
|
||||
state.edgeOverrides[edgeKey({ x: 2, y: 4 }, "S")] = "wall";
|
||||
return { state, a, b };
|
||||
}
|
||||
|
||||
it("its bearer casts an attack straight through the wall", () => {
|
||||
const { state, a, b } = stoneRig();
|
||||
const fb = giveCard(state, a.id, "fireball", "FB", 1);
|
||||
const refused = applyCommand(state, a.id, {
|
||||
type: "cast", instanceId: fb.instanceId, target: { kind: "player", playerId: b.id },
|
||||
});
|
||||
expect(refused.ok).toBe(false);
|
||||
const stone = giveCard(state, a.id, "visionstone", "VS");
|
||||
a.displayed.push(stone.instanceId);
|
||||
const cast = applyCommand(state, a.id, {
|
||||
type: "cast", instanceId: fb.instanceId, target: { kind: "player", playerId: b.id },
|
||||
});
|
||||
expect(cast.ok).toBe(true);
|
||||
});
|
||||
|
||||
it("the client's sighted squares agree with the stone", () => {
|
||||
const { state, a, b } = stoneRig();
|
||||
expect(sightedCellsFor(viewFor(state, a.id)).has(cellKey(b.position))).toBe(false);
|
||||
const stone = giveCard(state, a.id, "visionstone", "VS");
|
||||
a.displayed.push(stone.instanceId);
|
||||
expect(sightedCellsFor(viewFor(state, a.id)).has(cellKey(b.position))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("teleport wraps the maze's rim", () => {
|
||||
it("four spaces counted through a warp mouth reach the far side", () => {
|
||||
let { state } = newGame();
|
||||
|
||||
Reference in New Issue
Block a user