791 lines
28 KiB
C
791 lines
28 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 | --level NIVEAU] [--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
|
|
* 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) :
|
|
* évaluation, puis classement des coups
|
|
* avec DÉS, décision de videau sans
|
|
* levels niveaux de jeu, seuils d'appréciation
|
|
* et de chance de gnubg
|
|
* analysemove POSITION_ID MATCH_ID DÉS POSITION_JOUÉE
|
|
* analyse du coup joué (POSITION_JOUÉE :
|
|
* position après le coup, vue du joueur
|
|
* qui l'a joué, comme position_id dans
|
|
* moves et legal)
|
|
* analysecube POSITION_ID MATCH_ID DÉCISION
|
|
* analyse d'une décision de videau :
|
|
* no_double ou double (joueur au trait),
|
|
* take ou pass (son adversaire)
|
|
* luck POSITION_ID MATCH_ID DÉS chance du jet pour le joueur au trait
|
|
*
|
|
* --ply : niveau de gnubg, de 0 (défaut) à 4 (meilleur coup comme « hint »,
|
|
* analyses comme l'analyse de partie de gnubg).
|
|
* --level : niveau de jeu de l'ordinateur (voir levels), pour bestmove, moves
|
|
* et cube : coups classés comme l'ordinateur de gnubg les joue, bruit compris.
|
|
* --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"
|
|
|
|
/* Liste des commandes, pour les messages d'erreur. */
|
|
#define COMMANDS "bestmove, moves, legal, cube, eval, position, halfmoves, matchid, reference, " \
|
|
"levels, analysemove, analysecube ou luck"
|
|
|
|
/* 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. */
|
|
#define MAX_ARGS 4
|
|
|
|
typedef struct {
|
|
int ply;
|
|
int level; /* -1 : pas de --level */
|
|
const char *data_dir;
|
|
const char *command;
|
|
const char *args[MAX_ARGS];
|
|
int arg_count;
|
|
} options;
|
|
|
|
/* Noms JSON des appréciations (engine_skill), des chances
|
|
* (engine_luck_rating) et des décisions de videau (engine_cube_action). */
|
|
static const char *const skill_names[] = { "none", "doubtful", "bad", "very_bad" };
|
|
static const char *const luck_names[] = {
|
|
"very_unlucky", "unlucky", "none", "lucky", "very_lucky"
|
|
};
|
|
static const char *const cube_action_names[] = { "no_double", "double", "take", "pass" };
|
|
|
|
/* 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";
|
|
case ENGINE_ERROR_LEVEL:
|
|
return "invalid_level";
|
|
case ENGINE_ERROR_MOVE:
|
|
return "invalid_move";
|
|
case ENGINE_ERROR_CUBE_ACTION:
|
|
return "invalid_cube_action";
|
|
}
|
|
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]);
|
|
}
|
|
|
|
/* 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\", \"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);
|
|
}
|
|
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);
|
|
}
|
|
|
|
/* « , "ply": N » ou « , "level": "NOM" », selon les options. */
|
|
static void
|
|
print_level_or_ply(const options *opt)
|
|
{
|
|
engine_level_settings settings;
|
|
|
|
if (opt->level >= 0 && engine_level_settings_of(opt->level, &settings) == ENGINE_OK)
|
|
printf(", \"level\": \"%s\"", settings.name);
|
|
else
|
|
printf(", \"ply\": %d", opt->ply);
|
|
}
|
|
|
|
/* 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 && opt->level >= 0)
|
|
status = engine_level_best_moves(position_id, match_id, die1, die2, opt->level,
|
|
moves, ENGINE_MAX_MOVES, &count);
|
|
else 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)
|
|
print_level_or_ply(opt);
|
|
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;
|
|
if (opt->level >= 0)
|
|
status = engine_level_cube_decision(position_id, match_id, opt->level, &c);
|
|
else
|
|
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\"", position_id, match_id);
|
|
print_level_or_ply(opt);
|
|
printf(", \"match\": ");
|
|
print_match(&match);
|
|
printf(", \"probabilities\": ");
|
|
print_probabilities(c.probabilities);
|
|
printf(", ");
|
|
print_cube_fields(&c);
|
|
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).
|
|
*/
|
|
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;
|
|
}
|
|
|
|
/* levels : niveaux de jeu, seuils d'appréciation et de chance. */
|
|
static int
|
|
run_levels_command(const options *opt)
|
|
{
|
|
engine_level_settings s;
|
|
int level;
|
|
|
|
if (opt->arg_count != 0)
|
|
return usage_error("levels n'attend pas d'argument");
|
|
printf("{\"levels\": [");
|
|
for (level = 0; level < ENGINE_LEVEL_COUNT; level++) {
|
|
engine_level_settings_of(level, &s);
|
|
printf("%s{\"name\": \"%s\", \"plies\": %d, \"noise\": " NUMBER
|
|
", \"deterministic\": %s, \"prune\": %s}", level ? ", " : "", s.name, s.plies,
|
|
s.noise, s.deterministic ? "true" : "false", s.prune ? "true" : "false");
|
|
}
|
|
printf("], \"skill_thresholds\": {\"doubtful\": " NUMBER ", \"bad\": " NUMBER
|
|
", \"very_bad\": " NUMBER "}, \"luck_thresholds\": {\"lucky\": " NUMBER
|
|
", \"very_lucky\": " NUMBER "}}\n",
|
|
ENGINE_SKILL_DOUBTFUL_THRESHOLD, ENGINE_SKILL_BAD_THRESHOLD,
|
|
ENGINE_SKILL_VERY_BAD_THRESHOLD, ENGINE_LUCK_THRESHOLD, ENGINE_LUCK_VERY_THRESHOLD);
|
|
return EXIT_SUCCESS;
|
|
}
|
|
|
|
/* analysemove : analyse du coup joué. */
|
|
static int
|
|
run_analyse_move_command(const options *opt)
|
|
{
|
|
const char *position_id = opt->args[0], *match_id = opt->args[1];
|
|
const char *played_id = opt->args[3];
|
|
engine_move_analysis a;
|
|
engine_status status;
|
|
int die1, die2;
|
|
|
|
if (opt->arg_count != 4)
|
|
return usage_error("arguments attendus : POSITION_ID MATCH_ID DÉS POSITION_JOUÉE");
|
|
if (!parse_dice(opt->args[2], &die1, &die2))
|
|
return usage_error("dés attendus sous la forme 31");
|
|
status = engine_analyse_move(position_id, match_id, die1, die2, played_id, opt->ply, &a);
|
|
if (status != ENGINE_OK)
|
|
return print_engine_error(status);
|
|
|
|
printf("{\"position_id\": \"%s\", \"match_id\": \"%s\", \"dice\": [%d, %d], \"ply\": %d, "
|
|
"\"played_position_id\": \"%s\", \"move_count\": %d",
|
|
position_id, match_id, die1, die2, opt->ply, played_id, a.move_count);
|
|
if (a.move_count == 0) {
|
|
printf(", \"rank\": null, \"played\": null, \"best\": null");
|
|
} else {
|
|
printf(", \"rank\": %d, \"played\": ", a.rank);
|
|
print_move(&a.played, 1);
|
|
printf(", \"best\": ");
|
|
print_move(&a.best, 1);
|
|
}
|
|
printf(", \"equity_loss\": " NUMBER ", \"skill\": \"%s\"}\n", a.equity_loss,
|
|
skill_names[a.skill]);
|
|
return EXIT_SUCCESS;
|
|
}
|
|
|
|
/* analysecube : analyse d'une décision de videau. */
|
|
static int
|
|
run_analyse_cube_command(const options *opt)
|
|
{
|
|
const char *position_id = opt->args[0], *match_id = opt->args[1];
|
|
engine_cube_analysis a;
|
|
engine_status status;
|
|
int action;
|
|
|
|
if (opt->arg_count != 3)
|
|
return usage_error("arguments attendus : POSITION_ID MATCH_ID DÉCISION");
|
|
for (action = 0; action < 4 && strcmp(opt->args[2], cube_action_names[action]) != 0; action++)
|
|
;
|
|
if (action == 4)
|
|
return usage_error("DÉCISION attend no_double, double, take ou pass");
|
|
status = engine_analyse_cube(position_id, match_id, opt->ply, (engine_cube_action) action, &a);
|
|
if (status != ENGINE_OK)
|
|
return print_engine_error(status);
|
|
|
|
printf("{\"position_id\": \"%s\", \"match_id\": \"%s\", \"ply\": %d, \"action\": \"%s\", "
|
|
"\"analysed\": %s, \"cube\": ", position_id, match_id, opt->ply,
|
|
cube_action_names[action], a.analysed ? "true" : "false");
|
|
if (a.analysed) {
|
|
printf("{\"probabilities\": ");
|
|
print_probabilities(a.cube.probabilities);
|
|
printf(", ");
|
|
print_cube_fields(&a.cube);
|
|
printf("}");
|
|
} else {
|
|
printf("null");
|
|
}
|
|
printf(", \"equity_loss\": " NUMBER ", \"skill\": \"%s\"}\n", a.equity_loss,
|
|
skill_names[a.skill]);
|
|
return EXIT_SUCCESS;
|
|
}
|
|
|
|
/* luck : chance du jet pour le joueur au trait. */
|
|
static int
|
|
run_luck_command(const options *opt)
|
|
{
|
|
const char *position_id = opt->args[0], *match_id = opt->args[1];
|
|
engine_luck luck;
|
|
engine_status status;
|
|
int die1, die2;
|
|
|
|
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");
|
|
status = engine_luck_of(position_id, match_id, die1, die2, &luck);
|
|
if (status != ENGINE_OK)
|
|
return print_engine_error(status);
|
|
printf("{\"position_id\": \"%s\", \"match_id\": \"%s\", \"dice\": [%d, %d], \"luck\": " NUMBER
|
|
", \"rating\": \"%s\"}\n", position_id, match_id, die1, die2, luck.luck,
|
|
luck_names[luck.rating]);
|
|
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;
|
|
|
|
int has_ply = 0;
|
|
|
|
memset(opt, 0, sizeof *opt);
|
|
opt->level = -1;
|
|
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";
|
|
has_ply = 1;
|
|
} else if (strcmp(argv[i], "--level") == 0 && i + 1 < argc) {
|
|
if (engine_level_from_name(argv[++i], &opt->level) != ENGINE_OK)
|
|
return "--level attend un niveau : beginner, casual, intermediate, advanced, "
|
|
"0ply, 1ply, 2ply, 3ply ou 4ply";
|
|
} 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 < MAX_ARGS) {
|
|
opt->args[opt->arg_count++] = argv[i];
|
|
} else {
|
|
return "trop d'arguments";
|
|
}
|
|
}
|
|
if (!opt->command)
|
|
return "commande attendue : " COMMANDS;
|
|
if (opt->level >= 0 && has_ply)
|
|
return "--ply et --level ne vont pas ensemble";
|
|
if (opt->level >= 0 && strcmp(opt->command, "bestmove") != 0
|
|
&& strcmp(opt->command, "moves") != 0 && strcmp(opt->command, "cube") != 0)
|
|
return "--level ne s'applique qu'à bestmove, moves et cube";
|
|
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 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 if (strcmp(opt.command, "levels") == 0)
|
|
result = run_levels_command(&opt);
|
|
else if (strcmp(opt.command, "analysemove") == 0)
|
|
result = run_analyse_move_command(&opt);
|
|
else if (strcmp(opt.command, "analysecube") == 0)
|
|
result = run_analyse_cube_command(&opt);
|
|
else if (strcmp(opt.command, "luck") == 0)
|
|
result = run_luck_command(&opt);
|
|
else
|
|
result = usage_error("commande inconnue : " COMMANDS);
|
|
|
|
#ifndef __EMSCRIPTEN__
|
|
engine_shutdown();
|
|
#endif
|
|
return result;
|
|
}
|