implement first naive draw by threefold repetition

Fri, 14 Aug 2026 13:40:26 +0200

author
Mike Becker <universe@uap-core.de>
date
Fri, 14 Aug 2026 13:40:26 +0200
changeset 173
e541b6002933
parent 172
ba1bf80d10cc
child 174
2929e86c4817

implement first naive draw by threefold repetition

partially resolves #843

src/chess/pgn.c file | annotate | diff | comparison | revisions
src/chess/rules.c file | annotate | diff | comparison | revisions
src/chess/rules.h file | annotate | diff | comparison | revisions
--- a/src/chess/pgn.c	Fri Aug 14 11:35:36 2026 +0200
+++ b/src/chess/pgn.c	Fri Aug 14 13:40:26 2026 +0200
@@ -59,7 +59,7 @@
 }
 
 static const char *pgn_result(GameState *gamestate) {
-    if (gamestate->stalemate || gamestate->remis) {
+    if (is_game_drawn(gamestate)) {
         return "1/2-1/2";
     } else if (gamestate->wresign) {
         return "0-1";
@@ -196,7 +196,8 @@
                 movetext_ends_with_result = true;
                 break;
             } else if (c == '/') {
-                gamestate->remis = !gamestate->stalemate;
+                /* only set remis flag if the game was not drawn otherwise */
+                gamestate->remis = !is_game_drawn(gamestate);
                 movetext_ends_with_result = true;
                 break;
             } else {
--- a/src/chess/rules.c	Fri Aug 14 11:35:36 2026 +0200
+++ b/src/chess/rules.c	Fri Aug 14 13:40:26 2026 +0200
@@ -247,7 +247,7 @@
     }
 }
 
-static bool is_stalemate(const GameState *gamestate) {
+static bool check_stalemate(const GameState *gamestate) {
     Color next_player = gamestate->movecount % 2 == 0 ? WHITE : BLACK;
 
     /* scan the board for pieces of the next player's color */
@@ -264,6 +264,39 @@
     return true;
 }
 
+static size_t fen_len_without_moves(const char *fen) {
+    size_t len = strlen(fen);
+    /* find first space that separates the two move counters */
+    while (--len > 0 && fen[len] != ' ') {}
+    /* find second space that separates the move counters from the rest */
+    while (--len > 0 && fen[len] != ' ') {}
+    return len;
+}
+
+bool check_threefold_repetition(const GameState *gamestate) {
+    if (gamestate->movecount < 3) {
+        return false;
+    }
+
+    /* take the part of the FEN that only describes the board */
+    const char *fen = gamestate->fen[gamestate->movecount - 1];
+    size_t fen_len = fen_len_without_moves(fen);
+
+    // TODO: develop a test case that involves en passant and add support here
+
+    /* count the previous occurrences */
+    unsigned c = 0;
+    for (size_t i = gamestate->movecount - 1; i > 0;) {
+        const char *other = gamestate->fen[--i];
+        size_t other_len = fen_len_without_moves(other);
+        if (fen_len != other_len) continue;
+        if (strncmp(fen, other, fen_len) == 0) {
+            if (++c == 2) return true;
+        }
+    }
+    return false;
+}
+
 char getpiecechr(Piece piece) {
     switch (piece_type(piece)) {
     case ROOK: return 'R';
@@ -310,6 +343,7 @@
     }
 }
 
+/* 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 &&
@@ -375,15 +409,18 @@
 
     /* 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_internal(gamestate, move);
-    if (!gamestate->checkmate) {
-        gamestate->stalemate = is_stalemate(gamestate);
+
+    /* calculate gamestate flags in order of efficiency */
+    if (move->checkmate) {
+        gamestate->checkmate = true;
+    } else if (check_threefold_repetition(gamestate)) {
+        gamestate->threefold = true;
+    } else if (check_stalemate(gamestate)) {
+        gamestate->stalemate = true;
     }
 }
 
@@ -1181,11 +1218,6 @@
     }
 }
 
-bool check_threefold_repetition(const GameState *gamestate) {
-    // TODO: implement threefold repetition detection
-    return false;
-}
-
 size_t filter_moves_allowed(const GameState *gamestate,
         Color c, Row r, File f, Move *moves, moves_generator_func func) {
 
--- a/src/chess/rules.h	Fri Aug 14 11:35:36 2026 +0200
+++ b/src/chess/rules.h	Fri Aug 14 13:40:26 2026 +0200
@@ -195,10 +195,13 @@
     return gamestate->board[row][file] & ENPASSANT_THREAT;
 }
 
+static inline bool is_game_drawn(const GameState *gamestate) {
+    return gamestate->threefold || gamestate->stalemate || gamestate->remis;
+}
+
 static inline bool is_game_running(const GameState *gamestate) {
     return !(gamestate->checkmate || gamestate->wresign || gamestate->bresign
-        || gamestate->threefold || gamestate->stalemate || gamestate->remis
-        || gamestate->review);
+        || is_game_drawn(gamestate) || gamestate->review);
 }
 
 static inline bool is_check_position(const GameState *gamestate) {

mercurial