Files
Ustagammon/engine/engine.c
T
Pierre MARTINandClaude Opus 5.5 da5fa71ff8 Interface engine.h et programme engine-cli
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 <noreply@anthropic.com>
2026-11-02 13:56:29 +01:00

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