Piece Identity and Physical Setup¶
This chapter covers the seam that the rest of the physical stack is organised around. The engine reasons about piece types; the machine has to move specific physical objects. Where those two views meet is the single most consequential design boundary in the system, and most of the non-obvious behaviour in the board layer follows from it.
Why types are not enough¶
The engine’s world is bitboards. “A white rook is on a1” is the complete truth as far as search and evaluation are concerned; which rook it is has no meaning, because the two are interchangeable in every rule of chess.
The physical board cannot afford that abstraction, for one concrete reason: storage is addressed by identity. Each of the 32 pieces has exactly one designated storage slot — SLOT_R1, SLOT_R2, SLOT_N1, SLOT_P1 through SLOT_P8, and so on per side. When a rook is captured, the gantry must carry it to its own slot. “A rook was captured” does not determine where to put it.
kirin/src/hardware/piece_tracker.h is what closes that gap: it maintains the mapping from individual piece identity to square, updated as moves are made, so that the interpreter can answer “where does this piece belong” at the moment a capture happens.
Promotion: the board and the FEN diverge on purpose¶
There are 32 physical pieces and no spares. A promotion therefore cannot produce a second queen, and the system does not pretend otherwise. From game_controller.cpp:
“For now, simplified approach: just move the pawn to the last rank. The pawn physically stays there but is tracked as a promoted piece by the engine.”
A promoted queen is physically a pawn, carrying its original SLOT_Px identity. The engine’s position and the physical arrangement have knowingly diverged, and every downstream component has to be written with that in mind.
D-26 — Promotion is physically faked — the pawn stays on the square and keeps its SLOT_Px identity
Context Storage holds exactly the 32 real starting pieces. There is no spare queen to fetch, and a promotion demands one.
Decision Move the pawn to the last rank and leave it there. The engine tracks it as a promoted piece; the board tracks it as the pawn it physically is.
Rejected Carrying spare queens — it means more pieces than a chess set has, somewhere to keep them, and a storage model that is no longer a bijection with the 32 starting slots.
Consequences The FEN and the physical board diverge on purpose from the first promotion onward, which is the root of D-25: a FEN stops being a faithful description of the object being manipulated. It also pays an unexpected dividend — because a pawn can already stand in for a queen, a promoted-piece FEN turns out to be physically buildable after all, which is what makes the puzzle stand-in policy of D-27 possible rather than a refusal.
D-25 — The target of a physical setup is a PieceTracker identity snapshot, never a FEN
Context Match review needs the machine to arrange the pieces into a chosen position from a past game. The obvious interface is “drive the board to this FEN.”
Decision The target type is a BoardIdentity — for each of the 32 slot-identities, the square it occupies or the storage slot it sits in. Replaying a game’s UCI move list through a PieceTracker yields that snapshot at every ply, so it costs nothing to produce.
Rejected Planning over a FEN fails three ways at once, and they all point the same direction. A FEN with two white queens describes a position the board cannot physically hold, so a FEN-based planner can be handed targets that do not exist. A FEN cannot say which rook is which, but the planner must know, because a captured piece has exactly one correct storage slot. And after any promotion the FEN and the physical board have already diverged, so the FEN is not even a faithful description of the thing being manipulated.
Consequences Every target the planner is asked for is physically realisable by construction, because it is a state the board actually held. The corollary is a real limitation stated plainly: setting up from an arbitrary FEN — a pasted position, a puzzle — is a strictly harder and partly unsolvable problem, and is out of scope for review. Puzzles need it anyway, and solve it separately via a matching algorithm (§Arbitrary FENs: the puzzle case). piece_tracker.h already carried a SLOT_UNKNOWN value commented “e.g. after FEN setup mid-game” — the codebase had anticipated this problem before it was named.
The setup planner¶
kirin/src/hardware/board_setup_planner.h answers one question: given where the 32 pieces are now and where they must end up, in what order should the physical operations happen so that a piece is never placed onto a still-occupied square?
It solves ordering only. Path collisions are not its problem — those go to the existing planMove()/executeMove() pipeline, which already handles blocker parking and A^*^ routing (Ch. Blocker Detection and Relocation, Ch. A* Pathfinding). Keeping the two concerns apart is what lets the planner stay pure, hardware-free and fully unit-testable.
constexpr int OFF_BOARD = -1;
struct BoardIdentity {
int square[2][16]; // [side][StartingSlot] -> square, or OFF_BOARD
static BoardIdentity fromTracker(const PieceTracker& t);
};
Operations come in three kinds: Move (square to square), Store (square to the piece’s own storage slot), and Retrieve (storage back onto the board). The ordering problem is real rather than incidental — a naive sequence deadlocks the moment two pieces need to swap squares, and the planner must break such cycles by routing one piece through storage.
Arbitrary FENs: the puzzle case¶
Puzzles break the guarantee D-25 relies on. They are drawn from real games, arrive as bare FENs, and a meaningful minority contain promoted pieces — two queens, three knights — that 32 physical pieces cannot represent. kirin/src/app/fen_identity.h bridges this:
D-27 — An arbitrary FEN is assigned to physical pieces by optimal min-cost matching, and promoted pieces are represented by stand-ins
Context Lichess puzzles are the highest-value content the board can offer, and every one of them is a FEN — exactly the input D-25 declared out of scope.
Decision Assign each occupied square one of the 32 physical pieces by optimal min-cost matching (Hungarian; at most 16 per side, so the cost is irrelevant), where the cost is travel distance from where that piece currently stands. A piece already on its target square costs zero, so the plan moves only the delta between the board’s present state and the puzzle.
Rejected Refusing every FEN containing a promoted piece was rejected once it was noticed that the board already fakes promotion: a pawn keeps its magnet and its SLOT_Px identity while standing in for a queen. A spare pawn can therefore stand in for a promoted piece, and a two-queen FEN is buildable. Always allowing stand-ins was equally rejected: it is honest in a game the player promoted in and baffling in a puzzle handed to them cold. It is a caller policy (FenIdentityOptions::allowPromotedStandIns), off by default for puzzles — batches of up to 50 are fetched and unsuitable ones skipped. A FEN is refused only when the physical set genuinely cannot express it: nine pawns, or no king.
Consequences Zero-cost matching for already-correct pieces is what makes puzzles viable at all, given that each piece placement is a gantry pick-and-place taking seconds. It also enables the companion decision to never tear down between puzzles (D-28) and to order a fetched batch by setup distance, which collapses what would be a full rebuild into a handful of moves.