Generalize insufficient material calculation
authorFabian Fichter <ianfab@users.noreply.github.com>
Fri, 19 Jun 2020 22:50:22 +0000 (00:50 +0200)
committerFabian Fichter <ianfab@users.noreply.github.com>
Fri, 19 Jun 2020 22:50:22 +0000 (00:50 +0200)
Closes #139.

src/pyffish.cpp
test.py

index f4bab02..89bf194 100644 (file)
@@ -285,42 +285,36 @@ const std::string move_to_san(Position& pos, Move m, Notation n) {
 
 bool hasInsufficientMaterial(Color c, const Position& pos) {
 
-    if (   pos.captures_to_hand() || pos.count_in_hand(c, ALL_PIECES)
+    // Other win rules
+    if (   pos.captures_to_hand()
+        || pos.count_in_hand(c, ALL_PIECES)
+        || pos.extinction_value() != VALUE_NONE
         || (pos.capture_the_flag_piece() && pos.count(c, pos.capture_the_flag_piece())))
         return false;
 
-    for (PieceType pt : { ROOK, QUEEN, ARCHBISHOP, CHANCELLOR, SILVER })
-        if (pos.count(c, pt) || (pos.count(c, PAWN) && pos.promotion_piece_types().find(pt) != pos.promotion_piece_types().end()))
-            return false;
-
-    // To avoid false positives, treat pawn + anything as sufficient mating material.
-    // This is too conservative for South-East Asian variants.
-    if (pos.count(c, PAWN) && pos.count<ALL_PIECES>() >= 4)
-        return false;
-
-    if (pos.count(c, KNIGHT) >= 2 || (pos.count(c, KNIGHT) && (pos.count(c, BISHOP) || pos.count(c, FERS) || pos.count(c, FERS_ALFIL))))
-        return false;
+    // Restricted pieces
+    Bitboard restricted = pos.pieces(~c, KING);
+    for (PieceType pt : pos.piece_types())
+        if (pt == KING || !(pos.board_bb(c, pt) & pos.board_bb(~c, KING)))
+            restricted |= pos.pieces(c, pt);
 
-    // Check for opposite colored color-bound pieces
-    if (   (pos.count(c, BISHOP) || pos.count(c, FERS_ALFIL))
-        && (DarkSquares & pos.pieces(BISHOP, FERS_ALFIL)) && (~DarkSquares & pos.pieces(BISHOP, FERS_ALFIL)))
-        return false;
-
-    if (pos.count(c, FERS) && (DarkSquares & pos.pieces(FERS)) && (~DarkSquares & pos.pieces(FERS)))
-        return false;
+    // Mating pieces
+    for (PieceType pt : { ROOK, QUEEN, ARCHBISHOP, CHANCELLOR, SILVER, GOLD, COMMONER, CENTAUR })
+        if ((pos.pieces(c, pt) & ~restricted) || (pos.count(c, PAWN) && pos.promotion_piece_types().find(pt) != pos.promotion_piece_types().end()))
+            return false;
 
-    if (pos.pieces(c, CANNON, JANGGI_CANNON) && (pos.count(c, ALL_PIECES) > 2 || pos.count(~c, ALL_PIECES) > 1))
+    // Color-bound pieces
+    Bitboard colorbound = 0, unbound;
+    for (PieceType pt : { BISHOP, FERS, FERS_ALFIL, ALFIL, ELEPHANT })
+        colorbound |= pos.pieces(pt) & ~restricted;
+    unbound = pos.pieces() ^ restricted ^ colorbound;
+    if ((colorbound & pos.pieces(c)) && (((DarkSquares & colorbound) && (~DarkSquares & colorbound)) || unbound))
         return false;
 
-    if (pos.count(c, JANGGI_ELEPHANT) >= 2)
+    // Unbound pieces require one helper piece of either color
+    if ((pos.pieces(c) & unbound) && (popcount(pos.pieces() ^ restricted) >= 2 || pos.stalemate_value() != VALUE_DRAW))
         return false;
 
-    // Pieces sufficient for stalemate (Xiangqi)
-    if (pos.stalemate_value() != VALUE_DRAW)
-        for (PieceType pt : { HORSE, SOLDIER, JANGGI_ELEPHANT })
-            if (pos.count(c, pt))
-                return false;
-
     return true;
 }
 
diff --git a/test.py b/test.py
index 2bce111..11ebe34 100644 (file)
--- a/test.py
+++ b/test.py
@@ -78,6 +78,7 @@ variant_positions = {
         "k7/b1b5/8/8/8/8/8/K7 w - - 0 1": (True, True),  # K vs KBB same color
         "kb6/8/8/8/8/8/8/K1B6 w - - 0 1": (True, True),  # KB vs KB same color
         "kb6/8/8/8/8/8/8/KB7 w - - 0 1": (False, False),  # KB vs KB opp color
+        "8/8/8/8/8/6KN/8/6nk w - - 0 1": (False, False),  # KN vs KN
     },
     "seirawan": {
         "k7/8/8/8/8/8/8/K7[] w - - 0 1": (True, True),  # K vs K