/* * 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 » et l'analyse passent à * FindnSaveBestMoves. */ #define DOUBTFUL_THRESHOLD ENGINE_SKILL_DOUBTFUL_THRESHOLD /* Position de départ : la même vue des deux joueurs. */ #define START_POSITION_ID "4HPwATDgc/ABMA" /* 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"; case ENGINE_ERROR_LEVEL: return "niveau de jeu inconnu"; case ENGINE_ERROR_MOVE: return "coup joué absent des coups légaux"; case ENGINE_ERROR_CUBE_ACTION: return "décision de videau inconnue"; } 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; } /* * Vide le cache d'évaluation de gnubg avant chaque calcul : un résultat ne * dépend ainsi que de ses arguments, comme les références (gnubg de bureau, * « clear cache » avant chaque position). Sans cela, une recherche à 2 plis * avec élagage laisse dans le cache des évaluations à 0 pli faites avec les * réseaux d'élagage, rangées sous la même clé que les évaluations à 0 pli * ordinaires (EvalKey, eval.c l. 2302 : l'élagage n'y compte qu'à partir * d'1 pli) ; une chance de jet ou un coup à 0 pli calculé ensuite les * reprendrait (écarts mesurés jusqu'à 2,5e-2, tests/reference/README.md). * Coût mesuré : environ 3 ms par appel sous Node sur le PC. */ static void start_evaluation(void) { EvalCacheFlush(); } /* Réglages prédéfinis de gnubg pour un niveau, sans bruit ; prépare le * calcul. */ static engine_status eval_context_for_ply(int ply, evalcontext *context) { if (ply < 0 || ply > ENGINE_MAX_PLY) return ENGINE_ERROR_PLY; start_evaluation(); *context = aecSettings[SETTINGS_0PLY + ply]; return ENGINE_OK; } /* -------------------------------------------------------- Niveaux de jeu */ /* * Les niveaux d'engine_level sont, dans le même ordre, les réglages * prédéfinis de gnubg (aecSettings, eval.c l. 318-328, et noms de * aszSettings, l. 305-315, où « casual play » devient « casual »). */ static const char *const level_names[ENGINE_LEVEL_COUNT] = { "beginner", "casual", "intermediate", "advanced", "0ply", "1ply", "2ply", "3ply", "4ply" }; /* Réglages d'évaluation d'un niveau (bruit compris) ; prépare le calcul. */ static engine_status eval_context_for_level(int level, evalcontext *context) { if (level < 0 || level >= ENGINE_LEVEL_COUNT) return ENGINE_ERROR_LEVEL; start_evaluation(); *context = aecSettings[level]; return ENGINE_OK; } /* Filtre de coups d'un niveau : aiSettingsMoveFilter (eval.c l. 331) vaut * « Normal » à partir d'1 pli, et « sans objet » à 0 pli, où aucun filtre ne * sert ; le joueur de gnubg garde alors son filtre par défaut, « Normal » * aussi (ap dans gnubg.c l. 461). */ static movefilter (*move_filter_for_level(int level))[MAX_FILTER_PLIES] { int index = aiSettingsMoveFilter[level]; return aaamfMoveFilterSettings[index < 0 ? 0 : index]; } engine_status engine_level_settings_of(int level, engine_level_settings *settings) { evalcontext context; engine_status status = eval_context_for_level(level, &context); if (status != ENGINE_OK) return status; settings->name = level_names[level]; settings->plies = (int) context.nPlies; settings->noise = context.rNoise; settings->deterministic = (int) context.fDeterministic; settings->prune = (int) context.fUsePrune; return ENGINE_OK; } engine_status engine_level_from_name(const char *name, int *level) { int i; for (i = 0; name && i < ENGINE_LEVEL_COUNT; i++) if (strcmp(name, level_names[i]) == 0) { *level = i; return ENGINE_OK; } return ENGINE_ERROR_LEVEL; } /* ----------------------------------------------- Appréciation et chance */ /* Skill de gnubg (analysis.c l. 288), avec les seuils de arSkillLevel. */ engine_skill engine_skill_of(float equity_loss) { if (equity_loss < -ENGINE_SKILL_VERY_BAD_THRESHOLD) return ENGINE_SKILL_VERY_BAD; if (equity_loss < -ENGINE_SKILL_BAD_THRESHOLD) return ENGINE_SKILL_BAD; if (equity_loss < -ENGINE_SKILL_DOUBTFUL_THRESHOLD) return ENGINE_SKILL_DOUBTFUL; return ENGINE_SKILL_NONE; } /* Luck de gnubg (analysis.c l. 272), avec les seuils de arLuckLevel. */ engine_luck_rating engine_luck_rating_of(float luck) { if (luck > ENGINE_LUCK_VERY_THRESHOLD) return ENGINE_LUCK_VERY_LUCKY; if (luck > ENGINE_LUCK_THRESHOLD) return ENGINE_LUCK_LUCKY; if (luck < -ENGINE_LUCK_VERY_THRESHOLD) return ENGINE_LUCK_VERY_UNLUCKY; if (luck < -ENGINE_LUCK_THRESHOLD) return ENGINE_LUCK_UNLUCKY; return ENGINE_LUCK_NONE; } /* 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; } /* Copie une liste classée par gnubg, puis la libère. */ static engine_status copy_ranked_moves(movelist *list, const position *pos, engine_move *moves, int max_moves, int *count) { engine_status status = ENGINE_OK; unsigned int i; 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; } 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; 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); return copy_ranked_moves(&list, &pos, moves, max_moves, count); } engine_status engine_level_best_moves(const char *position_id, const char *match_id, int die1, int die2, int level, engine_move *moves, int max_moves, int *count) { position pos; movelist list; evalcontext context; engine_status status; 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_level(level, &context); if (status != ENGINE_OK) return status; /* * Comme l'ordinateur de gnubg quand c'est son tour (ComputerTurn dans * play.c, l. 1436-1453) : FindnSaveBestMoves sans coup à garder, sans * analyse et sans seuil, avec les réglages et le filtre du joueur, puis * RefreshMoveList ; il joue le premier coup de la liste. */ if (FindnSaveBestMoves(&list, die1, die2, (ConstTanBoard) pos.board, NULL, FALSE, 0.0f, &pos.cube, &context, move_filter_for_level(level)) < 0) return ENGINE_ERROR_INTERNAL; RefreshMoveList(&list, NULL); return copy_ranked_moves(&list, &pos, moves, max_moves, count); } /* ------------------------------------------------- Analyse d'un coup joué */ engine_status engine_analyse_move(const char *position_id, const char *match_id, int die1, int die2, const char *played_position_id, int ply, engine_move_analysis *analysis) { position pos; TanBoard played_board; positionkey played_key; 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; if (!is_base64_id(played_position_id, L_POSITIONID) || !PositionFromID(played_board, played_position_id)) return ENGINE_ERROR_MOVE; PositionKey((ConstTanBoard) played_board, &played_key); /* * Comme l'analyse de partie de gnubg (AnalyzeMove dans analysis.c, * l. 685-735) : FindnSaveBestMoves avec la clé du coup joué (évalué au * niveau le plus profond), fAnalyse vrai, seuil « douteux » et filtre * d'analyse (aamfAnalysis, « Normal » par défaut, gnubg.c l. 406) ; pas * de RefreshMoveList. Le rang est la place du coup joué dans cette * liste, et la perte rChequerSkill (l. 726) son équité moins celle du * premier. */ memset(analysis, 0, sizeof *analysis); if (FindnSaveBestMoves(&list, die1, die2, (ConstTanBoard) pos.board, &played_key, TRUE, DOUBTFUL_THRESHOLD, &pos.cube, &context, aaamfMoveFilterSettings[0]) < 0) return ENGINE_ERROR_INTERNAL; if (list.cMoves == 0) return ENGINE_OK; /* aucun coup possible : rien à analyser */ status = ENGINE_ERROR_MOVE; for (i = 0; i < list.cMoves; i++) if (EqualKeys(played_key, list.amMoves[i].key)) { analysis->move_count = (int) list.cMoves; analysis->rank = (int) i + 1; fill_move(&analysis->played, &list.amMoves[i], &pos, 1); fill_move(&analysis->best, &list.amMoves[0], &pos, 1); analysis->equity_loss = list.amMoves[i].rScore - list.amMoves[0].rScore; analysis->skill = engine_skill_of(analysis->equity_loss); status = ENGINE_OK; break; } 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" }; /* Décision de videau de la position avec ces réglages d'évaluation. */ static engine_status cube_decision(position *pos, const evalcontext *context, engine_cube *cube) { float output[2][NUM_ROLLOUT_OUTPUTS]; float equities[NUM_CUBEFUL_OUTPUTS]; cubedecision decision; int i; 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; } engine_status engine_cube_decision(const char *position_id, const char *match_id, int ply, engine_cube *cube) { position pos; evalcontext context; engine_status status; 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; return cube_decision(&pos, &context, cube); } engine_status engine_level_cube_decision(const char *position_id, const char *match_id, int level, engine_cube *cube) { position pos; evalcontext context; engine_status status; status = decode_position(position_id, match_id, &pos); if (status == ENGINE_OK) status = eval_context_for_level(level, &context); if (status != ENGINE_OK) return status; return cube_decision(&pos, &context, cube); } /* -------------------------------------------- Analyse d'une décision de videau */ /* Le plus petit de deux nombres. */ static float min_float(float a, float b) { return a < b ? a : b; } engine_status engine_analyse_cube(const char *position_id, const char *match_id, int ply, engine_cube_action action, engine_cube_analysis *analysis) { position pos; evalcontext context; engine_status status; int can_double; float take, pass, optimal; status = decode_position(position_id, match_id, &pos); if (status == ENGINE_OK) status = eval_context_for_ply(ply, &context); if (status == ENGINE_OK && (action < ENGINE_CUBE_ACTION_NO_DOUBLE || action > ENGINE_CUBE_ACTION_PASS)) status = ENGINE_ERROR_CUBE_ACTION; if (status != ENGINE_OK) return status; /* * Quand gnubg analyse-t-il la décision (AnalyzeMove dans analysis.c) ? * - ne pas doubler (cas MOVE_NORMAL, l. 655) : hors de la position de * départ, et si le joueur au trait a accès au videau (GetDPEq) ; * - doubler, prendre, passer (cas MOVE_DOUBLE, l. 760) : si GetDPEq le * permet, ou si le videau est au centre ou au joueur au trait. */ memset(analysis, 0, sizeof *analysis); can_double = GetDPEq(NULL, NULL, &pos.cube); if (action == ENGINE_CUBE_ACTION_NO_DOUBLE) analysis->analysed = can_double && strcmp(position_id, START_POSITION_ID) != 0; else analysis->analysed = can_double || pos.cube.fCubeOwner < 0 || pos.cube.fCubeOwner == pos.cube.fMove; if (!analysis->analysed) return ENGINE_OK; status = cube_decision(&pos, &context, &analysis->cube); if (status != ENGINE_OK) return status; /* * Pertes de gnubg (AnalyzeMove), à partir des équités de FindCubeDecision * vues du joueur au trait : * - ne pas doubler : sans double moins optimale (l. 672) ; * - doubler : la moins bonne des réponses moins optimale (l. 777) ; * - prendre : Skill(-doubleError) avec doubleError = prend moins passe * (l. 772 et 803), soit passe moins prend pour l'adversaire ; * - passer : Skill(doubleError) (l. 823), soit prend moins passe. * Les deux dernières ne sont des erreurs que négatives : un écart positif * est un bon choix, ramené à 0. */ take = analysis->cube.double_take; pass = analysis->cube.double_pass; optimal = analysis->cube.optimal; switch (action) { case ENGINE_CUBE_ACTION_NO_DOUBLE: analysis->equity_loss = analysis->cube.no_double - optimal; break; case ENGINE_CUBE_ACTION_DOUBLE: analysis->equity_loss = (take < pass ? take : pass) - optimal; break; case ENGINE_CUBE_ACTION_TAKE: analysis->equity_loss = min_float(0.0f, pass - take); break; case ENGINE_CUBE_ACTION_PASS: analysis->equity_loss = min_float(0.0f, take - pass); break; } analysis->skill = engine_skill_of(analysis->equity_loss); return ENGINE_OK; } /* ------------------------------------------------------- Chance d'un jet */ /* * Réglages de l'analyse de la chance : ecLuck de gnubg (analysis.c l. 89), * 0 pli avec videau, sans bruit. */ static const evalcontext luck_context = { .fCubeful = TRUE, .nPlies = 0, .fUsePrune = FALSE, .fDeterministic = TRUE, .rNoise = 0.0f }; /* * Équité, pour le joueur au trait (cube), après le meilleur coup du jet * die1, die2 à 0 pli ; s'il ne peut pas jouer, équité de la position vue * de l'adversaire (opponent), changée de signe. Repris de la boucle de * LuckNormal (analysis.c l. 213-243). Renvoie ERR_VAL en cas d'échec. */ static float equity_after_best_move(ConstTanBoard board, int die1, int die2, const cubeinfo *cube, cubeinfo *opponent) { TanBoard swapped; float output[NUM_ROLLOUT_OUTPUTS]; movelist list; float equity; if (FindnSaveBestMoves(&list, die1, die2, board, NULL, FALSE, 0.0f, cube, &luck_context, defaultFilters) < 0) { g_free(list.amMoves); return ERR_VAL; } if (list.cMoves) { equity = list.amMoves[0].rScore; g_free(list.amMoves); return equity; } memcpy(swapped, board, sizeof swapped); SwapSides(swapped); if (GeneralEvaluationE(output, (ConstTanBoard) swapped, opponent, &luck_context) < 0) return ERR_VAL; return -normalized_equity(output[OUTPUT_CUBEFUL_EQUITY], opponent); } /* * LuckNormal (analysis.c l. 200-247) : équité du jet moins la moyenne des * 36 jets. i et j sont les dés moins 1, i >= j. */ static float luck_normal(ConstTanBoard board, int i0, int j0, cubeinfo *cube) { cubeinfo opponent = *cube; float equities[6][6], mean = 0.0f; int i, j; opponent.fMove = !cube->fMove; for (i = 0; i < 6; i++) for (j = 0; j <= i; j++) { equities[i][j] = equity_after_best_move(board, i + 1, j + 1, cube, &opponent); if (equities[i][j] == ERR_VAL) return ERR_VAL; mean += i == j ? equities[i][j] : equities[i][j] * 2.0f; } return equities[i0][j0] - mean / 36.0f; } /* * LuckFirst (analysis.c l. 104-197) : au premier jet de la partie, les * doubles sont impossibles et le jet peut échoir à l'un ou l'autre joueur. * Équité du jet moins la moyenne des 30 cas : les 15 jets pour le joueur au * trait, et les 15 pour l'adversaire (meilleur coup de l'adversaire, changé * de signe). i et j sont les dés moins 1, i > j. */ static float luck_first(ConstTanBoard board, int i0, int j0, cubeinfo *cube) { cubeinfo opponent = *cube; TanBoard swapped; float equities[6][6], mean = 0.0f, equity; int i, j; opponent.fMove = !cube->fMove; for (i = 0; i < 6; i++) for (j = 0; j < i; j++) { equities[i][j] = equity_after_best_move(board, i + 1, j + 1, cube, &opponent); if (equities[i][j] == ERR_VAL) return ERR_VAL; mean += equities[i][j]; } /* L'adversaire au trait : son meilleur coup, vu de lui, changé de signe. * gnubg, s'il ne peut pas jouer (impossible au départ), reprend * l'évaluation du joueur au trait (l. 168-181). */ memcpy(swapped, board, sizeof swapped); SwapSides(swapped); for (i = 0; i < 6; i++) for (j = i + 1; j < 6; j++) { equity = equity_after_best_move((ConstTanBoard) swapped, i + 1, j + 1, &opponent, cube); if (equity == ERR_VAL) return ERR_VAL; equities[i][j] = -equity; mean += equities[i][j]; } return equities[i0][j0] - mean / 30.0f; } engine_status engine_luck_of(const char *position_id, const char *match_id, int die1, int die2, engine_luck *luck) { position pos; engine_status status; int high, low; float value; status = decode_position(position_id, match_id, &pos); if (status == ENGINE_OK) status = check_dice(die1, die2); if (status != ENGINE_OK) return status; start_evaluation(); /* LuckAnalysis (analysis.c l. 252-268) : dés rangés du plus fort au plus * faible ; LuckFirst à la position de départ hors doubles. */ high = (die1 > die2 ? die1 : die2) - 1; low = (die1 > die2 ? die2 : die1) - 1; if (strcmp(position_id, START_POSITION_ID) == 0 && high != low) value = luck_first((ConstTanBoard) pos.board, high, low, &pos.cube); else value = luck_normal((ConstTanBoard) pos.board, high, low, &pos.cube); if (value == ERR_VAL) return ENGINE_ERROR_INTERNAL; luck->luck = value; luck->rating = engine_luck_rating_of(value); 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 aucun pion 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; }