#!/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, niveaux de jeu (bruit reproductible), analyses et chance. 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") $(valeur cube_owner "$sortie")" = "4 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 aucun pion # (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 : le pion le plus éloigné 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" # Niveaux de jeu : les neuf réglages de gnubg, seuils exposés. sortie="$($cli levels)" [ "$(grep -o '"name"' <<<"$sortie" | wc -l)" = 9 ] || echouer "levels : 9 niveaux attendus" grep -qF '"name": "beginner", "plies": 0, "noise": 0.0599999987' <<<"$sortie" \ || echouer "levels : réglage débutant inattendu" grep -qF '"skill_thresholds": {"doubtful": 0.0299999993, "bad": 0.0599999987, "very_bad": 0.119999997}' \ <<<"$sortie" || echouer "levels : seuils d'appréciation inattendus" # Le bruit est déterministe : le même coup à chaque appel, et le bruit change # le coup de cette position (vérifié contre gnubg par analysis/level-*.json). milieu=mM/gATCw5+ABMA milieu_mid=cAkTAAAAAAAA for essai in 1 2 3; do coup="$(valeur move "$($cli --level beginner bestmove "$milieu" "$milieu_mid" 64)")" [ "$coup" = "24/20 24/18" ] || echouer "niveau débutant, essai $essai : « $coup »" done coup="$(valeur move "$($cli --level 0ply bestmove "$milieu" "$milieu_mid" 64)")" [ "$coup" = "24/14" ] || echouer "niveau 0 pli : « $coup » au lieu de 24/14" grep -qF '"level": "advanced"' <<<"$($cli --level advanced cube "$depart" "$libre_j0")" \ || echouer "cube au niveau avancé : niveau absent de la réponse" attendre_erreur usage --level champion bestmove "$depart" "$libre_j0" 31 attendre_erreur usage --level beginner --ply 2 bestmove "$depart" "$libre_j0" 31 attendre_erreur usage --level beginner eval "$depart" "$libre_j0" # Analyse d'un coup : le meilleur coup n'est pas une erreur ; un coup illégal # est refusé ; sans coup possible, rien à analyser. sortie="$($cli analysemove "$depart" "$libre_j0" 31 4HPwATCwZ/ABMA)" [ "$(valeur rank "$sortie") $(valeur skill "$sortie")" = "1 none" ] \ || echouer "analysemove 8/5 6/5 : rang 1 sans erreur attendus ($sortie)" attendre_erreur invalid_move analysemove "$depart" "$libre_j0" 31 "$depart" grep -qF '"move_count": 0, "rank": null' <<<"$($cli analysemove 27YBBwAcz8EDQA cIkWAAAAAAAA 55 27YBBwAcz8EDQA)" \ || echouer "analysemove sans coup possible : move_count 0 attendu" # Analyse du videau : décision inconnue refusée ; pas d'analyse quand le # joueur au trait ne peut pas doubler. attendre_erreur usage analysecube "$depart" "$libre_j0" redouble grep -qF '"analysed": false, "cube": null, "equity_loss": 0, "skill": "none"' \ <<<"$($cli analysecube "$course" "$(valeur match_id "$($cli matchid 2 1)")" no_double)" \ || echouer "analysecube, videau chez l'adversaire : pas d'analyse attendue" # Chance : même jet, même chance ; ordre des dés indifférent. [ "$($cli luck "$milieu" "$milieu_mid" 64 | sed 's/"dice": \[[0-9], [0-9]\]//')" \ = "$($cli luck "$milieu" "$milieu_mid" 46 | sed 's/"dice": \[[0-9], [0-9]\]//')" ] \ || echouer "luck : l'ordre des dés change la chance" if [ "$echecs" -gt 0 ]; then echo " $echecs vérification(s) du moteur en échec." exit 1 fi