Thu, 06 Aug 2026 16:11:37 +0200
implements stalemate detection + fixes missing const qualifier in new *_moves[_allowed] API
resolves #947
relates to #952
--- a/Makefile Thu Aug 06 14:45:55 2026 +0200 +++ b/Makefile Thu Aug 06 16:11:37 2026 +0200 @@ -36,7 +36,7 @@ tests: $(BUILDDIR) $(BUILDDIR)/libchess$(LIB_EXT) FORCE cd test && $(MAKE) -check: tests FORCE +check: all FORCE $(BUILDDIR)/run-tests $(BUILDDIR):
--- a/src/chess/bishop.c Thu Aug 06 14:45:55 2026 +0200 +++ b/src/chess/bishop.c Thu Aug 06 16:11:37 2026 +0200 @@ -53,7 +53,8 @@ return false; } -size_t bishop_moves(GameState *gamestate, Color c, Row r, File f, Move *moves) { +size_t bishop_moves(const GameState *gamestate, + Color c, Row r, File f, Move *moves) { size_t count = 0; const int directions[4][2] = {
--- a/src/chess/bishop.h Thu Aug 06 14:45:55 2026 +0200 +++ b/src/chess/bishop.h Thu Aug 06 16:11:37 2026 +0200 @@ -40,9 +40,10 @@ bool bishop_isblocked(const GameState *gamestate, const Move *move); #define BISHOP_MOVES_MAX 13 -size_t bishop_moves(GameState *gamestate, Color c, Row r, File f, Move *moves); +size_t bishop_moves(const GameState *gamestate, + Color c, Row r, File f, Move *moves); #define bishop_moves_allowed(gamestate, color, row, file, moves) \ - filter_moves_allowed(gamestate, color, row, file, moves, bishop_moves) + filter_moves_allowed(gamestate, color, row, file, moves, bishop_moves) #ifdef __cplusplus }
--- a/src/chess/king.c Thu Aug 06 14:45:55 2026 +0200 +++ b/src/chess/king.c Thu Aug 06 16:11:37 2026 +0200 @@ -88,7 +88,8 @@ return blocked; } -size_t king_moves(GameState *gamestate, Color c, Row r, File f, Move *moves) { +size_t king_moves(const GameState *gamestate, + Color c, Row r, File f, Move *moves) { size_t count = 0; Piece king = mkpiece(KING, c);
--- a/src/chess/king.h Thu Aug 06 14:45:55 2026 +0200 +++ b/src/chess/king.h Thu Aug 06 16:11:37 2026 +0200 @@ -41,9 +41,10 @@ bool king_isblocked(const GameState *gamestate, const Move *move); #define KING_MOVES_MAX 8 -size_t king_moves(GameState *gamestate, Color c, Row r, File f, Move *moves); +size_t king_moves(const GameState *gamestate, + Color c, Row r, File f, Move *moves); #define king_moves_allowed(gamestate, color, row, file, moves) \ - filter_moves_allowed(gamestate, color, row, file, moves, king_moves) + filter_moves_allowed(gamestate, color, row, file, moves, king_moves) #ifdef __cplusplus }
--- a/src/chess/knight.c Thu Aug 06 14:45:55 2026 +0200 +++ b/src/chess/knight.c Thu Aug 06 16:11:37 2026 +0200 @@ -38,7 +38,8 @@ return (dx == 2 && dy == 1) || (dx == 1 && dy == 2); } -size_t knight_moves(GameState *gamestate, Color c, Row r, File f, Move *moves) { +size_t knight_moves(const GameState *gamestate, + Color c, Row r, File f, Move *moves) { size_t count = 0; const int offsets[8][2] = {
--- a/src/chess/knight.h Thu Aug 06 14:45:55 2026 +0200 +++ b/src/chess/knight.h Thu Aug 06 16:11:37 2026 +0200 @@ -40,9 +40,10 @@ #define knight_isblocked(gs,m) false #define KNIGHT_MOVES_MAX 8 -size_t knight_moves(GameState *gamestate, Color c, Row r, File f, Move *moves); +size_t knight_moves(const GameState *gamestate, + Color c, Row r, File f, Move *moves); #define knight_moves_allowed(gamestate, color, row, file, moves) \ - filter_moves_allowed(gamestate, color, row, file, moves, knight_moves) + filter_moves_allowed(gamestate, color, row, file, moves, knight_moves) #ifdef __cplusplus }
--- a/src/chess/pawn.c Thu Aug 06 14:45:55 2026 +0200 +++ b/src/chess/pawn.c Thu Aug 06 16:11:37 2026 +0200 @@ -78,7 +78,8 @@ } } -size_t pawn_moves(GameState *gamestate, Color c, Row r, File f, Move *moves) { +size_t pawn_moves(const GameState *gamestate, + Color c, Row r, File f, Move *moves) { size_t count = 0; int rowdelta = c == WHITE ? 1 : -1; int promotionrow = c == WHITE ? 7 : 0;
--- a/src/chess/pawn.h Thu Aug 06 14:45:55 2026 +0200 +++ b/src/chess/pawn.h Thu Aug 06 16:11:37 2026 +0200 @@ -40,9 +40,10 @@ bool pawn_isblocked(const GameState *gamestate, const Move *move); #define PAWN_MOVES_MAX 4 -size_t pawn_moves(GameState *gamestate, Color c, Row r, File f, Move *moves); +size_t pawn_moves(const GameState *gamestate, + Color c, Row r, File f, Move *moves); #define pawn_moves_allowed(gamestate, color, row, file, moves) \ - filter_moves_allowed(gamestate, color, row, file, moves, pawn_moves) + filter_moves_allowed(gamestate, color, row, file, moves, pawn_moves) #ifdef __cplusplus }
--- a/src/chess/queen.c Thu Aug 06 14:45:55 2026 +0200 +++ b/src/chess/queen.c Thu Aug 06 16:11:37 2026 +0200 @@ -44,7 +44,9 @@ } } -size_t queen_moves(GameState *gamestate, Color c, Row r, File f, Move *moves) { +size_t queen_moves(const GameState *gamestate, + Color c, Row r, File f, Move *moves) { + size_t count = 0; const int directions[8][2] = { { 1, 0},
--- a/src/chess/queen.h Thu Aug 06 14:45:55 2026 +0200 +++ b/src/chess/queen.h Thu Aug 06 16:11:37 2026 +0200 @@ -40,9 +40,10 @@ bool queen_isblocked(const GameState *gamestate, const Move *move); #define QUEEN_MOVES_MAX 27 -size_t queen_moves(GameState *gamestate, Color c, Row r, File f, Move *moves); +size_t queen_moves(const GameState *gamestate, + Color c, Row r, File f, Move *moves); #define queen_moves_allowed(gamestate, color, row, file, moves) \ - filter_moves_allowed(gamestate, color, row, file, moves, queen_moves) + filter_moves_allowed(gamestate, color, row, file, moves, queen_moves) #ifdef __cplusplus }
--- a/src/chess/rook.c Thu Aug 06 14:45:55 2026 +0200 +++ b/src/chess/rook.c Thu Aug 06 16:11:37 2026 +0200 @@ -59,7 +59,8 @@ return false; } -size_t rook_moves(GameState *gamestate, Color c, Row r, File f, Move *moves) { +size_t rook_moves(const GameState *gamestate, + Color c, Row r, File f, Move *moves) { size_t count = 0; const int directions[4][2] = { { 1, 0},
--- a/src/chess/rook.h Thu Aug 06 14:45:55 2026 +0200 +++ b/src/chess/rook.h Thu Aug 06 16:11:37 2026 +0200 @@ -40,9 +40,10 @@ bool rook_isblocked(const GameState *gamestate, const Move *move); #define ROOK_MOVES_MAX 14 -size_t rook_moves(GameState *gamestate, Color c, Row r, File f, Move *moves); +size_t rook_moves(const GameState *gamestate, + Color c, Row r, File f, Move *moves); #define rook_moves_allowed(gamestate, color, row, file, moves) \ - filter_moves_allowed(gamestate, color, row, file, moves, rook_moves) + filter_moves_allowed(gamestate, color, row, file, moves, rook_moves) #ifdef __cplusplus }
--- 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 */
--- a/src/chess/rules.h Thu Aug 06 14:45:55 2026 +0200 +++ b/src/chess/rules.h Thu Aug 06 16:11:37 2026 +0200 @@ -287,10 +287,24 @@ piece_set(gamestate, row, file, 0); } -typedef size_t(*moves_generator_func)(GameState *gamestate, +typedef size_t(*moves_generator_func)(const GameState *gamestate, Color c, Row r, File f, Move *moves); /** + * Calculates all allowed moves for a specific piece. + * + * Use the macros for the specific pieces instead. + * + * @param gamestate the current gamestate + * @param r the row of the piece + * @param f the file of the piece + * @param moves target array for the list of moves + * @return the number of moves stored in the @p moves array + */ +size_t piece_moves_allowed(const GameState *gamestate, + Row r, File f, Move *moves); + +/** * Internal function used to filter out illegal moves. * * Use the macros for the specific pieces instead. @@ -303,7 +317,7 @@ * @param func a function that unconditionally generates the moves * @return the number of moves stored in the @p moves array */ -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); /**
--- a/test/test-real-pgn.c Thu Aug 06 14:45:55 2026 +0200 +++ b/test/test-real-pgn.c Thu Aug 06 16:11:37 2026 +0200 @@ -56,7 +56,7 @@ GameState *gamestate, int game) { // TODO: fix that we need an initialized game state for parsing PGNs gamestate_init(gamestate); - CX_TEST_ASSERT(0 == parse_pgn(pgn_data[game], gamestate)); + CX_TEST_ASSERT(0 == parse_pgn(pgn_data[game-1], gamestate)); CX_TEST_ASSERT(gamestate->stalemate); CX_TEST_ASSERT(!gamestate->wresign); CX_TEST_ASSERT(!gamestate->bresign);