# Correctifs appliqués à ZCT 0.3.2498 → 0.3.2499

Contexte : lors du tournoi (`tournoi.log` / `tournoi.pgn`), ZCT a été
déconnecté après avoir renvoyé un score de mat corrompu (`+32765`) suivi
d'un coup illégal (`ILL(B2B7,0)`) en boucle, jusqu'à ce que le gestionnaire
de tournoi tue le processus (`Terminating process of engine ZCT 0.3.2498(1)`).

Le fichier `logfile.txt` fourni dans l'archive d'origine montre que ce même
symptôme (`ILL(G6G5,0)` en boucle avec un score à ±32764/32765) s'était déjà
produit avant — ce n'est donc pas un accident isolé.

## Fichiers modifiés

- `hash.c` — **correction de la cause racine**
- `zct.c`, `searchroot.c`, `functions.h` — **filet de sécurité applicatif**
- `CHANGES.txt` — entrée de changelog documentant le correctif

## 1. `hash.c` — course de données (data race) dans la table de hachage SMP

`hash_probe()` et `hash_store()` lisaient/écrivaient les champs `hashkey`
et `data` d'une entrée comme deux accès mémoire ordinaires non
synchronisés, alors qu'ils sont partagés par tous les threads de recherche
(le moteur tournait ici sur 4 processeurs, `Using 4 processors`). Rien ne
garantissait l'ordre des écritures/lectures entre threads : un thread
pouvait lire une entrée « déchirée » (un champ appartenant à une position,
l'autre à une position totalement différente venant d'être écrite par un
autre thread), et la vérification par XOR (« lockless hashing ») pouvait
occasionnellement passer quand même. C'est exactement ce type de course qui
peut produire un score aberrant et un coup qui n'appartient pas à la
position réelle.

**Correctif** : utilisation d'opérations atomiques explicites
(`__atomic_load_n` / `__atomic_store_n`, intrinsèques GCC/Clang) avec un
ordre garanti :
- écriture : `data` (relaxed) **puis** `hashkey` (release) ;
- lecture : `hashkey` (acquire) **puis** `data` (relaxed).

Ainsi, un thread qui voit le nouveau `hashkey` est garanti de voir aussi le
`data` associé, et jamais un mélange des deux.

## 2. Filet de sécurité applicatif (`zct.c` / `searchroot.c`)

Le code contenait déjà une vérification de légalité pour la partie du PV
extraite du hash au-delà de la profondeur recherchée (`pv_string()` dans
`output.c`), avec le commentaire « *hash collisions can cause bad moves* ».
Mais **cette vérification n'existait pas** au moment où le coup final
(`board.pv_stack[0][0]`) était envoyé au protocole (XBoard/UCI/console) —
c'est justement ce chemin qui a produit `ILL(B2B7,0)` en boucle.

**Correctif** :
- Dans `zct.c` (boucle principale `game_loop()`), le coup choisi par la
  recherche est revérifié (`move_is_valid()` + `is_legal()`) juste avant
  d'être imprimé/joué.
- En cas de coup invalide, on bascule sur `fallback_legal_root_move()`
  (nouvelle fonction, `searchroot.c`), qui regénère la liste des coups
  légaux à la racine (`generate_root_moves()` applique déjà
  `check_legality()`) et retourne le premier coup — donc **garanti légal**,
  même si ce n'est pas celui préféré par la recherche corrompue.

Résultat : même si une corruption résiduelle devait un jour se reproduire
(bug futur, autre course non identifiée...), ZCT enverra toujours un coup
légal à l'arbitre au lieu de boucler indéfiniment sur `ILL(...)` jusqu'au
timeout/déconnexion.

## Recommandation complémentaire (hors code)

En attendant/en plus de ce correctif, il reste prudent de surveiller les
prochaines parties en SMP à 4 threads, et de configurer le gestionnaire de
tournoi avec un timeout de détection plus court en cas de messages
d'erreur répétés, pour éviter qu'une partie reste bloquée aussi longtemps
avant d'être arbitrée.

## Vérification effectuée

Le projet corrigé compile sans erreur ni nouveau warning
(`make CFLAGS="-DSMP -DMAX_CPUS=4 -Wall"`) et a été testé en exécution
(recherche multi-thread fonctionnelle, aucun message `ILL(...)`).
