Engine: persistent transposition table and pondering#25
Merged
Conversation
Add PersistentNegamaxEngine, an opt-in class conforming to ChessEngine whose transposition table survives across search calls, so consecutive searches of the same game start warm. NegamaxEngine stays a value type with bit-for-bit reproducible searches; its driver is refactored to accept an injected Search session (the public API always passes a fresh one, so existing exact-node-count guarantees are untouched). - Search sessions can be seeded with a table and read back afterwards; killers/history stay per-session since their ply indexing is only meaningful relative to one root. Entries are only written for fully searched subtrees, so interrupted searches never store invalid data. - SearchStopSignal: a lock-guarded flag polled every 1024 nodes, letting another thread cut a running search short (the interrupted call still returns its best fully searched move). - ponder(_:limit:): predicts the opponent's reply, then searches the expected follow-up position; everything lands in the persistent table. - GameSession now uses the persistent engine and ponders on the player's time, stopping the ponder when the player moves, takes back, asks for a hint, or resigns. Measured on deterministic fixed-depth searches: repeating a depth-4 middlegame search drops 27,953 -> 1,413 nodes; carrying the table across consecutive game positions searches 2,809 vs 4,152 nodes cold; a correct ponder prediction drops the real search 1,250 -> 82 nodes. Closes #16 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This was referenced Jul 10, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #16
What
Two related engine upgrades, built as opt-in additions so the deterministic value-type engine (and its exact-node-count tests) are untouched:
PersistentNegamaxEngine(new,ChessKit/Sources/ChessProtocol/PersistentNegamaxEngine.swift)A
final classconforming toChessEnginewhose transposition table survives acrosssearchcalls, so consecutive searches of the same game start warm.NSLock; the type is@unchecked Sendablewith the lock discipline documented. The table is handed to the search session and reclaimed afterwards with the engine's reference emptied in between, so the session mutates the sole dictionary reference in place (no copy-on-write clone per move).stopSearch()sets a lock-guardedSearchStopSignalpolled every 1024 nodes. The interrupted call still returns its best fully searched move, and since TT entries are only written for fully searched subtrees, an aborted search never leaves invalid entries behind.ponder(_:limit:)predicts the opponent's reply (pass 1: search their position), then searches the expected follow-up position (pass 2). Results are discarded; the warm table is the payoff.clearTable()/tableEntryCountfor lifecycle control and introspection.NegamaxEnginerefactor (behavior-preserving)The search driver now accepts an injected
Searchsession; the publicsearch(_:limit:)always passes a fresh one, so the struct's searches remain bit-for-bit reproducible.Searchgained a seedable/readable table and an optional stop signal (nilfor the struct — no behavior change; the deadline check is unchanged when no signal is attached).App wiring (
GameSession)The vs-engine session now uses
PersistentNegamaxEngine(book: .standard)and ponders on the player's time: after the engine moves (and at game start when the player is White), a background task ponders the current position at the difficulty's limit. Pondering stops when the player moves, takes back, requests a hint (which then rides the just-warmed table), or resigns.Evidence (deterministic fixed-depth searches, printed by the new tests)
Same best move and score in the repeat-search case; mate distances stay correct across moves (ply-relative mate-score store/probe is root-agnostic).
Tests
PersistentEngineTests: cold persistent engine matchesNegamaxEngineexactly (move/score/nodes); TT reuse (same move, >2x fewer nodes); table carry across consecutive game positions;clearTablerestores cold reproducibility; mate scores stay correct across moves; ponder returns a legal prediction and warms the table; ponder on a terminal position;stopSearchinterrupts a pondering engine promptly; a simulated game with concurrent pondering never produces an illegal move or corrupts the game record.swift test): all 9 suites, including the pre-existing exact-node-count determinism and efficiency tests, unchanged.xcodebuild ... buildsucceeds; only pre-existing warnings in unrelated files).🤖 Generated with Claude Code