src/chess/fen.c

changeset 218
1e9751f8eb0d
parent 216
d0c3d3016650
--- 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

mercurial