Skip to content

Engine: pawn structure, king safety, and tapered king PST - #202

Draft
testtest126 wants to merge 1 commit into
mainfrom
engine/max-strength
Draft

Engine: pawn structure, king safety, and tapered king PST#202
testtest126 wants to merge 1 commit into
mainfrom
engine/max-strength

Conversation

@testtest126

Copy link
Copy Markdown
Owner

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. NegamaxEngine already 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):

  • Pawn structure (Eval enum, 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.
  • 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 (×(1 − endgameFactor)) — shield safety stops being a meaningful concept on a near-empty board.
  • Tapered king PST: the existing king table (correctly rewards tucking into a corner) is now blended with a new kingEndgame table (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) via pseudoLegalMoves().count; wiring that into every evaluateFast() 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-tests Eval.pawnStructureScore and PST.kingBonus directly as pure functions with controlled inputs (exact-value assertions, hand-verified term-by-term to avoid cross-term confounds), plus Board.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.signatureAtCIBudget updated 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 in testBenchIsDeterministicAndMatchesSnapshot passed before I touched the constant; only the comparison against the old pinned snapshot failed, exactly as designed).
  • Existing mate-in-N (testFindsMateInOne/Two), hanging-piece (testCapturesFreeQueen), quiescence-recapture (testQuiescenceAvoidsHorizonBlunder), and always-legal-move coverage were already present and needed no changes — all still pass.
  • Perft, move generation, TT, opening book, self-play, and pondering are untouched by this diff.
  • 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.

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>
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.

1 participant