Fri, 21 Aug 2026 17:04:43 +0200
implement draw due to insufficient material
resolves #951
| 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 | |
| test/Makefile | file | annotate | diff | comparison | revisions | |
| test/run-tests.c | file | annotate | diff | comparison | revisions | |
| test/test-real-pgn.c | file | annotate | diff | comparison | revisions | |
| test/test-rules-helper.c | file | annotate | diff | comparison | revisions |
--- a/src/chess/rules.c Fri Aug 21 16:08:50 2026 +0200 +++ b/src/chess/rules.c Fri Aug 21 17:04:43 2026 +0200 @@ -297,6 +297,79 @@ return false; } +static bool check_no_material_color(Color color, const GameState *gamestate) { + /* count the available pieces */ + unsigned piece_count[7] = {0}; + unsigned op_piece_count[7] = {0}; + bool has_bbishop = false, has_wbishop = false; + bool op_has_bbishop = false, op_has_wbishop = false; + for (Row r = 0 ; r < 8 ; r++) { + for (File f = 0 ; f < 8 ; f++) { + Piece p = piece_at(gamestate, r, f); + if (piece_color(p) == color) { + piece_count[piece_type(p)]++; + if (piece_type(p) == BISHOP) { + if (field_color(r, f) == WHITE) { + has_wbishop = true; + } else { + has_bbishop = true; + } + } + } else { + op_piece_count[piece_type(p)]++; + if (piece_type(p) == BISHOP) { + if (field_color(r, f) == WHITE) { + op_has_wbishop = true; + } else { + op_has_bbishop = true; + } + } + } + } + } + + /* rooks and queens are always enough - don't test them below */ + if (piece_count[ROOK] > 0 || piece_count[QUEEN] > 0) + return false; + + /* only the king left */ + if (piece_count[PAWN] == 0 && piece_count[KNIGHT] == 0 + && piece_count[BISHOP] == 0) + return true; + + /* king + knight and the opponent has only king + queens */ + if (piece_count[PAWN] == 0 && piece_count[BISHOP] == 0 + && piece_count[KNIGHT] == 1 + && op_piece_count[ROOK] == 0 && op_piece_count[BISHOP] == 0 + && op_piece_count[KNIGHT] == 0 && op_piece_count[PAWN] == 0 + && op_piece_count[QUEEN] > 0) + return true; + + /* king + bishop and the opponent doesn't have + * opposite color bishops or knights or pawns */ + if (piece_count[PAWN] == 0 && piece_count[KNIGHT] == 0 + && piece_count[BISHOP] > 0) { + + if (op_piece_count[KNIGHT] > 0 || op_piece_count[PAWN] > 0) + return false; + + if (has_bbishop && op_has_wbishop) + return false; + + if (has_wbishop && op_has_bbishop) + return false; + + return true; + } + + return false; +} + +bool check_no_material(const GameState *gamestate) { + return check_no_material_color(WHITE, gamestate) + && check_no_material_color(BLACK, gamestate); +} + char getpiecechr(Piece piece) { switch (piece_type(piece)) { case ROOK: return 'R'; @@ -417,6 +490,8 @@ /* calculate gamestate flags in order of efficiency */ if (move->checkmate) { gamestate->checkmate = true; + } else if (check_no_material(gamestate)) { + gamestate->nomaterial = true; } else if (check_threefold_repetition(gamestate)) { gamestate->threefold = true; } else if (check_stalemate(gamestate)) {
--- a/src/chess/rules.h Fri Aug 21 16:08:50 2026 +0200 +++ b/src/chess/rules.h Fri Aug 21 17:04:43 2026 +0200 @@ -144,6 +144,8 @@ bool checkmate; bool stalemate; bool threefold; + /** drawn due to insufficient material on both sides */ + bool nomaterial; /** flag is only set when players agreed on remis */ bool remis; bool wresign; @@ -196,7 +198,8 @@ } static inline bool is_game_drawn(const GameState *gamestate) { - return gamestate->threefold || gamestate->stalemate || gamestate->remis; + return gamestate->threefold || gamestate->stalemate + || gamestate->nomaterial || gamestate->remis; } static inline bool is_game_running(const GameState *gamestate) { @@ -208,6 +211,10 @@ return gamestate->moves[gamestate->movecount - 1].check; } +static inline Color field_color(Row r, File f) { + return (r + f) % 2 == 0 ? BLACK : WHITE; +} + /** * Initializes a game state and prepares the chess board. * @param gamestate the game state to initialize @@ -600,5 +607,19 @@ */ bool check_threefold_repetition(const GameState *gamestate); +/** + * Checks if neither side has enough material left to win the game. + * + * Returns true if for both players one of the following three cases are true: + * - they have only the king left + * - they have a king + knight and the opponent has king + queens + * - they have a king + bishop and the opponent doesn't have + * opposite color bishops or knights or pawns + * + * @param gamestate the current game state + * @return true if neither side can win due to insufficient material + */ +bool check_no_material(const GameState *gamestate); + #endif /* RULES_H */
--- a/src/main.c Fri Aug 21 16:08:50 2026 +0200 +++ b/src/main.c Fri Aug 21 17:04:43 2026 +0200 @@ -479,12 +479,10 @@ result = validate_move(gamestate, &move); if (result == VALID_MOVE_SEMANTICS) { apply_move(gamestate, &move); - if (gamestate->checkmate) { + if (is_game_running(gamestate)) { + return 0; + } else { return 1; - } else if (gamestate->stalemate) { - return 1; - } else { - return 0; } } else { eval_move_failed_msg(result); @@ -627,12 +625,13 @@ } else if (code == NETCODE_ACCEPT || code == NETCODE_CHECK || code == NETCODE_CHECKMATE - || code == NETCODE_STALEMATE) { + || code == NETCODE_STALEMATE + || code == NETCODE_NOMATERIAL) { apply_move(gamestate, &move); - if (gamestate->checkmate || gamestate->stalemate) { + if (is_game_running(gamestate)) { + return 0; + } else { return 1; - } else { - return 0; } } else if (code == NETCODE_CONNLOST) { printw("Your opponent left the game."); @@ -772,6 +771,9 @@ } else if (gamestate->stalemate) { net_send_code(opponent, NETCODE_STALEMATE); return 1; + } else if (gamestate->nomaterial) { + net_send_code(opponent, NETCODE_NOMATERIAL); + return 1; } else if (move.check) { net_send_code(opponent, NETCODE_CHECK); } else { @@ -784,17 +786,17 @@ * claim the draw. But since we do it automatically, it makes * no practical difference. */ - if (check_threefold_repetition(gamestate)) { + if (gamestate->threefold) { /* send this as a new package (basically as next move) */ net_send_code(opponent, NETCODE_THREEFOLD); /* the protocol supports declining the claim */ uint8_t resp = net_recieve_code(opponent); if (resp == NETCODE_ACCEPT) { - gamestate->threefold = true; return 1; } else { /* does not happen in our implementation */ - // TODO: somehow add a message to the UI + // TODO: but what do we do if some other client declines? simply rage quit? + // should we instead rework the protocol that it's not possible to decline? return 0; } } else { @@ -848,6 +850,8 @@ addstr("The game ended in a stalemate.\n"); } else if (gamestate->threefold) { addstr("The game was drawn after threefold repetition.\n"); + } else if (gamestate->nomaterial) { + addstr("The game is drawn due to insufficient material.\n"); } else if (gamestate->checkmate) { printw("%s was checkmated.\n", gamestate->movecount % 2 == 0 ? "White" : "Black");
--- a/src/network.h Fri Aug 21 16:08:50 2026 +0200 +++ b/src/network.h Fri Aug 21 17:04:43 2026 +0200 @@ -44,8 +44,9 @@ #define NETCODE_PGNDATA 0x11 #define NETCODE_MOVE 0x20 #define NETCODE_CHECK 0x22 -#define NETCODE_CHECKMATE 0x24 +#define NETCODE_CHECKMATE 0x23 #define NETCODE_STALEMATE 0x28 +#define NETCODE_NOMATERIAL 0x29 #define NETCODE_THREEFOLD 0x30 #define NETCODE_RESIGN 0x41 #define NETCODE_REMIS 0x42 @@ -55,7 +56,7 @@ #define NETCODE_CONNLOST 0x80 #define NETCODE_ERROR 0xFF -#define NETCODE_VERSION 22 +#define NETCODE_VERSION 23 typedef struct { int fd; /* -1, if we are the client */
--- a/test/Makefile Fri Aug 21 16:08:50 2026 +0200 +++ b/test/Makefile Fri Aug 21 17:04:43 2026 +0200 @@ -29,6 +29,7 @@ include ../config.mk SRC = run-tests.c \ + test-rules-helper.c \ test-real-pgn.c OBJ = $(SRC:%.c=$(BUILDDIR)/%.o) @@ -88,3 +89,8 @@ @echo "Compiling $<" $(CC) -o $@ $(CFLAGS) -c $< +$(BUILDDIR)/test-rules-helper.o: test-rules-helper.c ucxtest.h \ + ../src/chess/rules.h + @echo "Compiling $<" + $(CC) -o $@ $(CFLAGS) -c $< +
--- a/test/run-tests.c Fri Aug 21 16:08:50 2026 +0200 +++ b/test/run-tests.c Fri Aug 21 17:04:43 2026 +0200 @@ -30,10 +30,12 @@ #include "ucxtest.h" CxTestSuite *test_real_pgn_suite(void); +CxTestSuite *test_rules_helper_suite(void); int main(void) { CxTestSuite *suites[] = { - test_real_pgn_suite() + test_real_pgn_suite(), + test_rules_helper_suite() }; size_t suites_count = sizeof(suites)/sizeof(CxTestSuite *); unsigned int failure = 0, success = 0; @@ -43,7 +45,7 @@ success += suites[i]->success; cx_test_suite_free(suites[i]); } - printf("\nTotal failure: %u | success: %u\n", failure, success); + printf("\nTotal success: %u | failure: %u\n", success, failure); return failure > 0 ? 1 : 0; }
--- a/test/test-real-pgn.c Fri Aug 21 16:08:50 2026 +0200 +++ b/test/test-real-pgn.c Fri Aug 21 17:04:43 2026 +0200 @@ -53,8 +53,8 @@ CX_TEST_ASSERT(gs->stalemate == (rs == EXPECTED_STALEMATE)); CX_TEST_ASSERT(gs->checkmate == (rs == EXPECTED_CHECKMATE)); CX_TEST_ASSERT(gs->threefold == (rs == EXPECTED_THREEFOLD)); + CX_TEST_ASSERT(gs->nomaterial == (rs == EXPECTED_NOMATERIAL)); CX_TEST_ASSERT(!gs->remis); /* no draw offers in example games */ - // TODO: implement insufficient material detection const Move *last_move = &gs->moves[gs->movecount-1]; CX_TEST_ASSERT((last_move->checkmate == 1) == (rs == EXPECTED_CHECKMATE));
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/test-rules-helper.c Fri Aug 21 17:04:43 2026 +0200 @@ -0,0 +1,59 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2026 Mike Becker. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ + +#include "ucxtest.h" +#include "chess/rules.h" + +static CX_TEST(test_field_color) { + Board colors = { + {BLACK, WHITE, BLACK, WHITE, BLACK, WHITE, BLACK, WHITE}, + {WHITE, BLACK, WHITE, BLACK, WHITE, BLACK, WHITE, BLACK}, + {BLACK, WHITE, BLACK, WHITE, BLACK, WHITE, BLACK, WHITE}, + {WHITE, BLACK, WHITE, BLACK, WHITE, BLACK, WHITE, BLACK}, + {BLACK, WHITE, BLACK, WHITE, BLACK, WHITE, BLACK, WHITE}, + {WHITE, BLACK, WHITE, BLACK, WHITE, BLACK, WHITE, BLACK}, + {BLACK, WHITE, BLACK, WHITE, BLACK, WHITE, BLACK, WHITE}, + {WHITE, BLACK, WHITE, BLACK, WHITE, BLACK, WHITE, BLACK}, + }; + CX_TEST_DO { + for (Row r = 0 ; r < 8 ; r++) { + for (File f = 0 ; f < 8 ; f++) { + CX_TEST_ASSERT(field_color(r, f) == colors[r][f]); + } + } + } +} + +CxTestSuite* test_rules_helper_suite(void) { + CxTestSuite* suite = cx_test_suite_new("Helper Functions in rules.h"); + + cx_test_register(suite, test_field_color); + + return suite; +} \ No newline at end of file