diff -r 9962f5d98764 -r bde99d803caf src/chess/rules.c --- 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 */