make castling rights part of the game state default tip

Thu, 03 Sep 2026 16:18:24 +0200

author
Mike Becker <universe@uap-core.de>
date
Thu, 03 Sep 2026 16:18:24 +0200
changeset 216
d0c3d3016650
parent 215
d20887f0e8b3

make castling rights part of the game state

required for issue #939
because creating a state from FEN
will not contain a move history

bonus: make is_check_position() robust

src/chess/fen.c file | annotate | diff | comparison | revisions
src/chess/king.c file | annotate | diff | comparison | revisions
src/chess/rules.c file | annotate | diff | comparison | revisions
src/chess/rules.h file | annotate | diff | comparison | revisions
--- a/src/chess/fen.c	Thu Sep 03 15:28:54 2026 +0200
+++ b/src/chess/fen.c	Thu Sep 03 16:18:24 2026 +0200
@@ -77,40 +77,12 @@
     return 1;
 }
 
-static bool fen_castling_chkmoved(GameState *gamestate,
-    Rank rank, File file) {
-
-    for (unsigned i = 0 ; i < gamestate->movecount ; i++) {
-        if (gamestate->moves[i].fromfile == file
-            && gamestate->moves[i].fromrank == rank) {
-            return true;
-        }
-    }
-
-    return false;
-}
-
 static size_t fen_castling(char *str, GameState *gamestate) {
-    bool K, Q, k, q;
-
-    if (fen_castling_chkmoved(gamestate, rankidx('1'), fileidx('e'))) {
-        K = Q = false;
-    } else {
-        K = !fen_castling_chkmoved(gamestate, rankidx('1'), fileidx('h'));
-        Q = !fen_castling_chkmoved(gamestate, rankidx('1'), fileidx('a'));
-    }
-    if (fen_castling_chkmoved(gamestate, rankidx('8'), fileidx('e'))) {
-        k = q = false;
-    } else {
-        k = !fen_castling_chkmoved(gamestate, rankidx('8'), fileidx('h'));
-        q = !fen_castling_chkmoved(gamestate, rankidx('8'), fileidx('a'));
-    }
-
     size_t i = 0;
-    if (K) str[i++] = 'K';
-    if (Q) str[i++] = 'Q';
-    if (k) str[i++] = 'k';
-    if (q) str[i++] = 'q';
+    if (!gamestate->castling.K) str[i++] = 'K';
+    if (!gamestate->castling.Q) str[i++] = 'Q';
+    if (!gamestate->castling.k) str[i++] = 'k';
+    if (!gamestate->castling.q) str[i++] = 'q';
     if (!i) str[i++] = '-';
 
     return i;
--- a/src/chess/king.c	Thu Sep 03 15:28:54 2026 +0200
+++ b/src/chess/king.c	Thu Sep 03 16:18:24 2026 +0200
@@ -30,62 +30,57 @@
 #include "rules.h"
 #include "king.h"
 
-static bool king_castling_chkmoved(
-        const GameState *gamestate, File file, Rank rank) {
-
-    for (unsigned i = 0; i < gamestate->movecount; i++) {
-        if (gamestate->moves[i].fromfile == file
-            && gamestate->moves[i].fromrank == rank) {
-            return true;
-        }
-    }
-    
-    return false;
-}
-
 bool king_chkrules(const GameState *gamestate, const Move* move) {
     if (abs(move->torank - move->fromrank) <= 1 &&
         abs(move->tofile - move->fromfile) <= 1) {
         return true;
-    } else {
+    } else if (move->fromrank == move->torank) {
         /* castling */
-        if (move->fromrank == move->torank &&
-            move->fromrank == (piece_color(move->piece) == WHITE ? 0 : 7) &&
-            move->fromfile == fileidx('e') &&
-            (move->tofile == fileidx('c') || move->tofile == fileidx('g'))) {
-            
-            return !king_castling_chkmoved(gamestate,
-                    move->fromfile, move->fromrank) &&
-                !king_castling_chkmoved(gamestate,
-                    move->tofile == fileidx('c') ? 0 : 7, move->fromrank);
+        Rank backrank;
+        bool k_allowed, q_allowed;
+        if (piece_color(move->piece) == WHITE) {
+            backrank = rankidx('1');
+            k_allowed = !gamestate->castling.K;
+            q_allowed = !gamestate->castling.Q;
         } else {
-            return false;
+            backrank = rankidx('8');
+            k_allowed = !gamestate->castling.k;
+            q_allowed = !gamestate->castling.q;
         }
+        bool castle_q = false, castle_k = false;
+        if (move->fromrank == backrank && move->fromfile == fileidx('e')) {
+            castle_q = move->tofile == fileidx('c');
+            castle_k = move->tofile == fileidx('g');
+        }
+        /* note that here we do not consider threats!
+         * that is what king_isblocked() does */
+        return (castle_q && q_allowed) || (castle_k && k_allowed);
+    } else {
+        return false;
     }
 }
 
 bool king_isblocked(const GameState *gamestate, const Move *move) {
-    
-    Color op_color = opponent_color(piece_color(move->piece));
-    
-    /* being in check does not "block" the king, so don't test it here */
-    bool blocked = false;
-    
     /* just test, if castling move is blocked */
     if (abs(move->tofile - move->fromfile) == 2) {
         if (move->tofile == fileidx('c')) {
-            blocked |= gamestate->board[move->torank][fileidx('b')];
+            /* check if rook can travel the queen-side */
+            if (gamestate->board[move->torank][fileidx('b')]) {
+                return true;
+            }
         }
+        /* check if new field for the rook is free */
         File midfile = (move->tofile+move->fromfile)/2;
-        bool incheck = false;
-        if (gamestate->movecount > 0) {
-            incheck = is_check_position(gamestate);
+        if (gamestate->board[move->torank][midfile]) {
+            return true;
         }
-        blocked |= incheck || gamestate->board[move->torank][midfile] ||
-            is_covered(gamestate, midfile, move->torank, op_color);
+        /* check if the king or the target field for the rook is threatened */
+        if (is_check_position(gamestate) || is_covered(gamestate,
+            midfile, move->torank, opponent_color(piece_color(move->piece)))) {
+            return true;
+        }
     }
-    
-    return blocked;
+    return false;
 }
 
 size_t king_moves(const GameState *gamestate,
--- a/src/chess/rules.c	Thu Sep 03 15:28:54 2026 +0200
+++ b/src/chess/rules.c	Thu Sep 03 16:18:24 2026 +0200
@@ -346,14 +346,48 @@
     }
     
     /* 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;
+                }
+            }
         }
     }
     
@@ -941,11 +975,7 @@
 static int getlocation(const GameState *gamestate, Move *move) {
 
     Color color = piece_color(move->piece);
-    bool incheck = false;
-    if (gamestate->movecount > 0) {
-        incheck = is_check_position(gamestate);
-    }
-    
+
     Move candidates[16], *candidate = NULL;
     size_t candidatecount;
 
@@ -983,7 +1013,7 @@
             if (found) {
                 if (piece_type(move->piece) == KING) {
                     return KING_MOVES_INTO_CHECK;
-                } else if (incheck) {
+                } else if (is_check_position(gamestate)) {
                     return KING_IN_CHECK;
                 } else {
                     return PIECE_PINNED;
--- a/src/chess/rules.h	Thu Sep 03 15:28:54 2026 +0200
+++ b/src/chess/rules.h	Thu Sep 03 16:18:24 2026 +0200
@@ -179,6 +179,12 @@
     /** this flag is only supposed to be set when the opponent disconnects */
     bool ragequit;
     bool review;
+    struct {
+        bool K; /* white lost right to castle king-side */
+        bool Q; /* white lost right to castle queen-side */
+        bool k; /* black lost right to castle king-side */
+        bool q; /* black lost right to castle queen-side */
+    } castling; /* castling rights */
 } GameState;
 
 #define piece_type(piece) ((uint8_t)(piece)&PIECE_MASK)
@@ -226,6 +232,7 @@
 }
 
 static inline bool is_check_position(const GameState *gamestate) {
+    if (gamestate->movecount == 0) return false;
     return gamestate->moves[gamestate->movecount - 1].check;
 }
 

mercurial