# HG changeset patch # User Mike Becker # Date 1787321330 -7200 # Node ID 724e8acff6f836e4e8584241bb8eb73f2fe81cf1 # Parent 18dbb0dc9cd6aec26694b4d682315293e67389c4 add create_pgn() function and use it in test-real-pgn resolves #961 diff -r 18dbb0dc9cd6 -r 724e8acff6f8 src/chess/pgn.c --- a/src/chess/pgn.c Mon Aug 17 12:22:08 2026 +0200 +++ b/src/chess/pgn.c Fri Aug 21 16:08:50 2026 +0200 @@ -58,7 +58,7 @@ return pgn_error_pos; } -static const char *pgn_result(GameState *gamestate) { +static const char *pgn_result(const GameState *gamestate) { if (is_game_drawn(gamestate)) { return "1/2-1/2"; } else if (gamestate->wresign) { @@ -269,7 +269,7 @@ } } -static const char *pgn_date(GameState *gamestate) { +static const char *pgn_date(const GameState *gamestate) { static char date[16]; time_t dateseconds; if (gamestate->movecount == 0) { @@ -301,98 +301,101 @@ return date; } -const char *pgn_player_name(GameState *gamestate, uint8_t color) { +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"; } -void write_pgn(FILE* stream, GameState *gamestate, bool export_comments) { +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 */ - fprintf(stream, "[Event \"%s\"]\n", "terminal-chess game"); - fprintf(stream, "[Site \"%s\"]\n", "Somewhere on Earth"); - fprintf(stream, "[Date \"%s\"]\n", pgn_date(gamestate)); - fprintf(stream, "[Round \"%s\"]\n", "-"); - fprintf(stream, "[White \"%s\"]\n", pgn_player_name(gamestate, WHITE)); - fprintf(stream, "[Black \"%s\"]\n", pgn_player_name(gamestate, BLACK)); - const char *result = pgn_result(gamestate); - fprintf(stream, "[Result \"%s\"]\n\n", result); + 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 */ - size_t moveblkcap = 4096; - char *moveblk = malloc(moveblkcap); - char *moveblkptr = moveblk; - if (moveblk == NULL) { - // TODO: error handling (for the entire function actually) - abort(); - } + char *moveblk = resp; /* remember where the move block starts */ + for (unsigned i = 0 ; i < gamestate->movecount ; i++) { - /* reallocate move block buffer if needed */ + /* reallocate buffer if needed */ { - size_t moveblksize = moveblkptr - moveblk; - if (moveblksize + 128 > moveblkcap) { - moveblkcap *= 2; - char *newmoveblk = realloc(moveblk, moveblkcap); - if (newmoveblk == NULL) { - free(moveblk); - abort(); + size_t rsize = resp - result; + if (rsize + 128 > resultcap) { + resultcap *= 2; + char *newresult = realloc(result, resultcap); + if (newresult == NULL) { + free(result); + return NULL; } - moveblk = newmoveblk; - moveblkptr = moveblk + moveblksize; + result = newresult; + resp = result + rsize; } } - int snpr; /* return value of printf calls */ - if (i % 2 == 0) { - snpr = snprintf(moveblkptr, 16, "%d.\x1f%s", + resp += snprintf(resp, 16, "%d.\x1f%s", 1+i/2, gamestate->moves[i].string); } else { - snpr = snprintf(moveblkptr, 16, "%s", + resp += snprintf(resp, 16, "%s", gamestate->moves[i].string); } - moveblkptr += snpr; if (!export_comments) { - *(moveblkptr++) = ' '; + *(resp++) = ' '; continue; } /* add clock times when the game was under time control */ if (gamestate->info.timecontrol) { - memcpy(moveblkptr, " {[%clk ", 8); - moveblkptr += 8; + memcpy(resp, " {[%clk ", 8); + resp += 8; unsigned clkmv = i + 2; /* time for the next move! */ uint16_t clk = remaining_movetime2(gamestate, clkmv); - snpr = print_clk(clk, moveblkptr, true); - moveblkptr += snpr; - *(moveblkptr++) = ']'; - *(moveblkptr++) = '}'; + resp += print_clk(clk, resp, true); + *(resp++) = ']'; + *(resp++) = '}'; /* elapsed move time */ - memcpy(moveblkptr, " {[%emt ", 8); - moveblkptr += 8; + memcpy(resp, " {[%emt ", 8); + resp += 8; uint16_t emt = gamestate->moves[i].movetime.tv_sec; - snpr = print_clk(emt, moveblkptr, true); - moveblkptr += snpr; - *(moveblkptr++) = ']'; - *(moveblkptr++) = '}'; + resp += print_clk(emt, resp, true); + *(resp++) = ']'; + *(resp++) = '}'; } - *(moveblkptr++) = ' '; + *(resp++) = ' '; } - if (result[0] != '*') { - size_t rlen = strlen(result); - memcpy(moveblkptr, result, rlen); - moveblkptr += rlen; + if (gameresult[0] != '*') { + size_t rlen = strlen(gameresult); + memcpy(resp, gameresult, rlen); + resp += rlen; } - *(moveblkptr++) = '\n'; - *moveblkptr = 0; + *(resp++) = '\n'; + *resp = 0; + /* post-process move block and format it nicely */ pgn_insert_newlines(moveblk); - fputs(moveblk, stream); - free(moveblk); + return result; } diff -r 18dbb0dc9cd6 -r 724e8acff6f8 src/chess/pgn.h --- a/src/chess/pgn.h Mon Aug 17 12:22:08 2026 +0200 +++ b/src/chess/pgn.h Fri Aug 21 16:08:50 2026 +0200 @@ -52,11 +52,12 @@ }; int read_pgn(FILE *stream, GameState *gamestate); +int write_pgn(FILE* stream, const GameState *gamestate, bool export_comments); + int parse_pgn(const char *data, GameState *gamestate); -void write_pgn(FILE* stream, GameState *gamestate, bool export_comments); -void compute_fen(char *str, GameState *gamestate); +char *create_pgn(const GameState *gamestate, bool export_comments); -const char *pgn_player_name(GameState *gamestate, uint8_t color); +const char *pgn_player_name(const GameState *gamestate, uint8_t color); const char* pgn_error_str(int code); size_t pgn_error_position(void); diff -r 18dbb0dc9cd6 -r 724e8acff6f8 test/test-real-pgn.c --- a/test/test-real-pgn.c Mon Aug 17 12:22:08 2026 +0200 +++ b/test/test-real-pgn.c Fri Aug 21 16:08:50 2026 +0200 @@ -65,6 +65,7 @@ ++index; GameState gamestate; gamestate_init(&gamestate); + char *chkpgn = NULL; CX_TEST_DO { const enum expected_result rs = pgn_data[index-1][0]; const char *pgn = pgn_data[index-1]+1; @@ -74,7 +75,26 @@ index, result, pgn_error_position()); CX_TEST_ASSERTM(result == pgn_no_error, msg); CX_TEST_CALL_SUBROUTINE(verify_expected_result, &gamestate, rs); + + /* check if we get the PGN text back */ + chkpgn = create_pgn(&gamestate, false); + /* find move blocks (ignore the STR) */ + char *chkpgn_moves = strstr(chkpgn, "1. "); + const char *pgn_moves = strstr(pgn, "1. "); + /* revert the formatting (test data is also unformatted) */ + for (size_t i = 0; ; i++) { + if (chkpgn_moves[i] == '\n') chkpgn_moves[i] = ' '; + if (chkpgn_moves[i] == '\0') { + /* do not replace last line break */ + chkpgn_moves[i-1] = '\n'; + break; + } + } + + /* compare */ + CX_TEST_ASSERTM(strcmp(chkpgn_moves, pgn_moves) == 0, "PGN move text mismatch"); } + free(chkpgn); gamestate_cleanup(&gamestate); }