#!/usr/bin/env bash # Vérifie la cohérence des positions de référence (tests/reference/) : # JSON valide, au moins 30 positions, chaque catégorie présente, un résultat # par position aux niveaux 0 et 2 plis avec tous les champs du format # (voir tests/reference/README.md). Ne lance pas gnubg : jq tourne dans # l'image de la chaîne C (ADR 0004), dans laquelle le script se relance. set -euo pipefail cd "$(dirname "$0")/.." if [ -z "${USTAGAMMON_IN_DOCKER:-}" ]; then image="${BUILD_C_IMAGE:?lancer via make test}" docker image inspect "$image" >/dev/null 2>&1 \ || { echo "Chaîne C absente : lancer d'abord make setup"; exit 1; } exec docker run --rm --user "$(id -u):$(id -g)" -e USTAGAMMON_IN_DOCKER=1 \ -v "$PWD:/src:ro" -w /src "$image" bash tests/test_reference.sh fi cd tests/reference erreurs=0 signaler() { echo "$1" erreurs=$((erreurs + 1)) } jq empty positions.json || { echo "positions.json : JSON invalide"; exit 1; } nombre="$(jq '.positions | length' positions.json)" [ "$nombre" -ge 30 ] || signaler "positions.json : $nombre positions, 30 au minimum" doublons="$(jq -r '[.positions[].id] | group_by(.) | map(select(length > 1)[0]) | join(" ")' positions.json)" [ -z "$doublons" ] || signaler "positions.json : identifiants en double : $doublons" for categorie in opening middlegame prime blitz race bearoff \ cube_no_double cube_double_take cube_double_pass match crawford jacoby; do jq -e --arg c "$categorie" 'any(.positions[]; .categories | index($c))' positions.json >/dev/null \ || signaler "positions.json : aucune position de catégorie « $categorie »" done # Champs obligatoires de chaque position d'entrée. invalides="$(jq -r '.positions[] | select((.id | type) != "string" or (.type | IN("move", "cube") | not) or (.position_id | test("^[A-Za-z0-9+/]{14}$") | not) or (.match_id | test("^[A-Za-z0-9+/]{12}$") | not) or (.categories | type) != "array" or (.description | type) != "string" or (if .type == "move" then (.dice | type) != "array" else .dice != null end)) | "positions.json : position \(.id) incomplète ou invalide"' positions.json)" [ -z "$invalides" ] || signaler "$invalides" # Filtre jq : liste les défauts d'un fichier de résultat (vide si conforme). read -r -d '' controle <<'JQ' || true def nombre: type == "number"; def probas: (keys == ["lose_backgammon", "lose_gammon", "win", "win_backgammon", "win_gammon"]) and all(.[]; nombre and . >= 0 and . <= 1); def defauts($pos): (if .id != $pos.id or .type != $pos.type or .position_id != $pos.position_id or .match_id != $pos.match_id or .categories != $pos.categories then "en-tête différent de positions.json" else empty end), (if (.gnubg_commit | test("^[0-9a-f]{40}$")) and .met == "Kazaross-XG2" then empty else "gnubg_commit ou met invalide" end), (.context | if (.dice == $pos.dice) and (.match_length | nombre) and (.score_player | nombre) and (.score_opponent | nombre) and (.cube_value | nombre) and (.cube_owner | IN("centered", "player", "opponent")) and ([.crawford, .jacoby, .beavers] | all(type == "boolean")) then empty else "context incomplet" end), (if (.plies | keys) == ["0", "2"] then empty else "niveaux attendus : 0 et 2" end), (.plies | to_entries[] | .key as $n | .value | (if (.evaluation.probabilities | probas) and (.evaluation.equity | nombre) then empty else "niveau \($n) : evaluation incomplète" end), (if $pos.type == "move" then (.moves | if (.legal_move_count | nombre) and (.ranking | length) == ([.legal_move_count, 10] | min) and all(.ranking[]; (.rank | nombre) and (.move | type) == "string" and (.ply | nombre) and (.equity | nombre) and (.equity_diff | nombre) and (.probabilities | probas)) then empty else "niveau \($n) : classement des coups incomplet" end) else (.cube | if ([.no_double, .double_take, .double_pass, .optimal] | all(nombre)) and (.decision | type) == "string" and (.decision_text | type) == "string" then empty else "niveau \($n) : décision de videau incomplète" end) end)); defauts($pos) JQ # Décision de videau attendue à 2 plis selon la catégorie. read -r -d '' decision <<'JQ' || true .categories as $c | .plies["2"].cube.decision as $d | if ($c | index("cube_no_double")) and ($d | test("^NO_?(RE)?DOUBLE_") | not) then "pas de double attendu, gnubg dit \($d)" elif ($c | index("cube_double_take")) and ($d | IN("DOUBLE_TAKE", "REDOUBLE_TAKE") | not) then "double/prend attendu, gnubg dit \($d)" elif ($c | index("cube_double_pass")) and ($d | IN("DOUBLE_PASS", "REDOUBLE_PASS") | not) then "double/passe attendu, gnubg dit \($d)" else empty end JQ mapfile -t ids < <(jq -r '.positions[].id' positions.json) for id in "${ids[@]}"; do f="results/$id.json" if [ ! -f "$f" ]; then signaler "$f absent : lancer make reference" continue fi if ! jq empty "$f" 2>/dev/null; then signaler "$f : JSON invalide" continue fi pos="$(jq --arg id "$id" '.positions[] | select(.id == $id)' positions.json)" while read -r defaut; do [ -n "$defaut" ] && signaler "$f : $defaut" done < <(jq -r --argjson pos "$pos" "$controle" "$f"; jq -r "$decision" "$f") done for f in results/*.json; do [ -e "$f" ] || continue id="$(basename "$f" .json)" 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 ]