diff --git a/engine-cli/main.c b/engine-cli/main.c index 38ac0c2..bd05cda 100644 --- a/engine-cli/main.c +++ b/engine-cli/main.c @@ -19,9 +19,19 @@ * 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 ; + * matchid VIDEAU PROPRIÉTAIRE [LONGUEUR SCORE0 SCORE1 CRAWFORD] + * Match ID joueur 0 au trait : partie + * libre avec la règle Jacoby, ou match + * en LONGUEUR points (CRAWFORD : 1 + * pour la partie Crawford, sinon 0) ; * PROPRIÉTAIRE : -1 (centre), 0 ou 1 + * double POSITION_ID MATCH_ID (avec --level) l'ordinateur au trait + * double-t-il ? (ComputerTurn de gnubg) + * take POSITION_ID MATCH_ID (avec --level) l'ordinateur prend-il + * le double du joueur au trait ? + * aftergame MATCH_ID GAGNANT POINTS le match après une partie : score, + * fin du match, partie Crawford + * suivante * reference POSITION_ID MATCH_ID [DÉS] * un niveau au format des positions de * référence (tests/reference/README.md) : @@ -42,8 +52,9 @@ * * --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. + * --level : niveau de jeu de l'ordinateur (voir levels), pour bestmove, moves, + * cube, double et take : coups classés et videau 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 @@ -71,13 +82,13 @@ /* Liste des commandes, pour les messages d'erreur. */ #define COMMANDS "bestmove, moves, legal, cube, eval, position, halfmoves, matchid, reference, " \ - "levels, analysemove, analysecube ou luck" + "levels, analysemove, analysecube, luck, double, take ou aftergame" /* 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 +#define MAX_ARGS 6 typedef struct { int ply; @@ -125,6 +136,8 @@ status_code(engine_status status) return "invalid_move"; case ENGINE_ERROR_CUBE_ACTION: return "invalid_cube_action"; + case ENGINE_ERROR_RESULT: + return "invalid_result"; } return "unknown_error"; } @@ -475,24 +488,45 @@ run_half_moves_command(const options *opt) return EXIT_SUCCESS; } -/* matchid : Match ID d'une partie libre avec la règle Jacoby, joueur 0 au - * trait. */ +/* Lit un entier ; renvoie 0 si le texte n'en est pas un. */ +static int +parse_int(const char *text, int *value) +{ + char *end; + + *value = (int) strtol(text, &end, 10); + return !*end && end != text; +} + +/* matchid : Match ID joueur 0 au trait, sans dés. Partie libre avec la + * règle Jacoby, ou match (LONGUEUR SCORE0 SCORE1 CRAWFORD, ce dernier à 0 + * ou 1), sans Jacoby. */ 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]) + if (opt->arg_count != 2 && opt->arg_count != 6) + return usage_error("arguments attendus : VIDEAU PROPRIÉTAIRE " + "[LONGUEUR SCORE0 SCORE1 CRAWFORD]"); + if (!parse_int(opt->args[0], &match.cube_value)) return usage_error("VIDEAU attend un nombre"); - match.cube_owner = (int) strtol(opt->args[1], &end, 10); - if (*end || end == opt->args[1]) + if (!parse_int(opt->args[1], &match.cube_owner)) return usage_error("PROPRIÉTAIRE attend -1, 0 ou 1"); + if (opt->arg_count == 6) { + if (!parse_int(opt->args[2], &match.match_length) || match.match_length < 1 + || !parse_int(opt->args[3], &match.score[0]) || match.score[0] < 0 + || !parse_int(opt->args[4], &match.score[1]) || match.score[1] < 0 + || match.score[0] >= match.match_length || match.score[1] >= match.match_length) + return usage_error("LONGUEUR attend un nombre positif, SCORE0 et SCORE1 des " + "scores inférieurs à la longueur"); + if (!parse_int(opt->args[5], &match.crawford) || match.crawford < 0 + || match.crawford > 1) + return usage_error("CRAWFORD attend 0 ou 1"); + match.jacoby = 0; + } status = engine_encode_match(&match, match_id); if (status != ENGINE_OK) @@ -503,8 +537,67 @@ run_match_id_command(const options *opt) return EXIT_SUCCESS; } +/* double et take (avec --level) : le videau de l'ordinateur, comme + * ComputerTurn de gnubg (voir engine.h). */ +static int +run_level_cube_command(const options *opt) +{ + const char *position_id = opt->args[0], *match_id = opt->args[1]; + engine_status status; + + if (opt->arg_count != 2) + return usage_error("arguments attendus : POSITION_ID MATCH_ID"); + if (opt->level < 0) + return usage_error("double et take demandent --level"); + if (strcmp(opt->command, "double") == 0) { + engine_level_double d; + status = engine_level_double_decision(position_id, match_id, opt->level, &d); + 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(", \"available\": %s, \"market_window\": %s, \"double\": %s, \"decision\": ", + d.available ? "true" : "false", d.market_window ? "true" : "false", + d.doubles ? "true" : "false"); + if (d.decision) + printf("\"%s\"}\n", d.decision); + else + printf("null}\n"); + } else { + int takes; + const char *decision; + status = engine_level_take_decision(position_id, match_id, opt->level, &takes, &decision); + 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(", \"take\": %s, \"decision\": \"%s\"}\n", takes ? "true" : "false", decision); + } + return EXIT_SUCCESS; +} + +/* aftergame : le match après une partie (voir engine_match_after_game). */ +static int +run_after_game_command(const options *opt) +{ + engine_match_after after; + engine_status status; + int winner, points; + + if (opt->arg_count != 3) + return usage_error("arguments attendus : MATCH_ID GAGNANT POINTS"); + if (!parse_int(opt->args[1], &winner) || !parse_int(opt->args[2], &points)) + return usage_error("GAGNANT et POINTS attendent des nombres"); + status = engine_match_after_game(opt->args[0], winner, points, &after); + if (status != ENGINE_OK) + return print_engine_error(status); + printf("{\"score\": [%d, %d], \"match_over\": %s, \"crawford\": %s}\n", after.score[0], + after.score[1], after.match_over ? "true" : "false", after.crawford ? "true" : "false"); + return EXIT_SUCCESS; +} + /* - * reference : un niveau d'un fichier de résultat de tests/reference/, de + * reference :un niveau d'un fichier de résultat de tests/reference/, de * même structure que plies[""] (voir tests/reference/README.md). */ static int @@ -731,8 +824,9 @@ parse_options(int argc, char **argv, options *opt) 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"; + && strcmp(opt->command, "moves") != 0 && strcmp(opt->command, "cube") != 0 + && strcmp(opt->command, "double") != 0 && strcmp(opt->command, "take") != 0) + return "--level ne s'applique qu'à bestmove, moves, cube, double et take"; return NULL; } @@ -772,6 +866,10 @@ main(int argc, char **argv) result = run_half_moves_command(&opt); else if (strcmp(opt.command, "matchid") == 0) result = run_match_id_command(&opt); + else if (strcmp(opt.command, "double") == 0 || strcmp(opt.command, "take") == 0) + result = run_level_cube_command(&opt); + else if (strcmp(opt.command, "aftergame") == 0) + result = run_after_game_command(&opt); else if (strcmp(opt.command, "levels") == 0) result = run_levels_command(&opt); else if (strcmp(opt.command, "analysemove") == 0) diff --git a/engine/engine.c b/engine/engine.c index 01ed5f5..d038d74 100644 --- a/engine/engine.c +++ b/engine/engine.c @@ -156,6 +156,8 @@ engine_status_message(engine_status status) return "coup joué absent des coups légaux"; case ENGINE_ERROR_CUBE_ACTION: return "décision de videau inconnue"; + case ENGINE_ERROR_RESULT: + return "résultat de partie invalide"; } return "erreur inconnue"; } @@ -667,6 +669,158 @@ engine_level_cube_decision(const char *position_id, const char *match_id, int le return cube_decision(&pos, &context, cube); } +/* -------------------------------------------- Videau de l'ordinateur */ + +/* Valeur maximale du videau (MAX_CUBE dans backgammon.h de gnubg, non + * repris ici). */ +#define MAX_CUBE_VALUE (1 << 12) + +/* La décision de videau de gnubg pour une position, avec ces réglages. */ +static engine_status +find_cube_decision(position *pos, const evalcontext *context, cubedecision *decision) +{ + float output[2][NUM_ROLLOUT_OUTPUTS]; + float equities[NUM_CUBEFUL_OUTPUTS]; + + if (GeneralCubeDecisionE(output, (ConstTanBoard) pos->board, &pos->cube, context, NULL) < 0) + return ENGINE_ERROR_INTERNAL; + *decision = FindCubeDecision(equities, output, &pos->cube); + if ((size_t) *decision >= sizeof cube_decision_names / sizeof cube_decision_names[0]) + return ENGINE_ERROR_INTERNAL; + return ENGINE_OK; +} + +/* Voir engine.h : ComputerTurn, play.c l. 1312-1398. */ +engine_status +engine_level_double_decision(const char *position_id, const char *match_id, int level, + engine_level_double *result) +{ + position pos; + evalcontext context, market_context; + float output[NUM_ROLLOUT_OUTPUTS]; + cubedecision decision; + engine_status status; + int available = 0; + + memset(result, 0, sizeof *result); + 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; + + /* « if (ms.fCubeUse && !ms.anDice[0] && ms.nCube < MAX_CUBE && GetDPEq(...)) » */ + GetDPEq(&available, NULL, &pos.cube); + if (!available || pos.cube.nCube >= MAX_CUBE_VALUE) + return ENGINE_OK; + result->available = 1; + + /* Fenêtre de marché : évaluation sans videau, un pli de moins. */ + market_context = context; + market_context.fCubeful = FALSE; + if (market_context.nPlies) + market_context.nPlies--; + if (EvaluatePosition(NULL, (ConstTanBoard) pos.board, output, &pos.cube, &market_context)) + return ENGINE_ERROR_INTERNAL; + if (output[0] < GetDoublePointDeadCube(output, &pos.cube)) + return ENGINE_OK; + result->market_window = 1; + + status = find_cube_decision(&pos, &context, &decision); + if (status != ENGINE_OK) + return status; + result->decision = cube_decision_names[decision]; + switch (decision) { + case DOUBLE_TAKE: + case REDOUBLE_TAKE: + case DOUBLE_PASS: + case REDOUBLE_PASS: + case DOUBLE_BEAVER: + result->doubles = 1; + break; + case OPTIONAL_DOUBLE_BEAVER: + case OPTIONAL_DOUBLE_TAKE: + case OPTIONAL_REDOUBLE_TAKE: + case OPTIONAL_DOUBLE_PASS: + case OPTIONAL_REDOUBLE_PASS: + /* « double if 0-ply except when about to lose game » */ + result->doubles = context.nPlies == 0 && output[0] > 0.001f; + break; + default: + /* « better leave cube where it is » */ + break; + } + return ENGINE_OK; +} + +/* Voir engine.h : ComputerTurn, play.c l. 1139-1235. */ +engine_status +engine_level_take_decision(const char *position_id, const char *match_id, int level, + int *takes, const char **decision_name) +{ + position pos; + evalcontext context; + cubedecision decision; + 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) + status = find_cube_decision(&pos, &context, &decision); + if (status != ENGINE_OK) + return status; + switch (decision) { + case DOUBLE_PASS: + case TOOGOOD_PASS: + case REDOUBLE_PASS: + case TOOGOODRE_PASS: + case OPTIONAL_DOUBLE_PASS: + case OPTIONAL_REDOUBLE_PASS: + *takes = 0; + break; + default: + *takes = 1; + break; + } + *decision_name = cube_decision_names[decision]; + return ENGINE_OK; +} + +/* ------------------------------------------------------------------ Match */ + +/* Voir engine.h : play.c l. 303, 1817-1820 et 4277. */ +engine_status +engine_match_after_game(const char *match_id, int winner, int points, + engine_match_after *after) +{ + engine_match m; + engine_status status = engine_decode_match(match_id, &m); + int length, post_crawford; + + if (status != ENGINE_OK) + return status; + if (winner < 0 || winner > 1 || points < 1) + return ENGINE_ERROR_RESULT; + memset(after, 0, sizeof *after); + length = m.match_length; + after->score[0] = m.score[0]; + after->score[1] = m.score[1]; + after->score[winner] += points; + if (length == 0) + return ENGINE_OK; + after->match_over = after->score[winner] >= length; + if (after->match_over) + return ENGINE_OK; + /* Partie Crawford déjà jouée, lue comme gnubg (play.c l. 303). */ + post_crawford = !m.crawford && (m.score[0] == length - 1 || m.score[1] == length - 1); + /* play.c l. 1817-1820. */ + post_crawford |= m.crawford && after->score[winner] < length; + after->crawford = !post_crawford && !m.crawford && after->score[winner] == length - 1 + && after->score[!winner] != length - 1; + return ENGINE_OK; +} + /* -------------------------------------------- Analyse d'une décision de videau */ /* Le plus petit de deux nombres. */ diff --git a/engine/include/engine.h b/engine/include/engine.h index eac37f3..4e99ed4 100644 --- a/engine/include/engine.h +++ b/engine/include/engine.h @@ -51,7 +51,8 @@ typedef enum { 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_ERROR_CUBE_ACTION, /* décision de videau inconnue */ + ENGINE_ERROR_RESULT /* résultat de partie invalide */ } engine_status; #define ENGINE_MAX_PLY 4 @@ -374,6 +375,71 @@ engine_status engine_level_best_moves(const char *position_id, const char *match engine_status engine_level_cube_decision(const char *position_id, const char *match_id, int level, engine_cube *cube); +/* + * Le videau de l'ordinateur à ce niveau, comme le joue gnubg de bureau + * (ComputerTurn dans play.c, l. 1312-1398, « Consider doubling ») : + * - il ne pense à doubler que s'il a accès au videau (GetDPEq : pas en + * partie Crawford, pas avec un videau mort…) et que le videau vaut moins + * de 4096 (MAX_CUBE) ; + * - pré-filtre « fenêtre de marché » : ses chances de gain, évaluées sans + * videau à un pli de moins que le niveau (bruit compris), doivent atteindre + * le point de double à videau mort (GetDoublePointDeadCube) ; sinon il ne + * double pas, sans évaluer la décision complète ; + * - dans la fenêtre, il double sur DOUBLE_TAKE, REDOUBLE_TAKE, DOUBLE_PASS, + * REDOUBLE_PASS et DOUBLE_BEAVER ; sur un double « facultatif » + * (OPTIONAL_*), seulement s'il évalue à 0 pli et que ses chances de gain + * dépassent 0,001 (« double if 0-ply except when about to lose game ») ; + * sinon il garde le videau. + */ +typedef struct { + int available; /* le joueur au trait a accès au videau */ + int market_window; /* le pré-filtre est passé : décision évaluée */ + int doubles; /* l'ordinateur double */ + const char *decision; /* décision complète (noms d'engine_cube.decision) + * si elle a été évaluée, sinon NULL */ +} engine_level_double; + +engine_status engine_level_double_decision(const char *position_id, const char *match_id, + int level, engine_level_double *result); + +/* + * L'ordinateur, adversaire du joueur au trait qui vient de doubler, prend-il + * ? Comme ComputerTurn (play.c, l. 1139-1235, « normal double by + * opponent ») : la décision de videau est évaluée du point de vue de celui + * qui double, avec les réglages du niveau de l'ordinateur ; il passe sur + * DOUBLE_PASS, TOOGOOD_PASS, REDOUBLE_PASS, TOOGOODRE_PASS et les + * OPTIONAL_*_PASS, et prend sinon (Ustagammon ne joue pas les castors : + * gnubg prend aussi quand il ne peut pas en faire). position_id et match_id + * sont ceux du joueur qui double ; *decision reçoit le nom de la décision. + */ +engine_status engine_level_take_decision(const char *position_id, const char *match_id, + int level, int *takes, const char **decision); + +/* ------------------------------------------------------------------ Match */ + +/* Le match après une partie. */ +typedef struct { + int score[2]; /* score après la partie */ + int match_over; /* le gagnant a atteint la longueur du match */ + int crawford; /* la partie suivante est la partie Crawford */ +} engine_match_after; + +/* + * Le match après une partie gagnée par winner (0 ou 1, numérotation du + * Match ID) pour points points. match_id est celui de la partie jouée : sa + * longueur, son score avant la partie et son drapeau Crawford comptent, le + * reste est ignoré. Comme gnubg de bureau à la fin d'une partie (play.c, + * l. 1817-1820, règle de Crawford automatique) : la partie suivante est la + * partie Crawford si le gagnant arrive à un point du but, que son adversaire + * n'y est pas et qu'il n'y a pas encore eu de partie Crawford. Qu'une partie + * Crawford ait déjà eu lieu se lit dans le Match ID comme gnubg le lit + * (play.c, l. 303 et l. 4277) : pas de drapeau Crawford alors qu'un joueur + * est à un point du but. En partie libre (longueur 0), le score s'ajoute + * seulement. ENGINE_ERROR_RESULT si winner ou points sont invalides. + */ +engine_status engine_match_after_game(const char *match_id, int winner, int points, + engine_match_after *after); + /* ------------------------------------------------------------- Analyse */ /* Appréciation de gnubg pour une perte d'équité (0 ou négative). */ diff --git a/tests/test_engine.sh b/tests/test_engine.sh index d13b4d2..22af1ab 100644 --- a/tests/test_engine.sh +++ b/tests/test_engine.sh @@ -259,6 +259,62 @@ grep -qF '"analysed": false, "cube": null, "equity_loss": 0, "skill": "none"' \ <<<"$($cli analysecube "$course" "$(valeur match_id "$($cli matchid 2 1)")" no_double)" \ || echouer "analysecube, videau chez l'adversaire : pas d'analyse attendue" +# 7. Match et videau de l'ordinateur (étape 1c). + +# Match ID d'un match : longueur, score et Crawford relus à l'identique. +sortie="$($cli matchid 1 -1 5 4 2 1)" +grep -qF '"match_length": 5, "score": [4, 2], "cube_value": 1, "cube_owner": -1, "player_on_roll": 0, "crawford": true, "jacoby": false' \ + <<<"$sortie" || echouer "matchid d'un match mal codé : $sortie" +[ "$(valeur match_id "$sortie")" = "$(valeur match_id "$($cli matchid 1 -1 5 4 2 1)")" ] \ + || echouer "matchid d'un match : pas reproductible" +attendre_erreur usage matchid 1 -1 3 3 0 0 +attendre_erreur usage matchid 1 -1 3 0 0 2 + +# Le match après une partie (règle de Crawford de gnubg, play.c). +apres() { + local id + id="$(valeur match_id "$($cli matchid 1 -1 "$1" "$2" "$3" "$4")")" + $cli aftergame "$id" "$5" "$6" +} +while read -r longueur s0 s1 drapeau gagnant points attendu; do + sortie="$(apres "$longueur" "$s0" "$s1" "$drapeau" "$gagnant" "$points")" + grep -qF "$attendu" <<<"$sortie" \ + || echouer "aftergame $longueur $s0-$s1 crawford $drapeau, $gagnant gagne $points : $sortie" +done <<-'EOF' + 3 0 0 0 0 2 {"score": [2, 0], "match_over": false, "crawford": true} + 3 0 0 0 1 1 {"score": [0, 1], "match_over": false, "crawford": false} + 3 1 1 0 0 1 {"score": [2, 1], "match_over": false, "crawford": true} + 3 2 0 1 1 1 {"score": [2, 1], "match_over": false, "crawford": false} + 3 2 0 1 0 1 {"score": [3, 0], "match_over": true, "crawford": false} + 3 2 1 0 1 1 {"score": [2, 2], "match_over": false, "crawford": false} + 3 2 1 0 1 4 {"score": [2, 5], "match_over": true, "crawford": false} + 5 3 3 0 1 1 {"score": [3, 4], "match_over": false, "crawford": true} + 5 4 3 0 1 1 {"score": [4, 4], "match_over": false, "crawford": false} +EOF +grep -qF '{"score": [2, 0], "match_over": false, "crawford": false}' \ + <<<"$($cli aftergame "$libre_j0" 0 2)" || echouer "aftergame en partie libre" +attendre_erreur invalid_result aftergame "$libre_j0" 2 1 +attendre_erreur invalid_result aftergame "$libre_j0" 0 0 + +# L'ordinateur double (ComputerTurn de gnubg) : course gagnée, double et +# passe ; partie Crawford, videau inaccessible ; position perdue, hors de la +# fenêtre de marché. +grep -qF '"available": true, "market_window": true, "double": true, "decision": "DOUBLE_PASS"' \ + <<<"$($cli --level 2ply double "$course" "$libre_j0")" || echouer "double en course gagnée" +grep -qF '"available": false, "market_window": false, "double": false, "decision": null' \ + <<<"$($cli --level 2ply double "$depart" "$crawford")" || echouer "double en partie Crawford" +perdue="$(valeur swapped_position_id "$($cli position "$course" "$libre_j0")")" +grep -qF '"available": true, "market_window": false, "double": false, "decision": null' \ + <<<"$($cli --level beginner double "$perdue" "$libre_j0")" || echouer "double hors fenêtre de marché" +grep -qF '"double": false' <<<"$($cli --level 2ply double "$depart" "$libre_j0")" \ + || echouer "double à la position de départ" +# L'ordinateur répond au double : passe en course perdue, prend au départ. +grep -qF '"take": false, "decision": "DOUBLE_PASS"' \ + <<<"$($cli --level 2ply take "$course" "$libre_j0")" || echouer "take en course perdue" +grep -qF '"take": true' <<<"$($cli --level beginner take "$depart" "$libre_j0")" \ + || echouer "take au départ" +attendre_erreur usage double "$depart" "$libre_j0" + # Chance : même jet, même chance ; ordre des dés indifférent. [ "$($cli luck "$milieu" "$milieu_mid" 64 | sed 's/"dice": \[[0-9], [0-9]\]//')" \ = "$($cli luck "$milieu" "$milieu_mid" 46 | sed 's/"dice": \[[0-9], [0-9]\]//')" ] \