feat(engine): apply profile at game start

This commit is contained in:
Joey Yakimowich-Payne 2026-04-18 22:42:27 -06:00
commit 0e9809007d
No known key found for this signature in database
3 changed files with 704 additions and 0 deletions

View file

@ -69,6 +69,15 @@ import { PIECE_TYPE_REGISTRY } from "./presets/piece-type-registry.js";
// Importing from the barrel guarantees every preset module's
// side-effect registration has run before the first engine is created.
import "./presets/index.js";
// Side-effect import: registers every modifier descriptor AND the
// `__modifier-profile-integration__` preset with PRESET_REGISTRY, so
// the engine can activate it when a profile is supplied.
import "./modifiers/index.js";
import {
applyProfileToSession,
MODIFIER_INTEGRATION_PRESET_ID,
} from "./modifiers/apply.js";
import type { ModifierProfile } from "./modifiers/types.js";
type MoveGetter = (session: Session, pieceId: EntityId) => LegalMove[];
@ -299,6 +308,21 @@ class PresetStateImpl<T extends Record<string, unknown>>
export interface EngineOptions {
readonly activePresets?: ActivePresetSet;
readonly layout?: StartingLayout;
/**
* Optional ModifierProfile to apply at game start. When supplied,
* the engine:
* 1. Applies the layout (as normal).
* 2. Calls `applyProfileToSession(session, profile, layout)` to
* seed all modifier facts on piece entities.
* 3. Auto-activates the `__modifier-profile-integration__` preset
* (scope=both, permanent) so CaptureFlags, DirectionAdditions,
* and DamageResistance facts drive actual gameplay.
*
* Order: profile is applied BEFORE `activePresets` activation, so
* preset `onActivate` hooks (e.g. piece-hp seeding Hp) observe any
* HpBonus the profile contributed.
*/
readonly profile?: ModifierProfile;
}
export class ChessEngine {
@ -421,8 +445,39 @@ export class ChessEngine {
const layout = opts.layout ?? CLASSIC_LAYOUT;
applyLayout(this.session, layout);
// Profile seeding runs BEFORE the position is recorded for
// threefold repetition — the modifier facts are part of the
// "initial position" from a repetition-tracking perspective, and
// are not expected to change mid-game (profiles hot-swap via
// turn-boundary replacement, which re-seeds).
if (opts.profile) {
applyProfileToSession(this.session, opts.profile, layout);
}
recordPosition(this.session);
this.activePresets = opts.activePresets ?? new ActivePresetSet();
// When a profile is present, auto-activate the integration preset
// so its filterMoves / getExtraMoves / onDamage hooks fire. We
// prepend it to whatever the caller provided so it always runs
// FIRST in the preset-iteration order (cheap predicate — cheap to
// short-circuit).
if (opts.profile) {
const existing = this.activePresets.list();
this.activePresets.replaceAll([
{
id: MODIFIER_INTEGRATION_PRESET_ID,
scope: "both",
turnsRemaining: null,
},
...existing.map((e) => ({
id: e.id,
scope: e.scope,
turnsRemaining: e.turnsRemaining,
})),
]);
}
}
/**