Chess engine

zenith

C17

A separate engine in C, small and simple by design, with a network it trains itself. The strongest of the five.

Strength
about +110 to +135 Elo over pawnstar
Licence
GPLv3 or later
Source
GitHub

README.md · on GitHub

A UCI chess engine in C17 with its own independently-trained NNUE evaluation.

The source code was written by Claude Code under the close supervision and direction of Jonny Reckless.

Zenith pairs a modern alpha-beta search — iterative deepening, principal-variation search, a lockless transposition table, the full pruning/reduction stack, and Lazy SMP — with a king-bucketed neural network evaluation trained by the repository's own pipeline.

Goals for the project were:

Build

Requires a C17 compiler (clang 18 recommended; gcc 13+ builds warning-clean and produces the identical bench node signature). Threads use C11 threads.h with a pthread fallback (src/platform.h) for platforms without it (e.g. macOS). Magic bitboards keep the code portable; -march=native is for local dev — a release build would fan out per microarchitecture.

make                 # -> ./build/zenith        (release: clang -std=c17, -O3 -flto -march=native, whole-program LTO)
make ARCH=x86-64-v2  # portable release build: override the default -march=native (see .github/workflows/release.yml)
make debug           # -> ./build/zenith-debug  (AddressSanitizer + UBSan, -O1 -g; use for movegen/make_move work)
make pext            # -> ./build/zenith-pext   (BMI2 PEXT sliding attacks; bit-identical, ~2% faster on Haswell+/Zen3+)
make datagen         # -> ./build/zenith-datagen + ./build/zenith-bullet2text (standalone NNUE training-data tools)
make datagen-debug   # -> ASan+UBSan builds of both datagen tools (they live outside src/, so `make debug` skips them)
make check           # build + run every correctness gate (mirrors CI; see Verify below)
make perft           # build + run the perft movegen gate
make bench           # build + run the fixed-depth node-signature benchmark
make baseline        # snapshot the current ./build/zenith -> ./build/zenith-base (the SPRT reference binary)
make get-book        # download a free Polyglot opening book -> books/ (gitignored); prints the setoption lines
make tables          # regenerate the committed constant tables (src/generated/*.inc: Zobrist, Q28 ln,
                     #   bitboard geometry, magic multipliers) — a
                     #   deliberate step, never a build side effect; the bench signature guards the values
make format          # clang-format all sources in place (src/ and datagen/) — use clang-format 22.1.8
make hooks           # install the clang-format pre-commit hook (once per clone; core.hooksPath -> .githooks)
make doc             # -> doc/html/index.html (Doxygen API reference)
make clean           # remove build outputs (binaries + doc/html)
./build/zenith       # interactive UCI (prints "Zenith <major>.<minor>.<git commit count>")

Verify

Every correctness property has an executable gate. make check runs them all; CI runs the same gates on Linux and Windows on every push (asserting the bench signature on each), plus a threaded UCI smoke and the NNUE gates against a synthetic net; macOS runs the full suite on release candidates. Any failure aborts non-zero.

make check                          # build + run every gate below
./build/zenith perft                # movegen vs known counts: canonical + ep/castling/promotion catchers + Ethereal 128
./build/zenith bench 13             # deterministic fixed-depth node signature (guards search behaviour) + nps
./build/zenith legalcheck           # the copy-free legality/check predicates == copy-make ground truth over a perft walk
./build/zenith seecheck             # static exchange evaluation vs hand-verified capture positions
./build/zenith fuzzcheck            # malformed-FEN/UCI hardening (memory safety; run the `make debug` build under ASan)
./build/zenith nnuecheck <net.nnue> # incremental accumulator == full refresh, bit-identical
./build/zenith bookcheck            # Polyglot key computation vs the 9 official spec test vectors
./build/zenith embookcheck          # the build-time-embedded book: blob, entry invariants, probe path

main is protected: changes land via pull request with all CI checks green (the release workflow's version-bump commit is the one sanctioned exception, via a scoped deploy key).

The bench node signature is the linchpin: a fixed-depth search produces a deterministic node count, so any unintended change to search behaviour shows up immediately, and a pure-speed change is proven safe by leaving it identical. tools/sprt.sh runs a self-play SPRT (via fastchess) to decide whether a change that does alter the signature is actually a strength gain — no strength change lands on intuition.

Architecture

One translation unit per file, flat src/. No globals: all mutable engine state lives in an Engine aggregate (engine.h — the transposition table, the shared search state, the eval cache, and the loaded NNUE net), built by engine_new() and released by engine_delete() in main, passed explicitly everywhere; UCI session state is a UciSession on uci_loop's stack. The Zobrist keys, Q28 ln table, and the leaper/geometry attack tables are generated compile-time constants (tools/generate_tables.py → src/generated/*.inc). The one exception is bitboard.c's ~850KB magic sliding-attack tables, deterministically filled from generated constant multipliers by init_bitboards() — idempotent, called lazily by engine_new(), with no search or PRNG — before any thread exists.

File Contains
src/types.h Color, Piece (NO_PIECE=0..KING=6), Square, packed 16-bit Move + inline accessors, bit helpers
src/platform.h C11 threads.h / pthread thread shim + a monotonic clock
src/bitboard.* precomputed pawn/knight/king attacks, BetweenBB/LineBB, rook/bishop MAGIC bitboards
src/position.* bitboards + mailbox, Zobrist + pawn key, FEN I/O, copy-make, legality/check oracles
src/movegen.* one masked setwise generator: pseudo-legal + single-pass legal instantiations
src/engine.* the Engine aggregate (TT + shared search state + eval cache + NNUE net) + its new/delete
src/eval.* evaluate(): the NNUE forward pass (a net is always loaded); shared eval cache
src/nnue.* king-bucketed quantised NNUE: loader, feature indexing, AVX2 forward, finny refresh cache
src/book.* Polyglot opening book: key computation, probing, weighted move choice
src/tt.* lockless transposition table ({key^data, data} slots, bit-field payload)
src/search.* iterative deepening, PVS, quiescence, the pruning/reduction/extension stack, Lazy SMP
src/uci.* the UCI protocol loop, time manager, and CLI self-test subcommands
src/version.h major.minor + build number (git commit count, stamped by the Makefile)
src/main.c entry + CLI dispatch
datagen/* standalone training-data tools (make datagen): self-play generation + a bulletformat-to-text converter

Board representation

Move generation

One masked setwise generator (generate_moves) is instantiated twice. generate_pseudo uses permissive masks and emits pseudo-legal moves into a caller-provided Move[MAX_MOVES] buffer terminated by a MOVE_NONE sentinel; the search then filters each move with the copy-free legality oracle — position_is_move_legal(move, checkers, pinned), built on precomputed pins and checkers — before it pays for make_move, so illegal and pruned moves never cost a copy. generate_legal instantiates the same skeleton with real check-evasion and pin-ray masks baked into the target sets (per-destination king safety; a full test only for en passant), producing fully legal moves in a single pass with no filter — this backs perft (~550 Mnps), datagen, and UCI move parsing. The oracles (position_is_move_legal, pinned_to_king, gives_check_fast, discovered_check_candidates) are differentially validated against the copy-make ground truth (position_is_move_legal_slow) by the legalcheck gate.

Evaluation

A single seam — evaluate(const Position *, EvalCache *), centipawns from the side-to-move's perspective — is the one call site the network replaces.

Fail-soft principal-variation search inside iterative deepening with aspiration windows:

All the pruning/reduction margins live in SearchParams, exposed as UCI spin options and SPSA-tuned (tools/spsa.py); the tuned values are the shipped defaults.

UCI & time management

The protocol loop (uci.c) runs the search on a coordinator thread that spawns the Lazy-SMP helpers; stop sets a shared atomic. Options: Hash, Clear Hash, Threads, Move Overhead, EvalFile, OwnBook/BookFile, plus the tunable search parameters. The same binary exposes the CLI self-tests (bench, perft, legalcheck, seecheck, fuzzcheck, bookcheck, embookcheck, nnuecheck) and the NNUE verification helper (nnueeval); the training-data tools are separate executables built by make datagen.

NNUE training pipeline

The network is trained by an independent pipeline: datagen (or a public dataset) → trainer/ (PyTorch, CUDA) → a quantised .nnue → the engine loader. The single source of truth for the feature indexing and quantisation contract is trainer/features.py, which src/nnue.c must reproduce byte-for-byte; trainer/verify.py enforces a 0 cp difference between the engine's integer eval and the trainer reference. See NNUE_TRAINING.md for the full contract and workflow.

# convert public bulletformat data to text, featurise to shard caches, then stream-train
make datagen         # -> ./build/zenith-datagen + ./build/zenith-bullet2text
./build/zenith-bullet2text <shard.data> data/plenty/shard.txt 0 4
PYTHONPATH=trainer python trainer/train.py --featurise-shard data/plenty/shard.txt data/shards/s00.npz
PYTHONPATH=trainer python trainer/train.py --shard-dir data/shards --out nets/zenith.nnue \
    --hidden-size 512 --epochs 8 --batch-size 32768 --lr 1.2e-3 --wdl-lambda 0.3
PYTHONPATH=trainer python trainer/verify.py --net nets/zenith.nnue --fens <fens> --engine ./build/zenith

The streaming trainer (--shard-dir, one shard in RAM at a time) is what allows training on datasets far larger than memory. ./build/zenith-datagen can also generate the engine's own self-play data (fen;stm_score;wdl records) across all cores via tools/datagen_parallel.sh.

Opening book

Standard Polyglot .bin books are supported (src/book.c; the Polyglot Zobrist key computation is validated against the official spec vectors by ./build/zenith bookcheck). The repo ships Zenith's own book, books/zenith-book-r4.bin (487KB, ~14,000 positions) — built entirely from Zenith self-play by tools/build_book.py (screen/rescore every move with the engine, self-play playouts for near-ties, negamax backup over the position DAG), worth ~+30 Elo of clock savings and opening variety in self-play gates. Beyond the moves Zenith plays, it carries opponent coverage: prepared replies to plausible moves Zenith would not choose itself (1. d4, 1. c4, and ~14k others), so it stays in book against foreign repertoires. The previous, coverage-free zenith-book-r3.bin is retained alongside. Inspect it with tools/show_book.py (one SAN line of play per row). The book is embedded in every binary (like the net), so setoption name OwnBook value true alone enables it — no BookFile needed; BookFile loads an external book instead. OwnBook stays off by default: testing is bookless. Alternatively supply your own book, or fetch a free one with make get-book (downloads performance.bin from the GPL-3.0 python-chess repo into the gitignored books/):

make get-book                              # -> books/performance.bin (optional convenience)
setoption name BookFile value books/performance.bin
setoption name OwnBook value true          # default false — testing always runs bookless

Provenance & attribution

Zenith is written from scratch and shares no source code with any other engine. It uses the same published, textbook techniques every strong engine does — bitboards, magic bitboards, PVS, transposition tables, null-move/LMR/futility pruning, SEE, NNUE, Lazy SMP (see the Chess Programming Wiki) — implemented independently.

License

Zenith is free software licensed under the GNU General Public License v3.0 or later (see LICENSE). Copyright © 2026 Jonny Reckless. You may use, modify, and redistribute it under the terms of the GPL; derivative works must remain open under the same license.

← All five engines