Références : format des analyses documenté et vérifié

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Pierre MARTIN
2026-11-03 14:51:41 +01:00
co-authored by Claude Opus 5.5
parent 242a2433d2
commit 7e958fab8e
2 changed files with 136 additions and 3 deletions
+89 -3
View File
@@ -11,14 +11,16 @@ d'un fichier de résultat.
| --- | --- |
| `positions.json` | Positions d'entrée, versionnées, écrites à la main |
| `results/<id>.json` | Résultats de gnubg, un fichier par position, générés |
| `generate.py` | Script exécuté dans gnubg qui produit `results/` |
| `compare.py` | Compare le moteur embarqué à `results/` (dernière section) |
| `analyses.json` | Analyses d'entrée : coups joués, décisions de videau, niveaux de jeu |
| `analysis/<id>.json` | Résultats de l'analyse de partie de gnubg, générés (section « Analyses ») |
| `generate.py` | Script exécuté dans gnubg qui produit `results/` et `analysis/` |
| `compare.py` | Compare le moteur embarqué à `results/` et `analysis/` (dernière section) |
Commandes :
```
make reference-image # construit l'image Docker du gnubg de référence
make reference # régénère results/ (une vingtaine de secondes)
make reference # régénère results/ et analysis/ (une quarantaine de secondes)
make test # vérifie la cohérence (tests/test_reference.sh) et
# compare le moteur embarqué (tests/test_engine_reference.sh)
```
@@ -222,6 +224,69 @@ Sortie de `gnubg.cfevaluate` (`GeneralCubeDecisionE` puis
`TOOGOOD_PASS`, `REDOUBLE_TAKE`, `NODOUBLE_DEADCUBE`…) ;
- `decision_text` : le libellé anglais correspondant de gnubg.
## Analyses : `analyses.json` et `analysis/<id>.json`
Ce que gnubg dit d'un coup joué, d'une décision de videau et d'un jet de dés
(sa commande « analyse game »), et le coup que joue son ordinateur à chaque
niveau. Pour chaque cas, `generate.py` rejoue dans gnubg une courte partie
entre deux joueurs humains : la position (`set gnubgid`), puis le coup ou la
décision, puis l'analyse. Rien n'est calculé dans le script : les valeurs
viennent des enregistrements de la partie analysée et des statistiques que
gnubg en tire.
`analyses.json` a trois listes :
- `moves` : `position_id`, `match_id`, `dice` et `played`, le coup joué en
notation de gnubg (`null` : le meilleur coup selon gnubg, pour les cas qui
ne servent qu'à la chance du jet) ;
- `cubes` : `position_id`, `match_id` (sans dés) et `action`, la décision :
`no_double` ou `double` pour le joueur au trait, `take` ou `pass` pour son
adversaire ;
- `levels` : `position_id`, `match_id` et `dice` (`null` : décision de
videau).
Un fichier de résultat reprend le cas, avec `type` (`move`, `cube` ou
`level`), `gnubg_commit` et `met`, puis, aux niveaux d'analyse 0 et 2 plis
(clés de `plies`, réglages prédéfinis de gnubg sans bruit, filtre
« Normal ») :
- **coup joué** : `played_move` et `played_position_id` (position après le
coup, vue de celui qui l'a joué), `move_count` (coups légaux), `rank` (place
du coup joué dans le classement de l'analyse, à partir de 1 ; `imove` de
gnubg plus 1), `played_equity`, `best_move`, `best_equity`, `equity_loss`
(perte du coup joué, 0 ou négative : statistique « error-skill » de gnubg
changée de signe), `skill` (`none`, `doubtful`, `bad`, `very_bad`) et
`ranking` (les dix premiers coups, et jusqu'au coup joué) ; plus, une fois
pour toutes, `luck` : `value` (chance du jet) et `rating`
(`very_unlucky`, `unlucky`, `none`, `lucky`, `very_lucky`) ;
- **videau** : `analysed` (gnubg analyse-t-il cette décision ? pas à la
partie Crawford ni quand l'adversaire possède le videau), `cube` (la
décision de videau de gnubg au même niveau, format de la section
précédente, `null` si non analysée), `equity_loss` (perte de celui qui
décide, statistique « error-skill » changée de signe) et `skill` ;
- **niveaux** (`levels`, à la place de `plies`) : pour les niveaux
`beginner`, `casual`, `intermediate`, `advanced`, `0ply`, `1ply` et `2ply`
(3 et 4 plis sont trop lents pour la suite de tests), `plies` et `noise`
(réglages de gnubg), puis `move`, le coup que joue l'ordinateur de gnubg
réglé à ce niveau (commande « play »), ou la décision de videau de gnubg
avec ces réglages, bruit compris.
Deux précautions, faute de quoi gnubg donne des valeurs qui dépendent de ce
qu'il a calculé avant :
- **le cache d'évaluation est vidé avant chaque analyse**, et l'analyse du
videau est désactivée pour les coups joués. Une évaluation à 2 plis avec
élagage range dans le cache des évaluations à 0 pli faites avec les réseaux
d'élagage, sous la même clé que les évaluations à 0 pli ordinaires (la clé
d'`EvalKey`, dans `eval.c`, n'inclut l'élagage qu'à partir d'1 pli). Une
analyse à 0 pli, ou l'analyse de la chance qui suit l'analyse du videau à
2 plis, les reprend : écarts observés de 5e-3 à 2,5e-2. Le moteur embarqué
a le même cache ; `compare.py` l'interroge par un processus neuf à chaque
commande ;
- **le coup choisi pour un cas sans `played` vient de `findbestmove`** et non
de « hint », dont gnubg garde l'analyse dans l'enregistrement du coup :
« analyse game » la reprendrait au lieu de calculer au niveau demandé.
## Comparaison avec le moteur embarqué
`make test` compare le moteur embarqué à ces résultats
@@ -264,6 +329,22 @@ Pour chaque position et chaque niveau :
- même décision de videau (`decision` et `decision_text`), et les quatre
équités du videau à la tolérance près.
Pour chaque analyse (`engine-cli analysemove`, `analysecube`, `luck`, et
`--level NIVEAU bestmove` ou `cube`) :
- coup joué : même nombre de coups légaux, même notation du coup joué, même
rang sauf égalité (les coups entre les deux rangs ont pour gnubg la même
équité que le coup joué, à la tolérance près : remarque), même meilleur coup
sauf égalité, et `played_equity`, `best_equity`, `equity_loss` à la
tolérance près ;
- chance : `value` à la tolérance près ;
- videau : même `analysed`, même décision et équités à la tolérance près,
`equity_loss` à la tolérance près ;
- appréciation (`skill`) et chance (`rating`) identiques, sauf si la valeur
de gnubg est à moins de la tolérance d'un seuil (remarque) ;
- niveaux : mêmes réglages que gnubg (`engine-cli levels`), même coup choisi,
ou même décision de videau et équités à la tolérance près.
Le rapport donne, pour chaque écart hors critères, la position et le niveau,
le champ, la valeur attendue (gnubg), la valeur obtenue (moteur) et l'écart ;
il donne toujours l'écart maximal observé.
@@ -309,6 +390,11 @@ qu'affiche gnubg (trois décimales). Les vrais défauts (mauvais niveau,
mauvais contexte de videau, mauvaise conversion d'équité) donnent des écarts
d'un ou plusieurs centièmes.
Mesure du 2026-11-03, avec les 37 analyses (2 890 nombres comparés, natif et
WebAssembly) : écart maximal 1,64e-3, sur `move-jacoby` à 2 plis (équité du
coup joué `8/2 5/1*`, et donc sa perte), même phénomène de bascule à 2 plis ; tous les autres
nombres des analyses restent sous 1e-3. La tolérance n'a pas été changée.
Sur téléphone (NEON) les écarts seront à mesurer de la même façon : avec
`-ffast-math`, gnubg y remplace aussi la division par une approximation
(`vrecpeq_f32` et une itération de Newton), différente de celle de SSE2.
+47
View File
@@ -117,4 +117,51 @@ for f in results/*.json; do
printf '%s\n' "${ids[@]}" | grep -qxF "$id" || signaler "$f : position absente de positions.json"
done
# Analyses (README.md, section « Analyses ») : un résultat par cas, et les
# champs du format.
jq empty analyses.json || { echo "analyses.json : JSON invalide"; exit 1; }
read -r -d '' controle_analyse <<'JQ' || true
def nombre: type == "number";
def skill: IN("none", "doubtful", "bad", "very_bad");
if .type == "move" then
(if (.plies | keys) == ["0", "2"] and (.luck.value | nombre)
and (.luck.rating | IN("very_unlucky", "unlucky", "none", "lucky", "very_lucky"))
and all(.plies[]; (.move_count | nombre) and (.rank | nombre) and (.played_move | type) == "string"
and (.played_position_id | test("^[A-Za-z0-9+/]{14}$")) and (.played_equity | nombre)
and (.best_move | type) == "string" and (.best_equity | nombre)
and (.equity_loss | nombre) and .equity_loss <= 0 and (.skill | skill)
and (.ranking | length) >= ([.rank, ([.move_count, 10] | min)] | max))
then empty else "analyse de coup incomplète" end)
elif .type == "cube" then
(if (.plies | keys) == ["0", "2"] and (.action | IN("no_double", "double", "take", "pass"))
and all(.plies[]; (.analysed | type) == "boolean" and (.equity_loss | nombre)
and .equity_loss <= 0 and (.skill | skill)
and (if .analysed then (.cube.decision | type) == "string" else .cube == null end))
then empty else "analyse de videau incomplète" end)
elif .type == "level" then
(if (.levels | length) > 0 and all(.levels[]; (.plies | nombre) and (.noise | nombre)
and ((.move | type) == "string" or (.decision | type) == "string"))
then empty else "niveaux incomplets" end)
else "type inconnu" end
JQ
mapfile -t analyses < <(jq -r '(.moves + .cubes + .levels)[].id' analyses.json)
doublons="$(printf '%s\n' "${analyses[@]}" "${ids[@]}" | sort | uniq -d | paste -sd' ' -)"
[ -z "$doublons" ] || signaler "analyses.json : identifiants en double : $doublons"
for id in "${analyses[@]}"; do
f="analysis/$id.json"
if [ ! -f "$f" ]; then
signaler "$f absent : lancer make reference"
continue
fi
while read -r defaut; do
[ -n "$defaut" ] && signaler "$f : $defaut"
done < <(jq -r "$controle_analyse" "$f")
done
for f in analysis/*.json; do
[ -e "$f" ] || continue
printf '%s\n' "${analyses[@]}" | grep -qxF "$(basename "$f" .json)" \
|| signaler "$f : cas absent d'analyses.json"
done
[ "$erreurs" -eq 0 ]