diff --git a/engine-cli/main.c b/engine-cli/main.c index 871bfbb..dddcb9b 100644 --- a/engine-cli/main.c +++ b/engine-cli/main.c @@ -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[""] (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; diff --git a/engine/engine.c b/engine/engine.c index 7b1b1bc..1b6a157 100644 --- a/engine/engine.c +++ b/engine/engine.c @@ -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; } diff --git a/engine/include/engine.h b/engine/include/engine.h index 32c6889..39cbaeb 100644 --- a/engine/include/engine.h +++ b/engine/include/engine.h @@ -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, diff --git a/tests/test_engine.sh b/tests/test_engine.sh index f5f9294..c09f1e6 100644 --- a/tests/test_engine.sh +++ b/tests/test_engine.sh @@ -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() {