/* * This file is part of pgn-extract: a Portable Game Notation (PGN) extractor. * Copyright (C) 1994-2022 David J. Barnes * * pgn-extract is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * pgn-extract is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with pgn-extract. If not, see . * * David J. Barnes may be contacted as d.j.barnes@kent.ac.uk * https://www.cs.kent.ac.uk/people/staff/djb/ */ #include #include #include #include #include #include #include "bool.h" #include "mymalloc.h" #include "defs.h" #include "typedef.h" #include "map.h" #include "apply.h" #include "tokens.h" #include "taglist.h" #include "output.h" #include "lex.h" #include "grammar.h" #include "moves.h" #include "eco.h" #include "decode.h" #include "hashing.h" #include "fenmatcher.h" #include "zobrist.h" /* Define a positional search depth that should look at the * full length of a game. This is used in play_moves(). */ #define DEFAULT_POSITIONAL_DEPTH 300 /* Prototypes of functions limited to this file. */ static const char *position_matches(const Board *board); static Boolean play_moves(Game *game_details, Board *board, Move *moves, unsigned max_depth, Boolean check_move_validity, Boolean mainline); static Boolean apply_variations(const Game *game_details, const Board *board, Variation *variation, Boolean check_move_validity); static Boolean rewrite_variations(const Board *board, Variation *variation); static Boolean rewrite_moves(Game *game, Board *board, Move *move_details); static void build_FEN_components(const Board *board, char *epd, char *fen_suffix); static unsigned plies_in_move_sequence(Move *moves); static Boolean drop_plies_from_start(Game *game, Move *moves, int plies_to_drop); #if 0 static void append_evaluation(Move *move_details, const Board *board); static void append_FEN_comment(Move *move_details, const Board *board); static void append_hashcode_comment(Move *move_details, Board *board); #endif static double evaluate(const Board *board); static double shannonEvaluation(const Board *board); static void print_board(const Board *board, FILE *outfp); static StringList * find_matching_comment(const char *comment_pattern, const CommentList *comment); /* The English SAN piece characters. These are * always used when building a FEN string, rather * than using any language-dependent user settings. */ static char SAN_piece_characters[NUM_PIECE_VALUES] = { '?', '?', 'P', 'N', 'B', 'R', 'Q', 'K' }; /* These letters may be changed via a call to set_output_piece_characters * with a string of the form "PNBRQK". * This would normally be done with the -Wsan argument. */ static const char *output_piece_characters[NUM_PIECE_VALUES] = { "?", "?", "P", "N", "B", "R", "Q", "K" }; /* Check whether the given result is valid. */ static Boolean valid_result(const char *result) { return (strcmp(result,"1-0") == 0) || (strcmp(result,"0-1") == 0) || (strcmp(result,"1/2-1/2") == 0) || (strcmp(result,"*") == 0); } /* letters should contain a string of the form: "PNBRQK" */ void set_output_piece_characters(const char *letters) { if (letters == NULL) { fprintf(GlobalState.logfile, "NULL string passed to set_output_piece_characters.\n"); } else { Piece piece; int piece_index; for (piece_index = 0, piece = PAWN; piece <= KING && letters[piece_index] != '\0'; piece++) { /* Check whether we have a single character piece, * or one of the form X+Y, where the piece is represented * by the combination XY. */ if (letters[piece_index + 1] == '+') { /* A two-char piece. */ static char double_char_piece[] = "XY"; double_char_piece[0] = letters[piece_index]; piece_index++; /* Skip the plus. */ piece_index++; if (letters[piece_index] != '\0') { double_char_piece[1] = letters[piece_index]; output_piece_characters[piece] = copy_string(double_char_piece); piece_index++; } else { fprintf(GlobalState.logfile, "Missing piece letter following + in -Wsan%s.\n", letters); exit(1); } } else { static char single_char_piece[] = "X"; *single_char_piece = letters[piece_index]; output_piece_characters[piece] = copy_string(single_char_piece); piece_index++; } } if (piece < NUM_PIECE_VALUES) { fprintf(GlobalState.logfile, "Insufficient piece letters found with -Wsan%s.\n", letters); fprintf(GlobalState.logfile, "The argument should be of the form -Wsan%s.\n", "PNBRQK"); exit(1); } else if (letters[piece_index] != '\0') { fprintf(GlobalState.logfile, "Too many piece letters found with -Wsan%s.\n", letters); fprintf(GlobalState.logfile, "The argument should be of the form -Wsan%s.\n", "PNBRQK"); exit(1); } else { /* Ok. */ } } } /* Return a fresh copy of the given string. */ char * copy_string(const char *str) { char *result; if(str != NULL) { size_t len = strlen(str); result = (char *) malloc_or_die(len + 1); strcpy(result, str); } else { result = NULL; } return result; } /* Allocate space for a new board. */ static Board * allocate_new_board(void) { return (Board *) malloc_or_die(sizeof (Board)); } /* Free the board space. */ void free_board(Board *board) { (void) free((void *) board); } Piece convert_FEN_char_to_piece(char c) { Piece piece = EMPTY; switch (c) { case 'K': case 'k': piece = KING; break; case 'Q': case 'q': piece = QUEEN; break; case 'R': case 'r': piece = ROOK; break; case 'N': case 'n': piece = KNIGHT; break; case 'B': case 'b': piece = BISHOP; break; case 'P': case 'p': piece = PAWN; break; } return piece; } /* Return the SAN letter associated with the given piece. */ char SAN_piece_letter(Piece piece) { if (piece < NUM_PIECE_VALUES) { return SAN_piece_characters[piece]; } else { return '?'; } } /* Return the SAN letter for the given Piece. */ char coloured_piece_to_SAN_letter(Piece coloured_piece) { Piece piece = EXTRACT_PIECE(coloured_piece); char letter = SAN_piece_letter(piece); if (EXTRACT_COLOUR(coloured_piece) == BLACK) { letter = tolower(letter); } return letter; } /* Find the position of the innermost or outermost rook for * the given castling move. */ static Col find_rook_starting_position(const Board *board, Colour colour, MoveClass castling, Boolean outermost) { Rank rank; Col col, boundary; int direction; Piece rook = MAKE_COLOURED_PIECE(colour, ROOK); /* Default search is outermost for the first rook and rook_count is 1. * If the innermost is required then the rook_count is 2. */ int rook_count; if (outermost) { rook_count = 1; } else { rook_count = 2; } if (colour == WHITE) { rank = FIRSTRANK; } else { rank = LASTRANK; } if (castling == KINGSIDE_CASTLE) { direction = -1; col = LASTCOL; boundary = FIRSTCOL - 1; } else { direction = 1; col = FIRSTCOL; boundary = LASTCOL + 1; } while (col != boundary && rook_count > 0) { if (board->board[RankConvert(rank)][ColConvert(col)] != rook) { col += direction; } else { rook_count--; if (rook_count != 0) { col += direction; } } } if (col != boundary) { return colour == WHITE ? col : col; } else { return '\0'; } } /* Set up the board from the FEN string passed as * argument. * This has the form: * Forsythe string of the setup position. * w/b - colour to move. * castling permissions. * en-passant square. * half-moves since pawn move/piece capture. * move number. */ Board * new_fen_board(const char *fen) { Board *new_board = allocate_new_board(); /* Start with a clear board. */ static const Board initial_board = { { { OFF, OFF, OFF, OFF, OFF, OFF, OFF, OFF, OFF, OFF, OFF, OFF}, { OFF, OFF, OFF, OFF, OFF, OFF, OFF, OFF, OFF, OFF, OFF, OFF}, { OFF, OFF, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, OFF, OFF}, { OFF, OFF, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, OFF, OFF}, { OFF, OFF, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, OFF, OFF}, { OFF, OFF, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, OFF, OFF}, { OFF, OFF, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, OFF, OFF}, { OFF, OFF, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, OFF, OFF}, { OFF, OFF, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, OFF, OFF}, { OFF, OFF, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, OFF, OFF}, { OFF, OFF, OFF, OFF, OFF, OFF, OFF, OFF, OFF, OFF, OFF, OFF}, { OFF, OFF, OFF, OFF, OFF, OFF, OFF, OFF, OFF, OFF, OFF, OFF} }, /* Who to move next. */ WHITE, /* Move number. */ 1, /* Default castling rights. Support Chess960. */ 'h', 'a', 'h', 'a', /* Initial king positions. */ 'e', FIRSTRANK, 'e', LASTRANK, /* En Passant rights. */ FALSE, 0, 0, /* Initial hash value. */ 0ul, /* half-move_clock */ 0, }; Rank rank = LASTRANK; Col col; const char *fen_char = fen; Boolean Ok = TRUE; /* In some circumstances we will try to parse the game data, * even if there are errors. */ Boolean try_to_parse_game = FALSE; /* Reset the contents of the new board. */ *new_board = initial_board; /* Extract the piece positions. */ col = FIRSTCOL; while (Ok && (*fen_char != ' ') && (*fen_char != '\0') && (rank >= FIRSTRANK)) { Piece piece; char ch = *fen_char; Colour colour; if ((piece = convert_FEN_char_to_piece(ch)) != EMPTY) { if (isupper((int) ch)) { colour = WHITE; } else { colour = BLACK; } if (col <= LASTCOL) { new_board->board[RankConvert(rank)][ColConvert(col)] = MAKE_COLOURED_PIECE(colour, piece); if (piece == KING) { if (colour == WHITE) { new_board->WKingCol = col; new_board->WKingRank = rank; } else { new_board->BKingCol = col; new_board->BKingRank = rank; } } col++; fen_char++; } else { Ok = FALSE; } } else if (isdigit((int) ch)) { if (('1' <= ch) && (ch <= '8')) { col += ch - '0'; /* In filling up the remaining columns of a rank we will * temporarily exceed LASTCOL, but we expect the * next character to be '/' so that we reset. */ if (col <= (LASTCOL + 1)) { fen_char++; } else { Ok = FALSE; } } else { Ok = FALSE; } } else if (ch == '/') { /* End of that rank. We should have completely filled the * previous rank. */ if (col == (LASTCOL + 1)) { col = FIRSTCOL; rank--; fen_char++; } else { Ok = FALSE; } } else { /* Unknown character. */ Ok = FALSE; } } /* As we don't print any error messages until the end of the function, * we don't need to guard everything with if(Ok). */ if (*fen_char == ' ') { /* Find out who is to move. */ fen_char++; } else { Ok = FALSE; } if (*fen_char == 'w') { new_board->to_move = WHITE; fen_char++; } else if (*fen_char == 'b') { new_board->to_move = BLACK; fen_char++; } else { Ok = FALSE; } if (*fen_char == ' ') { fen_char++; } else { Ok = FALSE; } /* Determine castling rights. */ if (*fen_char == '-') { /* No castling rights -- default above. */ new_board->WKingCastle = new_board->WQueenCastle = new_board->BKingCastle = new_board->BQueenCastle = '\0'; fen_char++; } else { /* Check to make sure that this section isn't empty. */ if (*fen_char == ' ') { Ok = FALSE; } /* Accommodate Chess960 notation for castling. */ if (*fen_char == 'K') { new_board->WKingCastle = find_rook_starting_position(new_board, WHITE, KINGSIDE_CASTLE, TRUE); fen_char++; } else if (*fen_char >= 'A' && *fen_char <= 'H') { new_board->WKingCastle = tolower(*fen_char); fen_char++; } else { new_board->WKingCastle = '\0'; } if (*fen_char == 'Q') { new_board->WQueenCastle = find_rook_starting_position(new_board, WHITE, QUEENSIDE_CASTLE, TRUE); fen_char++; } else if (*fen_char >= 'A' && *fen_char <= 'H') { new_board->WQueenCastle = tolower(*fen_char); fen_char++; } else { new_board->WQueenCastle = '\0'; } if (*fen_char == 'k') { new_board->BKingCastle = find_rook_starting_position(new_board, BLACK, KINGSIDE_CASTLE, TRUE); fen_char++; } else if (*fen_char >= 'a' && *fen_char <= 'h') { new_board->BKingCastle = *fen_char; fen_char++; } else { new_board->BKingCastle = '\0'; } if (*fen_char == 'q') { new_board->BQueenCastle = find_rook_starting_position(new_board, BLACK, QUEENSIDE_CASTLE, TRUE); fen_char++; } else if (*fen_char >= 'a' && *fen_char <= 'h') { new_board->BQueenCastle = *fen_char; fen_char++; } else { new_board->BQueenCastle = '\0'; } } if (*fen_char == ' ') { fen_char++; } else { Ok = FALSE; } /* If we are ok to this point, try to make a best efforts approach * to handle the game, even if there are subsequent errors. */ if (Ok) { try_to_parse_game = TRUE; } /* Check for an en-passant square. */ if (*fen_char == '-') { /* None. */ fen_char++; } else if (is_col(*fen_char)) { col = *fen_char; fen_char++; if (is_rank(*fen_char)) { rank = *fen_char; fen_char++; /* Make sure that the en-passant indicator is consistent * with whose move it is. */ if (((new_board->to_move == WHITE) && (rank == '6')) || ((new_board->to_move == BLACK) && (rank == '3'))) { /* Consistent. */ new_board->EnPassant = TRUE; new_board->ep_rank = rank; new_board->ep_col = col; } else { Ok = FALSE; } } else { Ok = FALSE; } } else { Ok = FALSE; } if (*fen_char == ' ') { fen_char++; } else { Ok = FALSE; } /* Check for half-move count since last pawn move * or capture. */ if (isdigit((int) *fen_char)) { unsigned halfmove_clock = *fen_char - '0'; fen_char++; while (isdigit((int) *fen_char)) { halfmove_clock = (halfmove_clock * 10)+(*fen_char - '0'); fen_char++; } new_board->halfmove_clock = halfmove_clock; } else { Ok = FALSE; } if (*fen_char == ' ') { fen_char++; } else { Ok = FALSE; } /* Check for current move number. */ if (isdigit((int) *fen_char)) { unsigned move_number = 0; move_number = *fen_char - '0'; fen_char++; while (isdigit((int) *fen_char)) { move_number = (move_number * 10)+(*fen_char - '0'); fen_char++; } if (move_number < 1) { move_number = 1; } new_board->move_number = move_number; } else { Ok = FALSE; } /* Allow trailing space. */ while (isspace((int) *fen_char)) { fen_char++; } if (*fen_char != '\0') { Ok = FALSE; } if (!Ok) { fprintf(GlobalState.logfile, "Illegal FEN string %s at %s", fen, fen_char); if (try_to_parse_game) { fprintf(GlobalState.logfile, " Attempting to parse the game, anyway."); } else { free_board(new_board); new_board = NULL; } putc('\n', GlobalState.logfile); report_details(GlobalState.logfile); print_error_context(GlobalState.logfile); } return new_board; } /* add_fen_castling is TRUE and castling permissions are absent. * Liberally assume them based on the King and Rook positions. */ void add_fen_castling(Game *game_details, Board *board) { Boolean added = FALSE; /* NB: This is not coded for Chess 960 positions. */ if(board->WKingCol == 'e' && board->WKingRank == '1') { board->WKingCastle = board->board[RankConvert('1')][ColConvert('h')] == MAKE_COLOURED_PIECE(WHITE, ROOK) ? 'h' : '\0'; board->WQueenCastle = board->board[RankConvert('1')][ColConvert('a')] == MAKE_COLOURED_PIECE(WHITE, ROOK) ? 'a' : '\0'; added = TRUE; } else { board->WKingCastle = '\0'; board->WQueenCastle = '\0'; } if(board->BKingCol == 'e' && board->BKingRank == '8') { board->BKingCastle = board->board[RankConvert('8')][ColConvert('h')] == MAKE_COLOURED_PIECE(BLACK, ROOK) ? 'h' : '\0'; board->BQueenCastle = board->board[RankConvert('8')][ColConvert('a')] == MAKE_COLOURED_PIECE(BLACK, ROOK) ? 'a' : '\0'; added = TRUE; } else { board->BKingCastle = '\0'; board->BQueenCastle = '\0'; } if(added) { char *old_fen = game_details->tags[FEN_TAG]; free(old_fen); game_details->tags[FEN_TAG] = get_FEN_string(board); } } /* Set up a board structure for a new game. * This involves placing the pieces in their initial positions, * setting up castling and en-passant rights, and initialising * the hash positions. * If the fen argument is NULL then a completely new board is * setup, otherwise the indicated FEN position is returned. */ Board * new_game_board(const char *fen) { Board *new_board = NULL; static const Board initial_board ={ { { OFF, OFF, OFF, OFF, OFF, OFF, OFF, OFF, OFF, OFF, OFF, OFF}, { OFF, OFF, OFF, OFF, OFF, OFF, OFF, OFF, OFF, OFF, OFF, OFF}, { OFF, OFF, W(ROOK), W(KNIGHT), W(BISHOP), W(QUEEN), W(KING), W(BISHOP), W(KNIGHT), W(ROOK), OFF, OFF}, { OFF, OFF, W(PAWN), W(PAWN), W(PAWN), W(PAWN), W(PAWN), W(PAWN), W(PAWN), W(PAWN), OFF, OFF}, { OFF, OFF, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, OFF, OFF}, { OFF, OFF, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, OFF, OFF}, { OFF, OFF, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, OFF, OFF}, { OFF, OFF, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, OFF, OFF}, { OFF, OFF, B(PAWN), B(PAWN), B(PAWN), B(PAWN), B(PAWN), B(PAWN), B(PAWN), B(PAWN), OFF, OFF}, { OFF, OFF, B(ROOK), B(KNIGHT), B(BISHOP), B(QUEEN), B(KING), B(BISHOP), B(KNIGHT), B(ROOK), OFF, OFF}, { OFF, OFF, OFF, OFF, OFF, OFF, OFF, OFF, OFF, OFF, OFF, OFF}, { OFF, OFF, OFF, OFF, OFF, OFF, OFF, OFF, OFF, OFF, OFF, OFF} }, /* Who to move next. */ WHITE, /* Move number. */ 1, /* Castling rights. Support Chess960. */ 'h', 'a', 'h', 'a', /* Initial king positions. */ 'e', FIRSTRANK, 'e', LASTRANK, /* En Passant rights. */ FALSE, 0, 0, /* Initial hash value. */ 0ul, /* half-move_clock */ 0, }; /* Iterate over the columns. */ Col col; if (fen != NULL) { new_board = new_fen_board(fen); } /* Guard against failure of new_fen_board as well as the * normal game situation. */ if ((fen == NULL) || (new_board == NULL)) { /* Use the initial board setup. */ new_board = allocate_new_board(); *new_board = initial_board; } /* Generate the hash value for the initial position. */ for (col = FIRSTCOL; col <= LASTCOL; col++) { Rank rank; for (rank = FIRSTRANK; rank <= LASTRANK; rank++) { /* Find the basic components. */ Piece coloured_piece = new_board->board[ RankConvert(rank)][ColConvert(col)]; Piece piece = EXTRACT_PIECE(coloured_piece); Colour colour = EXTRACT_COLOUR(coloured_piece); if (coloured_piece != EMPTY) { new_board->weak_hash_value ^= hash_lookup(col, rank, piece, colour); } } } return new_board; } /* Print out the current occupant of the given square. */ static void print_square(Col col, Rank rank, const Board *board, FILE *outfp) { int r = RankConvert(rank); int c = ColConvert(col); Piece coloured_piece = board->board[r][c]; switch ((int) coloured_piece) { case W(PAWN): case W(KNIGHT): case W(BISHOP): case W(ROOK): case W(QUEEN): case W(KING): case B(PAWN): case B(KNIGHT): case B(BISHOP): case B(ROOK): case B(QUEEN): case B(KING): putc(coloured_piece_to_SAN_letter(coloured_piece), outfp); break; case EMPTY: putc('.', outfp); break; case OFF: putc('?', outfp); break; default: fprintf(GlobalState.logfile, "Attempt to print illegal square %c%c in print_square.\n", col, rank); break; } } /* Print out the contents of the given board. */ static void print_board(const Board *board, FILE *outfp) { Rank rank; Col col; for (rank = LASTRANK; rank >= FIRSTRANK; rank--) { for (col = FIRSTCOL; col <= LASTCOL; col++) { print_square(col, rank, board, outfp); } putc('\n', outfp); } putc('\n', outfp); } #if INCLUDE_UNUSED_FUNCTIONS /* Check the consistency of the board. */ static void check_board(const Board *board, const char *where) { Rank rank; Col col; for (rank = LASTRANK; rank >= FIRSTRANK; rank--) { for (col = FIRSTCOL; col <= LASTCOL; col++) { int r = RankConvert(rank); int c = ColConvert(col); switch (board->board[r][c]) { case W(PAWN): case W(KNIGHT): case W(BISHOP): case W(ROOK): case W(QUEEN): case W(KING): case B(PAWN): case B(KNIGHT): case B(BISHOP): case B(ROOK): case B(QUEEN): case B(KING): case EMPTY: break; default: fprintf(GlobalState.logfile, "%s: Illegal square %c%c (%u %u) contains %d.\n", where, col, rank, r, c, board->board[r][c]); report_details(GlobalState.logfile); abort(); break; } } } } #endif /* Return the number of half moves that have been completed * on the board. */ static int half_moves_played(const Board *board) { int half_moves = 2 * (board->move_number - 1); if (board->to_move == BLACK) { half_moves++; } return half_moves; } /* Implement move_details on the board. * Return TRUE if the move is ok, FALSE otherwise. * move_details is completed by the call to determine_move_details. * Thereafter, it is safe to make the move on board. */ Boolean apply_move(Move *move_details, Board *board) { /* Assume success. */ Boolean Ok = TRUE; Colour colour = board->to_move; if (determine_move_details(colour, move_details, board)) { Piece piece_to_move = move_details->piece_to_move; if (move_details->class != NULL_MOVE) { make_move(move_details->class, move_details->from_col, move_details->from_rank, move_details->to_col, move_details->to_rank, piece_to_move, colour, board); } /* See if there are any subsidiary actions. */ switch (move_details->class) { case PAWN_MOVE: case PIECE_MOVE: case ENPASSANT_PAWN_MOVE: /* Nothing more to do. */ break; case PAWN_MOVE_WITH_PROMOTION: if (move_details->promoted_piece != EMPTY) { /* Now make the promotion. */ make_move(move_details->class, move_details->to_col, move_details->to_rank, move_details->to_col, move_details->to_rank, move_details->promoted_piece, colour, board); } else { Ok = FALSE; } break; case KINGSIDE_CASTLE: break; case QUEENSIDE_CASTLE: break; case NULL_MOVE: /* Nothing more to do. */ break; case UNKNOWN_MOVE: default: Ok = FALSE; break; } /* Determine whether or not this move gives check. */ if (Ok) { move_details->check_status = king_is_in_check(board, OPPOSITE_COLOUR(colour)); if (move_details->check_status == CHECK) { /* See whether it is checkmate. */ if (king_is_in_checkmate(OPPOSITE_COLOUR(colour), board)) { move_details->check_status = CHECKMATE; } } /* Get ready for the next move. */ board->to_move = OPPOSITE_COLOUR(board->to_move); if (board->to_move == WHITE) { board->move_number++; } if (GlobalState.output_format == EPD || GlobalState.add_FEN_comments) { char epd[FEN_SPACE], fen_suffix[FEN_SPACE]; build_FEN_components(board, epd, fen_suffix); move_details->epd = copy_string(epd); move_details->fen_suffix = copy_string(fen_suffix); } } } else { Ok = FALSE; } return Ok; } /* Play out the moves on the given board. * These could be either the main line or a variation. * game_details is updated with the final_ and cumulative_ hash values. * Check move validity unless a NULL_MOVE has been found in this * variation. * Return TRUE if the game is valid and matches all matching criteria, * FALSE otherwise. */ static Boolean play_moves(Game *game_details, Board *board, Move *moves, unsigned max_depth, Boolean check_move_validity, Boolean mainline) { Boolean game_ok = TRUE; /* Ply number at which any error was found. */ int error_ply = 0; /* Force a match if we aren't looking for positional variations. */ Boolean game_matches = !GlobalState.positional_variations; Move *next_move = moves; /* Keep track of the final ECO match. */ EcoLog *eco_match = NULL; Boolean null_move_in_main_line = FALSE; /* Whether the fifty-move rule was available in the main line. */ Boolean N_move_rule_applies = FALSE; /* Number of ply, based on the current move number. */ unsigned plies = board->move_number * 2 - (board->to_move == WHITE ? 1 : 0); /* Whether there has been an underpromotion. */ Boolean underpromotion = FALSE; const char *match_label = NULL; /* Try the initial board position for a match. * This is required because the game might have been set up * from a FEN string, rather than being the normal starting * position. */ if (!game_matches && plies >= GlobalState.startply && (match_label = position_matches(board)) != NULL) { game_matches = TRUE; if (GlobalState.add_position_match_comments) { CommentList *comment = create_match_comment(board); comment->next = game_details->prefix_comment; game_details->prefix_comment = comment; } } /* Ensure that the RESULT_TAG (if present) is valid. */ if(game_details->tags[RESULT_TAG] != NULL && !valid_result(game_details->tags[RESULT_TAG])) { /* Assume an indefinite one rather than an invalid one. */ free((void *) game_details->tags[RESULT_TAG]); game_details->tags[RESULT_TAG] = copy_string("*"); } /* Keep going while the game is ok, and we have some more * moves and we haven't exceeded the search depth without finding * a match. */ while (game_ok && (next_move != NULL) && (game_matches || (plies <= max_depth))) { if (*(next_move->move) != '\0') { /* There might be a restriction on when to start checking for a match. */ Boolean check_for_match = plies >= GlobalState.startply; /* See if there are any variations associated with this move. */ if ((next_move->Variants != NULL) && GlobalState.keep_variations) { game_matches |= apply_variations(game_details, board, next_move->Variants, check_move_validity); } /* Now try the main move. */ if (next_move->class == NULL_MOVE) { null_move_in_main_line = TRUE; /* We might not be able to check the validity of * subsequent moves. */ #if 0 check_move_validity = FALSE; #endif } if (check_move_validity) { if (apply_move(next_move, board)) { /* Don't try for a positional match if we already have one. */ if (check_for_match && !game_matches && (match_label = position_matches(board)) != NULL) { game_matches = TRUE; if (GlobalState.add_position_match_comments) { CommentList *comment = create_match_comment(board); append_comments_to_move(next_move, comment); } } /* Combine this hash value with the cumulative one. */ game_details->cumulative_hash_value += board->weak_hash_value; if (check_for_match && GlobalState.fuzzy_match_duplicates) { /* Consider remembering this hash value for fuzzy matches. */ if (GlobalState.fuzzy_match_depth == plies) { /* Remember it. */ game_details->fuzzy_duplicate_hash = board->weak_hash_value; } } if (check_for_match && GlobalState.check_for_repetition > 0) { unsigned repetition = update_position_counts(game_details->position_counts, board); if (repetition >= GlobalState.check_for_repetition && GlobalState.add_position_match_comments) { CommentList *comment = create_match_comment(board); append_comments_to_move(next_move, comment); } } if (check_for_match && GlobalState.check_for_N_move_rule > 0 && mainline) { if (board->halfmove_clock >= 2 * GlobalState.check_for_N_move_rule) { /* N moves by both players with no pawn move or capture. */ N_move_rule_applies = TRUE; if (GlobalState.add_position_match_comments) { CommentList *comment = create_match_comment(board); append_comments_to_move(next_move, comment); } } } if(check_for_match && GlobalState.match_underpromotion && next_move->class == PAWN_MOVE_WITH_PROMOTION) { if(next_move->promoted_piece != QUEEN) { underpromotion = TRUE; } } if (next_move->next == NULL && mainline) { /* End of the game. */ if (check_for_match && GlobalState.fuzzy_match_duplicates && GlobalState.fuzzy_match_depth == 0) { game_details->fuzzy_duplicate_hash = board->weak_hash_value; } /* Ensure that the result tag is consistent with the * final status of the game. */ const char *result = game_details->tags[RESULT_TAG]; const char *corrected_result = NULL; if (result != NULL) { if (next_move->check_status == CHECKMATE) { if (board->to_move == BLACK) { if (strncmp(result, "1-0", 3) != 0) { if (GlobalState.fix_result_tags) { corrected_result = "1-0"; } else { fprintf(GlobalState.logfile, "Warning: Result of %s is inconsistent with checkmate by white in\n", result); report_details(GlobalState.logfile); print_error_context(GlobalState.logfile); } } } else { if (strncmp(result, "0-1", 3) != 0) { if (GlobalState.fix_result_tags) { corrected_result = "0-1"; } else { fprintf(GlobalState.logfile, "Warning: Result of %s is inconsistent with checkmate by black in\n", result); report_details(GlobalState.logfile); print_error_context(GlobalState.logfile); } } } } else if (is_stalemate(board, NULL)) { if (strncmp(result, "1/2", 3) != 0) { if (GlobalState.fix_result_tags) { corrected_result = "1/2-1/2"; } else { fprintf(GlobalState.logfile, "Warning: Result of %s is inconsistent with stalemate in\n", result); report_details(GlobalState.logfile); print_error_context(GlobalState.logfile); } } } if (corrected_result != NULL) { free((void *) result); game_details->tags[RESULT_TAG] = copy_string(corrected_result); if(next_move->terminating_result != NULL) { free((void *) next_move->terminating_result); next_move->terminating_result = NULL; } next_move->terminating_result = copy_string(corrected_result); } } /* Check for inconsistency between Result tag and game result. */ const char *move_result = next_move->terminating_result; const char *result_tag = game_details->tags[RESULT_TAG]; if (result_tag != NULL && move_result != NULL && strcmp(result_tag, move_result) != 0) { /* Mismatch. */ /* Whether to report it. */ Boolean report = TRUE; if (GlobalState.fix_result_tags) { if(strcmp(move_result, "*") == 0 || strcmp(result_tag, "*") == 0) { /* Prefer the move result. NB: This could lead to an inconsistency if there were no moves in the game because the move-less result is discarded and the Result tag's value is used in that case. However, if there are no moves the result is largely irrelevant. */ free((void *) result_tag); game_details->tags[RESULT_TAG] = copy_string(move_result); report = FALSE; } else { /* Irreconcilable conflict. */ } } else if(strcmp(move_result, "*") == 0) { /* Don't report this form of inconsistency. */ report = FALSE; } if(report) { print_error_context(GlobalState.logfile); fprintf(GlobalState.logfile, "Inconsistent result strings %s vs %s in the following game.\n", result_tag, move_result); report_details(GlobalState.logfile); if (GlobalState.reject_inconsistent_results) { game_ok = FALSE; } } } } if (GlobalState.add_ECO && !GlobalState.parsing_ECO_file) { int half_moves = half_moves_played(board); EcoLog *entry = eco_matches( board, game_details->cumulative_hash_value, half_moves); if (entry != NULL) { /* Consider keeping the match. * Could try to avoid spurious matches which become * more likely with larger ECO files and * the longer a game goes on. * Could be mitigated partly by preferring * an ECO line of exactly the same length as * the current game line. * Not currently implemented. */ if (eco_match == NULL) { /* We don't have one yet. */ eco_match = entry; } else { /* Keep it anyway. * This logic always prefers a longer match * to a shorter, irrespective of whether * either match is exact or not. */ eco_match = entry; } } } next_move = next_move->next; } else { print_error_context(GlobalState.logfile); fprintf(GlobalState.logfile, "Failed to make move %u%s %s in the game:\n", board->move_number, (board->to_move == WHITE) ? "." : "...", next_move->move); print_board(board, GlobalState.logfile); report_details(GlobalState.logfile); print_error_context(GlobalState.logfile); game_ok = FALSE; /* Work out where the error was. */ error_ply = 2 * board->move_number - 1; /* Check who has just moved. */ if (board->to_move == BLACK) { error_ply++; } } } else { /* Go through the motions as if the move were checked. */ board->to_move = OPPOSITE_COLOUR(board->to_move); if (board->to_move == WHITE) { board->move_number++; } next_move = next_move->next; } } else { /* An empty move. */ fprintf(GlobalState.logfile, "Internal error: Empty move in play_moves.\n"); report_details(GlobalState.logfile); game_ok = FALSE; /* Work out where the error was. */ error_ply = 2 * board->move_number - 1; /* Check who has just moved. */ if (board->to_move == BLACK) { error_ply++; } } plies++; } /* Record whether the full game was checked or not. */ game_details->moves_checked = next_move == NULL; if (null_move_in_main_line) { /* From v17.50: Don't automatically rule this game out. */ if(!GlobalState.allow_null_moves) { game_ok = FALSE; } } if (game_ok) { if (eco_match != NULL) { /* Free any details of the old one. */ if (game_details->tags[ECO_TAG] != NULL) { (void) free((void *) game_details->tags[ECO_TAG]); game_details->tags[ECO_TAG] = NULL; } if (game_details->tags[OPENING_TAG] != NULL) { (void) free((void *) game_details->tags[OPENING_TAG]); game_details->tags[OPENING_TAG] = NULL; } if (game_details->tags[VARIATION_TAG] != NULL) { (void) free((void *) game_details->tags[VARIATION_TAG]); game_details->tags[VARIATION_TAG] = NULL; } if (game_details->tags[SUB_VARIATION_TAG] != NULL) { (void) free((void *) game_details->tags[SUB_VARIATION_TAG]); game_details->tags[SUB_VARIATION_TAG] = NULL; } /* Add in the new one. */ if (eco_match->ECO_tag != NULL) { game_details->tags[ECO_TAG] = copy_string(eco_match->ECO_tag); } if (eco_match->Opening_tag != NULL) { game_details->tags[OPENING_TAG] = copy_string(eco_match->Opening_tag); } if (eco_match->Variation_tag != NULL) { game_details->tags[VARIATION_TAG] = copy_string(eco_match->Variation_tag); } if (eco_match->Sub_Variation_tag != NULL) { game_details->tags[SUB_VARIATION_TAG] = copy_string(eco_match->Sub_Variation_tag); } } if(game_matches && GlobalState.check_for_N_move_rule > 0 && mainline) { game_matches = N_move_rule_applies; } if(game_matches && GlobalState.match_underpromotion && ! underpromotion) { game_matches = FALSE; } /* Add a tag containing the matching FENPattern's label * if appropriate. */ if(GlobalState.add_matchlabel_tag && match_label != NULL && *match_label != '\0') { game_details->tags[MATCHLABEL_TAG] = copy_string(match_label); } } /* Fill in the hash value of the final position reached. */ game_details->final_hash_value = board->weak_hash_value; game_details->moves_ok = game_ok; game_details->error_ply = error_ply; if (!game_ok) { /* Decide whether to keep it anyway. */ if (GlobalState.keep_broken_games) { } #if 0 else if (GlobalState.positional_variations) { /* NB: Not rejecting the match when a game is not ok led to * a memory bug in v18-10. This is now fixed so this code * could be retained. However, ... * I think I felt it appropriate to retain broken games * when positional variations are being sought so that games * to be truncated before the end would still be matched, but it * has the consequence that errors are reported twice and broken * games may be counted as matches. * For the time being, I am closing this route but it might * be appropriate to re-open it at some point. */ } #endif else { /* Only return a match if it genuinely matched a variation * in which we were interested. */ /* We can't have found a genuine match. */ game_matches = FALSE; } } return game_matches; } /* Play out the moves of an ECO line on the given board. * game_details is updated with the final_ and cumulative_ hash * values. */ static void play_eco_moves(Game *game_details, Board *board, Move *moves) { Boolean game_ok = TRUE; /* Ply number at which any error was found. */ int error_ply = 0; Move *next_move = moves; /* Keep going while the game is ok, and we have some more * moves and we haven't exceeded the search depth without finding * a match. */ while (game_ok && (next_move != NULL)) { if (*(next_move->move) != '\0') { /* Ignore variations. */ if (apply_move(next_move, board)) { /* Combine this hash value to the cumulative one. */ game_details->cumulative_hash_value += board->weak_hash_value; next_move = next_move->next; } else { print_error_context(GlobalState.logfile); fprintf(GlobalState.logfile, "Failed to make move %u%s %s in the game:\n", board->move_number, (board->to_move == WHITE) ? "." : "...", next_move->move); print_board(board, GlobalState.logfile); report_details(GlobalState.logfile); print_error_context(GlobalState.logfile); game_ok = FALSE; /* Work out where the error was. */ error_ply = 2 * board->move_number - 1; /* Check who has just moved. */ if (board->to_move == BLACK) { error_ply++; } } } else { /* An empty move. */ fprintf(GlobalState.logfile, "Internal error: Empty move in play_eco_moves.\n"); report_details(GlobalState.logfile); game_ok = FALSE; /* Work out where the error was. */ error_ply = 2 * board->move_number - 1; /* Check who has just moved. */ if (board->to_move == BLACK) { error_ply++; } } } /* Record whether the full game was checked or not. */ game_details->moves_checked = next_move == NULL; /* Fill in the hash value of the final position reached. */ game_details->final_hash_value = board->weak_hash_value; game_details->moves_ok = game_ok; game_details->error_ply = error_ply; } /* Play out a variation. * Check move validity unless a NULL_MOVE has been found in this * variation. * Return TRUE if the variation matches a position that * we are looking for. */ static Boolean apply_variations(const Game *game_details, const Board *board, Variation *variation, Boolean check_move_validity) { /* Force a match if we aren't looking for positional variations. */ Boolean variation_matches = GlobalState.positional_variations ? FALSE : TRUE; /* Allocate space for the copies. * Allocation is done, rather than relying on local copies in the body * of the loop because the recursive nature of this function has * resulted in stack overflow on the PC version. */ Game *copy_game = (Game *) malloc_or_die(sizeof (*copy_game)); Board *copy_board = allocate_new_board(); while (variation != NULL) { /* Work on the copies. */ *copy_game = *game_details; *copy_board = *board; /* Don't look for repetitions. */ copy_game->position_counts = NULL; /* We only need one variation to match to declare a match. * Play out the variation to its full depth, because we * will want the full move information if the main line * later matches. */ variation_matches |= play_moves(copy_game, copy_board, variation->moves, DEFAULT_POSITIONAL_DEPTH, check_move_validity, FALSE); variation = variation->next; } (void) free((void *) copy_game); free_board(copy_board); return variation_matches; } /* game_details contains a complete move score. * Try to apply each move on a new board. * Store in plycount the number of ply played. * Return TRUE if the game matches a variation that we are * looking for. */ Boolean apply_move_list(Game *game_details, unsigned *plycount, unsigned max_depth) { Move *moves = game_details->moves; Board *board = new_game_board(game_details->tags[FEN_TAG]); Boolean game_matches; /* Ensure that we have a sensible search depth. */ if (max_depth == 0) { /* No positional variations specified. */ max_depth = DEFAULT_POSITIONAL_DEPTH; } /* Start off the cumulative hash value. */ game_details->cumulative_hash_value = 0; if (GlobalState.check_for_repetition > 0 && game_details->position_counts == NULL) { game_details->position_counts = new_position_count_list(board); } /* Play through the moves and see if we have a match. * Check move validity. */ game_matches = play_moves(game_details, board, moves, max_depth, TRUE, TRUE); /* Record how long the game was. */ if (board->to_move == BLACK) { *plycount = 2 * (board->move_number - 1) + 1; } else { /* This move number hasn't been played. */ *plycount = 2 * (board->move_number - 1); } if (game_matches) { game_matches = check_for_only_stalemate(board, moves); } free_board(board); return game_matches; } /* game_details contains a complete move score. * Try to apply each move on a new board. * Store in number_of_moves the length of the game. * Return the final Board if the game is ok, otherwise NULL. */ Board * apply_eco_move_list(Game *game_details, unsigned *number_of_half_moves) { Move *moves = game_details->moves; Board *board = new_game_board(game_details->tags[FEN_TAG]); /* Start off the cumulative hash value. */ game_details->cumulative_hash_value = 0; play_eco_moves(game_details, board, moves); /* Record how long the game was. */ *number_of_half_moves = half_moves_played(board); if(! game_details->moves_ok) { free_board(board); board = NULL; } return board; } /* Return the string associated with the given piece. */ const char * piece_str(Piece piece) { if (piece < NUM_PIECE_VALUES) { return output_piece_characters[piece]; } else { return "?"; } } /* Rewrite move_details->move according to the details held * within the structure and the current state of the board. */ static Boolean rewrite_SAN_string(Colour colour, Move *move_details, Board *board) { Boolean Ok = TRUE; if (move_details == NULL) { /* Shouldn't happen. */ fprintf(GlobalState.logfile, "Internal error: NULL move details in rewrite_SAN_string.\n"); Ok = FALSE; } else if (move_details->move[0] == '\0') { /* Shouldn't happen. */ fprintf(GlobalState.logfile, "Empty move in rewrite_SAN_string.\n"); Ok = FALSE; } else { const unsigned char *move = move_details->move; MoveClass class = move_details->class; MovePair *move_list = NULL; Col to_col = move_details->to_col; Rank to_rank = move_details->to_rank; unsigned char new_move_str[MAX_MOVE_LEN + 1] = ""; switch (class) { case PAWN_MOVE: case ENPASSANT_PAWN_MOVE: case PAWN_MOVE_WITH_PROMOTION: move_list = find_pawn_moves(move_details->from_col, '0', to_col, to_rank, colour, board); break; case PIECE_MOVE: switch (move_details->piece_to_move) { case KING: move_list = find_king_moves(to_col, to_rank, colour, board); break; case QUEEN: move_list = find_queen_moves(to_col, to_rank, colour, board); break; case ROOK: move_list = find_rook_moves(to_col, to_rank, colour, board); break; case KNIGHT: move_list = find_knight_moves(to_col, to_rank, colour, board); break; case BISHOP: move_list = find_bishop_moves(to_col, to_rank, colour, board); break; default: fprintf(GlobalState.logfile, "Unknown piece move %s\n", move); Ok = FALSE; break; } break; case KINGSIDE_CASTLE: case QUEENSIDE_CASTLE: /* No move list to prepare. */ break; case NULL_MOVE: /* No move list to prepare. */ break; case UNKNOWN_MOVE: default: fprintf(GlobalState.logfile, "Unknown move class in rewrite_SAN_string(%d).\n", move_details->class); Ok = FALSE; break; } if (move_list != NULL) { move_list = exclude_checks(move_details->piece_to_move, colour, move_list, board); } if ((move_list == NULL) && (class != KINGSIDE_CASTLE) && (class != QUEENSIDE_CASTLE) && (class != NULL_MOVE)) { Ok = FALSE; } /* We should now have enough information in move_details to compose a * SAN string. */ if (Ok) { size_t new_move_index = 0; switch (class) { case PAWN_MOVE: case ENPASSANT_PAWN_MOVE: case PAWN_MOVE_WITH_PROMOTION: /* See if we need to give the source column. */ if (move_details->captured_piece != EMPTY) { new_move_str[new_move_index] = move_details->from_col; new_move_index++; new_move_str[new_move_index] = 'x'; new_move_index++; } else if (move_list->next != NULL) { new_move_str[new_move_index] = move_details->from_col; new_move_index++; } /* Add in the destination. */ new_move_str[new_move_index] = to_col; new_move_index++; new_move_str[new_move_index] = to_rank; new_move_index++; if (class == PAWN_MOVE_WITH_PROMOTION) { const char *promoted_piece = piece_str(move_details->promoted_piece); new_move_str[new_move_index] = '='; new_move_index++; strcpy((char *) &new_move_str[new_move_index], promoted_piece); new_move_index += strlen(promoted_piece); } new_move_str[new_move_index] = '\0'; break; case PIECE_MOVE: { const char *piece = piece_str(move_details->piece_to_move); strcpy((char *) &new_move_str[0], piece); new_move_index += strlen(piece); /* Check for the need to disambiguate. */ if (move_list->next != NULL) { /* It is necessary. Count how many times * the from_ col and rank occur in the list * of possibles in order to determine which to use * for this purpose. */ int col_times = 0, rank_times = 0; MovePair *possible; Col from_col = move_details->from_col; Rank from_rank = move_details->from_rank; for (possible = move_list; possible != NULL; possible = possible->next) { if (possible->from_col == from_col) { col_times++; } if (possible->from_rank == from_rank) { rank_times++; } } if (col_times == 1) { /* Use the col. */ new_move_str[new_move_index] = from_col; new_move_index++; } else if (rank_times == 1) { /* Use the rank. */ new_move_str[new_move_index] = from_rank; new_move_index++; } else { /* Use both. */ new_move_str[new_move_index] = from_col; new_move_index++; new_move_str[new_move_index] = from_rank; new_move_index++; } } /* See if a capture symbol is needed. */ if (move_details->captured_piece != EMPTY) { new_move_str[new_move_index] = 'x'; new_move_index++; } /* Add in the destination. */ new_move_str[new_move_index] = to_col; new_move_index++; new_move_str[new_move_index] = to_rank; new_move_index++; new_move_str[new_move_index] = '\0'; } break; case KINGSIDE_CASTLE: strcpy((char *) new_move_str, "O-O"); break; case QUEENSIDE_CASTLE: strcpy((char *) new_move_str, "O-O-O"); break; case NULL_MOVE: strcpy((char *) new_move_str, (char *) NULL_MOVE_STRING); break; case UNKNOWN_MOVE: default: Ok = FALSE; break; } if (Ok) { if (move_details->check_status != NOCHECK) { if (move_details->check_status == CHECK) { /* It isn't mate. */ strcat((char *) new_move_str, "+"); } else { if (GlobalState.output_format == CM) { strcat((char *) new_move_str, "++"); } else { strcat((char *) new_move_str, "#"); } } } } /* Update the move_details structure with the new string. */ strcpy((char *) move_details->move, (const char *) new_move_str); } if (move_list != NULL) { free_move_pair_list(move_list); } } return Ok; } /* Apply the move to board and rewrite move_details->move unless * the output format is the original source. * Return TRUE if the move is ok, FALSE otherwise. */ static Boolean rewrite_move(Game *game, Colour colour, Move *move_details, Board *board) { /* Assume success. */ Boolean Ok = TRUE; if(move_details->class == UNKNOWN_MOVE) { Ok = FALSE; } else if (GlobalState.output_format == SOURCE || rewrite_SAN_string(colour, move_details, board)) { Piece piece_to_move = move_details->piece_to_move; MoveClass class = move_details->class; Col castling_rook_col = '\0'; /* Try to accommodate Chess960 castling format where the King * is moved to the position of the Rook. * NB: The contents of the Variant tag vary a lot for * chess 960 games.. */ if ((class == KINGSIDE_CASTLE || class == QUEENSIDE_CASTLE) && game != NULL && game->tags[VARIANT_TAG] != NULL && strstr(game->tags[VARIANT_TAG], "960") != NULL) { /* Remember where the Rook was before the move is applied. */ castling_rook_col = find_castling_rook_col(colour, board, class); } if (class != NULL_MOVE) { make_move(class, move_details->from_col, move_details->from_rank, move_details->to_col, move_details->to_rank, piece_to_move, colour, board); if(castling_rook_col != '\0') { /* Rewrite the King's target column to be the original * position of the Rook to accommodate Chess960 format. */ move_details->to_col = castling_rook_col; } } /* See if there are any subsidiary actions. */ switch (class) { case PAWN_MOVE: case PIECE_MOVE: case ENPASSANT_PAWN_MOVE: case KINGSIDE_CASTLE: case QUEENSIDE_CASTLE: /* Nothing more to do. */ break; case PAWN_MOVE_WITH_PROMOTION: if (move_details->promoted_piece != EMPTY) { /* @@@ Do promoted moves have '+' properly appended? */ /* Now make the promotion. */ make_move(class, move_details->to_col, move_details->to_rank, move_details->to_col, move_details->to_rank, move_details->promoted_piece, colour, board); } else { Ok = FALSE; } break; case NULL_MOVE: /* Nothing more. */ break; case UNKNOWN_MOVE: default: Ok = FALSE; break; } } else { Ok = FALSE; } return Ok; } /* Rewrite the list of moves by playing through the game. * If game is NULL then the moves are a variation rather than * the main line. */ static Boolean rewrite_moves(Game *game, Board *board, Move *moves) { Boolean game_ok = TRUE; Move *move_details = moves; int plies_to_drop = GlobalState.drop_ply_number; while (game_ok && (move_details != NULL)) { if (*(move_details->move) != '\0') { /* See if there are any variations associated with this move. */ if ((move_details->Variants != NULL) && GlobalState.keep_variations && !rewrite_variations(board, move_details->Variants)) { /* Something wrong with the variations. */ game_ok = FALSE; } if (rewrite_move(game, board->to_move, move_details, board)) { if(move_details->class == NULL_MOVE && game != NULL) { /* NULL_MOVE not allowed in the main line. */ } board->to_move = OPPOSITE_COLOUR(board->to_move); if (board->to_move == WHITE) { board->move_number++; } if (GlobalState.output_evaluation) { move_details->evaluation = evaluate(board); } if (GlobalState.add_hashcode_comments) { /* Append a hashcode comment using the new state of the board * with the move having been played. */ move_details->zobrist = generate_zobrist_hash_from_board(board); } if(GlobalState.drop_comment_pattern != NULL && move_details->comment_list != NULL) { if(find_matching_comment(GlobalState.drop_comment_pattern, move_details->comment_list) != NULL) { /* We have a match. */ plies_to_drop = (board->move_number - 1) * 2; if(board->to_move == BLACK) { plies_to_drop++; } } } /* See if a comment should be replaced by a FEN comment. */ if(GlobalState.FEN_comment_pattern != NULL && move_details->comment_list != NULL) { StringList *comment_to_replace = find_matching_comment(GlobalState.FEN_comment_pattern, move_details->comment_list); if(comment_to_replace != NULL) { /* Replace it. */ (void) free((void *) comment_to_replace->str); comment_to_replace->str = get_FEN_string(board); } } move_details = move_details->next; } else { /* Broken games that are being kept have probably already been * reported, so don't repeat the error. */ if (!GlobalState.keep_broken_games) { fprintf(GlobalState.logfile, "Failed to rewrite move %u%s %s in the game:\n", board->move_number, (board->to_move == WHITE) ? "." : "...", move_details->move); report_details(GlobalState.logfile); print_error_context(GlobalState.logfile); print_board(board, GlobalState.logfile); } game_ok = FALSE; } } else { /* An empty move. */ fprintf(GlobalState.logfile, "Internal error: Empty move in rewrite_moves.\n"); report_details(GlobalState.logfile); game_ok = FALSE; } } if (!game_ok) { if(GlobalState.keep_broken_games && move_details != NULL) { /* Try to place the remaining moves into a comment. */ CommentList *comment = (CommentList*) malloc_or_die(sizeof (*comment)); /* Break the link from the previous move. */ Move *prev; StringList *commented_move_list = NULL; /* Find the move before the current one, if any. */ if (move_details == moves) { /* Broken from the first move. */ prev = NULL; } else { prev = moves; while (prev != NULL && prev->next != move_details) { prev = prev->next; } if (prev != NULL) { prev->next = NULL; } } /* Build the comment string. */ char *terminating_result = NULL; while (move_details != NULL) { commented_move_list = save_string_list_item(commented_move_list, (char *) move_details->move); if (move_details->next == NULL) { /* Remove the terminating result. */ terminating_result = move_details->terminating_result; move_details->terminating_result = NULL; } move_details = move_details->next; } comment->comment = commented_move_list; comment->next = NULL; if (prev != NULL) { prev->terminating_result = terminating_result; append_comments_to_move(prev, comment); } else { /* The whole game is broken. */ if (game != NULL) { if (terminating_result != NULL) { commented_move_list = save_string_list_item(commented_move_list, terminating_result); } game->prefix_comment = comment; game->moves = NULL; } } } } else if(plies_to_drop != 0 && game != NULL) { if(plies_to_drop < 0) { unsigned plies = plies_in_move_sequence(moves); plies_to_drop = plies + plies_to_drop; } if(plies_to_drop >= 0) { game_ok = drop_plies_from_start(game, moves, plies_to_drop); } else { game_ok = FALSE; } } return game_ok; } /* Rewrite the list of variations. * Return TRUE if the variation are ok. a position that */ static Boolean rewrite_variations(const Board *board, Variation *variation) { Board *copy_board = allocate_new_board(); Boolean variations_ok = TRUE; while ((variation != NULL) && variations_ok) { /* Work on the copy. */ *copy_board = *board; variations_ok = rewrite_moves((Game *) NULL, copy_board, variation->moves); variation = variation->next; } free_board(copy_board); return variations_ok; } /* moves contains a complete game score. * Try to rewrite each move into SAN as it is played on a new board. * Return the final Board position if the game was rewritten alright, * NULL otherwise. */ Board * rewrite_game(Game *current_game) { Board *board = new_game_board(current_game->tags[FEN_TAG]); Boolean game_ok; /* No null-move found at the start of the game. */ game_ok = rewrite_moves(current_game, board, current_game->moves); if (game_ok) { } else if (GlobalState.keep_broken_games) { } else { free_board(board); board = NULL; } return board; } /* Return the number of plies in the given move * sequence. */ static unsigned plies_in_move_sequence(Move *moves) { int num_plies = 0; while(moves != NULL) { moves = moves->next; num_plies++; } return num_plies; } /* Drop the given number of play from the start of this game. * If plies_to_drop is negative, drop all but that number * from the beginning. * Return FALSE if there are insufficient. * If there are sufficient replace the game's FEN tag with * the revised starting state. */ static Boolean drop_plies_from_start(Game *game, Move *moves, int plies_to_drop) { char *fen = game->tags[FEN_TAG]; Board *board = new_game_board(fen); Boolean game_ok = TRUE; int plies = 0; Move *new_head = moves; if(plies_to_drop > 0 && game->prefix_comment != NULL) { /* Don't free the prefix_comment because that is * handled elsewhere. */ game->prefix_comment = NULL; } while(plies < plies_to_drop && new_head != NULL && game_ok) { /* Rewriting is not strictly necessary, because it has already been done, * but it provides all the required functionality. */ if (rewrite_move(game, board->to_move, new_head, board)) { board->to_move = OPPOSITE_COLOUR(board->to_move); if (board->to_move == WHITE) { board->move_number++; } plies++; new_head = new_head->next; } else { game_ok = FALSE; } } if(plies == plies_to_drop && game_ok) { if(fen != NULL) { (void) free((void *) fen); } #if 0 /* Reset the move number. */ board->move_number = 1; #endif game->moves = new_head; game->tags[FEN_TAG] = get_FEN_string(board); game->tags[SETUP_TAG] = copy_string("1"); if(game->tags[PLY_COUNT_TAG] != NULL || GlobalState.output_plycount) { add_plycount(game); } if(game->tags[TOTAL_PLY_COUNT_TAG] != NULL || GlobalState.output_total_plycount) { add_total_plycount(game, GlobalState.keep_variations); } /* Make sure we aren't dropping 0 moves. */ if(new_head != moves) { /* Free the dropped moves. */ Move *move_to_drop = moves; while(move_to_drop->next != new_head) { move_to_drop = move_to_drop->next; } move_to_drop->next = NULL; free_move_list(moves); } } else { game_ok = FALSE; } (void) free((void *) board); return game_ok; } /* Define a table to hold the positional hash codes of interest. * Size should be a prime number for collision avoidance. */ #define MAX_NON_POLYGLOT_CODE 541 static HashLog *non_polyglot_codes_of_interest[MAX_NON_POLYGLOT_CODE]; /* Whether or not the non-polyglot hashcodes are in use. */ Boolean using_non_polyglot = FALSE; /* move_details is either the start of a variation in which we are interested * or it is NULL. * fen is either a position we are interested in or it is NULL. * Generate and store the hash value for the variation, or the FEN * position in non_polyglot_codes_of_interest. */ void store_hash_value(Move *move_details, const char *fen) { Move *move = move_details; Board *board = new_game_board(fen); Boolean Ok = TRUE; while ((move != NULL) && Ok) { /* Reset print_move number if a variation was printed. */ if (*(move->move) == '\0') { /* A comment node, not a proper move. */ move = move->next; } else if (apply_move(move, board)) { move = move->next; } else { print_error_context(GlobalState.logfile); fprintf(GlobalState.logfile, "Failed to make move %u%s %s\n", board->move_number, (board->to_move == WHITE) ? "." : "...", move->move); Ok = FALSE; } } if (Ok) { HashLog *entry = (HashLog *) malloc_or_die(sizeof (*entry)); unsigned ix = board->weak_hash_value % MAX_NON_POLYGLOT_CODE; /* We don't include the cumulative hash value as the sequence * of moves to reach this position is not important. */ entry->cumulative_hash_value = 0; entry->final_hash_value = board->weak_hash_value; /* Link it into the head at this index. */ entry->next = non_polyglot_codes_of_interest[ix]; non_polyglot_codes_of_interest[ix] = entry; using_non_polyglot = TRUE; } else { exit(1); } free_board(board); } /* Define a table to hold the positional hash codes of interest. * Size should be a prime number for collision avoidance. */ #define MAX_POLYGLOT_CODE 541 static HashLog *polyglot_codes_of_interest[MAX_POLYGLOT_CODE]; /* Whether or not the polyglot hashcodes are in use. */ static Boolean using_polyglot = FALSE; /** * Convert the given hex string to an int and save it * for position matching. * @param value A hexadecimal string of up to 16 characters. * @return TRUE if the value is decoded ok; FALSE otherwise. */ Boolean save_polyglot_hashcode(const char *value) { Boolean Ok; if(value != NULL && *value != '\0') { size_t len = strspn(value, "0123456789abcdefABCDEF"); if(len > 0 && value[len] == '\0') { /* Versions prior to 22-02 failed to convert correctly values shorter * than 16 characters. */ uint64_t hash = 0x0; char *end; hash = strtoull(value, &end, 16); Ok = (errno == 0 && *end == '\0'); if (Ok) { HashLog *entry = (HashLog *) malloc_or_die(sizeof (*entry)); unsigned ix = hash % MAX_POLYGLOT_CODE; /* We don't include the cumulative hash value as the sequence * of moves to reach this position is not important. */ entry->cumulative_hash_value = 0; entry->final_hash_value = hash; /* Link it into the head at this index. */ entry->next = polyglot_codes_of_interest[ix]; polyglot_codes_of_interest[ix] = entry; using_polyglot = TRUE; } else { fprintf(GlobalState.logfile, "Unrecognised hash value %s\n", value); } } else { Ok = FALSE; } } else { Ok = FALSE; } return Ok; } /* Does the current board match a position of interest. * Look in codes_of_interest for current_hash_value. * Return NULL if no match, otherwise a possible label for the * match to be added to the game's tags. An empty string is * used for no label. */ static const char * position_matches(const Board *board) { Boolean found = FALSE; if(using_non_polyglot) { HashCode current_hash_value = board->weak_hash_value; unsigned ix = current_hash_value % MAX_NON_POLYGLOT_CODE; for (HashLog *entry = non_polyglot_codes_of_interest[ix]; !found && (entry != NULL); entry = entry->next) { /* We can test against just the position value. */ if (entry->final_hash_value == current_hash_value) { found = TRUE; } } } if(!found && using_polyglot) { uint64_t current_hash_value = generate_zobrist_hash_from_board(board); unsigned ix = current_hash_value % MAX_POLYGLOT_CODE; for (HashLog *entry = polyglot_codes_of_interest[ix]; !found && (entry != NULL); entry = entry->next) { /* We can test against just the position value. */ if (entry->final_hash_value == current_hash_value) { found = TRUE; } } } if(found && GlobalState.whose_move != EITHER_TO_MOVE) { if(board->to_move == WHITE && GlobalState.whose_move == BLACK_TO_MOVE) { found = FALSE; } else if(board->to_move == BLACK && GlobalState.whose_move == WHITE_TO_MOVE) { found = FALSE; } } if (found) { return ""; } else { const char *match_label = pattern_match_board(board); if(match_label != NULL && GlobalState.whose_move != EITHER_TO_MOVE) { if(board->to_move == WHITE && GlobalState.whose_move == BLACK_TO_MOVE) { match_label = NULL; } else if(board->to_move == BLACK && GlobalState.whose_move == WHITE_TO_MOVE) { match_label = NULL; } } return match_label; } } /* Build a basic EPD string from the given board. */ void build_basic_EPD_string(const Board *board, char *epd) { Rank rank; int ix = 0; #if 0 Boolean castling_allowed; #endif /* The board. */ for (rank = LASTRANK; rank >= FIRSTRANK; rank--) { Col col; int consecutive_spaces = 0; for (col = FIRSTCOL; col <= LASTCOL; col++) { int coloured_piece = board->board[RankConvert(rank)][ColConvert(col)]; if (coloured_piece != EMPTY) { if (consecutive_spaces > 0) { epd[ix] = '0' + consecutive_spaces; ix++; consecutive_spaces = 0; } epd[ix] = coloured_piece_to_SAN_letter(coloured_piece); ix++; } else { consecutive_spaces++; } } if (consecutive_spaces > 0) { epd[ix] = '0' + consecutive_spaces; ix++; } /* Terminate the row. */ if (rank != FIRSTRANK) { epd[ix] = '/'; ix++; } } epd[ix] = ' '; ix++; epd[ix] = board->to_move == WHITE ? 'w' : 'b'; ix++; epd[ix] = ' '; ix++; /* Castling details. */ #if 1 /* Try to cover Chess960 requirements. */ if (board->WKingCastle == '\0' && board->WQueenCastle == '\0' && board->BKingCastle == '\0' && board->BQueenCastle == '\0') { epd[ix] = '-'; ix++; } else { if (board->WKingCastle != '\0' || board->WQueenCastle != '\0') { /* At least one White castling right. */ if (board->WKingCastle != '\0' && board->WQueenCastle != '\0') { /* Both possible. */ if (board->WKingCastle > board->WQueenCastle) { /* Regular notation. * NB: This loses square specifics, but that seems to be ok in * X-FEN according to https://en.wikipedia.org/wiki/X-FEN */ epd[ix] = 'K'; ix++; epd[ix] = 'Q'; ix++; } else { /* Out of order so store Rook cols. */ epd[ix] = toupper(board->WKingCastle); ix++; epd[ix] = toupper(board->WQueenCastle); ix++; } } else { /* Only one. */ if (board->WKingCastle != '\0') { if (board->WKingCastle != LASTCOL) { epd[ix] = toupper(board->WKingCastle); ix++; } else { /* Only kingside, from the regular position. */ epd[ix] = 'K'; ix++; } } else { if (board->WQueenCastle != FIRSTCOL) { epd[ix] = toupper(board->WQueenCastle); ix++; } else { /* Only queenside, from the regular position. */ epd[ix] = 'Q'; ix++; } } } } else { /* Only Black castling. */ } /* Same for the queenside. */ if (board->BKingCastle != '\0' || board->BQueenCastle != '\0') { /* At least one Black castling right. */ if (board->BKingCastle != '\0' && board->BQueenCastle != '\0') { /* Both possible. */ if (board->BKingCastle > board->BQueenCastle) { /* Regular notation. */ epd[ix] = 'k'; ix++; epd[ix] = 'q'; ix++; } else { /* Out of order. */ epd[ix] = board->BKingCastle; ix++; epd[ix] = board->BQueenCastle; ix++; } } else { /* Only one. */ if (board->BKingCastle != '\0') { if (board->BKingCastle != LASTCOL) { epd[ix] = board->BKingCastle; ix++; } else { /* Only kingside, from the regular position. */ epd[ix] = 'k'; ix++; } } else { if (board->BQueenCastle != FIRSTCOL) { epd[ix] = board->BQueenCastle; ix++; } else { /* Only queenside, from the regular position. */ epd[ix] = 'q'; ix++; } } } } else { /* Only White castling. */ } } #else castling_allowed = FALSE; if (board->WKingCastle != '\0') { epd[ix] = 'K'; ix++; castling_allowed = TRUE; } if (board->WQueenCastle != '\0') { epd[ix] = 'Q'; ix++; castling_allowed = TRUE; } if (board->BKingCastle != '\0') { epd[ix] = 'k'; ix++; castling_allowed = TRUE; } if (board->BQueenCastle != '\0') { epd[ix] = 'q'; ix++; castling_allowed = TRUE; } if (!castling_allowed) { /* There are no castling rights. */ epd[ix] = '-'; ix++; } #endif epd[ix] = ' '; ix++; /* Enpassant. */ if (board->EnPassant) { /* It might be required to suppress redundant ep info. */ Boolean suppress = FALSE; if (GlobalState.suppress_redundant_ep_info) { /* Determine whether the ep indication is redundant or not. * Assume that it is unless there is a pawn in position to * take advantage of it. */ Boolean redundant = TRUE; Col ep_col = board->ep_col; Rank from_rank; Piece pawn; if (board->to_move == WHITE) { /* White pawn on the fifth rank capturing a black pawn. */ from_rank = '5'; pawn = W(PAWN); } else { /* Black pawn on the fourth rank capturing a white pawn. */ from_rank = '4'; pawn = B(PAWN); } if ((ep_col > FIRSTCOL) && (board->board[RankConvert(from_rank)][ColConvert(ep_col - 1)] == pawn)) { /* Check that the move does not leave the king in check. */ Board copy_board = *board; make_move(UNKNOWN_MOVE, ep_col - 1, from_rank, board->ep_col, board->ep_rank, PAWN, board->to_move, ©_board); if (king_is_in_check(©_board, copy_board.to_move) == NOCHECK) { redundant = FALSE; } } if (redundant && (ep_col < LASTCOL) && (board->board[RankConvert(from_rank)][ColConvert(ep_col + 1)] == pawn)) { /* Check that the move does not leave the king in check. */ Board copy_board = *board; make_move(UNKNOWN_MOVE, ep_col + 1, from_rank, board->ep_col, board->ep_rank, PAWN, board->to_move, ©_board); if (king_is_in_check(©_board, copy_board.to_move) == NOCHECK) { redundant = FALSE; } } suppress = redundant; } if (suppress) { epd[ix] = '-'; ix++; } else { epd[ix] = board->ep_col; ix++; epd[ix] = board->ep_rank; ix++; } } else { epd[ix] = '-'; ix++; } epd[ix] = '\0'; } /* Build and return a FEN string for the given board. */ char *get_FEN_string(const Board *board) { char epd[FEN_SPACE], fen_suffix[FEN_SPACE]; build_FEN_components(board, epd, fen_suffix); char *FEN_string = (char *) malloc_or_die(strlen(epd) + 1 + strlen(fen_suffix) + 1); sprintf(FEN_string, "%s %s", epd, fen_suffix); return FEN_string; } /* Build a FEN string from the given board. * Place the EPD portion in epd and the half-move * count and following in fen_suffix. */ static void build_FEN_components(const Board *board, char *epd, char *fen_suffix) { build_basic_EPD_string(board, epd); /* Format the (pseudo) half move count and the full move count. */ size_t ix = 0; /* Half moves since the last capture or pawn move. */ sprintf(fen_suffix, "%u", board->halfmove_clock); ix = strlen(fen_suffix); fen_suffix[ix] = ' '; ix++; /* The full move number. */ sprintf(&fen_suffix[ix], "%u", board->move_number); } #if 0 /* Append to move_details a FEN comment of the board. * The board state is immediately following application of the * given move. */ static void append_FEN_comment(Move *move_details, const Board *board) { CommentList *comment = (CommentList*) malloc_or_die(sizeof (*comment)); StringList *current_comment = save_string_list_item(NULL, get_FEN_string(board)); comment->comment = current_comment; comment->next = NULL; append_comments_to_move(move_details, comment); } /* Maximum length of a 64-bit unsigned int in decimal. * NB: At the moment, the output is hex, which requires * only 16 characters. */ #define HASH_64_BIT_SPACE 20 /* Append to move_details a hashcode comment from the state of the board. * The board state is immediately following application of the * given move. */ static void append_hashcode_comment(Move *move_details, Board *board) { uint64_t hash = generate_zobrist_hash_from_board(board); char *hashcode_comment = (char *) malloc_or_die(HASH_64_BIT_SPACE + 1); CommentList *comment = (CommentList*) malloc_or_die(sizeof (*comment)); StringList *current_comment = save_string_list_item(NULL, hashcode_comment); sprintf(hashcode_comment, "%016" PRIx64, hash); comment->comment = current_comment; comment->next = NULL; append_comments_to_move(move_details, comment); } /* Append to move_details an evaluation value for board. * The board state is immediately following application of the * given move. */ static void append_evaluation(Move *move_details, const Board *board) { CommentList *comment = (CommentList*) malloc_or_die(sizeof (*comment)); /* Space template for the value. * @@@ There is a buffer-overflow risk here if the evaluation value * is too large. */ const char valueSpace[] = "-012456789.00"; char *evaluation = (char *) malloc_or_die(sizeof (valueSpace)); StringList *current_comment; double value = evaluate(board); /* @@@ Overflow possible here if the value is too big to fit. */ sprintf(evaluation, "%.2f", value); if (strlen(evaluation) > strlen(valueSpace)) { fprintf(GlobalState.logfile, "Overflow in evaluation space in append_evaluation()\n"); exit(1); } current_comment = save_string_list_item(NULL, evaluation); comment->comment = current_comment; comment->next = NULL; append_comments_to_move(move_details, comment); } #endif /* Create a comment indicating in a positional match. */ CommentList * create_match_comment(const Board *board) { /* The comment string. */ char *match_comment; if(strcmp(GlobalState.position_match_comment, "FEN") != 0) { match_comment = copy_string(GlobalState.position_match_comment); } else { match_comment = get_FEN_string(board); } StringList *current_comment = save_string_list_item(NULL, match_comment); CommentList *comment = (CommentList*) malloc_or_die(sizeof (*comment)); comment->comment = current_comment; comment->next = NULL; return comment; } /* Return an evaluation of board. */ static double evaluate(const Board *board) { return shannonEvaluation(board); } /* Return an evaluation of board based on * Claude Shannon's technique. */ static double shannonEvaluation(const Board *board) { MovePair *white_moves, *black_moves; int whiteMoveCount = 0, blackMoveCount = 0; int whitePieceCount = 0, blackPieceCount = 0; double shannonValue = 0.0; Rank rank; Col col; /* Determine the mobilities. */ white_moves = find_all_moves(board, WHITE); if (white_moves != NULL) { MovePair *m; for (m = white_moves; m != NULL; m = m->next) { whiteMoveCount++; } free_move_pair_list(white_moves); } black_moves = find_all_moves(board, BLACK); if (black_moves != NULL) { MovePair *m; for (m = black_moves; m != NULL; m = m->next) { blackMoveCount++; } free_move_pair_list(black_moves); } /* Pick up each piece of the required colour. */ for (rank = LASTRANK; rank >= FIRSTRANK; rank--) { int r = RankConvert(rank); for (col = FIRSTCOL; col <= LASTCOL; col++) { int c = ColConvert(col); int pieceValue = 0; Piece occupant = board->board[r][c]; if (occupant != EMPTY) { /* This square is occupied by a piece of the required colour. */ Piece piece = EXTRACT_PIECE(occupant); switch (piece) { case PAWN: pieceValue = 1; break; case BISHOP: case KNIGHT: pieceValue = 3; break; case ROOK: pieceValue = 5; break; case QUEEN: pieceValue = 9; break; case KING: break; default: fprintf(GlobalState.logfile, "Internal error: unknown piece %d in append_evaluation().\n", piece); } if (EXTRACT_COLOUR(occupant) == WHITE) { whitePieceCount += pieceValue; } else { blackPieceCount += pieceValue; } } } } shannonValue = (whitePieceCount - blackPieceCount) + (whiteMoveCount - blackMoveCount) * 0.1; return shannonValue; } /* Look for comment_pattern in the given list of comments. * If found, return where it was found, otherwise NULL. */ static StringList * find_matching_comment(const char *comment_pattern, const CommentList *comment) { Boolean match = FALSE; StringList *string_list; while(!match && comment != NULL) { string_list = comment->comment; while(!match && string_list != NULL) { if(strcmp(comment_pattern, string_list->str) == 0) { match = TRUE; } else { string_list = string_list->next; } } if(!match) { comment = comment->next; } } if(match) { return string_list; } else { return (StringList *) NULL; } }