Comment je l’ai construit.
Zugwunder est écrit de zéro en Swift : aucune bibliothèque de moteur en dessous, aucun framework au-delà de ce qu’Apple fournit. Cette page est la visite de l’intérieur, pour les joueurs curieux et pour les programmeurs qui veulent savoir sur quoi reposent les chiffres.
Pourquoi de zéro
Parce qu’un moteur d’échecs est un programme que l’on peut mesurer.
Je construis des logiciels qui tournent sur mes propres machines plutôt que dans le centre de données de quelqu’un d’autre, et un moteur d’échecs est le banc d’essai idéal : chaque idée peut être soumise à quelques milliers de parties et gagne de l’Elo ou n’en gagne pas. On ne discute pas avec la feuille de partie. Zugwunder a commencé sous le nom de Mattlotse, mon ancien solveur de problèmes de mat en n. Le 25 septembre 2026, je l’ai renommé Zugwunder, et la recherche, l’évaluation, l’entraîneur, les parties, le réseau de neurones et les deux autres programmes en sont sortis dans les semaines qui ont suivi, version après version, un match contre la version précédente décidant si un changement restait.
Tout est en Swift 6 avec concurrence stricte, construit avec le Swift Package Manager, et ne dépend de rien en dehors des frameworks d’Apple et de la bibliothèque standard de Swift. La seule exception est Fathom, une petite bibliothèque C sous licence MIT qui lit les tables de finales Syzygy ; écrire mon propre lecteur pour ce format n’aurait ajouté que des bugs.
L’échiquier
Bitboards, clés de Zobrist, une table de hachage sans verrou.
La position est un ensemble de bitboards de 64 bits, un par type de pièce et par couleur, de sorte que « toutes les cases qu’un cavalier attaque d’ici » est une consultation de table et « toutes les pièces que cette tour attaque » quelques opérations sur les bits. La génération des coups produit strictement des coups légaux, roque, prise en passant et promotions compris, avec un chemin rapide séparé pour les prises, utilisé par la recherche de quiescence.
Chaque position possède une clé de Zobrist de 64 bits, qui pilote la table de transposition, la détection des répétitions et le cache de structure de pions. La table de transposition est partagée par tous les threads de recherche sans verrou, et les scores de mat sont normalisés par rapport à la distance à la racine, de sorte qu’une entrée trouvée à une profondeur reste correcte à une autre.
FEN et PGN ont leurs propres analyseurs et rédacteurs. Le PGN conserve balises, commentaires, évaluations et variantes imbriquées sur un aller-retour, et le validateur explique pourquoi une position est illégale (deux rois en contact, un pion sur la première rangée, un droit de roque sans tour) au lieu de laisser le moteur la chercher.
La recherche
Une recherche de variante principale avec les élagages habituels, chaque élément mesuré avant d’être gardé.
Le cœur est une recherche alpha-bêta sous sa forme de variante principale, avec approfondissement itératif et fenêtres d’aspiration. Autour d’elle se trouve la boîte à outils standard d’un moteur moderne, ajoutée pièce par pièce et conservée seulement quand un match montrait un gain :
- Élagage par coup nul, reverse futility pruning, razoring et internal iterative reductions.
- Late move reductions et late move pruning, futility pruning et élagage par évaluation statique des échanges (SEE).
- Tri des coups à partir du coup de hachage, des prises par SEE, des coups tueurs, des contre-coups et d’une table d’historique avec récompenses et pénalités.
- Recherche de quiescence pour les prises et les promotions, afin que l’évaluation ne soit interrogée que sur des positions calmes.
- Lazy SMP : plusieurs threads explorent le même arbre à des profondeurs différentes et partagent la table de hachage, jusqu’à 64 cœurs.
- Une gestion du temps qui s’arrête tôt quand le meilleur coup est stable et réfléchit plus longtemps quand il change (l’option
SoftTime), et une table de hachage conservée d’un coup à l’autre de la partie avec une marque d’âge au lieu d’être vidée.
Tout n’a pas survécu. Ramener l’évaluation vers zéro à mesure que le compteur des cinquante coups monte, et un historique de correction pour l’évaluation statique, ont tous deux perdu leurs matchs et ont été retirés. Les notes de version consignent les expériences rejetées aussi soigneusement que les acceptées, pour qu’une idée ne soit pas essayée deux fois par accident.
L’évaluation
Un petit réseau de neurones, entraîné sur les propres parties du moteur.
La première évaluation était écrite à la main : tables pièce-case, sécurité du roi et structure de pions, ajustées par la méthode Texel sur les propres parties du moteur. Elle est toujours dans le binaire (UseNNUE false) comme référence. L’évaluation qui joue aujourd’hui est un NNUE, le type de réseau que Stockfish a rendu standard, mais entraîné ici à partir des propres données du moteur :
- 768 entrées, une par pièce, couleur et case, vues du point de vue de chaque camp ; deux accumulateurs cachés de 256 neurones avec une ReLU bornée ; une sortie.
- Des poids quantifiés en entiers 16 bits. Les accumulateurs sont mis à jour de façon incrémentale à chaque déplacement de pièce, 16 neurones par vecteur SIMD, de sorte qu’évaluer une position coûte quelques centaines d’opérations entières plutôt qu’une passe complète.
- Le réseau vit dans le binaire sous forme de fichier source Swift généré à partir des poids entraînés ; il n’y a pas de fichier séparé à perdre. Un autre réseau peut être chargé avec l’option
NNUEFilepour des expériences.
La boucle d’entraînement est aussi simple qu’il y paraît. zugwunder-datagen fait jouer le moteur contre lui-même et contre Stockfish et écrit un enregistrement de 68 octets par position : les caractéristiques, le score de recherche propre à Zugwunder et le résultat de la partie. Environ quatre millions de positions par heure sur huit cœurs. Un script PyTorch de 150 lignes entraîne le réseau sur un GPU Apple, embed.py transforme les poids en Swift, et la version suivante embarque le nouveau réseau. La sixième génération a été entraînée sur 236,6 millions de positions ; chaque génération n’a été acceptée qu’après des matchs contre la précédente et contre des moteurs extérieurs.
Ce qui n’y est pas entré : aucun code Stockfish, aucun poids Stockfish, aucune évaluation Stockfish. Stockfish a été un adversaire et un étalon, rien d’autre.
Connaissance des finales
Calculée, pas téléchargée.
Pour roi et dame, tour ou pion contre roi seul, et pour les deux finales à quatre pièces fou et cavalier contre roi et tour contre fou, Zugwunder n’estime pas : il calcule lui-même les tables exactes au démarrage par analyse rétrograde, en utilisant les symétries de l’échiquier pour limiter le travail. Les tables à quatre pièces prennent environ une seconde et demie en arrière-plan. La recherche, le solveur et le générateur de problèmes les lisent tous.
Pour tout le reste jusqu’à cinq pièces, il y a les tables Syzygy : indiquez un dossier au moteur et il joue ces finales parfaitement et utilise leurs verdicts dans la recherche après chaque prise ou coup de pion.
Le générateur de problèmes est le plus petit programme de cette page et mon préféré. Il parcourt les 64 × 64 × 64 placements de trois pièces dans un ordre aléatoire avec un pas impair, de sorte que chaque placement est visité exactement une fois sans liste ; ne garde que les positions légales dont l’entrée de table dit « mat en exactement n » ; puis vérifie qu’un seul premier coup y parvient. La position de la page d’accueil en est sortie.
Solveurs exacts
Des arbres de preuve pour le mat, le mat aidé, l’automat et le pat.
Une recherche de jeu répond à « qu’est-ce qui est bon ici ». Un solveur de problèmes doit répondre à « y a-t-il un mat forcé en trois, et est-il unique », ce qui est un autre programme. Les solveurs de Zugwunder construisent des arbres de preuve ET/OU : aux coups de l’attaquant, une branche doit fonctionner ; aux coups du défenseur, chaque réponse doit être réfutée. Le résultat est l’arbre de solution complet avec toutes les défenses, un verdict prouvé, réfuté ou indécis dans la limite, et un avertissement lorsqu’un second premier coup résout aussi le problème, car pour un problémiste, un problème démoli est un problème cassé.
Les mats aidés (les deux camps coopèrent, toutes les solutions sont listées), les automats (le camp au trait force l’adversaire à le mater) et les problèmes de pat utilisent la même mécanique avec un autre test d’objectif. Ces solveurs parlent directement à la bibliothèque du moteur plutôt qu’en UCI, parce qu’ils ont besoin de l’arbre de preuve, pas d’un meilleur coup.
Mesurer
Des milliers de parties, et la discipline de les croire.
Le projet livre ses propres outils de mesure. zugwunder-match fait jouer deux moteurs l’un contre l’autre à partir d’un livre d’ouvertures, chaque ouverture avec les deux couleurs, avec pendules, arbitrage et un test séquentiel du rapport de vraisemblance (SPRT) qui s’arrête dès que le résultat est statistiquement clair. Un changement de recherche doit passer ce test contre la version précédente avant d’être intégré. zugwunder-rating transforme les résultats de matchs contre Stockfish à plusieurs niveaux UCI_Elo en une estimation du maximum de vraisemblance sur l’échelle de Stockfish.
Le chiffre principal, 3065 ± 20 Elo pour le réseau actuel, provient de 500 parties contre Stockfish à UCI_Elo 3100 dans des conditions proches du tournoi : 60 secondes plus 0,6 par coup, un cœur, 256 Mo de hachage, tables Syzygy, le binaire du moteur, les réseaux et l’adversaire étant figés dans un dossier de référence et leurs sommes de contrôle consignées. Une seconde série contre Arasan a donné une image cohérente. Le même dispositif figé avait placé le réseau précédent à 3045 ± 20. Ces 500 parties sont la mesure de référence figée ; au fil des semaines, le cluster de machines derrière le projet en a joué des milliers d’autres contre Stockfish et d’autres moteurs, pour les tests SPRT, la calibration et la collecte de données. Tout cela reste des estimations sur l’échelle calibrée d’un autre moteur, pas une liste de classement officielle, et je le dis partout où le chiffre apparaît.
La force réglable du moteur UCI a été calibrée de la même façon : plus de 5 000 parties contre Stockfish 17.1 sur une machine Linux à sept niveaux, avec des intervalles de confiance pour chacun, interpolés en une échelle UCI_Elo de 1100 à 3000. Elle reste provisoire tant qu’elle n’a pas été vérifiée contre un second adversaire et sous macOS, car une calibration contre un adversaire est une calibration contre un adversaire.
Sous les matchs, il y a 364 tests Swift couvrant le moteur, l’app et Wolfgang, et 32 tests de protocole qui font passer le vrai binaire zugwunder-uci à travers la spécification UCI de 2006 : réflexion sur le temps adverse, searchmoves, limites de nœuds, un stop qui arrive avant le début de la recherche, une entrée répartie sur plusieurs écritures dans le tube. Le test de protocole est inclus dans chaque paquet pour que vous puissiez l’exécuter sur votre propre machine.
Les apps
SwiftUI et AppKit, quatre langues, VoiceOver, un programme de mise à jour soigneux.
Zugwunder et Wolfgang sont des apps macOS natives en SwiftUI, avec AppKit là où SwiftUI ne suffit pas (fenêtres, clavier, captures d’écran, trousseau). L’échiquier est dessiné par l’app, prend en charge le glisser-déposer et la commande complète au clavier, et énonce les coups sous VoiceOver. Les quelque 900 textes d’interface existent en allemand, anglais, français et néerlandais, et la langue se change dans l’en-tête de la fenêtre.
Les deux apps embarquent leur propre copie de zugwunder-uci et lui parlent en UCI, exactement comme elles le feraient avec Stockfish : l’app envoie la position de départ et la liste complète des coups, et le processus du moteur, avec sa table de hachage, survit toute la partie. Seuls les solveurs exacts appellent directement la bibliothèque du moteur. Une version de l’app ne change donc jamais la version du moteur, et le moteur peut être testé seul.
Le programme de mise à jour est un petit assistant séparé. L’app interroge un flux JSON via HTTPS, vérifie que le téléchargement vient du même hôte, contrôle sa taille et son SHA-256, monte l’image disque en lecture seule, vérifie l’identifiant du bundle et l’équipe Developer ID, remplace le bundle de façon atomique avec une sauvegarde, et relance. Si une étape échoue, l’ancienne version est remise en place.
La fonction la plus récente, reconnaître une position sur une photo, est le seul endroit où l’app parle à un service : elle envoie l’image à Claude d’Anthropic via la clé API que vous saisissez, et seulement lorsque vous appuyez sur le bouton. La réponse est traitée comme une proposition, validée comme position d’échecs et affichée pour correction. Une version antérieure utilisait un modèle local via Ollama ; l’actuelle utilise Claude, et le dit à côté du bouton.
Trois plateformes, une construction
macOS, Linux et le navigateur à partir de la même source.
- macOS : Apple Silicon, signé avec un Developer ID, notarisé par Apple et agrafé. Le script de publication incrémente la version, construit, signe, notarise, écrit le flux de mise à jour et un manifeste avec sommes de contrôle, et conserve les journaux.
- Linux : compilé de façon croisée sur le Mac avec le SDK Linux statique de Swift pour x86_64 (avec AVX2) et ARM64. Le résultat est un seul fichier lié statiquement qui n’a besoin d’aucune bibliothèque, ce qui lui permet de tourner sur un Raspberry Pi comme sur un serveur.
- WebAssembly : construit avec le SDK Swift pour WebAssembly sur WASI et FoundationEssentials. Un Web Worker exécute le moteur pour que la page reste réactive ; le module compressé fait moins de trois mégaoctets. La page de jeu l’utilise, et le même paquet est dans les téléchargements si vous voulez l’héberger vous-même.
Une construction complète exécute les tests Swift, les tests de protocole et les tests du collecteur de données, construit tous les paquets, signe et notarise ceux pour macOS, et refuse de publier si quoi que ce soit échoue. Le dossier qu’elle produit contient les paquets, un manifeste, un fichier SHA256SUMS et quarante fichiers journaux.
En chiffres
Comptés le 2 novembre 2026.
| Moteur | 10 200 lignes | Swift, sans les 4 400 lignes générées qui contiennent les poids du réseau. |
|---|---|---|
| App Zugwunder | 11 900 lignes | SwiftUI et AppKit, y compris l’interface des solveurs, l’entraîneur, le client de mise à jour et l’import d’images. |
| Wolfgang | 8 800 lignes | L’interface d’échecs UCI avec tournois et gestionnaire de moteurs. |
| Outils | 1 400 lignes | Programmes de match, d’ajustement, de génération de données, de classement, d’analyse et de réétiquetage, plus les frontaux UCI et WebAssembly. |
| Tests | 7 500 lignes | 364 tests Swift, 32 tests de protocole et les vérifications Python du collecteur de données. |
| Entraînement | 150 lignes | Le script PyTorch qui a produit six générations de réseaux. |
| Versions | Plus de 30 | Versions de l’app depuis le 25 septembre 2026, de Mattlotse 1.0.3 à Zugwunder 1.1.15. |
Chronologie
D’un solveur de mats à trois programmes.
Mattlotse devient Zugwunder
Mon ancienne app Mattlotse, un solveur de mat en n avec arbres de preuve complets et détection des démolitions, renommée Zugwunder. PVS, coup nul, LMR et l’évaluation écrite à la main arrivent le jour même.
Parties, entraîneur, fichiers
Niveaux de jeu, pendule, bibliothèque de problèmes, PGN avec commentaires et variantes, solveurs de mat aidé et d’automat, tables de finales maison, le moteur UCI et l’outil de match avec SPRT, Lazy SMP.
Quatre langues, réseau v4
Allemand, anglais, français et néerlandais. Le quatrième réseau (113 millions de positions) gagne 78 Elo sur le troisième.
Réseau v5, première estimation de classement
170 millions de positions, +25 Elo. Mille parties contre Stockfish 19 placent le moteur à 3006 ± 17 sur l’échelle de type CCRL.
Dans le navigateur
Le moteur se compile en WebAssembly et joue sur f97.be sans installation.
Trois paquets
App, Wolfgang et moteur deviennent des paquets séparés avec un seul script de publication. Les deux apps embarquent le moteur et lui parlent en UCI.
Réseau v6, UCI complet
Matchs de 500 parties contre Stockfish et Arasan ; 3065 ± 20 Elo. Chaque commande de la spécification UCI implémentée et testée.
Calibration de la force
Plus de 5 000 parties contre Stockfish 17.1 donnent au moteur une échelle
UCI_Eloprovisoire de 1100 à 3000.Générateur de problèmes, import d’images
De nouveaux problèmes issus des tables de finales avec un coup clé vérifié unique ; des positions à partir de photos dans les quatre langues ; des captures de fenêtre.
Vérifiez les chiffres vous-même.
Chaque paquet est livré avec ses sommes de contrôle et le test de protocole. Les outils de match sont dans l’archive des outils.