StockShark

A chess engine written from scratch in Rust, empirically measured at ~2460 Elo against Stockfish - a strength built up through ~+400 Elo of SPRT-validated improvements, where every change was kept or discarded on game results. Bitboard move generation, an iterative-deepening alpha-beta search with null-move pruning, PVS and late-move reductions, a tapered PeSTO evaluation, Polyglot opening books, a native GUI, and full UCI support for tournament play - all with zero runtime dependencies.

Rust Bitboards Alpha-Beta Zobrist Hashing Quiescence Transposition Table PeSTO Eval SPRT Testing Polyglot Books egui UCI

~2460

measured Elo vs Stockfish (UCI_Elo scale)

+400

Elo gained through SPRT-validated changes

12

bitboards representing the whole board

0

runtime dependencies to distribute

Grandmaster strength from first principles

Chess has roughly 10120 possible games - more than there are atoms in the observable universe. A strong engine can't come close to enumerating that, so its strength comes entirely from how cleverly it doesn't look at moves: pruning branches that can't matter, ordering moves so the best ones are examined first, caching results so the same position is never searched twice, and evaluating quiet positions with enough nuance to tell a winning plan from a losing one.

StockShark implements the full modern engine pipeline - bitboards, iterative-deepening alpha-beta with null-move pruning, principal variation search and late-move reductions, quiescence search, a transposition table, killer and history heuristics, and a tapered PeSTO evaluation. Every part was written from scratch in safe Rust, with no chess libraries.

The result is empirically calibrated at ~2460 Elo against Stockfish - grandmaster strength - and every step of the ~+400 Elo climb from its baseline was proven with statistical game testing rather than intuition, while remaining a single self-contained binary you can hand to anyone with no installation required.

Speed is strength

Every doubling of nodes searched per second is worth roughly another half-ply of depth. Bitboards turn "which squares do the white pawns attack?" into two shifts and a mask instead of a loop over 64 squares.

Search without pruning is hopeless

The branching factor of chess is ~35. Alpha-beta with good move ordering cuts the effective factor to near its square root, turning a depth that would take days into one that takes a second.

Evaluation is the art

Material alone plays like a beginner. Rewriting the evaluation to tapered PeSTO tables measured +165 Elo on its own - worth as much as an entire batch of search heuristics.

Rust with zero unsafe chess libs

Move generation, hashing, search, and evaluation are all hand-written. The only crates used are for the GUI window - the engine core is pure, portable Rust.

The engine pipeline

A single search request flows through five subsystems. The board layer answers "what is the position and what is its hash?"; the move layer generates and applies moves with full undo; the search layer explores the tree; the evaluation layer scores the leaves; and the book layer short-circuits the opening entirely with known theory.

The codebase mirrors this exactly - each concern lives in its own module under src/, so the search never touches the bit-twiddling and the evaluation never touches the GUI.

1

Board

board/ - bitboards, FEN, Zobrist hashing

2

Moves

moves/ - generation + make / unmake with undo

3

Search

engine/search.rs - alpha-beta, quiescence, TT

4

Evaluation

engine/eval.rs - material, PSTs, structure, endgame

5

Book

engine/book.rs · polyglot.rs - opening theory

stockshark / src
// module layout
src/
main.rs - GUI vs UCI mode switch
├─ board/
bitboard.rs position.rs zobrist.rs
├─ moves/
generator.rs make_move.rs
├─ engine/
search.rs eval.rs tt.rs
book.rs polyglot.rs
├─ uci/
mod.rs - UCI protocol loop
└─ gui/
app.rs - board, eval bar, engine thread
testing/ - strength-testing harness
sprt.sh gauntlet.sh config.sh books/

The board is twelve 64-bit integers

Rather than an array of squares, the position is stored as bitboards - one 64-bit integer per piece type per colour, so 12 in total. Bit N being set means that piece occupies square N. Two extra occupancy boards track "all white pieces" and "all black pieces".

This turns whole-board questions into single machine instructions. Pawn attacks, sliding-piece rays, and legal-square masks all become bitwise shifts, ANDs and ORs the CPU executes in one cycle - the foundation of the engine's search speed.

Each Position also carries side-to-move, castling rights, the en-passant square, the halfmove clock, a redundant [Option<Piece>; 64] array for O(1) lookup by square, and a Zobrist hash.

The Zobrist hash is a 64-bit fingerprint built by XOR-ing a random key for every piece/square combination. Because XOR is its own inverse, it's updated incrementally on each move - flip the key for the from-square and the to-square rather than rehashing the whole board. That single number is what makes the transposition table and repetition detection possible.

// white_pawns bitboard, starting position
0b00000000
  00000000
  00000000
  00000000
  00000000
  00000000
  11111111 ← rank 2
  00000000

Generate fast, undo faster

generator.rs produces all pseudo-legal moves; the search filters out any that leave the king in check. Splitting it this way is a deliberate speed trade - full legality checks are expensive, and most illegal moves are never reached once a branch is pruned.

  • Pawns - single/double pushes, all four promotions, diagonal captures, en passant.
  • Sliding pieces - ray-casting loops in each direction, stopping at the first blocker.
  • Castling - enforces all five FIDE rules: king & rook unmoved, clear path, and the king never in, through, or into check.

The real trick is the undo path. make_move applies a move and returns an Undo struct capturing everything needed to reverse it - previous castling rights, en-passant square, halfmove clock, the captured piece, and the prior Zobrist hash.

unmake_move restores all of it with no cloning of the position. Since the search visits millions of nodes, mutating one shared board and stepping back out is dramatically faster than copying the board at every node.

moves / make_move.rs
// mutate in place, remember how to undo
let undo = pos.make_move(mv);
↳ Undo {
castling, ep_square,
halfmove_clock,
captured: Some(Pawn),
zobrist: 0x9f3a…,
}
// …search deeper, then step back
pos.unmake_move(mv, undo);
✓ position restored, no alloc

Where the strength comes from

The heart of the engine is an iterative-deepening alpha-beta search with quiescence. On its own, alpha-beta prunes; but its power depends almost entirely on searching the best moves first. StockShark layers move-ordering heuristics, a transposition table, and a battery of modern pruning and reduction techniques on top - each one kept only after game testing proved it made the engine stronger.

Alpha-beta pruning

Minimax with cutoffs: once a move is proven good enough, any opponent reply that does worse for us is never explored. With perfect ordering this halves the effective branching factor at every ply.

Iterative deepening

Search depth 1, then 2, then 3… until time runs out - each shallow pass supplies move ordering that generates cutoffs in the next. The engine allots ~1/25 of its clock per move and reserves a safety margin so it always answers before the deadline. Removing an old fixed depth-6 cap measured +125 Elo.

Aspiration windows

From depth 5 the search opens a narrow window around the previous depth's score instead of (−∞, +∞). Most searches land inside it and finish faster; on a fail the window widens and the search retries.

Move ordering

Tried in order: the TT's best move, then captures by MVV-LVA (most valuable victim / least valuable attacker), then killer moves, then the history heuristic. Good ordering is the whole game.

Principal variation search

The first move gets a full window; every later move is probed with a zero-width window and only re-searched if it unexpectedly beats alpha. Good ordering makes the cheap probes succeed almost every time.

Null-move pruning

If handing the opponent a free move still leaves the score above beta, the subtree is pruned. Disabled in check, near mate, and in pawn-only endings where the zugzwang assumption breaks.

Late move reductions

Quiet moves ordered late are searched at reduced depth first, with a log-scaled reduction eased for PV nodes and killers. Anything that beats alpha gets re-searched at full depth.

Check extensions

When the side to move is in check, the search goes one ply deeper instead of dropping into quiescence, so forcing sequences are fully resolved rather than cut off mid-attack.

Forward pruning

Reverse futility cuts when the static eval already beats beta by a depth-scaled margin; futility and late-move pruning skip quiet moves near the leaves that provably can't reach alpha.

Quiescence search

At leaf nodes the search keeps going on captures only until the position is "quiet", so it never evaluates a position halfway through a queen trade. Delta pruning skips captures that can't raise the score to alpha even in the best case.

Transposition table

A 16 MB hash table keyed by Zobrist hash stores each position's searched depth, best move, and whether the score is exact or a bound. Positions reachable by different move orders are searched once, not thousands of times.

Repetition & contempt

Every hash on the path is tracked; a move into a twice-seen position scores as a draw. A −25 cp contempt factor nudges the engine to keep pressing for a win rather than repeat.

engine / search.rs - iterative deepening
# depth grows until the time budget is spent
info depth 6  score cp +28  nodes 142,880  tt-hits 31%
info depth 8  score cp +34  nodes 1,284,301  tt-hits 44%
info depth 10 score cp +41  nodes 9,772,140  tt-hits 58%
info pv e2e4 e7e5 g1f3 b8c6 f1b5 a7a6 b5a4
bestmove e2e4  time 1.21s

Turning a position into a number

At every leaf the engine scores the position in centipawns (hundredths of a pawn), always from the side-to-move's perspective. Material and placement are fused into tapered PeSTO piece-square tables - the Texel-tuned values from the chess-programming literature.

Every piece has two tables - a middlegame set and an endgame set - and the final score blends them, weighted by a game phase computed from the remaining material (minor piece = 1, rook = 2, queen = 4; full board = 24). Values shift smoothly as pieces come off - a knight on the rim, a centralised king, an advanced pawn are all worth different amounts in the opening than in the endgame - instead of flipping abruptly at a fixed material threshold, as earlier versions did. Switching to this scheme measured +165 Elo on its own.

Mobility, king safety, and structural terms add the positional judgement that separates a club player from a master - and dedicated endgame coordination bonuses let it actually finish won endings like K+R vs K instead of shuffling until the 50-move rule.

Mobility

Knights, bishops, rooks and queens earn a per-square bonus for every square they can reach, centred so a typically-placed piece scores near zero and weighted per piece type.

King safety

A pawn-shield term rewards pawns in front of the king and fades toward the endgame. Deliberately cheap: a fuller version that also scanned enemy attacks around the king measured −16 Elo and was cut, because it ran at every leaf of the search.

Pawn structure

Doubled pawns −15 cp, isolated pawns −20 cp, and a passed-pawn bonus that scales from +15 cp at home up to +115 cp on the 7th rank.

Piece activity

Rook on an open file +20 cp, semi-open file +10 cp, and a +30 cp bishop-pair bonus for keeping both bishops.

Endgame coordination

With a material edge, up to +60 cp for driving the enemy king to the edge and +60 cp for keeping the kings close - the "box" technique for mating with K+Q or K+R.

Insufficient material

Instantly scored a draw for K vs K, K+N vs K, K+B vs K, and K+B vs K+B with both bishops on the same colour.

Measured, not guessed

Every change to the engine was kept or discarded based on game results, never intuition. Matches run under fastchess against Stockfish 18 throttled to fixed strengths via its UCI_Elo option - 8s + 0.08s per side, from a book of 99 balanced openings, 8 games in parallel.

Two scripts drive it. sprt.sh is the A/B gate: each candidate build plays the previous one head-to-head under a sequential probability ratio test (SPRT) that stops as soon as the result is statistically decided. A change ships only when the data rejects "no improvement". gauntlet.sh handles absolute calibration: the engine plays Stockfish across a spread of Elo levels, and where it scores ~50%, its rating ≈ that level - the conversion is only trusted near 50%, where it's least sensitive to noise.

The loop caught things no amount of code reading would. The very first match was a clean sweep of losses - not from bad moves, but from overrunning the clock by milliseconds, so a time-safety margin became the first fix. The engine was also self-throttling at a fixed depth 6 and leaving most of its clock unused; removing that cap was the single cheapest large gain. And a full king-safety term (pawn shield plus scanning enemy attacks around the king) measured −16 Elo - the per-node cost outweighed the knowledge - while cutting it back to the cheap pawn-shield half turned the same batch into a +32 gain.

Ratings are on Stockfish's UCI_Elo scale at a fast time control, which reads higher than public lists like CCRL or FIDE - a consistent relative yardstick for progress, not a guaranteed list rating.

testing / sprt.sh - validated builds
# every build A/B tested against the last
baseline  starting point ─────────────── ~2060 Elo
v2  search to the clock, not depth 6  +125 ±35
v3  null-move, PVS, LMR, check ext    +165 ±40
v4  aspiration windows + delta        +78 ±23
v5  futility / LMP, log-based LMR     +15 ±10
v6  tapered PeSTO evaluation         +165 ±36
v8  mobility + pawn-shield safety     +32 ±15
# rejected: full king safety scan −16 Elo ✗
# gauntlet calibration
57.9% vs SF 2400 · 44.6% vs 2500 · 33.8% vs 2600
crossover  ≈ 2460 Elo  (+400 measured)

Standing on centuries of theory

No search is needed for well-known openings - they've been analysed for hundreds of years. StockShark plays book moves instantly and only switches to its own search once the position leaves theory.

It reads the standard Polyglot .bin format used across the chess world. Drop a book like Titans.bin beside the binary and the engine loads it automatically, computing the Polyglot-compatible Zobrist hash (a separate key table from its internal one, required for cross-compatibility) and binary-searching the sorted file. Moves are chosen weighted by their stored grandmaster frequency, so mainlines appear often and sidelines occasionally.

With no book file present it falls back to ~35 hardcoded opening lines covering every main system - Ruy Lopez, Italian, Sicilian, French, Caro-Kann, Queen's Gambit, King's Indian, Nimzo-Indian and more. A per-game seed from the system clock picks among candidates, so no two games open the same way.

engine / polyglot.rs
📖 Loaded: Titans.bin (1.9 MB)
# position hashed → binary search
book hash 0x463b02… → 4 candidates
e2e4 weight 8420
d2d4 weight 6110
g1f3 weight 2740
c2c4 weight 1980
played e2e4 (Book…)

A GUI to play, UCI to compete

Native GUI · egui / eframe

A self-contained desktop app - the board is drawn as 64 rectangles with Unicode piece glyphs, so there are no image assets to ship. It offers four game modes (Human/Engine in any combination), a live eval bar, an adjustable 1–12 ply depth slider, board flip, full move-by-move game review with the arrow keys, and draw detection.

Search runs on a background thread and is delivered to the UI through an Arc<Mutex<…>> polled each frame, so the interface never freezes while the engine thinks.

UCI protocol · tournament ready

Launched with --uci, StockShark speaks the Universal Chess Interface - the lingua franca of chess engines. That makes it a drop-in engine for Arena, Cutechess, and Lichess bots.

It handles uci, isready, ucinewgame, position, go and stop, tracking position history across commands so repetition detection stays correct in match play.

Built with these tools

Language

Rust

Safe, zero-cost abstractions with release builds at opt-level 3 and full LTO. The whole engine core is hand-written - no chess crates.

Board

Bitboards + Zobrist

12 piece bitboards and an incrementally-updated 64-bit hash powering the transposition table and repetition detection.

Search

Alpha-Beta + TT

Iterative deepening with quiescence, null-move pruning, PVS, late-move reductions, aspiration windows, and a 16 MB transposition table.

Evaluation

Tapered PeSTO

Phase-blended material and piece-square tables with mobility, pawn structure, king safety and endgame king coordination.

Opening book

Polyglot

Reads any standard .bin book with frequency-weighted move selection, plus ~35 built-in fallback lines.

GUI

egui / eframe

Immediate-mode desktop UI with a threaded engine, live eval bar, depth slider and full game review.

Protocol

UCI

Standard interface for Arena, Cutechess and Lichess bots - a single portable binary with zero runtime dependencies.

Testing

fastchess + SPRT

Every change A/B tested with a sequential probability ratio test and calibrated against Stockfish gauntlets at fixed Elo levels.

View source on GitHub →

hey, I built this AI too!