Files
Ustagammon/engine-cli/main.c
T
Pierre MARTINandClaude Opus 5.5 b0030521d8 Moteur en WebAssembly : make engine-wasm et suite de contrat
Le moteur et engine-cli sont compilés avec Emscripten (image figée
emscripten/emsdk:3.1.64), en C générique, sans threads ; les données
de build/data/ sont incluses dans engine.data. Le JavaScript appelle
engine_init une fois puis main pour chaque commande : la sortie est le
JSON d'engine-cli, sans code de mise en forme dupliqué.

tests/test_wasm_reference.sh rejoue les 32 positions de référence sous
Node avec le même comparateur (option --cli) et la même tolérance.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 19:07:01 +01:00

404 lines
14 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
* reference POSITION_ID MATCH_ID [DÉS]
* un niveau au format des positions de
* référence (tests/reference/README.md) :
* évaluation, puis classement des coups
* avec DÉS, décision de videau sans
*
* --ply : niveau de gnubg, de 0 (défaut) à 4.
* --data : dossier des données ; par défaut « data » à côté du programme.
*
* En WebAssembly (engine-wasm/), le même programme sert d'interface : le
* JavaScript appelle main pour chaque commande, sans recharger le moteur.
* engine_init ne fait rien quand le moteur est déjà prêt, et main ne l'arrête
* pas à la fin.
*
* 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
/* Format des nombres : 9 chiffres significatifs redonnent exactement le
* float32 calculé par gnubg. */
#define NUMBER "%.9g"
/* Coups gardés dans le classement de la commande reference (comme hint). */
#define REFERENCE_MAX_MOVES 10
/* 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\": " NUMBER ", \"win_gammon\": " NUMBER ", \"win_backgammon\": " NUMBER ", "
"\"lose_gammon\": " NUMBER ", \"lose_backgammon\": " NUMBER "}",
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\": " NUMBER ", \"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");
}
/* Champs d'une décision de videau, sans les accolades. */
static void
print_cube_fields(const engine_cube *c)
{
printf("\"no_double\": " NUMBER ", \"double_take\": " NUMBER ", \"double_pass\": " NUMBER
", \"optimal\": " NUMBER ", \"decision\": \"%s\", \"decision_text\": \"%s\"",
c->no_double, c->double_take, c->double_pass, c->optimal, c->decision,
c->decision_text);
}
/* Début d'un niveau de référence : {"evaluation": {...} (objet laissé ouvert). */
static void
print_reference_evaluation(const engine_evaluation *e)
{
printf("{\"evaluation\": {\"probabilities\": ");
print_probabilities(e->probabilities);
printf(", \"equity\": " NUMBER "}", e->cubeless_equity);
}
/* 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\": " NUMBER ", \"cubeful_equity\": " NUMBER "}\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(", ");
print_cube_fields(&c);
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).
*/
static int
run_reference_command(const options *opt)
{
const char *position_id = opt->args[0], *match_id = opt->args[1];
engine_evaluation e;
engine_status status;
int die1, die2, count, i;
if (opt->arg_count != 2 && opt->arg_count != 3)
return usage_error("arguments attendus : POSITION_ID MATCH_ID [DÉS]");
if (opt->arg_count == 3 && !parse_dice(opt->args[2], &die1, &die2))
return usage_error("dés attendus sous la forme 31");
status = engine_evaluate(position_id, match_id, opt->ply, &e);
if (status != ENGINE_OK)
return print_engine_error(status);
if (opt->arg_count == 3) {
engine_move *moves = malloc(sizeof *moves * ENGINE_MAX_MOVES);
if (!moves)
return print_error("out_of_memory", "mémoire insuffisante", EXIT_ENGINE_ERROR);
status = engine_best_moves(position_id, match_id, die1, die2, opt->ply,
moves, ENGINE_MAX_MOVES, &count);
if (status != ENGINE_OK) {
free(moves);
return print_engine_error(status);
}
print_reference_evaluation(&e);
printf(", \"moves\": {\"legal_move_count\": %d, \"ranking\": [", count);
for (i = 0; i < count && i < REFERENCE_MAX_MOVES; i++) {
printf("%s{\"rank\": %d, \"move\": \"%s\", \"ply\": %d, \"equity\": " NUMBER
", \"equity_diff\": " NUMBER ", \"probabilities\": ",
i ? ", " : "", i + 1, moves[i].notation, moves[i].ply, moves[i].equity,
moves[i].equity - moves[0].equity);
print_probabilities(moves[i].probabilities);
printf("}");
}
printf("]}}\n");
free(moves);
} else {
engine_cube c;
status = engine_cube_decision(position_id, match_id, opt->ply, &c);
if (status != ENGINE_OK)
return print_engine_error(status);
print_reference_evaluation(&e);
printf(", \"cube\": {");
print_cube_fields(&c);
printf("}}\n");
}
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, eval ou reference";
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 if (strcmp(opt.command, "reference") == 0)
result = run_reference_command(&opt);
else
result = usage_error("commande inconnue : bestmove, moves, legal, cube, eval ou reference");
#ifndef __EMSCRIPTEN__
engine_shutdown();
#endif
return result;
}