--- 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