Files
chess-games/pgn-extract/end.c
2024-01-22 07:30:05 +01:00

759 lines
24 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 "end.h"
#include "lines.h"
#include "tokens.h"
#include "taglist.h"
#include "lex.h"
#include "apply.h"
#include "grammar.h"
/**
* Code to handle specifications describing the state of the board
* in terms of numbers of pieces and material balance between opponents.
*
* Games are then matched against these specifications.
*/
/* Keep a list of endings to be found. */
static Material_details *endings_to_match = NULL;
/* What kind of piece is the character, c, likely to represent?
* NB: This is NOT the same as is_piece() in decode.c
*/
/* Define pseudo-letter for minor pieces, used later. */
#define MINOR_PIECE 'L'
static Piece
is_English_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;
}
/* Initialise the count of required pieces prior to reading
* in the data.
*/
static Material_details *
new_ending_details(Boolean both_colours)
{
Material_details *details = (Material_details *) malloc_or_die(sizeof (Material_details));
int c;
Piece piece;
details->both_colours = both_colours;
for (piece = PAWN; piece <= KING; piece++) {
for (c = 0; c < 2; c++) {
details->num_pieces[c][piece] = 0;
details->occurs[c][piece] = EXACTLY;
}
}
/* Fill out some miscellaneous colour based information. */
for (c = 0; c < 2; c++) {
/* Only the KING is a requirement for each side. */
details->num_pieces[c][KING] = 1;
details->match_depth[c] = 0;
/* How many general minor pieces to match. */
details->num_minor_pieces[c] = 0;
details->minor_occurs[c] = EXACTLY;
}
/* Assume that the match must always have a depth of at least two for
* two half-move stability.
*/
details->move_depth = 2;
details->next = NULL;
return details;
}
static const char *
extract_combination(const char *p, Occurs *p_occurs, int *p_number, const char *line)
{
Boolean Ok = TRUE;
Occurs occurs = EXACTLY;
int number = 1;
if (isdigit((int) *p)) {
/* Only single digits are allowed. */
number = *p - '0';
p++;
if (isdigit((int) *p)) {
fprintf(GlobalState.logfile, "Number > 9 is too big in %s.\n",
line);
while (isdigit((int) *p)) {
p++;
}
Ok = FALSE;
}
}
if (Ok) {
/* Look for trailing annotations. */
switch (*p) {
case '*':
number = 0;
occurs = NUM_OR_MORE;
p++;
break;
case '+':
occurs = NUM_OR_MORE;
p++;
break;
case '-':
occurs = NUM_OR_LESS;
p++;
break;
case '?':
number = 1;
occurs = NUM_OR_LESS;
p++;
break;
case '=':
case '#':
case '<':
case '>':
switch (*p) {
case '=':
p++;
occurs = SAME_AS_OPPONENT;
break;
case '#':
p++;
occurs = NOT_SAME_AS_OPPONENT;
break;
case '<':
p++;
if (*p == '=') {
occurs = LESS_EQ_THAN_OPPONENT;
p++;
}
else {
occurs = LESS_THAN_OPPONENT;
}
break;
case '>':
p++;
if (*p == '=') {
occurs = MORE_EQ_THAN_OPPONENT;
p++;
}
else {
occurs = MORE_THAN_OPPONENT;
}
break;
}
break;
}
}
if (Ok) {
*p_occurs = occurs;
*p_number = number;
return p;
}
else {
return NULL;
}
}
/* Extract a single piece set of information from line.
* Return where we have got to as the result.
* colour == WHITE means we are looking at the first set of
* pieces, so some of the notation is illegal (i.e. the relative ops).
*
* The basic syntax for a piece description is:
* piece [number] [occurs]
* For instance:
* P2+ Pawn occurs at least twice or more.
* R= Rook occurs same number of times as opponent. (colour == BLACK)
* P1>= Exactly one pawn more than the opponent. (colour == BLACK)
*/
static const char *
extract_piece_information(const char *line, Material_details *details, Colour colour)
{
const char *p = line;
Boolean Ok = TRUE;
while (Ok && (*p != '\0') && !isspace((int) *p) && *p != MATERIAL_CONSTRAINT) {
Piece piece = is_English_piece(*p);
/* By default a piece should occur exactly once. */
Occurs occurs = EXACTLY;
int number = 1;
if (piece != EMPTY) {
/* Skip over the piece. */
p++;
p = extract_combination(p, &occurs, &number, line);
if (p != NULL) {
if ((piece == KING) && (number != 1)) {
fprintf(GlobalState.logfile, "A king must occur exactly once.\n");
number = 1;
}
else if ((piece == PAWN) && (number > 8)) {
fprintf(GlobalState.logfile,
"No more than 8 pawns are allowed.\n");
number = 8;
}
details->num_pieces[colour][piece] = number;
details->occurs[colour][piece] = occurs;
}
else {
Ok = FALSE;
}
}
else if (isalpha((int) *p) && (toupper((int) *p) == MINOR_PIECE)) {
p++;
p = extract_combination(p, &occurs, &number, line);
if (p != NULL) {
details->num_minor_pieces[colour] = number;
details->minor_occurs[colour] = occurs;
}
else {
Ok = FALSE;
}
}
else {
fprintf(GlobalState.logfile, "Unknown symbol at %s\n", p);
Ok = FALSE;
}
}
if (Ok) {
/* Make a sanity check on the use of minor pieces. */
if ((details->num_minor_pieces[colour] > 0) ||
(details->minor_occurs[colour] != EXACTLY)) {
/* Warn about use of BISHOP and KNIGHT letters. */
if ((details->num_pieces[colour][BISHOP] > 0) ||
(details->occurs[colour][BISHOP] != EXACTLY) ||
(details->num_pieces[colour][KNIGHT] > 0) ||
(details->occurs[colour][KNIGHT] != EXACTLY)) {
fprintf(GlobalState.logfile,
"Warning: the mixture of minor pieces in %s is not guaranteed to work.\n",
line);
fprintf(GlobalState.logfile,
"In a single set it is advisable to stick to either L or B and/or N.\n");
}
}
return p;
}
else {
return NULL;
}
}
/* Extract the piece specification from line and fill out
* details with the pattern information.
*/
static Boolean
decompose_line(const char *line, Material_details *details)
{
const char *p = line;
Boolean Ok = TRUE;
/* Skip initial space. */
while (isspace((int) *p)) {
p++;
}
/* Look for a move depth. */
if (isdigit((int) *p)) {
unsigned depth;
depth = *p - '0';
p++;
while (isdigit((int) *p)) {
depth = (depth * 10)+(*p - '0');
p++;
}
while (isspace((int) *p)) {
p++;
}
details->move_depth = depth;
}
/* Extract two pairs of piece information.
* NB: If the first set of pieces consists of a lone king then that must
* be included explicitly. If the second set consists of a lone
* king then that can be omitted.
*/
p = extract_piece_information(p, details, WHITE);
if (p != NULL) {
while ((*p != '\0') && (isspace((int) *p) || (*p == MATERIAL_CONSTRAINT))) {
p++;
}
if (*p != '\0') {
p = extract_piece_information(p, details, BLACK);
}
else {
/* No explicit requirements for the other colour. */
Piece piece;
for (piece = PAWN; piece <= KING; piece++) {
details->num_pieces[BLACK][piece] = 0;
details->occurs[BLACK][piece] = EXACTLY;
}
details->num_pieces[BLACK][KING] = 1;
details->occurs[BLACK][KING] = EXACTLY;
}
}
if (p != NULL) {
/* Allow trailing text as a comment. */
}
else {
Ok = FALSE;
}
return Ok;
}
/* A new game to be looked for. Indicate that we have not
* started matching any yet.
*/
static void
reset_match_depths(Material_details *endings)
{
for (; endings != NULL; endings = endings->next) {
endings->match_depth[WHITE] = 0;
endings->match_depth[BLACK] = 0;
}
}
/* Try to find a match for the given number of piece details. */
static Boolean
piece_match(int num_available, int num_to_find, int num_opponents, Occurs occurs)
{
Boolean match = FALSE;
switch (occurs) {
case EXACTLY:
match = num_available == num_to_find;
break;
case NUM_OR_MORE:
match = num_available >= num_to_find;
break;
case NUM_OR_LESS:
match = num_available <= num_to_find;
break;
case SAME_AS_OPPONENT:
match = num_available == num_opponents;
break;
case NOT_SAME_AS_OPPONENT:
match = num_available != num_opponents;
break;
case LESS_THAN_OPPONENT:
match = (num_available + num_to_find) <= num_opponents;
break;
case MORE_THAN_OPPONENT:
match = (num_available - num_to_find) >= num_opponents;
break;
case LESS_EQ_THAN_OPPONENT:
/* This means exactly num_to_find less than the
* opponent.
*/
match = (num_available + num_to_find) == num_opponents;
break;
case MORE_EQ_THAN_OPPONENT:
/* This means exactly num_to_find greater than the
* opponent.
*/
match = (num_available - num_to_find) == num_opponents;
break;
default:
fprintf(GlobalState.logfile,
"Inconsistent state %d in piece_match.\n", occurs);
match = FALSE;
}
return match;
}
/* Try to find a match against one player's pieces in the piece_set_colour
* set of details_to_find.
*/
static Boolean
piece_set_match(const Material_details *details_to_find,
int num_pieces[2][NUM_PIECE_VALUES],
Colour game_colour, Colour piece_set_colour)
{
Boolean match = TRUE;
Piece piece;
/* Determine whether we failed on a match for minor pieces or not. */
Boolean minor_failure = FALSE;
/* No need to check KING. */
for (piece = PAWN; (piece < KING) && match; piece++) {
int num_available = num_pieces[game_colour][piece];
int num_opponents = num_pieces[OPPOSITE_COLOUR(game_colour)][piece];
int num_to_find = details_to_find->num_pieces[piece_set_colour][piece];
Occurs occurs = details_to_find->occurs[piece_set_colour][piece];
match = piece_match(num_available, num_to_find, num_opponents, occurs);
if (!match) {
if ((piece == KNIGHT) || (piece == BISHOP)) {
minor_failure = TRUE;
/* Carry on trying to match. */
match = TRUE;
}
else {
minor_failure = FALSE;
}
}
}
if (match) {
/* Ensure that the minor pieces match if there is a minor pieces
* requirement.
*/
int num_to_find = details_to_find->num_minor_pieces[piece_set_colour];
Occurs occurs = details_to_find->minor_occurs[piece_set_colour];
if ((num_to_find > 0) || (occurs != EXACTLY)) {
int num_available =
num_pieces[game_colour][BISHOP] +
num_pieces[game_colour][KNIGHT];
int num_opponents = num_pieces[OPPOSITE_COLOUR(game_colour)][BISHOP] +
num_pieces[OPPOSITE_COLOUR(game_colour)][KNIGHT];
match = piece_match(num_available, num_to_find, num_opponents, occurs);
}
else if (minor_failure) {
/* We actually failed with proper matching of individual minor
* pieces, and no minor match fixup is possible.
*/
match = FALSE;
}
else {
/* Match stands. */
}
}
return match;
}
/* Look for a material match between current_details and
* details_to_find. Only return TRUE if we have both a match
* and match_depth >= move_depth in details_to_find.
* NB: If the game ends before the required depth is reached then a
* potential match would be missed. This could be considered
* as a bug.
*/
static Boolean
material_match(Material_details *details_to_find, int num_pieces[2][NUM_PIECE_VALUES],
Colour game_colour)
{
Boolean match = TRUE;
Colour piece_set_colour = WHITE;
match = piece_set_match(details_to_find, num_pieces, game_colour,
piece_set_colour);
if (match) {
game_colour = OPPOSITE_COLOUR(game_colour);
piece_set_colour = OPPOSITE_COLOUR(piece_set_colour);
match = piece_set_match(details_to_find, num_pieces, game_colour,
piece_set_colour);
/* Reset colour to its original value. */
game_colour = OPPOSITE_COLOUR(game_colour);
}
if (match) {
if (details_to_find->match_depth[game_colour] < details_to_find->move_depth) {
/* Not a full match yet. */
match = FALSE;
details_to_find->match_depth[game_colour]++;
}
else {
/* A stable match. */
}
}
else {
/* Reset the match counter. */
details_to_find->match_depth[game_colour] = 0;
}
return match;
}
/* Extract the numbers of each type of piece from the given board. */
static void extract_pieces_from_board(int num_pieces[2][NUM_PIECE_VALUES], const Board *board)
{
/* Set up num_pieces from the board. */
for(int c = 0; c < 2; c++) {
for(int p = 0; p < NUM_PIECE_VALUES; p++) {
num_pieces[c][p] = 0;
}
}
for(char rank = FIRSTRANK; rank <= LASTRANK; rank++) {
for(char col = FIRSTCOL; col <= LASTCOL; col++) {
int r = RankConvert(rank);
int c = ColConvert(col);
Piece coloured_piece = board->board[r][c];
if(coloured_piece != EMPTY) {
int p = EXTRACT_PIECE(coloured_piece);
num_pieces[EXTRACT_COLOUR(coloured_piece)][p]++;
}
}
}
}
/* Check to see whether the given moves lead to a position
* that matches the given 'ending' position.
* In other words, a position with the required balance
* of pieces.
*/
static Boolean
look_for_material_match(Game *game_details)
{
Boolean game_ok = TRUE;
Boolean match_comment_added = FALSE;
Move *next_move = game_details->moves;
Move *move_for_comment = NULL;
Colour colour = WHITE;
/* The initial game position has the full set of piece details. */
int num_pieces[2][NUM_PIECE_VALUES] = {
/* Dummies for OFF and EMPTY at the start. */
/* P N B R Q K */
{0, 0, 8, 2, 2, 2, 1, 1},
{0, 0, 8, 2, 2, 2, 1, 1}
};
Board *board = new_game_board(game_details->tags[FEN_TAG]);
if(game_details->tags[FEN_TAG] != NULL) {
extract_pieces_from_board(num_pieces, board);
}
/* Ensure that all previous match indications are cleared. */
reset_match_depths(endings_to_match);
/* 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.
*/
Boolean matches = FALSE;
Boolean end_of_game = FALSE;
Boolean white_matches = FALSE, black_matches = FALSE;
while (game_ok && !matches && !end_of_game) {
for (Material_details *details_to_find = endings_to_match; !matches && (details_to_find != NULL);
details_to_find = details_to_find->next) {
/* Try before applying each move.
* Note, that we wish to try both ways around because we might
* have WT,BT WF,BT ... If we don't try BLACK on WHITE success
* then we might miss a match because a full match takes several
* separate individual match steps.
*/
white_matches = material_match(details_to_find, num_pieces, WHITE);
if(details_to_find->both_colours) {
black_matches = material_match(details_to_find, num_pieces, BLACK);
}
else {
black_matches = FALSE;
}
if (white_matches || black_matches) {
matches = TRUE;
/* See whether a matching comment is required. */
if (GlobalState.add_position_match_comments && !match_comment_added) {
CommentList *match_comment = create_match_comment(board);
if (move_for_comment != NULL) {
append_comments_to_move(move_for_comment, match_comment);
}
else {
if(game_details->prefix_comment == NULL) {
game_details->prefix_comment = match_comment;
}
else {
CommentList *comm = game_details->prefix_comment;
while(comm->next != NULL) {
comm = comm->next;
}
comm->next = match_comment;
}
}
}
}
}
if(matches) {
/* Nothing required. */
}
else if(next_move == NULL) {
end_of_game = TRUE;
}
else if (*(next_move->move) != '\0') {
/* Try the next position. */
if (apply_move(next_move, board)) {
/* Remove any captured pieces. */
if (next_move->captured_piece != EMPTY) {
num_pieces[OPPOSITE_COLOUR(colour)][next_move->captured_piece]--;
}
if (next_move->promoted_piece != EMPTY) {
num_pieces[colour][next_move->promoted_piece]++;
/* Remove the promoting pawn. */
num_pieces[colour][PAWN]--;
}
move_for_comment = next_move;
colour = OPPOSITE_COLOUR(colour);
next_move = next_move->next;
}
else {
game_ok = FALSE;
}
}
else {
/* An empty move. */
fprintf(GlobalState.logfile,
"Internal error: Empty move in look_for_material_match.\n");
game_ok = FALSE;
}
}
(void) free((void *) board);
if(game_ok && matches) {
if(GlobalState.add_match_tag) {
game_details->tags[MATERIAL_MATCH_TAG] =
copy_string(white_matches ? "White" : "Black");
}
return TRUE;
}
else {
return FALSE;
}
}
/* Check to see whether the given moves lead to a position
* that matches one of the required 'material match' positions.
* In other words, a position with the required balance
* of pieces.
*/
Boolean
check_for_material_match(Game *game)
{
/* Match if there are no endings to match. */
if(endings_to_match != NULL) {
return look_for_material_match(game);
}
else {
return TRUE;
}
}
/* Does the board's material match the constraints of details_to_find?
* Return TRUE if it does, FALSE otherwise.
*/
Boolean
constraint_material_match(Material_details *details_to_find, const Board *board)
{
/* Only a single match position is required. */
details_to_find->move_depth = 0;
details_to_find->match_depth[0] = 0;
details_to_find->match_depth[1] = 0;
int num_pieces[2][NUM_PIECE_VALUES];
extract_pieces_from_board(num_pieces, board);
Boolean white_matches = material_match(details_to_find, num_pieces, WHITE);
Boolean black_matches;
if(details_to_find->both_colours) {
black_matches = material_match(details_to_find, num_pieces, BLACK);
}
else {
black_matches = FALSE;
}
return white_matches || black_matches;
}
/* Decompose the text of line to extract two sets of
* piece configurations.
* If both_colours is TRUE then matches will be tried
* for both colours in each configuration.
* Otherwise, the first set of pieces are assumed to
* be white and the second to be black.
* If pattern_constraint is TRUE then the description
* is a constraint of a FEN pattern and should not be
* retained as a separate material match.
*/
Material_details *
process_material_description(const char *line, Boolean both_colours, Boolean pattern_constraint)
{
Material_details *details = NULL;
if (non_blank_line(line)) {
details = new_ending_details(both_colours);
if (decompose_line(line, details)) {
if(!pattern_constraint) {
/* Add it on to the list. */
details->next = endings_to_match;
endings_to_match = details;
}
}
else {
(void) free((void *) details);
details = NULL;
}
}
return details;
}
/* Read a file containing material matches. */
Boolean
build_endings(const char *infile, Boolean both_colours)
{
FILE *fp = fopen(infile, "r");
Boolean Ok = TRUE;
if (fp == NULL) {
fprintf(GlobalState.logfile, "Cannot open %s for reading.\n", infile);
exit(1);
}
else {
char *line;
while ((line = read_line(fp)) != NULL) {
if(process_material_description(line, both_colours, FALSE) == NULL) {
Ok = FALSE;
}
(void) free(line);
}
(void) fclose(fp);
}
return Ok;
}