# HG changeset patch # User Mike Becker # Date 1788522588 -7200 # Node ID 1e9751f8eb0d2379fa5608611c6d886c8c783df7 # Parent 507490519676622c700d1990e1ca97d1b1b486cf add function to reconstruct a board from a FEN string resolves #939 in principle, but there are many TODOs left diff -r 507490519676 -r 1e9751f8eb0d src/chess/fen.c --- 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 #include +#include -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 diff -r 507490519676 -r 1e9751f8eb0d src/chess/fen.h --- 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" */ diff -r 507490519676 -r 1e9751f8eb0d src/chess/rules.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) { diff -r 507490519676 -r 1e9751f8eb0d src/chess/rules.h --- 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; diff -r 507490519676 -r 1e9751f8eb0d test/Makefile --- 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 $< diff -r 507490519676 -r 1e9751f8eb0d test/run-tests.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(), diff -r 507490519676 -r 1e9751f8eb0d test/test-fen.c --- /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; +}