Skip to content

feat(search): killer/history ordering, null-move pruning, LMR - #9

Merged
cjunius merged 1 commit into
mainfrom
feat/killers-history-nmp-lmr
Aug 30, 2026
Merged

cjunius merged 1 commit into
mainfrom
feat/killers-history-nmp-lmr

Conversation

@cjunius

@cjunius cjunius commented Aug 30, 2026

Copy link
Copy Markdown
Owner

What

Adds three classic search heuristics on top of the transposition table and Lazy SMP that landed in #8.

Killer moves + history heuristic

  • Two killer slots per ply for quiet moves that caused a beta cut-off.
  • [side][from][to] history table, += depth² per cut-off, clamped at 1<<22.
  • orderMoves now ranks: TT move → promotions → MVV-LVA captures → killers → quiets by history score.
  • Both tables live on the per-worker searcher, so Lazy SMP stays lock-free.

Null-move pruning

  • R = 2, R = 3 from depth 6.
  • Guards: not in check, depth >= 3, non-mate beta, side-to-move has non-pawn material (zugzwang), no consecutive nulls, static eval ≥ beta.
  • The pass position is rebuilt through FEN — dragontoothmg keeps the Zobrist hash and en-passant square in unexported fields, so a plain struct copy would feed a stale hash into the shared TT.

Late move reductions

  • Late quiet moves that neither give nor evade check are searched 1–2 ply shallower.
  • A reduced search that beats alpha is re-searched at full depth, so no score is lost.

negamax is split into outOfTime / terminalScore / tryNullMove / searchMove to stay within the gocyclo budget.

Impact

Single-threaded, Hash 128, from the start position:

depth nodes (TT only, was) nodes (now) time (now)
8 ~5.9M ~167k 0.07s
10 ~548k 0.23s
12 ~2.29M 1.13s

The engine now reaches depth 12 from the opening in roughly the wall-clock depth 8 used to take. Node rate is ~unchanged (~2.5M nps).

Tests

  • New engine internal tests: nullMoveBoard, hasNonPawnMaterial, isQuiet, recordCutoff (killer shift + history cap), orderMoves ranking, tryNullMove guard matrix, board-unmodified invariant.
  • New external search tests: mate-with-pruning-active, deep-depth completion, tactical parity with the pre-pruning result.
  • Broader UCI tests: setoption Hash/Threads (incl. clamping and bad values), ucinewgame reset, clock-budget go, malformed position inputs.
  • New cmd/gochess/main_test.go covering runBench, runPerft, dispatch, and the UCI entry point.

Coverage: engine 90.4%, uci 95.8%, cmd 88.4% — 91.5% overall.

golangci-lint (v2.13.2), gofumpt, go vet, go test -race ./..., and the full perft suite all pass.

Docs

README, docs/architecture.md, docs/design.md, docs/performance.md, docs/engine-strength.md, and CHANGELOG.md updated for the new heuristics and refreshed benchmark tables. The strength estimate is revised upward (~1600 → ~2000 CCRL-blitz, still an estimate pending SPRT).

🤖 Generated with Claude Code

Add three search heuristics on top of the transposition table:

- Killer moves + history heuristic. Quiet moves that cause a beta
  cut-off become killers for their ply (two slots) and accumulate a
  depth^2 bonus in a [side][from][to] history table; orderMoves ranks
  killers just below captures and sorts the remaining quiets by history.
  Both tables live on the per-worker searcher, so Lazy SMP stays
  lock-free.
- Null-move pruning (R=2, R=3 from depth 6) with the usual guards: not
  in check, depth >= 3, non-mate beta, non-pawn material, no consecutive
  nulls. The pass position is built through FEN because dragontoothmg
  keeps the Zobrist hash and en-passant square in unexported fields and a
  stale hash would corrupt the shared TT.
- Late move reductions. Late quiet moves that neither give nor evade
  check are searched 1-2 ply shallower and re-searched at full depth
  only when the reduced search beats alpha.

negamax is split into outOfTime / terminalScore / tryNullMove /
searchMove helpers to keep each within the complexity budget.

Depth-8 from the opening drops from ~5.9M nodes to ~167k; the
single-threaded search now reaches depth 12 in ~1s.

Tests: new engine internal tests for nullMoveBoard, hasNonPawnMaterial,
isQuiet, recordCutoff (killers + history cap), orderMoves ranking, and
tryNullMove guard conditions; external tests for mate-finding and
tactical soundness with pruning active; broader UCI coverage
(setoption, ucinewgame, clock budget, malformed input); a cmd/gochess
main_test. Package coverage: engine 90.4%, uci 95.8%, cmd 88.4%
(overall 91.5%).

Docs: README, architecture, design, performance, engine-strength and
CHANGELOG updated for the new heuristics and refreshed benchmark tables.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 95.69892% with 4 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/engine/search.go 95.69% 4 Missing ⚠️

📢 Thoughts on this report? Let us know!

@cjunius
cjunius merged commit 0f8f7c8 into main Aug 30, 2026
8 checks passed
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.

2 participants