Moteur : classement exact de hint et commande engine-cli reference
- engine_best_moves suit hint_move de gnubg : fAnalyse vrai, seuil « douteux », puis RefreshMoveList (classement par niveau d'évaluation et départage des égalités). - Correction : en match, l'équité des coups était convertie deux fois par mwc2eq (ScoreMove la convertit déjà). - engine_cube : ajout de optimal et decision_text ; decision prend le nom de l'énumération cubedecision de gnubg, comme les références. - engine-cli : commande reference (un niveau au format de tests/reference/) et nombres écrits avec 9 chiffres significatifs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
856e4d64bd
commit
be6a4b4446
+98
-8
@@ -13,6 +13,11 @@
|
||||
* legal POSITION_ID MATCH_ID DÉS coups légaux, sans évaluation
|
||||
* cube POSITION_ID MATCH_ID décision de videau
|
||||
* eval POSITION_ID MATCH_ID évaluation de la position
|
||||
* reference POSITION_ID MATCH_ID [DÉS]
|
||||
* un niveau au format des positions de
|
||||
* référence (tests/reference/README.md) :
|
||||
* évaluation, puis classement des coups
|
||||
* avec DÉS, décision de videau sans
|
||||
*
|
||||
* --ply : niveau de gnubg, de 0 (défaut) à 4.
|
||||
* --data : dossier des données ; par défaut « data » à côté du programme.
|
||||
@@ -31,6 +36,13 @@
|
||||
#define EXIT_ENGINE_ERROR 1
|
||||
#define EXIT_USAGE_ERROR 2
|
||||
|
||||
/* Format des nombres : 9 chiffres significatifs redonnent exactement le
|
||||
* float32 calculé par gnubg. */
|
||||
#define NUMBER "%.9g"
|
||||
|
||||
/* Coups gardés dans le classement de la commande reference (comme hint). */
|
||||
#define REFERENCE_MAX_MOVES 10
|
||||
|
||||
/* Arguments de la ligne de commande, une fois lus. */
|
||||
typedef struct {
|
||||
int ply;
|
||||
@@ -90,8 +102,8 @@ usage_error(const char *message)
|
||||
static void
|
||||
print_probabilities(const float p[ENGINE_NUM_PROBABILITIES])
|
||||
{
|
||||
printf("{\"win\": %.6f, \"win_gammon\": %.6f, \"win_backgammon\": %.6f, "
|
||||
"\"lose_gammon\": %.6f, \"lose_backgammon\": %.6f}",
|
||||
printf("{\"win\": " NUMBER ", \"win_gammon\": " NUMBER ", \"win_backgammon\": " NUMBER ", "
|
||||
"\"lose_gammon\": " NUMBER ", \"lose_backgammon\": " NUMBER "}",
|
||||
p[ENGINE_WIN], p[ENGINE_WIN_GAMMON], p[ENGINE_WIN_BACKGAMMON],
|
||||
p[ENGINE_LOSE_GAMMON], p[ENGINE_LOSE_BACKGAMMON]);
|
||||
}
|
||||
@@ -101,7 +113,7 @@ print_move(const engine_move *m, int with_evaluation)
|
||||
{
|
||||
printf("{\"move\": \"%s\", \"position_id\": \"%s\"", m->notation, m->position_id);
|
||||
if (with_evaluation) {
|
||||
printf(", \"ply\": %d, \"equity\": %.6f, \"probabilities\": ", m->ply, m->equity);
|
||||
printf(", \"ply\": %d, \"equity\": " NUMBER ", \"probabilities\": ", m->ply, m->equity);
|
||||
print_probabilities(m->probabilities);
|
||||
}
|
||||
printf("}");
|
||||
@@ -116,6 +128,25 @@ print_match(const engine_match *m)
|
||||
m->player_on_roll, m->crawford ? "true" : "false", m->jacoby ? "true" : "false");
|
||||
}
|
||||
|
||||
/* Champs d'une décision de videau, sans les accolades. */
|
||||
static void
|
||||
print_cube_fields(const engine_cube *c)
|
||||
{
|
||||
printf("\"no_double\": " NUMBER ", \"double_take\": " NUMBER ", \"double_pass\": " NUMBER
|
||||
", \"optimal\": " NUMBER ", \"decision\": \"%s\", \"decision_text\": \"%s\"",
|
||||
c->no_double, c->double_take, c->double_pass, c->optimal, c->decision,
|
||||
c->decision_text);
|
||||
}
|
||||
|
||||
/* Début d'un niveau de référence : {"evaluation": {...} (objet laissé ouvert). */
|
||||
static void
|
||||
print_reference_evaluation(const engine_evaluation *e)
|
||||
{
|
||||
printf("{\"evaluation\": {\"probabilities\": ");
|
||||
print_probabilities(e->probabilities);
|
||||
printf(", \"equity\": " NUMBER "}", e->cubeless_equity);
|
||||
}
|
||||
|
||||
/* Lit des dés au format « 31 » ; renvoie 0 si le format est faux. */
|
||||
static int
|
||||
parse_dice(const char *text, int *die1, int *die2)
|
||||
@@ -204,7 +235,7 @@ run_position_command(const options *opt)
|
||||
print_match(&match);
|
||||
printf(", \"probabilities\": ");
|
||||
print_probabilities(e.probabilities);
|
||||
printf(", \"cubeless_equity\": %.6f, \"cubeful_equity\": %.6f}\n",
|
||||
printf(", \"cubeless_equity\": " NUMBER ", \"cubeful_equity\": " NUMBER "}\n",
|
||||
e.cubeless_equity, e.cubeful_equity);
|
||||
} else {
|
||||
engine_cube c;
|
||||
@@ -216,8 +247,65 @@ run_position_command(const options *opt)
|
||||
print_match(&match);
|
||||
printf(", \"probabilities\": ");
|
||||
print_probabilities(c.probabilities);
|
||||
printf(", \"no_double\": %.6f, \"double_take\": %.6f, \"double_pass\": %.6f, "
|
||||
"\"decision\": \"%s\"}\n", c.no_double, c.double_take, c.double_pass, c.decision);
|
||||
printf(", ");
|
||||
print_cube_fields(&c);
|
||||
printf("}\n");
|
||||
}
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
/*
|
||||
* reference : un niveau d'un fichier de résultat de tests/reference/, de
|
||||
* même structure que plies["<n>"] (voir tests/reference/README.md).
|
||||
*/
|
||||
static int
|
||||
run_reference_command(const options *opt)
|
||||
{
|
||||
const char *position_id = opt->args[0], *match_id = opt->args[1];
|
||||
engine_evaluation e;
|
||||
engine_status status;
|
||||
int die1, die2, count, i;
|
||||
|
||||
if (opt->arg_count != 2 && opt->arg_count != 3)
|
||||
return usage_error("arguments attendus : POSITION_ID MATCH_ID [DÉS]");
|
||||
if (opt->arg_count == 3 && !parse_dice(opt->args[2], &die1, &die2))
|
||||
return usage_error("dés attendus sous la forme 31");
|
||||
|
||||
status = engine_evaluate(position_id, match_id, opt->ply, &e);
|
||||
if (status != ENGINE_OK)
|
||||
return print_engine_error(status);
|
||||
|
||||
if (opt->arg_count == 3) {
|
||||
engine_move *moves = malloc(sizeof *moves * ENGINE_MAX_MOVES);
|
||||
if (!moves)
|
||||
return print_error("out_of_memory", "mémoire insuffisante", EXIT_ENGINE_ERROR);
|
||||
status = engine_best_moves(position_id, match_id, die1, die2, opt->ply,
|
||||
moves, ENGINE_MAX_MOVES, &count);
|
||||
if (status != ENGINE_OK) {
|
||||
free(moves);
|
||||
return print_engine_error(status);
|
||||
}
|
||||
print_reference_evaluation(&e);
|
||||
printf(", \"moves\": {\"legal_move_count\": %d, \"ranking\": [", count);
|
||||
for (i = 0; i < count && i < REFERENCE_MAX_MOVES; i++) {
|
||||
printf("%s{\"rank\": %d, \"move\": \"%s\", \"ply\": %d, \"equity\": " NUMBER
|
||||
", \"equity_diff\": " NUMBER ", \"probabilities\": ",
|
||||
i ? ", " : "", i + 1, moves[i].notation, moves[i].ply, moves[i].equity,
|
||||
moves[i].equity - moves[0].equity);
|
||||
print_probabilities(moves[i].probabilities);
|
||||
printf("}");
|
||||
}
|
||||
printf("]}}\n");
|
||||
free(moves);
|
||||
} else {
|
||||
engine_cube c;
|
||||
status = engine_cube_decision(position_id, match_id, opt->ply, &c);
|
||||
if (status != ENGINE_OK)
|
||||
return print_engine_error(status);
|
||||
print_reference_evaluation(&e);
|
||||
printf(", \"cube\": {");
|
||||
print_cube_fields(&c);
|
||||
printf("}}\n");
|
||||
}
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
@@ -266,7 +354,7 @@ parse_options(int argc, char **argv, options *opt)
|
||||
}
|
||||
}
|
||||
if (!opt->command)
|
||||
return "commande attendue : bestmove, moves, legal, cube ou eval";
|
||||
return "commande attendue : bestmove, moves, legal, cube, eval ou reference";
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -298,8 +386,10 @@ main(int argc, char **argv)
|
||||
result = run_moves_command(&opt);
|
||||
else if (strcmp(opt.command, "eval") == 0 || strcmp(opt.command, "cube") == 0)
|
||||
result = run_position_command(&opt);
|
||||
else if (strcmp(opt.command, "reference") == 0)
|
||||
result = run_reference_command(&opt);
|
||||
else
|
||||
result = usage_error("commande inconnue : bestmove, moves, legal, cube ou eval");
|
||||
result = usage_error("commande inconnue : bestmove, moves, legal, cube, eval ou reference");
|
||||
|
||||
engine_shutdown();
|
||||
return result;
|
||||
|
||||
+26
-12
@@ -25,6 +25,10 @@
|
||||
/* Nombre de castors autorisés en partie libre : valeur par défaut de gnubg. */
|
||||
#define DEFAULT_BEAVERS 3
|
||||
|
||||
/* Seuil d'erreur « douteuse » de gnubg (arSkillLevel[SKILL_DOUBTFUL] dans
|
||||
* gnubg.c), que la commande « hint » passe à FindnSaveBestMoves. */
|
||||
#define DOUBTFUL_THRESHOLD 0.03f
|
||||
|
||||
/* Fichier de table d'équité de match : celui de gnubg de bureau par défaut. */
|
||||
#define MET_FILE "Kazaross-XG2.xml"
|
||||
|
||||
@@ -236,7 +240,8 @@ fill_move(engine_move *out, const move *m, const position *pos, int evaluated)
|
||||
out->ply = evaluated ? (int) m->esMove.ec.nPlies : -1;
|
||||
for (i = 0; i < ENGINE_NUM_PROBABILITIES; i++)
|
||||
out->probabilities[i] = evaluated ? m->arEvalMove[i] : 0.0f;
|
||||
out->equity = evaluated ? normalized_equity(m->rScore, &pos->cube) : 0.0f;
|
||||
/* ScoreMove a déjà converti rScore en équité normalisée en match. */
|
||||
out->equity = evaluated ? m->rScore : 0.0f;
|
||||
}
|
||||
|
||||
engine_status
|
||||
@@ -282,10 +287,17 @@ engine_best_moves(const char *position_id, const char *match_id, int die1, int d
|
||||
if (status != ENGINE_OK)
|
||||
return status;
|
||||
|
||||
/* Comme la commande « hint » de gnubg, avec les filtres « normaux ». */
|
||||
if (FindnSaveBestMoves(&list, die1, die2, (ConstTanBoard) pos.board, NULL, FALSE, 0.0f,
|
||||
&pos.cube, &context, aaamfMoveFilterSettings[0]) < 0)
|
||||
/*
|
||||
* Comme la commande « hint » de gnubg (hint_move dans gnubg.c), avec le
|
||||
* filtre « Normal » : fAnalyse vrai (un coup évalué moins profondément
|
||||
* ne peut pas passer devant un coup mieux évalué), puis RefreshMoveList,
|
||||
* qui classe d'abord par niveau d'évaluation et départage les égalités.
|
||||
*/
|
||||
if (FindnSaveBestMoves(&list, die1, die2, (ConstTanBoard) pos.board, NULL, TRUE,
|
||||
DOUBTFUL_THRESHOLD, &pos.cube, &context,
|
||||
aaamfMoveFilterSettings[0]) < 0)
|
||||
return ENGINE_ERROR_INTERNAL;
|
||||
RefreshMoveList(&list, NULL);
|
||||
|
||||
status = ENGINE_OK;
|
||||
if (list.cMoves > (unsigned int) max_moves)
|
||||
@@ -327,14 +339,14 @@ engine_evaluate(const char *position_id, const char *match_id, int ply,
|
||||
return ENGINE_OK;
|
||||
}
|
||||
|
||||
/* Noms anglais des décisions, dans l'ordre de l'énumération cubedecision. */
|
||||
/* Noms des décisions, dans l'ordre de l'énumération cubedecision (eval.h). */
|
||||
static const char *const cube_decision_names[] = {
|
||||
"double_take", "double_pass", "no_double_take", "too_good_take", "too_good_pass",
|
||||
"double_beaver", "no_double_beaver", "redouble_take", "redouble_pass",
|
||||
"no_redouble_take", "too_good_redouble_take", "too_good_redouble_pass",
|
||||
"no_redouble_beaver", "no_double_dead_cube", "no_redouble_dead_cube",
|
||||
"not_available", "optional_double_take", "optional_redouble_take",
|
||||
"optional_double_beaver", "optional_double_pass", "optional_redouble_pass"
|
||||
"DOUBLE_TAKE", "DOUBLE_PASS", "NODOUBLE_TAKE", "TOOGOOD_TAKE", "TOOGOOD_PASS",
|
||||
"DOUBLE_BEAVER", "NODOUBLE_BEAVER", "REDOUBLE_TAKE", "REDOUBLE_PASS",
|
||||
"NO_REDOUBLE_TAKE", "TOOGOODRE_TAKE", "TOOGOODRE_PASS",
|
||||
"NO_REDOUBLE_BEAVER", "NODOUBLE_DEADCUBE", "NO_REDOUBLE_DEADCUBE",
|
||||
"NOT_AVAILABLE", "OPTIONAL_DOUBLE_TAKE", "OPTIONAL_REDOUBLE_TAKE",
|
||||
"OPTIONAL_DOUBLE_BEAVER", "OPTIONAL_DOUBLE_PASS", "OPTIONAL_REDOUBLE_PASS"
|
||||
};
|
||||
|
||||
engine_status
|
||||
@@ -343,7 +355,7 @@ engine_cube_decision(const char *position_id, const char *match_id, int ply, eng
|
||||
position pos;
|
||||
evalcontext context;
|
||||
float output[2][NUM_ROLLOUT_OUTPUTS];
|
||||
float equities[4];
|
||||
float equities[NUM_CUBEFUL_OUTPUTS];
|
||||
cubedecision decision;
|
||||
engine_status status;
|
||||
int i;
|
||||
@@ -366,6 +378,8 @@ engine_cube_decision(const char *position_id, const char *match_id, int ply, eng
|
||||
cube->no_double = equities[OUTPUT_NODOUBLE];
|
||||
cube->double_take = equities[OUTPUT_TAKE];
|
||||
cube->double_pass = equities[OUTPUT_DROP];
|
||||
cube->optimal = equities[OUTPUT_OPTIMAL];
|
||||
cube->decision = cube_decision_names[decision];
|
||||
cube->decision_text = GetCubeRecommendation(decision);
|
||||
return ENGINE_OK;
|
||||
}
|
||||
|
||||
+11
-6
@@ -89,9 +89,13 @@ typedef struct {
|
||||
float no_double; /* équité si pas de double */
|
||||
float double_take; /* équité si double et prend */
|
||||
float double_pass; /* équité si double et passe */
|
||||
const char *decision; /* décision de gnubg, en anglais : « no_double_take »,
|
||||
* « double_take », « double_pass »,
|
||||
* « too_good_pass », « not_available »… */
|
||||
float optimal; /* équité après les meilleures décisions des deux
|
||||
* joueurs */
|
||||
const char *decision; /* nom de la décision dans l'énumération
|
||||
* cubedecision de gnubg : « NODOUBLE_TAKE »,
|
||||
* « DOUBLE_TAKE », « DOUBLE_PASS »,
|
||||
* « TOOGOOD_PASS », « NOT_AVAILABLE »… */
|
||||
const char *decision_text; /* libellé anglais de gnubg, ex. « Double, take » */
|
||||
} engine_cube;
|
||||
|
||||
/* Description lisible d'un Match ID décodé. */
|
||||
@@ -132,9 +136,10 @@ engine_status engine_legal_moves(const char *position_id, const char *match_id,
|
||||
engine_move *moves, int max_moves, int *count);
|
||||
|
||||
/*
|
||||
* Coups légaux classés du meilleur au moins bon, au niveau ply. Comme dans
|
||||
* gnubg, les coups écartés par les filtres gardent l'évaluation du niveau
|
||||
* inférieur (voir leur champ ply).
|
||||
* Coups légaux classés du meilleur au moins bon, au niveau ply, exactement
|
||||
* comme la commande « hint » de gnubg (filtre de coups « Normal »). Les coups
|
||||
* écartés par les filtres gardent l'évaluation du niveau inférieur (voir
|
||||
* leur champ ply) et sont classés après ceux évalués plus profondément.
|
||||
*/
|
||||
engine_status engine_best_moves(const char *position_id, const char *match_id,
|
||||
int die1, int die2, int ply,
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
# 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).
|
||||
# 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).
|
||||
|
||||
@@ -34,7 +34,7 @@ echouer() {
|
||||
|
||||
# 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/"$//'
|
||||
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"
|
||||
@@ -104,19 +104,19 @@ verifier_probabilites "coup 64 à 2 plis" "$($cli --ply 2 bestmove "$depart" "$l
|
||||
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) ;;
|
||||
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" = "no_double_dead_cube" ] || [ "$decision" = "not_available" ] \
|
||||
[ "$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 »"
|
||||
[ "$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() {
|
||||
|
||||
Reference in New Issue
Block a user