Étape 0a.3 : moteur embarqué (cœur de gnubg), engine.h et engine-cli

Fusion du travail du sous-agent (branche moteur-embarque-travail) ; plan
supprimé à la clôture, son résultat est repris dans la merge request.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Pierre MARTIN
2026-11-02 14:02:56 +01:00
co-authored by Claude Opus 5.5
68 changed files with 131963 additions and 15 deletions
+5
View File
@@ -29,6 +29,11 @@ for f in "${fichiers[@]}"; do
if grep -q $'\r' "$f"; then
signaler "$f : fins de ligne Windows (CRLF)"
fi
# Copies fidèles de GNU Backgammon (sources et données) : on ne
# reformate pas leurs espaces, pour rester identique à l'original.
case "$f" in
engine/gnubg/* | engine/data/*) continue ;;
esac
if grep -nE '[[:blank:]]+$' "$f" >/dev/null; then
signaler "$f : espaces en fin de ligne (lignes $(grep -nE '[[:blank:]]+$' "$f" | cut -d: -f1 | paste -sd, -))"
fi
+144
View File
@@ -0,0 +1,144 @@
#!/usr/bin/env bash
# Tests du moteur embarqué, via build/engine-cli (étape 0a.3) :
# meilleurs coups d'ouverture connus, nombre de coups légaux, bornes et
# symétrie des probabilités, décision de videau évidente, identifiants
# invalides. La comparaison systématique au gnubg de bureau viendra ensuite
# (tests/reference/, étape 0a.4).
#
# Lance `make engine` (sans effet si tout est à jour).
set -euo pipefail
cd "$(dirname "$0")/.."
# Lancé depuis `make test` : repartir d'un make indépendant du make parent.
unset MAKEFLAGS MFLAGS MAKELEVEL
make engine >/dev/null
cli=build/engine-cli
depart=4HPwATDgc/ABMA
# Match ID de partie libre, videau au centre, joueur 0 au trait.
libre_j0=MAEAAAAAAAAA
# Le même, joueur 1 au trait.
libre_j1=cAkAAAAAAAAA
# Match en 5 points, 4-2, partie Crawford, joueur 0 au trait.
crawford=sAGgAEAAEAAA
# Course : le joueur au trait n'a plus que deux dames, sur ses points 6 et 5 ;
# l'adversaire a ses 15 dames sur son point 1.
course=/38AAABQAAAAAA
echecs=0
echouer() {
echo " $1"
echecs=$((echecs + 1))
}
# Valeur d'une clé JSON numérique ou textuelle (première occurrence).
valeur() {
grep -oE "\"$1\": (\"[^\"]*\"|[-0-9.]+)" <<<"$2" | head -n1 | sed -E 's/^"[^"]*": "?//; s/"$//'
}
# Évalue une condition numérique avec awk ; ex. : vrai "0 <= 0.5 && 0.5 <= 1"
vrai() {
awk "BEGIN { exit !($1) }"
}
# 1. Meilleurs coups d'ouverture, sans ambiguïté, à 0 et 2 plis.
for ply in 0 2; do
while read -r des attendu; do
sortie="$($cli --ply "$ply" bestmove "$depart" "$libre_j0" "$des")"
coup="$(valeur move "$sortie")"
[ "$coup" = "$attendu" ] \
|| echouer "ouverture $des à $ply pli(s) : « $coup » au lieu de « $attendu »"
done <<-EOF
31 8/5 6/5
42 8/4 6/4
53 8/3 6/3
61 13/7 8/7
EOF
done
# Le Match ID du critère de réussite du plan (joueur 1 au trait) donne le même coup.
coup="$(valeur move "$($cli bestmove "$depart" "$libre_j1" 31)")"
[ "$coup" = "8/5 6/5" ] || echouer "ouverture 31 avec $libre_j1 : « $coup »"
# 2. Nombre de coups légaux. Course, jet 21 : 6/3, 5/2 ou 6/4 5/4.
n="$(valeur count "$($cli legal "$course" "$libre_j0" 21)")"
[ "$n" = 3 ] || echouer "course, jet 21 : $n coups légaux au lieu de 3"
# 3. Probabilités : bornes, cohérence et symétrie.
verifier_probabilites() {
local nom="$1" json="$2" g gg gbg pg pbg
g="$(valeur win "$json")"
gg="$(valeur win_gammon "$json")"
gbg="$(valeur win_backgammon "$json")"
pg="$(valeur lose_gammon "$json")"
pbg="$(valeur lose_backgammon "$json")"
vrai "0 <= $g && $g <= 1" || echouer "$nom : gain $g hors de [0, 1]"
vrai "0 <= $gbg && $gbg <= $gg && $gg <= $g" \
|| echouer "$nom : gain $g, gammon $gg, backgammon $gbg incohérents"
vrai "0 <= $pbg && $pbg <= $pg && $pg <= 1 - $g" \
|| echouer "$nom : gain $g, perte gammon $pg, perte backgammon $pbg incohérents"
}
for ply in 0 2; do
j0="$($cli --ply "$ply" eval "$depart" "$libre_j0")"
j1="$($cli --ply "$ply" eval "$depart" "$libre_j1")"
verifier_probabilites "départ à $ply pli(s)" "$j0"
# Le départ est symétrique : peu importe quel joueur est au trait.
[ "${j0#*\"probabilities\"}" = "${j1#*\"probabilities\"}" ] \
|| echouer "départ à $ply pli(s) : évaluations différentes selon le joueur au trait"
# Avantage au joueur qui lance les dés en premier, mais faible.
e="$(valeur cubeless_equity "$j0")"
vrai "0 < $e && $e < 0.2" || echouer "départ à $ply pli(s) : équité $e hors de ]0, 0.2["
done
course_eval="$($cli eval "$course" "$libre_j0")"
verifier_probabilites "course" "$course_eval"
g="$(valeur win "$course_eval")"
vrai "$g > 0.999" || echouer "course : gain $g, attendu quasi certain"
# Les coups classés ont aussi des probabilités cohérentes.
verifier_probabilites "coup 64 à 2 plis" "$($cli --ply 2 bestmove "$depart" "$libre_j0" 64)"
# 4. Videau : au départ, pas de double, en partie libre comme en match.
for match_id in "$libre_j0" "$crawford"; do
decision="$(valeur decision "$($cli cube "$depart" "$match_id")")"
case "$decision" in
no_double* | no_redouble* | not_available) ;;
*) echouer "videau au départ ($match_id) : « $decision » au lieu de pas de double" ;;
esac
done
# Partie Crawford : le videau n'est pas disponible.
decision="$(valeur decision "$($cli cube "$depart" "$crawford")")"
[ "$decision" = "no_double_dead_cube" ] || [ "$decision" = "not_available" ] \
|| echouer "videau en partie Crawford : « $decision »"
# À l'inverse, la course gagnée d'avance est un double/passe (règle Jacoby :
# jouer pour le gammon ne rapporte rien, videau au centre).
decision="$(valeur decision "$($cli cube "$course" "$libre_j0")")"
[ "$decision" = "double_pass" ] || echouer "videau en course gagnée : « $decision »"
# 5. Erreurs : JSON, code d'erreur précis et code de sortie non nul.
attendre_erreur() {
local code_attendu="$1"
shift
local sortie statut=0
sortie="$($cli "$@")" || statut=$?
[ "$statut" -ne 0 ] || echouer "« $* » aurait dû échouer"
grep -qF "\"code\": \"$code_attendu\"" <<<"$sortie" \
|| echouer "« $* » : code d'erreur attendu $code_attendu, reçu : $sortie"
}
attendre_erreur invalid_position_id bestmove 'ceci-nest-pas-un-id' "$libre_j0" 31
attendre_erreur invalid_position_id bestmove '//////////////' "$libre_j0" 31
attendre_erreur invalid_match_id bestmove "$depart" 'XYZ' 31
attendre_erreur invalid_dice bestmove "$depart" "$libre_j0" 71
attendre_erreur invalid_ply --ply 9 bestmove "$depart" "$libre_j0" 31
attendre_erreur usage bestmove "$depart" "$libre_j0"
attendre_erreur usage inconnue "$depart" "$libre_j0"
attendre_erreur invalid_data --data /dossier/absent bestmove "$depart" "$libre_j0" 31
if [ "$echecs" -gt 0 ]; then
echo " $echecs vérification(s) du moteur en échec."
exit 1
fi
+9 -4
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
# Vérifie le contrat du Makefile décrit dans CLAUDE.md :
# toutes les commandes obligatoires existent et apparaissent dans `make help`,
# et `make clean` peut être relancé sans erreur.
# et `make clean` supprime le dossier de build et peut être relancé sans erreur.
set -euo pipefail
cd "$(dirname "$0")/.."
@@ -17,6 +17,11 @@ for cible in setup build test lint deploy clean help; do
fi
done
make clean >/dev/null
make clean >/dev/null
[ ! -e build ] || { echo "make clean n'a pas supprimé build/"; exit 1; }
# make clean est essayé sur un dossier de build jetable : vider le vrai build/
# forcerait à recompiler le moteur et à régénérer ses données à chaque test.
faux_build="$(mktemp -d)"
touch "$faux_build/artefact"
make clean BUILD_DIR="$faux_build" >/dev/null
make clean BUILD_DIR="$faux_build" >/dev/null
[ ! -e "$faux_build" ] || { echo "make clean n'a pas supprimé le dossier de build"; exit 1; }
grep -qx 'BUILD_DIR := build' Makefile || { echo "BUILD_DIR ne vaut plus build"; exit 1; }