Hoe ik het gebouwd heb.
Zugwunder is van de grond af in Swift geschreven: geen enginebibliotheek eronder, geen framework buiten wat Apple meelevert. Deze pagina is de rondleiding door het binnenwerk, voor nieuwsgierige spelers en voor programmeurs die willen weten waar de cijfers op rusten.
Waarom van de grond af
Omdat een schaakengine een programma is dat je kunt meten.
Ik bouw software die op mijn eigen machines draait in plaats van in andermans datacenter, en een schaakengine is de ideale proef: elk idee kun je een paar duizend partijen laten spelen, en het wint Elo of niet. Met het notatieformulier valt niet te twisten. Zugwunder begon als Mattlotse, mijn oudere oplosser voor mat-in-n-problemen. Op 25 september 2026 heb ik hem Zugwunder genoemd, en in de weken daarna groeiden daaruit het zoekalgoritme, de evaluatie, de trainer, de partijen, het neurale netwerk en de twee andere programma's, release na release, waarbij een match tegen de vorige versie besliste of een verandering bleef.
Alles is Swift 6 met strikte concurrency, gebouwd met de Swift Package Manager, en het hangt van niets af buiten de frameworks van Apple en de Swift-standaardbibliotheek. De enige uitzondering is Fathom, een kleine C-bibliotheek onder MIT-licentie die Syzygy-eindspeltabellen leest; een eigen lezer voor dat formaat had alleen maar bugs opgeleverd.
Het bord
Bitboards, Zobrist-sleutels, een lock-vrije hashtabel.
De stelling is een reeks 64-bits bitboards, één per stuksoort en kleur, zodat „alle velden die een paard van hieruit aanvalt“ een tabelopzoeking is en „alle stukken die deze toren aanvalt“ een paar bitbewerkingen. De zettengenerator levert uitsluitend reglementaire zetten, inclusief rokade, en passant en promoties, met een apart snel pad voor slagzetten dat het quiescence-zoeken gebruikt.
Elke stelling heeft een 64-bits Zobrist-sleutel, die de transpositietabel, de herkenning van zetherhaling en de pionstructuurcache aandrijft. De transpositietabel wordt door alle zoekthreads zonder locks gedeeld, en matscores worden genormaliseerd naar de afstand tot de wortel, zodat een ingang die op de ene diepte werd gevonden op een andere diepte correct blijft.
FEN en PGN hebben hun eigen parsers en schrijvers. PGN behoudt tags, commentaar, evaluaties en geneste varianten over een volledige rondgang, en de validator legt uit waarom een stelling onreglementair is (twee koningen naast elkaar, een pion op de eerste rij, een rokaderecht zonder toren) in plaats van de engine erin te laten zoeken.
Het zoeken
Principal variation search met de gebruikelijke snoeitechnieken, elk onderdeel gemeten voordat het bleef.
De kern is een alfa-bèta-zoekalgoritme in zijn principal-variation-vorm met iteratief verdiepen en aspiratievensters. Daaromheen zit de standaardgereedschapskist van een moderne engine, één voor één toegevoegd en alleen behouden wanneer een match winst liet zien:
- Null-move pruning, reverse futility pruning, razoring en internal iterative reductions.
- Late move reductions en late move pruning, futility pruning en snoeien op basis van statische ruilevaluatie (SEE).
- Zetordening vanuit de hashzet, slagzetten via SEE, killerzetten, countermoves en een history-tabel met beloningen en straffen.
- Quiescence-zoeken voor slagzetten en promoties, zodat de evaluatie alleen over rustige stellingen wordt bevraagd.
- Lazy SMP: meerdere threads doorzoeken dezelfde boom op verschillende dieptes en delen de hashtabel, op tot 64 kernen.
- Tijdbeheer dat vroeg stopt wanneer de beste zet stabiel is en langer denkt wanneer die verandert (de optie
SoftTime), en een hashtabel die met een leeftijdsmarkering over de zetten van een partij heen wordt bewaard in plaats van gewist.
Niet alles overleefde. De evaluatie naar nul laten schalen naarmate de vijftigzettenteller oploopt, en een correction history voor de statische evaluatie, verloren allebei hun matches en gingen er weer uit. De versienotities leggen verworpen experimenten even zorgvuldig vast als aanvaarde, zodat een idee niet per ongeluk twee keer wordt geprobeerd.
De evaluatie
Een klein neuraal netwerk, getraind op de eigen partijen van de engine.
De eerste evaluatie was met de hand geschreven: stuk-veldtabellen, koningsveiligheid en pionstructuur, afgesteld met de Texel-methode op de eigen partijen van de engine. Ze zit nog in het binaire bestand (UseNNUE false) als referentie. De evaluatie die vandaag speelt is een NNUE, het soort netwerk dat Stockfish tot standaard maakte, maar hier getraind op de eigen data van de engine:
- 768 ingangen, één per stuk, kleur en veld, gezien vanuit elke kant; twee verborgen accumulatoren van 256 neuronen met een clipped ReLU; één uitgang.
- Gewichten gekwantiseerd naar 16-bits gehele getallen. De accumulatoren worden incrementeel bijgewerkt terwijl stukken bewegen, 16 neuronen per SIMD-vector, zodat het evalueren van een stelling een paar honderd geheeltallige bewerkingen kost in plaats van een volledige doorrekening.
- Het netwerk leeft in het binaire bestand als een Swift-bronbestand dat uit de getrainde gewichten wordt gegenereerd; er is geen apart bestand dat kwijt kan raken. Met de optie
NNUEFilekun je voor experimenten een ander netwerk laden.
De trainingslus is zo eenvoudig als het klinkt. zugwunder-datagen laat de engine tegen zichzelf en tegen Stockfish spelen en schrijft per stelling een record van 68 bytes: de kenmerken, Zugwunders eigen zoekscore en de partijuitslag. Ongeveer vier miljoen stellingen per uur op acht kernen. Een PyTorch-script van 150 regels traint het netwerk op een Apple-GPU, embed.py zet de gewichten om in Swift, en de volgende release draagt het nieuwe netwerk. De zesde generatie werd getraind op 236,6 miljoen stellingen; elke generatie werd pas aanvaard na matches tegen haar voorganger en tegen externe engines.
Wat er niet in ging: geen Stockfish-code, geen Stockfish-gewichten, geen Stockfish-evaluaties. Stockfish was tegenstander en meetlat, verder niets.
Eindspelkennis
Berekend, niet gedownload.
Voor koning en dame, toren of pion tegen de kale koning, en voor de twee vierstukseindspelen loper en paard tegen koning en toren tegen loper, schat Zugwunder niet: hij berekent de exacte tabellen bij het opstarten zelf door retrograde analyse en gebruikt de symmetrieën van het bord om het werk klein te houden. De vierstukstabellen kosten ongeveer anderhalve seconde op de achtergrond. Het zoeken, de oplosser en de puzzelgenerator lezen ze allemaal.
Voor al het andere met tot vijf stukken zijn er de Syzygy-tabellen: wijs de engine een map aan en hij speelt die eindspelen perfect en gebruikt de uitspraken in het zoeken na elke slag- of pionzet.
De puzzelgenerator is het kleinste programma op deze pagina en mijn favoriet. Hij loopt de 64 × 64 × 64 plaatsingen van drie stukken in willekeurige volgorde af met een oneven stapgrootte, zodat elke plaatsing precies één keer wordt bezocht zonder lijst; houdt alleen reglementaire stellingen over waarvan de tabelingang „mat in precies n“ zegt; en controleert dan dat precies één eerste zet dat bereikt. De stelling op de voorpagina komt eruit.
Exacte oplossers
Bewijsbomen voor mat, hulpmat, zelfmat en pat.
Een speelzoekalgoritme beantwoordt „wat is hier goed“. Een probleemoplosser moet „is er een gedwongen mat in drie, en is het uniek“ beantwoorden, en dat is een ander programma. Zugwunders oplossers bouwen EN/OF-bewijsbomen: bij de zetten van de aanvaller moet één tak werken, bij de zetten van de verdediger moet elk antwoord beantwoord zijn. Het resultaat is de volledige oplossingsboom met alle verdedigingen, een oordeel bewezen, weerlegd of onbeslist binnen de limiet, en een waarschuwing wanneer een tweede eerste zet het probleem ook oplost, want voor een probleemcomponist is een probleem met bijoplossing een kapot probleem.
Hulpmats (beide kanten werken samen, alle oplossingen worden opgesomd), zelfmats (de partij aan zet dwingt de tegenstander haar mat te zetten) en patproblemen gebruiken dezelfde machinerie met een andere doeltoets. Deze oplossers praten rechtstreeks met de enginebibliotheek in plaats van via UCI, omdat ze de bewijsboom nodig hebben, niet een beste zet.
Meten
Duizenden partijen, en de discipline om ze te geloven.
Het project levert zijn eigen meetgereedschap mee. zugwunder-match laat twee engines tegen elkaar spelen vanuit een openingsboek, elke opening met beide kleuren, met klokken, adjudicatie en een sequential probability ratio test (SPRT) die stopt zodra het resultaat statistisch duidelijk is. Een zoekverandering moet die test tegen de vorige build doorstaan voordat ze wordt overgenomen. zugwunder-rating zet matchresultaten tegen Stockfish op meerdere UCI_Elo-niveaus om in een maximum-likelihood-schatting op de schaal van Stockfish.
Het hoofdcijfer, 3065 ± 20 Elo voor het huidige netwerk, komt uit 500 partijen tegen Stockfish op UCI_Elo 3100 onder toernooiachtige omstandigheden: 60 seconden plus 0,6 per zet, één kern, 256 MB hash, Syzygy-tabellen, waarbij het engineprogramma, de netwerken en de tegenstander in een referentiemap zijn bevroren en hun controlesommen zijn vastgelegd. Een tweede reeks tegen Arasan gaf een consistent beeld. Dezelfde bevroren opstelling had het vorige netwerk op 3045 ± 20 gezet. Die 500 partijen zijn de bevroren referentiemeting; in de loop van de weken speelde het cluster van machines achter het project er duizenden meer tegen Stockfish en andere engines, voor SPRT-tests, kalibratie en dataverzameling. Dit zijn allemaal schattingen op de gekalibreerde schaal van een andere engine, geen officiële ratinglijst, en dat zeg ik overal waar het getal verschijnt.
De instelbare sterkte van de UCI-engine werd op dezelfde manier gekalibreerd: meer dan 5.000 partijen tegen Stockfish 17.1 op een Linux-machine op zeven vaardigheidsniveaus, met betrouwbaarheidsintervallen voor elk, geïnterpoleerd tot een UCI_Elo-schaal van 1100 tot 3000. Ze geldt als voorlopig tot ze tegen een tweede tegenstander en op macOS is gecontroleerd, want een kalibratie tegen één tegenstander is een kalibratie tegen één tegenstander.
Onder de matches liggen 364 Swift-tests over de engine, de app en Wolfgang, en 32 protocoltests die het echte zugwunder-uci-programma door de UCI-specificatie van 2006 jagen: ponderen, searchmoves, knooplimieten, een stop die aankomt voordat het zoeken is begonnen, invoer verdeeld over meerdere pipe-schrijfacties. De protocoltest zit bij elk pakket, zodat je hem op je eigen machine kunt draaien.
De apps
SwiftUI en AppKit, vier talen, VoiceOver, een zorgvuldige updater.
Zugwunder en Wolfgang zijn native macOS-apps in SwiftUI, met AppKit waar SwiftUI tekortschiet (vensters, het toetsenbord, schermafbeeldingen, de sleutelhanger). Het bord wordt door de app zelf getekend, ondersteunt slepen en neerzetten en volledige toetsenbordbediening, en leest zetten voor onder VoiceOver. Alle ruim 900 interfaceteksten bestaan in het Duits, Engels, Frans en Nederlands, en de taal schakel je om in de kopbalk van het venster.
Beide apps bundelen hun eigen kopie van zugwunder-uci en praten ermee via UCI, precies zoals ze dat met Stockfish zouden doen: de app stuurt de beginstelling en de volledige zettenlijst, en het engineproces met zijn hashtabel blijft de hele partij in leven. Alleen de exacte oplossers roepen de enginebibliotheek rechtstreeks aan. Een app-release verandert daardoor nooit de versie van de engine, en de engine kan op zichzelf worden getest.
De updater is een kleine aparte helper. De app bevraagt een JSON-feed over HTTPS, controleert dat de download van dezelfde host komt, verifieert grootte en SHA-256, koppelt de schijfkopie alleen-lezen aan, controleert de bundle-identifier en het Developer ID-team, wisselt de bundel atomair met een back-up en herstart. Als een stap mislukt, wordt de oude versie teruggezet.
De nieuwste functie, een stelling herkennen uit een foto, is de ene plek waar de app met een dienst praat: hij stuurt de afbeelding via de API-sleutel die je invoert naar Claude van Anthropic, en alleen wanneer je op de knop drukt. Het antwoord wordt als voorstel behandeld, als schaakstelling gevalideerd en ter correctie getoond. Een eerdere versie gebruikte een lokaal model via Ollama; de huidige gebruikt Claude, en zegt dat naast de knop.
Drie platforms, één build
macOS, Linux en de browser uit dezelfde bron.
- macOS: Apple silicon, ondertekend met een Developer ID, genotariseerd door Apple en gestapled. Het releasescript verhoogt de versie, bouwt, ondertekent, notariseert, schrijft de updatefeed en een manifest met controlesommen, en bewaart de logboeken.
- Linux: op de Mac cross-gecompileerd met de statische Linux-SDK van Swift voor x86_64 (met AVX2) en ARM64. Het resultaat is één statisch gelinkt bestand dat geen bibliotheken nodig heeft, en daarom zowel op een Raspberry Pi als op een server draait.
- WebAssembly: gebouwd met de Swift-SDK voor WebAssembly tegen WASI en FoundationEssentials. Een Web Worker voert de engine uit zodat de pagina reageert; de gecomprimeerde module is kleiner dan drie megabyte. De speelpagina gebruikt hem, en hetzelfde pakket staat bij de downloads als je het zelf wilt hosten.
Een volledige build draait de Swift-tests, de protocoltests en de tests van de dataverzamelaar, bouwt alle pakketten, ondertekent en notariseert de macOS-pakketten en weigert te publiceren als er iets mislukt. De map die hij oplevert bevat de pakketten, een manifest, een SHA256SUMS-bestand en veertig logbestanden.
In cijfers
Geteld op 2 november 2026.
| Engine | 10.200 regels | Swift, zonder de 4.400 gegenereerde regels met de netwerkgewichten. |
|---|---|---|
| Zugwunder-app | 11.900 regels | SwiftUI en AppKit, inclusief de interface van de oplossers, de trainer, de updaterclient en de beeldimport. |
| Wolfgang | 8.800 regels | De UCI-schaakinterface met toernooien en enginebeheer. |
| Gereedschap | 1.400 regels | Programma's voor matches, tuning, datageneratie, rating, analyse en herlabeling, plus de UCI- en WebAssembly-frontends. |
| Tests | 7.500 regels | 364 Swift-tests, 32 protocoltests en de Python-controles voor de dataverzamelaar. |
| Training | 150 regels | Het PyTorch-script dat zes generaties netwerken opleverde. |
| Releases | Meer dan 30 | App-versies sinds 25 september 2026, van Mattlotse 1.0.3 tot Zugwunder 1.1.15. |
Tijdlijn
Van een matoplosser naar drie programma's.
Mattlotse wordt Zugwunder
Mijn oude app Mattlotse, een mat-in-n-oplosser met volledige bewijsbomen en controle op bijoplossingen, hernoemd tot Zugwunder. PVS, null move, LMR en de handgeschreven evaluatie komen dezelfde dag.
Partijen, trainer, bestanden
Speelsterktes, schaakklok, puzzelbibliotheek, PGN met commentaar en varianten, hulpmat- en zelfmatoplossers, eigen eindspeltabellen, de UCI-engine en het matchgereedschap met SPRT, Lazy SMP.
Vier talen, netwerk v4
Duits, Engels, Frans en Nederlands. Het vierde netwerk (113 miljoen stellingen) wint 78 Elo op het derde.
Netwerk v5, eerste ratingschatting
170 miljoen stellingen, +25 Elo. Duizend partijen tegen Stockfish 19 zetten de engine op 3006 ± 17 op de CCRL-achtige schaal.
In de browser
De engine bouwt voor WebAssembly en speelt op f97.be zonder installatie.
Drie pakketten
App, Wolfgang en engine worden aparte pakketten met één releasescript. Beide apps bundelen de engine en spreken UCI met hem.
Netwerk v6, UCI compleet
Matches van 500 partijen tegen Stockfish en Arasan; 3065 ± 20 Elo. Elke opdracht van de UCI-specificatie geïmplementeerd en getest.
Sterktekalibratie
Meer dan 5.000 partijen tegen Stockfish 17.1 geven de engine een voorlopige
UCI_Elo-schaal van 1100 tot 3000.Puzzelgenerator, beeldimport
Nieuwe puzzels uit de eindspeltabellen met een gecontroleerd unieke sleutelzet; stellingen uit foto's in alle vier de talen; vensteropnamen.
Controleer de cijfers zelf.
Elk pakket bevat controlesommen en de protocoltest. Het matchgereedschap zit in het gereedschapsarchief.