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>
307 lines
9.8 KiB
C
307 lines
9.8 KiB
C
/*
|
|
* engine-cli -- interroge le moteur d'Ustagammon en ligne de commande.
|
|
*
|
|
* Copyright (C) 2026 les auteurs d'Ustagammon.
|
|
* Licence : GPLv3 ou ultérieure (voir LICENSE).
|
|
*
|
|
* Usage :
|
|
* engine-cli [--ply N] [--data DOSSIER] COMMANDE ARGUMENTS
|
|
*
|
|
* Commandes :
|
|
* bestmove POSITION_ID MATCH_ID DÉS meilleur coup (DÉS : ex. 31)
|
|
* moves POSITION_ID MATCH_ID DÉS coups légaux classés, évalués
|
|
* 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
|
|
*
|
|
* --ply : niveau de gnubg, de 0 (défaut) à 4.
|
|
* --data : dossier des données ; par défaut « data » à côté du programme.
|
|
*
|
|
* Sortie : un objet JSON sur la sortie standard. En cas d'erreur, un objet
|
|
* {"error": {"code": ..., "message": ...}} et un code de sortie non nul :
|
|
* 1 pour une erreur du moteur, 2 pour une erreur d'utilisation.
|
|
*/
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include "engine.h"
|
|
|
|
#define EXIT_ENGINE_ERROR 1
|
|
#define EXIT_USAGE_ERROR 2
|
|
|
|
/* Arguments de la ligne de commande, une fois lus. */
|
|
typedef struct {
|
|
int ply;
|
|
const char *data_dir;
|
|
const char *command;
|
|
const char *args[3];
|
|
int arg_count;
|
|
} options;
|
|
|
|
/* Code d'erreur JSON pour chaque engine_status. */
|
|
static const char *
|
|
status_code(engine_status status)
|
|
{
|
|
switch (status) {
|
|
case ENGINE_OK:
|
|
return "ok";
|
|
case ENGINE_ERROR_NOT_INITIALISED:
|
|
return "not_initialised";
|
|
case ENGINE_ERROR_DATA:
|
|
return "invalid_data";
|
|
case ENGINE_ERROR_POSITION_ID:
|
|
return "invalid_position_id";
|
|
case ENGINE_ERROR_MATCH_ID:
|
|
return "invalid_match_id";
|
|
case ENGINE_ERROR_DICE:
|
|
return "invalid_dice";
|
|
case ENGINE_ERROR_PLY:
|
|
return "invalid_ply";
|
|
case ENGINE_ERROR_TOO_MANY_MOVES:
|
|
return "too_many_moves";
|
|
case ENGINE_ERROR_INTERNAL:
|
|
return "internal_error";
|
|
}
|
|
return "unknown_error";
|
|
}
|
|
|
|
/* Les messages sont des constantes sans guillemets : pas d'échappement. */
|
|
static int
|
|
print_error(const char *code, const char *message, int exit_code)
|
|
{
|
|
printf("{\"error\": {\"code\": \"%s\", \"message\": \"%s\"}}\n", code, message);
|
|
return exit_code;
|
|
}
|
|
|
|
static int
|
|
print_engine_error(engine_status status)
|
|
{
|
|
return print_error(status_code(status), engine_status_message(status), EXIT_ENGINE_ERROR);
|
|
}
|
|
|
|
static int
|
|
usage_error(const char *message)
|
|
{
|
|
return print_error("usage", message, EXIT_USAGE_ERROR);
|
|
}
|
|
|
|
static void
|
|
print_probabilities(const float p[ENGINE_NUM_PROBABILITIES])
|
|
{
|
|
printf("{\"win\": %.6f, \"win_gammon\": %.6f, \"win_backgammon\": %.6f, "
|
|
"\"lose_gammon\": %.6f, \"lose_backgammon\": %.6f}",
|
|
p[ENGINE_WIN], p[ENGINE_WIN_GAMMON], p[ENGINE_WIN_BACKGAMMON],
|
|
p[ENGINE_LOSE_GAMMON], p[ENGINE_LOSE_BACKGAMMON]);
|
|
}
|
|
|
|
static void
|
|
print_move(const engine_move *m, int with_evaluation)
|
|
{
|
|
printf("{\"move\": \"%s\", \"position_id\": \"%s\"", m->notation, m->position_id);
|
|
if (with_evaluation) {
|
|
printf(", \"ply\": %d, \"equity\": %.6f, \"probabilities\": ", m->ply, m->equity);
|
|
print_probabilities(m->probabilities);
|
|
}
|
|
printf("}");
|
|
}
|
|
|
|
static void
|
|
print_match(const engine_match *m)
|
|
{
|
|
printf("{\"match_length\": %d, \"score\": [%d, %d], \"cube_value\": %d, "
|
|
"\"cube_owner\": %d, \"player_on_roll\": %d, \"crawford\": %s, \"jacoby\": %s}",
|
|
m->match_length, m->score[0], m->score[1], m->cube_value, m->cube_owner,
|
|
m->player_on_roll, m->crawford ? "true" : "false", m->jacoby ? "true" : "false");
|
|
}
|
|
|
|
/* Lit des dés au format « 31 » ; renvoie 0 si le format est faux. */
|
|
static int
|
|
parse_dice(const char *text, int *die1, int *die2)
|
|
{
|
|
if (strlen(text) != 2 || text[0] < '0' || text[0] > '9' || text[1] < '0' || text[1] > '9')
|
|
return 0;
|
|
*die1 = text[0] - '0';
|
|
*die2 = text[1] - '0';
|
|
return 1;
|
|
}
|
|
|
|
/* bestmove, moves et legal. */
|
|
static int
|
|
run_moves_command(const options *opt)
|
|
{
|
|
const char *position_id = opt->args[0], *match_id = opt->args[1];
|
|
int best_only = strcmp(opt->command, "bestmove") == 0;
|
|
int evaluate = strcmp(opt->command, "legal") != 0;
|
|
int die1, die2, count, i;
|
|
engine_move *moves;
|
|
engine_status status;
|
|
|
|
if (opt->arg_count != 3)
|
|
return usage_error("arguments attendus : POSITION_ID MATCH_ID DÉS");
|
|
if (!parse_dice(opt->args[2], &die1, &die2))
|
|
return usage_error("dés attendus sous la forme 31");
|
|
|
|
moves = malloc(sizeof *moves * ENGINE_MAX_MOVES);
|
|
if (!moves)
|
|
return print_error("out_of_memory", "mémoire insuffisante", EXIT_ENGINE_ERROR);
|
|
if (evaluate)
|
|
status = engine_best_moves(position_id, match_id, die1, die2, opt->ply,
|
|
moves, ENGINE_MAX_MOVES, &count);
|
|
else
|
|
status = engine_legal_moves(position_id, match_id, die1, die2,
|
|
moves, ENGINE_MAX_MOVES, &count);
|
|
if (status != ENGINE_OK) {
|
|
free(moves);
|
|
return print_engine_error(status);
|
|
}
|
|
|
|
printf("{\"position_id\": \"%s\", \"match_id\": \"%s\", \"dice\": [%d, %d]",
|
|
position_id, match_id, die1, die2);
|
|
if (evaluate)
|
|
printf(", \"ply\": %d", opt->ply);
|
|
if (best_only) {
|
|
printf(", \"best_move\": ");
|
|
if (count > 0)
|
|
print_move(&moves[0], 1);
|
|
else
|
|
printf("null");
|
|
} else {
|
|
printf(", \"count\": %d, \"moves\": [", count);
|
|
for (i = 0; i < count; i++) {
|
|
printf(i ? ", " : "");
|
|
print_move(&moves[i], evaluate);
|
|
}
|
|
printf("]");
|
|
}
|
|
printf("}\n");
|
|
free(moves);
|
|
return EXIT_SUCCESS;
|
|
}
|
|
|
|
/* eval et cube. */
|
|
static int
|
|
run_position_command(const options *opt)
|
|
{
|
|
const char *position_id = opt->args[0], *match_id = opt->args[1];
|
|
engine_match match;
|
|
engine_status status;
|
|
|
|
if (opt->arg_count != 2)
|
|
return usage_error("arguments attendus : POSITION_ID MATCH_ID");
|
|
status = engine_decode_match(match_id, &match);
|
|
if (status != ENGINE_OK)
|
|
return print_engine_error(status);
|
|
|
|
if (strcmp(opt->command, "eval") == 0) {
|
|
engine_evaluation e;
|
|
status = engine_evaluate(position_id, match_id, opt->ply, &e);
|
|
if (status != ENGINE_OK)
|
|
return print_engine_error(status);
|
|
printf("{\"position_id\": \"%s\", \"match_id\": \"%s\", \"ply\": %d, \"match\": ",
|
|
position_id, match_id, opt->ply);
|
|
print_match(&match);
|
|
printf(", \"probabilities\": ");
|
|
print_probabilities(e.probabilities);
|
|
printf(", \"cubeless_equity\": %.6f, \"cubeful_equity\": %.6f}\n",
|
|
e.cubeless_equity, e.cubeful_equity);
|
|
} else {
|
|
engine_cube c;
|
|
status = engine_cube_decision(position_id, match_id, opt->ply, &c);
|
|
if (status != ENGINE_OK)
|
|
return print_engine_error(status);
|
|
printf("{\"position_id\": \"%s\", \"match_id\": \"%s\", \"ply\": %d, \"match\": ",
|
|
position_id, match_id, opt->ply);
|
|
print_match(&match);
|
|
printf(", \"probabilities\": ");
|
|
print_probabilities(c.probabilities);
|
|
printf(", \"no_double\": %.6f, \"double_take\": %.6f, \"double_pass\": %.6f, "
|
|
"\"decision\": \"%s\"}\n", c.no_double, c.double_take, c.double_pass, c.decision);
|
|
}
|
|
return EXIT_SUCCESS;
|
|
}
|
|
|
|
/* Dossier « data » à côté du programme. */
|
|
static char *
|
|
default_data_dir(const char *program)
|
|
{
|
|
const char *slash = strrchr(program, '/');
|
|
size_t dir_length = slash ? (size_t) (slash - program) : 1;
|
|
char *dir = malloc(dir_length + sizeof "/data");
|
|
|
|
if (!dir)
|
|
return NULL;
|
|
if (slash)
|
|
memcpy(dir, program, dir_length);
|
|
else
|
|
dir[0] = '.';
|
|
strcpy(dir + dir_length, "/data");
|
|
return dir;
|
|
}
|
|
|
|
/* Lit les options ; renvoie un message d'erreur, ou NULL si tout va bien. */
|
|
static const char *
|
|
parse_options(int argc, char **argv, options *opt)
|
|
{
|
|
int i;
|
|
|
|
memset(opt, 0, sizeof *opt);
|
|
for (i = 1; i < argc; i++) {
|
|
if (strcmp(argv[i], "--ply") == 0 && i + 1 < argc) {
|
|
char *end;
|
|
opt->ply = (int) strtol(argv[++i], &end, 10);
|
|
if (*end || end == argv[i])
|
|
return "--ply attend un nombre";
|
|
} else if (strcmp(argv[i], "--data") == 0 && i + 1 < argc) {
|
|
opt->data_dir = argv[++i];
|
|
} else if (argv[i][0] == '-' && argv[i][1] == '-') {
|
|
return "option inconnue ou sans valeur";
|
|
} else if (!opt->command) {
|
|
opt->command = argv[i];
|
|
} else if (opt->arg_count < 3) {
|
|
opt->args[opt->arg_count++] = argv[i];
|
|
} else {
|
|
return "trop d'arguments";
|
|
}
|
|
}
|
|
if (!opt->command)
|
|
return "commande attendue : bestmove, moves, legal, cube ou eval";
|
|
return NULL;
|
|
}
|
|
|
|
int
|
|
main(int argc, char **argv)
|
|
{
|
|
options opt;
|
|
const char *problem = parse_options(argc, argv, &opt);
|
|
char *data_dir = NULL;
|
|
engine_status status;
|
|
int result;
|
|
|
|
if (problem)
|
|
return usage_error(problem);
|
|
if (!opt.data_dir) {
|
|
data_dir = default_data_dir(argv[0]);
|
|
if (!data_dir)
|
|
return print_error("out_of_memory", "mémoire insuffisante", EXIT_ENGINE_ERROR);
|
|
opt.data_dir = data_dir;
|
|
}
|
|
|
|
status = engine_init(opt.data_dir);
|
|
free(data_dir);
|
|
if (status != ENGINE_OK)
|
|
return print_engine_error(status);
|
|
|
|
if (strcmp(opt.command, "bestmove") == 0 || strcmp(opt.command, "moves") == 0
|
|
|| strcmp(opt.command, "legal") == 0)
|
|
result = run_moves_command(&opt);
|
|
else if (strcmp(opt.command, "eval") == 0 || strcmp(opt.command, "cube") == 0)
|
|
result = run_position_command(&opt);
|
|
else
|
|
result = usage_error("commande inconnue : bestmove, moves, legal, cube ou eval");
|
|
|
|
engine_shutdown();
|
|
return result;
|
|
}
|