# HG changeset patch # User Mike Becker # Date 1788604564 -7200 # Node ID 24b866230dd46da619e4d2a0b8ad532c0f050ff1 # Parent 1e9751f8eb0d2379fa5608611c6d886c8c783df7 identify all and fix some occurrences where the new move_start offset matters relates to #939 diff -r 1e9751f8eb0d -r 24b866230dd4 src/chess/fen.c --- a/src/chess/fen.c Fri Sep 04 13:49:48 2026 +0200 +++ b/src/chess/fen.c Sat Sep 05 12:36:04 2026 +0200 @@ -108,8 +108,7 @@ } static size_t fen_halfmove(char *str, const GameState *gamestate) { - // TODO: respect a possible fifty_ctr_start - unsigned int hm = 0; + unsigned int hm = gamestate->fifty_cntr_start; for (unsigned int i = 0; i < gamestate->movecount; i++) { if (gamestate->moves[i].capture || piece_type(gamestate->moves[i].piece) == PAWN) { @@ -123,8 +122,7 @@ } static size_t fen_movenr(char *str, const GameState *gamestate) { - unsigned mc = gamestate->movecount + gamestate->move_start; - return sprintf(str, "%u", 1 + mc / 2); + return sprintf(str, "%u", move_number(gamestate)); } static size_t fen_space(char *str) { diff -r 1e9751f8eb0d -r 24b866230dd4 src/chess/pgn.c --- a/src/chess/pgn.c Fri Sep 04 13:49:48 2026 +0200 +++ b/src/chess/pgn.c Sat Sep 05 12:36:04 2026 +0200 @@ -379,6 +379,7 @@ } if (i % 2 == 0) { + // TODO: add move_start offset resp += snprintf(resp, 16, "%d.\x1f%s", 1+i/2, gamestate->moves[i].string); } else { diff -r 1e9751f8eb0d -r 24b866230dd4 src/chess/rules.c --- a/src/chess/rules.c Fri Sep 04 13:49:48 2026 +0200 +++ b/src/chess/rules.c Sat Sep 05 12:36:04 2026 +0200 @@ -45,7 +45,6 @@ void gamestate_init(GameState *gamestate) { memset(gamestate, 0, sizeof(GameState)); - // TODO: implement feature - game can be started from arbitrary position Board initboard = { {WROOK, WKNIGHT, WBISHOP, WQUEEN, WKING, WBISHOP, WKNIGHT, WROOK}, {WPAWN, WPAWN, WPAWN, WPAWN, WPAWN, WPAWN, WPAWN, WPAWN}, @@ -99,13 +98,9 @@ simulation.fen_start = strdup(gamestate->fen_start); } - return simulation; -} + // TODO: think about setting move_start to a reasonable value -Color current_color(const GameState *gamestate) { - // TODO: better would be an API that returns the correct number - unsigned mc = gamestate->movecount + gamestate->move_start; - return (mc % 2 == 0) ? WHITE : BLACK; + return simulation; } static void calc_movetime(GameState *gamestate, Move *move) { @@ -150,7 +145,7 @@ } static bool check_stalemate(const GameState *gamestate) { - Color next_player = gamestate->movecount % 2 == 0 ? WHITE : BLACK; + Color next_player = current_color(gamestate); /* scan the board for pieces of the next player's color */ Move moves[QUEEN_MOVES_MAX]; @@ -436,6 +431,7 @@ gamestate_init(replay); memcpy(&replay->info, &gamestate->info, sizeof(GameInfo)); replay->review = true; + // TODO: respect move_start offset if (move_number > gamestate->movecount) { move_number = gamestate->movecount; } @@ -1262,6 +1258,7 @@ } uint16_t remaining_movetime(const GameState *gamestate, Color color) { + // TODO: we probably need to add the move_start offset here... unsigned move_number = gamestate->movecount; if (color == BLACK) { move_number |= 1; @@ -1272,6 +1269,7 @@ } uint16_t remaining_movetime2(const GameState *gamestate, unsigned move_number) { + // TODO: ... and remove / consider the move_start offset here if (!gamestate->info.timecontrol) { return 0; } diff -r 1e9751f8eb0d -r 24b866230dd4 src/chess/rules.h --- a/src/chess/rules.h Fri Sep 04 13:49:48 2026 +0200 +++ b/src/chess/rules.h Sat Sep 05 12:36:04 2026 +0200 @@ -166,7 +166,7 @@ /** 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 + unsigned int move_start; /** 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 */ @@ -210,6 +210,28 @@ static inline char filechr(File file) {return (char)file+'a';} +/** + * Returns the current move number. + * + * @param gamestate the current game state + * @return the full move number of the move that is next to play + */ +static inline unsigned move_number(const GameState *gamestate) { + return 1 + (gamestate->move_start + gamestate->movecount) / 2; +} + +/** + * Returns the current half-move number. + * + * The counter starts with one. + * + * @param gamestate the current game state + * @return the half-move number of the move that is next to play + */ +static inline unsigned move_number_half(const GameState *gamestate) { + return 1 + gamestate->move_start + gamestate->movecount; +} + static inline void enpassant_threat_add(GameState *gamestate, File file, Rank rank) { gamestate->board[rank][file] |= ENPASSANT_THREAT; @@ -241,7 +263,7 @@ } static inline Color field_color(File f, Rank r) { - return (r + f) % 2 == 0 ? BLACK : WHITE; + return ((unsigned)r + (unsigned)f) % 2 == 0 ? BLACK : WHITE; } /** @@ -294,7 +316,9 @@ * @param gamestate the current game state * @return the color of the player who is next to move */ -Color current_color(const GameState *gamestate); +static inline Color current_color(const GameState *gamestate) { + return move_number_half(gamestate) % 2 ? WHITE : BLACK; +} /** * Returns the piece at the specified position. diff -r 1e9751f8eb0d -r 24b866230dd4 src/main.c --- a/src/main.c Fri Sep 04 13:49:48 2026 +0200 +++ b/src/main.c Sat Sep 05 12:36:04 2026 +0200 @@ -353,7 +353,7 @@ bool iswhite = mi % 2 == 0; if (iswhite) { logi++; - printw("%d. ", logi); + printw("%d. ", logi); // TODO: add move_start offset } addstr(gamestate->moves[mi].string); @@ -919,7 +919,7 @@ } else if (gamestate->nomaterial) { addstr("The game is drawn due to insufficient material.\n"); } else if (gamestate->checkmate) { - addstr(gamestate->movecount % 2 == 0 ? "White" : "Black"); + addstr(current_color(gamestate) == WHITE ? "White" : "Black"); addstr(" was checkmated.\n"); } else if (gamestate->ragequit) { addstr("Your opponent disconnected.\n"); @@ -1036,6 +1036,7 @@ addch('\n'); for (unsigned i = 0 ; i < gamestate->movecount ; i++) { if (i % 2 == 0) { + // TODO: add move_start offset printw("%d. %s", 1+i/2, gamestate->moves[i].string); } else { printw("%s", gamestate->moves[i].string); diff -r 1e9751f8eb0d -r 24b866230dd4 test/test-fen.c --- a/test/test-fen.c Fri Sep 04 13:49:48 2026 +0200 +++ b/test/test-fen.c Sat Sep 05 12:36:04 2026 +0200 @@ -52,6 +52,8 @@ CX_TEST_ASSERT(gs.fifty_cntr_start == 0); /* we start after 53 half-moves have been played */ CX_TEST_ASSERT(gs.move_start == 53); + /* it's black's turn */ + CX_TEST_ASSERT(current_color(&gs) == BLACK); /* we have no move history */ CX_TEST_ASSERT(gs.movecount == 0); CX_TEST_ASSERT(gs.movecapacity == 0); @@ -94,12 +96,78 @@ gamestate_cleanup(&gs); } +static CX_TEST(test_fen_with_castling_rights) { + /* a test that simply checks if an arbitrary FEN is parsed correctly */ + const char * fen = "1rbqk1nr/ppp2ppp/2np4/2b1p3/2P5/2N1P1P1/PP1P1PBP/R1BQK1NR w KQk - 1 6"; + Board expected_board = { + {WROOK, 0, WBISHOP, WQUEEN, WKING, 0, WKNIGHT, WROOK}, + {WPAWN, WPAWN, 0, WPAWN, 0, WPAWN, WBISHOP, WPAWN}, + {0, 0, WKNIGHT, 0, WPAWN, 0, WPAWN, 0}, + {0, 0, WPAWN, 0, 0, 0, 0, 0}, + {0, 0, BBISHOP, 0, BPAWN, 0, 0, 0}, + {0, 0, BKNIGHT, BPAWN, 0, 0, 0, 0}, + {BPAWN, BPAWN, BPAWN, 0, 0, BPAWN, BPAWN, BPAWN}, + {0, BROOK, BBISHOP, BQUEEN, BKING, 0, BKNIGHT, BROOK} + }; + 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 */ + CX_TEST_ASSERT(gs.fifty_cntr_start == 1); + /* we start after 10 half-moves have been played */ + CX_TEST_ASSERT(gs.move_start == 10); + /* it's white's turn */ + CX_TEST_ASSERT(current_color(&gs) == WHITE); + /* 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); + /* black queen-side castling right revoked */ + CX_TEST_ASSERT(gs.castling.K == false); + CX_TEST_ASSERT(gs.castling.Q == false); + CX_TEST_ASSERT(gs.castling.k == false); + 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); + cx_test_register(suite, test_fen_with_castling_rights); // 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;