RFC: deepen the piece-placement cluster behind a PieceReader port #117
Labels
No labels
bug
documentation
duplicate
enhancement
good first issue
help wanted
invalid
question
spec
wayfinder:grilling
wayfinder:map
wayfinder:prototype
wayfinder:research
wayfinder:task
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
amaurycoudr/chess-project#117
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
"Where the pieces are" is currently modeled by four overlapping things, all in
packages/engine/src/:Position.board: Board— the placement field of an immutable snapshotBoard— immutable class wrappingsets: Record<PieceSetKey, SquareSet>, a 12-slot record (one bitset per color+type)PieceSets— that raw 12-slot record type, exported, used structurally byattacks.tsandsearch-board.tsSearchBoard— mutable; holds both#sets: PieceSets(raw record it mutates in place) and#board: Board(aBoard.wrapSets(#sets)aliasing the same record)SquareSetitself is a deep module. The shallowness is the cluster around it — four near-synonymous types co-owning one concept, with leaky seams between them:Board.wrapSetsaliases the ref,Board.fromSetsshallow-copies,Board.toSetscopies out. The choice is invisible at the call site — a copy-vs-alias slip silently corrupts aGame's history.isAttackedInbypassesBoard. It takes raw{ sets: PieceSets; occupancy: SquareSet }. Its two callers feed it differently:rules.isCheckpasses a freshposition.board.toSets()copy,SearchBoard.legalMovespasses the live#setsref.PieceSetsandpieceSetKeyOfare exported; the internal shape leaks acrossboard.ts → attacks.ts → search-board.ts.occupancy()is an O(12) reduce recomputed on every call, including inside the make/unmake hot loop.Understanding placement means bouncing between four files; the integration risk lives entirely in the aliasing seams between them.
Proposed Interface
A single read port every rule module depends on, with two implementations behind it — an immutable snapshot and a mutable search representation. (Ports & adapters pattern; the "port" here is internal — it decouples rule logic from placement representation.)
Consumer signatures lose the raw record entirely:
Usage. Immutable callers receive a
PieceReaderand never know the implementation:SearchBoardholds oneMutablePlacement(replacing both#setsand#board) and passes it as the port to every rule module:What it hides: the 12-slot
Record<PieceSetKey, SquareSet>layout (PieceSetKey,PIECE_SET_KEYS,PIECE_FOR_KEY,pieceSetKeyOfall become module-private); occupancy derivation and its cost; the aliasing/copying distinction (one snapshot builder, always copies — silent aliasing becomes unrepresentable). It also folds the three identical edit shapes (ApplyEditinsearch-board.ts,ZobristEditinzobrist.ts,SquareEditinboard.ts) into onePieceEdit.This is a hybrid of four explored designs: a ports-&-adapters skeleton (keeps
Board's name and identity → minimal churn, ADR-faithful), a deliberately minimal 4-method read surface (no speculativesquares()/pieces()iteration — add only when a caller needs it), and a correctness-onlyoccupancy()contract.Dependency Strategy
In-process — pure computation, no I/O. The module is merged directly; no fakes, no adapters for external systems. The "port" is internal: one contract (
PieceReader), two adapters (Boardimmutable,MutablePlacementmutable).isAttackedIn,movegen, andzobristare rewritten to depend onPieceReaderalone, so the same move-generation code runs over a frozen snapshot (cold path) and a live mid-mutation board (hot path) with byte-identical behavior.The immutable/mutable duality is handled by interface segregation, not a mode flag:
applyEditslives only onMutablePlacement. The immutableBoardcannot be mutated because the method is not on its type — the type-safe replacement for ADR 0007's rejected "@internalmutation method onBoard". Crossing the boundary always copies:MutablePlacement.fromReader(cold, once perSearchBoardconstruction) andBoard.fromReader(cold, once per played ply). The hot make/unmake loop never crosses.Testing Strategy
PieceReaderconformance suite run against bothBoardandMutablePlacement—pieceAt,squaresOf,occupancy,occupancyOfover a range of positions.MutablePlacementoccupancy invariant: after any sequence ofapplyEdits(and their inverses),occupancy()equals the recomputed union of all piece sets, and an apply-then-unapply round-trip restores the exact prior state.Board.fromReader/MutablePlacement.fromReaderindependence: mutating the source after a crossing never affects the copy.isAttackedIntested directly against aPieceReader(no{ sets, occupancy }assembly).Board-internal cases inboard.test.tsthat assert onwrapSets/fromSets/toSetsaliasing semantics — those mechanics no longer exist. Placement-shape assertions inattacks.test.tsmigrate to the conformance suite.perft.test.tsmust be byte-identical at every step of the refactor (the refactor is behavior-preserving).Implementation Recommendations
Durable guidance, not coupled to today's file paths:
applyEdits), and the immutable↔mutable crossings (always copying).pieceAt,squaresOf,occupancy,occupancyOf); an immutable builder family (empty,fromPieces,fromReader); a mutable entry (fromReader+applyEdits). The port promises correctness, never a complexity class. Resist adding iteration helpers until a concrete caller needs one.Boardor{ sets, occupancy }is widened toPieceReader; bodies are unchanged.SearchBoarddrops its#sets/#boardpair for oneMutablePlacement.pieceSetKeyOfand thePieceSets/PieceSetKeytypes stop being exported. It is one coordinated, mechanical change — leavingBoardalongside a new type would preserve the four-overlapping-things friction, so the migration must land atomically.Gameuntouched), ADR 0007 (Boardgains no mutation method;SquareSetinstances stay immutable —MutablePlacementmutates its slot references and occupancy fields, never aSquareSet;snapshotBoardstill pays one copy crossing back). Worth a short note in ADR 0007's lineage, or a new ADR, recording thatPieceReaderis now the seam between rule logic and placement representation.Promoted to branch
refactor/introduce-the-piecereader-read-port-118(PR #122), not yet merged to main. A dependent slice shouldgit fetch origin refactor/introduce-the-piecereader-read-port-118and branch off it.mentioned in issue #118
mentioned in issue #119
mentioned in issue #120
mentioned in issue #121
mentioned in commit
5ef5e336a2mentioned in commit
658aeacfa2