Ce dont le domaine d'une partie a besoin, sans réimplémenter de règle : décodage d'un Position ID en plateau et position vue de l'adversaire, résultat d'une partie finie (gammon, règle Jacoby), codage d'un Match ID, vérification de demi-coups saisis un par un (la légalité vient de GenerateMoves de gnubg), demi-coups de chaque coup légal, droit de doubler et recommandations de double et de prise. Nouvelles commandes d'engine-cli position, halfmoves et matchid, avec leurs tests natifs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
226 lines
10 KiB
Bash
226 lines
10 KiB
Bash
#!/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 est faite par
|
|
# tests/test_engine_reference.sh.
|
|
#
|
|
# 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.eE+]+)" <<<"$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
|
|
NODOUBLE_* | 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" = "NODOUBLE_DEADCUBE" ] || [ "$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
|
|
|
|
# 6. Ce dont a besoin le domaine d'une partie (étape 0d.1).
|
|
|
|
# Les coups légaux donnent leurs demi-coups, dans un ordre jouable.
|
|
grep -qF '"move": "8/5 6/5", "position_id": "4HPwATCwZ/ABMA", "half_moves": [[8, 5], [6, 5]]' \
|
|
<<<"$($cli legal "$depart" "$libre_j0" 31)" || echouer "legal : demi-coups de 8/5 6/5 absents"
|
|
|
|
# Match ID : codage, et décodage identique.
|
|
[ "$(valeur match_id "$($cli matchid 1 -1)")" = "$libre_j0" ] || echouer "matchid 1 -1 : $libre_j0 attendu"
|
|
videau4="$(valeur match_id "$($cli matchid 4 1)")"
|
|
sortie="$($cli eval "$depart" "$videau4")"
|
|
[ "$(valeur cube_value "$sortie")" = 4 ] && [ "$(valeur cube_owner "$sortie")" = 1 ] \
|
|
|| echouer "matchid 4 1 : videau mal codé ($sortie)"
|
|
attendre_erreur invalid_match_id matchid 3 0
|
|
attendre_erreur invalid_match_id matchid 2 5
|
|
|
|
# Plateau : position de départ, symétrique, sans résultat.
|
|
sortie="$($cli position "$depart" "$libre_j0")"
|
|
grep -qF '"on_roll": {"points": [0, 0, 0, 0, 0, 5, 0, 3, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2], "bar": 0, "off": 0, "pip_count": 167}' <<<"$sortie" \
|
|
|| echouer "position de départ mal décodée : $sortie"
|
|
[ "$(valeur swapped_position_id "$sortie")" = "$depart" ] || echouer "départ : la position vue de l'adversaire diffère"
|
|
grep -qF '"result": null' <<<"$sortie" || echouer "départ : la partie ne devrait pas être finie"
|
|
attendre_erreur invalid_position_id position 'ceci-nest-pas-un-id' "$libre_j0"
|
|
|
|
# Fin de partie : le joueur au trait a tout sorti, l'adversaire aucune dame
|
|
# (gammon). Règle Jacoby : 1 point tant que le videau est au centre ; sinon
|
|
# 2 fois le videau.
|
|
fini=/38AAAAAAAAAAA
|
|
grep -qF '"result": {"winner": "on_roll", "kind": "gammon", "points": 1}' \
|
|
<<<"$($cli position "$fini" "$libre_j0")" || echouer "gammon avec Jacoby, videau au centre : 1 point attendu"
|
|
grep -qF '"result": {"winner": "on_roll", "kind": "gammon", "points": 8}' \
|
|
<<<"$($cli position "$fini" "$videau4")" || echouer "gammon, videau à 4 : 8 points attendus"
|
|
vu_du_perdant="$(valeur swapped_position_id "$($cli position "$fini" "$libre_j0")")"
|
|
grep -qF '"winner": "opponent"' <<<"$($cli position "$vu_du_perdant" "$libre_j0")" \
|
|
|| echouer "fin de partie vue du perdant : l'adversaire devrait gagner"
|
|
|
|
# Demi-coups : « valid complete position_id » pour une saisie.
|
|
demi_coups() {
|
|
local sortie valid complete
|
|
sortie="$($cli halfmoves "$@")"
|
|
valid="$(grep -oE '"valid": (true|false)' <<<"$sortie" | cut -d' ' -f2)"
|
|
complete="$(grep -oE '"complete": (true|false)' <<<"$sortie" | cut -d' ' -f2)"
|
|
echo "$valid $complete $(valeur result_position_id "$sortie")"
|
|
}
|
|
attendre_demi_coups() {
|
|
local attendu="$1"
|
|
shift
|
|
local obtenu
|
|
obtenu="$(demi_coups "$@")"
|
|
[ "$obtenu" = "$attendu" ] || echouer "halfmoves $* : « $obtenu » au lieu de « $attendu »"
|
|
}
|
|
attendre_demi_coups "true false 4HPwATDQZ/ABMA" "$depart" 31 "8/5"
|
|
attendre_demi_coups "true true 4HPwATCwZ/ABMA" "$depart" 31 "8/5 6/5"
|
|
attendre_demi_coups "true true 4HPwATCwZ/ABMA" "$depart" 31 "6/5 8/5"
|
|
attendre_demi_coups "true false $depart" "$depart" 31
|
|
# Case 12 tenue par l'adversaire ; 8/4 demande deux dés.
|
|
attendre_demi_coups "false false " "$depart" 31 "13/12"
|
|
attendre_demi_coups "false false " "$depart" 31 "8/4"
|
|
# Le même pion avec les deux dés.
|
|
attendre_demi_coups "true true 4HPwATDgc/ABIQ" "$depart" 31 "24/21 21/20"
|
|
# Sortie : la dame la plus éloignée d'abord avec un dé trop fort.
|
|
attendre_demi_coups "true true /38AAAAAAAAAAA" "$course" 66 "6/off 5/off"
|
|
attendre_demi_coups "false false " "$course" 66 "5/off"
|
|
attendre_demi_coups "true false /38AAAAgAAAAAA" "$course" 65 "5/off"
|
|
# Course, jet 21 : il faut jouer les deux dés.
|
|
attendre_demi_coups "false false " "$course" 21 "6/3"
|
|
attendre_demi_coups "true true /38AAAAkAAAAAA" "$course" 21 "6/5 5/3"
|
|
# Aucun coup possible : rien n'est valide.
|
|
attendre_demi_coups "false false " 27YBBwAcz8EDQA 66
|
|
attendre_erreur usage halfmoves "$depart" 31 "8-5"
|
|
attendre_erreur invalid_dice halfmoves "$depart" 71 "8/5"
|
|
|
|
# Videau : droit de doubler et recommandations.
|
|
sortie="$($cli cube "$depart" "$libre_j0")"
|
|
grep -qF '"available": true, "double": false, "take": true' <<<"$sortie" \
|
|
|| echouer "videau au départ : disponible, pas de double, prendre attendus"
|
|
grep -qF '"available": true, "double": true, "take": false' <<<"$($cli cube "$course" "$libre_j0")" \
|
|
|| echouer "videau en course gagnée : double et passe attendus"
|
|
# Videau chez l'adversaire : le joueur au trait ne peut pas doubler.
|
|
grep -qF '"available": false, "double": false' <<<"$($cli cube "$course" "$(valeur match_id "$($cli matchid 2 1)")")" \
|
|
|| echouer "videau chez l'adversaire : double indisponible attendu"
|
|
|
|
if [ "$echecs" -gt 0 ]; then
|
|
echo " $echecs vérification(s) du moteur en échec."
|
|
exit 1
|
|
fi
|