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];
|
const [x, y] = key.split(",").map(Number) as [number, number];
|
||||||
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 —
|
||||||
|
// 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;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest";
|
|||||||
import { applyCommand, activePlayer, boardView, createGame, type GameState } from "../src/game";
|
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 { cellKey, edgeKey, hasLineOfSight, neighbor, type Cell, type Side, SIDES, stepTarget } from "../src/board";
|
||||||
import type { CardInstance } from "../src/cards";
|
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";
|
import { newGame, must, giveCard, toRound2, faceOff } from "./helpers";
|
||||||
|
|
||||||
/** Test surgery: put a specific card into a player's hand (swapping one out). */
|
/** 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", () => {
|
describe("teleport wraps the maze's rim", () => {
|
||||||
it("four spaces counted through a warp mouth reach the far side", () => {
|
it("four spaces counted through a warp mouth reach the far side", () => {
|
||||||
let { state } = newGame();
|
let { state } = newGame();
|
||||||
|
|||||||
Reference in New Issue
Block a user