major refactoring of rules API default tip

Thu, 30 Jul 2026 19:17:38 +0200

author
Mike Becker <universe@uap-core.de>
date
Thu, 30 Jul 2026 19:17:38 +0200
changeset 163
2a6d83f4677e
parent 162
f0fc70b6f8f9

major refactoring of rules API

- validate_move() no longer alters the game state (fixes #935)
- get_threats() now correctly only reports threats
while get_candidates() reports possible moves (fixes #959)
- qualify GameState and Move parameters as const where possible

src/chess/bishop.c file | annotate | diff | comparison | revisions
src/chess/bishop.h file | annotate | diff | comparison | revisions
src/chess/fen.c 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/pgn.c 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
src/main.c file | annotate | diff | comparison | revisions
src/network.h file | annotate | diff | comparison | revisions
--- a/src/chess/bishop.c	Thu Jul 30 18:34:15 2026 +0200
+++ b/src/chess/bishop.c	Thu Jul 30 19:17:38 2026 +0200
@@ -31,11 +31,11 @@
 #include "rules.h"
 #include <stdlib.h>
 
-bool bishop_chkrules(Move* move) {
+bool bishop_chkrules(const Move* move) {
     return abs(move->torow-move->fromrow) == abs(move->tofile-move->fromfile);
 }
 
-bool bishop_isblocked(GameState *gamestate, Move *move) {
+bool bishop_isblocked(const GameState *gamestate, const Move *move) {
     int dy = move->torow > move->fromrow ? 1 : -1;
     int dx = move->tofile > move->fromfile ? 1 : -1;
     
--- a/src/chess/bishop.h	Thu Jul 30 18:34:15 2026 +0200
+++ b/src/chess/bishop.h	Thu Jul 30 19:17:38 2026 +0200
@@ -36,8 +36,8 @@
 extern "C" {
 #endif
 
-bool bishop_chkrules(Move *move);
-bool bishop_isblocked(GameState *gamestate, Move *move);
+bool bishop_chkrules(const Move *move);
+bool bishop_isblocked(const GameState *gamestate, const Move *move);
 
 #ifdef	__cplusplus
 }
--- a/src/chess/fen.c	Thu Jul 30 18:34:15 2026 +0200
+++ b/src/chess/fen.c	Thu Jul 30 19:17:38 2026 +0200
@@ -42,7 +42,7 @@
                     str[i++] = '0'+skip;
                     skip = 0;
                 }
-                switch (gamestate->board[row][file] & ~ENPASSANT_THREAT) {
+                switch (piece_at(gamestate, row, file)) {
                 case WHITE|KING: str[i++] = 'K'; break;
                 case WHITE|QUEEN: str[i++] = 'Q'; break;
                 case WHITE|BISHOP: str[i++] = 'B'; break;
@@ -120,11 +120,11 @@
     str[0] = '-';
 
     for (int file = 0 ; file < 8 ; file++) {
-        if (gamestate->board[3][file] & ENPASSANT_THREAT) {
+        if (enpassant_threat_exists(gamestate, 3, file)) {
             str[0] = filechr(file);
             str[1] = rowchr(2);
         }
-        if (gamestate->board[4][file] & ENPASSANT_THREAT) {
+        if (enpassant_threat_exists(gamestate, 4, file)) {
             str[0] = filechr(file);
             str[1] = rowchr(5);
         }
@@ -137,7 +137,7 @@
     unsigned int hm = 0;
     for (unsigned int i = 0; i < gamestate->movecount; i++) {
         if (gamestate->moves[i].capture
-            || (gamestate->moves[i].piece & PIECE_MASK) == PAWN) {
+            || piece_type(gamestate->moves[i].piece) == PAWN) {
             hm = 0;
         } else {
             hm++;
--- a/src/chess/king.c	Thu Jul 30 18:34:15 2026 +0200
+++ b/src/chess/king.c	Thu Jul 30 19:17:38 2026 +0200
@@ -32,7 +32,9 @@
 
 #include <string.h>
 
-static bool king_castling_chkmoved(GameState *gamestate, Row row, File file) {
+static bool king_castling_chkmoved(
+        const GameState *gamestate, Row row, File file) {
+
     for (unsigned i = 0; i < gamestate->movecount; i++) {
         if (gamestate->moves[i].fromfile == file
             && gamestate->moves[i].fromrow == row) {
@@ -43,7 +45,7 @@
     return false;
 }
 
-bool king_chkrules(GameState *gamestate, Move* move) {
+bool king_chkrules(const GameState *gamestate, const Move* move) {
     if (abs(move->torow - move->fromrow) <= 1 &&
         abs(move->tofile - move->fromfile) <= 1) {
         return true;
@@ -64,7 +66,7 @@
     }
 }
 
-bool king_isblocked(GameState *gamestate, Move *move) {
+bool king_isblocked(const GameState *gamestate, const Move *move) {
     
     uint8_t opponent_color = opponent_color(piece_color(move->piece));
     
@@ -79,7 +81,7 @@
         uint8_t midfile = (move->tofile+move->fromfile)/2;
         bool incheck = false;
         if (gamestate->movecount > 0) {
-            incheck = gamestate->moves[gamestate->movecount-1].check;
+            incheck = is_check_position(gamestate);
         }
         blocked |= incheck || gamestate->board[move->torow][midfile] ||
             is_covered(gamestate, move->torow, midfile, opponent_color);
--- a/src/chess/king.h	Thu Jul 30 18:34:15 2026 +0200
+++ b/src/chess/king.h	Thu Jul 30 19:17:38 2026 +0200
@@ -37,8 +37,8 @@
 extern "C" {
 #endif
 
-bool king_chkrules(GameState *gamestate, Move *move);
-bool king_isblocked(GameState *gamestate, Move *move);
+bool king_chkrules(const GameState *gamestate, const Move *move);
+bool king_isblocked(const GameState *gamestate, const Move *move);
 
 #ifdef	__cplusplus
 }
--- a/src/chess/knight.c	Thu Jul 30 18:34:15 2026 +0200
+++ b/src/chess/knight.c	Thu Jul 30 19:17:38 2026 +0200
@@ -31,7 +31,7 @@
 #include "rules.h"
 #include <stdlib.h>
 
-bool knight_chkrules(Move *move) {
+bool knight_chkrules(const Move *move) {
     int dx = abs(move->fromfile - move->tofile);
     int dy = abs(move->fromrow - move->torow);
     
--- a/src/chess/knight.h	Thu Jul 30 18:34:15 2026 +0200
+++ b/src/chess/knight.h	Thu Jul 30 19:17:38 2026 +0200
@@ -36,7 +36,7 @@
 extern "C" {
 #endif
 
-bool knight_chkrules(Move *move);
+bool knight_chkrules(const Move *move);
 #define knight_isblocked(gs,m) false
 
 #ifdef	__cplusplus
--- a/src/chess/pawn.c	Thu Jul 30 18:34:15 2026 +0200
+++ b/src/chess/pawn.c	Thu Jul 30 19:17:38 2026 +0200
@@ -30,7 +30,7 @@
 #include "pawn.h"
 #include "rules.h"
 
-bool pawn_chkrules(GameState *gamestate, Move *move) {
+bool pawn_chkrules(const GameState *gamestate, const Move *move) {
     int8_t d = piece_color(move->piece) == WHITE ? -1 : 1;
     
     if (move->torow == (d < 0 ? 7 : 0)) {
@@ -69,7 +69,7 @@
     }
 }
 
-bool pawn_isblocked(GameState *gamestate, Move *move) {
+bool pawn_isblocked(const GameState *gamestate, const Move *move) {
     if (move->torow == move->fromrow + 1 || move->torow == move->fromrow - 1) {
         return piece_at(gamestate, mdst(move)) && !move->capture;
     } else {
--- a/src/chess/pawn.h	Thu Jul 30 18:34:15 2026 +0200
+++ b/src/chess/pawn.h	Thu Jul 30 19:17:38 2026 +0200
@@ -36,8 +36,8 @@
 extern "C" {
 #endif
 
-bool pawn_chkrules(GameState *gamestate, Move *move);
-bool pawn_isblocked(GameState *gamestate, Move *move);
+bool pawn_chkrules(const GameState *gamestate, const Move *move);
+bool pawn_isblocked(const GameState *gamestate, const Move *move);
 
 #ifdef	__cplusplus
 }
--- a/src/chess/pgn.c	Thu Jul 30 18:34:15 2026 +0200
+++ b/src/chess/pgn.c	Thu Jul 30 19:17:38 2026 +0200
@@ -147,11 +147,12 @@
             }
         } while (!isspace(c = *(pgndata++)));
         movestr[i] = '\0';
-        if (eval_move(gamestate, movestr, &move, curcol)
+        if (eval_move(gamestate, movestr, curcol, &move)
                 != VALID_MOVE_SYNTAX) {
             return(pgn_error_move_syntax);
         }
-        if (validate_move(gamestate, &move) != VALID_MOVE_SEMANTICS) {
+        int move_validate_result = validate_move(gamestate, &move);
+        if (move_validate_result != VALID_MOVE_SEMANTICS) {
             return(pgn_error_move_semantics);
         }
         apply_move(gamestate, &move);
--- a/src/chess/queen.c	Thu Jul 30 18:34:15 2026 +0200
+++ b/src/chess/queen.c	Thu Jul 30 19:17:38 2026 +0200
@@ -32,11 +32,11 @@
 #include "bishop.h"
 #include "queen.h"
 
-bool queen_chkrules(Move* move) {
+bool queen_chkrules(const Move* move) {
     return bishop_chkrules(move) || rook_chkrules(move);
 }
 
-bool queen_isblocked(GameState *gamestate, Move *move) {
+bool queen_isblocked(const GameState *gamestate, const Move *move) {
     if (rook_chkrules(move)) {
         return rook_isblocked(gamestate, move);
     } else {
--- a/src/chess/queen.h	Thu Jul 30 18:34:15 2026 +0200
+++ b/src/chess/queen.h	Thu Jul 30 19:17:38 2026 +0200
@@ -36,8 +36,8 @@
 extern "C" {
 #endif
 
-bool queen_chkrules(Move *move);
-bool queen_isblocked(GameState *gamestate, Move *move);
+bool queen_chkrules(const Move *move);
+bool queen_isblocked(const GameState *gamestate, const Move *move);
 
 #ifdef	__cplusplus
 }
--- a/src/chess/rook.c	Thu Jul 30 18:34:15 2026 +0200
+++ b/src/chess/rook.c	Thu Jul 30 19:17:38 2026 +0200
@@ -30,11 +30,11 @@
 #include "rules.h"
 #include "rook.h"
 
-bool rook_chkrules(Move *move) {
+bool rook_chkrules(const Move *move) {
     return move->torow == move->fromrow || move->tofile == move->fromfile;
 }
 
-bool rook_isblocked(GameState *gamestate, Move *move) {
+bool rook_isblocked(const GameState *gamestate, const Move *move) {
     
     if (move->torow == move->fromrow) {
         int d = move->tofile > move->fromfile ? 1 : -1;
--- a/src/chess/rook.h	Thu Jul 30 18:34:15 2026 +0200
+++ b/src/chess/rook.h	Thu Jul 30 19:17:38 2026 +0200
@@ -36,8 +36,8 @@
 extern "C" {
 #endif
 
-bool rook_chkrules(Move *move);
-bool rook_isblocked(GameState *gamestate, Move *move);
+bool rook_chkrules(const Move *move);
+bool rook_isblocked(const GameState *gamestate, const Move *move);
 
 #ifdef	__cplusplus
 }
--- a/src/chess/rules.c	Thu Jul 30 18:34:15 2026 +0200
+++ b/src/chess/rules.c	Thu Jul 30 19:17:38 2026 +0200
@@ -79,7 +79,7 @@
     gamestate->movecount = gamestate->movecapacity = 0;
 }
 
-static GameState gamestate_copy_sim(GameState *gamestate) {
+static GameState gamestate_copy_sim(const GameState *gamestate) {
     GameState simulation = *gamestate;
 
     /* create new move and position lists for the simulation */
@@ -102,12 +102,12 @@
     return simulation;
 }
 
-Color current_color(GameState *gamestate) {
+Color current_color(const GameState *gamestate) {
     return (gamestate->movecount % 2 == 0) ? WHITE : BLACK;
 }
 
 /* MUST be called BETWEEN validating AND applying a move to work correctly */
-static void format_move(GameState *gamestate, Move *move) {
+static void format_move(const GameState *gamestate, Move *move) {
     char *string = &(move->string[0]);
     
     /* at least 8 characters should be available, wipe them out */
@@ -136,18 +136,18 @@
             }
         } else if (piece_type(move->piece) != KING) {
             /* resolve ambiguities, if any */
-            Move threats[16];
-            size_t threatcount;
-            if (get_threats(gamestate, move->torow, move->tofile,
-                    piece_color(move->piece), threats, &threatcount)) {
+            Move candidates[16];
+            size_t ccount;
+            if (get_candidates(gamestate, move->torow, move->tofile,
+                    piece_color(move->piece), candidates, &ccount)) {
                 unsigned int ambrows = 0, ambfiles = 0, ambpiece = 0;
-                for (size_t i = 0 ; i < threatcount ; i++) {
-                    if (threats[i].piece == move->piece) {
+                for (size_t i = 0 ; i < ccount ; i++) {
+                    if (candidates[i].piece == move->piece) {
                         ambpiece++;
-                        if (threats[i].fromrow == move->fromrow) {
+                        if (candidates[i].fromrow == move->fromrow) {
                             ambrows++;
                         }
-                        if (threats[i].fromfile == move->fromfile) {
+                        if (candidates[i].fromfile == move->fromfile) {
                             ambfiles++;
                         }
                     }
@@ -187,8 +187,10 @@
     }
     
     /* check? */
-    if (move->check) {
-        string[idx++] = gamestate->checkmate?'#':'+';
+    if (move->checkmate) {
+        string[idx++] = '#';
+    } else if (move->check) {
+        string[idx++] = '+';
     }
 }
 
@@ -269,15 +271,7 @@
     }
 }
 
-// TODO: check if we really need this "simulate" flag for performance
-static void apply_move_impl(GameState *gamestate, Move *move, bool simulate) {
-    /* format move before moving (s.t. ambiguities can be resolved) */
-    if (!simulate) {
-        if (!move->string[0]) {
-            format_move(gamestate, move);
-        }
-    }
-
+void apply_move(GameState *gamestate, Move *move) {
     /* en passant capture */
     if (move->capture && piece_type(move->piece) == PAWN &&
             piece_at(gamestate, mdst(move)) == 0) {
@@ -342,13 +336,12 @@
 
     /* important: only "add" the move after calculating the time! */
     gamestate->movecount++;
+
+    /* did this move checkmate the other king? */
+    gamestate->checkmate = move->checkmate;
 }
 
-void apply_move(GameState *gamestate, Move *move) {
-    apply_move_impl(gamestate, move, false);
-}
-
-void gamestate_at_move(GameState *gamestate,
+void gamestate_at_move(const GameState *gamestate,
         unsigned move_number, GameState *replay) {
     gamestate_init(replay);
     memcpy(&replay->info, &gamestate->info, sizeof(GameInfo));
@@ -357,11 +350,137 @@
         move_number = gamestate->movecount;
     }
     for (unsigned i = 0 ; i < move_number ; i++) {
-        apply_move_impl(replay, &(gamestate->moves[i]), true);
+        apply_move(replay, &(gamestate->moves[i]));
     }
 }
 
-static int validate_move_rules(GameState *gamestate, Move *move) {
+/* 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(&simulation, &simmove);
+
+    /* find the opposing king */
+    Color piececolor = piece_color(move->piece);
+    Color oppcolor = opponent_color(piececolor);
+    File opkingfile = 0;
+    Row opkingrow = 0;
+    for (Row row = 0 ; row < 8 ; row++) {
+        for (File file = 0 ; file < 8 ; file++) {
+            Piece p = piece_at(&simulation, row, file);
+            if (p == mkpiece(KING, oppcolor)) {
+                opkingfile = file;
+                opkingrow = row;
+            }
+        }
+    }
+
+    /* determine if the opposing king is now threatened */
+    Move threats[16];
+    size_t threatcount;
+    bool incheck = get_threats(&simulation, opkingrow, opkingfile,
+        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;
+            Row er = opkingrow + 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, er, ef, 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.fromrow = opkingrow;
+                move_retaliate.fromfile = opkingfile;
+                move_retaliate.torow = er;
+                move_retaliate.tofile = ef;
+                move_retaliate.capture = true;
+                apply_move(&sim_retaliate, &move_retaliate);
+                canescape = !is_covered(&sim_retaliate, er, ef, 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].fromrow, threats[0].fromfile, 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->fromrow == threat->torow) {
+                /* rook aspect (on row) */
+                int d = threat->tofile > threat->fromfile ? 1 : -1;
+                File file = threat->fromfile;
+                while (!canescape && file != threat->tofile - d) {
+                    file += d;
+                    canescape |= is_protected(&simulation,
+                        threat->torow, file, oppcolor);
+                }
+            } else if (threat->fromfile == threat->tofile) {
+                /* rook aspect (on file) */
+                int d = threat->torow > threat->fromrow ? 1 : -1;
+                Row row = threat->fromrow;
+                while (!canescape && row != threat->torow - d) {
+                    row += d;
+                    canescape |= is_protected(&simulation,
+                        row, threat->tofile, oppcolor);
+                }
+            } else {
+                /* bishop aspect */
+                int dr = threat->torow > threat->fromrow ? 1 : -1;
+                int df = threat->tofile > threat->fromfile ? 1 : -1;
+
+                Row row = threat->fromrow;
+                File file = threat->fromfile;
+                while (!canescape && file != threat->tofile - df
+                        && row != threat->torow - dr) {
+                    row += dr;
+                    file += df;
+                    canescape |= is_protected(&simulation, row, file,
+                        oppcolor);
+                }
+            }
+        }
+    }
+    gamestate_cleanup(&simulation);
+    return canescape ? 1 : 2;
+}
+
+static int validate_move_rules(const GameState *gamestate, const Move *move) {
     assert((move->piece & ~(PIECE_MASK|COLOR_MASK)) == 0);
 
     /* validate indices (don't trust opponent) */
@@ -369,12 +488,12 @@
         !isidx(move->torow) || !isidx(move->tofile)) {
         return INVALID_MOVE_SYNTAX;
     }
-    
+
     /* must move */
     if (move->fromfile == move->tofile && move->fromrow == move->torow) {
         return INVALID_MOVE_SYNTAX;
     }
-    
+
     /* does piece exist */
     if (piece_at(gamestate, msrc(move)) != move->piece) {
         return PIECE_NOT_FOUND;
@@ -382,7 +501,7 @@
 
     /* is there any piece at the destination? */
     Piece piece_at_dst = piece_at(gamestate, mdst(move));
-    
+
     /* can't capture own pieces */
     if (piece_color(piece_at_dst) == piece_color(move->piece)) {
         return RULES_VIOLATED;
@@ -397,188 +516,100 @@
             return INVALID_MOVE_SYNTAX;
         }
     }
-    
+
     /* validate individual rules */
     bool chkrules;
     switch (piece_type(move->piece)) {
-    case PAWN:
-        chkrules = pawn_chkrules(gamestate, move) &&
-            !pawn_isblocked(gamestate, move);
-        break;
-    case ROOK:
-        chkrules = rook_chkrules(move) &&
-            !rook_isblocked(gamestate, move);
-        break;
-    case KNIGHT:
-        chkrules = knight_chkrules(move); /* knight is never blocked */
-        break;
-    case BISHOP:
-        chkrules = bishop_chkrules(move) &&
-            !bishop_isblocked(gamestate, move);
-        break;
-    case QUEEN:
-        chkrules = queen_chkrules(move) &&
-            !queen_isblocked(gamestate, move);
-        break;
-    case KING:
-        chkrules = king_chkrules(gamestate, move) &&
-            !king_isblocked(gamestate, move);
-        break;
-    default:
-        return INVALID_MOVE_SYNTAX;
+        case PAWN:
+            chkrules = pawn_chkrules(gamestate, move) &&
+                !pawn_isblocked(gamestate, move);
+            break;
+        case ROOK:
+            chkrules = rook_chkrules(move) &&
+                !rook_isblocked(gamestate, move);
+            break;
+        case KNIGHT:
+            chkrules = knight_chkrules(move) &&
+                !knight_isblocked(gamestate, move);
+            break;
+        case BISHOP:
+            chkrules = bishop_chkrules(move) &&
+                !bishop_isblocked(gamestate, move);
+            break;
+        case QUEEN:
+            chkrules = queen_chkrules(move) &&
+                !queen_isblocked(gamestate, move);
+            break;
+        case KING:
+            chkrules = king_chkrules(gamestate, move) &&
+                !king_isblocked(gamestate, move);
+            break;
+        default:
+            return INVALID_MOVE_SYNTAX;
+    }
+
+    /* cancel processing to save resources */
+    if (!chkrules) {
+        return RULES_VIOLATED;
     }
     
-    return chkrules ? VALID_MOVE_SEMANTICS : RULES_VIOLATED;
-}
-
-int validate_move(GameState *gamestate, Move *move) {
-    
-    int result = validate_move_rules(gamestate, move);
-    
-    /* cancel processing to save resources */
-    if (result != VALID_MOVE_SEMANTICS) {
-        return result;
-    }
-    
-    /* simulate move for check validation */
+    /* test if the move would expose our own king */
     GameState simulation = gamestate_copy_sim(gamestate);
     Move simmove = *move;
-    apply_move_impl(&simulation, &simmove, true);
-    
-    /* find kings for check validation */
+    apply_move(&simulation, &simmove);
     Color piececolor = piece_color(move->piece);
     Color oppcolor = opponent_color(piececolor);
-    
-    File mykingfile = 0, opkingfile = 0;
-    Row  mykingrow = 0, opkingrow = 0;
+    File kingfile = 0;
+    Row kingrow = 0;
     for (Row row = 0 ; row < 8 ; row++) {
         for (File file = 0 ; file < 8 ; file++) {
             Piece p = piece_at(&simulation, row, file);
             if (p == mkpiece(KING, piececolor)) {
-                mykingfile = file;
-                mykingrow = row;
-            } else if (p == mkpiece(KING, oppcolor)) {
-                opkingfile = file;
-                opkingrow = row;
+                kingfile = file;
+                kingrow = row;
             }
         }
     }
-    
-    /* don't move into or stay in check position */
-    if (is_covered(&simulation, mykingrow, mykingfile, oppcolor)) {
+    int result = VALID_MOVE_SEMANTICS;
+    if (is_covered(&simulation, kingrow, kingfile, oppcolor)) {
         gamestate_cleanup(&simulation);
         if (piece_type(move->piece) == KING) {
-            return KING_MOVES_INTO_CHECK;
+            result = KING_MOVES_INTO_CHECK;
         } else {
-            if (gamestate->moves[gamestate->movecount - 1].check) {
-                return KING_IN_CHECK;
+            if (is_check_position(gamestate)) {
+                result = KING_IN_CHECK;
             } else {
-                return PIECE_PINNED;
+                result = PIECE_PINNED;
             }
         }
     }
-    
-    /* correct check and checkmate flags (move is still valid) */
-    Move threats[16];
-    size_t threatcount;
-    move->check = get_threats(&simulation, opkingrow, opkingfile,
-        piececolor, threats, &threatcount);
-    
-    if (move->check) {
-        /* 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;
-                if (!isidx(opkingrow + dr)) continue;
-                if (!isidx(opkingfile + df)) continue;
-                Row er = opkingrow + dr;
-                File ef = opkingfile + df;
+    gamestate_cleanup(&simulation);
 
-                /* check if escape field is already covered (threatened) */
-                if (is_covered(&simulation, er, ef, piececolor))
-                    continue;
+    return result;
+}
 
-                /* check if piece of the king's color blocks the field */
-                if (piece_color(simulation.board[er][ef]) == oppcolor)
-                    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.fromrow = opkingrow;
-                    move_retaliate.fromfile = opkingfile;
-                    move_retaliate.torow = er;
-                    move_retaliate.tofile = ef;
-                    move_retaliate.capture = 1;
-                    apply_move_impl(&sim_retaliate, &move_retaliate, true);
-                    canescape = !is_covered(&sim_retaliate, er, ef, piececolor);
-                    gamestate_cleanup(&sim_retaliate);
-                } else {
-                    /* the field is not covered and unoccupied */
-                    canescape = true;
-                }
-            }
-        }
+int validate_move(const GameState *gamestate, const Move *move) {
+    int result = validate_move_rules(gamestate, move);
+    if (result != VALID_MOVE_SEMANTICS) {
+        return result;
+    }
 
-        /* can't escape, can the king be rescued? */
-        if (!canescape && threatcount == 1) {
-            canescape = is_protected(&simulation,
-                    threats[0].fromrow, threats[0].fromfile, oppcolor);
+    /* validate check and checkmate flags */
+    int cocm = determine_check_or_checkmate(gamestate, move);
+    if (cocm == 2) {
+        if (!move->checkmate) {
+            return MISSING_CHECKMATE;
         }
-        
-        /* 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->fromrow == threat->torow) {
-                    /* rook aspect (on row) */
-                    int d = threat->tofile > threat->fromfile ? 1 : -1;
-                    File file = threat->fromfile;
-                    while (!canescape && file != threat->tofile - d) {
-                        file += d;
-                        canescape |= is_protected(&simulation,
-                            threat->torow, file, oppcolor);
-                    }
-                } else if (threat->fromfile == threat->tofile) {
-                    /* rook aspect (on file) */
-                    int d = threat->torow > threat->fromrow ? 1 : -1;
-                    Row row = threat->fromrow;
-                    while (!canescape && row != threat->torow - d) {
-                        row += d;
-                        canescape |= is_protected(&simulation,
-                            row, threat->tofile, oppcolor);
-                    }
-                } else {
-                    /* bishop aspect */
-                    int dr = threat->torow > threat->fromrow ? 1 : -1;
-                    int df = threat->tofile > threat->fromfile ? 1 : -1;
+    } else if (cocm == 1) {
+        if (!move->check) {
+            return MISSING_CHECK;
+        }
+    } else if (move->checkmate) {
+        return INVALID_CHECKMATE;
+    } else if (move->check) {
+        return INVALID_CHECK;
+    }
 
-                    Row row = threat->fromrow;
-                    File file = threat->fromfile;
-                    while (!canescape && file != threat->tofile - df
-                            && row != threat->torow - dr) {
-                        row += dr;
-                        file += df;
-                        canescape |= is_protected(&simulation, row, file,
-                            oppcolor);
-                    }
-                }
-            }
-        }
-            
-        if (!canescape) {
-            gamestate->checkmate = true;
-        }
-    }
-    
-    gamestate_cleanup(&simulation);
     
     return VALID_MOVE_SEMANTICS;
 }
@@ -591,16 +622,16 @@
     gamestate->board[row][file] = piece;
 }
 
-bool get_threats(GameState *gamestate, Row row, File file,
-        Color color, Move *threats, size_t *threatcount) {
+bool get_candidates(const GameState *gamestate, Row row, File file,
+        Color color, Move *moves, size_t *movecount) {
     Move candidates[32];
     size_t ccount = 0;
     for (Row r = 0 ; r < 8 ; r++) {
         for (File f = 0 ; f < 8 ; f++) {
-            if (piece_color(gamestate->board[r][f]) == color) {
+            Piece p = piece_at(gamestate, r, f);
+            if (piece_color(p) == color) {
                 /* non-capturing move */
                 memset(&(candidates[ccount]), 0, sizeof(Move));
-                Piece p = piece_at(gamestate, r, f);
                 candidates[ccount].piece = p;
                 candidates[ccount].fromrow = r;
                 candidates[ccount].fromfile = f;
@@ -615,7 +646,55 @@
                 /* capturing move */
                 memcpy(&(candidates[ccount]),
                     &(candidates[ccount-1]), sizeof(Move));
-                candidates[ccount].capture = 1;
+                candidates[ccount].capture = true;
+                ccount++;
+            }
+        }
+    }
+
+    if (movecount) {
+        *movecount = 0;
+    }
+
+    bool result = false;
+
+    for (size_t i = 0 ; i < ccount ; i++) {
+        if (validate_move_rules(gamestate, &(candidates[i]))
+                == VALID_MOVE_SEMANTICS) {
+            result = true;
+            if (moves && movecount) {
+                moves[(*movecount)++] = candidates[i];
+            }
+        }
+    }
+
+    return result;
+}
+
+bool get_threats(const GameState *gamestate, Row row, File file,
+        Color color, Move *threats, size_t *threatcount) {
+
+    /* simulate a capturing move on the target position */
+    Color opcolor = opponent_color(color);
+    GameState simulation = gamestate_copy_sim(gamestate);
+    if (piece_color(piece_at(&simulation, row, file)) != opcolor) {
+        /* set a fake pawn if the field is not occupied by the opponent */
+        piece_set(&simulation, row, file, mkpiece(PAWN, opcolor));
+    }
+
+    Move candidates[16];
+    size_t ccount = 0;
+    for (Row r = 0 ; r < 8 ; r++) {
+        for (File f = 0 ; f < 8 ; f++) {
+            Piece p = piece_at(&simulation, r, f);
+            if (piece_color(p) == color) {
+                memset(&(candidates[ccount]), 0, sizeof(Move));
+                candidates[ccount].piece = p;
+                candidates[ccount].fromrow = r;
+                candidates[ccount].fromfile = f;
+                candidates[ccount].torow = row;
+                candidates[ccount].tofile = file;
+                candidates[ccount].capture = true;
                 ccount++;
             }
         }
@@ -625,11 +704,10 @@
         *threatcount = 0;
     }
     
-    
     bool result = false;
     
     for (size_t i = 0 ; i < ccount ; i++) {
-        if (validate_move_rules(gamestate, &(candidates[i]))
+        if (validate_move_rules(&simulation, &(candidates[i]))
                 == VALID_MOVE_SEMANTICS) {
             result = true;
             if (threats && threatcount) {
@@ -637,11 +715,13 @@
             }
         }
     }
+
+    gamestate_cleanup(&simulation);
     
     return result;
 }
 
-bool is_pinned(GameState *gamestate, Move *move) {
+bool is_pinned(const GameState *gamestate, const Move *move) {
     Color color = piece_color(move->piece);
 
     GameState simulation = gamestate_copy_sim(gamestate);
@@ -666,7 +746,7 @@
     return covered;
 }
 
-bool get_real_threats(GameState *gamestate, Row row, File file,
+bool get_real_threats(const GameState *gamestate, Row row, File file,
         Color color, Move *threats, size_t *threatcount) {
     
     if (threatcount) {
@@ -710,48 +790,48 @@
     }
 }
 
-static int getlocation(GameState *gamestate, Move *move) {   
+static int getlocation(const GameState *gamestate, Move *move) {
 
     Color color = piece_color(move->piece);
     bool incheck = false;
     if (gamestate->movecount > 0) {
-        incheck = gamestate->moves[gamestate->movecount - 1].check;
+        incheck = is_check_position(gamestate);
     }
     
-    Move threats[16], *threat = NULL;
-    size_t threatcount;
+    Move candidates[16], *candidate = NULL;
+    size_t candidatecount;
 
-    /* determine all threats and sort out the unreal threats on our own */
-    if (get_threats(gamestate, move->torow, move->tofile, color,
-            threats, &threatcount)) {
+    /* determine all candidate moves and sort out the invalid ones */
+    if (get_candidates(gamestate, move->torow, move->tofile, color,
+            candidates, &candidatecount)) {
         
         bool found = false;
         
-        /* find threats for the specified position */
-        for (size_t i = 0 ; i < threatcount ; i++) {
-            if (threats[i].piece == move->piece &&
+        for (size_t i = 0 ; i < candidatecount ; i++) {
+            /* filter by partial fromrow/fromfile information */
+            if (candidates[i].piece == move->piece &&
                     (move->fromrow == POS_UNSPECIFIED ||
-                    move->fromrow == threats[i].fromrow) &&
+                    move->fromrow == candidates[i].fromrow) &&
                     (move->fromfile == POS_UNSPECIFIED ||
-                    move->fromfile == threats[i].fromfile)) {
+                    move->fromfile == candidates[i].fromfile)) {
 
-                /* found a threat, here it does not matter if it's valid! */
+                /* found a candidate, here it does not matter if it's valid! */
                 found = true;
 
                 /* discard pinned pieces */
-                if (!is_pinned(gamestate, &(threats[i]))) {
-                    if (threat) {
-                        /* we've already found a valid threat */
+                if (!is_pinned(gamestate, &(candidates[i]))) {
+                    if (candidate) {
+                        /* we've already found a valid candidate */
                         return AMBIGUOUS_MOVE;
                     } else {
-                        threat = &(threats[i]);
+                        candidate = &(candidates[i]);
                     }
                 }
             }
         }
         
-        /* can't threaten specified position */
-        if (!threat) {
+        /* no valid candidate left */
+        if (!candidate) {
             if (found) {
                 if (piece_type(move->piece) == KING) {
                     return KING_MOVES_INTO_CHECK;
@@ -765,9 +845,9 @@
             }
         }
 
-        /* found a threat, copy the source location */
-        move->fromrow = threat->fromrow;
-        move->fromfile = threat->fromfile;
+        /* found a candidate, copy the source location */
+        move->fromrow = candidate->fromrow;
+        move->fromfile = candidate->fromfile;
         return VALID_MOVE_SYNTAX;
     } else {
         return PIECE_NOT_FOUND;
@@ -790,6 +870,7 @@
     if (mstr[len-1] == '+' || mstr[len-1] == '#') {
         len--; mstr[len] = '\0';
         move->check = true;
+        move->checkmate = mstr[len-1] == '#';
     }
     
     /* evaluate promotion */
@@ -829,7 +910,7 @@
         }
         if (mstr[1] == 'x') {
             /* capture (e.g. "Nxf3", "dxe5") */
-            move->capture = 1;
+            move->capture = true;
         } else {
             /* move (e.g. "Ndf3", "N2c3", "e2e4") */
             if (isfile(mstr[1])) {
@@ -855,7 +936,7 @@
         } else {
             move->piece = getpiece(mstr[0], color);
             if (mstr[2] == 'x') {
-                move->capture = 1;
+                move->capture = true;
                 if (move->piece) {
                     /* capture (e.g. "Ndxf3" or "R1xh3") */
                     if (isfile(mstr[1])) {
@@ -882,7 +963,7 @@
     } else if (len == 6) {
         /* long notation capture (e.g. "Nc5xf3") */
         if (mstr[3] == 'x') {
-            move->capture = 1;
+            move->capture = true;
             move->piece = getpiece(mstr[0], color);
             move->fromfile = fileidx(mstr[1]);
             move->fromrow = rowidx(mstr[2]);
@@ -914,24 +995,47 @@
     return VALID_MOVE_SYNTAX;
 }
 
-int eval_move(GameState *gamestate, const char *mstr,
-        Move *move, Color color) {
+int eval_move2(const GameState *gamestate,
+        const char *mstr, Color color, Move *move, bool lazy) {
     int result = eval_move1(mstr, move, color);
     if (result == VALID_MOVE_SYNTAX) {
         if (move->fromfile == POS_UNSPECIFIED
             || move->fromrow == POS_UNSPECIFIED) {
-            return getlocation(gamestate, move);
+            result = getlocation(gamestate, move);
+        }
+        if (result == VALID_MOVE_SYNTAX) {
+            /* correct check/checkmate flags */
+            if (lazy) {
+                move->check = move->checkmate = false;
+                switch (determine_check_or_checkmate(gamestate, move)) {
+                    case 2: move->checkmate = true;
+                    case 1: move->check = true;
+                }
+            }
+
+            /* format the move string */
+            format_move(gamestate, move);
         }
     }
     return result;
 }
 
+int eval_move_lazy(const GameState *gamestate,
+        const char *mstr, Color color, Move *move) {
+    return eval_move2(gamestate, mstr, color, move, true);
+}
+
+int eval_move(const GameState *gamestate,
+        const char *mstr, Color color, Move *move) {
+    return eval_move2(gamestate, mstr, color, move, false);
+}
+
 int check_move(const char *mstr, Color color) {
     Move move;
     return eval_move1(mstr, &move, color);
 }
 
-bool is_protected(GameState *gamestate, Row row, File file, Color color) {
+bool is_protected(const GameState *gamestate, Row row, File file, Color color) {
     Move threats[16];
     size_t threatcount;
     if (get_real_threats(gamestate, row, file, color, threats, &threatcount)) {
@@ -946,7 +1050,7 @@
     }
 }
 
-uint16_t remaining_movetime(GameState *gamestate, Color color) {
+uint16_t remaining_movetime(const GameState *gamestate, Color color) {
     unsigned move_number = gamestate->movecount;
     if (color == BLACK) {
         move_number |= 1;
@@ -956,7 +1060,7 @@
     return remaining_movetime2(gamestate, move_number);
 }
 
-uint16_t remaining_movetime2(GameState *gamestate, unsigned move_number) {
+uint16_t remaining_movetime2(const GameState *gamestate, unsigned move_number) {
     if (!gamestate->info.timecontrol) {
         return 0;
     }
@@ -1023,7 +1127,7 @@
     }
 }
 
-bool check_threefold_repetition(GameState *gamestate) {
+bool check_threefold_repetition(const GameState *gamestate) {
     // TODO: implement threefold repetition detection
     return false;
 }
--- a/src/chess/rules.h	Thu Jul 30 18:34:15 2026 +0200
+++ b/src/chess/rules.h	Thu Jul 30 19:17:38 2026 +0200
@@ -43,7 +43,11 @@
 #define PIECE_PINNED           5
 #define KING_IN_CHECK          6
 #define KING_MOVES_INTO_CHECK  7
-#define RULES_VIOLATED        10
+#define MISSING_CHECK          8
+#define MISSING_CHECKMATE      9
+#define INVALID_CHECK         10
+#define INVALID_CHECKMATE     11
+#define RULES_VIOLATED        32
 
 #define ENPASSANT_THREAT 0x40u
 
@@ -83,7 +87,7 @@
 
 struct movetimeval {
     uint64_t tv_sec;
-    int32_t tv_usec; // important that this is signed b/c potential carry
+    int32_t tv_usec; /* important that this is signed b/c potential carry */
 };
 
 typedef struct {
@@ -92,9 +96,11 @@
     Row fromrow;
     File tofile;
     Row torow;
-    uint8_t promotion;
-    uint8_t check;
+    Piece promotion;
+    uint8_t check; /* must always be set if checkmate is set */
+    uint8_t checkmate;
     uint8_t capture;
+    uint8_t padding[7]; /* necessary for stable ABI across networks */
     struct movetimeval timestamp;
     struct movetimeval movetime;
     char string[8];
@@ -189,7 +195,13 @@
 }
 
 static inline bool is_game_running(const GameState *gamestate) {
-    return !(gamestate->checkmate || gamestate->wresign || gamestate->bresign || gamestate->threefold || gamestate->stalemate || gamestate->remis || gamestate->review);
+    return !(gamestate->checkmate || gamestate->wresign || gamestate->bresign
+        || gamestate->threefold || gamestate->stalemate || gamestate->remis
+        || gamestate->review);
+}
+
+static inline bool is_check_position(const GameState *gamestate) {
+    return gamestate->moves[gamestate->movecount - 1].check;
 }
 
 /**
@@ -242,7 +254,7 @@
  * @param gamestate the current game state
  * @return the color of the player who is next to move
  */
-Color current_color(GameState *gamestate);
+Color current_color(const GameState *gamestate);
 
 /**
  * Returns the piece at the specified position.
@@ -276,7 +288,29 @@
 }
 
 /**
- * Checks, if a specified field is covered by a piece of a certain color.
+ * Determines a list of possible moves to the specified field.
+ *
+ * The out-parameters may both be NULL, but if any of them is set, the other
+ * must be set, too.
+ *
+ * @param gamestate the current game state
+ * @param row row of the field to check
+ * @param file file of the field to check
+ * @param color the color of the piece that should move to the field
+ * @param moves the array where to store the moves
+ * (must be large enough, 16 is always enough)
+ * @param movecount a pointer where the number of moves is stored
+ * @return true, if any piece of the specified color can move to the specified
+ * field
+ */
+bool get_candidates(const GameState *gamestate, Row row, File file,
+        Color color, Move* moves, size_t* movecount);
+
+/**
+ * Checks, if a specified field is threatened by a piece of a certain color.
+ *
+ * A field is threatened, if there is a piece of the specified color that could
+ * capture an opponent piece on this field, regardless of being pinned.
  * 
  * The out-parameters may both be NULL, but if any of them is set, the other
  * must be set, too.
@@ -285,17 +319,17 @@
  * @param row row of the field to check
  * @param file file of the field to check
  * @param color the color of the piece that should threaten the field
- * @param threats the array where to store the threats (should be able to hold
- * the rare maximum of 16 elements)
+ * @param threats the array where to store the threats
+ * (must be large enough, 16 is always enough)
  * @param threatcount a pointer where the count of threats is stored
  * @return true, if any piece of the specified color threatens the specified
- * field (i.e. could capture an opponent piece)
+ * field
  */
-bool get_threats(GameState *gamestate, Row row, File file,
+bool get_threats(const GameState *gamestate, Row row, File file,
         Color color, Move* threats, size_t* threatcount);
 
 /**
- * Checks, if a specified field is covered by a piece of a certain color AND
+ * Checks, if a specified field is threatened by a piece of a certain color AND
  * if this piece is not pinned and therefore able to perform the move.
  * 
  * The out-parameters may both be NULL, but if any of them is set, the other
@@ -305,17 +339,20 @@
  * @param row row of the field to check
  * @param file file of the field to check
  * @param color the color of the piece that should threaten the field
- * @param threats the array where to store the threats (should be able to hold
- * the rare maximum of 16 elements)
+ * @param threats the array where to store the threats
+ * (must be large enough, 16 is always enough)
  * @param threatcount a pointer where the count of threats is stored
  * @return true, if any piece of the specified color threatens the specified
- * field (i.e. could capture an opponent piece)
+ * field and is not pinned
  */
-bool get_real_threats(GameState *gamestate, Row row, File file,
+bool get_real_threats(const GameState *gamestate, Row row, File file,
         Color color, Move* threats, size_t* threatcount);
 
 /**
- * Checks, if a specified field is covered by a piece of a certain color.
+ * Checks, if a specified field is threatened by a piece of a certain color.
+ *
+ * A field is threatened, if there is a piece of the specified color that could
+ * capture an opponent piece on this field, regardless of being pinned.
  * 
  * @param gamestate the current game state
  * @param row row of the field to check
@@ -330,7 +367,7 @@
 /**
  * Checks, if a specified field is attacked by a piece of a certain color.
  * 
- * I.e. the field is covered by a piece AND this piece is not pinned and
+ * I.e. the field is threatened by a piece AND this piece is not pinned and
  * therefore able to perform the move.
  * 
  * @param gamestate the current game state
@@ -346,8 +383,8 @@
 /**
  * Checks, if a specified field is protected by a piece of a certain color.
  * 
- * I.e. the field is covered by a piece that is NOT the king AND this piece is
- * not pinned and therefore able to perform the move.
+ * This is the same as is_attacked(), but only considers pieces that are not
+ * the king.
  * 
  * @param gamestate the current game state
  * @param row row of the field to check
@@ -356,7 +393,7 @@
  * @return true, if any piece (excluding the king) of the specified color
  * threatens the specified field and could capture an opponent piece
  */
-bool is_protected(GameState *gamestate, Row row, File file, Color color);
+bool is_protected(const GameState *gamestate, Row row, File file, Color color);
 
 /**
  * Checks, if the specified move cannot be performed, because the piece is
@@ -372,25 +409,51 @@
  * @return true, if the move cannot be performed because the king would be in
  * check after the move
  */
-bool is_pinned(GameState *gamestate, Move *move);
+bool is_pinned(const GameState *gamestate, const Move *move);
 
 /**
  * Evaluates a move syntactically and stores the move data in the specified
  * object.
  *
  * When short algebraic notation is used, the source position is determined by
- * the evaluating the allowed moves according to the current game state.
+ * evaluating the allowed moves according to the current game state.
+ *
+ * This function expects correct notation of check and checkmate indicators.
+ * For a more lazy evaluation, use eval_move_lazy().
  *
  * For a purely syntactic check, regardless of whether a piece exists that is
  * allowed to move that way, use check_move().
  * 
  * @param gamestate the current game state
  * @param mstr the input string to parse
+ * @param color the color of the player to evaluate the move for
  * @param move a pointer to object where the move data shall be stored
- * @param color the color of the player to evaluate the move for
  * @return status code (see macros in this file for the list of codes)
  */
-int eval_move(GameState *gamestate, const char *mstr, Move *move, Color color);
+int eval_move(const GameState *gamestate,
+    const char *mstr, Color color, Move *move);
+
+/**
+ * Evaluates a move syntactically and stores the move data in the specified
+ * object.
+ *
+ * When short algebraic notation is used, the source position is determined by
+ * evaluating the allowed moves according to the current game state.
+ *
+ * This function automatically corrects missing or incorrect check/checkmate
+ * indicators. Use eval_move() if you want to keep the original notation.
+ *
+ * For a purely syntactic check, regardless of whether a piece exists that is
+ * allowed to move that way, use check_move().
+ *
+ * @param gamestate the current game state
+ * @param mstr the input string to parse
+ * @param color the color of the player to evaluate the move for
+ * @param move a pointer to object where the move data shall be stored
+ * @return status code (see macros in this file for the list of codes)
+ */
+int eval_move_lazy(const GameState *gamestate,
+    const char *mstr, Color color, Move *move);
 
 /**
  * Syntactically checks a move without verifying that a piece exists that is
@@ -408,7 +471,7 @@
  * @param move the move to validate
  * @return status code (see macros in this file for the list of codes)
  */
-int validate_move(GameState *gamestate, Move *move);
+int validate_move(const GameState *gamestate, const Move *move);
 
 /**
  * Applies a move and deletes captured pieces.
@@ -427,7 +490,7 @@
  * @param move_number the half-move that would now be played
  * @param replay the struct to populate with the state at the specified move
  */
-void gamestate_at_move(GameState *gamestate,
+void gamestate_at_move(const GameState *gamestate,
         unsigned move_number, GameState *replay);
 
 /**
@@ -439,7 +502,7 @@
  * @return the remaining time - if time control is disabled, this function
  * always returns zero
  */
-uint16_t remaining_movetime2(GameState *gamestate, unsigned move_number);
+uint16_t remaining_movetime2(const GameState *gamestate, unsigned move_number);
 
 /**
  * Returns the remaining time on the clock for the specified player.
@@ -449,7 +512,7 @@
  * @return the remaining time - if time control is disabled, this function
  * always returns zero
  */
-uint16_t remaining_movetime(GameState *gamestate, Color color);
+uint16_t remaining_movetime(const GameState *gamestate, Color color);
 
 /**
  * Converts clock time to string.
@@ -472,7 +535,7 @@
  * @param gamestate the current game state
  * @return true if the game is in a threefold repetition position
  */
-bool check_threefold_repetition(GameState *gamestate);
+bool check_threefold_repetition(const GameState *gamestate);
 
 #endif	/* RULES_H */
 
--- a/src/main.c	Thu Jul 30 18:34:15 2026 +0200
+++ b/src/main.c	Thu Jul 30 19:17:38 2026 +0200
@@ -355,7 +355,7 @@
     }
 }
 
-static void eval_move_failed_msg(int code) {
+static void eval_move_failed_msg(uint32_t code) {
     switch (code) {
     case AMBIGUOUS_MOVE:
         printw("Ambiguous move - please specify the piece to move.");
@@ -381,6 +381,18 @@
     case KING_MOVES_INTO_CHECK:
         printw("Can't move the king into a check position.");
         break;
+    case MISSING_CHECK:
+        printw("Move does not indicate check.");
+        break;
+    case MISSING_CHECKMATE:
+        printw("Move does not indicate checkmate.");
+        break;
+    case INVALID_CHECK:
+        printw("Move incorrectly indicates check.");
+        break;
+    case INVALID_CHECKMATE:
+        printw("Move incorrectly indicates checkmate.");
+        break;
     default:
         printw("Unknown move parser error.");
     }
@@ -461,7 +473,8 @@
                 /* ignore empty move strings and ask again */
             } else {
                 Move move;
-                int result = eval_move(gamestate, movestr, &move, curcolor);
+                int result = eval_move_lazy(gamestate,
+                    movestr, curcolor, &move);
                 if (result == VALID_MOVE_SYNTAX) {
                     result = validate_move(gamestate, &move);
                     if (result == VALID_MOVE_SEMANTICS) {
@@ -596,13 +609,14 @@
                 /* ignore empty move strings and ask again */
             } else {
                 Move move;
-                int eval_result = eval_move(gamestate, movestr, &move, mycolor);
+                int eval_result = eval_move_lazy(gamestate,
+                    movestr, mycolor, &move);
                 switch (eval_result) {
                 case VALID_MOVE_SYNTAX:
                     net_send_data(opponent, NETCODE_MOVE, &move, sizeof(Move));
                     code = net_recieve_code(opponent);
-                    move.check = code == NETCODE_CHECK ||
-                        code == NETCODE_CHECKMATE;
+                    /* we could validate the move's check/checkmate flag with
+                     * the network response code, but we choose not to do it */
                     gamestate->checkmate = code == NETCODE_CHECKMATE;
                     gamestate->stalemate = code == NETCODE_STALEMATE;
                     if (code == NETCODE_DECLINE) {
--- a/src/network.h	Thu Jul 30 18:34:15 2026 +0200
+++ b/src/network.h	Thu Jul 30 19:17:38 2026 +0200
@@ -54,7 +54,7 @@
 #define NETCODE_CONNLOST 0x80
 #define NETCODE_ERROR 0xFF
 
-#define NETCODE_VERSION 20
+#define NETCODE_VERSION 21
 
 typedef struct {
     int fd; /* -1, if we are the client */

mercurial