1094 lines
36 KiB
C
1094 lines
36 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 » 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;
|
|
}
|
|
|
|
/* 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;
|
|
}
|
|
|
|
/* -------------------------------------------------------- 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). */
|
|
static engine_status
|
|
eval_context_for_level(int level, evalcontext *context)
|
|
{
|
|
if (level < 0 || level >= ENGINE_LEVEL_COUNT)
|
|
return ENGINE_ERROR_LEVEL;
|
|
*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;
|
|
|
|
/* 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;
|
|
}
|