Board-agnostic kernels¶
The kernel layer is a set of pure-logic, actor-free types that sit between the
transport's BoardEvent stream and the session's game state. All kernels
import only ChessCore and Foundation; none import UI, Bluetooth, or
networking. They are testable without live hardware.
OccupancyMoveInference¶
Reconstructs chess moves from the physical lift-and-place sequence reported by an occupancy-sensing board.
The board reports per-square presence transitions but no piece identity. Up to
four slots are tracked so simple moves, captures, castling, and en-passant can
be reconstructed. After every event, handle(square:isLift:) returns
candidates for the session to validate against the current legal-move list.
public final class OccupancyMoveInference {
public var isCastlingStillLegal: ((_ rookHomeSquare: String) -> Bool)?
public func reset()
public func handle(square: String, isLift: Bool) -> OccupancyInferenceFeedback
public func commit(_ uci: String)
}
public enum OccupancyInferenceFeedback: Equatable, Sendable {
case pieceLifted(square: String)
case moveCandidates(_ uciMoves: [String])
case noChange
}
Feedback cases¶
| Case | When emitted |
|---|---|
.pieceLifted(square:) |
Only the first lift observed — highlight legal destinations |
.moveCandidates([String]) |
One or more UCI candidates; session validates against legal moves |
.noChange |
No complete shape yet (castling deferral or mid-sequence) |
Castling oracle¶
Wire isCastlingStillLegal before using the inference machine. Without it, a
physical rook move from a corner square after castling rights are gone may be
incorrectly deferred. Set it to a closure that queries the game's current
castling rights for the given rook-home square.
Commit¶
Call commit(_:) after the session accepts a move. This resets all four slots
so subsequent events build a fresh move. Leftover slots from a castling
king+rook sequence are also cleared.
BoardExecutionGate¶
Tracks whether the human has physically executed an app-dictated move on the
board. The session creates a gate for an engine reply or analysis navigation
move, then routes field events through it instead of OccupancyMoveInference
until the gate reaches .executed or .deviated.
public final class BoardExecutionGate {
public let expectedUCI: String
public let san: String // e.g. "Nf3" — you write the sentence
public init(move: Move, positionBefore: Position)
public func feed(square: String, isLift: Bool) -> State
}
public enum BoardExecutionGate.State: Equatable {
case inProgress
case executed
case deviated([String]) // payload = offending squares
}
The gate computes the required physical effect set from the move geometry:
| Move type | Required lifts | Required places |
|---|---|---|
| Simple move | {from} |
{to} |
| Capture | {from, to} |
{to} |
| En passant | {from, capturedPawn} |
{to} |
| Castling | {kingFrom, rookFrom} |
{kingTo, rookTo} |
| Promotion | {from} |
{to} |
Terminal states (.executed, .deviated) are sticky — further calls return
the same value without mutating tracking state.
BoardDiffResolver¶
Reconciles the app's Position against a 64-bit occupancy array reported by
the board, by searching the legal-move tree for sequences whose resulting
occupancy matches the snapshot.
public enum BoardDiffResolver {
public struct Resolution: Equatable, Sendable {
public let moves: [String] // UCI strings
public var depth: Int // moves.count
}
public static func resolve(
from position: Position,
targetOccupancy: [Bool],
maxDepth: Int = 3,
limit: Int = 8
) -> [Resolution]
public static func occupancyArray(for position: Position) -> [Bool]
}
resolve performs a BFS over the legal-move tree up to maxDepth plies and
returns the shallowest set of explanations, sorted by a wander score. A
resolution "wanders" when it touches squares that are neither part of the
occupancy diff nor a capture target — a signature of a coincidental sequence.
Zero-wander resolutions (the actual move) are surfaced first; the rest are
truncated to the three least-wandering alternatives.
occupancyArray(for:) converts a Position into the same file-major [Bool]
the board reports (a1=0, a2=1, …, h8=63). Useful for building the diff before
calling resolve.
BoardCorrectionPlanner¶
Computes the minimal, identity-aware set of physical corrections needed to
bring the board back into agreement with the app's expected Position.
public enum BoardCorrectionPlanner {
public struct Correction: Equatable, Sendable, Hashable {
public enum Kind: Equatable, Sendable, Hashable {
case place(piece: Piece, from: String?) // `from` = relocate source
case remove
}
public let square: String
public let kind: Kind
public var expectedPiece: Piece? // non-nil for .place
public var fromSquare: String? // non-nil for .place(..., from: nonNil)
}
public static func corrections(
for expected: Position,
boardOccupancy: [Bool]
) -> [Correction]
}
The planner is identity-aware: a missing piece on one square and a stray
piece on another are paired as a single relocate action (.place(piece:,
from: straySquare)) when possible, rather than a bare remove + place. Pairing
uses Chebyshev distance so the most nearby stray is preferred.
Returns an empty array when occupancy already matches. Corrections are emitted only for disagreeing squares; correct squares are silently skipped.
BoardTakebackDetector¶
Detects a physical take-back: the player has picked pieces up and restored an earlier position on the current game line.
BoardDiffResolver only searches forward from the app's position. A take-back
is never forward-reachable, so it reaches this detector only after the forward
resolver has failed.
public enum BoardTakebackDetector {
public static func pliesToUndo(
boardOccupancy: [Bool],
ancestorPositions: [Position]
) -> Int?
}
ancestorPositions is [Position] indexed as 0 = one ply back, 1 = two plies
back, etc. The caller bounds the list (e.g. the last 5–10 plies) to prevent a
distant coincidental occupancy from triggering a huge rollback.
Returns the shallowest matching depth (1-based), or nil if no ancestor
matches.
ChessBoardGeometry¶
Pure square-geometry helpers shared by all board integrations.
public enum ChessBoardGeometry {
// 180° board rotation: a1 ↔ h8, e4 ↔ d5, …
// Returns nil for malformed input.
public static func flippedSquare(_ square: String) -> String?
// File-major occupancy array index: a1=0, a2=1, …, h8=63.
// Returns nil for malformed input.
public static func boardOccupancyIndex(for square: String) -> Int?
// Squares where board and position disagree (both file-major comparison).
public static func mismatchedSquares(
position: Position,
boardOccupancy: [Bool]
) -> [String]
}
flippedSquare applies the 180° orientation flip used when a player is seated
on the Black side. The session applies it to incoming squareSensed.square
values when orientationFlipped is active; adapters are always stateless with
respect to UI orientation preferences.
boardOccupancyIndex converts an algebraic square to its file-major index —
the index scheme used by [Bool] occupancy arrays throughout BoardKit.
mismatchedSquares lists every square whose occupancy differs between the
app's Position and the board's reported array, for desync display or
correction planning.