Skip to content

Engine: persistent transposition table and pondering#25

Merged
testtest126 merged 2 commits into
mainfrom
feature/engine-persistent-tt
Jul 10, 2026
Merged

Engine: persistent transposition table and pondering#25
testtest126 merged 2 commits into
mainfrom
feature/engine-persistent-tt

Conversation

@testtest126

Copy link
Copy Markdown
Owner

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 class conforming to ChessEngine whose transposition table survives across search calls, so consecutive searches of the same game start warm.

  • Sendable contract: searches are serialized behind an NSLock; the type is @unchecked Sendable with 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).
  • Interruptible: stopSearch() sets a lock-guarded SearchStopSignal polled 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.
  • Pondering: 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() / tableEntryCount for lifecycle control and introspection.

NegamaxEngine refactor (behavior-preserving)

The search driver now accepts an injected Search session; the public search(_:limit:) always passes a fresh one, so the struct's searches remain bit-for-bit reproducible. Search gained a seedable/readable table and an optional stop signal (nil for 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)

Scenario Cold Warm
Repeat depth-4 middlegame search 27,953 nodes 1,413 nodes
Next-move search after two engine searches 4,152 nodes 2,809 nodes
Real search after a correct ponder prediction 1,250 nodes 82 nodes

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

  • 9 new tests in PersistentEngineTests: cold persistent engine matches NegamaxEngine exactly (move/score/nodes); TT reuse (same move, >2x fewer nodes); table carry across consecutive game positions; clearTable restores cold reproducibility; mate scores stay correct across moves; ponder returns a legal prediction and warms the table; ponder on a terminal position; stopSearch interrupts a pondering engine promptly; a simulated game with concurrent pondering never produces an illegal move or corrupts the game record.
  • Full ChessKit suite passes (swift test): all 9 suites, including the pre-existing exact-node-count determinism and efficiency tests, unchanged.
  • iOS app builds for the simulator (xcodebuild ... build succeeds; only pre-existing warnings in unrelated files).

🤖 Generated with Claude Code

testtest126 and others added 2 commits July 10, 2026 04:23
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>
@testtest126 testtest126 merged commit dc7d233 into main Jul 10, 2026
3 checks passed
@testtest126 testtest126 deleted the feature/engine-persistent-tt branch July 10, 2026 02:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Engine: pondering and a persistent transposition table across moves

1 participant