feat(chess/modifiers): add 7 new attr consumers + pre-move check/promotion snapshots
Combines T3 (consumer registrations for the 7 new hook attrs) and T4 (pre-move state snapshots) since both modify apply.ts and gate the incoming wave of trigger primitives. T3 — Consumer registrations: Adds registerAttrConsumer() calls for the 7 trigger hook attrs added to ChessAttrMap (OnMoveHooks, OnTurnEndHooks, OnPromotionHooks, OnCheckReceivedHooks, OnCheckDeliveredHooks, OnMovedOntoSquareHooks, OnCapturedHooks). Without these, the load-time assertSeedConsumerIntegrity() check would fail loudly when the upcoming Wave 2 primitives start writing the attrs. T4 — Pre-move snapshots: Two new per-engine WeakMaps mirror the existing PRE_MOVE_HP_SNAPSHOTS lifecycle: - PRE_MOVE_CHECK_STATE_SNAPSHOTS: per-color royal IDs + their attacker IDs at onBeforeMove time, used by the upcoming on-check-received (edge-triggered transition into check) and on-check-delivered (revealed-attacker discovered check) evaluators. - PRE_MOVE_PROMOTION_PAWNS: pawn IDs eligible to promote this move (white pawns on rank 6, black pawns on rank 1) — feeds the upcoming on-promotion evaluator without re-walking the board post-move. Snapshots populate in onBeforeMove, expose read-only getters (getPreMoveCheckState, getPreMovePromotionPawns), and clear in onAfterMove inside try/finally to prevent leaks even if a trigger evaluator throws. Reuses the engine's existing attackProbe helper (via PIECE_TYPE_REGISTRY) and getActiveRoyalEntityIds for preset-aware royal resolution rather than computing check lines from scratch. Adds 4 new tests in apply.test.ts: snapshot capture, promotion-pawn flagging, post-move cleanup, and no-leak across 3 sequential moves.
This commit is contained in:
parent
3b6f79ac74
commit
8f6c666ade
2 changed files with 418 additions and 17 deletions
|
|
@ -11,12 +11,19 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
|
|||
import { Session } from "@paratype/rete";
|
||||
import type { EntityId } from "@paratype/rete";
|
||||
import { applyLayout, CLASSIC_LAYOUT } from "../starting-position.js";
|
||||
import { applyProfileToSession } from "./apply.js";
|
||||
import {
|
||||
applyProfileToSession,
|
||||
getPreMoveCheckState,
|
||||
getPreMovePromotionPawns,
|
||||
} from "./apply.js";
|
||||
import type { ModifierProfile } from "./types.js";
|
||||
import { CaptureFlag } from "../schema.js";
|
||||
import { CaptureFlag, GAME_ENTITY } from "../schema.js";
|
||||
import { ChessEngine } from "../engine.js";
|
||||
import { clearBoard, placePiece, pieceAt } from "../presets/test-utils.js";
|
||||
// Side-effect import — ensures every descriptor is registered so
|
||||
// `MODIFIER_REGISTRY.get(kind)` resolves in applyProfileToSession.
|
||||
import "./index.js";
|
||||
import "../presets/index.js";
|
||||
|
||||
/**
|
||||
* Helper: return EntityIds of pieces matching the given filter. We
|
||||
|
|
@ -274,3 +281,198 @@ describe("applyProfileToSession", () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* T4 — pre-move snapshot WeakMaps.
|
||||
*
|
||||
* These tests drive the integration preset end-to-end via a real
|
||||
* ChessEngine (the WeakMaps are module-local and only populated by
|
||||
* the preset's onBeforeMove hook). A minimal no-op profile is enough
|
||||
* to auto-activate the integration preset; the pre-move snapshots
|
||||
* are captured for every engine that has the preset live, regardless
|
||||
* of what the profile actually configures.
|
||||
*
|
||||
* Intercepting the snapshot while it's still live is done by
|
||||
* registering a second preset whose own onBeforeMove runs AFTER the
|
||||
* integration preset's (preset iteration order is registration order;
|
||||
* the integration preset is prepended so any user preset runs after
|
||||
* it). That second hook reads the snapshot via the public getter and
|
||||
* copies the data out for the test to assert on.
|
||||
*/
|
||||
describe("T4 pre-move snapshot WeakMaps", () => {
|
||||
/** Minimal profile that just triggers integration-preset auto-activation. */
|
||||
const NOOP_PROFILE: ModifierProfile = {
|
||||
id: "t4-noop",
|
||||
name: "t4-noop",
|
||||
description: "",
|
||||
perType: [],
|
||||
perInstance: [],
|
||||
version: 1,
|
||||
source: "custom",
|
||||
};
|
||||
|
||||
/**
|
||||
* Register (or re-register) a test-only preset with an onBeforeMove
|
||||
* hook that records the live snapshot into `captured`. Returns the
|
||||
* preset id so the test can activate it. We rely on PRESET_REGISTRY
|
||||
* accepting idempotent re-registration via the standard register()
|
||||
* call — tests using unique ids don't collide across runs.
|
||||
*/
|
||||
async function registerSnapshotCaptor(
|
||||
presetId: string,
|
||||
captured: {
|
||||
checkState?: ReturnType<typeof getPreMoveCheckState>;
|
||||
promotionPawns?: ReadonlySet<EntityId>;
|
||||
},
|
||||
): Promise<void> {
|
||||
const { PRESET_REGISTRY } = await import("../presets/registry.js");
|
||||
PRESET_REGISTRY.register({
|
||||
id: presetId,
|
||||
name: presetId,
|
||||
description: "T4 test captor",
|
||||
incompatibleWith: [],
|
||||
requires: [],
|
||||
onBeforeMove(ctx): void {
|
||||
// Clone the live snapshot into the outer slot. The integration
|
||||
// preset's onBeforeMove runs BEFORE this one (it's registered
|
||||
// first in the active set), so the snapshot is live here.
|
||||
const live = getPreMoveCheckState(ctx.engine);
|
||||
if (live !== undefined) {
|
||||
captured.checkState = {
|
||||
white: new Map(live.white),
|
||||
black: new Map(live.black),
|
||||
};
|
||||
}
|
||||
const promo = getPreMovePromotionPawns(ctx.engine);
|
||||
if (promo !== undefined) {
|
||||
captured.promotionPawns = new Set(promo);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** Build an engine with the integration preset + the test captor live. */
|
||||
function makeEngineWithCaptor(presetId: string): ChessEngine {
|
||||
const engine = new ChessEngine({ profile: NOOP_PROFILE });
|
||||
const existing = engine.activePresets.list();
|
||||
engine.activePresets.replaceAll([
|
||||
...existing.map((e) => ({
|
||||
id: e.id,
|
||||
scope: e.scope,
|
||||
turnsRemaining: e.turnsRemaining,
|
||||
})),
|
||||
{ id: presetId, scope: "both" as const, turnsRemaining: null },
|
||||
]);
|
||||
return engine;
|
||||
}
|
||||
|
||||
it("T4 pre-move check snapshot captured (white king attacked by black rook)", async () => {
|
||||
const captured: {
|
||||
checkState?: ReturnType<typeof getPreMoveCheckState>;
|
||||
} = {};
|
||||
await registerSnapshotCaptor("__t4-check-captor__", captured);
|
||||
|
||||
const engine = makeEngineWithCaptor("__t4-check-captor__");
|
||||
|
||||
// Minimal position: white king e1, black rook e4 (checks king
|
||||
// down the e-file; white queen on d1 blocks nothing). We need a
|
||||
// black king somewhere harmless; h8.
|
||||
clearBoard(engine, { preserveKings: false });
|
||||
placePiece(engine, "king", "white", "e1");
|
||||
placePiece(engine, "king", "black", "h8");
|
||||
placePiece(engine, "rook", "black", "e4");
|
||||
engine.session.insert(GAME_ENTITY, "Turn", "white");
|
||||
|
||||
const whiteKingId = pieceAt(engine, "e1")!;
|
||||
const blackRookId = pieceAt(engine, "e4")!;
|
||||
expect(whiteKingId).not.toBeNull();
|
||||
expect(blackRookId).not.toBeNull();
|
||||
|
||||
// White's only sensible move: king out of check. Any legal move
|
||||
// the engine produces will do — we just need onBeforeMove to fire.
|
||||
const moves = engine.getAllLegalMoves();
|
||||
expect(moves.length).toBeGreaterThan(0);
|
||||
engine.applyMove(moves[0]!);
|
||||
|
||||
// Snapshot must have been captured and include the black rook as
|
||||
// an attacker of the white king.
|
||||
expect(captured.checkState).toBeDefined();
|
||||
const whiteRoyalAttackers = captured.checkState!.white.get(whiteKingId);
|
||||
expect(whiteRoyalAttackers).toBeDefined();
|
||||
expect(whiteRoyalAttackers!).toContain(blackRookId);
|
||||
|
||||
// Black king was not attacked pre-move → empty list.
|
||||
const blackKingId = pieceAt(engine, "h8")!;
|
||||
expect(captured.checkState!.black.get(blackKingId)).toEqual([]);
|
||||
});
|
||||
|
||||
it("T4 pre-move promotion pawns flag (white pawn on 7th rank)", async () => {
|
||||
const captured: { promotionPawns?: ReadonlySet<EntityId> } = {};
|
||||
await registerSnapshotCaptor("__t4-promo-captor__", captured);
|
||||
|
||||
const engine = makeEngineWithCaptor("__t4-promo-captor__");
|
||||
|
||||
clearBoard(engine, { preserveKings: false });
|
||||
placePiece(engine, "king", "white", "a1");
|
||||
placePiece(engine, "king", "black", "h8");
|
||||
// White pawn on a7 — rank 6, eligible to promote on the next push.
|
||||
const promoPawnId = placePiece(engine, "pawn", "white", "a7");
|
||||
// A non-promoting pawn for contrast on rank 1 (white pawn on b2).
|
||||
const normalPawnId = placePiece(engine, "pawn", "white", "b2");
|
||||
engine.session.insert(GAME_ENTITY, "Turn", "white");
|
||||
|
||||
const moves = engine.getAllLegalMoves();
|
||||
expect(moves.length).toBeGreaterThan(0);
|
||||
engine.applyMove(moves[0]!);
|
||||
|
||||
expect(captured.promotionPawns).toBeDefined();
|
||||
expect(captured.promotionPawns!.has(promoPawnId)).toBe(true);
|
||||
expect(captured.promotionPawns!.has(normalPawnId)).toBe(false);
|
||||
});
|
||||
|
||||
it("T4 snapshot cleared after onAfterMove", () => {
|
||||
const engine = new ChessEngine({ profile: NOOP_PROFILE });
|
||||
|
||||
// Pre-move: no snapshot yet.
|
||||
expect(getPreMoveCheckState(engine)).toBeUndefined();
|
||||
expect(getPreMovePromotionPawns(engine)).toBeUndefined();
|
||||
|
||||
const moves = engine.getAllLegalMoves();
|
||||
const e2e4 = moves.find((m) => m.from === 12 && m.to === 28);
|
||||
expect(e2e4).toBeDefined();
|
||||
engine.applyMove(e2e4!);
|
||||
|
||||
// Post-move: the onAfterMove hook deletes both maps as its last
|
||||
// action. Neither should be accessible any more.
|
||||
expect(getPreMoveCheckState(engine)).toBeUndefined();
|
||||
expect(getPreMovePromotionPawns(engine)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("T4 no leak across moves (3 sequential moves)", () => {
|
||||
const engine = new ChessEngine({ profile: NOOP_PROFILE });
|
||||
|
||||
// Move 1: e2-e4 (white).
|
||||
let moves = engine.getAllLegalMoves();
|
||||
const e2e4 = moves.find((m) => m.from === 12 && m.to === 28);
|
||||
expect(e2e4).toBeDefined();
|
||||
engine.applyMove(e2e4!);
|
||||
expect(getPreMoveCheckState(engine)).toBeUndefined();
|
||||
expect(getPreMovePromotionPawns(engine)).toBeUndefined();
|
||||
|
||||
// Move 2: e7-e5 (black).
|
||||
moves = engine.getAllLegalMoves();
|
||||
const e7e5 = moves.find((m) => m.from === 52 && m.to === 36);
|
||||
expect(e7e5).toBeDefined();
|
||||
engine.applyMove(e7e5!);
|
||||
expect(getPreMoveCheckState(engine)).toBeUndefined();
|
||||
expect(getPreMovePromotionPawns(engine)).toBeUndefined();
|
||||
|
||||
// Move 3: g1-f3 (white knight).
|
||||
moves = engine.getAllLegalMoves();
|
||||
const g1f3 = moves.find((m) => m.from === 6 && m.to === 21);
|
||||
expect(g1f3).toBeDefined();
|
||||
engine.applyMove(g1f3!);
|
||||
expect(getPreMoveCheckState(engine)).toBeUndefined();
|
||||
expect(getPreMovePromotionPawns(engine)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ import { stackResistances, applyResistance } from "./descriptors/damage-resistan
|
|||
import { hasCaptureFlag } from "./descriptors/capture-flags.js";
|
||||
import { generateDirectionMoves } from "./descriptors/direction-additions.js";
|
||||
import { PRESET_REGISTRY } from "../presets/registry.js";
|
||||
import { PIECE_TYPE_REGISTRY } from "../presets/piece-type-registry.js";
|
||||
import type { LegalMove } from "../rules/types.js";
|
||||
import { getPieceAt } from "../rules/board-queries.js";
|
||||
import type { ChessEngine } from "../engine.js";
|
||||
|
|
@ -96,6 +97,14 @@ registerAttrConsumer("OnCaptureHooks");
|
|||
registerAttrConsumer("OnDamagedHooks");
|
||||
registerAttrConsumer("ConditionalHooks");
|
||||
registerAttrConsumer("AuraSpec");
|
||||
// T3-extension trigger hook attrs (read by triggers.ts evaluators added in T12)
|
||||
registerAttrConsumer("OnMoveHooks");
|
||||
registerAttrConsumer("OnTurnEndHooks");
|
||||
registerAttrConsumer("OnPromotionHooks");
|
||||
registerAttrConsumer("OnCheckReceivedHooks");
|
||||
registerAttrConsumer("OnCheckDeliveredHooks");
|
||||
registerAttrConsumer("OnCapturedHooks");
|
||||
registerAttrConsumer("OnMovedOntoSquareHooks");
|
||||
|
||||
/**
|
||||
* Per-engine pre-move HP snapshot, used by the on-damaged trigger
|
||||
|
|
@ -133,6 +142,174 @@ function classifyNonCaptureMove(from: number, to: number): "step" | "slide" {
|
|||
*/
|
||||
const PRE_MOVE_CAPTURE_ATTACKERS = new WeakMap<ChessEngine, EntityId>();
|
||||
|
||||
/**
|
||||
* Pre-move check-state snapshot. For each color, maps every royal's
|
||||
* EntityId to the list of enemy attacker EntityIds threatening it
|
||||
* BEFORE the move. Edge-triggered on-check-received / on-check-delivered
|
||||
* evaluators compare this to the post-move state to determine whether
|
||||
* the move transitioned into or out of check for each royal.
|
||||
*
|
||||
* Empty attacker list → the royal is NOT in check pre-move. A royal
|
||||
* missing from the map entirely means "no royalty of this color"
|
||||
* (preset-dependent; see ChessEngine.getActiveRoyalEntityIds).
|
||||
*
|
||||
* Maps are built fresh each onBeforeMove and deleted at the END of
|
||||
* onAfterMove (after all fire*Hooks run), so evaluators can read them
|
||||
* but state never leaks across moves.
|
||||
*/
|
||||
export interface PreMoveCheckState {
|
||||
readonly white: ReadonlyMap<EntityId, readonly EntityId[]>;
|
||||
readonly black: ReadonlyMap<EntityId, readonly EntityId[]>;
|
||||
}
|
||||
|
||||
const PRE_MOVE_CHECK_STATE_SNAPSHOTS = new WeakMap<
|
||||
ChessEngine,
|
||||
PreMoveCheckState
|
||||
>();
|
||||
|
||||
/**
|
||||
* Pre-move promotion-candidate snapshot: set of pawn EntityIds that
|
||||
* sit on their penultimate rank BEFORE the move. Coarse filter — a
|
||||
* pawn in the set MIGHT promote this move (iff the move is actually
|
||||
* that pawn advancing to the last rank and it is legal). Evaluators
|
||||
* for on-promotion triggers use this to avoid redundant rank checks
|
||||
* and to distinguish "promotion happened this move" from "promotion
|
||||
* happened on a prior move" when inspecting post-move state.
|
||||
*/
|
||||
const PRE_MOVE_PROMOTION_PAWNS = new WeakMap<ChessEngine, Set<EntityId>>();
|
||||
|
||||
/**
|
||||
* Read-only accessor for the pre-move check-state snapshot. Returns
|
||||
* `undefined` when no snapshot is currently held for the engine — the
|
||||
* normal state outside of an onBeforeMove/onAfterMove window, or when
|
||||
* the integration preset is inactive.
|
||||
*/
|
||||
export function getPreMoveCheckState(
|
||||
engine: ChessEngine,
|
||||
): PreMoveCheckState | undefined {
|
||||
return PRE_MOVE_CHECK_STATE_SNAPSHOTS.get(engine);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read-only accessor for the pre-move promotion-candidate snapshot.
|
||||
* Returns `undefined` when no snapshot is currently held (outside a
|
||||
* move window or integration preset inactive).
|
||||
*/
|
||||
export function getPreMovePromotionPawns(
|
||||
engine: ChessEngine,
|
||||
): ReadonlySet<EntityId> | undefined {
|
||||
return PRE_MOVE_PROMOTION_PAWNS.get(engine);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a per-color royal→attackers map by walking the preset-resolved
|
||||
* royal set for each color and asking each enemy piece's `attackProbe`
|
||||
* whether it threatens the royal's current square. Mirrors the
|
||||
* check-detection path in `rules/check.ts::isInCheck` / `isSquareAttacked`
|
||||
* but records the attackers rather than returning a boolean — needed
|
||||
* so the on-check-received evaluator can report which piece delivered
|
||||
* the check, and so on-check-delivered can attribute the threat.
|
||||
*
|
||||
* Runs O(royals × enemy pieces) per color, which is cheap in practice
|
||||
* (≤ 2 royals × ≤ 16 enemies = ≤ 32 probe calls per color).
|
||||
*/
|
||||
function captureCheckStateForColor(
|
||||
engine: ChessEngine,
|
||||
color: PieceColor,
|
||||
): ReadonlyMap<EntityId, readonly EntityId[]> {
|
||||
const out = new Map<EntityId, readonly EntityId[]>();
|
||||
const session = engine.session;
|
||||
const facts = session.allFacts();
|
||||
|
||||
// Royal resolution mirrors `rules/check.ts::isInCheck`:
|
||||
// - preset-contributed set → use as-is (empty = no royalty, skip).
|
||||
// - undefined (no preset touched royalty) → fall back to "every
|
||||
// PieceType=king of this color", so the snapshot is meaningful
|
||||
// even in FIDE-default / no-preset configurations.
|
||||
const presetRoyals = engine.getActiveRoyalEntityIds(color);
|
||||
let royals: readonly EntityId[];
|
||||
if (presetRoyals !== undefined) {
|
||||
if (presetRoyals.length === 0) return out;
|
||||
royals = presetRoyals;
|
||||
} else {
|
||||
const defaultIds: EntityId[] = [];
|
||||
for (const f of facts) {
|
||||
if (f.attr !== "PieceType" || f.value !== "king") continue;
|
||||
if ((f.id as number) <= 0) continue;
|
||||
const cf = facts.find((c) => c.id === f.id && c.attr === "Color");
|
||||
if (cf !== undefined && cf.value === color) defaultIds.push(f.id);
|
||||
}
|
||||
if (defaultIds.length === 0) return out;
|
||||
royals = defaultIds;
|
||||
}
|
||||
|
||||
const attackerColor: PieceColor = color === "white" ? "black" : "white";
|
||||
|
||||
// Pre-filter enemy (id, type) once so each royal iteration reuses.
|
||||
const enemies: Array<{ id: EntityId; type: PieceType }> = [];
|
||||
for (const f of facts) {
|
||||
if (f.attr !== "Color" || f.value !== attackerColor) continue;
|
||||
if ((f.id as number) <= 0) continue;
|
||||
const typeFact = facts.find(
|
||||
(t) => t.id === f.id && t.attr === "PieceType",
|
||||
);
|
||||
if (typeFact === undefined) continue;
|
||||
enemies.push({ id: f.id, type: typeFact.value as PieceType });
|
||||
}
|
||||
|
||||
for (const royalId of royals) {
|
||||
const posFact = facts.find(
|
||||
(f) => f.id === royalId && f.attr === "Position",
|
||||
);
|
||||
if (posFact === undefined) {
|
||||
out.set(royalId, []);
|
||||
continue;
|
||||
}
|
||||
const royalSquare = posFact.value as Square;
|
||||
const attackers: EntityId[] = [];
|
||||
for (const enemy of enemies) {
|
||||
const def = PIECE_TYPE_REGISTRY.get(enemy.type);
|
||||
if (def === undefined) continue;
|
||||
if (def.attackProbe(session, enemy.id, royalSquare)) {
|
||||
attackers.push(enemy.id);
|
||||
}
|
||||
}
|
||||
out.set(royalId, attackers);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enumerate every pawn currently on its penultimate rank. White pawns
|
||||
* on rank 6 (squares 48..55) could promote by advancing to rank 7;
|
||||
* black pawns on rank 1 (squares 8..15) could promote by advancing to
|
||||
* rank 0. This is the CANDIDATE set — movegen / move legality is what
|
||||
* actually determines whether a promotion occurs; downstream
|
||||
* on-promotion evaluators intersect this with the post-move state to
|
||||
* detect "a flagged pawn is no longer a pawn" == promoted.
|
||||
*/
|
||||
function capturePromotionCandidates(session: Session): Set<EntityId> {
|
||||
const candidates = new Set<EntityId>();
|
||||
const facts = session.allFacts();
|
||||
for (const f of facts) {
|
||||
if (f.attr !== "PieceType" || f.value !== "pawn") continue;
|
||||
if ((f.id as number) <= 0) continue;
|
||||
const posFact = facts.find((p) => p.id === f.id && p.attr === "Position");
|
||||
if (posFact === undefined) continue;
|
||||
const rank = Math.floor((posFact.value as number) / 8);
|
||||
const colorFact = facts.find((c) => c.id === f.id && c.attr === "Color");
|
||||
if (colorFact === undefined) continue;
|
||||
const color = colorFact.value as PieceColor;
|
||||
if (
|
||||
(color === "white" && rank === 6) ||
|
||||
(color === "black" && rank === 1)
|
||||
) {
|
||||
candidates.add(f.id as EntityId);
|
||||
}
|
||||
}
|
||||
return candidates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stable id for the pseudo-preset that wires modifier facts into
|
||||
* engine runtime behaviour. Reserved name — userland presets must not
|
||||
|
|
@ -643,6 +820,19 @@ PRESET_REGISTRY.register({
|
|||
} else {
|
||||
PRE_MOVE_CAPTURE_ATTACKERS.delete(ctx.engine);
|
||||
}
|
||||
|
||||
// T4: snapshot pre-move check lines for BOTH colors and the set of
|
||||
// pawns eligible to promote this move. Captured here (not in
|
||||
// onAfterMove) because the post-move board no longer reflects who
|
||||
// was in check / which pawns were on the 7th rank BEFORE the move.
|
||||
PRE_MOVE_CHECK_STATE_SNAPSHOTS.set(ctx.engine, {
|
||||
white: captureCheckStateForColor(ctx.engine, "white"),
|
||||
black: captureCheckStateForColor(ctx.engine, "black"),
|
||||
});
|
||||
PRE_MOVE_PROMOTION_PAWNS.set(
|
||||
ctx.engine,
|
||||
capturePromotionCandidates(ctx.engine.session),
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -663,22 +853,31 @@ PRESET_REGISTRY.register({
|
|||
* turn-start runs last so it sees a fully-resolved board.
|
||||
*/
|
||||
onAfterMove(ctx): void {
|
||||
computeAuraFacts(ctx.engine.session);
|
||||
try {
|
||||
computeAuraFacts(ctx.engine.session);
|
||||
|
||||
const preHp = PRE_MOVE_HP_SNAPSHOTS.get(ctx.engine);
|
||||
if (preHp !== undefined) {
|
||||
fireOnDamagedHooks(ctx.engine, preHp);
|
||||
PRE_MOVE_HP_SNAPSHOTS.delete(ctx.engine);
|
||||
const preHp = PRE_MOVE_HP_SNAPSHOTS.get(ctx.engine);
|
||||
if (preHp !== undefined) {
|
||||
fireOnDamagedHooks(ctx.engine, preHp);
|
||||
PRE_MOVE_HP_SNAPSHOTS.delete(ctx.engine);
|
||||
}
|
||||
|
||||
const attacker = PRE_MOVE_CAPTURE_ATTACKERS.get(ctx.engine) ?? null;
|
||||
fireOnCaptureHooks(ctx.engine, attacker);
|
||||
PRE_MOVE_CAPTURE_ATTACKERS.delete(ctx.engine);
|
||||
|
||||
fireConditionalHooks(ctx.engine);
|
||||
|
||||
const nextTurn: "white" | "black" =
|
||||
ctx.mover === "white" ? "black" : "white";
|
||||
fireOnTurnStartHooks(ctx.engine, nextTurn);
|
||||
} finally {
|
||||
// T4: clear the pre-move snapshots LAST — after every trigger
|
||||
// evaluator has had a chance to read them. `try/finally` so an
|
||||
// exception in any fire*Hooks path doesn't leak stale state
|
||||
// into the next move.
|
||||
PRE_MOVE_CHECK_STATE_SNAPSHOTS.delete(ctx.engine);
|
||||
PRE_MOVE_PROMOTION_PAWNS.delete(ctx.engine);
|
||||
}
|
||||
|
||||
const attacker = PRE_MOVE_CAPTURE_ATTACKERS.get(ctx.engine) ?? null;
|
||||
fireOnCaptureHooks(ctx.engine, attacker);
|
||||
PRE_MOVE_CAPTURE_ATTACKERS.delete(ctx.engine);
|
||||
|
||||
fireConditionalHooks(ctx.engine);
|
||||
|
||||
const nextTurn: "white" | "black" =
|
||||
ctx.mover === "white" ? "black" : "white";
|
||||
fireOnTurnStartHooks(ctx.engine, nextTurn);
|
||||
},
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue