/* * 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; }