Moteur : niveaux de jeu, analyse d'un coup et du videau, chance d'un jet
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
b5d8eed262
commit
34c3eaeca5
+460
-25
@@ -26,8 +26,12 @@
|
||||
#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
|
||||
* 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"
|
||||
@@ -146,6 +150,12 @@ engine_status_message(engine_status status)
|
||||
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";
|
||||
}
|
||||
@@ -252,6 +262,97 @@ eval_context_for_ply(int ply, evalcontext *context)
|
||||
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)
|
||||
@@ -307,6 +408,25 @@ engine_legal_moves(const char *position_id, const char *match_id, int die1, int
|
||||
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)
|
||||
@@ -315,7 +435,6 @@ engine_best_moves(const char *position_id, const char *match_id, int die1, int d
|
||||
movelist list;
|
||||
evalcontext context;
|
||||
engine_status status;
|
||||
unsigned int i;
|
||||
|
||||
status = decode_position(position_id, match_id, &pos);
|
||||
if (status == ENGINE_OK)
|
||||
@@ -336,15 +455,94 @@ engine_best_moves(const char *position_id, const char *match_id, int die1, int d
|
||||
aaamfMoveFilterSettings[0]) < 0)
|
||||
return ENGINE_ERROR_INTERNAL;
|
||||
RefreshMoveList(&list, NULL);
|
||||
return copy_ranked_moves(&list, &pos, moves, max_moves, count);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
@@ -387,27 +585,19 @@ static const char *const cube_decision_names[] = {
|
||||
"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)
|
||||
/* 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)
|
||||
{
|
||||
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)
|
||||
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);
|
||||
decision = FindCubeDecision(equities, output, &pos->cube);
|
||||
if ((size_t) decision >= sizeof cube_decision_names / sizeof cube_decision_names[0])
|
||||
return ENGINE_ERROR_INTERNAL;
|
||||
|
||||
@@ -419,13 +609,258 @@ engine_cube_decision(const char *position_id, const char *match_id, int ply, eng
|
||||
cube->optimal = equities[OUTPUT_OPTIMAL];
|
||||
cube->decision = cube_decision_names[decision];
|
||||
cube->decision_text = GetCubeRecommendation(decision);
|
||||
GetDPEq(&cube->available, NULL, &pos.cube);
|
||||
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
|
||||
|
||||
+177
-1
@@ -48,11 +48,93 @@ typedef enum {
|
||||
ENGINE_ERROR_DICE, /* dés hors de 1..6 */
|
||||
ENGINE_ERROR_PLY, /* niveau hors de 0..ENGINE_MAX_PLY */
|
||||
ENGINE_ERROR_TOO_MANY_MOVES, /* tableau de coups trop petit */
|
||||
ENGINE_ERROR_INTERNAL /* échec signalé par gnubg */
|
||||
ENGINE_ERROR_INTERNAL, /* échec signalé par gnubg */
|
||||
ENGINE_ERROR_LEVEL, /* niveau de jeu inconnu */
|
||||
ENGINE_ERROR_MOVE, /* coup joué absent des coups légaux */
|
||||
ENGINE_ERROR_CUBE_ACTION /* décision de videau inconnue */
|
||||
} engine_status;
|
||||
|
||||
#define ENGINE_MAX_PLY 4
|
||||
|
||||
/*
|
||||
* Niveaux de jeu de l'ordinateur : les réglages prédéfinis de gnubg, dans
|
||||
* l'ordre de son tableau aecSettings (engine/gnubg/eval.c, l. 305-328, noms
|
||||
* de aszSettings). Les quatre premiers évaluent à 0 pli et ajoutent un
|
||||
* bruit (rNoise, écart type en équité) qui fait commettre des erreurs ; ce
|
||||
* bruit est « déterministe » (fDeterministic) : il est tiré d'un hachage MD5
|
||||
* de la position (Noise, eval.c l. 2241), donc la même position donne
|
||||
* toujours le même coup. Les suivants sont sans bruit, de 0 à 4 plis, avec
|
||||
* le filtre de coups « Normal » (aiSettingsMoveFilter, eval.c l. 331).
|
||||
*
|
||||
* Les anciennes versions de gnubg nommaient « expert » le niveau 0 pli et
|
||||
* « world class » (champion du monde) le niveau 2 plis.
|
||||
*/
|
||||
typedef enum {
|
||||
ENGINE_LEVEL_BEGINNER = 0, /* « beginner » : 0 pli, bruit 0,060 */
|
||||
ENGINE_LEVEL_CASUAL, /* « casual play » : 0 pli, bruit 0,050 */
|
||||
ENGINE_LEVEL_INTERMEDIATE, /* « intermediate » : 0 pli, bruit 0,040 */
|
||||
ENGINE_LEVEL_ADVANCED, /* « advanced » : 0 pli, bruit 0,015 */
|
||||
ENGINE_LEVEL_0PLY, /* « 0ply » (ancien « expert ») */
|
||||
ENGINE_LEVEL_1PLY, /* « 1ply » */
|
||||
ENGINE_LEVEL_2PLY, /* « 2ply » (ancien « world class ») */
|
||||
ENGINE_LEVEL_3PLY, /* « 3ply » */
|
||||
ENGINE_LEVEL_4PLY, /* « 4ply » */
|
||||
ENGINE_LEVEL_COUNT
|
||||
} engine_level;
|
||||
|
||||
/* Réglages d'un niveau, tels que gnubg les définit. */
|
||||
typedef struct {
|
||||
const char *name; /* identifiant : « beginner », « casual »,
|
||||
* « intermediate », « advanced », « 0ply »… « 4ply » */
|
||||
int plies; /* profondeur, en plis de gnubg */
|
||||
float noise; /* écart type du bruit (0 : aucun) */
|
||||
int deterministic; /* bruit tiré de la position, reproductible */
|
||||
int prune; /* élagage des réponses (gnubg : à partir d'1 pli) */
|
||||
} engine_level_settings;
|
||||
|
||||
/*
|
||||
* Appréciation d'un coup ou d'une décision de videau : seuils de gnubg sur
|
||||
* l'équité perdue, normalisée pour un videau de 1 (arSkillLevel dans
|
||||
* gnubg.c l. 280-285 et Skill dans analysis.c l. 288). Une perte
|
||||
* strictement plus grande que le seuil fait passer à la catégorie.
|
||||
*/
|
||||
#define ENGINE_SKILL_DOUBTFUL_THRESHOLD 0.03f /* douteux */
|
||||
#define ENGINE_SKILL_BAD_THRESHOLD 0.06f /* mauvais */
|
||||
#define ENGINE_SKILL_VERY_BAD_THRESHOLD 0.12f /* très mauvais */
|
||||
|
||||
typedef enum {
|
||||
ENGINE_SKILL_NONE = 0, /* rien à signaler */
|
||||
ENGINE_SKILL_DOUBTFUL, /* douteux */
|
||||
ENGINE_SKILL_BAD, /* mauvais */
|
||||
ENGINE_SKILL_VERY_BAD /* très mauvais */
|
||||
} engine_skill;
|
||||
|
||||
/*
|
||||
* Chance d'un jet : seuils de gnubg sur l'équité gagnée ou perdue par
|
||||
* rapport à la moyenne des jets (arLuckLevel dans gnubg.c l. 274-279 et Luck
|
||||
* dans analysis.c l. 272). Un écart strictement plus grand que le seuil
|
||||
* fait passer à la catégorie.
|
||||
*/
|
||||
#define ENGINE_LUCK_THRESHOLD 0.3f /* chanceux, malchanceux */
|
||||
#define ENGINE_LUCK_VERY_THRESHOLD 0.6f /* très chanceux, très malchanceux */
|
||||
|
||||
typedef enum {
|
||||
ENGINE_LUCK_VERY_UNLUCKY = 0, /* très malchanceux */
|
||||
ENGINE_LUCK_UNLUCKY, /* malchanceux */
|
||||
ENGINE_LUCK_NONE, /* ordinaire */
|
||||
ENGINE_LUCK_LUCKY, /* chanceux */
|
||||
ENGINE_LUCK_VERY_LUCKY /* très chanceux */
|
||||
} engine_luck_rating;
|
||||
|
||||
/* Décision de videau prise par un joueur. Les deux premières sont celles
|
||||
* du joueur au trait, les deux dernières sa réponse par l'adversaire. */
|
||||
typedef enum {
|
||||
ENGINE_CUBE_ACTION_NO_DOUBLE = 0,
|
||||
ENGINE_CUBE_ACTION_DOUBLE,
|
||||
ENGINE_CUBE_ACTION_TAKE,
|
||||
ENGINE_CUBE_ACTION_PASS
|
||||
} engine_cube_action;
|
||||
|
||||
/* Nombre maximal de coups légaux pour un jet (MAX_MOVES de gnubg). */
|
||||
#define ENGINE_MAX_MOVES 3060
|
||||
|
||||
@@ -124,6 +206,39 @@ typedef struct {
|
||||
* (double_take <= double_pass) */
|
||||
} engine_cube;
|
||||
|
||||
/* Analyse d'un coup joué, comme l'analyse de partie de gnubg. */
|
||||
typedef struct {
|
||||
int move_count; /* nombre de coups légaux (0 : aucun coup
|
||||
* possible, rien d'autre n'est rempli) */
|
||||
int rank; /* rang du coup joué dans le classement, à
|
||||
* partir de 1 */
|
||||
engine_move played; /* le coup joué, évalué */
|
||||
engine_move best; /* le meilleur coup, évalué */
|
||||
float equity_loss; /* équité du coup joué moins celle du meilleur :
|
||||
* 0 ou négative */
|
||||
engine_skill skill; /* appréciation de gnubg pour cette perte */
|
||||
} engine_move_analysis;
|
||||
|
||||
/* Analyse d'une décision de videau, comme l'analyse de partie de gnubg. */
|
||||
typedef struct {
|
||||
int analysed; /* gnubg analyse cette décision ; sinon (videau
|
||||
* inaccessible, position de départ…), perte
|
||||
* nulle et cube non rempli */
|
||||
engine_cube cube; /* la décision de videau du joueur au trait */
|
||||
float equity_loss; /* équité perdue par celui qui décide, du point
|
||||
* de vue de celui-ci, normalisée pour un videau
|
||||
* de 1 : 0 ou négative */
|
||||
engine_skill skill; /* appréciation de gnubg pour cette perte */
|
||||
} engine_cube_analysis;
|
||||
|
||||
/* Chance d'un jet, comme l'analyse de partie de gnubg. */
|
||||
typedef struct {
|
||||
float luck; /* équité gagnée (positive) ou perdue par ce jet
|
||||
* par rapport à la moyenne des jets, du point
|
||||
* de vue du joueur au trait */
|
||||
engine_luck_rating rating;
|
||||
} engine_luck;
|
||||
|
||||
/* Un camp sur le plateau, vu de lui-même (sa case 1 est la plus proche de
|
||||
* la sortie). */
|
||||
typedef struct {
|
||||
@@ -237,6 +352,67 @@ engine_status engine_evaluate(const char *position_id, const char *match_id,
|
||||
engine_status engine_cube_decision(const char *position_id, const char *match_id,
|
||||
int ply, engine_cube *cube);
|
||||
|
||||
/* ------------------------------------------------------ Niveaux de jeu */
|
||||
|
||||
/* Réglages d'un niveau (engine_level). */
|
||||
engine_status engine_level_settings_of(int level, engine_level_settings *settings);
|
||||
|
||||
/* Le niveau qui porte ce nom, ou ENGINE_ERROR_LEVEL. */
|
||||
engine_status engine_level_from_name(const char *name, int *level);
|
||||
|
||||
/*
|
||||
* Coups légaux classés comme les classe l'ordinateur de gnubg quand il joue
|
||||
* à ce niveau (ComputerTurn dans play.c : FindnSaveBestMoves sans analyse
|
||||
* ni seuil, filtre « Normal », puis RefreshMoveList) ; il joue le premier.
|
||||
* Les équités incluent le bruit du niveau.
|
||||
*/
|
||||
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);
|
||||
|
||||
/* Décision de videau évaluée avec les réglages de ce niveau, bruit compris. */
|
||||
engine_status engine_level_cube_decision(const char *position_id, const char *match_id,
|
||||
int level, engine_cube *cube);
|
||||
|
||||
/* ------------------------------------------------------------- Analyse */
|
||||
|
||||
/* Appréciation de gnubg pour une perte d'équité (0 ou négative). */
|
||||
engine_skill engine_skill_of(float equity_loss);
|
||||
|
||||
/* Catégorie de chance de gnubg pour une chance de jet. */
|
||||
engine_luck_rating engine_luck_rating_of(float luck);
|
||||
|
||||
/*
|
||||
* Analyse le coup joué par le joueur au trait avec le jet die1, die2, au
|
||||
* niveau ply, comme l'analyse de partie de gnubg (AnalyzeMove dans
|
||||
* analysis.c : FindnSaveBestMoves avec le coup joué, seuil « douteux »,
|
||||
* filtre « Normal », sans RefreshMoveList). played_position_id est la
|
||||
* position après le coup, vue du joueur qui vient de le jouer (champ
|
||||
* position_id d'engine_move). ENGINE_ERROR_MOVE si ce n'est pas un coup
|
||||
* légal ; si aucun coup n'est possible, move_count vaut 0.
|
||||
*/
|
||||
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);
|
||||
|
||||
/*
|
||||
* Analyse une décision de videau au niveau ply, comme l'analyse de partie de
|
||||
* gnubg (AnalyzeMove, cas MOVE_NORMAL, MOVE_DOUBLE, MOVE_TAKE et
|
||||
* MOVE_DROP). La position et le Match ID sont ceux du joueur au trait, qui
|
||||
* double ou non ; pour TAKE et PASS, la perte est celle de son adversaire.
|
||||
*/
|
||||
engine_status engine_analyse_cube(const char *position_id, const char *match_id, int ply,
|
||||
engine_cube_action action, engine_cube_analysis *analysis);
|
||||
|
||||
/*
|
||||
* Chance du jet die1, die2 pour le joueur au trait, comme l'analyse de
|
||||
* partie de gnubg (LuckAnalysis dans analysis.c : meilleur coup de chaque
|
||||
* jet à 0 pli, avec videau ; à la position de départ, le premier jet ne
|
||||
* peut pas être un double).
|
||||
*/
|
||||
engine_status engine_luck_of(const char *position_id, const char *match_id,
|
||||
int die1, int die2, engine_luck *luck);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user