Moteur : plateau, fin de partie, Match ID et saisie demi-coup par demi-coup

Ce dont le domaine d'une partie a besoin, sans réimplémenter de règle :
décodage d'un Position ID en plateau et position vue de l'adversaire,
résultat d'une partie finie (gammon, règle Jacoby), codage d'un Match ID,
vérification de demi-coups saisis un par un (la légalité vient de
GenerateMoves de gnubg), demi-coups de chaque coup légal, droit de doubler
et recommandations de double et de prise. Nouvelles commandes d'engine-cli
position, halfmoves et matchid, avec leurs tests natifs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Pierre MARTIN
2026-11-02 20:23:04 +01:00
co-authored by Claude Opus 5.5
parent 03b26ccba7
commit 6af82f73b0
4 changed files with 644 additions and 8 deletions
+202 -4
View File
@@ -13,6 +13,15 @@
* legal POSITION_ID MATCH_ID DÉS coups légaux, sans évaluation
* cube POSITION_ID MATCH_ID décision de videau
* eval POSITION_ID MATCH_ID évaluation de la position
* position POSITION_ID MATCH_ID plateau décodé, même position vue de
* l'adversaire, fin de partie éventuelle
* halfmoves POSITION_ID DÉS [DEMI-COUPS]
* vérifie des demi-coups saisis un par
* un (ex. « 8/5 6/5 », « bar/22 »,
* « 6/off »)
* matchid VIDEAU PROPRIÉTAIRE Match ID d'une partie libre avec la
* règle Jacoby, joueur 0 au trait ;
* PROPRIÉTAIRE : -1 (centre), 0 ou 1
* reference POSITION_ID MATCH_ID [DÉS]
* un niveau au format des positions de
* référence (tests/reference/README.md) :
@@ -45,6 +54,9 @@
* float32 calculé par gnubg. */
#define NUMBER "%.9g"
/* Liste des commandes, pour les messages d'erreur. */
#define COMMANDS "bestmove, moves, legal, cube, eval, position, halfmoves, matchid ou reference"
/* Coups gardés dans le classement de la commande reference (comme hint). */
#define REFERENCE_MAX_MOVES 10
@@ -113,10 +125,24 @@ print_probabilities(const float p[ENGINE_NUM_PROBABILITIES])
p[ENGINE_LOSE_GAMMON], p[ENGINE_LOSE_BACKGAMMON]);
}
/* Demi-coups sous la forme [[8, 5], [6, 5]]. */
static void
print_half_moves(const engine_half_move *half_moves, int count)
{
int i;
printf("[");
for (i = 0; i < count; i++)
printf("%s[%d, %d]", i ? ", " : "", half_moves[i].from, half_moves[i].to);
printf("]");
}
static void
print_move(const engine_move *m, int with_evaluation)
{
printf("{\"move\": \"%s\", \"position_id\": \"%s\"", m->notation, m->position_id);
printf("{\"move\": \"%s\", \"position_id\": \"%s\", \"half_moves\": ",
m->notation, m->position_id);
print_half_moves(m->half_moves, m->half_move_count);
if (with_evaluation) {
printf(", \"ply\": %d, \"equity\": " NUMBER ", \"probabilities\": ", m->ply, m->equity);
print_probabilities(m->probabilities);
@@ -254,11 +280,177 @@ run_position_command(const options *opt)
print_probabilities(c.probabilities);
printf(", ");
print_cube_fields(&c);
printf("}\n");
printf(", \"available\": %s, \"double\": %s, \"take\": %s}\n",
c.available ? "true" : "false", c.should_double ? "true" : "false",
c.should_take ? "true" : "false");
}
return EXIT_SUCCESS;
}
static void
print_side(const engine_side *side)
{
int i;
printf("{\"points\": [");
for (i = 0; i < 24; i++)
printf("%s%d", i ? ", " : "", side->points[i]);
printf("], \"bar\": %d, \"off\": %d, \"pip_count\": %d}", side->bar, side->off,
side->pip_count);
}
/* position : plateau, même position vue de l'adversaire, fin de partie. */
static int
run_board_command(const options *opt)
{
static const char *const kinds[] = { "", "single", "gammon", "backgammon" };
const char *position_id = opt->args[0], *match_id = opt->args[1];
engine_board board;
engine_result result;
engine_status status;
if (opt->arg_count != 2)
return usage_error("arguments attendus : POSITION_ID MATCH_ID");
status = engine_decode_position(position_id, &board);
if (status == ENGINE_OK)
status = engine_game_result(position_id, match_id, &result);
if (status != ENGINE_OK)
return print_engine_error(status);
printf("{\"position_id\": \"%s\", \"match_id\": \"%s\", \"on_roll\": ", position_id,
match_id);
print_side(&board.on_roll);
printf(", \"opponent\": ");
print_side(&board.opponent);
printf(", \"swapped_position_id\": \"%s\", \"result\": ", board.swapped_position_id);
if (result.over)
printf("{\"winner\": \"%s\", \"kind\": \"%s\", \"points\": %d}",
result.winner == 0 ? "on_roll" : "opponent", kinds[result.kind], result.points);
else
printf("null");
printf("}\n");
return EXIT_SUCCESS;
}
/* Lit une case d'un demi-coup : 1 à 25, « bar » (25) ou « off » (0). */
static int
parse_point(const char *text, size_t length, int *point)
{
char buffer[4];
char *end;
long value;
if (length == 0 || length >= sizeof buffer)
return 0;
memcpy(buffer, text, length);
buffer[length] = '\0';
if (strcmp(buffer, "bar") == 0) {
*point = ENGINE_BAR;
return 1;
}
if (strcmp(buffer, "off") == 0) {
*point = ENGINE_OFF;
return 1;
}
value = strtol(buffer, &end, 10);
if (*end || value < 1 || value > ENGINE_BAR)
return 0;
*point = (int) value;
return 1;
}
/* Lit des demi-coups séparés par des espaces, chacun « DÉPART/ARRIVÉE ».
* Renvoie leur nombre, ou -1 si le texte est mal formé. */
static int
parse_half_moves(const char *text, engine_half_move half_moves[ENGINE_MAX_HALF_MOVES])
{
int count = 0;
while (*text) {
const char *start, *slash;
size_t length;
while (*text == ' ')
text++;
if (!*text)
break;
start = text;
while (*text && *text != ' ')
text++;
length = (size_t) (text - start);
slash = memchr(start, '/', length);
if (!slash || count == ENGINE_MAX_HALF_MOVES
|| !parse_point(start, (size_t) (slash - start), &half_moves[count].from)
|| !parse_point(slash + 1, length - (size_t) (slash - start) - 1, &half_moves[count].to))
return -1;
count++;
}
return count;
}
/* halfmoves : vérifie des demi-coups saisis un par un. */
static int
run_half_moves_command(const options *opt)
{
const char *position_id = opt->args[0];
engine_half_move half_moves[ENGINE_MAX_HALF_MOVES];
engine_half_moves_check check;
engine_status status;
int die1, die2, count = 0;
if (opt->arg_count != 2 && opt->arg_count != 3)
return usage_error("arguments attendus : POSITION_ID DÉS [DEMI-COUPS]");
if (!parse_dice(opt->args[1], &die1, &die2))
return usage_error("dés attendus sous la forme 31");
if (opt->arg_count == 3)
count = parse_half_moves(opt->args[2], half_moves);
if (count < 0)
return usage_error("demi-coups attendus sous la forme 8/5 6/5, bar/22 ou 6/off");
status = engine_check_half_moves(position_id, die1, die2, half_moves, count, &check);
if (status != ENGINE_OK)
return print_engine_error(status);
printf("{\"position_id\": \"%s\", \"dice\": [%d, %d], \"half_moves\": ", position_id,
die1, die2);
print_half_moves(half_moves, count);
printf(", \"valid\": %s, \"complete\": %s, \"result_position_id\": ",
check.valid ? "true" : "false", check.complete ? "true" : "false");
if (check.valid)
printf("\"%s\"}\n", check.position_id);
else
printf("null}\n");
return EXIT_SUCCESS;
}
/* matchid : Match ID d'une partie libre avec la règle Jacoby, joueur 0 au
* trait. */
static int
run_match_id_command(const options *opt)
{
engine_match match = { .cube_value = 1, .cube_owner = -1, .jacoby = 1 };
char match_id[13];
char *end;
engine_status status;
if (opt->arg_count != 2)
return usage_error("arguments attendus : VIDEAU PROPRIÉTAIRE");
match.cube_value = (int) strtol(opt->args[0], &end, 10);
if (*end || end == opt->args[0])
return usage_error("VIDEAU attend un nombre");
match.cube_owner = (int) strtol(opt->args[1], &end, 10);
if (*end || end == opt->args[1])
return usage_error("PROPRIÉTAIRE attend -1, 0 ou 1");
status = engine_encode_match(&match, match_id);
if (status != ENGINE_OK)
return print_engine_error(status);
printf("{\"match_id\": \"%s\", \"match\": ", match_id);
print_match(&match);
printf("}\n");
return EXIT_SUCCESS;
}
/*
* reference : un niveau d'un fichier de résultat de tests/reference/, de
* même structure que plies["<n>"] (voir tests/reference/README.md).
@@ -359,7 +551,7 @@ parse_options(int argc, char **argv, options *opt)
}
}
if (!opt->command)
return "commande attendue : bestmove, moves, legal, cube, eval ou reference";
return "commande attendue : " COMMANDS;
return NULL;
}
@@ -393,8 +585,14 @@ main(int argc, char **argv)
result = run_position_command(&opt);
else if (strcmp(opt.command, "reference") == 0)
result = run_reference_command(&opt);
else if (strcmp(opt.command, "position") == 0)
result = run_board_command(&opt);
else if (strcmp(opt.command, "halfmoves") == 0)
result = run_half_moves_command(&opt);
else if (strcmp(opt.command, "matchid") == 0)
result = run_match_id_command(&opt);
else
result = usage_error("commande inconnue : bestmove, moves, legal, cube, eval ou reference");
result = usage_error("commande inconnue : " COMMANDS);
#ifndef __EMSCRIPTEN__
engine_shutdown();