implements stalemate detection + fixes missing const qualifier in new *_moves[_allowed] API default tip

Thu, 06 Aug 2026 16:11:37 +0200

author
Mike Becker <universe@uap-core.de>
date
Thu, 06 Aug 2026 16:11:37 +0200
changeset 170
bde99d803caf
parent 169
9962f5d98764

implements stalemate detection + fixes missing const qualifier in new *_moves[_allowed] API

resolves #947
relates to #952

Makefile file | annotate | diff | comparison | revisions
src/chess/bishop.c file | annotate | diff | comparison | revisions
src/chess/bishop.h file | annotate | diff | comparison | revisions
src/chess/king.c file | annotate | diff | comparison | revisions
src/chess/king.h file | annotate | diff | comparison | revisions
src/chess/knight.c file | annotate | diff | comparison | revisions
src/chess/knight.h file | annotate | diff | comparison | revisions
src/chess/pawn.c file | annotate | diff | comparison | revisions
src/chess/pawn.h file | annotate | diff | comparison | revisions
src/chess/queen.c file | annotate | diff | comparison | revisions
src/chess/queen.h file | annotate | diff | comparison | revisions
src/chess/rook.c file | annotate | diff | comparison | revisions
src/chess/rook.h 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-real-pgn.c file | annotate | diff | comparison | revisions
--- a/Makefile	Thu Aug 06 14:45:55 2026 +0200
+++ b/Makefile	Thu Aug 06 16:11:37 2026 +0200
@@ -36,7 +36,7 @@
 tests: $(BUILDDIR) $(BUILDDIR)/libchess$(LIB_EXT) FORCE
 	cd test && $(MAKE)
 
-check: tests FORCE
+check: all FORCE
 	$(BUILDDIR)/run-tests
 
 $(BUILDDIR):
--- a/src/chess/bishop.c	Thu Aug 06 14:45:55 2026 +0200
+++ b/src/chess/bishop.c	Thu Aug 06 16:11:37 2026 +0200
@@ -53,7 +53,8 @@
     return false;
 }
 
-size_t bishop_moves(GameState *gamestate, Color c, Row r, File f, Move *moves) {
+size_t bishop_moves(const GameState *gamestate,
+        Color c, Row r, File f, Move *moves) {
 
     size_t count = 0;
     const int directions[4][2] = {
--- a/src/chess/bishop.h	Thu Aug 06 14:45:55 2026 +0200
+++ b/src/chess/bishop.h	Thu Aug 06 16:11:37 2026 +0200
@@ -40,9 +40,10 @@
 bool bishop_isblocked(const GameState *gamestate, const Move *move);
 
 #define BISHOP_MOVES_MAX 13
-size_t bishop_moves(GameState *gamestate, Color c, Row r, File f, Move *moves);
+size_t bishop_moves(const GameState *gamestate,
+        Color c, Row r, File f, Move *moves);
 #define bishop_moves_allowed(gamestate, color, row, file, moves) \
-    filter_moves_allowed(gamestate, color, row, file, moves, bishop_moves)
+        filter_moves_allowed(gamestate, color, row, file, moves, bishop_moves)
 
 #ifdef	__cplusplus
 }
--- a/src/chess/king.c	Thu Aug 06 14:45:55 2026 +0200
+++ b/src/chess/king.c	Thu Aug 06 16:11:37 2026 +0200
@@ -88,7 +88,8 @@
     return blocked;
 }
 
-size_t king_moves(GameState *gamestate, Color c, Row r, File f, Move *moves) {
+size_t king_moves(const GameState *gamestate,
+        Color c, Row r, File f, Move *moves) {
 
     size_t count = 0;
     Piece king = mkpiece(KING, c);
--- a/src/chess/king.h	Thu Aug 06 14:45:55 2026 +0200
+++ b/src/chess/king.h	Thu Aug 06 16:11:37 2026 +0200
@@ -41,9 +41,10 @@
 bool king_isblocked(const GameState *gamestate, const Move *move);
 
 #define KING_MOVES_MAX 8
-size_t king_moves(GameState *gamestate, Color c, Row r, File f, Move *moves);
+size_t king_moves(const GameState *gamestate,
+        Color c, Row r, File f, Move *moves);
 #define king_moves_allowed(gamestate, color, row, file, moves) \
-    filter_moves_allowed(gamestate, color, row, file, moves, king_moves)
+        filter_moves_allowed(gamestate, color, row, file, moves, king_moves)
 
 #ifdef	__cplusplus
 }
--- a/src/chess/knight.c	Thu Aug 06 14:45:55 2026 +0200
+++ b/src/chess/knight.c	Thu Aug 06 16:11:37 2026 +0200
@@ -38,7 +38,8 @@
     return (dx == 2 && dy == 1) || (dx == 1 && dy == 2);
 }
 
-size_t knight_moves(GameState *gamestate, Color c, Row r, File f, Move *moves) {
+size_t knight_moves(const GameState *gamestate,
+        Color c, Row r, File f, Move *moves) {
 
     size_t count = 0;
     const int offsets[8][2] = {
--- a/src/chess/knight.h	Thu Aug 06 14:45:55 2026 +0200
+++ b/src/chess/knight.h	Thu Aug 06 16:11:37 2026 +0200
@@ -40,9 +40,10 @@
 #define knight_isblocked(gs,m) false
 
 #define KNIGHT_MOVES_MAX 8
-size_t knight_moves(GameState *gamestate, Color c, Row r, File f, Move *moves);
+size_t knight_moves(const GameState *gamestate,
+        Color c, Row r, File f, Move *moves);
 #define knight_moves_allowed(gamestate, color, row, file, moves) \
-    filter_moves_allowed(gamestate, color, row, file, moves, knight_moves)
+        filter_moves_allowed(gamestate, color, row, file, moves, knight_moves)
 
 #ifdef	__cplusplus
 }
--- a/src/chess/pawn.c	Thu Aug 06 14:45:55 2026 +0200
+++ b/src/chess/pawn.c	Thu Aug 06 16:11:37 2026 +0200
@@ -78,7 +78,8 @@
     }
 }
 
-size_t pawn_moves(GameState *gamestate, Color c, Row r, File f, Move *moves) {
+size_t pawn_moves(const GameState *gamestate,
+        Color c, Row r, File f, Move *moves) {
     size_t count = 0;
     int rowdelta = c == WHITE ? 1 : -1;
     int promotionrow = c == WHITE ? 7 : 0;
--- a/src/chess/pawn.h	Thu Aug 06 14:45:55 2026 +0200
+++ b/src/chess/pawn.h	Thu Aug 06 16:11:37 2026 +0200
@@ -40,9 +40,10 @@
 bool pawn_isblocked(const GameState *gamestate, const Move *move);
 
 #define PAWN_MOVES_MAX 4
-size_t pawn_moves(GameState *gamestate, Color c, Row r, File f, Move *moves);
+size_t pawn_moves(const GameState *gamestate,
+        Color c, Row r, File f, Move *moves);
 #define pawn_moves_allowed(gamestate, color, row, file, moves) \
-    filter_moves_allowed(gamestate, color, row, file, moves, pawn_moves)
+        filter_moves_allowed(gamestate, color, row, file, moves, pawn_moves)
 
 #ifdef	__cplusplus
 }
--- a/src/chess/queen.c	Thu Aug 06 14:45:55 2026 +0200
+++ b/src/chess/queen.c	Thu Aug 06 16:11:37 2026 +0200
@@ -44,7 +44,9 @@
     }
 }
 
-size_t queen_moves(GameState *gamestate, Color c, Row r, File f, Move *moves) {
+size_t queen_moves(const GameState *gamestate,
+        Color c, Row r, File f, Move *moves) {
+
     size_t count = 0;
     const int directions[8][2] = {
         { 1,  0},
--- a/src/chess/queen.h	Thu Aug 06 14:45:55 2026 +0200
+++ b/src/chess/queen.h	Thu Aug 06 16:11:37 2026 +0200
@@ -40,9 +40,10 @@
 bool queen_isblocked(const GameState *gamestate, const Move *move);
 
 #define QUEEN_MOVES_MAX 27
-size_t queen_moves(GameState *gamestate, Color c, Row r, File f, Move *moves);
+size_t queen_moves(const GameState *gamestate,
+        Color c, Row r, File f, Move *moves);
 #define queen_moves_allowed(gamestate, color, row, file, moves) \
-    filter_moves_allowed(gamestate, color, row, file, moves, queen_moves)
+        filter_moves_allowed(gamestate, color, row, file, moves, queen_moves)
 
 #ifdef	__cplusplus
 }
--- a/src/chess/rook.c	Thu Aug 06 14:45:55 2026 +0200
+++ b/src/chess/rook.c	Thu Aug 06 16:11:37 2026 +0200
@@ -59,7 +59,8 @@
     return false;
 }
 
-size_t rook_moves(GameState *gamestate, Color c, Row r, File f, Move *moves) {
+size_t rook_moves(const GameState *gamestate,
+        Color c, Row r, File f, Move *moves) {
     size_t count = 0;
     const int directions[4][2] = {
         { 1,  0},
--- a/src/chess/rook.h	Thu Aug 06 14:45:55 2026 +0200
+++ b/src/chess/rook.h	Thu Aug 06 16:11:37 2026 +0200
@@ -40,9 +40,10 @@
 bool rook_isblocked(const GameState *gamestate, const Move *move);
 
 #define ROOK_MOVES_MAX 14
-size_t rook_moves(GameState *gamestate, Color c, Row r, File f, Move *moves);
+size_t rook_moves(const GameState *gamestate,
+        Color c, Row r, File f, Move *moves);
 #define rook_moves_allowed(gamestate, color, row, file, moves) \
-    filter_moves_allowed(gamestate, color, row, file, moves, rook_moves)
+        filter_moves_allowed(gamestate, color, row, file, moves, rook_moves)
 
 #ifdef	__cplusplus
 }
--- a/src/chess/rules.c	Thu Aug 06 14:45:55 2026 +0200
+++ b/src/chess/rules.c	Thu Aug 06 16:11:37 2026 +0200
@@ -225,6 +225,45 @@
     }
 }
 
+size_t piece_moves_allowed(const GameState *gamestate,
+        Row r, File f, Move *moves) {
+    Piece p = piece_at(gamestate, r, f);
+    Color c = piece_color(p);
+    switch (piece_type(p)) {
+        case KING:
+            return king_moves_allowed(gamestate, c, r, f, moves);
+        case QUEEN:
+            return queen_moves_allowed(gamestate, c, r, f, moves);
+        case ROOK:
+            return rook_moves_allowed(gamestate, c, r, f, moves);
+        case KNIGHT:
+            return knight_moves_allowed(gamestate, c, r, f, moves);
+        case BISHOP:
+            return bishop_moves_allowed(gamestate, c, r, f, moves);
+        case PAWN:
+            return pawn_moves_allowed(gamestate, c, r, f, moves);
+        default:
+            return 0;
+    }
+}
+
+static bool is_stalemate(const GameState *gamestate) {
+    Color next_player = gamestate->movecount % 2 == 0 ? WHITE : BLACK;
+
+    /* scan the board for pieces of the next player's color */
+    Move moves[QUEEN_MOVES_MAX];
+    for (Row r = 0; r < 8; r++) {
+        for (File f = 0; f < 8; f++) {
+            if (piece_color(piece_at(gamestate, r, f)) == next_player
+                && piece_moves_allowed(gamestate, r, f, moves) > 0) {
+                return false;
+            }
+        }
+    }
+
+    return true;
+}
+
 char getpiecechr(Piece piece) {
     switch (piece_type(piece)) {
     case ROOK: return 'R';
@@ -271,7 +310,7 @@
     }
 }
 
-void apply_move(GameState *gamestate, Move *move) {
+static void apply_move_internal(GameState *gamestate, Move *move) {
     /* en passant capture */
     if (move->capture && piece_type(move->piece) == PAWN &&
             piece_at(gamestate, mdst(move)) == 0) {
@@ -341,6 +380,13 @@
     gamestate->checkmate = move->checkmate;
 }
 
+void apply_move(GameState *gamestate, Move *move) {
+    apply_move_internal(gamestate, move);
+    if (!gamestate->checkmate) {
+        gamestate->stalemate = is_stalemate(gamestate);
+    }
+}
+
 void gamestate_at_move(const GameState *gamestate,
         unsigned move_number, GameState *replay) {
     gamestate_init(replay);
@@ -361,7 +407,7 @@
     /* simulate the move */
     GameState simulation = gamestate_copy_sim(gamestate);
     Move simmove = *move;
-    apply_move(&simulation, &simmove);
+    apply_move_internal(&simulation, &simmove);
 
     /* find the opposing king */
     Color piececolor = piece_color(move->piece);
@@ -417,7 +463,7 @@
                 move_retaliate.torow = er;
                 move_retaliate.tofile = ef;
                 move_retaliate.capture = true;
-                apply_move(&sim_retaliate, &move_retaliate);
+                apply_move_internal(&sim_retaliate, &move_retaliate);
                 canescape = !is_covered(&sim_retaliate, er, ef, piececolor);
                 gamestate_cleanup(&sim_retaliate);
                 continue;
@@ -560,7 +606,7 @@
     /* test if the move would expose our own king */
     GameState simulation = gamestate_copy_sim(gamestate);
     Move simmove = *move;
-    apply_move(&simulation, &simmove);
+    apply_move_internal(&simulation, &simmove);
     Color piececolor = piece_color(move->piece);
     Color oppcolor = opponent_color(piececolor);
     File kingfile = 0;
@@ -587,6 +633,10 @@
     }
     gamestate_cleanup(&simulation);
 
+    if (result != VALID_MOVE_SEMANTICS) {
+        return result;
+    }
+
     /* validate check and checkmate flags */
     int cocm = determine_check_or_checkmate(gamestate, move);
     if (cocm == 2) {
@@ -722,7 +772,7 @@
 
     GameState simulation = gamestate_copy_sim(gamestate);
     Move simmove = *move;
-    apply_move(&simulation, &simmove);
+    apply_move_internal(&simulation, &simmove);
     
     File kingfile = 0;
     Row kingrow = 0;
@@ -1136,7 +1186,7 @@
     return false;
 }
 
-size_t filter_moves_allowed(GameState *gamestate,
+size_t filter_moves_allowed(const GameState *gamestate,
         Color c, Row r, File f, Move *moves, moves_generator_func func) {
 
     /* worst case: the queen has the most moves */
--- a/src/chess/rules.h	Thu Aug 06 14:45:55 2026 +0200
+++ b/src/chess/rules.h	Thu Aug 06 16:11:37 2026 +0200
@@ -287,10 +287,24 @@
     piece_set(gamestate, row, file, 0);
 }
 
-typedef size_t(*moves_generator_func)(GameState *gamestate,
+typedef size_t(*moves_generator_func)(const GameState *gamestate,
         Color c, Row r, File f, Move *moves);
 
 /**
+ * Calculates all allowed moves for a specific piece.
+ *
+ * Use the macros for the specific pieces instead.
+ *
+ * @param gamestate the current gamestate
+ * @param r the row of the piece
+ * @param f the file of the piece
+ * @param moves target array for the list of moves
+ * @return the number of moves stored in the @p moves array
+ */
+size_t piece_moves_allowed(const GameState *gamestate,
+        Row r, File f, Move *moves);
+
+/**
  * Internal function used to filter out illegal moves.
  *
  * Use the macros for the specific pieces instead.
@@ -303,7 +317,7 @@
  * @param func a function that unconditionally generates the moves
  * @return the number of moves stored in the @p moves array
  */
-size_t filter_moves_allowed(GameState *gamestate,
+size_t filter_moves_allowed(const GameState *gamestate,
     Color c, Row r, File f, Move *moves, moves_generator_func func);
 
 /**
--- a/test/test-real-pgn.c	Thu Aug 06 14:45:55 2026 +0200
+++ b/test/test-real-pgn.c	Thu Aug 06 16:11:37 2026 +0200
@@ -56,7 +56,7 @@
         GameState *gamestate, int game) {
     // TODO: fix that we need an initialized game state for parsing PGNs
     gamestate_init(gamestate);
-    CX_TEST_ASSERT(0 == parse_pgn(pgn_data[game], gamestate));
+    CX_TEST_ASSERT(0 == parse_pgn(pgn_data[game-1], gamestate));
     CX_TEST_ASSERT(gamestate->stalemate);
     CX_TEST_ASSERT(!gamestate->wresign);
     CX_TEST_ASSERT(!gamestate->bresign);

mercurial