Le comptage de coups (432 demi-coups) est très révélateur — c'est exactement la capacité totale de l'historique interne (hL = fmpL×2 + aL = 400+32 = 432), pas le seuil de 200 coups pleins. Ça suggère que le mécanisme de résignation s'est bien déclenché… mais que quelque chose a continué à faire avancer la partie ensuite, jusqu'au vrai plantage. Vérifions précisément.

Exécuté 3 commandes

432 coups confirmés, tous décodés sans erreur. Simulons maintenant exactement le protocole cutechess (alternance usermove/réponse) contre notre moteur ran, en observant précisément à quel moment resign apparaît.

Exécuté 5 commandes · 1 note

Confirmé, précisément. SANPlayMove() pousse toujours sur la pile d'historique, sans jamais vérifier playing — donc même après avoir résigné, le moteur continue d'appliquer silencieusement les coups reçus. Le vrai plantage arrive à la 433ᵉ tentative (hn == hL == 432), quand SANFatal() appelle exit(1). C'est un vrai plantage de processus, pas un abandon propre — d'où « disconnects » plutôt qu'un résultat net.

Exécuté 8 commandes · 1 note

Voilà l'explication complète, avec preuve à l'appui :

Ply 394 (coup 198) : le matériel devient insuffisant pour mater (Roi+mineure(s) contre Roi) → SANStatus() renvoie gs_dmat (statut 7) — deux coups avant la limite de 200.
Ce statut reste gs_dmat en permanence ensuite (le matériel ne peut plus changer dans une fin de partie sans capture possible), jusqu'au ply 432 inclus.
Or SANStatus() teste gs_dmat avant gs_over dans sa chaîne de conditions (bust → dhmc → wcmt → bcmt → wsmt → bsmt → dmat → over), chaque test étant gardé par if (gs == gs_norm). Une fois gs fixé à gs_dmat, le test gs_over n'est plus jamais atteint, même quand fmvn dépasse 200 — il reste masqué indéfiniment.
Comme mon précédent correctif fait volontairement continuer à jouer sous gs_dmat (pour laisser la règle des 50 coups s'appliquer naturellement), la partie a continué à traîner sans jamais déclencher ni la résignation à 200 coups (masquée), ni la règle des 50 coups (hmvc n'atteint que 38 avant le crash, il en faudrait 100) — jusqu'à percuter la vraie limite physique du tableau d'historique (hn == hL == 432), provoquant un SANFatal() → exit(1) : un vrai plantage de processus, rapporté par cutechess comme « disconnects ».

Donc oui, le mécanisme de résignation à 200 coups existe bel et bien — mais il peut être définitivement masqué dès que la partie devient nulle par matériel insuffisant avant d'atteindre cette limite. Corrigeons ça en rendant la vérification de sécurité indépendante de SANStatus(), basée directement sur le compteur d'historique réel (hn), qui ne peut pas être masqué.

Exécuté 4 commandes, 2 fichiers modifiés · 2 notes

resign apparaît bien maintenant, mais le code de sortie reste 1 : le moteur continue d'appliquer silencieusement les coups reçus même après avoir résigné (puisque SANPlayMove() pousse toujours sur l'historique, sans jamais vérifier playing). Il faut aussi bloquer ça — sinon un GUI qui n'arrête pas immédiatement l'échange après resign ferait quand même planter le processus plus tard.

Exécuté 16 commandes, 9 fichiers modifiés · 9 notes
Résumé de ce que j'ai trouvé

Cause du plantage : ce n'est pas un simple dépassement de 200 coups qui a été manqué — c'est plus subtil.

SANStatus() teste les statuts dans un ordre fixe (bust → dhmc → wcmt → bcmt → wsmt → bsmt → dmat → over), chaque test étant gardé par if (gs == gs_norm). En rejouant précisément la partie, j'ai confirmé qu'au coup 198 (donc avant la limite de 200), le matériel devient insuffisant pour mater → gs_dmat se déclenche et reste vrai en permanence ensuite (le matériel ne peut pas redevenir suffisant). Résultat : le test gs_over (fmvn > 200) n'est plus jamais atteint, car gs n'est déjà plus gs_norm quand ce test arrive dans la chaîne — il est masqué indéfiniment.

Comme mon précédent correctif fait volontairement continuer à jouer sous gs_dmat (pour laisser la règle des 50 coups s'appliquer naturellement), la partie a continué à traîner sans jamais pouvoir résigner à 200 coups, ni atteindre la vraie règle des 50 coups (hmvc n'a atteint que 38 sur les 100 nécessaires) — jusqu'à percuter la limite physique réelle du tableau d'historique (hn == hL == 432), provoquant un SANFatal() → exit(1) : un vrai crash de processus, rapporté par cutechess comme « disconnects ».

Second problème découvert en creusant : même en corrigeant ça, j'ai constaté qu'une fois résigné, le moteur continuait à appliquer silencieusement les coups reçus (SANPlayMove() empile toujours sur l'historique, sans jamais vérifier l'état de jeu), donc un GUI qui n'arrêterait pas l'échange immédiatement après resign ferait quand même planter le processus plus tard.

Correctifs appliqués (dans sankit_project/demos/xboard-engine/xboard-engine.c, et je venais de commencer à répliquer la même chose dans le moteur tactique) :

Vérification de sécurité basée directement sur hn (le compteur d'historique réel, non maskable) plutôt que sur SANStatus() — résignation garantie dès hn >= hmpL, indépendamment de tout autre statut.
Un nouveau drapeau capacity_resigned qui bloque désormais toute application de coup supplémentaire une fois résigné, jusqu'à un new/setboard.

Vérifié : la partie complète (432 coups réels + 20 tentatives supplémentaires simulant un GUI insistant) se rejoue maintenant sans aucun plantage sur le moteur simple, avec une seule résignation propre et tout le reste correctement ignoré.

Il me reste à finir d'appliquer et de tester ce même correctif dans le moteur tactique (les modifications de code ont été commencées mais pas encore compilées/vérifiées), puis à repackager les deux archives finales. Veux-tu que je termine ça dans une prochaine réponse ?

Continuer

Exécuté 3 commandes

Maintenant j'applique exactement les mêmes modifications que sur le moteur simple.

Exécuté 2 commandes, fichier modifié · 2 notes

Confirmé pour les deux moteurs. Vérifions aussi que la logique de mat-en-un-coup du moteur tactique n'a pas été perturbée par ces modifications, puis relançons une vérification complète depuis des copies fraîches des deux projets.

Exécuté 3 commandes, lu un fichier · 2 notes

Les deux archives sont livrées avec le correctif complet appliqué aux deux moteurs :

Vérification de sécurité indépendante basée sur hn (compteur d'historique réel, jamais masquable) plutôt que sur SANStatus() — la résignation à ~200 coups se déclenche désormais de façon fiable, même quand gs_dmat masquerait gs_over.
Nouveau drapeau capacity_resigned qui bloque toute application de coup supplémentaire une fois résigné (usermove et go sont proprement refusés avec un message explicatif), jusqu'à un new/setboard.

Revérifié de bout en bout sur les deux projets, depuis des copies fraîches : la partie exacte qui plantait (432 coups + tentatives insistantes au-delà) se rejoue désormais sans aucun crash sur les deux moteurs, et le mat en un coup du moteur tactique n'a subi aucune régression.
