Engine: pawn structure, king safety, and tapered king PST - #202
Draft
testtest126 wants to merge 1 commit into
Draft
Engine: pawn structure, king safety, and tapered king PST#202testtest126 wants to merge 1 commit into
testtest126 wants to merge 1 commit into
Conversation
Requested strengthening of the built-in engine, staying MIT (no Stockfish/GPL). Turned out the search side of that ask was already largely built: alpha-beta + iterative deepening + aspiration windows, quiescence search, a Zobrist-keyed transposition table, null-move pruning, and MVV-LVA + killer + history move ordering are all already in NegamaxEngine. The real gap was the evaluation function `evaluateFast()` uses (the one every search leaf calls): material + PST only, no pawn structure, no king safety, no notion of game phase. This PR adds those, in one pass over the board (no second scan, no move generation): - Pawn structure: doubled/isolated pawn penalties, passed-pawn bonus by rank that scales up in the endgame (an unstoppable passer is worth far more once there's no piece play left to compensate). - King safety: pawn-shield bonus (friendly pawns on the squares directly ahead of the king) and an open/semi-open king-file penalty. Faded out as material is traded off — shield safety isn't a meaningful concept on a near-empty board. - Tapered king PST: the existing king table (correctly favors tucking into a corner) is now blended with a new endgame table (favors centralizing) by a phase estimate from remaining non-pawn material. The king's own PST bonus is deferred until after the per-square pass, once the phase is known. Deliberately left out of the hot path: full legal-move mobility. It's already computed in the slower `evaluate()` (used for game review, not search) via `pseudoLegalMoves().count`; wiring that into the every-leaf `evaluateFast()` would roughly double search cost for a comparatively small eval signal, on an engine that's already the search's bottleneck (~160k nps release). Cheap, single-pass terms only. Tests: `EvaluationTests.swift` unit-tests `Eval.pawnStructureScore` and `PST.kingBonus` directly (pure functions, exact values, confound-free) plus a few `Board.evaluateFast()` integration checks (sheltered beats exposed king, centralized beats cornered king in a bare endgame, connected beats doubled pawns). `BenchTests.signatureAtCIBudget` updated to the new deterministic value — the search is still bit-for-bit reproducible (verified: both intra-run determinism assertions passed before I updated the pinned constant, only the snapshot moved, exactly as expected for an intentional eval change). Everything else (perft, move generation, TT, quiescence, opening book, self-play, pondering) is untouched. `swift test`: 122/122 pass. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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.
Summary
Requested: strengthen the built-in engine as much as reasonable, staying MIT — no Stockfish, no GPL/external engine (owner's explicit decision).
Honest framing up front: most of what was asked for on the search side already existed before this PR.
NegamaxEnginealready has alpha-beta + iterative deepening + aspiration windows, quiescence search on captures, a Zobrist-keyed transposition table, null-move pruning, and MVV-LVA + killer + history move ordering — all deterministic, all already tested (EngineTests,PersistentEngineTests,BenchTests). The real gap versus the ask was the evaluation function:Board.evaluateFast()(the one every search leaf calls) was material + piece-square tables only — no pawn structure, no king safety, no notion of game phase.This PR adds those.
What changed
All in
Board.evaluateFast()'s existing one-pass-over-the-board loop (no second scan, no move generation added):Evalenum,ChessKit/Sources/ChessKit/Board.swift): doubled-pawn and isolated-pawn penalties; passed-pawn bonus by rank, scaled up (×(1 + endgameFactor)) in the endgame — an unstoppable passer matters far more once there's no piece play left to compensate for losing it.×(1 − endgameFactor)) — shield safety stops being a meaningful concept on a near-empty board.kingEndgametable (rewards centralizing) by a phase estimate (PieceKind.phaseWeight/maxPhaseWeight) from remaining non-pawn material. The king's own PST bonus is deferred until after the per-square pass, once the phase is known — everything else keeps scoring inline.Deliberately left out: full legal-move mobility in the hot path. It's already computed in the slower
Board.evaluate()(used for game review, not search) viapseudoLegalMoves().count; wiring that into everyevaluateFast()leaf call would roughly double search cost on an engine whose bench comment already flags it as slow (~160k nps release). Cheap, single-pass terms only — mobility stays where it already was.Tests
ChessKit/Tests/ChessKitTests/EvaluationTests.swift(new, 10 tests): unit-testsEval.pawnStructureScoreandPST.kingBonusdirectly as pure functions with controlled inputs (exact-value assertions, hand-verified term-by-term to avoid cross-term confounds), plusBoard.evaluateFast()integration checks — sheltered king beats exposed king at equal material, centralized king beats cornered king in a bare king-and-pawn endgame, connected pawns beat doubled pawns.BenchTests.signatureAtCIBudgetupdated to the new deterministic value (0x55b1_10b1_c3c9_c16a). This is expected to change with any eval change (per the bench test's own comment) — confirmed the search is still bit-for-bit deterministic first (both intra-run equality assertions intestBenchIsDeterministicAndMatchesSnapshotpassed before I touched the constant; only the comparison against the old pinned snapshot failed, exactly as designed).testFindsMateInOne/Two), hanging-piece (testCapturesFreeQueen), quiescence-recapture (testQuiescenceAvoidsHorizonBlunder), and always-legal-move coverage were already present and needed no changes — all still pass.swift test --package-path ChessKit: 122/122 pass locally (Xcode-beta toolchain).Honest read on strength
This is a strong classical engine for its size, not remotely Stockfish-class. What it now has: real alpha-beta search with proper pruning (null-move, TT, aspiration windows), quiescence to avoid horizon blunders, and an evaluation that finally accounts for pawn structure and king safety instead of just material+PST. What it still lacks versus any serious modern engine: NNUE/any learned evaluation, bitboards (this is a plain 64-element array board — perft numbers are correct but movegen is not fast), a proper 15-ply+ search depth in real time (bench notes ~160k nps, so mid-single-digit-plies in a UI-reasonable time budget), and endgame tablebases. Realistic ballpark: comfortably beats a casual/club player at low-mid difficulty settings, not competitive with any titled player at full strength. That's consistent with "strong classical, not Stockfish" as scoped.
Gate
Draft only, per explicit instruction — not merging, not marking ready, not pushing to main. Not auth/token/crypto/session code, so CLAUDE.md rule 5's security-review gate doesn't apply, but this stays draft regardless per the task's own instruction. Yakiv reviews and merges (or requests changes) himself.