src/chess/pgn.c

Sun, 01 Oct 2023 13:56:42 +0200

author
Mike Becker <universe@uap-core.de>
date
Sun, 01 Oct 2023 13:56:42 +0200
changeset 72
7e58e0f74e50
parent 70
5427beba96d1
permissions
-rw-r--r--

improve Makefiles

/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright 2016 Mike Becker. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *   1. Redistributions of source code must retain the above copyright
 *      notice, this list of conditions and the following disclaimer.
 *
 *   2. Redistributions in binary form must reproduce the above copyright
 *      notice, this list of conditions and the following disclaimer in the
 *      documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 *
 */

#include "pgn.h"
#include <ctype.h>
#include <stdlib.h>
#include <string.h>

enum {
    pgn_error_missing_quote = 1,
    pgn_error_missing_bracket,
    pgn_error_missing_brace,
    pgn_error_missing_dot,
    pgn_error_move_syntax,
    pgn_error_move_semantics
};

static const char* pgn_error_strings[] = {
    "No Error.",
    "Tag values must be enclosed in double-quotes.",
    "Missing closing brace '}' for comment.",
    "Tags must be enclosed in square brackets: '[Key \"Value\"]'.",
    "Move numbers must be terminated with a dot (e.g. '13.' - not '13').",
    "Move is syntactically incorrect.",
    "Move is not valid according to chess rules."
};

const char* pgn_error_str(int code) {
    return pgn_error_strings[code];
}

int read_pgn(FILE* stream, GameState *gamestate, GameInfo *gameinfo) {
    int c, i;
    
    char result[8];
    
    char tagkey[32];
    char tagvalue[128];
    
    /* read tag pairs */
    _Bool readmoves = 0;
    while (!readmoves) {
        while (isspace(c = fgetc(stream)));
        if (c == '1') {
            readmoves = 1;
            break;
        }
        if (c != '[') {
            return pgn_error_missing_bracket;
        }
        while (isspace(c = fgetc(stream)));
        i = 0;
        do {
            tagkey[i++] = c;
        } while (!isspace(c = fgetc(stream)));
        tagkey[i] = '\0';
        while (isspace(c = fgetc(stream)));
        if (c != '"') {
            return pgn_error_missing_quote;
        }
        i = 0;
        while ((c = fgetc(stream)) != '"') {
            if (c == '\n' || c == EOF) {
                return pgn_error_missing_quote;
            }
            tagvalue[i++] = c;
        }
        tagvalue[i] = '\0';
        if (fgetc(stream) != ']') {
            return pgn_error_missing_bracket;
        }

        if (strcmp("Result", tagkey) == 0) {
            memcpy(result, tagvalue, 8);
        }
    }
    
    /* read moves */
    if (fgetc(stream) != '.') {
        return pgn_error_missing_dot;
    }
    
    char movestr[10];
    Move move;
    uint8_t curcol = WHITE;
    
    while (readmoves) {
        /* move */
        while (isspace(c = fgetc(stream)));
        i = 0;
        do {
            movestr[i++] = c;
            if (i >= 10) {
                return 1;
            }
        } while (!isspace(c = fgetc(stream)));
        movestr[i] = '\0';
        if (eval_move(gamestate, movestr, &move, curcol)
                != VALID_MOVE_SYNTAX) {
            return pgn_error_move_syntax;
        }
        if (validate_move(gamestate, &move) != VALID_MOVE_SEMANTICS) {
            return pgn_error_move_semantics;
        }
        apply_move(gamestate, &move);
        
        /* skip spaces */
        while (isspace(c = fgetc(stream)));
        
        /* parse possible comment */
        if (c == '{') {
            // TODO: interpret comment (may contain clock info)
            do {
                c = fgetc(stream);
            } while (c != '}' && c != EOF);
            if (c == EOF) {
                return pgn_error_missing_brace;
            }
        }
        
        /* skip spaces */
        while (isspace(c = fgetc(stream)));
        
        /* end of game data encountered */
        if (c == EOF) {
            break;
        }
        if (c == '1' || c == '0') {
            c = fgetc(stream);
            if (c == '-') {
                gamestate->resign = !gamestate->checkmate;
                break;
            } else if (c == '/') {
                gamestate->remis = !gamestate->stalemate;
                break;
            } else {
                /* oops, it was a move number, go back! */
                fseek(stream, -1, SEEK_CUR);
            }
        }
        
        /* we have eaten the next valuable byte, so go back */
        fseek(stream, -1, SEEK_CUR);
        
        /* skip move number after black move */
        if (curcol == BLACK) {
            while (isdigit(c = fgetc(stream)));
            if (c != '.') {
                return pgn_error_missing_dot;
            }
        }
        curcol = opponent_color(curcol);
    }
    
    return 0;
}

size_t write_pgn(FILE* stream, GameState *gamestate, GameInfo *gameinfo) {
    // TODO: tag pairs
    size_t bytes = 0;
    
    /* Result */
    char *result;
    if (gamestate->stalemate || gamestate->remis) {
        result = "1/2-1/2";
    } else if (gamestate->checkmate || gamestate->resign) {
        if (gamestate->lastmove) {
            result = (gamestate->lastmove->move.piece & COLOR_MASK) == WHITE ?
                "1-0" : "0-1";
        } else {
            result = "0-1";
        }
    } else {
        result = "*";
    }
    fprintf(stream, "[Result \"%s\"]\n\n", result);
    
    /* moves */
    int i = 1;
    for (MoveList *movelist = gamestate->movelist ;
        movelist ; movelist = movelist->next) {
        
        if (++i % 2 == 0) {
            fprintf(stream, "%d. %s", i/2, movelist->move.string);
        } else {
            fprintf(stream, " %s", movelist->move.string);
        }
        
        // TODO: move time and maybe other comments
        
        /* line break every 10 moves */
        if ((i-1) % 20)  {
            fputc(' ', stream);
        } else {
            fputc('\n', stream);
        }
    }
    
    if (result[0] == '*') {
        fputc('\n', stream);
    } else {
        fprintf(stream, "%s\n", result);
    }
    
    
    return bytes;
}

static size_t fen_pieces(char *str, GameState *gamestate) {
    size_t i = 0;
    for (int row = 7 ; row >= 0 ; row--) {
        unsigned int skip = 0;
        for (int file = 0 ; file < 8 ; file++) {
            if (gamestate->board[row][file]) {
                if (skip > 0) {
                    str[i++] = '0'+skip;
                    skip = 0;
                }
                switch (gamestate->board[row][file] & ~ENPASSANT_THREAT) {
                case WHITE|KING: str[i++] = 'K'; break;
                case WHITE|QUEEN: str[i++] = 'Q'; break;
                case WHITE|BISHOP: str[i++] = 'B'; break;
                case WHITE|KNIGHT: str[i++] = 'N'; break;
                case WHITE|ROOK: str[i++] = 'R'; break;
                case WHITE|PAWN: str[i++] = 'P'; break;
                case BLACK|KING: str[i++] = 'k'; break;
                case BLACK|QUEEN: str[i++] = 'q'; break;
                case BLACK|BISHOP: str[i++] = 'b'; break;
                case BLACK|KNIGHT: str[i++] = 'n'; break;
                case BLACK|ROOK: str[i++] = 'r'; break;
                case BLACK|PAWN: str[i++] = 'p'; break;
                }
            } else {
                skip++;
            }
        }
        if (skip > 0) {
            str[i++] = '0'+skip;
        }
        if (row > 0) {
            str[i++] = '/';
        }
    }
    
    return i;
}

static size_t fen_color(char *str, GameState *gamestate) {
    uint8_t color = opponent_color(gamestate->lastmove ?
        (gamestate->lastmove->move.piece & COLOR_MASK) : BLACK);
    
    str[0] = color == WHITE ? 'w' : 'b';
    
    return 1;
}

static _Bool fen_castling_chkmoved(GameState *gamestate,
    uint8_t row, uint8_t file) {
    
    MoveList *ml = gamestate->movelist;
    while (ml) {
        if (ml->move.fromfile == file && ml->move.fromrow == row) {
            return 1;
        }
        ml = ml->next;
    }
    
    return 0;
}

static size_t fen_castling(char *str, GameState *gamestate) {
    _Bool K, Q, k, q;
    
    if (fen_castling_chkmoved(gamestate, rowidx('1'), fileidx('e'))) {
        K = Q = 0;
    } else {
        K = !fen_castling_chkmoved(gamestate, rowidx('1'), fileidx('h'));
        Q = !fen_castling_chkmoved(gamestate, rowidx('1'), fileidx('a'));
    }
    if (fen_castling_chkmoved(gamestate, rowidx('8'), fileidx('e'))) {
        k = q = 0;
    } else {
        k = !fen_castling_chkmoved(gamestate, rowidx('8'), fileidx('h'));
        q = !fen_castling_chkmoved(gamestate, rowidx('8'), fileidx('a'));
    }
    
    size_t i = 0;
    if (K) str[i++] = 'K';
    if (Q) str[i++] = 'Q';
    if (k) str[i++] = 'k';
    if (q) str[i++] = 'q';
    if (!i) str[i++] = '-';
    
    return i;
}

static size_t fen_enpassant(char *str, GameState *gamestate) {
    
    str[0] = '-'; str[1] = '\0';
    
    for (int file = 0 ; file < 8 ; file++) {
        if (gamestate->board[3][file] & ENPASSANT_THREAT) {
            str[0] = filechr(file);
            str[1] = rowchr(2);
        }
        if (gamestate->board[4][file] & ENPASSANT_THREAT) {
            str[0] = filechr(file);
            str[1] = rowchr(5);
        }
    }
    
    return str[0] == '-' ? 1 : 2;
}

static size_t fen_halfmove(char *str, GameState *gamestate) {
    
    unsigned int i = 0;
    for (MoveList *move = gamestate->movelist ; move ; move = move->next) {
        if (move->move.capture || (move->move.piece & PIECE_MASK) == PAWN) {
            i = 0;
        } else {
            i++;
        }
    }
    
    char m[8];
    size_t len = sprintf(m, "%u", i);
    memcpy(str, m, len);
    
    return len;
}

static size_t fen_movenr(char *str, GameState *gamestate) {
    
    MoveList *move = gamestate->movelist;
    unsigned int i = 1;
    while (move) {
        i++;
        move = move->next;
    }
    
    char m[8];
    size_t len = sprintf(m, "%u", i);
    memcpy(str, m, len);
    
    return len;
}

void compute_fen(char *str, GameState *gamestate) {
    str += fen_pieces(str, gamestate);
    *str = ' '; str++;
    str += fen_color(str, gamestate);
    *str = ' '; str++;
    str += fen_castling(str, gamestate);
    *str = ' '; str++;
    str += fen_enpassant(str, gamestate);
    *str = ' '; str++;
    str += fen_halfmove(str, gamestate);
    *str = ' '; str++;
    str += fen_movenr(str, gamestate);
    str[0] = '\0';
}

mercurial