diff --git a/tests/test_engine.sh b/tests/test_engine.sh new file mode 100644 index 0000000..f5f9294 --- /dev/null +++ b/tests/test_engine.sh @@ -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 diff --git a/tests/test_makefile.sh b/tests/test_makefile.sh index 7451454..494593b 100755 --- a/tests/test_makefile.sh +++ b/tests/test_makefile.sh @@ -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; }