/* * engine-cli -- interroge le moteur d'Ustagammon en ligne de commande. * * Copyright (C) 2026 les auteurs d'Ustagammon. * Licence : GPLv3 ou ultérieure (voir LICENSE). * * Usage : * engine-cli [--ply N] [--data DOSSIER] COMMANDE ARGUMENTS * * Commandes : * bestmove POSITION_ID MATCH_ID DÉS meilleur coup (DÉS : ex. 31) * moves POSITION_ID MATCH_ID DÉS coups légaux classés, évalués * 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 * position POSITION_ID MATCH_ID plateau décodé, même position vue de * l'adversaire, fin de partie éventuelle * halfmoves POSITION_ID DÉS [DEMI-COUPS] * vérifie des demi-coups saisis un par * un (ex. « 8/5 6/5 », « bar/22 », * « 6/off ») * matchid VIDEAU PROPRIÉTAIRE Match ID d'une partie libre avec la * règle Jacoby, joueur 0 au trait ; * PROPRIÉTAIRE : -1 (centre), 0 ou 1 * 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. * * En WebAssembly (engine-wasm/), le même programme sert d'interface : le * JavaScript appelle main pour chaque commande, sans recharger le moteur. * engine_init ne fait rien quand le moteur est déjà prêt, et main ne l'arrête * pas à la fin. * * Sortie : un objet JSON sur la sortie standard. En cas d'erreur, un objet * {"error": {"code": ..., "message": ...}} et un code de sortie non nul : * 1 pour une erreur du moteur, 2 pour une erreur d'utilisation. */ #include #include #include #include "engine.h" #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" /* Liste des commandes, pour les messages d'erreur. */ #define COMMANDS "bestmove, moves, legal, cube, eval, position, halfmoves, matchid ou reference" /* 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; const char *data_dir; const char *command; const char *args[3]; int arg_count; } options; /* Code d'erreur JSON pour chaque engine_status. */ static const char * status_code(engine_status status) { switch (status) { case ENGINE_OK: return "ok"; case ENGINE_ERROR_NOT_INITIALISED: return "not_initialised"; case ENGINE_ERROR_DATA: return "invalid_data"; case ENGINE_ERROR_POSITION_ID: return "invalid_position_id"; case ENGINE_ERROR_MATCH_ID: return "invalid_match_id"; case ENGINE_ERROR_DICE: return "invalid_dice"; case ENGINE_ERROR_PLY: return "invalid_ply"; case ENGINE_ERROR_TOO_MANY_MOVES: return "too_many_moves"; case ENGINE_ERROR_INTERNAL: return "internal_error"; } return "unknown_error"; } /* Les messages sont des constantes sans guillemets : pas d'échappement. */ static int print_error(const char *code, const char *message, int exit_code) { printf("{\"error\": {\"code\": \"%s\", \"message\": \"%s\"}}\n", code, message); return exit_code; } static int print_engine_error(engine_status status) { return print_error(status_code(status), engine_status_message(status), EXIT_ENGINE_ERROR); } static int usage_error(const char *message) { return print_error("usage", message, EXIT_USAGE_ERROR); } static void print_probabilities(const float p[ENGINE_NUM_PROBABILITIES]) { 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]); } /* Demi-coups sous la forme [[8, 5], [6, 5]]. */ static void print_half_moves(const engine_half_move *half_moves, int count) { int i; printf("["); for (i = 0; i < count; i++) printf("%s[%d, %d]", i ? ", " : "", half_moves[i].from, half_moves[i].to); printf("]"); } static void print_move(const engine_move *m, int with_evaluation) { printf("{\"move\": \"%s\", \"position_id\": \"%s\", \"half_moves\": ", m->notation, m->position_id); print_half_moves(m->half_moves, m->half_move_count); if (with_evaluation) { printf(", \"ply\": %d, \"equity\": " NUMBER ", \"probabilities\": ", m->ply, m->equity); print_probabilities(m->probabilities); } printf("}"); } static void print_match(const engine_match *m) { printf("{\"match_length\": %d, \"score\": [%d, %d], \"cube_value\": %d, " "\"cube_owner\": %d, \"player_on_roll\": %d, \"crawford\": %s, \"jacoby\": %s}", m->match_length, m->score[0], m->score[1], m->cube_value, m->cube_owner, 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) { if (strlen(text) != 2 || text[0] < '0' || text[0] > '9' || text[1] < '0' || text[1] > '9') return 0; *die1 = text[0] - '0'; *die2 = text[1] - '0'; return 1; } /* bestmove, moves et legal. */ static int run_moves_command(const options *opt) { const char *position_id = opt->args[0], *match_id = opt->args[1]; int best_only = strcmp(opt->command, "bestmove") == 0; int evaluate = strcmp(opt->command, "legal") != 0; int die1, die2, count, i; engine_move *moves; engine_status status; if (opt->arg_count != 3) return usage_error("arguments attendus : POSITION_ID MATCH_ID DÉS"); if (!parse_dice(opt->args[2], &die1, &die2)) return usage_error("dés attendus sous la forme 31"); moves = malloc(sizeof *moves * ENGINE_MAX_MOVES); if (!moves) return print_error("out_of_memory", "mémoire insuffisante", EXIT_ENGINE_ERROR); if (evaluate) status = engine_best_moves(position_id, match_id, die1, die2, opt->ply, moves, ENGINE_MAX_MOVES, &count); else status = engine_legal_moves(position_id, match_id, die1, die2, moves, ENGINE_MAX_MOVES, &count); if (status != ENGINE_OK) { free(moves); return print_engine_error(status); } printf("{\"position_id\": \"%s\", \"match_id\": \"%s\", \"dice\": [%d, %d]", position_id, match_id, die1, die2); if (evaluate) printf(", \"ply\": %d", opt->ply); if (best_only) { printf(", \"best_move\": "); if (count > 0) print_move(&moves[0], 1); else printf("null"); } else { printf(", \"count\": %d, \"moves\": [", count); for (i = 0; i < count; i++) { printf(i ? ", " : ""); print_move(&moves[i], evaluate); } printf("]"); } printf("}\n"); free(moves); return EXIT_SUCCESS; } /* eval et cube. */ static int run_position_command(const options *opt) { const char *position_id = opt->args[0], *match_id = opt->args[1]; engine_match match; engine_status status; if (opt->arg_count != 2) return usage_error("arguments attendus : POSITION_ID MATCH_ID"); status = engine_decode_match(match_id, &match); if (status != ENGINE_OK) return print_engine_error(status); if (strcmp(opt->command, "eval") == 0) { engine_evaluation e; status = engine_evaluate(position_id, match_id, opt->ply, &e); if (status != ENGINE_OK) return print_engine_error(status); printf("{\"position_id\": \"%s\", \"match_id\": \"%s\", \"ply\": %d, \"match\": ", position_id, match_id, opt->ply); print_match(&match); printf(", \"probabilities\": "); print_probabilities(e.probabilities); printf(", \"cubeless_equity\": " NUMBER ", \"cubeful_equity\": " NUMBER "}\n", e.cubeless_equity, e.cubeful_equity); } else { engine_cube c; status = engine_cube_decision(position_id, match_id, opt->ply, &c); if (status != ENGINE_OK) return print_engine_error(status); printf("{\"position_id\": \"%s\", \"match_id\": \"%s\", \"ply\": %d, \"match\": ", position_id, match_id, opt->ply); print_match(&match); printf(", \"probabilities\": "); print_probabilities(c.probabilities); printf(", "); print_cube_fields(&c); printf(", \"available\": %s, \"double\": %s, \"take\": %s}\n", c.available ? "true" : "false", c.should_double ? "true" : "false", c.should_take ? "true" : "false"); } return EXIT_SUCCESS; } static void print_side(const engine_side *side) { int i; printf("{\"points\": ["); for (i = 0; i < 24; i++) printf("%s%d", i ? ", " : "", side->points[i]); printf("], \"bar\": %d, \"off\": %d, \"pip_count\": %d}", side->bar, side->off, side->pip_count); } /* position : plateau, même position vue de l'adversaire, fin de partie. */ static int run_board_command(const options *opt) { static const char *const kinds[] = { "", "single", "gammon", "backgammon" }; const char *position_id = opt->args[0], *match_id = opt->args[1]; engine_board board; engine_result result; engine_status status; if (opt->arg_count != 2) return usage_error("arguments attendus : POSITION_ID MATCH_ID"); status = engine_decode_position(position_id, &board); if (status == ENGINE_OK) status = engine_game_result(position_id, match_id, &result); if (status != ENGINE_OK) return print_engine_error(status); printf("{\"position_id\": \"%s\", \"match_id\": \"%s\", \"on_roll\": ", position_id, match_id); print_side(&board.on_roll); printf(", \"opponent\": "); print_side(&board.opponent); printf(", \"swapped_position_id\": \"%s\", \"result\": ", board.swapped_position_id); if (result.over) printf("{\"winner\": \"%s\", \"kind\": \"%s\", \"points\": %d}", result.winner == 0 ? "on_roll" : "opponent", kinds[result.kind], result.points); else printf("null"); printf("}\n"); return EXIT_SUCCESS; } /* Lit une case d'un demi-coup : 1 à 25, « bar » (25) ou « off » (0). */ static int parse_point(const char *text, size_t length, int *point) { char buffer[4]; char *end; long value; if (length == 0 || length >= sizeof buffer) return 0; memcpy(buffer, text, length); buffer[length] = '\0'; if (strcmp(buffer, "bar") == 0) { *point = ENGINE_BAR; return 1; } if (strcmp(buffer, "off") == 0) { *point = ENGINE_OFF; return 1; } value = strtol(buffer, &end, 10); if (*end || value < 1 || value > ENGINE_BAR) return 0; *point = (int) value; return 1; } /* Lit des demi-coups séparés par des espaces, chacun « DÉPART/ARRIVÉE ». * Renvoie leur nombre, ou -1 si le texte est mal formé. */ static int parse_half_moves(const char *text, engine_half_move half_moves[ENGINE_MAX_HALF_MOVES]) { int count = 0; while (*text) { const char *start, *slash; size_t length; while (*text == ' ') text++; if (!*text) break; start = text; while (*text && *text != ' ') text++; length = (size_t) (text - start); slash = memchr(start, '/', length); if (!slash || count == ENGINE_MAX_HALF_MOVES || !parse_point(start, (size_t) (slash - start), &half_moves[count].from) || !parse_point(slash + 1, length - (size_t) (slash - start) - 1, &half_moves[count].to)) return -1; count++; } return count; } /* halfmoves : vérifie des demi-coups saisis un par un. */ static int run_half_moves_command(const options *opt) { const char *position_id = opt->args[0]; engine_half_move half_moves[ENGINE_MAX_HALF_MOVES]; engine_half_moves_check check; engine_status status; int die1, die2, count = 0; if (opt->arg_count != 2 && opt->arg_count != 3) return usage_error("arguments attendus : POSITION_ID DÉS [DEMI-COUPS]"); if (!parse_dice(opt->args[1], &die1, &die2)) return usage_error("dés attendus sous la forme 31"); if (opt->arg_count == 3) count = parse_half_moves(opt->args[2], half_moves); if (count < 0) return usage_error("demi-coups attendus sous la forme 8/5 6/5, bar/22 ou 6/off"); status = engine_check_half_moves(position_id, die1, die2, half_moves, count, &check); if (status != ENGINE_OK) return print_engine_error(status); printf("{\"position_id\": \"%s\", \"dice\": [%d, %d], \"half_moves\": ", position_id, die1, die2); print_half_moves(half_moves, count); printf(", \"valid\": %s, \"complete\": %s, \"result_position_id\": ", check.valid ? "true" : "false", check.complete ? "true" : "false"); if (check.valid) printf("\"%s\"}\n", check.position_id); else printf("null}\n"); return EXIT_SUCCESS; } /* matchid : Match ID d'une partie libre avec la règle Jacoby, joueur 0 au * trait. */ static int run_match_id_command(const options *opt) { engine_match match = { .cube_value = 1, .cube_owner = -1, .jacoby = 1 }; char match_id[13]; char *end; engine_status status; if (opt->arg_count != 2) return usage_error("arguments attendus : VIDEAU PROPRIÉTAIRE"); match.cube_value = (int) strtol(opt->args[0], &end, 10); if (*end || end == opt->args[0]) return usage_error("VIDEAU attend un nombre"); match.cube_owner = (int) strtol(opt->args[1], &end, 10); if (*end || end == opt->args[1]) return usage_error("PROPRIÉTAIRE attend -1, 0 ou 1"); status = engine_encode_match(&match, match_id); if (status != ENGINE_OK) return print_engine_error(status); printf("{\"match_id\": \"%s\", \"match\": ", match_id); print_match(&match); 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; } /* Dossier « data » à côté du programme. */ static char * default_data_dir(const char *program) { const char *slash = strrchr(program, '/'); size_t dir_length = slash ? (size_t) (slash - program) : 1; char *dir = malloc(dir_length + sizeof "/data"); if (!dir) return NULL; if (slash) memcpy(dir, program, dir_length); else dir[0] = '.'; strcpy(dir + dir_length, "/data"); return dir; } /* Lit les options ; renvoie un message d'erreur, ou NULL si tout va bien. */ static const char * parse_options(int argc, char **argv, options *opt) { int i; memset(opt, 0, sizeof *opt); for (i = 1; i < argc; i++) { if (strcmp(argv[i], "--ply") == 0 && i + 1 < argc) { char *end; opt->ply = (int) strtol(argv[++i], &end, 10); if (*end || end == argv[i]) return "--ply attend un nombre"; } else if (strcmp(argv[i], "--data") == 0 && i + 1 < argc) { opt->data_dir = argv[++i]; } else if (argv[i][0] == '-' && argv[i][1] == '-') { return "option inconnue ou sans valeur"; } else if (!opt->command) { opt->command = argv[i]; } else if (opt->arg_count < 3) { opt->args[opt->arg_count++] = argv[i]; } else { return "trop d'arguments"; } } if (!opt->command) return "commande attendue : " COMMANDS; return NULL; } int main(int argc, char **argv) { options opt; const char *problem = parse_options(argc, argv, &opt); char *data_dir = NULL; engine_status status; int result; if (problem) return usage_error(problem); if (!opt.data_dir) { data_dir = default_data_dir(argv[0]); if (!data_dir) return print_error("out_of_memory", "mémoire insuffisante", EXIT_ENGINE_ERROR); opt.data_dir = data_dir; } status = engine_init(opt.data_dir); free(data_dir); if (status != ENGINE_OK) return print_engine_error(status); if (strcmp(opt.command, "bestmove") == 0 || strcmp(opt.command, "moves") == 0 || strcmp(opt.command, "legal") == 0) 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 if (strcmp(opt.command, "position") == 0) result = run_board_command(&opt); else if (strcmp(opt.command, "halfmoves") == 0) result = run_half_moves_command(&opt); else if (strcmp(opt.command, "matchid") == 0) result = run_match_id_command(&opt); else result = usage_error("commande inconnue : " COMMANDS); #ifndef __EMSCRIPTEN__ engine_shutdown(); #endif return result; }