diff --git a/tests/reference/README.md b/tests/reference/README.md index 5676d31..31b7f88 100644 --- a/tests/reference/README.md +++ b/tests/reference/README.md @@ -11,14 +11,16 @@ d'un fichier de résultat. | --- | --- | | `positions.json` | Positions d'entrée, versionnées, écrites à la main | | `results/.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/.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/.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. diff --git a/tests/test_reference.sh b/tests/test_reference.sh index dd5bfed..12bb5db 100644 --- a/tests/test_reference.sh +++ b/tests/test_reference.sh @@ -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 ]