add create_pgn() function and use it in test-real-pgn

Fri, 21 Aug 2026 16:08:50 +0200

author
Mike Becker <universe@uap-core.de>
date
Fri, 21 Aug 2026 16:08:50 +0200
changeset 178
724e8acff6f8
parent 177
18dbb0dc9cd6
child 179
5ef724e21702

add create_pgn() function and use it in test-real-pgn

resolves #961

src/chess/pgn.c file | annotate | diff | comparison | revisions
src/chess/pgn.h file | annotate | diff | comparison | revisions
test/test-real-pgn.c file | annotate | diff | comparison | revisions
--- 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;
 }
--- 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);
--- 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);
 }
 

mercurial