/* * 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 * 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" /* 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]); } static void 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\": " 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("}\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 : bestmove, moves, legal, cube, eval ou reference"; 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 result = usage_error("commande inconnue : bestmove, moves, legal, cube, eval ou reference"); #ifndef __EMSCRIPTEN__ engine_shutdown(); #endif return result; }