Around the Corner bends every spell, hairpins included

Two holes, found when a SAFE cast at a treasure 'around the 180 degree
corner' refused with no line of sight (room LZ5R). First: the bend was
honored only in the attack path — every neutral resolve (creations,
summons, utilities, edge targets) checked straight sight and ignored
the attached card, though the card says 'any line of sight spell'.
castSight/castSightEdge now thread the bend through all of them, and
the chronicle narrates the neutral bend too.

Second: the bend itself pivoted only at cell centers, so the card's
flagship shot — doubling back up to 180 degrees around a wall's end —
never resolved: both legs graze the corner and die. The pivot may now
sit in the wall's open gap itself (bentSightThroughGap), which is what
rounding a corner physically is. Closed doors still block; two corners
still refuse.

The client lights bent-reachable squares for real now instead of the
old one-step-adjacency guess, creations included. Pure widening, so no
rev bump: every one of these casts was refused before, so no ledger
holds one. All 26 production ledgers replay clean; the LZ5R moment
itself replayed and the exact refused cast now lands.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015RCWSTnb1KYTPyL4GmhGnF
This commit is contained in:
Eric Wagoner
2026-08-27 17:34:48 -04:00
co-authored by Claude Fable 5
parent f53f26ace9
commit 3bf3c33fdc
5 changed files with 263 additions and 49 deletions
+31
View File
@@ -480,3 +480,34 @@ export function sightBetween(
return hasLineOfSight(board, from, to, blockedCells) ||
hasWarpLineOfSight(board, from, to, blockedCells);
}
/**
* AROUND THE CORNER's full reach: "around one corner (up to 180 degrees)".
* A hairpin around the end of a wall pivots IN the wall's gap, not at any
* cell's center — so try the midpoint of every open edge as the bend point,
* with a clear straight segment from caster to pivot and pivot to target.
*/
export function bentSightThroughGap(
board: AssembledBoard,
from: Cell,
to: Cell,
blockedCells?: Record<string, true>,
): boolean {
const fx = from.x + 0.5, fy = from.y + 0.5;
const tx = to.x + 0.5, ty = to.y + 0.5;
const skip = [cellKey(from), cellKey(to)];
for (const key of Object.keys(board.cells)) {
const [x, y] = key.split(",").map(Number) as [number, number];
const c = { x, y };
for (const side of ["E", "S"] as Side[]) {
if (!board.cells[cellKey(neighbor(c, side))]) continue;
if (edgeState(board, c, side) !== "open") continue;
const [px, py] = side === "E" ? [x + 1, y + 0.5] : [x + 0.5, y + 1];
if (segmentClear(board, fx, fy, px, py, blockedCells, skip) &&
segmentClear(board, px, py, tx, ty, blockedCells, skip)) {
return true;
}
}
}
return false;
}