feat(chess): starting-layout foundation (Phase A)
Introduces a pluggable StartingLayout abstraction so the engine can
open from positions other than FIDE without per-caller special casing.
- layouts/{types,registry,index}.ts: StartingLayout + LAYOUT_REGISTRY,
mirroring the PRESET_REGISTRY / PIECE_TYPE_REGISTRY pattern.
- starting-position.ts: CLASSIC_LAYOUT + applyLayout(session, layout)
as the parametrized spawn path. generateStartingPosition stays as
a thin back-compat wrapper so no existing call site changes.
- layouts/{classic,empty}.ts: first two premades registered via
side-effect imports from the barrel.
- engine.ts: ChessEngine constructor now accepts either legacy
(activePresets) positional or new options-bag form
({ activePresets?, layout? }). Detection uses a method-shape
probe rather than instanceof so both overload forms compose
cleanly under strict TS.
- Tests: 11 new tests in starting-position.test.ts + engine-presets
cover applyLayout ordering, hasMoved pre-revocation, empty
layout, classic equivalence, options-bag equivalence with legacy.
Also lands the full execution plans for starting-layouts and
rule-variants under .sisyphus/plans/ (both Momus-reviewed OKAY).
941 tests passing; bun run check clean.
This commit is contained in:
parent
653267daf0
commit
d93bcf6c81
12 changed files with 1319 additions and 26 deletions
|
|
@ -12,7 +12,8 @@ import {
|
|||
type PieceType,
|
||||
type PieceColor,
|
||||
} from "./schema.js";
|
||||
import { generateStartingPosition } from "./starting-position.js";
|
||||
import { applyLayout, CLASSIC_LAYOUT } from "./starting-position.js";
|
||||
import type { StartingLayout } from "./layouts/types.js";
|
||||
import {
|
||||
getCastlingMoves,
|
||||
applyCastlingMove,
|
||||
|
|
@ -282,6 +283,24 @@ class PresetStateImpl<T extends Record<string, unknown>>
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Options bag for the `ChessEngine` constructor.
|
||||
*
|
||||
* All fields optional. Omitted fields use a sensible default:
|
||||
* - `activePresets` → a fresh empty ActivePresetSet (no presets).
|
||||
* - `layout` → `CLASSIC_LAYOUT` (standard FIDE starting position).
|
||||
*
|
||||
* Passing a `layout` is how callers start from a non-FIDE position
|
||||
* (Dunsany, Monster, Horde, custom editor output, etc.). The engine
|
||||
* applies the layout FIRST, then activates presets, so every
|
||||
* preset's `onActivate` scans the layout's pieces — no per-layout
|
||||
* special-casing inside presets.
|
||||
*/
|
||||
export interface EngineOptions {
|
||||
readonly activePresets?: ActivePresetSet;
|
||||
readonly layout?: StartingLayout;
|
||||
}
|
||||
|
||||
export class ChessEngine {
|
||||
public readonly session: Session;
|
||||
/**
|
||||
|
|
@ -360,11 +379,50 @@ export class ChessEngine {
|
|||
};
|
||||
}
|
||||
|
||||
constructor(activePresets?: ActivePresetSet) {
|
||||
/**
|
||||
* Construct a new engine.
|
||||
*
|
||||
* Two calling conventions are supported:
|
||||
*
|
||||
* 1. Legacy (no arg, or `activePresets` only):
|
||||
* `new ChessEngine()`
|
||||
* `new ChessEngine(activePresets)`
|
||||
*
|
||||
* 2. Options bag (preferred for new code):
|
||||
* `new ChessEngine({ activePresets, layout })`
|
||||
* `new ChessEngine({ layout: EMPTY_LAYOUT })`
|
||||
*
|
||||
* The options bag is how new callers specify a starting layout
|
||||
* other than FIDE. When `layout` is omitted, defaults to
|
||||
* `CLASSIC_LAYOUT` (the FIDE starting position), which keeps every
|
||||
* pre-layouts call site working unchanged.
|
||||
*
|
||||
* `activePresets` is applied AFTER the layout, so every preset's
|
||||
* `onActivate` hook sees the initial board state — piece-hp seeds
|
||||
* `Hp` on every piece in the layout (FIDE, Dunsany, Horde, …) with
|
||||
* no per-layout plumbing.
|
||||
*/
|
||||
constructor(activePresets?: ActivePresetSet);
|
||||
constructor(opts: EngineOptions);
|
||||
constructor(arg?: ActivePresetSet | EngineOptions) {
|
||||
this.session = new Session({ autoFire: false });
|
||||
generateStartingPosition(this.session);
|
||||
|
||||
// Normalize the argument: ActivePresetSet stays as-is (legacy
|
||||
// form); an options bag destructures. We detect by checking for
|
||||
// `replaceAll` — an internal ActivePresetSet method that isn't
|
||||
// on plain option bags.
|
||||
const isLegacyPresetArg =
|
||||
arg !== undefined &&
|
||||
arg !== null &&
|
||||
typeof (arg as ActivePresetSet).replaceAll === "function";
|
||||
const opts: EngineOptions = isLegacyPresetArg
|
||||
? { activePresets: arg as ActivePresetSet }
|
||||
: ((arg as EngineOptions | undefined) ?? {});
|
||||
|
||||
const layout = opts.layout ?? CLASSIC_LAYOUT;
|
||||
applyLayout(this.session, layout);
|
||||
recordPosition(this.session);
|
||||
this.activePresets = activePresets ?? new ActivePresetSet();
|
||||
this.activePresets = opts.activePresets ?? new ActivePresetSet();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue