1427 lines
46 KiB
C
1427 lines
46 KiB
C
/*
|
|
* 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 <http://www.gnu.org/licenses/>.
|
|
*
|
|
* David J. Barnes may be contacted as d.j.barnes@kent.ac.uk
|
|
* https://www.cs.kent.ac.uk/people/staff/djb/
|
|
*/
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <ctype.h>
|
|
#include "bool.h"
|
|
#include "mymalloc.h"
|
|
#include "defs.h"
|
|
#include "typedef.h"
|
|
#include "tokens.h"
|
|
#include "taglist.h"
|
|
#include "lex.h"
|
|
#include "moves.h"
|
|
#include "map.h"
|
|
#include "lists.h"
|
|
#include "apply.h"
|
|
#include "output.h"
|
|
#include "eco.h"
|
|
#include "end.h"
|
|
#include "grammar.h"
|
|
#include "hashing.h"
|
|
|
|
static TokenType current_symbol = NO_TOKEN;
|
|
|
|
/* Keep track of which RAV level we are at.
|
|
* This is used to check whether a TERMINATING_RESULT is the final one
|
|
* and whether NULL_MOVEs are allowed.
|
|
*/
|
|
static unsigned RAV_level = 0;
|
|
|
|
/* How often to report processing rate. */
|
|
static unsigned PROGRESS_RATE = 1000;
|
|
|
|
/* Retain details of the header of a game.
|
|
* This comprises the Tags and any comment prefixing the
|
|
* moves of the game.
|
|
*/
|
|
static struct {
|
|
/* The tag values. */
|
|
char **Tags;
|
|
unsigned header_tags_length;
|
|
CommentList *prefix_comment;
|
|
} GameHeader;
|
|
|
|
static void parse_opt_game_list(SourceFileType file_type);
|
|
static Boolean parse_game(Move **returned_move_list, unsigned long *start_line, unsigned long *end_line);
|
|
Boolean parse_opt_tag_list(void);
|
|
Boolean parse_tag(void);
|
|
static Move *parse_move_list(void);
|
|
static Move *parse_move_and_variants(void);
|
|
static Move *parse_move(void);
|
|
static Move *parse_move_unit(void);
|
|
static CommentList *parse_opt_comment_list(void);
|
|
Boolean parse_opt_move_number(void);
|
|
static void parse_opt_NAG_list(Move *move_details);
|
|
static Variation *parse_opt_variant_list(void);
|
|
static Variation *parse_variant(void);
|
|
static char *parse_result(void);
|
|
|
|
static void setup_for_new_game(void);
|
|
static CommentList *append_comment(CommentList *item, CommentList *list);
|
|
static void check_result(char **Tags, const char *terminating_result);
|
|
static Boolean chess960_setup(Board *board);
|
|
static void deal_with_ECO_line(Move *move_list);
|
|
static void deal_with_game(Move *move_list, unsigned long start_line, unsigned long end_line);
|
|
static Boolean finished_processing(void);
|
|
static void free_tags(void);
|
|
static CommentList *merge_comment_lists(CommentList *prefix, CommentList *suffix);
|
|
static void output_game(Game *game,FILE *outputfile);
|
|
static void split_variants(Game *game, FILE *outputfile, unsigned depth);
|
|
|
|
/* Initialise the game header structure to contain
|
|
* space for the default number of tags.
|
|
* The space will have to be increased if new tags are
|
|
* identified in the program source.
|
|
*/
|
|
void
|
|
init_game_header(void)
|
|
{
|
|
unsigned i;
|
|
GameHeader.header_tags_length = ORIGINAL_NUMBER_OF_TAGS;
|
|
GameHeader.Tags = (char **) malloc_or_die(GameHeader.header_tags_length *
|
|
sizeof (*GameHeader.Tags));
|
|
for (i = 0; i < GameHeader.header_tags_length; i++) {
|
|
GameHeader.Tags[i] = (char *) NULL;
|
|
}
|
|
GameHeader.prefix_comment = (CommentList *) NULL;
|
|
}
|
|
|
|
void
|
|
increase_game_header_tags_length(unsigned new_length)
|
|
{
|
|
unsigned i;
|
|
|
|
if (new_length <= GameHeader.header_tags_length) {
|
|
fprintf(GlobalState.logfile,
|
|
"Internal error: inappropriate length %d ", new_length);
|
|
fprintf(GlobalState.logfile,
|
|
" passed to increase_game_header_tags().\n");
|
|
exit(1);
|
|
}
|
|
GameHeader.Tags = (char **) realloc_or_die((void *) GameHeader.Tags,
|
|
new_length * sizeof (*GameHeader.Tags));
|
|
for (i = GameHeader.header_tags_length; i < new_length; i++) {
|
|
GameHeader.Tags[i] = NULL;
|
|
}
|
|
GameHeader.header_tags_length = new_length;
|
|
}
|
|
|
|
/* Try to open the given file. Error and exit on failure. */
|
|
FILE *
|
|
must_open_file(const char *filename, const char *mode)
|
|
{
|
|
FILE *fp;
|
|
|
|
fp = fopen(filename, mode);
|
|
if (fp == NULL) {
|
|
fprintf(GlobalState.logfile, "Unable to open the file: \"%s\"\n",
|
|
filename);
|
|
exit(1);
|
|
}
|
|
return fp;
|
|
}
|
|
|
|
/* Print out on outfp the current details and
|
|
* terminate with a newline.
|
|
*/
|
|
void
|
|
report_details(FILE *outfp)
|
|
{
|
|
if (GameHeader.Tags[WHITE_TAG] != NULL) {
|
|
fprintf(outfp, "%s - ", GameHeader.Tags[WHITE_TAG]);
|
|
}
|
|
if (GameHeader.Tags[BLACK_TAG] != NULL) {
|
|
fprintf(outfp, "%s ", GameHeader.Tags[BLACK_TAG]);
|
|
}
|
|
|
|
if (GameHeader.Tags[EVENT_TAG] != NULL) {
|
|
fprintf(outfp, "%s ", GameHeader.Tags[EVENT_TAG]);
|
|
}
|
|
if (GameHeader.Tags[SITE_TAG] != NULL) {
|
|
fprintf(outfp, "%s ", GameHeader.Tags[SITE_TAG]);
|
|
}
|
|
|
|
if (GameHeader.Tags[DATE_TAG] != NULL) {
|
|
fprintf(outfp, "%s ", GameHeader.Tags[DATE_TAG]);
|
|
}
|
|
putc('\n', outfp);
|
|
fflush(outfp);
|
|
}
|
|
|
|
/* Check that terminating_result is consistent with
|
|
* Tags[RESULT_TAG].
|
|
* If the latter is missing, fill it in from terminating_result.
|
|
* If Tags[RESULT_TAG] is the short form "1/2" then replace it
|
|
* with the long form.
|
|
*/
|
|
static void
|
|
check_result(char **Tags, const char *terminating_result)
|
|
{
|
|
char *result_tag = Tags[RESULT_TAG];
|
|
|
|
if(result_tag != NULL && strcmp(result_tag, "1/2") == 0) {
|
|
/* Inappropriate short form. */
|
|
(void) free(result_tag);
|
|
result_tag = Tags[RESULT_TAG] = copy_string("1/2-1/2");
|
|
}
|
|
|
|
if (terminating_result != NULL) {
|
|
if ((result_tag == NULL) || (*result_tag == '\0') ||
|
|
(strcmp(result_tag, "?") == 0)) {
|
|
/* Use a copy of terminating result. */
|
|
result_tag = copy_string(terminating_result);
|
|
Tags[RESULT_TAG] = result_tag;
|
|
}
|
|
else {
|
|
/* Consistency checks done later. */
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Select which file to write to based upon the game state.
|
|
* This will depend upon:
|
|
* Whether the number of games per file is limited.
|
|
* Whether ECO_level > DONT_DIVIDE.
|
|
*/
|
|
/* A length for filenames. */
|
|
#define FILENAME_LENGTH (250)
|
|
|
|
static FILE *
|
|
select_output_file(StateInfo *GameState, const char *eco)
|
|
{
|
|
if (GameState->games_per_file > 0) {
|
|
if (GameState->games_per_file == 1 ||
|
|
(GameState->num_games_matched % GameState->games_per_file) == 1) {
|
|
/* Time to open the next one. */
|
|
char filename[FILENAME_LENGTH];
|
|
|
|
if (GameState->outputfile != NULL) {
|
|
if (GlobalState.json_format && GameState->num_games_matched != 1) {
|
|
/* Terminate the output of the previous file. */
|
|
fputs("\n]\n", GlobalState.outputfile);
|
|
}
|
|
(void) fclose(GameState->outputfile);
|
|
}
|
|
sprintf(filename, "%u%s",
|
|
GameState->next_file_number,
|
|
output_file_suffix(GameState->output_format));
|
|
GameState->outputfile = must_open_file(filename, "w");
|
|
GameState->next_file_number++;
|
|
if (GlobalState.json_format) {
|
|
fputs("[\n", GlobalState.outputfile);
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
if (GameState->ECO_level > DONT_DIVIDE) {
|
|
/* Open a file of the appropriate name. */
|
|
if (GameState->outputfile != NULL) {
|
|
/* @@@ In practice, this might need refinement.
|
|
* Repeated opening and closing may prove inefficient.
|
|
*/
|
|
(void) fclose(GameState->outputfile);
|
|
GameState->outputfile = open_eco_output_file(
|
|
GameState->ECO_level,
|
|
eco);
|
|
}
|
|
}
|
|
else if (GlobalState.json_format && GameState->num_games_matched == 1) {
|
|
fputs("[\n", GlobalState.outputfile);
|
|
}
|
|
else {
|
|
}
|
|
}
|
|
return GameState->outputfile;
|
|
}
|
|
|
|
/*
|
|
* Conditions for finishing processing, other than all the input
|
|
* having been processed.
|
|
*/
|
|
static Boolean finished_processing(void)
|
|
{
|
|
return (GlobalState.matching_game_numbers != NULL &&
|
|
GlobalState.next_game_number_to_output == NULL) ||
|
|
(GlobalState.maximum_matches > 0 &&
|
|
GlobalState.num_games_matched == GlobalState.maximum_matches) ||
|
|
GlobalState.num_games_processed >= GlobalState.game_limit;
|
|
}
|
|
|
|
/*
|
|
* Is the given game number within the range to be matched?
|
|
*/
|
|
static Boolean in_game_number_range(unsigned long number,
|
|
game_number *range)
|
|
{
|
|
return range != NULL && range->min <= number && number <= range->max;
|
|
}
|
|
|
|
static void
|
|
parse_opt_game_list(SourceFileType file_type)
|
|
{
|
|
Move *move_list = NULL;
|
|
unsigned long start_line, end_line;
|
|
|
|
while (parse_game(&move_list, &start_line, &end_line) && !finished_processing()) {
|
|
if (file_type == NORMALFILE) {
|
|
deal_with_game(move_list, start_line, end_line);
|
|
}
|
|
else if (file_type == CHECKFILE) {
|
|
deal_with_game(move_list, start_line, end_line);
|
|
}
|
|
else if (file_type == ECOFILE) {
|
|
if (move_list != NULL) {
|
|
deal_with_ECO_line(move_list);
|
|
}
|
|
else {
|
|
fprintf(GlobalState.logfile, "ECO line with zero moves.\n");
|
|
report_details(GlobalState.logfile);
|
|
}
|
|
}
|
|
else {
|
|
/* Unknown type. */
|
|
free_tags();
|
|
free_move_list(move_list);
|
|
}
|
|
move_list = NULL;
|
|
setup_for_new_game();
|
|
}
|
|
if(move_list != NULL) {
|
|
free_move_list(move_list);
|
|
}
|
|
}
|
|
|
|
/* Parse a game and return a pointer to any valid list of moves
|
|
* in returned_move_list.
|
|
*/
|
|
static Boolean
|
|
parse_game(Move **returned_move_list, unsigned long *start_line, unsigned long *end_line)
|
|
{ /* Boolean something_found = FALSE; */
|
|
CommentList *prefix_comment;
|
|
Move *move_list = NULL;
|
|
char *result;
|
|
/* There shouldn't be a hanging comment before the result,
|
|
* but there sometimes is.
|
|
*/
|
|
CommentList *hanging_comment;
|
|
|
|
/* Assume that we won't return anything. */
|
|
*returned_move_list = NULL;
|
|
/* Skip over any junk between games. */
|
|
current_symbol = skip_to_next_game(current_symbol);
|
|
prefix_comment = parse_opt_comment_list();
|
|
if (prefix_comment != NULL) {
|
|
/* Free this here, as it is hard to
|
|
* know whether it belongs to the game or the file.
|
|
* It is better to put game comments after the tags.
|
|
*/
|
|
/* something_found = TRUE; */
|
|
free_comment_list(prefix_comment);
|
|
prefix_comment = NULL;
|
|
}
|
|
*start_line = get_line_number();
|
|
if (parse_opt_tag_list()) {
|
|
/* something_found = TRUE; */
|
|
}
|
|
|
|
/* Some games have an initial NAG as a print-board indication.
|
|
* This is not legal PGN.
|
|
* Silently delete it/them.
|
|
*/
|
|
while (current_symbol == NAG) {
|
|
current_symbol = next_token();
|
|
}
|
|
|
|
/* @@@ Beware of comments and/or tags without moves. */
|
|
move_list = parse_move_list();
|
|
|
|
/* @@@ Look for a comment with no move text before the result. */
|
|
hanging_comment = parse_opt_comment_list();
|
|
/* Append this to the final move, if there is one. */
|
|
|
|
/* Look for a result, even if there were no moves. */
|
|
result = parse_result();
|
|
*end_line = get_line_number();
|
|
if (move_list != NULL) {
|
|
/* Find the last move. */
|
|
Move *last_move = move_list;
|
|
|
|
while (last_move->next != NULL) {
|
|
last_move = last_move->next;
|
|
}
|
|
if (hanging_comment != NULL) {
|
|
last_move->comment_list = append_comment(hanging_comment, last_move->comment_list);
|
|
}
|
|
if (result != NULL) {
|
|
/* Append it to the last move. */
|
|
last_move->terminating_result = result;
|
|
check_result(GameHeader.Tags, result);
|
|
*returned_move_list = move_list;
|
|
}
|
|
else {
|
|
fprintf(GlobalState.logfile, "Missing result.\n");
|
|
report_details(GlobalState.logfile);
|
|
}
|
|
/* something_found = TRUE; */
|
|
}
|
|
else {
|
|
/* @@@ Nothing to attach the comment to. */
|
|
(void) free((void *) hanging_comment);
|
|
hanging_comment = NULL;
|
|
/*
|
|
* Workaround for games with zero moves.
|
|
* Check the result for consistency with the tags, but then
|
|
* there is no move to attach it to.
|
|
* When outputting a game, the missing result in this case
|
|
* will have to be supplied from the tags.
|
|
*/
|
|
check_result(GameHeader.Tags, result);
|
|
if (result != NULL) {
|
|
(void) free((void *) result);
|
|
}
|
|
*returned_move_list = NULL;
|
|
}
|
|
return current_symbol != EOF_TOKEN;
|
|
}
|
|
|
|
Boolean
|
|
parse_opt_tag_list(void)
|
|
{
|
|
Boolean something_found = FALSE;
|
|
CommentList *prefix_comment;
|
|
|
|
while (parse_tag()) {
|
|
something_found = TRUE;
|
|
}
|
|
prefix_comment = parse_opt_comment_list();
|
|
if (prefix_comment != NULL) {
|
|
GameHeader.prefix_comment = prefix_comment;
|
|
something_found = TRUE;
|
|
}
|
|
return something_found;
|
|
}
|
|
|
|
/* Return TRUE if it looks like board contains an initial
|
|
* Chess 960 setup position. FALSE otherwise.
|
|
* Assessment requires that:
|
|
* + The move number be 1.
|
|
* + All castling rights are intact.
|
|
* + The two home ranks are full.
|
|
* + Identical pieces are opposite each other on the back rank.
|
|
* + At least one piece is out of its standard position.
|
|
*/
|
|
static Boolean
|
|
chess960_setup(Board *board)
|
|
{
|
|
if(board->move_number == 1 &&
|
|
board->WKingRank == '1' &&
|
|
board->BKingRank == '8' &&
|
|
board->WKingCol == board->BKingCol &&
|
|
(board->WKingCastle != '\0' &&
|
|
board->WQueenCastle != '\0' &&
|
|
board->BKingCastle != '\0' &&
|
|
board->BQueenCastle != '\0')) {
|
|
/* Check for a full set of pawns. */
|
|
Boolean probable = TRUE;
|
|
int white_r = RankConvert('2');
|
|
int black_r = RankConvert('7');
|
|
Piece white_pawn = MAKE_COLOURED_PIECE(WHITE, PAWN);
|
|
Piece black_pawn = MAKE_COLOURED_PIECE(BLACK, PAWN);
|
|
for(int c = ColConvert('a'); c <= ColConvert('h') && probable; c++) {
|
|
probable = board->board[white_r][c] == white_pawn &&
|
|
board->board[black_r][c] == black_pawn;
|
|
/* Make sure the back rank is full and identical pieces
|
|
* are opposite each other.
|
|
*/
|
|
if(probable) {
|
|
probable =
|
|
board->board[white_r-1][c] != EMPTY &&
|
|
EXTRACT_PIECE(board->board[white_r-1][c]) ==
|
|
EXTRACT_PIECE(board->board[black_r+1][c]);
|
|
}
|
|
}
|
|
if(probable) {
|
|
/* Check for at least one piece type being out of position. */
|
|
/* Only need to check one colour because we already know the
|
|
* pieces are identically paired.
|
|
*/
|
|
white_r = RankConvert('1');
|
|
probable =
|
|
board->board[white_r][ColConvert('a')] != W(ROOK) ||
|
|
board->board[white_r][ColConvert('b')] != W(KNIGHT) ||
|
|
board->board[white_r][ColConvert('c')] != W(BISHOP) ||
|
|
board->board[white_r][ColConvert('d')] != W(QUEEN) ||
|
|
board->board[white_r][ColConvert('e')] != W(KING) ||
|
|
board->board[white_r][ColConvert('f')] != W(BISHOP) ||
|
|
board->board[white_r][ColConvert('g')] != W(KNIGHT) ||
|
|
board->board[white_r][ColConvert('h')] != W(ROOK);
|
|
}
|
|
return probable;
|
|
}
|
|
else {
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
Boolean
|
|
parse_tag(void)
|
|
{
|
|
Boolean TagFound = TRUE;
|
|
|
|
if (current_symbol == TAG) {
|
|
TagName tag_index = yylval.tag_index;
|
|
|
|
current_symbol = next_token();
|
|
if (current_symbol == STRING) {
|
|
char *tag_string = yylval.token_string;
|
|
|
|
if (tag_index < GameHeader.header_tags_length) {
|
|
GameHeader.Tags[tag_index] = tag_string;
|
|
}
|
|
else {
|
|
print_error_context(GlobalState.logfile);
|
|
fprintf(GlobalState.logfile,
|
|
"Internal error: Illegal tag index %d for %s\n",
|
|
tag_index, tag_string);
|
|
exit(1);
|
|
}
|
|
current_symbol = next_token();
|
|
}
|
|
else {
|
|
print_error_context(GlobalState.logfile);
|
|
fprintf(GlobalState.logfile, "Missing tag string.\n");
|
|
}
|
|
}
|
|
else if (current_symbol == STRING) {
|
|
print_error_context(GlobalState.logfile);
|
|
fprintf(GlobalState.logfile, "Missing tag for %s.\n", yylval.token_string);
|
|
(void) free((void *) yylval.token_string);
|
|
current_symbol = next_token();
|
|
}
|
|
else {
|
|
TagFound = FALSE;
|
|
}
|
|
return TagFound;
|
|
}
|
|
|
|
static Move *
|
|
parse_move_list(void)
|
|
{
|
|
Move *head = NULL, *tail = NULL;
|
|
|
|
head = parse_move_and_variants();
|
|
if (head != NULL) {
|
|
Move *next_move;
|
|
|
|
tail = head;
|
|
while ((next_move = parse_move_and_variants()) != NULL) {
|
|
tail->next = next_move;
|
|
tail = next_move;
|
|
}
|
|
}
|
|
return head;
|
|
}
|
|
|
|
static Move *
|
|
parse_move_and_variants(void)
|
|
{
|
|
Move *move_details;
|
|
|
|
move_details = parse_move();
|
|
if (move_details != NULL) {
|
|
CommentList *comment;
|
|
|
|
move_details->Variants = parse_opt_variant_list();
|
|
comment = parse_opt_comment_list();
|
|
if (comment != NULL) {
|
|
move_details->comment_list = append_comment(comment, move_details->comment_list);
|
|
}
|
|
}
|
|
return move_details;
|
|
}
|
|
|
|
static Move *
|
|
parse_move(void)
|
|
{
|
|
Move *move_details = NULL;
|
|
|
|
if (parse_opt_move_number()) {
|
|
}
|
|
/* @@@ Watch out for finding just the number. */
|
|
move_details = parse_move_unit();
|
|
if (move_details != NULL) {
|
|
parse_opt_NAG_list(move_details);
|
|
/* Any trailing comments will have been picked up
|
|
* and attached to the NAGs.
|
|
*/
|
|
}
|
|
return move_details;
|
|
}
|
|
|
|
static Move *
|
|
parse_move_unit(void)
|
|
{
|
|
Move *move_details = NULL;
|
|
|
|
if (current_symbol == MOVE) {
|
|
move_details = yylval.move_details;
|
|
|
|
if (move_details->class == NULL_MOVE && RAV_level == 0) {
|
|
if(!GlobalState.allow_null_moves) {
|
|
print_error_context(GlobalState.logfile);
|
|
fprintf(GlobalState.logfile, "Null moves (--) only allowed in variations.\n");
|
|
}
|
|
}
|
|
|
|
current_symbol = next_token();
|
|
if (current_symbol == CHECK_SYMBOL) {
|
|
strcat((char *) move_details->move, "+");
|
|
current_symbol = next_token();
|
|
/* Sometimes + is followed by #, so cover this case. */
|
|
if (current_symbol == CHECK_SYMBOL) {
|
|
current_symbol = next_token();
|
|
}
|
|
}
|
|
move_details->comment_list = parse_opt_comment_list();
|
|
}
|
|
return move_details;
|
|
}
|
|
|
|
static CommentList *
|
|
parse_opt_comment_list(void)
|
|
{
|
|
CommentList *head = NULL, *tail = NULL;
|
|
|
|
while (current_symbol == COMMENT) {
|
|
if (head == NULL) {
|
|
head = tail = yylval.comment;
|
|
}
|
|
else {
|
|
tail->next = yylval.comment;
|
|
tail = tail->next;
|
|
}
|
|
current_symbol = next_token();
|
|
}
|
|
return head;
|
|
}
|
|
|
|
Boolean
|
|
parse_opt_move_number(void)
|
|
{
|
|
Boolean something_found = FALSE;
|
|
|
|
if (current_symbol == MOVE_NUMBER) {
|
|
current_symbol = next_token();
|
|
something_found = TRUE;
|
|
}
|
|
return something_found;
|
|
}
|
|
|
|
/**
|
|
* Parse 0 or more NAGs, optionally followed by 0 or more comments.
|
|
* @param move_details
|
|
*/
|
|
static void
|
|
parse_opt_NAG_list(Move *move_details)
|
|
{
|
|
while (current_symbol == NAG) {
|
|
Nag *details = (Nag *) malloc_or_die(sizeof(*details));
|
|
details->text = NULL;
|
|
details->comments = NULL;
|
|
details->next = NULL;
|
|
do {
|
|
details->text = save_string_list_item(details->text, yylval.token_string);
|
|
current_symbol = next_token();
|
|
} while(current_symbol == NAG);
|
|
details->comments = parse_opt_comment_list();
|
|
if(move_details->NAGs == NULL) {
|
|
move_details->NAGs = details;
|
|
}
|
|
else {
|
|
Nag *nextNAG = move_details->NAGs;
|
|
while(nextNAG->next != NULL) {
|
|
nextNAG = nextNAG->next;
|
|
}
|
|
nextNAG->next = details;
|
|
}
|
|
}
|
|
}
|
|
|
|
static Variation *
|
|
parse_opt_variant_list(void)
|
|
{
|
|
Variation *head = NULL, *tail = NULL,
|
|
*variation;
|
|
|
|
while ((variation = parse_variant()) != NULL) {
|
|
if (head == NULL) {
|
|
head = tail = variation;
|
|
}
|
|
else {
|
|
tail->next = variation;
|
|
tail = variation;
|
|
}
|
|
}
|
|
return head;
|
|
}
|
|
|
|
static Variation *
|
|
parse_variant(void)
|
|
{
|
|
Variation *variation = NULL;
|
|
|
|
if (current_symbol == RAV_START) {
|
|
CommentList *prefix_comment;
|
|
CommentList *suffix_comment;
|
|
char *result = NULL;
|
|
Move *moves;
|
|
|
|
RAV_level++;
|
|
variation = (Variation *) malloc_or_die(sizeof (Variation));
|
|
|
|
current_symbol = next_token();
|
|
prefix_comment = parse_opt_comment_list();
|
|
moves = parse_move_list();
|
|
if (moves == NULL) {
|
|
print_error_context(GlobalState.logfile);
|
|
fprintf(GlobalState.logfile, "Missing move list in variation.\n");
|
|
}
|
|
else if(GlobalState.lichess_comment_fix && prefix_comment != NULL) {
|
|
/* lichess study deletes the prefix comment, so
|
|
* move it after the first move of the variation.
|
|
*/
|
|
moves->comment_list = merge_comment_lists(prefix_comment, moves->comment_list);
|
|
prefix_comment = NULL;
|
|
}
|
|
result = parse_result();
|
|
if ((result != NULL) && (moves != NULL)) {
|
|
/* Find the last move, to which to append the terminating
|
|
* result.
|
|
*/
|
|
Move *last_move = moves;
|
|
CommentList *trailing_comment;
|
|
|
|
while (last_move->next != NULL) {
|
|
last_move = last_move->next;
|
|
}
|
|
last_move->terminating_result = result;
|
|
/* Accept a comment after the result, but it will
|
|
* be printed out preceding the result.
|
|
*/
|
|
trailing_comment = parse_opt_comment_list();
|
|
if (trailing_comment != NULL) {
|
|
last_move->comment_list = append_comment(trailing_comment, last_move->comment_list);
|
|
}
|
|
}
|
|
else {
|
|
/* Ok. */
|
|
}
|
|
if (current_symbol == RAV_END) {
|
|
RAV_level--;
|
|
current_symbol = next_token();
|
|
}
|
|
else {
|
|
fprintf(GlobalState.logfile, "Missing ')' to close variation.\n");
|
|
print_error_context(GlobalState.logfile);
|
|
}
|
|
suffix_comment = parse_opt_comment_list();
|
|
variation->prefix_comment = prefix_comment;
|
|
variation->suffix_comment = suffix_comment;
|
|
variation->moves = moves;
|
|
variation->next = NULL;
|
|
}
|
|
return variation;
|
|
}
|
|
|
|
static char *
|
|
parse_result(void)
|
|
{
|
|
char *result = NULL;
|
|
|
|
if (current_symbol == TERMINATING_RESULT) {
|
|
result = yylval.token_string;
|
|
if (RAV_level == 0) {
|
|
/* In the interests of skipping any intervening material
|
|
* between games, set the lookahead to a dummy token.
|
|
*/
|
|
current_symbol = NO_TOKEN;
|
|
}
|
|
else {
|
|
current_symbol = next_token();
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
static void
|
|
setup_for_new_game(void)
|
|
{
|
|
restart_lex_for_new_game();
|
|
RAV_level = 0;
|
|
}
|
|
|
|
/* Discard any data held in the GameHeader.Tags structure. */
|
|
static void
|
|
free_tags(void)
|
|
{
|
|
unsigned tag;
|
|
|
|
for (tag = 0; tag < GameHeader.header_tags_length; tag++) {
|
|
if (GameHeader.Tags[tag] != NULL) {
|
|
free(GameHeader.Tags[tag]);
|
|
GameHeader.Tags[tag] = NULL;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Discard data from a gathered game. */
|
|
void
|
|
free_string_list(StringList *list)
|
|
{
|
|
StringList *next;
|
|
|
|
while (list != NULL) {
|
|
next = list;
|
|
list = list->next;
|
|
if (next->str != NULL) {
|
|
(void) free((void *) next->str);
|
|
}
|
|
(void) free((void *) next);
|
|
}
|
|
}
|
|
|
|
void
|
|
free_comment_list(CommentList *comment_list)
|
|
{
|
|
while (comment_list != NULL) {
|
|
CommentList *this_comment = comment_list;
|
|
|
|
if (comment_list->comment != NULL) {
|
|
free_string_list(comment_list->comment);
|
|
}
|
|
comment_list = comment_list->next;
|
|
(void) free((void *) this_comment);
|
|
}
|
|
}
|
|
|
|
static void
|
|
free_variation(Variation *variation)
|
|
{
|
|
Variation *next;
|
|
|
|
while (variation != NULL) {
|
|
next = variation;
|
|
variation = variation->next;
|
|
if (next->prefix_comment != NULL) {
|
|
free_comment_list(next->prefix_comment);
|
|
}
|
|
if (next->suffix_comment != NULL) {
|
|
free_comment_list(next->suffix_comment);
|
|
}
|
|
if (next->moves != NULL) {
|
|
(void) free_move_list(next->moves);
|
|
}
|
|
(void) free((void *) next);
|
|
}
|
|
}
|
|
|
|
static void free_NAG_list(Nag *nag_list)
|
|
{
|
|
while(nag_list != NULL) {
|
|
Nag *nextNAG = nag_list->next;
|
|
free_string_list(nag_list->text);
|
|
free_comment_list(nag_list->comments);
|
|
(void) free((void *) nag_list);
|
|
nag_list = nextNAG;
|
|
}
|
|
}
|
|
|
|
void
|
|
free_move_list(Move *move_list)
|
|
{
|
|
Move *nextMove;
|
|
|
|
while (move_list != NULL) {
|
|
nextMove = move_list;
|
|
move_list = move_list->next;
|
|
|
|
free_NAG_list(nextMove->NAGs);
|
|
free_comment_list(nextMove->comment_list);
|
|
free_variation(nextMove->Variants);
|
|
|
|
if (nextMove->epd != NULL) {
|
|
(void) free((void *) nextMove->epd);
|
|
}
|
|
if(nextMove->fen_suffix != NULL) {
|
|
(void) free((void *) nextMove->fen_suffix);
|
|
nextMove->fen_suffix = NULL;
|
|
}
|
|
if (nextMove->terminating_result != NULL) {
|
|
(void) free((void *) nextMove->terminating_result);
|
|
}
|
|
|
|
(void) free((void *) nextMove);
|
|
}
|
|
}
|
|
|
|
/* Add str onto the tail of list and
|
|
* return the head of the resulting list.
|
|
*/
|
|
StringList *
|
|
save_string_list_item(StringList *list, const char *str)
|
|
{
|
|
if (str != NULL && *str != '\0') {
|
|
StringList *new_item;
|
|
|
|
new_item = (StringList *) malloc_or_die(sizeof (*new_item));
|
|
new_item->str = str;
|
|
new_item->next = NULL;
|
|
if (list == NULL) {
|
|
list = new_item;
|
|
}
|
|
else {
|
|
StringList *tail = list;
|
|
|
|
while (tail->next != NULL) {
|
|
tail = tail->next;
|
|
}
|
|
tail->next = new_item;
|
|
}
|
|
}
|
|
#if 1
|
|
/* This is almost certainly correct to avoid losing
|
|
* two bytes with malloc'd empty strings.
|
|
* No problems with valgrind but just being
|
|
* cautious.
|
|
*/
|
|
else if(str != NULL) {
|
|
(void) free((void *) str);
|
|
}
|
|
#endif
|
|
return list;
|
|
}
|
|
|
|
/* Append any comments in Comment onto the end of
|
|
* any associated with move.
|
|
*/
|
|
void
|
|
append_comments_to_move(Move *move, CommentList *comment)
|
|
{
|
|
if (comment != NULL) {
|
|
move->comment_list = append_comment(comment, move->comment_list);
|
|
}
|
|
}
|
|
|
|
/* Add item to the end of list.
|
|
* If list is empty, return item.
|
|
*/
|
|
static CommentList *append_comment(CommentList *item, CommentList *list)
|
|
{
|
|
if (list == NULL) {
|
|
return item;
|
|
}
|
|
else {
|
|
CommentList *tail = list;
|
|
|
|
while (tail->next != NULL) {
|
|
tail = tail->next;
|
|
}
|
|
tail->next = item;
|
|
return list;
|
|
}
|
|
}
|
|
|
|
/* Add the suffix list (if any) to the end of the prefix list.
|
|
*/
|
|
static CommentList *merge_comment_lists(CommentList *prefix, CommentList *suffix)
|
|
{
|
|
if(prefix == NULL) {
|
|
return suffix;
|
|
}
|
|
else if(suffix != NULL) {
|
|
CommentList *tail = prefix;
|
|
while(tail->next != NULL) {
|
|
tail = tail->next;
|
|
}
|
|
tail->next = suffix;
|
|
}
|
|
return prefix;
|
|
}
|
|
|
|
/* Check for consistency of any FEN-related tags. */
|
|
static Boolean consistent_FEN_tags(Game *current_game)
|
|
{
|
|
Boolean consistent = TRUE;
|
|
|
|
if ((current_game->tags[SETUP_TAG] != NULL) &&
|
|
(strcmp(current_game->tags[SETUP_TAG], "1") == 0)) {
|
|
/* There must be a FEN_TAG to go with it. */
|
|
if (current_game->tags[FEN_TAG] == NULL) {
|
|
consistent = FALSE;
|
|
report_details(GlobalState.logfile);
|
|
fprintf(GlobalState.logfile,
|
|
"Missing %s Tag to accompany %s Tag.\n",
|
|
tag_header_string(FEN_TAG),
|
|
tag_header_string(SETUP_TAG));
|
|
print_error_context(GlobalState.logfile);
|
|
}
|
|
}
|
|
if(current_game->tags[FEN_TAG] != NULL) {
|
|
Board *board = new_fen_board(current_game->tags[FEN_TAG]);
|
|
if(board != NULL) {
|
|
/* There must be a SETUP_TAG to go with it. */
|
|
if(current_game->tags[SETUP_TAG] == NULL) {
|
|
// This is such a common problem that it makes
|
|
// more sense just to silently correct it.
|
|
#if 0
|
|
report_details(GlobalState.logfile);
|
|
fprintf(GlobalState.logfile,
|
|
"Missing %s Tag to accompany %s Tag.\n",
|
|
tag_header_string(SETUP_TAG),
|
|
tag_header_string(FEN_TAG));
|
|
print_error_context(GlobalState.logfile);
|
|
#endif
|
|
/* Fix the inconsistency. */
|
|
current_game->tags[SETUP_TAG] = copy_string("1");
|
|
}
|
|
|
|
Boolean chess960 = chess960_setup(board);
|
|
if(current_game->tags[VARIANT_TAG] == NULL) {
|
|
/* See if there should be a Variant tag. */
|
|
/* Look for an initial position found in Chess 960. */
|
|
if(chess960) {
|
|
const char *missing_value = "chess 960";
|
|
report_details(GlobalState.logfile);
|
|
fprintf(GlobalState.logfile,
|
|
"Missing %s Tag for non-standard setup; adding %s.\n",
|
|
tag_header_string(VARIANT_TAG),
|
|
missing_value);
|
|
/* Fix the inconsistency. */
|
|
current_game->tags[VARIANT_TAG] = copy_string(missing_value);
|
|
}
|
|
else if(GlobalState.add_fen_castling) {
|
|
/* If add_fen_castling is TRUE and castling permissions are absent
|
|
* then liberally assume them based on the King and Rook positions.
|
|
*/
|
|
if(!board->WKingCastle && !board->WQueenCastle &&
|
|
!board->BKingCastle && !board->BQueenCastle) {
|
|
add_fen_castling(current_game, board);
|
|
}
|
|
}
|
|
}
|
|
else if(chess960) {
|
|
/* @@@ Should really make sure the Variant tag is appropriate. */
|
|
}
|
|
|
|
free_board(board);
|
|
}
|
|
else {
|
|
consistent = FALSE;
|
|
}
|
|
}
|
|
return consistent;
|
|
}
|
|
|
|
static void
|
|
deal_with_game(Move *move_list, unsigned long start_line, unsigned long end_line)
|
|
{
|
|
Game current_game;
|
|
/* We need a dummy argument for apply_move_list. */
|
|
unsigned plycount;
|
|
/* Whether the game matches, as long as it is not in a CHECKFILE. */
|
|
Boolean game_matches = FALSE;
|
|
/* Whether to output the game. */
|
|
Boolean output_the_game = FALSE;
|
|
|
|
if (GlobalState.current_file_type != CHECKFILE) {
|
|
/* Update the count of how many games handled. */
|
|
GlobalState.num_games_processed++;
|
|
}
|
|
|
|
/* Fill in the information currently known. */
|
|
current_game.tags = GameHeader.Tags;
|
|
current_game.tags_length = GameHeader.header_tags_length;
|
|
current_game.prefix_comment = GameHeader.prefix_comment;
|
|
current_game.moves = move_list;
|
|
current_game.moves_checked = FALSE;
|
|
current_game.moves_ok = FALSE;
|
|
current_game.error_ply = 0;
|
|
current_game.position_counts = NULL;
|
|
current_game.start_line = start_line;
|
|
current_game.end_line = end_line;
|
|
|
|
/* Determine whether or not this game is wanted, on the
|
|
* basis of the various selection criteria available.
|
|
*/
|
|
|
|
/*
|
|
* apply_move_list checks out the moves.
|
|
* If it returns TRUE as a match, it will also fill in the
|
|
* current_game.final_hash_value and
|
|
* current_game.cumulative_hash_value
|
|
* fields of current_game so that these can be used in the
|
|
* previous_occurrence function.
|
|
*
|
|
* If there are any tag criteria, it will be easy to quickly
|
|
* eliminate most games without going through the lengthy
|
|
* process of game matching.
|
|
*
|
|
* If ECO adding is done, the order of checking may cause
|
|
* a conflict here since it won't be possible to reject a game
|
|
* based on its ECO code unless it already has one.
|
|
* Therefore, check for the ECO tag only after everything else has
|
|
* been checked.
|
|
*/
|
|
if (consistent_FEN_tags(¤t_game) &&
|
|
check_tag_details_not_ECO(current_game.tags, current_game.tags_length) &&
|
|
check_setup_tag(current_game.tags) &&
|
|
check_duplicate_setup(¤t_game) &&
|
|
apply_move_list(¤t_game, &plycount, GlobalState.depth_of_positional_search) &&
|
|
check_move_bounds(plycount) &&
|
|
check_textual_variations(¤t_game) &&
|
|
check_for_material_match(¤t_game) &&
|
|
check_for_only_checkmate(¤t_game) &&
|
|
check_for_only_repetition(current_game.position_counts) &&
|
|
check_ECO_tag(current_game.tags)) {
|
|
/* If there is no original filename then the game is not a
|
|
* duplicate.
|
|
*/
|
|
const char *original_filename = previous_occurance(current_game, plycount);
|
|
|
|
if ((original_filename == NULL) && GlobalState.suppress_originals) {
|
|
/* Don't output first occurrences. */
|
|
}
|
|
else if ((original_filename == NULL) || !GlobalState.suppress_duplicates) {
|
|
if (GlobalState.current_file_type == CHECKFILE) {
|
|
/* We are only checking, so don't count this as a matched game. */
|
|
}
|
|
else if(GlobalState.num_games_processed >= GlobalState.first_game_number) {
|
|
game_matches = TRUE;
|
|
GlobalState.num_games_matched++;
|
|
if (GlobalState.matching_game_numbers != NULL &&
|
|
!in_game_number_range(GlobalState.num_games_matched, GlobalState.next_game_number_to_output)) {
|
|
/* This is not the right matching game to be output. */
|
|
}
|
|
else if (GlobalState.skip_game_numbers != NULL &&
|
|
in_game_number_range(GlobalState.num_games_matched, GlobalState.next_game_number_to_skip)) {
|
|
/* Skip this matching game. */
|
|
if(GlobalState.num_games_matched == GlobalState.next_game_number_to_skip->max) {
|
|
GlobalState.next_game_number_to_skip = GlobalState.next_game_number_to_skip->next;
|
|
}
|
|
}
|
|
else if (GlobalState.check_only) {
|
|
/* We are only checking. */
|
|
if (GlobalState.verbosity > 1) {
|
|
/* Report progress on logfile. */
|
|
report_details(GlobalState.logfile);
|
|
}
|
|
}
|
|
else {
|
|
output_the_game = TRUE;
|
|
}
|
|
}
|
|
else {
|
|
/* Not wanted. */
|
|
}
|
|
if(output_the_game) {
|
|
/* This game is to be kept and output. */
|
|
FILE *outputfile = select_output_file(&GlobalState,
|
|
current_game.tags[ECO_TAG]);
|
|
|
|
/* See if we wish to separate out duplicates. */
|
|
if ((original_filename != NULL) &&
|
|
(GlobalState.duplicate_file != NULL)) {
|
|
static const char *last_input_file = NULL;
|
|
|
|
outputfile = GlobalState.duplicate_file;
|
|
if ((last_input_file != GlobalState.current_input_file) &&
|
|
(GlobalState.current_input_file != NULL)) {
|
|
if(GlobalState.keep_comments) {
|
|
/* Record which file this and succeeding
|
|
* duplicates come from.
|
|
*/
|
|
print_str(outputfile, "{ From: ");
|
|
print_str(outputfile,
|
|
GlobalState.current_input_file);
|
|
print_str(outputfile, " }");
|
|
terminate_line(outputfile);
|
|
}
|
|
last_input_file = GlobalState.current_input_file;
|
|
}
|
|
if(GlobalState.keep_comments) {
|
|
print_str(outputfile, "{ First found in: ");
|
|
print_str(outputfile, original_filename);
|
|
print_str(outputfile, " }");
|
|
terminate_line(outputfile);
|
|
}
|
|
}
|
|
/* Now output what we have. */
|
|
output_game(¤t_game, outputfile);
|
|
if (GlobalState.verbosity > 1) {
|
|
/* Report progress on logfile. */
|
|
report_details(GlobalState.logfile);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (!game_matches && (GlobalState.non_matching_file != NULL) &&
|
|
GlobalState.current_file_type != CHECKFILE) {
|
|
/* The user wants to keep everything else. */
|
|
if (!current_game.moves_checked) {
|
|
/* Make sure that the move text is in a reasonable state.
|
|
* Force checking of the whole game.
|
|
*/
|
|
(void) apply_move_list(¤t_game, &plycount, 0);
|
|
}
|
|
if (current_game.moves_ok || GlobalState.keep_broken_games) {
|
|
output_game(¤t_game, GlobalState.non_matching_file);
|
|
}
|
|
}
|
|
if (game_matches && GlobalState.matching_game_numbers != NULL &&
|
|
in_game_number_range(GlobalState.num_games_matched,
|
|
GlobalState.next_game_number_to_output)) {
|
|
if(GlobalState.num_games_matched == GlobalState.next_game_number_to_output->max) {
|
|
GlobalState.next_game_number_to_output = GlobalState.next_game_number_to_output->next;
|
|
}
|
|
}
|
|
|
|
/* Game is finished with, so free everything. */
|
|
if (GameHeader.prefix_comment != NULL) {
|
|
free_comment_list(GameHeader.prefix_comment);
|
|
}
|
|
/* Ensure that the GameHeader's prefix comment is NULL for
|
|
* the next game.
|
|
*/
|
|
GameHeader.prefix_comment = NULL;
|
|
|
|
free_tags();
|
|
free_move_list(current_game.moves);
|
|
if (current_game.position_counts != NULL) {
|
|
free_position_count_list(current_game.position_counts);
|
|
current_game.position_counts = NULL;
|
|
}
|
|
if (GlobalState.verbosity != 0 && (GlobalState.num_games_processed % PROGRESS_RATE) == 0) {
|
|
fprintf(stderr, "Games: %lu\r", GlobalState.num_games_processed);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Output the given game to the output file.
|
|
* If GlobalState.split_variants then this will involve outputting
|
|
* each variation separately.
|
|
*/
|
|
static void
|
|
output_game(Game *game, FILE *outputfile)
|
|
{
|
|
if(GlobalState.split_variants && GlobalState.keep_variations) {
|
|
split_variants(game, outputfile, 0);
|
|
}
|
|
else {
|
|
format_game(game, outputfile);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Output each variation separately, to the required depth.
|
|
* NB: This involves the removal of all variations from the game.
|
|
* This is done recursively and depth (>=0) defines the current
|
|
* level of recursion.
|
|
*/
|
|
static void
|
|
split_variants(Game *game, FILE *outputfile, unsigned depth)
|
|
{
|
|
/* Gather all the suffix comments at this level. */
|
|
Move *move = game->moves;
|
|
while(move != NULL) {
|
|
Variation *variants = move->Variants;
|
|
while (variants != NULL) {
|
|
if(variants->suffix_comment != NULL) {
|
|
move->comment_list = append_comment(variants->suffix_comment, move->comment_list);
|
|
variants->suffix_comment = NULL;
|
|
}
|
|
variants = variants->next;
|
|
}
|
|
move = move->next;
|
|
}
|
|
|
|
/* Format the main line at this level. */
|
|
format_game(game, outputfile);
|
|
|
|
if(GlobalState.split_depth_limit == 0 ||
|
|
GlobalState.split_depth_limit > depth) {
|
|
/* Now all the variations. */
|
|
char *result_tag = game->tags[RESULT_TAG];
|
|
game->tags[RESULT_TAG] = copy_string("*");
|
|
move = game->moves;
|
|
Move *prev = NULL;
|
|
while(move != NULL) {
|
|
Variation *variants = move->Variants;
|
|
while (variants != NULL) {
|
|
Variation *next_variant = variants->next;
|
|
Move *variant_moves = variants->moves;
|
|
if(variant_moves != NULL) {
|
|
/* Supply a result if it is missing. */
|
|
Move *last_move = variant_moves;
|
|
while(last_move->next != NULL) {
|
|
last_move = last_move->next;
|
|
}
|
|
if(last_move->terminating_result == NULL) {
|
|
last_move->terminating_result = copy_string("*");
|
|
}
|
|
/* Replace the main line with the variants. */
|
|
if(prev != NULL) {
|
|
prev->next = variant_moves;
|
|
}
|
|
else {
|
|
game->moves = variant_moves;
|
|
}
|
|
/* Detach following variations. */
|
|
variants->next = NULL;
|
|
CommentList *prefix_comment = variants->prefix_comment;
|
|
if(prefix_comment != NULL) {
|
|
if(prev != NULL) {
|
|
prev->comment_list = append_comment(prefix_comment, prev->comment_list);
|
|
}
|
|
else {
|
|
game->prefix_comment = append_comment(prefix_comment,
|
|
game->prefix_comment);
|
|
}
|
|
}
|
|
split_variants(game, outputfile, depth+1);
|
|
if(prefix_comment != NULL) {
|
|
/* Remove the appended comments. */
|
|
CommentList *list;
|
|
if(prev != NULL) {
|
|
list = prev->comment_list;
|
|
}
|
|
else {
|
|
list = game->prefix_comment;
|
|
}
|
|
if(list == prefix_comment) {
|
|
if(prev != NULL) {
|
|
prev->comment_list = NULL;
|
|
}
|
|
else {
|
|
game->prefix_comment = NULL;
|
|
}
|
|
}
|
|
else {
|
|
while(list->next != prefix_comment && list->next != NULL) {
|
|
list = list->next;
|
|
}
|
|
list->next = NULL;
|
|
}
|
|
}
|
|
variants->next = next_variant;
|
|
}
|
|
variants = next_variant;
|
|
}
|
|
if(move->Variants != NULL) {
|
|
/* The variation can now be disposed of. */
|
|
free_variation(move->Variants);
|
|
move->Variants = NULL;
|
|
/* Restore the move replaced by its variants. */
|
|
if(prev != NULL) {
|
|
prev->next = move;
|
|
}
|
|
else {
|
|
game->moves = move;
|
|
}
|
|
}
|
|
prev = move;
|
|
move = move->next;
|
|
}
|
|
/* Put everything back as it was. */
|
|
(void) free((void *) game->tags[RESULT_TAG]);
|
|
game->tags[RESULT_TAG] = result_tag;
|
|
}
|
|
}
|
|
|
|
static void
|
|
deal_with_ECO_line(Move *move_list)
|
|
{
|
|
Game current_game;
|
|
/* We need to know the length of a game to store with the
|
|
* hash information as a sanity check.
|
|
*/
|
|
unsigned number_of_half_moves;
|
|
|
|
/* Fill in the information currently known. */
|
|
current_game.tags = GameHeader.Tags;
|
|
current_game.tags_length = GameHeader.header_tags_length;
|
|
current_game.prefix_comment = GameHeader.prefix_comment;
|
|
current_game.moves = move_list;
|
|
current_game.moves_checked = FALSE;
|
|
current_game.moves_ok = FALSE;
|
|
current_game.error_ply = 0;
|
|
|
|
/* apply_eco_move_list checks out the moves.
|
|
* It will also fill in the
|
|
* current_game.final_hash_value and
|
|
* current_game.cumulative_hash_value
|
|
* fields of current_game.
|
|
*/
|
|
Board *final_position = apply_eco_move_list(¤t_game, &number_of_half_moves);
|
|
if(final_position != NULL) {
|
|
/* Store the ECO code in the appropriate hash location. */
|
|
save_eco_details(¤t_game, final_position, number_of_half_moves);
|
|
}
|
|
|
|
/* Game is finished with, so free everything. */
|
|
if (GameHeader.prefix_comment != NULL) {
|
|
free_comment_list(GameHeader.prefix_comment);
|
|
}
|
|
/* Ensure that the GameHeader's prefix comment is NULL for
|
|
* the next game.
|
|
*/
|
|
GameHeader.prefix_comment = NULL;
|
|
|
|
free_tags();
|
|
free_move_list(current_game.moves);
|
|
}
|
|
|
|
/* If file_type == ECOFILE we are dealing with a file of ECO
|
|
* input rather than a normal game file.
|
|
*/
|
|
int
|
|
yyparse(SourceFileType file_type)
|
|
{
|
|
setup_for_new_game();
|
|
current_symbol = skip_to_next_game(NO_TOKEN);
|
|
parse_opt_game_list(file_type);
|
|
if (current_symbol == EOF_TOKEN) {
|
|
/* Ok -- EOF. */
|
|
return 0;
|
|
}
|
|
else if (finished_processing()) {
|
|
/* Ok -- done all we need to. */
|
|
return 0;
|
|
}
|
|
else {
|
|
fprintf(GlobalState.logfile, "End of input reached before end of file.\n");
|
|
return 1;
|
|
}
|
|
}
|
|
|