add function to reconstruct a board from a FEN string default tip

Fri, 04 Sep 2026 13:49:48 +0200

author
Mike Becker <universe@uap-core.de>
date
Fri, 04 Sep 2026 13:49:48 +0200
changeset 218
1e9751f8eb0d
parent 217
507490519676

add function to reconstruct a board from a FEN string

resolves #939 in principle, but there are many TODOs left

src/chess/fen.c file | annotate | diff | comparison | revisions
src/chess/fen.h file | annotate | diff | comparison | revisions
src/chess/rules.c file | annotate | diff | comparison | revisions
src/chess/rules.h file | annotate | diff | comparison | revisions
test/Makefile file | annotate | diff | comparison | revisions
test/run-tests.c file | annotate | diff | comparison | revisions
test/test-fen.c file | annotate | diff | comparison | revisions
--- a/src/chess/fen.c	Thu Sep 03 18:32:47 2026 +0200
+++ b/src/chess/fen.c	Fri Sep 04 13:49:48 2026 +0200
@@ -31,8 +31,9 @@
 
 #include <stdlib.h>
 #include <stdio.h>
+#include <string.h>
 
-static size_t fen_pieces(char *str, GameState *gamestate) {
+static size_t fen_pieces(char *str, const GameState *gamestate) {
     size_t i = 0;
     Rank rank = 7;
     do {
@@ -72,12 +73,12 @@
     return i;
 }
 
-static size_t fen_color(char *str, GameState *gamestate) {
+static size_t fen_color(char *str, const GameState *gamestate) {
     str[0] = current_color(gamestate) == WHITE ? 'w' : 'b';
     return 1;
 }
 
-static size_t fen_castling(char *str, GameState *gamestate) {
+static size_t fen_castling(char *str, const GameState *gamestate) {
     size_t i = 0;
     if (!gamestate->castling.K) str[i++] = 'K';
     if (!gamestate->castling.Q) str[i++] = 'Q';
@@ -88,7 +89,7 @@
     return i;
 }
 
-static size_t fen_enpassant(char *str, GameState *gamestate) {
+static size_t fen_enpassant(char *str, const GameState *gamestate) {
 
     str[0] = '-';
 
@@ -106,7 +107,8 @@
     return str[0] == '-' ? 1 : 2;
 }
 
-static size_t fen_halfmove(char *str, GameState *gamestate) {
+static size_t fen_halfmove(char *str, const GameState *gamestate) {
+    // TODO: respect a possible fifty_ctr_start
     unsigned int hm = 0;
     for (unsigned int i = 0; i < gamestate->movecount; i++) {
         if (gamestate->moves[i].capture
@@ -120,8 +122,9 @@
     return sprintf(str, "%u", hm);
 }
 
-static size_t fen_movenr(char *str, GameState *gamestate) {
-    return sprintf(str, "%u", 1 + gamestate->movecount / 2);
+static size_t fen_movenr(char *str, const GameState *gamestate) {
+    unsigned mc = gamestate->movecount + gamestate->move_start;
+    return sprintf(str, "%u", 1 + mc / 2);
 }
 
 static size_t fen_space(char *str) {
@@ -129,7 +132,7 @@
     return 1;
 }
 
-void fen_compute(char *str, GameState *gamestate) {
+void fen_compute(char *str, const GameState *gamestate) {
     str += fen_pieces(str, gamestate);
     str += fen_space(str);
     str += fen_color(str, gamestate);
@@ -143,3 +146,153 @@
     str += fen_movenr(str, gamestate);
     *str = '\0';
 }
+
+static unsigned fen_parse_number(const char *str, unsigned *target) {
+    unsigned l = 0;
+    *target = 0;
+    while (str[l] >= '0' && str[l] <= '9') {
+        unsigned n = str[l] - '0';
+        *target *= 10;
+        *target += n;
+        l++;
+    }
+    /* safety precaution - reject unreasonable high numbers */
+    if (l > 5) return 0;
+    return l;
+}
+
+int fen_parse(const char *str, GameState *gamestate) {
+    const char * const fen_start = str;
+    // TODO: think about error reporting that is as good as for PGNs
+
+    if (str == NULL) return 1;
+
+    /* zero-initialize the game state */
+    memset(gamestate, 0,  sizeof(GameState));
+
+    /* parse the board (FEN starts top-left at "a8") */
+    Rank r = 7;
+    File f = 0;
+    while (true) {
+        switch (*str) {
+        case 'K': gamestate->board[r][f] = WKING; break;
+        case 'Q': gamestate->board[r][f] = WQUEEN; break;
+        case 'B': gamestate->board[r][f] = WBISHOP; break;
+        case 'N': gamestate->board[r][f] = WKNIGHT; break;
+        case 'R': gamestate->board[r][f] = WROOK; break;
+        case 'P': gamestate->board[r][f] = WPAWN; break;
+        case 'k': gamestate->board[r][f] = BKING; break;
+        case 'q': gamestate->board[r][f] = BQUEEN; break;
+        case 'b': gamestate->board[r][f] = BBISHOP; break;
+        case 'n': gamestate->board[r][f] = BKNIGHT; break;
+        case 'r': gamestate->board[r][f] = BROOK; break;
+        case 'p': gamestate->board[r][f] = BPAWN; break;
+        case '1': break;
+        case '2': f += 1; break;
+        case '3': f += 2; break;
+        case '4': f += 3; break;
+        case '5': f += 4; break;
+        case '6': f += 5; break;
+        case '7': f += 6; break;
+        case '8': f += 7; break;
+        default: return 1;
+        }
+        f++;
+        str++;
+        if (f == 8) {
+            /* rank complete - test for separator or ending space */
+            if (r > 0) {
+                if (*str != '/') return 1;
+                str++;
+                f = 0;
+                r--;
+            } else {
+                if (*str != ' ') return 1;
+                str++;
+                break;
+            }
+        }
+    }
+
+    /* whose turn is it? */
+    bool white_to_move;
+    if (str[0] == 'w') {
+        white_to_move = true;
+    } else if (str[0] == 'b') {
+        white_to_move = false;
+    } else {
+        return 1;
+    }
+    if (str[1] != ' ') return 1;
+    str += 2;
+
+    /* castling rights */
+    gamestate->castling.K = gamestate->castling.Q = true;
+    gamestate->castling.k = gamestate->castling.q = true;
+    if (*str == '-') {
+        str++;
+    } else {
+        char cstl[5] = "KQkq";
+        bool found = false;
+        for (unsigned i = 0 ; i < 4 ; i++) {
+            if (*str == cstl[i]) {
+                found = true;
+                switch (i) {
+                case 0: gamestate->castling.K = false; break;
+                case 1: gamestate->castling.Q = false; break;
+                case 2: gamestate->castling.k = false; break;
+                case 3: gamestate->castling.q = false; break;
+                }
+                str++;
+            }
+        }
+        if (!found) return 1; /* no castling info found */
+    }
+    if (*str != ' ') return 1;
+    str++;
+
+    /* is there an en-passant threat? */
+    if (*str == '-') {
+        str++;
+    } else {
+        if (isfile(str[0]) && isrank(str[1])) {
+            f = fileidx(str[0]);
+            r = rankidx(str[1]);
+            if (r == 2) {
+                r = 3;
+            } else if (r == 5) {
+                r = 4;
+            } else {
+                return 1;
+            }
+            /* the threat is applied to the pawn, not the field it passed */
+            enpassant_threat_add(gamestate, f, r);
+        } else {
+            return 1;
+        }
+    }
+    if (*str != ' ') return 1;
+    str++;
+
+    /* fifty-moves counter */
+    unsigned mnr;
+    unsigned mlen;
+    mlen = fen_parse_number(str, &mnr);
+    if (mlen == 0) return 1;
+    if (str[mlen] != ' ') return 1;
+    str += mlen+1;
+    gamestate->fifty_cntr_start = mnr;
+
+    /* move number */
+    mlen = fen_parse_number(str, &mnr);
+    if (mlen == 0) return 1;
+    if (mnr == 0) return 1;
+    if (str[mlen] != '\0') return 1;
+    str += mlen+1;
+    gamestate->move_start = 2*mnr - 1;
+    if (white_to_move) gamestate->move_start--;
+
+    /* only copy the fen string if everything is a success */
+    gamestate->fen_start = strdup(fen_start);
+    return 0;
+}
\ No newline at end of file
--- a/src/chess/fen.h	Thu Sep 03 18:32:47 2026 +0200
+++ b/src/chess/fen.h	Fri Sep 04 13:49:48 2026 +0200
@@ -50,7 +50,18 @@
  * @param gamestate the current game state
  * @see #FEN_MAX_LENGTH
  */
-void fen_compute(char *str, GameState *gamestate);
+void fen_compute(char *str, const GameState *gamestate);
+
+/**
+ * Constructs a game state from a FEN string.
+ *
+ * Note that this cannot reconstruct a move history by design.
+ *
+ * @param str the FEN string
+ * @param gamestate uninitialized target structure
+ * @return zero on success, non-zero when the FEN is invalid
+ */
+int fen_parse(const char *str, GameState *gamestate);
 
 #ifdef __cplusplus
 } /* extern "C" */
--- a/src/chess/rules.c	Thu Sep 03 18:32:47 2026 +0200
+++ b/src/chess/rules.c	Fri Sep 04 13:49:48 2026 +0200
@@ -103,7 +103,9 @@
 }
 
 Color current_color(const GameState *gamestate) {
-    return (gamestate->movecount % 2 == 0) ? WHITE : BLACK;
+    // TODO: better would be an API that returns the correct number
+    unsigned mc = gamestate->movecount + gamestate->move_start;
+    return (mc % 2 == 0) ? WHITE : BLACK;
 }
 
 static void calc_movetime(GameState *gamestate, Move *move) {
--- a/src/chess/rules.h	Thu Sep 03 18:32:47 2026 +0200
+++ b/src/chess/rules.h	Fri Sep 04 13:49:48 2026 +0200
@@ -165,6 +165,10 @@
     unsigned int movecapacity;
     /** number of (half-)moves (counting BOTH colors) */
     unsigned int movecount;
+    /** number of half-moves that have been played before reaching fen_start */
+    unsigned int move_start; // TODO: use this in board view and PGN export
+    /** number of half-moves w/o capture or pawn move played before fen_start */
+    unsigned int fifty_cntr_start;
     /** a premove that shall be evaluated next time it's our turn */
     char premove[8];
     bool checkmate;
--- a/test/Makefile	Thu Sep 03 18:32:47 2026 +0200
+++ b/test/Makefile	Fri Sep 04 13:49:48 2026 +0200
@@ -32,6 +32,7 @@
        test-datatypes.c \
        test-rules-helper.c \
        test-real-pgn.c \
+       test-fen.c \
        test-pawn.c \
        test-rook.c \
        test-knight.c \
@@ -63,6 +64,11 @@
 	@echo "Compiling $<"
 	$(CC) -o $@ $(CFLAGS) -c $<
 
+$(BUILDDIR)/test-fen.o: test-fen.c ../src/chess/fen.h \
+ ../src/chess/rules.h ucxtest.h
+	@echo "Compiling $<"
+	$(CC) -o $@ $(CFLAGS) -c $<
+
 $(BUILDDIR)/test-king.o: test-king.c ../src/chess/rules.h ucxtest.h
 	@echo "Compiling $<"
 	$(CC) -o $@ $(CFLAGS) -c $<
--- a/test/run-tests.c	Thu Sep 03 18:32:47 2026 +0200
+++ b/test/run-tests.c	Fri Sep 04 13:49:48 2026 +0200
@@ -29,8 +29,10 @@
 
 #include "ucxtest.h"
 
+CxTestSuite *test_rules_helper_suite(void);
+
 CxTestSuite *test_real_pgn_suite(void);
-CxTestSuite *test_rules_helper_suite(void);
+CxTestSuite *test_fen_suite(void);
 
 CxTestSuite *test_pawn_suite(void);
 CxTestSuite *test_rook_suite(void);
@@ -41,8 +43,9 @@
 
 int main(void) {
     CxTestSuite *suites[] = {
+        test_rules_helper_suite(),
         test_real_pgn_suite(),
-        test_rules_helper_suite(),
+        test_fen_suite(),
         test_pawn_suite(),
         test_rook_suite(),
         test_knight_suite(),
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/test-fen.c	Fri Sep 04 13:49:48 2026 +0200
@@ -0,0 +1,106 @@
+/*
+* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2026 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 "chess/fen.h"
+#include "ucxtest.h"
+
+static CX_TEST(test_fen_basic) {
+    /* a test that simply checks if an arbitrary FEN is parsed correctly */
+    const char * fen = "r1n3k1/5qbp/p5p1/1p6/4Q3/B6P/P4PP1/R4RK1 b - - 0 27";
+    Board expected_board = {
+        {WROOK,   0,       0,       0,      0,      WROOK,   WKING,   0},
+        {WPAWN,   0,       0,       0,      0,      WPAWN,   WPAWN,   0},
+        {WBISHOP, 0,       0,       0,      0,      0,       0,       WPAWN},
+        {0,       0,       0,       0,      WQUEEN, 0,       0,       0},
+        {0,       BPAWN,   0,       0,      0,      0,       0,       0},
+        {BPAWN,   0,       0,       0,      0,      0,       BPAWN,   0},
+        {0,       0,       0,       0,      0,      BQUEEN,  BBISHOP, BPAWN},
+        {BROOK,   0,       BKNIGHT, 0,      0,      0,       BKING,   0}
+    };
+    GameState gs;
+    int result = fen_parse(fen, &gs);
+    CX_TEST_DO {
+        CX_TEST_ASSERT(result == 0);
+        CX_TEST_ASSERT(strcmp(gs.fen_start, fen) == 0);
+        /* counter for fifty-move rule initialized with zero */
+        CX_TEST_ASSERT(gs.fifty_cntr_start == 0);
+        /* we start after 53 half-moves have been played */
+        CX_TEST_ASSERT(gs.move_start == 53);
+        /* we have no move history */
+        CX_TEST_ASSERT(gs.movecount == 0);
+        CX_TEST_ASSERT(gs.movecapacity == 0);
+        CX_TEST_ASSERT(gs.moves == NULL);
+        CX_TEST_ASSERT(gs.fen == NULL);
+        /* all castling rights should be revoked */
+        CX_TEST_ASSERT(gs.castling.K == true);
+        CX_TEST_ASSERT(gs.castling.Q == true);
+        CX_TEST_ASSERT(gs.castling.k == true);
+        CX_TEST_ASSERT(gs.castling.q == true);
+        /* check the board */
+        for (File f = 0 ; f < 8 ; f++) {
+            for (Rank r = 0 ; r < 8 ; r++) {
+                Piece exp = expected_board[r][f];
+                Piece p = piece_at(&gs, f, r);
+                char msg[64];
+                sprintf(msg, "expected %u at %c:%c but got %u",
+                    exp, filechr(f), rankchr(r), p);
+                CX_TEST_ASSERTM(exp == p, msg);
+            }
+        }
+        /* check that other stuff is empty-initialized */
+        CX_TEST_ASSERT(gs.bname[0] == 0);
+        CX_TEST_ASSERT(gs.wname[0] == 0);
+        CX_TEST_ASSERT(gs.premove[0] == 0);
+        CX_TEST_ASSERT(!gs.checkmate);
+        CX_TEST_ASSERT(!gs.stalemate);
+        CX_TEST_ASSERT(!gs.threefold);
+        CX_TEST_ASSERT(!gs.nomaterial);
+        CX_TEST_ASSERT(!gs.remis);
+        CX_TEST_ASSERT(!gs.wresign);
+        CX_TEST_ASSERT(!gs.bresign);
+        CX_TEST_ASSERT(!gs.ragequit);
+        CX_TEST_ASSERT(!gs.review);
+        /* check that we get the original FEN back */
+        char fen_chk[FEN_MAX_LENGTH];
+        fen_compute(fen_chk, &gs);
+        CX_TEST_ASSERTM(strcmp(fen, fen_chk) == 0, fen_chk);
+    }
+    gamestate_cleanup(&gs);
+}
+
+CxTestSuite* test_fen_suite(void) {
+    CxTestSuite* suite = cx_test_suite_new("Test FEN API");
+
+    cx_test_register(suite, test_fen_basic);
+    // TODO: test FENs that describe definitely ended games (for each type of ending)
+    // TODO: test FEN that comes with a non-zero fifty-move rule counter
+    // TODO: test FEN with en-passant threat
+
+    return suite;
+}

mercurial