implement that fen_parse() sets the game-end flags correctly default tip

Sat, 05 Sep 2026 13:03:09 +0200

author
Mike Becker <universe@uap-core.de>
date
Sat, 05 Sep 2026 13:03:09 +0200
changeset 220
04da225a5677
parent 219
24b866230dd4

implement that fen_parse() sets the game-end flags correctly

relates to #939

src/chess/fen.c file | annotate | diff | comparison | revisions
src/chess/rules.c file | annotate | diff | comparison | revisions
src/chess/rules.h file | annotate | diff | comparison | revisions
test/test-fen.c file | annotate | diff | comparison | revisions
--- a/src/chess/fen.c	Sat Sep 05 12:36:04 2026 +0200
+++ b/src/chess/fen.c	Sat Sep 05 13:03:09 2026 +0200
@@ -292,5 +292,17 @@
 
     /* only copy the fen string if everything is a success */
     gamestate->fen_start = strdup(fen_start);
+
+    /* check if the FEN describes an already ended game */
+    if (check_checkmate(gamestate)) {
+        gamestate->checkmate = true;
+    } else if (check_no_material(gamestate)) {
+        gamestate->nomaterial = true;
+    } else if (check_threefold_repetition(gamestate)) {
+        gamestate->threefold = true;
+    } else if (check_stalemate(gamestate)) {
+        gamestate->stalemate = true;
+    }
+
     return 0;
 }
\ No newline at end of file
--- a/src/chess/rules.c	Sat Sep 05 12:36:04 2026 +0200
+++ b/src/chess/rules.c	Sat Sep 05 13:03:09 2026 +0200
@@ -144,7 +144,242 @@
     }
 }
 
-static bool check_stalemate(const GameState *gamestate) {
+
+/* applies the move without recalculating gamestate flags */
+static void apply_move_internal(GameState *gamestate, Move *move) {
+    /* en passant capture */
+    if (move->capture && piece_type(move->piece) == PAWN &&
+            piece_at(gamestate, move->tofile, move->torank) == 0) {
+        piece_remove(gamestate, move->tofile, move->fromrank);
+    }
+
+    /* remove old en passant threats */
+    for (File file = 0 ; file < 8 ; file++) {
+        enpassant_threat_remove(gamestate, file, 3);
+        enpassant_threat_remove(gamestate, file, 4);
+    }
+
+    /* move (and maybe capture or promote) */
+    piece_remove(gamestate, move->fromfile, move->fromrank);
+    if (move->promotion) {
+        piece_set(gamestate, move->tofile, move->torank, move->promotion);
+    } else {
+        piece_set(gamestate, move->tofile, move->torank, move->piece);
+    }
+
+    /* add new en passant threat */
+    if (piece_type(move->piece) == PAWN && (
+            (move->fromrank == 1 && move->torank == 3) ||
+            (move->fromrank == 6 && move->torank == 4))) {
+        enpassant_threat_add(gamestate, move->tofile, move->torank);
+    }
+
+    /* castling */
+    bool castling_happend = false;
+    if (piece_type(move->piece) == KING && move->fromfile == fileidx('e')) {
+        const Color color = piece_color(move->piece);
+        if (move->tofile == fileidx('g')) {
+            gamestate->board[move->torank][fileidx('h')] = 0;
+            gamestate->board[move->torank][fileidx('f')] = mkpiece(ROOK, color);
+            castling_happend = true;
+        } else if (move->tofile == fileidx('c')) {
+            gamestate->board[move->torank][fileidx('a')] = 0;
+            gamestate->board[move->torank][fileidx('d')] = mkpiece(ROOK, color);
+            castling_happend = true;
+        }
+    }
+    if (castling_happend) {
+        if (piece_color(move->piece) == WHITE) {
+            gamestate->castling.K = true;
+            gamestate->castling.Q = true;
+        } else {
+            gamestate->castling.k = true;
+            gamestate->castling.q = true;
+        }
+    } else {
+        if (piece_color(move->piece) == WHITE) {
+            if (move->fromrank == rankidx('1')) {
+                if (move->fromfile == fileidx('e')) {
+                    gamestate->castling.K = gamestate->castling.Q = true;
+                } else if (move->fromfile == fileidx('h')) {
+                    gamestate->castling.K = true;
+                } else if (move->fromfile == fileidx('a')) {
+                    gamestate->castling.Q = true;
+                }
+            }
+        } else {
+            if (move->fromrank == rankidx('8')) {
+                if (move->fromfile == fileidx('e')) {
+                    gamestate->castling.k = gamestate->castling.q = true;
+                } else if (move->fromfile == fileidx('h')) {
+                    gamestate->castling.k = true;
+                } else if (move->fromfile == fileidx('a')) {
+                    gamestate->castling.q = true;
+                }
+            }
+        }
+    }
+
+    /* add move to the moves array and the new position to the FEN array */
+    if (gamestate->movecount == gamestate->movecapacity) {
+        gamestate->movecapacity += 64; /* 32 more full moves */
+        gamestate->moves = realloc(gamestate->moves,
+            gamestate->movecapacity * sizeof(Move));
+        gamestate->fen = realloc(gamestate->fen,
+            gamestate->movecapacity * sizeof(char*));
+    }
+
+    /* copy the move data into the game's move array */
+    Move *melem = &gamestate->moves[gamestate->movecount];
+    *melem = *move;
+    calc_movetime(gamestate, melem);
+
+    /* important: only "add" the move after calculating the time! */
+    gamestate->movecount++;
+
+    /* calculate the FEN of the new position and store it in the FEN array */
+    char fen[FEN_MAX_LENGTH];
+    fen_compute(fen, gamestate);
+    gamestate->fen[gamestate->movecount - 1] = strdup(fen);
+}
+
+/* return 0 = no check, 1 = check, 2 = checkmate */
+static int determine_check_or_checkmate(
+        const GameState *gamestate, const Move *move) {
+
+    /* either simulate one more move or check for current state */
+    Color piececolor, oppcolor;
+    GameState simulation = gamestate_copy_sim(gamestate);
+    if (move != NULL) {
+        piececolor = piece_color(move->piece);
+        oppcolor = opponent_color(piececolor);
+        Move simmove = *move;
+        apply_move_internal(&simulation, &simmove);
+    } else {
+        oppcolor = current_color(gamestate);
+        piececolor = opponent_color(oppcolor);
+    }
+
+    /* find the opposing king */
+    File opkingfile = 0;
+    Rank opkingrank = 0;
+    for (Rank rank = 0 ; rank < 8 ; rank++) {
+        for (File file = 0 ; file < 8 ; file++) {
+            Piece p = piece_at(&simulation, file, rank);
+            if (p == mkpiece(KING, oppcolor)) {
+                opkingfile = file;
+                opkingrank = rank;
+            }
+        }
+    }
+
+    /* determine if the opposing king is now threatened */
+    Move threats[16];
+    size_t threatcount;
+    bool incheck = get_threats(&simulation, opkingfile, opkingrank,
+        piececolor, threats, &threatcount);
+
+    if (!incheck) {
+        gamestate_cleanup(&simulation);
+        return 0;
+    }
+
+    /* determine possible escape fields */
+    bool canescape = false;
+    for (int dr = -1 ; dr <= 1 && !canescape ; dr++) {
+        for (int df = -1 ; df <= 1 && !canescape ; df++) {
+            if (dr == 0 && df == 0) continue;
+            Rank er = opkingrank + dr;
+            File ef = opkingfile + df;
+            if (!isidx(er) || !isidx(ef)) continue;
+
+            /* check if piece of the king's color blocks the field */
+            if (piece_color(simulation.board[er][ef]) == oppcolor)
+                continue;
+
+            /* check if escape field is already covered (threatened) */
+            if (is_covered(&simulation, ef, er, piececolor))
+                continue;
+
+            /* check if an attacking piece blocks the field */
+            if (piece_color(simulation.board[er][ef]) == piececolor) {
+                /* test if the king can fight back */
+                GameState sim_retaliate = gamestate_copy_sim(&simulation);
+                Move move_retaliate = {0};
+                move_retaliate.piece = mkpiece(KING, oppcolor);
+                move_retaliate.fromrank = opkingrank;
+                move_retaliate.fromfile = opkingfile;
+                move_retaliate.torank = er;
+                move_retaliate.tofile = ef;
+                move_retaliate.capture = true;
+                apply_move_internal(&sim_retaliate, &move_retaliate);
+                canescape = !is_covered(&sim_retaliate, ef, er, piececolor);
+                gamestate_cleanup(&sim_retaliate);
+                continue;
+            }
+
+            /* the field is not covered and unoccupied */
+            canescape = true;
+        }
+    }
+
+    /* can't escape, can the king be rescued? */
+    if (!canescape && threatcount == 1) {
+        canescape = is_protected(&simulation,
+            threats[0].fromfile, threats[0].fromrank, oppcolor);
+    }
+
+    /* can't capture, can he block? */
+    if (!canescape && threatcount == 1) {
+        Move *threat = &(threats[0]);
+        unsigned tptype = piece_type(threat->piece);
+
+        /* knight, pawns and the king cannot be blocked */
+        if (tptype == BISHOP || tptype == ROOK || tptype == QUEEN) {
+            if (threat->fromrank == threat->torank) {
+                /* rook aspect (on rank) */
+                int d = threat->tofile > threat->fromfile ? 1 : -1;
+                File file = threat->fromfile;
+                while (!canescape && file != threat->tofile - d) {
+                    file += d;
+                    canescape |= is_protected(&simulation,
+                        file, threat->torank, oppcolor);
+                }
+            } else if (threat->fromfile == threat->tofile) {
+                /* rook aspect (on file) */
+                int d = threat->torank > threat->fromrank ? 1 : -1;
+                Rank rank = threat->fromrank;
+                while (!canescape && rank != threat->torank - d) {
+                    rank += d;
+                    canescape |= is_protected(&simulation,
+                        threat->tofile, rank, oppcolor);
+                }
+            } else {
+                /* bishop aspect */
+                int dr = threat->torank > threat->fromrank ? 1 : -1;
+                int df = threat->tofile > threat->fromfile ? 1 : -1;
+
+                Rank rank = threat->fromrank;
+                File file = threat->fromfile;
+                while (!canescape && file != threat->tofile - df
+                        && rank != threat->torank - dr) {
+                    rank += dr;
+                    file += df;
+                    canescape |= is_protected(&simulation,
+                        file, rank, oppcolor);
+                }
+            }
+        }
+    }
+    gamestate_cleanup(&simulation);
+    return canescape ? 1 : 2;
+}
+
+bool check_checkmate(const GameState *gamestate) {
+    return determine_check_or_checkmate(gamestate, NULL) == 2;
+}
+
+bool check_stalemate(const GameState *gamestate) {
     Color next_player = current_color(gamestate);
 
     /* scan the board for pieces of the next player's color */
@@ -313,104 +548,6 @@
     }
 }
 
-/* applies the move without recalculating gamestate flags */
-static void apply_move_internal(GameState *gamestate, Move *move) {
-    /* en passant capture */
-    if (move->capture && piece_type(move->piece) == PAWN &&
-            piece_at(gamestate, move->tofile, move->torank) == 0) {
-        piece_remove(gamestate, move->tofile, move->fromrank);
-    }
-    
-    /* remove old en passant threats */
-    for (File file = 0 ; file < 8 ; file++) {
-        enpassant_threat_remove(gamestate, file, 3);
-        enpassant_threat_remove(gamestate, file, 4);
-    }
-    
-    /* move (and maybe capture or promote) */
-    piece_remove(gamestate, move->fromfile, move->fromrank);
-    if (move->promotion) {
-        piece_set(gamestate, move->tofile, move->torank, move->promotion);
-    } else {
-        piece_set(gamestate, move->tofile, move->torank, move->piece);
-    }
-
-    /* add new en passant threat */
-    if (piece_type(move->piece) == PAWN && (
-            (move->fromrank == 1 && move->torank == 3) ||
-            (move->fromrank == 6 && move->torank == 4))) {
-        enpassant_threat_add(gamestate, move->tofile, move->torank);
-    }
-    
-    /* castling */
-    bool castling_happend = false;
-    if (piece_type(move->piece) == KING && move->fromfile == fileidx('e')) {
-        const Color color = piece_color(move->piece);
-        if (move->tofile == fileidx('g')) {
-            gamestate->board[move->torank][fileidx('h')] = 0;
-            gamestate->board[move->torank][fileidx('f')] = mkpiece(ROOK, color);
-            castling_happend = true;
-        } else if (move->tofile == fileidx('c')) {
-            gamestate->board[move->torank][fileidx('a')] = 0;
-            gamestate->board[move->torank][fileidx('d')] = mkpiece(ROOK, color);
-            castling_happend = true;
-        }
-    }
-    if (castling_happend) {
-        if (piece_color(move->piece) == WHITE) {
-            gamestate->castling.K = true;
-            gamestate->castling.Q = true;
-        } else {
-            gamestate->castling.k = true;
-            gamestate->castling.q = true;
-        }
-    } else {
-        if (piece_color(move->piece) == WHITE) {
-            if (move->fromrank == rankidx('1')) {
-                if (move->fromfile == fileidx('e')) {
-                    gamestate->castling.K = gamestate->castling.Q = true;
-                } else if (move->fromfile == fileidx('h')) {
-                    gamestate->castling.K = true;
-                } else if (move->fromfile == fileidx('a')) {
-                    gamestate->castling.Q = true;
-                }
-            }
-        } else {
-            if (move->fromrank == rankidx('8')) {
-                if (move->fromfile == fileidx('e')) {
-                    gamestate->castling.k = gamestate->castling.q = true;
-                } else if (move->fromfile == fileidx('h')) {
-                    gamestate->castling.k = true;
-                } else if (move->fromfile == fileidx('a')) {
-                    gamestate->castling.q = true;
-                }
-            }
-        }
-    }
-    
-    /* add move to the moves array and the new position to the FEN array */
-    if (gamestate->movecount == gamestate->movecapacity) {
-        gamestate->movecapacity += 64; /* 32 more full moves */
-        gamestate->moves = realloc(gamestate->moves,
-            gamestate->movecapacity * sizeof(Move));
-        gamestate->fen = realloc(gamestate->fen,
-            gamestate->movecapacity * sizeof(char*));
-    }
-
-    /* copy the move data into the game's move array */
-    Move *melem = &gamestate->moves[gamestate->movecount];
-    *melem = *move;
-    calc_movetime(gamestate, melem);
-
-    /* important: only "add" the move after calculating the time! */
-    gamestate->movecount++;
-
-    /* calculate the FEN of the new position and store it in the FEN array */
-    char fen[FEN_MAX_LENGTH];
-    fen_compute(fen, gamestate);
-    gamestate->fen[gamestate->movecount - 1] = strdup(fen);
-}
-
 void apply_move(GameState *gamestate, Move *move) {
     apply_move_internal(gamestate, move);
 
@@ -440,132 +577,6 @@
     }
 }
 
-/* return 0 = no check, 1 = check, 2 = checkmate */
-static int determine_check_or_checkmate(
-        const GameState *gamestate, const Move *move) {
-
-    /* simulate the move */
-    GameState simulation = gamestate_copy_sim(gamestate);
-    Move simmove = *move;
-    apply_move_internal(&simulation, &simmove);
-
-    /* find the opposing king */
-    Color piececolor = piece_color(move->piece);
-    Color oppcolor = opponent_color(piececolor);
-    File opkingfile = 0;
-    Rank opkingrank = 0;
-    for (Rank rank = 0 ; rank < 8 ; rank++) {
-        for (File file = 0 ; file < 8 ; file++) {
-            Piece p = piece_at(&simulation, file, rank);
-            if (p == mkpiece(KING, oppcolor)) {
-                opkingfile = file;
-                opkingrank = rank;
-            }
-        }
-    }
-
-    /* determine if the opposing king is now threatened */
-    Move threats[16];
-    size_t threatcount;
-    bool incheck = get_threats(&simulation, opkingfile, opkingrank,
-        piececolor, threats, &threatcount);
-
-    if (!incheck) {
-        gamestate_cleanup(&simulation);
-        return 0;
-    }
-
-    /* determine possible escape fields */
-    bool canescape = false;
-    for (int dr = -1 ; dr <= 1 && !canescape ; dr++) {
-        for (int df = -1 ; df <= 1 && !canescape ; df++) {
-            if (dr == 0 && df == 0) continue;
-            Rank er = opkingrank + dr;
-            File ef = opkingfile + df;
-            if (!isidx(er) || !isidx(ef)) continue;
-
-            /* check if piece of the king's color blocks the field */
-            if (piece_color(simulation.board[er][ef]) == oppcolor)
-                continue;
-
-            /* check if escape field is already covered (threatened) */
-            if (is_covered(&simulation, ef, er, piececolor))
-                continue;
-
-            /* check if an attacking piece blocks the field */
-            if (piece_color(simulation.board[er][ef]) == piececolor) {
-                /* test if the king can fight back */
-                GameState sim_retaliate = gamestate_copy_sim(&simulation);
-                Move move_retaliate = {0};
-                move_retaliate.piece = mkpiece(KING, oppcolor);
-                move_retaliate.fromrank = opkingrank;
-                move_retaliate.fromfile = opkingfile;
-                move_retaliate.torank = er;
-                move_retaliate.tofile = ef;
-                move_retaliate.capture = true;
-                apply_move_internal(&sim_retaliate, &move_retaliate);
-                canescape = !is_covered(&sim_retaliate, ef, er, piececolor);
-                gamestate_cleanup(&sim_retaliate);
-                continue;
-            }
-
-            /* the field is not covered and unoccupied */
-            canescape = true;
-        }
-    }
-
-    /* can't escape, can the king be rescued? */
-    if (!canescape && threatcount == 1) {
-        canescape = is_protected(&simulation,
-            threats[0].fromfile, threats[0].fromrank, oppcolor);
-    }
-
-    /* can't capture, can he block? */
-    if (!canescape && threatcount == 1) {
-        Move *threat = &(threats[0]);
-        unsigned tptype = piece_type(threat->piece);
-
-        /* knight, pawns and the king cannot be blocked */
-        if (tptype == BISHOP || tptype == ROOK || tptype == QUEEN) {
-            if (threat->fromrank == threat->torank) {
-                /* rook aspect (on rank) */
-                int d = threat->tofile > threat->fromfile ? 1 : -1;
-                File file = threat->fromfile;
-                while (!canescape && file != threat->tofile - d) {
-                    file += d;
-                    canescape |= is_protected(&simulation,
-                        file, threat->torank, oppcolor);
-                }
-            } else if (threat->fromfile == threat->tofile) {
-                /* rook aspect (on file) */
-                int d = threat->torank > threat->fromrank ? 1 : -1;
-                Rank rank = threat->fromrank;
-                while (!canescape && rank != threat->torank - d) {
-                    rank += d;
-                    canescape |= is_protected(&simulation,
-                        threat->tofile, rank, oppcolor);
-                }
-            } else {
-                /* bishop aspect */
-                int dr = threat->torank > threat->fromrank ? 1 : -1;
-                int df = threat->tofile > threat->fromfile ? 1 : -1;
-
-                Rank rank = threat->fromrank;
-                File file = threat->fromfile;
-                while (!canescape && file != threat->tofile - df
-                        && rank != threat->torank - dr) {
-                    rank += dr;
-                    file += df;
-                    canescape |= is_protected(&simulation,
-                        file, rank, oppcolor);
-                }
-            }
-        }
-    }
-    gamestate_cleanup(&simulation);
-    return canescape ? 1 : 2;
-}
-
 
 void format_move(const GameState *gamestate, Move *move) {
     char *string = &(move->string[0]);
--- a/src/chess/rules.h	Sat Sep 05 12:36:04 2026 +0200
+++ b/src/chess/rules.h	Sat Sep 05 13:03:09 2026 +0200
@@ -663,6 +663,30 @@
 int print_clk(uint16_t time, char *str, bool always_hours);
 
 /**
+ * Checks if the current position is a checkmate.
+ *
+ * This does a full recalculation. You should use the checkmate flag
+ * in the game state instead, if you want to test this after applying a
+ * move.
+ *
+ * @param gamestate the current game state
+ * @return true if the game is in a checkmate
+ */
+bool check_checkmate(const GameState *gamestate);
+
+/**
+ * Checks if the current position is a stalemate.
+ *
+ * This does a full recalculation. You should use the stalemate flag
+ * in the game state instead, if you want to test this after applying a
+ * move.
+ *
+ * @param gamestate the current game state
+ * @return true if the game is in a stalemate position
+ */
+bool check_stalemate(const GameState *gamestate);
+
+/**
  * Checks if the current position already appeared two times before.
  *
  * This does not set the threefold flag in the game state as this flag is
@@ -685,6 +709,9 @@
  * - they have a king + bishop and the opponent doesn't have
  *   opposite color bishops or knights or pawns
  *
+ * If you want to test this after applying a move, you should use the
+ * nomaterial flag in the game state, instead.
+ *
  * @param gamestate the current game state
  * @return true if neither side can win due to insufficient material
  */
--- a/test/test-fen.c	Sat Sep 05 12:36:04 2026 +0200
+++ b/test/test-fen.c	Sat Sep 05 13:03:09 2026 +0200
@@ -97,7 +97,7 @@
 }
 
 static CX_TEST(test_fen_with_castling_rights) {
-    /* a test that simply checks if an arbitrary FEN is parsed correctly */
+    /* this test contains castling rights and a non-zero fifty-moves counter */
     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},
@@ -162,12 +162,42 @@
     gamestate_cleanup(&gs);
 }
 
+static CX_TEST(test_fen_checkmate) {
+    /* this test contains a full game where white is checkmated */
+    const char * fen = "5rk1/1pp4p/p1n3p1/4b1N1/8/1P1N4/P1P3P1/qKB4R w - - 2 26";
+    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);
+        CX_TEST_ASSERT(gs.fifty_cntr_start == 2);
+        CX_TEST_ASSERT(gs.move_start == 50);
+        CX_TEST_ASSERT(current_color(&gs) == WHITE);
+        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);
+        /* the checkmate flag must be set */
+        CX_TEST_ASSERT(gs.checkmate);
+        /* 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)
+    cx_test_register(suite, test_fen_checkmate);
+    // TODO: test FENs that describe stalemate, threefold, no-material positions
     // TODO: test FEN with en-passant threat
 
     return suite;

mercurial