implement draw due to insufficient material default tip

Fri, 21 Aug 2026 17:04:43 +0200

author
Mike Becker <universe@uap-core.de>
date
Fri, 21 Aug 2026 17:04:43 +0200
changeset 179
5ef724e21702
parent 178
724e8acff6f8

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

mercurial