diff --git a/engine-cli/main.c b/engine-cli/main.c index f32f293..5373e48 100644 --- a/engine-cli/main.c +++ b/engine-cli/main.c @@ -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[""] (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(); diff --git a/engine/engine.c b/engine/engine.c index 1b6a157..b8fe441 100644 --- a/engine/engine.c +++ b/engine/engine.c @@ -182,6 +182,38 @@ engine_decode_match(const char *match_id, engine_match *match) return ENGINE_OK; } +/* Code un Match ID : voir engine.h. */ +engine_status +engine_encode_match(const engine_match *match, char match_id[13]) +{ + const unsigned int no_dice[2] = { 0, 0 }; + int value = match->cube_value, log2 = 0; + + while (value > 1 && value % 2 == 0) { + value /= 2; + log2++; + } + if (value != 1 || log2 > 12 || match->cube_owner < -1 || match->cube_owner > 1 + || match->player_on_roll < 0 || match->player_on_roll > 1 || match->match_length < 0) + return ENGINE_ERROR_MATCH_ID; + snprintf(match_id, 13, "%s", + MatchID(no_dice, match->player_on_roll, 0, 0, match->player_on_roll, + match->cube_owner, match->crawford, match->match_length, match->score, + match->cube_value, match->jacoby, GAME_PLAYING)); + return ENGINE_OK; +} + +/* Décode un Position ID en plateau de gnubg. */ +static engine_status +decode_board(const char *position_id, TanBoard board) +{ + if (engine_state != READY) + return ENGINE_ERROR_NOT_INITIALISED; + if (!is_base64_id(position_id, L_POSITIONID) || !PositionFromID(board, position_id)) + return ENGINE_ERROR_POSITION_ID; + return ENGINE_OK; +} + /* Décode Position ID et Match ID, et prépare le cubeinfo de gnubg. */ static engine_status decode_position(const char *position_id, const char *match_id, position *pos) @@ -189,10 +221,9 @@ decode_position(const char *position_id, const char *match_id, position *pos) engine_match *m = &pos->match; engine_status status; - if (engine_state != READY) - return ENGINE_ERROR_NOT_INITIALISED; - if (!is_base64_id(position_id, L_POSITIONID) || !PositionFromID(pos->board, position_id)) - return ENGINE_ERROR_POSITION_ID; + status = decode_board(position_id, pos->board); + if (status != ENGINE_OK) + return status; status = engine_decode_match(match_id, m); if (status != ENGINE_OK) return status; @@ -237,6 +268,13 @@ fill_move(engine_move *out, const move *m, const position *pos, int evaluated) FormatMove(out->notation, (ConstTanBoard) pos->board, m->anMove); snprintf(out->position_id, sizeof out->position_id, "%s", PositionIDFromKey(&m->key)); + /* gnubg numérote les cases de 0 à 23, la barre 24 et la sortie -1. */ + out->half_move_count = 0; + for (i = 0; i < 2 * ENGINE_MAX_HALF_MOVES && m->anMove[i] >= 0; i += 2) { + out->half_moves[i / 2].from = m->anMove[i] + 1; + out->half_moves[i / 2].to = m->anMove[i + 1] + 1; + out->half_move_count++; + } out->ply = evaluated ? (int) m->esMove.ec.nPlies : -1; for (i = 0; i < ENGINE_NUM_PROBABILITIES; i++) out->probabilities[i] = evaluated ? m->arEvalMove[i] : 0.0f; @@ -381,5 +419,240 @@ engine_cube_decision(const char *position_id, const char *match_id, int ply, eng cube->optimal = equities[OUTPUT_OPTIMAL]; cube->decision = cube_decision_names[decision]; cube->decision_text = GetCubeRecommendation(decision); + GetDPEq(&cube->available, NULL, &pos.cube); + cube->should_double = decision == DOUBLE_TAKE || decision == DOUBLE_PASS + || decision == DOUBLE_BEAVER || decision == REDOUBLE_TAKE || decision == REDOUBLE_PASS; + cube->should_take = cube->double_take <= cube->double_pass; + return ENGINE_OK; +} + +/* ------------------------------------------------- Plateau et fin de partie */ + +static void +fill_side(engine_side *side, const unsigned int checkers[25]) +{ + int i, on_board = 0; + + side->pip_count = 0; + for (i = 0; i < 25; i++) { + on_board += (int) checkers[i]; + side->pip_count += (int) checkers[i] * (i + 1); + } + for (i = 0; i < 24; i++) + side->points[i] = (int) checkers[i]; + side->bar = (int) checkers[24]; + side->off = anChequers[VARIATION_STANDARD] - on_board; +} + +engine_status +engine_decode_position(const char *position_id, engine_board *board) +{ + TanBoard b; + engine_status status = decode_board(position_id, b); + + if (status != ENGINE_OK) + return status; + /* Dans gnubg, b[1] est le joueur au trait et b[0] son adversaire. */ + fill_side(&board->on_roll, b[1]); + fill_side(&board->opponent, b[0]); + SwapSides(b); + snprintf(board->swapped_position_id, sizeof board->swapped_position_id, "%s", PositionID((ConstTanBoard) b)); + return ENGINE_OK; +} + +engine_status +engine_game_result(const char *position_id, const char *match_id, engine_result *result) +{ + TanBoard b; + engine_match match; + engine_status status = decode_board(position_id, b); + int i; + + if (status == ENGINE_OK) + status = engine_decode_match(match_id, &match); + if (status != ENGINE_OK) + return status; + + memset(result, 0, sizeof *result); + result->kind = GameStatus((ConstTanBoard) b, VARIATION_STANDARD); + if (result->kind == 0) + return ENGINE_OK; + result->over = 1; + /* Le gagnant est celui qui n'a plus aucune dame sur le plateau. */ + for (i = 0; i < 25 && b[1][i] == 0; i++) + ; + result->winner = i == 25 ? 0 : 1; + /* Règle Jacoby : gammons et backgammons ne comptent qu'une fois le + * videau tourné, c'est-à-dire quand il n'est plus au centre. */ + if (match.match_length == 0 && match.jacoby && match.cube_owner == -1) + result->points = match.cube_value; + else + result->points = result->kind * match.cube_value; + return ENGINE_OK; +} + +/* ------------------------------------------ Saisie demi-coup par demi-coup */ + +/* + * La légalité de chaque demi-coup vient de gnubg : GenerateMoves avec un seul + * dé (le second à 0) donne exactement les demi-coups permis avec ce dé (barre + * d'abord, case libre, règles de sortie). Celle du coup entier aussi : les + * demi-coups doivent pouvoir se compléter en une position de la liste des + * coups légaux, qui impose de jouer le plus de dés possible. + */ + +/* Un demi-coup permis et la position où il mène. */ +typedef struct { + int from, to; /* numérotation de gnubg : 0 à 24, -1 sortie */ + positionkey key; +} step; + +/* Positions d'arrivée des coups légaux. */ +typedef struct { + positionkey *keys; + int count; +} legal_results; + +/* Les demi-coups permis avec un dé. Ils sont copiés : GenerateMoves réutilise + * un tableau interne, que l'appel suivant écraserait. Il y en a au plus un + * par case de départ. */ +static int +single_steps(ConstTanBoard board, int die, step steps[25]) +{ + movelist list; + unsigned int i; + + GenerateMoves(&list, board, die, 0, FALSE); + for (i = 0; i < list.cMoves && i < 25; i++) { + steps[i].from = list.amMoves[i].anMove[0]; + steps[i].to = list.amMoves[i].anMove[1]; + steps[i].key = list.amMoves[i].key; + } + return (int) i; +} + +static int +is_legal_result(ConstTanBoard board, const legal_results *legal) +{ + positionkey key; + int i; + + PositionKey(board, &key); + for (i = 0; i < legal->count; i++) + if (EqualKeys(key, legal->keys[i])) + return 1; + return 0; +} + +/* Les dés sans celui d'indice skip. */ +static void +remove_die(const int dice[], int count, int skip, int rest[]) +{ + int i, j = 0; + + for (i = 0; i < count; i++) + if (i != skip) + rest[j++] = dice[i]; +} + +/* Vrai si board peut encore mener à un coup légal avec les dés restants. */ +static int +can_complete(ConstTanBoard board, const int dice[], int count, const legal_results *legal) +{ + step steps[25]; + int rest[ENGINE_MAX_HALF_MOVES]; + TanBoard next; + int i, j, n; + + if (is_legal_result(board, legal)) + return 1; + for (i = 0; i < count; i++) { + if (i > 0 && dice[i] == dice[0]) + continue; /* même valeur déjà essayée (double) */ + remove_die(dice, count, i, rest); + n = single_steps(board, dice[i], steps); + for (j = 0; j < n; j++) { + PositionFromKey(next, &steps[j].key); + if (can_complete((ConstTanBoard) next, rest, count - 1, legal)) + return 1; + } + } + return 0; +} + +/* Joue les demi-coups ; en cas de succès, result reçoit la position atteinte. + * Un même demi-coup peut correspondre à plusieurs dés (une sortie avec un dé + * plus fort que nécessaire) : chacun est essayé. */ +static int +play_half_moves(ConstTanBoard board, const int dice[], int count, + const engine_half_move *half_moves, int remaining, + const legal_results *legal, TanBoard result) +{ + step steps[25]; + int rest[ENGINE_MAX_HALF_MOVES]; + TanBoard next; + int i, j, n; + + if (remaining == 0) { + if (!can_complete(board, dice, count, legal)) + return 0; + memcpy(result, board, sizeof(TanBoard)); + return 1; + } + for (i = 0; i < count; i++) { + if (i > 0 && dice[i] == dice[0]) + continue; /* même valeur déjà essayée (double) */ + remove_die(dice, count, i, rest); + n = single_steps(board, dice[i], steps); + for (j = 0; j < n; j++) { + if (steps[j].from != half_moves->from - 1 || steps[j].to != half_moves->to - 1) + continue; + PositionFromKey(next, &steps[j].key); + if (play_half_moves((ConstTanBoard) next, rest, count - 1, half_moves + 1, + remaining - 1, legal, result)) + return 1; + } + } + return 0; +} + +engine_status +engine_check_half_moves(const char *position_id, int die1, int die2, + const engine_half_move *half_moves, int count, + engine_half_moves_check *check) +{ + TanBoard board, result; + movelist list; + legal_results legal; + int dice[ENGINE_MAX_HALF_MOVES] = { die1, die2, die1, die2 }; + int dice_count = die1 == die2 ? 4 : 2; + engine_status status; + unsigned int i; + + status = decode_board(position_id, board); + if (status == ENGINE_OK) + status = check_dice(die1, die2); + if (status != ENGINE_OK) + return status; + + memset(check, 0, sizeof *check); + if (count < 0 || count > dice_count) + return ENGINE_OK; + + GenerateMoves(&list, (ConstTanBoard) board, die1, die2, FALSE); + if (list.cMoves == 0) + return ENGINE_OK; /* aucun coup possible : rien à saisir */ + legal.count = (int) list.cMoves; + legal.keys = g_malloc(sizeof *legal.keys * list.cMoves); + for (i = 0; i < list.cMoves; i++) + legal.keys[i] = list.amMoves[i].key; + + if (play_half_moves((ConstTanBoard) board, dice, dice_count, half_moves, count, &legal, result)) { + check->valid = 1; + check->complete = is_legal_result((ConstTanBoard) result, &legal); + snprintf(check->position_id, sizeof check->position_id, "%s", + PositionID((ConstTanBoard) result)); + } + g_free(legal.keys); return ENGINE_OK; } diff --git a/engine/include/engine.h b/engine/include/engine.h index 39cbaeb..58e6ebb 100644 --- a/engine/include/engine.h +++ b/engine/include/engine.h @@ -66,11 +66,29 @@ enum { ENGINE_NUM_PROBABILITIES }; +/* + * Demi-coup : une dame déplacée d'une case avec un seul dé. Les cases sont + * numérotées du point de vue du joueur qui joue, comme dans la notation de + * gnubg : 1 à 24, 25 pour la barre, 0 pour la sortie. + */ +#define ENGINE_BAR 25 +#define ENGINE_OFF 0 + +typedef struct { + int from; + int to; +} engine_half_move; + +/* Un coup complet utilise au plus quatre dés (un double). */ +#define ENGINE_MAX_HALF_MOVES 4 + /* Un coup, avec son évaluation quand il a été classé. */ typedef struct { char notation[32]; /* notation de gnubg, ex. « 8/5 6/5 » */ char position_id[16]; /* Position ID après le coup, plateau vu du * joueur qui vient de jouer */ + engine_half_move half_moves[ENGINE_MAX_HALF_MOVES]; + int half_move_count; /* demi-coups du coup, dans un ordre jouable */ int ply; /* niveau de l'évaluation (-1 : non évalué) */ float probabilities[ENGINE_NUM_PROBABILITIES]; float equity; /* équité de classement (avec videau) */ @@ -96,8 +114,50 @@ typedef struct { * « DOUBLE_TAKE », « DOUBLE_PASS », * « TOOGOOD_PASS », « NOT_AVAILABLE »… */ const char *decision_text; /* libellé anglais de gnubg, ex. « Double, take » */ + int available; /* le joueur au trait a le droit de doubler */ + int should_double; /* gnubg recommande de doubler : décision + * DOUBLE_TAKE, DOUBLE_PASS, DOUBLE_BEAVER, + * REDOUBLE_TAKE ou REDOUBLE_PASS (un double + * « facultatif » n'est pas recommandé) */ + int should_take; /* s'il double, l'adversaire doit prendre : + * prendre lui coûte moins que passer + * (double_take <= double_pass) */ } engine_cube; +/* Un camp sur le plateau, vu de lui-même (sa case 1 est la plus proche de + * la sortie). */ +typedef struct { + int points[24]; /* dames sur les cases 1 à 24 (indices 0 à 23) */ + int bar; /* dames sur la barre */ + int off; /* dames sorties */ + int pip_count; /* compte de points */ +} engine_side; + +/* Plateau décodé d'un Position ID. */ +typedef struct { + engine_side on_roll; /* le joueur au trait */ + engine_side opponent; /* son adversaire */ + char swapped_position_id[16]; /* la même position vue de l'adversaire */ +} engine_board; + +/* Fin de partie. */ +typedef struct { + int over; /* la partie est finie */ + int winner; /* 0 : le joueur au trait, 1 : son adversaire */ + int kind; /* 1 : simple, 2 : gammon, 3 : backgammon */ + int points; /* points gagnés : kind fois la valeur du videau, + * mais 1 fois en partie libre avec la règle + * Jacoby quand le videau n'a pas bougé */ +} engine_result; + +/* Où en est un coup saisi demi-coup par demi-coup. */ +typedef struct { + int valid; /* les demi-coups commencent un coup légal */ + int complete; /* ils forment à eux seuls un coup légal */ + char position_id[16]; /* si valid : position après ces demi-coups, vue + * du joueur qui joue ; sinon chaîne vide */ +} engine_half_moves_check; + /* Description lisible d'un Match ID décodé. */ typedef struct { int match_length; /* 0 : partie libre (money game) */ @@ -125,6 +185,30 @@ const char *engine_status_message(engine_status status); /* Décode un Match ID. */ engine_status engine_decode_match(const char *match_id, engine_match *match); +/* + * Code un Match ID (12 caractères et le zéro final), sans dés ni double en + * cours. cube_value : puissance de 2 de 1 à 4096 ; cube_owner : -1, 0 ou 1. + */ +engine_status engine_encode_match(const engine_match *match, char match_id[13]); + +/* Décode un Position ID en plateau. */ +engine_status engine_decode_position(const char *position_id, engine_board *board); + +/* La partie est-elle finie dans cette position, et combien rapporte-t-elle ? */ +engine_status engine_game_result(const char *position_id, const char *match_id, + engine_result *result); + +/* + * Vérifie des demi-coups saisis un par un (count de 0 à + * ENGINE_MAX_HALF_MOVES), avec le jet die1, die2 : chacun doit être permis + * par les règles, et l'ensemble doit pouvoir se compléter en un des coups + * légaux d'engine_legal_moves. Des demi-coups refusés ne sont pas une erreur : + * check->valid vaut alors 0. + */ +engine_status engine_check_half_moves(const char *position_id, int die1, int die2, + const engine_half_move *half_moves, int count, + engine_half_moves_check *check); + /* * Coups légaux pour une position et un jet (die1, die2 dans 1..6), sans * évaluation (ply vaut -1). moves doit pouvoir contenir max_moves coups ; diff --git a/tests/test_engine.sh b/tests/test_engine.sh index c09f1e6..a3cf6a7 100644 --- a/tests/test_engine.sh +++ b/tests/test_engine.sh @@ -138,6 +138,87 @@ attendre_erreur usage bestmove "$depart" "$libre_j0" attendre_erreur usage inconnue "$depart" "$libre_j0" attendre_erreur invalid_data --data /dossier/absent bestmove "$depart" "$libre_j0" 31 +# 6. Ce dont a besoin le domaine d'une partie (étape 0d.1). + +# Les coups légaux donnent leurs demi-coups, dans un ordre jouable. +grep -qF '"move": "8/5 6/5", "position_id": "4HPwATCwZ/ABMA", "half_moves": [[8, 5], [6, 5]]' \ + <<<"$($cli legal "$depart" "$libre_j0" 31)" || echouer "legal : demi-coups de 8/5 6/5 absents" + +# Match ID : codage, et décodage identique. +[ "$(valeur match_id "$($cli matchid 1 -1)")" = "$libre_j0" ] || echouer "matchid 1 -1 : $libre_j0 attendu" +videau4="$(valeur match_id "$($cli matchid 4 1)")" +sortie="$($cli eval "$depart" "$videau4")" +[ "$(valeur cube_value "$sortie")" = 4 ] && [ "$(valeur cube_owner "$sortie")" = 1 ] \ + || echouer "matchid 4 1 : videau mal codé ($sortie)" +attendre_erreur invalid_match_id matchid 3 0 +attendre_erreur invalid_match_id matchid 2 5 + +# Plateau : position de départ, symétrique, sans résultat. +sortie="$($cli position "$depart" "$libre_j0")" +grep -qF '"on_roll": {"points": [0, 0, 0, 0, 0, 5, 0, 3, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2], "bar": 0, "off": 0, "pip_count": 167}' <<<"$sortie" \ + || echouer "position de départ mal décodée : $sortie" +[ "$(valeur swapped_position_id "$sortie")" = "$depart" ] || echouer "départ : la position vue de l'adversaire diffère" +grep -qF '"result": null' <<<"$sortie" || echouer "départ : la partie ne devrait pas être finie" +attendre_erreur invalid_position_id position 'ceci-nest-pas-un-id' "$libre_j0" + +# Fin de partie : le joueur au trait a tout sorti, l'adversaire aucune dame +# (gammon). Règle Jacoby : 1 point tant que le videau est au centre ; sinon +# 2 fois le videau. +fini=/38AAAAAAAAAAA +grep -qF '"result": {"winner": "on_roll", "kind": "gammon", "points": 1}' \ + <<<"$($cli position "$fini" "$libre_j0")" || echouer "gammon avec Jacoby, videau au centre : 1 point attendu" +grep -qF '"result": {"winner": "on_roll", "kind": "gammon", "points": 8}' \ + <<<"$($cli position "$fini" "$videau4")" || echouer "gammon, videau à 4 : 8 points attendus" +vu_du_perdant="$(valeur swapped_position_id "$($cli position "$fini" "$libre_j0")")" +grep -qF '"winner": "opponent"' <<<"$($cli position "$vu_du_perdant" "$libre_j0")" \ + || echouer "fin de partie vue du perdant : l'adversaire devrait gagner" + +# Demi-coups : « valid complete position_id » pour une saisie. +demi_coups() { + local sortie valid complete + sortie="$($cli halfmoves "$@")" + valid="$(grep -oE '"valid": (true|false)' <<<"$sortie" | cut -d' ' -f2)" + complete="$(grep -oE '"complete": (true|false)' <<<"$sortie" | cut -d' ' -f2)" + echo "$valid $complete $(valeur result_position_id "$sortie")" +} +attendre_demi_coups() { + local attendu="$1" + shift + local obtenu + obtenu="$(demi_coups "$@")" + [ "$obtenu" = "$attendu" ] || echouer "halfmoves $* : « $obtenu » au lieu de « $attendu »" +} +attendre_demi_coups "true false 4HPwATDQZ/ABMA" "$depart" 31 "8/5" +attendre_demi_coups "true true 4HPwATCwZ/ABMA" "$depart" 31 "8/5 6/5" +attendre_demi_coups "true true 4HPwATCwZ/ABMA" "$depart" 31 "6/5 8/5" +attendre_demi_coups "true false $depart" "$depart" 31 +# Case 12 tenue par l'adversaire ; 8/4 demande deux dés. +attendre_demi_coups "false false " "$depart" 31 "13/12" +attendre_demi_coups "false false " "$depart" 31 "8/4" +# Le même pion avec les deux dés. +attendre_demi_coups "true true 4HPwATDgc/ABIQ" "$depart" 31 "24/21 21/20" +# Sortie : la dame la plus éloignée d'abord avec un dé trop fort. +attendre_demi_coups "true true /38AAAAAAAAAAA" "$course" 66 "6/off 5/off" +attendre_demi_coups "false false " "$course" 66 "5/off" +attendre_demi_coups "true false /38AAAAgAAAAAA" "$course" 65 "5/off" +# Course, jet 21 : il faut jouer les deux dés. +attendre_demi_coups "false false " "$course" 21 "6/3" +attendre_demi_coups "true true /38AAAAkAAAAAA" "$course" 21 "6/5 5/3" +# Aucun coup possible : rien n'est valide. +attendre_demi_coups "false false " 27YBBwAcz8EDQA 66 +attendre_erreur usage halfmoves "$depart" 31 "8-5" +attendre_erreur invalid_dice halfmoves "$depart" 71 "8/5" + +# Videau : droit de doubler et recommandations. +sortie="$($cli cube "$depart" "$libre_j0")" +grep -qF '"available": true, "double": false, "take": true' <<<"$sortie" \ + || echouer "videau au départ : disponible, pas de double, prendre attendus" +grep -qF '"available": true, "double": true, "take": false' <<<"$($cli cube "$course" "$libre_j0")" \ + || echouer "videau en course gagnée : double et passe attendus" +# Videau chez l'adversaire : le joueur au trait ne peut pas doubler. +grep -qF '"available": false, "double": false' <<<"$($cli cube "$course" "$(valeur match_id "$($cli matchid 2 1)")")" \ + || echouer "videau chez l'adversaire : double indisponible attendu" + if [ "$echecs" -gt 0 ]; then echo " $echecs vérification(s) du moteur en échec." exit 1