identify all and fix some occurrences where the new move_start offset matters

Sat, 05 Sep 2026 12:36:04 +0200

author
Mike Becker <universe@uap-core.de>
date
Sat, 05 Sep 2026 12:36:04 +0200
changeset 219
24b866230dd4
parent 218
1e9751f8eb0d
child 220
04da225a5677

identify all and fix some occurrences where the new move_start offset matters

relates to #939

src/chess/fen.c file | annotate | diff | comparison | revisions
src/chess/pgn.c file | annotate | diff | comparison | revisions
src/chess/rules.c file | annotate | diff | comparison | revisions
src/chess/rules.h file | annotate | diff | comparison | revisions
src/main.c file | annotate | diff | comparison | revisions
test/test-fen.c file | annotate | diff | comparison | revisions
--- 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) {
--- 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 {
--- 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;
     }
--- 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.
--- 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);
--- 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;

mercurial