How I built it.

Zugwunder is written from scratch in Swift: no engine library underneath, no framework beyond what Apple ships. This page is the tour of what is inside, for players who are curious and for programmers who want to know what the numbers rest on.

Why from scratch

Because a chess engine is a program you can measure.

I build software that runs on my own machines rather than in somebody else’s data centre, and a chess engine is the ideal test case: every idea can be put to a few thousand games and either gains Elo or it does not. There is no arguing with the score sheet. Zugwunder started as Mattlotse, my older solver for mate-in-n problems. On 25 September 2026 I renamed it Zugwunder, and the search, the evaluation, the trainer, the games, the neural network and the two other programs grew out of it in the weeks that followed, release by release, with a match against the previous version deciding whether a change stayed.

Everything is Swift 6 with strict concurrency, built with the Swift Package Manager, and it depends on nothing outside Apple’s frameworks and the Swift standard library. The one exception is Fathom, a small C library under the MIT licence that reads Syzygy endgame tables; writing my own reader for that format would have added nothing but bugs.

The board

Bitboards, Zobrist keys, a lock-free hash table.

The position is a set of 64-bit bitboards, one per piece type and colour, so that “all squares a knight attacks from here” is a table lookup and “all pieces attacked by this rook” a few bit operations. Move generation produces strictly legal moves, including castling, en passant and promotions, with a separate fast path for captures that the quiescence search uses.

Every position has a 64-bit Zobrist key, which drives the transposition table, repetition detection and the pawn-structure cache. The transposition table is shared by all search threads without locks, and mate scores are normalised to the distance from the root so that an entry found at one depth stays correct at another.

FEN and PGN have their own parsers and writers. PGN keeps tags, comments, evaluations and nested variations through a round trip, and the validator explains why a position is illegal (two kings in contact, a pawn on the first rank, a castling right without the rook) rather than letting the engine search it.

The evaluation

A small neural network, trained on the engine’s own games.

The first evaluation was hand-written: piece-square tables, king safety and pawn structure, tuned with the Texel method on the engine’s own games. It is still in the binary (UseNNUE false) as a baseline. The evaluation that plays today is an NNUE, the kind of network that Stockfish made standard, but trained here from the engine’s own data:

  • 768 inputs, one per piece, colour and square, seen from each side’s perspective; two hidden accumulators of 256 neurons with a clipped ReLU; one output.
  • Weights quantised to 16-bit integers. The accumulators are updated incrementally as pieces move, 16 neurons per SIMD vector, so that evaluating a position costs a few hundred integer operations rather than a full forward pass.
  • The network lives in the binary as a Swift source file generated from the trained weights; there is no separate file to lose. A different network can be loaded with the NNUEFile option for experiments.

The training loop is as plain as it sounds. zugwunder-datagen plays the engine against itself and against Stockfish and writes one 68-byte record per position: the features, Zugwunder’s own search score and the game result. About four million positions an hour on eight cores. A 150-line PyTorch script trains the network on an Apple GPU, embed.py turns the weights into Swift, and the next release carries the new network. The sixth generation was trained on 236.6 million positions; each generation was accepted only after matches against its predecessor and against outside engines.

What did not go in: no Stockfish code, no Stockfish weights, no Stockfish evaluations. Stockfish was an opponent and a measuring stick, nothing else.

Endgame knowledge

Computed, not downloaded.

For king and queen, rook or pawn against a bare king, and for the two four-piece endgames bishop and knight against king and rook against bishop, Zugwunder does not estimate: it computes the exact tables itself by retrograde analysis when it starts, using the board’s symmetries to keep the work small. The four-piece tables take about a second and a half in the background. The search, the solver and the puzzle generator all read them.

For everything else with up to five pieces there are the Syzygy tables: point the engine at a folder and it plays those endgames perfectly and uses the verdicts inside the search after every capture or pawn move.

The puzzle generator is the smallest program on this page and my favourite. It walks the 64 × 64 × 64 placements of three pieces in a random order using an odd stride, so that every placement is visited exactly once without a list; keeps only legal positions whose table entry says “mate in exactly n”; and then checks that precisely one first move achieves it. The position on the front page came out of it.

Exact solvers

Proof trees for mate, helpmate, selfmate and stalemate.

A playing search answers “what is good here”. A problem solver has to answer “is there a forced mate in three, and is it unique”, which is a different program. Zugwunder’s solvers build AND/OR proof trees: at the attacker’s moves one branch has to work, at the defender’s moves every reply has to be answered. The result is the full solution tree with all defences, a verdict of proven, refuted or undecided within the limit, and a warning when a second first move also solves the problem, because to a problemist a cooked problem is a broken one.

Helpmates (both sides cooperate, all solutions are listed), selfmates (the side to move forces the opponent to mate it) and stalemate problems use the same machinery with a different goal test. These solvers talk to the engine library directly rather than over UCI, because they need the proof tree, not a best move.

Measuring

Thousands of games, and the discipline to believe them.

The project ships its own measuring tools. zugwunder-match plays two engines against each other from a book of openings, both colours each, with clocks, adjudication and a sequential probability ratio test (SPRT) that stops as soon as the result is statistically clear. A search change has to pass that test against the previous build before it is merged. zugwunder-rating turns match results against Stockfish at several UCI_Elo levels into a maximum-likelihood estimate on Stockfish’s scale.

The headline figure, 3065 ± 20 Elo for the current network, comes from 500 games against Stockfish at UCI_Elo 3100 under tournament-like conditions: 60 seconds plus 0.6 per move, one core, 256 MB hash, Syzygy tables, with the engine binary, the networks and the opponent frozen in a reference folder and their checksums recorded. A second series against Arasan gave a consistent picture. The same frozen setup had put the previous network at 3045 ± 20. Those 500 games are the frozen reference measurement; over the weeks, the cluster of machines behind the project played thousands more against Stockfish and other engines, for SPRT tests, calibration and data collection. All of these are estimates on another engine’s calibrated scale, not an official rating list, and I say so wherever the number appears.

The adjustable strength of the UCI engine was calibrated the same way: more than 5,000 games against Stockfish 17.1 on a Linux machine at seven skill levels, with confidence intervals for each, interpolated into an UCI_Elo scale from 1100 to 3000. It is labelled provisional until it has been checked against a second opponent and on macOS, because a calibration against one opponent is a calibration against one opponent.

Below the matches there are 364 Swift tests across the engine, the app and Wolfgang, and 32 protocol tests that drive the real zugwunder-uci binary through the UCI specification of 2006: pondering, searchmoves, node limits, a stop that arrives before the search has started, input split across pipe writes. The protocol test is included in every package so that you can run it on your own machine.

The apps

SwiftUI and AppKit, four languages, VoiceOver, a careful updater.

Zugwunder and Wolfgang are native macOS apps in SwiftUI with AppKit where SwiftUI falls short (windows, the keyboard, screenshots, the Keychain). The board is drawn by the app, supports drag and drop and full keyboard control, and reads moves aloud under VoiceOver. All 900-odd interface strings exist in German, English, French and Dutch, and the language is switched from the window header.

Both apps bundle their own copy of zugwunder-uci and talk to it over UCI, exactly as they would to Stockfish: the app sends the starting position and the full move list, and the engine process with its hash table survives for the whole game. Only the exact solvers call the engine library directly. An app release therefore never changes the engine’s version, and the engine can be tested on its own.

The updater is a small separate helper. The app polls a JSON feed over HTTPS, checks that the download is on the same host, verifies its size and SHA-256, mounts the disk image read-only, checks the bundle identifier and the Developer ID team, swaps the bundle atomically with a backup, and relaunches. If any step fails, the old version is put back.

The newest feature, recognising a position from a photo, is the one place where the app talks to a service: it sends the picture to Anthropic’s Claude through the API key you enter, and only when you press the button. The reply is treated as a proposal, validated as a chess position, and shown for correction. An earlier version used a local model through Ollama; the current one uses Claude, and says so next to the button.

Three platforms, one build

macOS, Linux and the browser from the same source.

  • macOS: Apple silicon, signed with a Developer ID, notarised by Apple and stapled. The release script bumps the version, builds, signs, notarises, writes the update feed and a manifest with checksums, and keeps the logs.
  • Linux: cross-compiled on the Mac with the Swift static Linux SDK for x86_64 (with AVX2) and ARM64. The result is one statically linked file that needs no libraries, which is why it runs on a Raspberry Pi as well as on a server.
  • WebAssembly: built with the Swift SDK for WebAssembly against WASI and FoundationEssentials. A Web Worker runs the engine so that the page stays responsive; the compressed module is under three megabytes. The play page uses it, and the same package is in the downloads if you want to host your own.

A complete build runs the Swift tests, the protocol tests and the data-collector tests, builds all packages, signs and notarises the macOS ones, and refuses to publish if anything fails. The folder it produces contains the packages, a manifest, a SHA256SUMS file and forty log files.

In numbers

Counted on 2 November 2026.

Engine10,200 linesSwift, without the 4,400 generated lines that hold the network weights.
Zugwunder app11,900 linesSwiftUI and AppKit, including the solvers’ user interface, the trainer, the updater client and the image import.
Wolfgang8,800 linesThe UCI chess GUI with tournaments and the engine manager.
Tools1,400 linesMatch, tuning, data generation, rating, analysis and relabelling programs, plus the UCI and WebAssembly front ends.
Tests7,500 lines364 Swift tests, 32 protocol tests and the Python checks for the data collector.
Training150 linesThe PyTorch script that produced six generations of networks.
ReleasesMore than 30App versions since 25 September 2026, from Mattlotse 1.0.3 to Zugwunder 1.1.15.

Timeline

From a mate solver to three programs.

  • Mattlotse becomes Zugwunder

    Renamed my old app Mattlotse, a mate-in-n solver with full proof trees and cook detection, to Zugwunder. PVS, null move, LMR and the hand-written evaluation arrive the same day.

  • Games, trainer, files

    Playing strengths, chess clock, puzzle library, PGN with comments and variations, helpmate and selfmate solvers, own endgame tables, the UCI engine and the match tool with SPRT, Lazy SMP.

  • Four languages, network v4

    German, English, French and Dutch. The fourth network (113 million positions) gains 78 Elo over the third.

  • Network v5, first rating estimate

    170 million positions, +25 Elo. A thousand games against Stockfish 19 put the engine at 3006 ± 17 on the CCRL-like scale.

  • In the browser

    The engine builds for WebAssembly and plays on f97.be without installation.

  • Three packages

    App, Wolfgang and engine become separate packages with one release script. Both apps bundle the engine and speak UCI to it.

  • Network v6, UCI complete

    500-game matches against Stockfish and Arasan; 3065 ± 20 Elo. Every command of the UCI specification implemented and tested.

  • Strength calibration

    More than 5,000 games against Stockfish 17.1 give the engine a provisional UCI_Elo scale from 1100 to 3000.

  • Puzzle generator, image import

    New puzzles from the endgame tables with a verified unique key; positions from photos in all four languages; window snapshots.

Check the numbers yourself.

Every package ships with checksums and the protocol test. The match tools are in the tools archive.