Files

659 lines
21 KiB
C

/*
* 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 <stdio.h>
#include <string.h>
#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 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;
}