/* * 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 /* 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" /* 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; } /* Code un Match ID : voir engine.h. */ engine_status engine_encode_match(const engine_match *match, char match_id[13]) { const unsigned int no_dice[2] = { 0, 0 }; int value = match->cube_value, log2 = 0; while (value > 1 && value % 2 == 0) { value /= 2; log2++; } if (value != 1 || log2 > 12 || match->cube_owner < -1 || match->cube_owner > 1 || match->player_on_roll < 0 || match->player_on_roll > 1 || match->match_length < 0) return ENGINE_ERROR_MATCH_ID; snprintf(match_id, 13, "%s", MatchID(no_dice, match->player_on_roll, 0, 0, match->player_on_roll, match->cube_owner, match->crawford, match->match_length, match->score, match->cube_value, match->jacoby, GAME_PLAYING)); return ENGINE_OK; } /* Décode un Position ID en plateau de gnubg. */ static engine_status decode_board(const char *position_id, TanBoard board) { if (engine_state != READY) return ENGINE_ERROR_NOT_INITIALISED; if (!is_base64_id(position_id, L_POSITIONID) || !PositionFromID(board, position_id)) return ENGINE_ERROR_POSITION_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; status = decode_board(position_id, pos->board); if (status != ENGINE_OK) return status; 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)); /* gnubg numérote les cases de 0 à 23, la barre 24 et la sortie -1. */ out->half_move_count = 0; for (i = 0; i < 2 * ENGINE_MAX_HALF_MOVES && m->anMove[i] >= 0; i += 2) { out->half_moves[i / 2].from = m->anMove[i] + 1; out->half_moves[i / 2].to = m->anMove[i + 1] + 1; out->half_move_count++; } 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; /* ScoreMove a déjà converti rScore en équité normalisée en match. */ out->equity = evaluated ? m->rScore : 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 (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) 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 des décisions, dans l'ordre de l'énumération cubedecision (eval.h). */ static const char *const cube_decision_names[] = { "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 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[NUM_CUBEFUL_OUTPUTS]; 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->optimal = equities[OUTPUT_OPTIMAL]; cube->decision = cube_decision_names[decision]; cube->decision_text = GetCubeRecommendation(decision); GetDPEq(&cube->available, NULL, &pos.cube); cube->should_double = decision == DOUBLE_TAKE || decision == DOUBLE_PASS || decision == DOUBLE_BEAVER || decision == REDOUBLE_TAKE || decision == REDOUBLE_PASS; cube->should_take = cube->double_take <= cube->double_pass; return ENGINE_OK; } /* ------------------------------------------------- Plateau et fin de partie */ static void fill_side(engine_side *side, const unsigned int checkers[25]) { int i, on_board = 0; side->pip_count = 0; for (i = 0; i < 25; i++) { on_board += (int) checkers[i]; side->pip_count += (int) checkers[i] * (i + 1); } for (i = 0; i < 24; i++) side->points[i] = (int) checkers[i]; side->bar = (int) checkers[24]; side->off = anChequers[VARIATION_STANDARD] - on_board; } engine_status engine_decode_position(const char *position_id, engine_board *board) { TanBoard b; engine_status status = decode_board(position_id, b); if (status != ENGINE_OK) return status; /* Dans gnubg, b[1] est le joueur au trait et b[0] son adversaire. */ fill_side(&board->on_roll, b[1]); fill_side(&board->opponent, b[0]); SwapSides(b); snprintf(board->swapped_position_id, sizeof board->swapped_position_id, "%s", PositionID((ConstTanBoard) b)); return ENGINE_OK; } engine_status engine_game_result(const char *position_id, const char *match_id, engine_result *result) { TanBoard b; engine_match match; engine_status status = decode_board(position_id, b); int i; if (status == ENGINE_OK) status = engine_decode_match(match_id, &match); if (status != ENGINE_OK) return status; memset(result, 0, sizeof *result); result->kind = GameStatus((ConstTanBoard) b, VARIATION_STANDARD); if (result->kind == 0) return ENGINE_OK; result->over = 1; /* Le gagnant est celui qui n'a plus aucune dame sur le plateau. */ for (i = 0; i < 25 && b[1][i] == 0; i++) ; result->winner = i == 25 ? 0 : 1; /* Règle Jacoby : gammons et backgammons ne comptent qu'une fois le * videau tourné, c'est-à-dire quand il n'est plus au centre. */ if (match.match_length == 0 && match.jacoby && match.cube_owner == -1) result->points = match.cube_value; else result->points = result->kind * match.cube_value; return ENGINE_OK; } /* ------------------------------------------ Saisie demi-coup par demi-coup */ /* * La légalité de chaque demi-coup vient de gnubg : GenerateMoves avec un seul * dé (le second à 0) donne exactement les demi-coups permis avec ce dé (barre * d'abord, case libre, règles de sortie). Celle du coup entier aussi : les * demi-coups doivent pouvoir se compléter en une position de la liste des * coups légaux, qui impose de jouer le plus de dés possible. */ /* Un demi-coup permis et la position où il mène. */ typedef struct { int from, to; /* numérotation de gnubg : 0 à 24, -1 sortie */ positionkey key; } step; /* Positions d'arrivée des coups légaux. */ typedef struct { positionkey *keys; int count; } legal_results; /* Les demi-coups permis avec un dé. Ils sont copiés : GenerateMoves réutilise * un tableau interne, que l'appel suivant écraserait. Il y en a au plus un * par case de départ. */ static int single_steps(ConstTanBoard board, int die, step steps[25]) { movelist list; unsigned int i; GenerateMoves(&list, board, die, 0, FALSE); for (i = 0; i < list.cMoves && i < 25; i++) { steps[i].from = list.amMoves[i].anMove[0]; steps[i].to = list.amMoves[i].anMove[1]; steps[i].key = list.amMoves[i].key; } return (int) i; } static int is_legal_result(ConstTanBoard board, const legal_results *legal) { positionkey key; int i; PositionKey(board, &key); for (i = 0; i < legal->count; i++) if (EqualKeys(key, legal->keys[i])) return 1; return 0; } /* Les dés sans celui d'indice skip. */ static void remove_die(const int dice[], int count, int skip, int rest[]) { int i, j = 0; for (i = 0; i < count; i++) if (i != skip) rest[j++] = dice[i]; } /* Vrai si board peut encore mener à un coup légal avec les dés restants. */ static int can_complete(ConstTanBoard board, const int dice[], int count, const legal_results *legal) { step steps[25]; int rest[ENGINE_MAX_HALF_MOVES]; TanBoard next; int i, j, n; if (is_legal_result(board, legal)) return 1; for (i = 0; i < count; i++) { if (i > 0 && dice[i] == dice[0]) continue; /* même valeur déjà essayée (double) */ remove_die(dice, count, i, rest); n = single_steps(board, dice[i], steps); for (j = 0; j < n; j++) { PositionFromKey(next, &steps[j].key); if (can_complete((ConstTanBoard) next, rest, count - 1, legal)) return 1; } } return 0; } /* Joue les demi-coups ; en cas de succès, result reçoit la position atteinte. * Un même demi-coup peut correspondre à plusieurs dés (une sortie avec un dé * plus fort que nécessaire) : chacun est essayé. */ static int play_half_moves(ConstTanBoard board, const int dice[], int count, const engine_half_move *half_moves, int remaining, const legal_results *legal, TanBoard result) { step steps[25]; int rest[ENGINE_MAX_HALF_MOVES]; TanBoard next; int i, j, n; if (remaining == 0) { if (!can_complete(board, dice, count, legal)) return 0; memcpy(result, board, sizeof(TanBoard)); return 1; } for (i = 0; i < count; i++) { if (i > 0 && dice[i] == dice[0]) continue; /* même valeur déjà essayée (double) */ remove_die(dice, count, i, rest); n = single_steps(board, dice[i], steps); for (j = 0; j < n; j++) { if (steps[j].from != half_moves->from - 1 || steps[j].to != half_moves->to - 1) continue; PositionFromKey(next, &steps[j].key); if (play_half_moves((ConstTanBoard) next, rest, count - 1, half_moves + 1, remaining - 1, legal, result)) return 1; } } return 0; } engine_status engine_check_half_moves(const char *position_id, int die1, int die2, const engine_half_move *half_moves, int count, engine_half_moves_check *check) { TanBoard board, result; movelist list; legal_results legal; int dice[ENGINE_MAX_HALF_MOVES] = { die1, die2, die1, die2 }; int dice_count = die1 == die2 ? 4 : 2; engine_status status; unsigned int i; status = decode_board(position_id, board); if (status == ENGINE_OK) status = check_dice(die1, die2); if (status != ENGINE_OK) return status; memset(check, 0, sizeof *check); if (count < 0 || count > dice_count) return ENGINE_OK; GenerateMoves(&list, (ConstTanBoard) board, die1, die2, FALSE); if (list.cMoves == 0) return ENGINE_OK; /* aucun coup possible : rien à saisir */ legal.count = (int) list.cMoves; legal.keys = g_malloc(sizeof *legal.keys * list.cMoves); for (i = 0; i < list.cMoves; i++) legal.keys[i] = list.amMoves[i].key; if (play_half_moves((ConstTanBoard) board, dice, dice_count, half_moves, count, &legal, result)) { check->valid = 1; check->complete = is_legal_result((ConstTanBoard) result, &legal); snprintf(check->position_id, sizeof check->position_id, "%s", PositionID((ConstTanBoard) result)); } g_free(legal.keys); return ENGINE_OK; }