--- 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) {