universe@50: /* universe@50: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. universe@50: * universe@55: * Copyright 2016 Mike Becker. All rights reserved. universe@50: * universe@50: * Redistribution and use in source and binary forms, with or without universe@50: * modification, are permitted provided that the following conditions are met: universe@50: * universe@50: * 1. Redistributions of source code must retain the above copyright universe@50: * notice, this list of conditions and the following disclaimer. universe@50: * universe@50: * 2. Redistributions in binary form must reproduce the above copyright universe@50: * notice, this list of conditions and the following disclaimer in the universe@50: * documentation and/or other materials provided with the distribution. universe@50: * universe@50: * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" universe@50: * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE universe@50: * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE universe@50: * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE universe@50: * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR universe@50: * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF universe@50: * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS universe@50: * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN universe@50: * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) universe@50: * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE universe@50: * POSSIBILITY OF SUCH DAMAGE. universe@50: * universe@50: */ universe@50: universe@50: #include "pgn.h" universe@50: #include universe@50: #include universe@50: #include universe@50: universe@60: enum { universe@60: pgn_error_missing_quote = 1, universe@60: pgn_error_missing_bracket, universe@60: pgn_error_missing_dot, universe@60: pgn_error_move_syntax, universe@60: pgn_error_move_semantics universe@60: }; universe@60: universe@60: static const char* pgn_error_strings[] = { universe@60: "No Error.", universe@60: "Tag values must be enclosed in double-quotes.", universe@60: "Tags must be enclosed in square brackets: '[Key \"Value\"]'.", universe@60: "Move numbers must be terminated with a dot (e.g. '13.' - not '13').", universe@60: "Move is syntactically incorrect.", universe@60: "Move is not valid according to chess rules." universe@60: }; universe@60: universe@60: const char* pgn_error_str(int code) { universe@60: return pgn_error_strings[code]; universe@60: } universe@60: universe@50: int read_pgn(FILE* stream, GameState *gamestate, GameInfo *gameinfo) { universe@50: int c, i; universe@50: universe@50: char result[8]; universe@50: universe@50: char tagkey[32]; universe@50: char tagvalue[128]; universe@50: universe@64: /* read tag pairs */ universe@50: _Bool readmoves = 0; universe@50: while (!readmoves) { universe@50: while (isspace(c = fgetc(stream))); universe@50: if (c == '1') { universe@50: readmoves = 1; universe@50: break; universe@50: } universe@50: if (c != '[') { universe@60: return pgn_error_missing_bracket; universe@50: } universe@50: while (isspace(c = fgetc(stream))); universe@50: i = 0; universe@50: do { universe@50: tagkey[i++] = c; universe@50: } while (!isspace(c = fgetc(stream))); universe@50: tagkey[i] = '\0'; universe@50: while (isspace(c = fgetc(stream))); universe@50: if (c != '"') { universe@60: return pgn_error_missing_quote; universe@50: } universe@50: i = 0; universe@50: while ((c = fgetc(stream)) != '"') { universe@60: if (c == '\n' || c == EOF) { universe@60: return pgn_error_missing_quote; universe@50: } universe@50: tagvalue[i++] = c; universe@50: } universe@50: tagvalue[i] = '\0'; universe@50: if (fgetc(stream) != ']') { universe@60: return pgn_error_missing_bracket; universe@50: } universe@50: universe@50: if (strcmp("Result", tagkey) == 0) { universe@50: memcpy(result, tagvalue, 8); universe@50: } universe@50: } universe@50: universe@64: /* read moves */ universe@50: if (fgetc(stream) != '.') { universe@60: return pgn_error_missing_dot; universe@50: } universe@50: universe@50: char movestr[10]; universe@50: Move move; universe@50: uint8_t curcol = WHITE; universe@50: universe@50: while (readmoves) { universe@64: /* move */ universe@50: while (isspace(c = fgetc(stream))); universe@50: i = 0; universe@50: do { universe@50: movestr[i++] = c; universe@50: if (i >= 10) { universe@59: return 1; universe@50: } universe@50: } while (!isspace(c = fgetc(stream))); universe@50: movestr[i] = '\0'; universe@50: if (eval_move(gamestate, movestr, &move, curcol) universe@50: != VALID_MOVE_SYNTAX) { universe@60: return pgn_error_move_syntax; universe@50: } universe@50: if (validate_move(gamestate, &move) != VALID_MOVE_SEMANTICS) { universe@60: return pgn_error_move_semantics; universe@50: } universe@50: apply_move(gamestate, &move); universe@50: universe@50: // TODO: parse comments universe@50: while (isspace(c = fgetc(stream))); universe@50: universe@64: /* end of game data encountered */ universe@50: if (c == EOF) { universe@50: break; universe@50: } universe@50: if (c == '1' || c == '0') { universe@50: c = fgetc(stream); universe@50: if (c == '-') { universe@50: gamestate->resign = !gamestate->checkmate; universe@50: break; universe@50: } else if (c == '/') { universe@50: gamestate->remis = !gamestate->stalemate; universe@50: break; universe@50: } else { universe@64: /* oops, it was a move number, go back! */ universe@50: fseek(stream, -1, SEEK_CUR); universe@50: } universe@50: } universe@50: universe@64: /* we have eaten the next valuable byte, so go back */ universe@50: fseek(stream, -1, SEEK_CUR); universe@50: universe@64: /* skip move number after black move */ universe@50: if (curcol == BLACK) { universe@50: while (isdigit(c = fgetc(stream))); universe@50: if (c != '.') { universe@60: return pgn_error_missing_dot; universe@50: } universe@50: } universe@50: curcol = opponent_color(curcol); universe@50: } universe@50: universe@59: return 0; universe@50: } universe@50: universe@50: size_t write_pgn(FILE* stream, GameState *gamestate, GameInfo *gameinfo) { universe@50: // TODO: tag pairs universe@50: size_t bytes = 0; universe@50: universe@64: /* Result */ universe@50: char *result; universe@50: if (gamestate->stalemate || gamestate->remis) { universe@50: result = "1/2-1/2"; universe@50: } else if (gamestate->checkmate || gamestate->resign) { universe@50: if (gamestate->lastmove) { universe@50: result = (gamestate->lastmove->move.piece & COLOR_MASK) == WHITE ? universe@50: "1-0" : "0-1"; universe@50: } else { universe@50: result = "0-1"; universe@50: } universe@50: } else { universe@50: result = "*"; universe@50: } universe@50: fprintf(stream, "[Result \"%s\"]\n\n", result); universe@50: universe@64: /* moves */ universe@50: int i = 1; universe@50: for (MoveList *movelist = gamestate->movelist ; universe@50: movelist ; movelist = movelist->next) { universe@50: universe@50: if (++i % 2 == 0) { universe@50: fprintf(stream, "%d. %s", i/2, movelist->move.string); universe@50: } else { universe@50: fprintf(stream, " %s", movelist->move.string); universe@50: } universe@50: universe@50: // TODO: move time and maybe other comments universe@50: universe@64: /* line break every 10 moves */ universe@65: if ((i-1) % 20) { universe@50: fputc(' ', stream); universe@50: } else { universe@50: fputc('\n', stream); universe@50: } universe@50: } universe@50: universe@50: if (result[0] == '*') { universe@50: fputc('\n', stream); universe@50: } else { universe@50: fprintf(stream, "%s\n", result); universe@50: } universe@50: universe@50: universe@50: return bytes; universe@50: } universe@54: universe@54: static size_t fen_pieces(char *str, GameState *gamestate) { universe@54: size_t i = 0; universe@54: for (int row = 7 ; row >= 0 ; row--) { universe@54: unsigned int skip = 0; universe@54: for (int file = 0 ; file < 8 ; file++) { universe@54: if (gamestate->board[row][file]) { universe@54: if (skip > 0) { universe@54: str[i++] = '0'+skip; universe@54: skip = 0; universe@54: } universe@54: switch (gamestate->board[row][file] & ~ENPASSANT_THREAT) { universe@54: case WHITE|KING: str[i++] = 'K'; break; universe@54: case WHITE|QUEEN: str[i++] = 'Q'; break; universe@54: case WHITE|BISHOP: str[i++] = 'B'; break; universe@54: case WHITE|KNIGHT: str[i++] = 'N'; break; universe@54: case WHITE|ROOK: str[i++] = 'R'; break; universe@54: case WHITE|PAWN: str[i++] = 'P'; break; universe@54: case BLACK|KING: str[i++] = 'k'; break; universe@54: case BLACK|QUEEN: str[i++] = 'q'; break; universe@54: case BLACK|BISHOP: str[i++] = 'b'; break; universe@54: case BLACK|KNIGHT: str[i++] = 'n'; break; universe@54: case BLACK|ROOK: str[i++] = 'r'; break; universe@54: case BLACK|PAWN: str[i++] = 'p'; break; universe@54: } universe@54: } else { universe@54: skip++; universe@54: } universe@54: } universe@54: if (skip > 0) { universe@54: str[i++] = '0'+skip; universe@54: } universe@54: if (row > 0) { universe@54: str[i++] = '/'; universe@54: } universe@54: } universe@54: universe@54: return i; universe@54: } universe@54: universe@54: static size_t fen_color(char *str, GameState *gamestate) { universe@54: uint8_t color = opponent_color(gamestate->lastmove ? universe@54: (gamestate->lastmove->move.piece & COLOR_MASK) : BLACK); universe@54: universe@54: str[0] = color == WHITE ? 'w' : 'b'; universe@54: universe@54: return 1; universe@54: } universe@54: universe@54: static _Bool fen_castling_chkmoved(GameState *gamestate, universe@54: uint8_t row, uint8_t file) { universe@54: universe@54: MoveList *ml = gamestate->movelist; universe@54: while (ml) { universe@54: if (ml->move.fromfile == file && ml->move.fromrow == row) { universe@54: return 1; universe@54: } universe@54: ml = ml->next; universe@54: } universe@54: universe@54: return 0; universe@54: } universe@54: universe@54: static size_t fen_castling(char *str, GameState *gamestate) { universe@54: _Bool K, Q, k, q; universe@54: universe@54: if (fen_castling_chkmoved(gamestate, rowidx('1'), fileidx('e'))) { universe@54: K = Q = 0; universe@54: } else { universe@54: K = !fen_castling_chkmoved(gamestate, rowidx('1'), fileidx('h')); universe@54: Q = !fen_castling_chkmoved(gamestate, rowidx('1'), fileidx('a')); universe@54: } universe@54: if (fen_castling_chkmoved(gamestate, rowidx('8'), fileidx('e'))) { universe@54: k = q = 0; universe@54: } else { universe@54: k = !fen_castling_chkmoved(gamestate, rowidx('8'), fileidx('h')); universe@54: q = !fen_castling_chkmoved(gamestate, rowidx('8'), fileidx('a')); universe@54: } universe@54: universe@54: size_t i = 0; universe@54: if (K) str[i++] = 'K'; universe@54: if (Q) str[i++] = 'Q'; universe@54: if (k) str[i++] = 'k'; universe@54: if (q) str[i++] = 'q'; universe@54: if (!i) str[i++] = '-'; universe@54: universe@54: return i; universe@54: } universe@54: universe@54: static size_t fen_enpassant(char *str, GameState *gamestate) { universe@54: universe@54: str[0] = '-'; str[1] = '\0'; universe@54: universe@54: for (int file = 0 ; file < 8 ; file++) { universe@54: if (gamestate->board[3][file] & ENPASSANT_THREAT) { universe@54: str[0] = filechr(file); universe@54: str[1] = rowchr(2); universe@54: } universe@54: if (gamestate->board[4][file] & ENPASSANT_THREAT) { universe@54: str[0] = filechr(file); universe@54: str[1] = rowchr(5); universe@54: } universe@54: } universe@54: universe@54: return str[0] == '-' ? 1 : 2; universe@54: } universe@54: universe@54: static size_t fen_halfmove(char *str, GameState *gamestate) { universe@54: universe@54: unsigned int i = 0; universe@54: for (MoveList *move = gamestate->movelist ; move ; move = move->next) { universe@54: if (move->move.capture || (move->move.piece & PIECE_MASK) == PAWN) { universe@54: i = 0; universe@54: } else { universe@54: i++; universe@54: } universe@54: } universe@54: universe@54: char m[8]; universe@54: size_t len = sprintf(m, "%u", i); universe@54: memcpy(str, m, len); universe@54: universe@54: return len; universe@54: } universe@54: universe@54: static size_t fen_movenr(char *str, GameState *gamestate) { universe@54: universe@54: MoveList *move = gamestate->movelist; universe@54: unsigned int i = 1; universe@54: while (move) { universe@54: i++; universe@54: move = move->next; universe@54: } universe@54: universe@54: char m[8]; universe@54: size_t len = sprintf(m, "%u", i); universe@54: memcpy(str, m, len); universe@54: universe@54: return len; universe@54: } universe@54: universe@54: void compute_fen(char *str, GameState *gamestate) { universe@54: str += fen_pieces(str, gamestate); universe@54: *str = ' '; str++; universe@54: str += fen_color(str, gamestate); universe@54: *str = ' '; str++; universe@54: str += fen_castling(str, gamestate); universe@54: *str = ' '; str++; universe@54: str += fen_enpassant(str, gamestate); universe@54: *str = ' '; str++; universe@54: str += fen_halfmove(str, gamestate); universe@54: *str = ' '; str++; universe@54: str += fen_movenr(str, gamestate); universe@54: str[0] = '\0'; universe@54: }