From da5fa71ff871e97ebad00e06cfec17569d6ad8e3 Mon Sep 17 00:00:00 2001 From: Pierre MARTIN Date: Mon, 2 Nov 2026 13:56:29 +0100 Subject: [PATCH] Interface engine.h et programme engine-cli MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit engine.h expose initialisation, coups légaux, classement des coups, évaluation et décision de videau, avec Position ID et Match ID, sans type gnubg ; erreurs par code de retour. engine-cli l'interroge et répond en JSON (bestmove, moves, legal, cube, eval ; option --ply). Co-Authored-By: Claude Opus 5.5 --- engine-cli/main.c | 306 +++++++++++++++++++++++++++++++++ engine/engine.c | 371 ++++++++++++++++++++++++++++++++++++++++ engine/include/engine.h | 155 +++++++++++++++++ 3 files changed, 832 insertions(+) create mode 100644 engine-cli/main.c create mode 100644 engine/engine.c create mode 100644 engine/include/engine.h diff --git a/engine-cli/main.c b/engine-cli/main.c new file mode 100644 index 0000000..871bfbb --- /dev/null +++ b/engine-cli/main.c @@ -0,0 +1,306 @@ +/* + * 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 + * + * --ply : niveau de gnubg, de 0 (défaut) à 4. + * --data : dossier des données ; par défaut « data » à côté du programme. + * + * 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 + +/* 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\": %.6f, \"win_gammon\": %.6f, \"win_backgammon\": %.6f, " + "\"lose_gammon\": %.6f, \"lose_backgammon\": %.6f}", + 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\": %.6f, \"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"); +} + +/* 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\": %.6f, \"cubeful_equity\": %.6f}\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(", \"no_double\": %.6f, \"double_take\": %.6f, \"double_pass\": %.6f, " + "\"decision\": \"%s\"}\n", c.no_double, c.double_take, c.double_pass, c.decision); + } + 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 ou eval"; + 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 + result = usage_error("commande inconnue : bestmove, moves, legal, cube ou eval"); + + engine_shutdown(); + return result; +} diff --git a/engine/engine.c b/engine/engine.c new file mode 100644 index 0000000..7b1b1bc --- /dev/null +++ b/engine/engine.c @@ -0,0 +1,371 @@ +/* + * engine.c -- façade du moteur d'Ustagammon sur le cœur de GNU Backgammon. + * + * Copyright (C) 2026 les auteurs d'Ustagammon. + * Licence : GPLv3 ou ultérieure (voir LICENSE). + * + * Chaque fonction décode les identifiants de position, prépare le contexte + * de gnubg (cubeinfo, réglages d'évaluation) comme le fait gnubg de bureau + * pour ses commandes « hint » et « eval », puis appelle gnubg. + */ + +#include +#include + +#include "config.h" +#include "backgammon.h" +#include "drawboard.h" +#include "matchequity.h" +#include "matchid.h" +#include "multithread.h" +#include "positionid.h" +#include "gnubg-support.h" +#include "engine.h" + +/* Nombre de castors autorisés en partie libre : valeur par défaut de gnubg. */ +#define DEFAULT_BEAVERS 3 + +/* Fichier de table d'équité de match : celui de gnubg de bureau par défaut. */ +#define MET_FILE "Kazaross-XG2.xml" + +/* gnubg ne sait pas se réinitialiser après EvalShutdown : on retient l'état. */ +static enum { NOT_STARTED, READY, STOPPED } engine_state = NOT_STARTED; + +/* Une position décodée, prête pour gnubg. */ +typedef struct { + TanBoard board; + cubeinfo cube; + engine_match match; +} position; + +/* -------------------------------------------------------- Initialisation */ + +/* Vérifie l'en-tête de gnubg.wd, pour éviter l'exit() de EvalInitialise. */ +static int +binary_weights_ok(const char *path) +{ + float header[2]; + FILE *f = fopen(path, "rb"); + int ok; + + if (!f) + return 0; + ok = fread(header, sizeof header[0], 2, f) == 2 + && header[0] == WEIGHTS_MAGIC_BINARY && header[1] == WEIGHTS_VERSION_BINARY; + fclose(f); + return ok; +} + +static engine_status +load_match_equity_table(const char *data_dir) +{ + char *path = g_build_filename(data_dir, MET_FILE, NULL); + int ok; + + ok = g_file_test(path, G_FILE_TEST_IS_REGULAR); + if (ok) { + InitMatchEquity(path); + /* En cas d'échec, gnubg se rabat sans erreur sur la table de Zadeh. */ + ok = miCurrent.szFileName && strcmp(miCurrent.szFileName, path) == 0; + } + g_free(path); + return ok ? ENGINE_OK : ENGINE_ERROR_DATA; +} + +static engine_status +load_neural_nets_and_bearoff(const char *data_dir) +{ + char *weights = g_build_filename(data_dir, "gnubg.wd", NULL); + int ok = binary_weights_ok(weights); + + if (ok) { + EvalInitialise(NULL, weights, FALSE, NULL); + /* Bases de fin de partie : gnubg continue sans elles, pas nous. */ + ok = pbc1 && !pbc1->fHeuristic && pbc2; + } + g_free(weights); + return ok ? ENGINE_OK : ENGINE_ERROR_DATA; +} + +engine_status +engine_init(const char *data_dir) +{ + engine_status status; + + if (engine_state == READY) + return ENGINE_OK; + if (engine_state == STOPPED || !data_dir) + return ENGINE_ERROR_NOT_INITIALISED; + + gnubg_support_set_data_dir(data_dir); + status = load_match_equity_table(data_dir); + if (status == ENGINE_OK) + status = load_neural_nets_and_bearoff(data_dir); + if (status != ENGINE_OK) + return status; + + MT_InitThreads(); + engine_state = READY; + return ENGINE_OK; +} + +void +engine_shutdown(void) +{ + if (engine_state != READY) + return; + MT_Close(); + td.tld = NULL; + EvalShutdown(); + engine_state = STOPPED; +} + +const char * +engine_status_message(engine_status status) +{ + switch (status) { + case ENGINE_OK: + return "succès"; + case ENGINE_ERROR_NOT_INITIALISED: + return "moteur non initialisé"; + case ENGINE_ERROR_DATA: + return "données du moteur absentes ou invalides"; + case ENGINE_ERROR_POSITION_ID: + return "Position ID invalide"; + case ENGINE_ERROR_MATCH_ID: + return "Match ID invalide"; + case ENGINE_ERROR_DICE: + return "dés invalides (chacun entre 1 et 6)"; + case ENGINE_ERROR_PLY: + return "niveau invalide"; + case ENGINE_ERROR_TOO_MANY_MOVES: + return "tableau de coups trop petit"; + case ENGINE_ERROR_INTERNAL: + return "erreur interne de gnubg"; + } + return "erreur inconnue"; +} + +/* ------------------------------------------------------------- Décodage */ + +/* Vrai si id a exactement length caractères de l'alphabet base64 de gnubg. */ +static int +is_base64_id(const char *id, size_t length) +{ + size_t i; + + if (!id || strlen(id) != length) + return 0; + for (i = 0; i < length; i++) + if (Base64((unsigned char) id[i]) == 255) + return 0; + return 1; +} + +engine_status +engine_decode_match(const char *match_id, engine_match *match) +{ + unsigned int dice[2]; + int turn, resigned, doubled; + gamestate state; + + if (!is_base64_id(match_id, L_MATCHID)) + return ENGINE_ERROR_MATCH_ID; + if (MatchFromID(dice, &turn, &resigned, &doubled, &match->player_on_roll, + &match->cube_owner, &match->crawford, &match->match_length, + match->score, &match->cube_value, &match->jacoby, &state, match_id) != 0) + return ENGINE_ERROR_MATCH_ID; + return ENGINE_OK; +} + +/* Décode Position ID et Match ID, et prépare le cubeinfo de gnubg. */ +static engine_status +decode_position(const char *position_id, const char *match_id, position *pos) +{ + engine_match *m = &pos->match; + engine_status status; + + if (engine_state != READY) + return ENGINE_ERROR_NOT_INITIALISED; + if (!is_base64_id(position_id, L_POSITIONID) || !PositionFromID(pos->board, position_id)) + return ENGINE_ERROR_POSITION_ID; + status = engine_decode_match(match_id, m); + if (status != ENGINE_OK) + return status; + if (SetCubeInfo(&pos->cube, m->cube_value, m->cube_owner, m->player_on_roll, + m->match_length, m->score, m->crawford, m->jacoby, + DEFAULT_BEAVERS, VARIATION_STANDARD) != 0) + return ENGINE_ERROR_MATCH_ID; + return ENGINE_OK; +} + +static engine_status +check_dice(int die1, int die2) +{ + if (die1 < 1 || die1 > 6 || die2 < 1 || die2 > 6) + return ENGINE_ERROR_DICE; + return ENGINE_OK; +} + +/* Réglages prédéfinis de gnubg pour un niveau, sans bruit. */ +static engine_status +eval_context_for_ply(int ply, evalcontext *context) +{ + if (ply < 0 || ply > ENGINE_MAX_PLY) + return ENGINE_ERROR_PLY; + *context = aecSettings[SETTINGS_0PLY + ply]; + return ENGINE_OK; +} + +/* En match, gnubg calcule en chances de gain du match : on convertit. */ +static float +normalized_equity(float score, const cubeinfo *cube) +{ + return cube->nMatchTo ? mwc2eq(score, cube) : score; +} + +/* ------------------------------------------------------------------ Coups */ + +static void +fill_move(engine_move *out, const move *m, const position *pos, int evaluated) +{ + int i; + + FormatMove(out->notation, (ConstTanBoard) pos->board, m->anMove); + snprintf(out->position_id, sizeof out->position_id, "%s", PositionIDFromKey(&m->key)); + 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; +} + +engine_status +engine_legal_moves(const char *position_id, const char *match_id, int die1, int die2, + engine_move *moves, int max_moves, int *count) +{ + position pos; + movelist list; + engine_status status; + unsigned int i; + + status = decode_position(position_id, match_id, &pos); + if (status == ENGINE_OK) + status = check_dice(die1, die2); + if (status != ENGINE_OK) + return status; + + /* La liste pointe vers un tableau interne de gnubg : rien à libérer. */ + GenerateMoves(&list, (ConstTanBoard) pos.board, die1, die2, FALSE); + if (list.cMoves > (unsigned int) max_moves) + return ENGINE_ERROR_TOO_MANY_MOVES; + for (i = 0; i < list.cMoves; i++) + fill_move(&moves[i], &list.amMoves[i], &pos, 0); + *count = (int) list.cMoves; + return ENGINE_OK; +} + +engine_status +engine_best_moves(const char *position_id, const char *match_id, int die1, int die2, int ply, + engine_move *moves, int max_moves, int *count) +{ + position pos; + movelist list; + evalcontext context; + engine_status status; + unsigned int i; + + status = decode_position(position_id, match_id, &pos); + if (status == ENGINE_OK) + status = check_dice(die1, die2); + if (status == ENGINE_OK) + status = eval_context_for_ply(ply, &context); + 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) + return ENGINE_ERROR_INTERNAL; + + status = ENGINE_OK; + if (list.cMoves > (unsigned int) max_moves) + status = ENGINE_ERROR_TOO_MANY_MOVES; + else { + for (i = 0; i < list.cMoves; i++) + fill_move(&moves[i], &list.amMoves[i], &pos, 1); + *count = (int) list.cMoves; + } + g_free(list.amMoves); + return status; +} + +/* ------------------------------------------------- Évaluation et videau */ + +engine_status +engine_evaluate(const char *position_id, const char *match_id, int ply, + engine_evaluation *evaluation) +{ + position pos; + evalcontext context; + float output[NUM_ROLLOUT_OUTPUTS]; + engine_status status; + int i; + + status = decode_position(position_id, match_id, &pos); + if (status == ENGINE_OK) + status = eval_context_for_ply(ply, &context); + if (status != ENGINE_OK) + return status; + + if (GeneralEvaluationE(output, (ConstTanBoard) pos.board, &pos.cube, &context) < 0) + return ENGINE_ERROR_INTERNAL; + + for (i = 0; i < ENGINE_NUM_PROBABILITIES; i++) + evaluation->probabilities[i] = output[i]; + evaluation->cubeless_equity = output[OUTPUT_EQUITY]; + evaluation->cubeful_equity = normalized_equity(output[OUTPUT_CUBEFUL_EQUITY], &pos.cube); + return ENGINE_OK; +} + +/* Noms anglais des décisions, dans l'ordre de l'énumération cubedecision. */ +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" +}; + +engine_status +engine_cube_decision(const char *position_id, const char *match_id, int ply, engine_cube *cube) +{ + position pos; + evalcontext context; + float output[2][NUM_ROLLOUT_OUTPUTS]; + float equities[4]; + cubedecision decision; + engine_status status; + int i; + + status = decode_position(position_id, match_id, &pos); + if (status == ENGINE_OK) + status = eval_context_for_ply(ply, &context); + if (status != ENGINE_OK) + return status; + + if (GeneralCubeDecisionE(output, (ConstTanBoard) pos.board, &pos.cube, &context, NULL) < 0) + return ENGINE_ERROR_INTERNAL; + /* FindCubeDecision renvoie des équités déjà normalisées. */ + decision = FindCubeDecision(equities, output, &pos.cube); + if ((size_t) decision >= sizeof cube_decision_names / sizeof cube_decision_names[0]) + return ENGINE_ERROR_INTERNAL; + + for (i = 0; i < ENGINE_NUM_PROBABILITIES; i++) + cube->probabilities[i] = output[0][i]; + cube->no_double = equities[OUTPUT_NODOUBLE]; + cube->double_take = equities[OUTPUT_TAKE]; + cube->double_pass = equities[OUTPUT_DROP]; + cube->decision = cube_decision_names[decision]; + return ENGINE_OK; +} diff --git a/engine/include/engine.h b/engine/include/engine.h new file mode 100644 index 0000000..32c6889 --- /dev/null +++ b/engine/include/engine.h @@ -0,0 +1,155 @@ +/* + * engine.h -- interface C du moteur de backgammon d'Ustagammon. + * + * Copyright (C) 2026 les auteurs d'Ustagammon. + * Licence : GPLv3 ou ultérieure (voir LICENSE). + * + * Le moteur est le cœur de GNU Backgammon (engine/gnubg/), seule autorité + * pour les règles, les coups, le videau et les évaluations. Cette interface + * n'expose aucun type de gnubg. + * + * Positions : une position est décrite par deux identifiants de gnubg, + * - le Position ID (14 caractères), qui code le plateau du point de vue du + * joueur au trait ; + * - le Match ID (12 caractères), qui code le reste : longueur du match (0 en + * partie libre), score, videau et son propriétaire, joueur au trait, + * Crawford, Jacoby. Les dés éventuellement codés dans le Match ID sont + * ignorés : les dés sont passés à part. + * + * Niveaux (« ply ») : réglages prédéfinis de gnubg, sans bruit, de 0 + * (évaluation statique du réseau) à 4. gnubg compte les plis à partir de 0. + * + * Équités : toujours du point de vue du joueur au trait, normalisées pour + * un videau de 1. En match, gnubg convertit les chances de gain du match en + * équité normalisée, comme il l'affiche par défaut. + * + * Probabilités, dans l'ordre de gnubg : gain, gain gammon, gain backgammon, + * perte gammon, perte backgammon (les gammons incluent les backgammons). + * + * Contraintes : une seule instance du moteur (état global de gnubg) et des + * appels sérialisés, jamais concurrents. Toutes les fonctions renvoient un + * code engine_status ; aucune ne termine le programme pour une erreur + * d'utilisation. + */ + +#ifndef USTAGAMMON_ENGINE_H +#define USTAGAMMON_ENGINE_H + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum { + ENGINE_OK = 0, + ENGINE_ERROR_NOT_INITIALISED, /* engine_init n'a pas réussi */ + ENGINE_ERROR_DATA, /* données absentes ou invalides */ + ENGINE_ERROR_POSITION_ID, /* Position ID invalide */ + ENGINE_ERROR_MATCH_ID, /* Match ID invalide */ + ENGINE_ERROR_DICE, /* dés hors de 1..6 */ + ENGINE_ERROR_PLY, /* niveau hors de 0..ENGINE_MAX_PLY */ + ENGINE_ERROR_TOO_MANY_MOVES, /* tableau de coups trop petit */ + ENGINE_ERROR_INTERNAL /* échec signalé par gnubg */ +} engine_status; + +#define ENGINE_MAX_PLY 4 + +/* Nombre maximal de coups légaux pour un jet (MAX_MOVES de gnubg). */ +#define ENGINE_MAX_MOVES 3060 + +/* Indices du tableau probabilities. */ +enum { + ENGINE_WIN = 0, + ENGINE_WIN_GAMMON, + ENGINE_WIN_BACKGAMMON, + ENGINE_LOSE_GAMMON, + ENGINE_LOSE_BACKGAMMON, + ENGINE_NUM_PROBABILITIES +}; + +/* Un coup, avec son évaluation quand il a été classé. */ +typedef struct { + char notation[32]; /* notation de gnubg, ex. « 8/5 6/5 » */ + char position_id[16]; /* Position ID après le coup, plateau vu du + * joueur qui vient de jouer */ + int ply; /* niveau de l'évaluation (-1 : non évalué) */ + float probabilities[ENGINE_NUM_PROBABILITIES]; + float equity; /* équité de classement (avec videau) */ +} engine_move; + +/* Évaluation d'une position, joueur au trait avant de lancer les dés. */ +typedef struct { + float probabilities[ENGINE_NUM_PROBABILITIES]; + float cubeless_equity; /* équité sans videau */ + float cubeful_equity; /* équité avec videau */ +} engine_evaluation; + +/* Décision de videau du joueur au trait, avant de lancer les dés. */ +typedef struct { + float probabilities[ENGINE_NUM_PROBABILITIES]; + 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 »… */ +} engine_cube; + +/* Description lisible d'un Match ID décodé. */ +typedef struct { + int match_length; /* 0 : partie libre (money game) */ + int score[2]; + int cube_value; + int cube_owner; /* -1 : videau au centre ; sinon 0 ou 1 */ + int player_on_roll; /* 0 ou 1 */ + int crawford; /* partie Crawford en cours */ + int jacoby; /* règle Jacoby (partie libre) */ +} engine_match; + +/* + * Charge les données du moteur depuis data_dir : gnubg.wd (réseaux), + * gnubg_os0.bd et gnubg_ts0.bd (fins de partie), Kazaross-XG2.xml (table + * d'équité de match). À appeler une fois avant toute autre fonction. + */ +engine_status engine_init(const char *data_dir); + +/* Libère les ressources. Le moteur ne peut pas être réinitialisé ensuite. */ +void engine_shutdown(void); + +/* Message d'erreur en français pour un code de retour. */ +const char *engine_status_message(engine_status status); + +/* Décode un Match ID. */ +engine_status engine_decode_match(const char *match_id, engine_match *match); + +/* + * Coups légaux pour une position et un jet (die1, die2 dans 1..6), sans + * évaluation (ply vaut -1). moves doit pouvoir contenir max_moves coups ; + * ENGINE_MAX_MOVES suffit toujours. *count reçoit le nombre de coups (0 si + * le joueur ne peut pas jouer). + */ +engine_status engine_legal_moves(const char *position_id, const char *match_id, + int die1, int die2, + 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). + */ +engine_status engine_best_moves(const char *position_id, const char *match_id, + int die1, int die2, int ply, + engine_move *moves, int max_moves, int *count); + +/* Évaluation d'une position au niveau ply. */ +engine_status engine_evaluate(const char *position_id, const char *match_id, + int ply, engine_evaluation *evaluation); + +/* Décision de videau au niveau ply. */ +engine_status engine_cube_decision(const char *position_id, const char *match_id, + int ply, engine_cube *cube); + +#ifdef __cplusplus +} +#endif + +#endif /* USTAGAMMON_ENGINE_H */