/*
* This file is part of pgn-extract: a Portable Game Notation (PGN) extractor.
* Copyright (C) 1994-2019 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);
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);
void free_tags(void);
static void check_result(char **Tags, const char *terminating_result);
static void deal_with_ECO_line(Move *move_list);
static void deal_with_game(Move *move_list);
static Boolean finished_processing(void);
static void output_game(Game *game,FILE *outputfile);
static void split_variants(Game *game, FILE *outputfile, unsigned depth);
static Boolean chess960_setup(Board *board);
static CommentList *append_comment(CommentList *item, CommentList *list);
/* 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;
}
}
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);
}
/*
* 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;
while (parse_game(&move_list) && !finished_processing()) {
if (file_type == NORMALFILE) {
deal_with_game(move_list);
}
else if (file_type == CHECKFILE) {
deal_with_game(move_list);
}
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)
{ /* 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;
}
if (parse_opt_tag_list()) {
/* something_found = TRUE; */
}
/* @@@ 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();
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();
if (prefix_comment != NULL) {
}
moves = parse_move_list();
if (moves == NULL) {
print_error_context(GlobalState.logfile);
fprintf(GlobalState.logfile, "Missing move list in variation.\n");
}
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. */
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;
}
}
/* 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) {
report_details(GlobalState.logfile);
fprintf(GlobalState.logfile,
"Missing %s Tag for non-standard setup; adding %s.\n",
tag_header_string(VARIANT_TAG),
current_game->tags[VARIANT_TAG]);
/* Fix the inconsistency. */
current_game->tags[VARIANT_TAG] = copy_string("chess 960");
}
}
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)
{
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;
/* 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;
/* 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) &&
apply_move_list(¤t_game, &plycount, GlobalState.depth_of_positional_search) &&
check_move_bounds(plycount) &&
check_textual_variations(¤t_game) &&
check_for_ending(¤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 {
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;
}
}
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.
*/
if (apply_eco_move_list(¤t_game, &number_of_half_moves)) {
if (current_game.moves_ok) {
/* Store the ECO code in the appropriate hash location. */
save_eco_details(current_game, 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;
}
}