Fri, 21 Aug 2026 16:08:50 +0200
add create_pgn() function and use it in test-real-pgn
resolves #961
/* * 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 <stdlib.h> #include <ctype.h> #include <string.h> #include <sys/time.h> #include <time.h> 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.", "Result syntax is incorrect. Expected 1-0, 0-1, 1/2-1/2, or *.", "Result at end of move text does not match result in tag roster.", "Unexpected end of data.", }; const char* pgn_error_str(int code) { return pgn_error_strings[code]; } size_t pgn_error_pos; size_t pgn_error_position(void) { return pgn_error_pos; } static const char *pgn_result(const GameState *gamestate) { if (is_game_drawn(gamestate)) { return "1/2-1/2"; } else if (gamestate->wresign) { return "0-1"; } else if (gamestate->bresign) { return "1-0"; } else if (gamestate->checkmate) { if (current_color(gamestate) == BLACK) { return "1-0"; } else { return "0-1"; } } else { return "*"; } } #define return(code) pgn_error_pos = pgndata - pgndata0; return code int parse_pgn(const char *pgndata, GameState *gamestate) { const char * const pgndata0 = pgndata; int i; char c; char result[8] = {0}; char tagkey[32]; char tagvalue[128]; /* read tag pairs */ while (true) { while (isspace(c = *(pgndata++))); if (c == '1') { break; } if (c != '[') { return(pgn_error_missing_bracket); } while (isspace(c = *(pgndata++))); i = 0; do { tagkey[i++] = c; } while (!isspace(c = *(pgndata++))); tagkey[i] = '\0'; while (isspace(c = *(pgndata++))); if (c != '"') { return(pgn_error_missing_quote); } i = 0; while ((c = *(pgndata++)) != '"') { if (c == '\n' || c == 0) { return(pgn_error_missing_quote); } tagvalue[i++] = c; } tagvalue[i] = '\0'; if (*(pgndata++) != ']') { return(pgn_error_missing_bracket); } // TODO: read clock info if (strcmp("Result", tagkey) == 0) { memcpy(result, tagvalue, 8); } } /* read moves */ if (*(pgndata++) != '.') { return(pgn_error_missing_dot); } char movestr[10]; Move move; uint8_t curcol = WHITE; bool movetext_ends_with_result = false; while (true) { /* move */ while (isspace(c = *(pgndata++))); i = 0; do { movestr[i++] = c; if (i >= 10) { return(pgn_error_move_syntax); } } while (!isspace(c = *(pgndata++))); movestr[i] = '\0'; if (eval_move(gamestate, movestr, curcol, &move) != VALID_MOVE_SYNTAX) { return(pgn_error_move_syntax); } int move_validate_result = validate_move(gamestate, &move); if (move_validate_result != VALID_MOVE_SEMANTICS) { return(pgn_error_move_semantics); } apply_move(gamestate, &move); /* skip spaces */ while (isspace(c = *(pgndata++))); /* parse possible comment */ if (c == '{') { // TODO: interpret comment (may contain clock info) do { c = *(pgndata++); } while (c != '}' && c != 0); if (c == 0) { return(pgn_error_missing_brace); } /* skip spaces */ while (isspace(c = *(pgndata++))); } /* end of game data encountered */ if (c == 0) { break; } if (c == '1' || c == '0') { c = *(pgndata++); if (c == '-') { if (!gamestate->checkmate) { c = *(pgndata++); if (c == '1') { gamestate->wresign = true; } else if (c == '0') { gamestate->bresign = true; } else { return(pgn_error_result_syntax); } } movetext_ends_with_result = true; break; } else if (c == '/') { /* only set remis flag if the game was not drawn otherwise */ gamestate->remis = !is_game_drawn(gamestate); movetext_ends_with_result = true; break; } else { /* oops, it was a move number, go back! */ pgndata--; } } /* we have eaten the next valuable byte, so go back */ pgndata--; /* skip move number after black move */ if (curcol == BLACK) { while (isdigit(c = *(pgndata++))); if (c != '.') { return(pgn_error_missing_dot); } } curcol = opponent_color(curcol); } /* sanity check result - if it was specified */ if (movetext_ends_with_result && result[0]) { if (strncmp(result, pgn_result(gamestate), 8) != 0) { return(pgn_error_result_mismatch); } } return(pgn_no_error); } #undef return int read_pgn(FILE *stream, GameState *gamestate) { fseek(stream, 0, SEEK_END); long int size = ftell(stream); fseek(stream, 0, SEEK_SET); char *data = malloc(size + 1); memset(data, 0, size + 1); size_t read = 0; do { size_t r = fread(data + read, 1, size - read, stream); if (r == 0) { pgn_error_pos = read; return pgn_error_unexpected_eof; } read += r; } while (read < size); int result = parse_pgn(data, gamestate); free(data); return result; } static void pgn_insert_newlines(char *block) { size_t pos = 0; size_t last_space_pos = 0; size_t line_len = 0; while (block[pos] != '\0') { if (block[pos] == 0x1f) { block[pos++] = ' '; continue; } if (block[pos] == ' ') { last_space_pos = pos; } line_len++; if (line_len > 80) { block[last_space_pos] = '\n'; line_len = pos - last_space_pos; } pos++; } } static const char *pgn_date(const GameState *gamestate) { static char date[16]; time_t dateseconds; if (gamestate->movecount == 0) { struct timeval curtimestamp; gettimeofday(&curtimestamp, NULL); dateseconds = curtimestamp.tv_sec; } else { dateseconds = (time_t) gamestate->moves[0].timestamp.tv_sec; } struct tm *tm = gmtime(&dateseconds); if (tm == NULL) { /* fallback - should never happen in reality */ strncpy(date, "????.??.??", sizeof(date)); } else { int year = tm->tm_year + 1900; int month = tm->tm_mon + 1; int day = tm->tm_mday; #ifdef __GNUC__ /* this is required to tell GCC that it does not need to warn * about the small buffer size of the date string. */ if (year < 1900 || year > 9999) __builtin_unreachable(); if (month < 1 || month > 12) __builtin_unreachable(); if (day < 1 || day > 31) __builtin_unreachable(); #endif snprintf(date, sizeof(date), "%04d.%02d.%02d", year, month, day); } return date; } const char *pgn_player_name(const GameState *gamestate, uint8_t color) { const char *name = color == WHITE ? gamestate->wname : gamestate->bname; return name[0] != '\0' ? name : "Anonymous"; } int write_pgn(FILE* stream, const GameState *gamestate, bool export_comments) { char *pgn = create_pgn(gamestate, export_comments); if (pgn == NULL) return 1; fputs(pgn, stream); free(pgn); return 0; } char *create_pgn(const GameState *gamestate, bool export_comments) { size_t resultcap = 4096; char *result = malloc(resultcap); if (result == NULL) return NULL; char *resp = result; /* STR tag pairs */ resp += sprintf(resp, "[Event \"%s\"]\n", "terminal-chess game"); resp += sprintf(resp, "[Site \"%s\"]\n", "Somewhere on Earth"); resp += sprintf(resp, "[Date \"%s\"]\n", pgn_date(gamestate)); resp += sprintf(resp, "[Round \"%s\"]\n", "-"); resp += sprintf(resp, "[White \"%s\"]\n", pgn_player_name(gamestate, WHITE)); resp += sprintf(resp, "[Black \"%s\"]\n", pgn_player_name(gamestate, BLACK)); const char *gameresult = pgn_result(gamestate); resp += sprintf(resp, "[Result \"%s\"]\n\n", gameresult); // TODO: add optional clock info /* moves */ char *moveblk = resp; /* remember where the move block starts */ for (unsigned i = 0 ; i < gamestate->movecount ; i++) { /* reallocate buffer if needed */ { size_t rsize = resp - result; if (rsize + 128 > resultcap) { resultcap *= 2; char *newresult = realloc(result, resultcap); if (newresult == NULL) { free(result); return NULL; } result = newresult; resp = result + rsize; } } if (i % 2 == 0) { resp += snprintf(resp, 16, "%d.\x1f%s", 1+i/2, gamestate->moves[i].string); } else { resp += snprintf(resp, 16, "%s", gamestate->moves[i].string); } if (!export_comments) { *(resp++) = ' '; continue; } /* add clock times when the game was under time control */ if (gamestate->info.timecontrol) { memcpy(resp, " {[%clk ", 8); resp += 8; unsigned clkmv = i + 2; /* time for the next move! */ uint16_t clk = remaining_movetime2(gamestate, clkmv); resp += print_clk(clk, resp, true); *(resp++) = ']'; *(resp++) = '}'; /* elapsed move time */ memcpy(resp, " {[%emt ", 8); resp += 8; uint16_t emt = gamestate->moves[i].movetime.tv_sec; resp += print_clk(emt, resp, true); *(resp++) = ']'; *(resp++) = '}'; } *(resp++) = ' '; } if (gameresult[0] != '*') { size_t rlen = strlen(gameresult); memcpy(resp, gameresult, rlen); resp += rlen; } *(resp++) = '\n'; *resp = 0; /* post-process move block and format it nicely */ pgn_insert_newlines(moveblk); return result; }