Refactor capture the flag implementation
authorFabian Fichter <ianfab@users.noreply.github.com>
Sat, 22 Apr 2023 14:17:31 +0000 (16:17 +0200)
committerFabian Fichter <ianfab@users.noreply.github.com>
Sat, 22 Apr 2023 15:00:50 +0000 (17:00 +0200)
Simplify and generalize implementation to support color-specific
flag pieces as well as allowing all pieces to be eligible.

src/apiutil.h
src/evaluate.cpp
src/nnue/features/half_ka_v2_variants.cpp
src/parser.cpp
src/position.cpp
src/position.h
src/search.cpp
src/variant.cpp
src/variant.h
src/variants.ini
tests/perft.sh

index 988ff5e..d753579 100644 (file)
@@ -369,7 +369,7 @@ inline bool has_insufficient_material(Color c, const Position& pos) {
     if (   pos.captures_to_hand()
         || pos.count_in_hand(c, ALL_PIECES)
         || (pos.extinction_value() != VALUE_NONE && !pos.extinction_pseudo_royal())
-        || (pos.capture_the_flag_piece() && pos.count(c, pos.capture_the_flag_piece())))
+        || (pos.flag_region(c) && pos.count(c, pos.flag_piece(c))))
         return false;
 
     // Restricted pieces
index c76f2cc..8582766 100644 (file)
@@ -1163,8 +1163,8 @@ namespace {
     int weight = pos.count<ALL_PIECES>(Us) - 3 + std::min(pe->blocked_count(), 9);
     Score score = make_score(bonus * weight * weight / 16, 0);
 
-    if (pos.capture_the_flag(Us))
-        score += make_score(200, 200) * popcount(behind & safe & pos.capture_the_flag(Us));
+    if (pos.flag_region(Us))
+        score += make_score(200, 200) * popcount(behind & safe & pos.flag_region(Us));
 
     if constexpr (T)
         Trace::add(SPACE, Us, score);
@@ -1184,11 +1184,10 @@ namespace {
     Score score = SCORE_ZERO;
 
     // Capture the flag
-    if (pos.capture_the_flag(Us))
+    if (pos.flag_region(Us))
     {
-        PieceType ptCtf = pos.capture_the_flag_piece();
-        Bitboard ctfPieces = pos.pieces(Us, ptCtf);
-        Bitboard ctfTargets = pos.capture_the_flag(Us) & pos.board_bb();
+        Bitboard ctfPieces = pos.pieces(Us, pos.flag_piece(Us));
+        Bitboard ctfTargets = pos.flag_region(Us) & pos.board_bb();
         Bitboard onHold = 0;
         Bitboard onHold2 = 0;
         Bitboard processed = 0;
@@ -1201,6 +1200,8 @@ namespace {
         // Traverse all paths of the CTF pieces to the CTF targets.
         // Put squares that are attacked or occupied on hold for one iteration.
         // This reflects that likely a move will be needed to block or capture the attack.
+        // If all piece types are eligible, use the king path as a proxy for distance.
+        PieceType ptCtf = pos.flag_piece(Us) == ALL_PIECES ? KING : pos.flag_piece(Us);
         for (int dist = 0; (ctfPieces || onHold || onHold2) && (ctfTargets & ~processed); dist++)
         {
             int wins = popcount(ctfTargets & ctfPieces);
index 4d23de0..1e0fd6e 100644 (file)
@@ -32,7 +32,7 @@ namespace Stockfish::Eval::NNUE::Features {
   // Orient a square according to perspective (rotates by 180 for black)
   // Missing kings map to index 0 (SQ_A1)
   inline Square HalfKAv2Variants::orient(Color perspective, Square s, const Position& pos) {
-    return s != SQ_NONE ? to_variant_square(  perspective == WHITE || (pos.capture_the_flag(BLACK) & Rank8BB) ? s
+    return s != SQ_NONE ? to_variant_square(  perspective == WHITE || (pos.flag_region(BLACK) & Rank8BB) ? s
                                             : flip_rank(s, pos.max_rank()), pos) : SQ_A1;
   }
 
index d7eab8e..38b253e 100644 (file)
@@ -446,7 +446,12 @@ Variant* VariantParser<DoCheck>::parse(Variant* v) {
     parse_attribute("extinctionPieceTypes", v->extinctionPieceTypes, v->pieceToChar);
     parse_attribute("extinctionPieceCount", v->extinctionPieceCount);
     parse_attribute("extinctionOpponentPieceCount", v->extinctionOpponentPieceCount);
-    parse_attribute("flagPiece", v->flagPiece, v->pieceToChar);
+    parse_attribute("flagPiece", v->flagPiece[WHITE], v->pieceToChar);
+    parse_attribute("flagPiece", v->flagPiece[BLACK], v->pieceToChar);
+    parse_attribute("flagPieceWhite", v->flagPiece[WHITE], v->pieceToChar);
+    parse_attribute("flagPieceBlack", v->flagPiece[BLACK], v->pieceToChar);
+    parse_attribute("flagRegion", v->flagRegion[WHITE]);
+    parse_attribute("flagRegion", v->flagRegion[BLACK]);
     parse_attribute("flagRegionWhite", v->flagRegion[WHITE]);
     parse_attribute("flagRegionBlack", v->flagRegion[BLACK]);
     parse_attribute("flagPieceCount", v->flagPieceCount);
index 0e52486..6084c4d 100644 (file)
@@ -2673,64 +2673,28 @@ bool Position::is_immediate_game_end(Value& result, int ply) const {
           }
   }
   // capture the flag
-  if (   capture_the_flag_piece()
-      && flag_move()
-      && (
-           (popcount(capture_the_flag(sideToMove) & pieces(sideToMove, capture_the_flag_piece()))>=flag_piece_count()) // opponent has >= number of pieces needed to win
-           || //-or-
-           (
-             (flag_piece_blocked_win()) //flagPieceBlockedWin variant option true
-             && //-and-
-             (capture_the_flag(sideToMove) & pieces(sideToMove, capture_the_flag_piece())) //at least one piece in flag zone
-             && //-and-
-             !(capture_the_flag(sideToMove) & ~pieces()) //no empty squares in flag zone
-           )
-         )
-     )
-  {
-      result =
-           (
-             (
-               (popcount(capture_the_flag(~sideToMove) & pieces(~sideToMove, capture_the_flag_piece()))>=flag_piece_count()) // you have >= number of pieces needed to win
-               || //-or-
-               (
-                 (flag_piece_blocked_win()) //flagPieceBlockedWin variant option true
-                 && //-and-
-                 (capture_the_flag(~sideToMove) & pieces(~sideToMove, capture_the_flag_piece())) //at least one piece in flag zone
-                 && //-and-
-                 !(capture_the_flag(~sideToMove) & ~pieces()) //no empty squares in flag zone
-               )
-             )
-             &&
-               (sideToMove == WHITE) //opponent is white
-           )
-           ? VALUE_DRAW : mate_in(ply); //then it's a draw, otherwise, win
+  // A flag win by the side to move is only possible if flagMove is enabled
+  // and they already reached the flag region the move before.
+  // In the case both colors reached it, it is a draw if white was first.
+  if (flag_move() && flag_reached(sideToMove))
+  {
+      result = sideToMove == WHITE && flag_reached(BLACK) ? VALUE_DRAW : mate_in(ply);
       return true;
   }
-  if (   capture_the_flag_piece()
-      && (!flag_move() || capture_the_flag_piece() == KING) //if black doesn't get an extra move to draw, or flag piece is king,
-      && ( //-and-
-           (popcount(capture_the_flag(~sideToMove) & pieces(~sideToMove, capture_the_flag_piece()))>=flag_piece_count()) // you have >= number of pieces needed to win
-           || //-or-
-           (
-               (flag_piece_blocked_win()) //flagPieceBlockedWin variant option true
-             && //-and-
-               (capture_the_flag(~sideToMove) & pieces(~sideToMove, capture_the_flag_piece())) //at least one piece in flag zone
-             && //-and-
-               !(capture_the_flag(~sideToMove) & ~pieces()) //no empty squares in flag zone
-           )
-         )
-     )
+  // A direct flag win is possible if the opponent does not get an extra flag move
+  // or we can detect early for kings that they won't be able to reach the flag region
+  // Note: This condition has to be after the above, since both might be true e.g. in racing kings.
+  if (   (!flag_move() || flag_piece(sideToMove) == KING) // we can do early win detection only for the king
+       && flag_reached(~sideToMove))
   {
       bool gameEnd = true;
-      // Check whether king can move to CTF zone
+      // Check whether king can move to CTF zone (racing kings) to draw
       if (   flag_move() && sideToMove == BLACK && !checkers() && count<KING>(sideToMove)
-          && (capture_the_flag(sideToMove) & attacks_from(sideToMove, KING, square<KING>(sideToMove))))
+          && (flag_region(sideToMove) & attacks_from(sideToMove, KING, square<KING>(sideToMove))))
       {
-          assert(capture_the_flag_piece() == KING);
-          gameEnd = true;
+          assert(flag_piece(sideToMove) == KING);
           for (const auto& m : MoveList<NON_EVASIONS>(*this))
-              if (type_of(moved_piece(m)) == KING && (capture_the_flag(sideToMove) & to_sq(m)) && legal(m))
+              if (type_of(moved_piece(m)) == KING && (flag_region(sideToMove) & to_sq(m)) && legal(m))
               {
                   gameEnd = false;
                   break;
index 6cd9f6b..381f6fd 100644 (file)
@@ -197,12 +197,11 @@ public:
   int extinction_piece_count() const;
   int extinction_opponent_piece_count() const;
   bool extinction_pseudo_royal() const;
-  PieceType capture_the_flag_piece() const;
-  Bitboard capture_the_flag(Color c) const;
+  PieceType flag_piece(Color c) const;
+  Bitboard flag_region(Color c) const;
   bool flag_move() const;
+  bool flag_reached(Color c) const;
   bool check_counting() const;
-  int flag_piece_count() const;
-  bool flag_piece_blocked_win() const;
   int connect_n() const;
   CheckCount checks_remaining(Color c) const;
   MaterialCounting material_counting() const;
@@ -917,12 +916,12 @@ inline bool Position::extinction_pseudo_royal() const {
   return var->extinctionPseudoRoyal;
 }
 
-inline PieceType Position::capture_the_flag_piece() const {
+inline PieceType Position::flag_piece(Color c) const {
   assert(var != nullptr);
-  return var->flagPiece;
+  return var->flagPiece[c];
 }
 
-inline Bitboard Position::capture_the_flag(Color c) const {
+inline Bitboard Position::flag_region(Color c) const {
   assert(var != nullptr);
   return var->flagRegion[c];
 }
@@ -932,14 +931,11 @@ inline bool Position::flag_move() const {
   return var->flagMove;
 }
 
-inline int Position::flag_piece_count() const {
+inline bool Position::flag_reached(Color c) const {
   assert(var != nullptr);
-  return var->flagPieceCount;
-}
-
-inline bool Position::flag_piece_blocked_win() const {
-  assert(var != nullptr);
-  return var->flagPieceBlockedWin;
+  return   (flag_region(c) & pieces(c, flag_piece(c)))
+        && (   popcount(flag_region(c) & pieces(c, flag_piece(c))) >= var->flagPieceCount
+            || (var->flagPieceBlockedWin && !(flag_region(c) & ~pieces())));
 }
 
 inline bool Position::check_counting() const {
index e1c34c9..fc7c4f8 100644 (file)
@@ -982,7 +982,7 @@ namespace {
         }
     }
 
-    probCutBeta = beta + (209 + 20 * !!pos.capture_the_flag_piece() + 50 * pos.captures_to_hand()) * (1 + pos.check_counting() + pos.extinction_single_piece()) - 44 * improving;
+    probCutBeta = beta + (209 + 20 * !!pos.flag_region(~pos.side_to_move()) + 50 * pos.captures_to_hand()) * (1 + pos.check_counting() + pos.extinction_single_piece()) - 44 * improving;
 
     // Step 9. ProbCut (~4 Elo)
     // If we have a good enough capture and a reduced search returns a value
@@ -1185,7 +1185,7 @@ moves_loop: // When in check, search starts from here
                   continue;
 
               // Prune moves with negative SEE (~20 Elo)
-              if (!pos.variant()->duckGating && !pos.see_ge(move, Value(-(30 - std::min(lmrDepth, 18) + 10 * !!pos.capture_the_flag_piece()) * lmrDepth * lmrDepth)))
+              if (!pos.variant()->duckGating && !pos.see_ge(move, Value(-(30 - std::min(lmrDepth, 18) + 10 * !!pos.flag_region(pos.side_to_move())) * lmrDepth * lmrDepth)))
                   continue;
           }
       }
index 6a020a7..6332047 100644 (file)
@@ -325,7 +325,7 @@ namespace {
     // https://lichess.org/variant/kingOfTheHill
     Variant* kingofthehill_variant() {
         Variant* v = chess_variant_base()->init();
-        v->flagPiece = KING;
+        v->flagPiece[WHITE] = v->flagPiece[BLACK] = KING;
         v->flagRegion[WHITE] = (Rank4BB | Rank5BB) & (FileDBB | FileEBB);
         v->flagRegion[BLACK] = (Rank4BB | Rank5BB) & (FileDBB | FileEBB);
         v->flagMove = false;
@@ -336,7 +336,7 @@ namespace {
     Variant* racingkings_variant() {
         Variant* v = chess_variant_base()->init();
         v->startFen = "8/8/8/8/8/8/krbnNBRK/qrbnNBRQ w - - 0 1";
-        v->flagPiece = KING;
+        v->flagPiece[WHITE] = v->flagPiece[BLACK] = KING;
         v->flagRegion[WHITE] = Rank8BB;
         v->flagRegion[BLACK] = Rank8BB;
         v->flagMove = true;
@@ -549,7 +549,7 @@ namespace {
         v->add_piece(CUSTOM_PIECE_2, 'f', "mF"); //Fox
         v->startFen = "1h1h1h1h/8/8/8/8/8/8/4F3 w - - 0 1";
         v->stalemateValue = -VALUE_MATE;
-        v->flagPiece = CUSTOM_PIECE_2;
+        v->flagPiece[WHITE] = CUSTOM_PIECE_2;
         v->flagRegion[WHITE] = Rank8BB;
         return v;
     }
@@ -845,7 +845,7 @@ namespace {
         v->mandatoryPiecePromotion = true;
         v->immobilityIllegal = false;
         v->shogiPawnDropMateIllegal = false;
-        v->flagPiece = KING;
+        v->flagPiece[WHITE] = v->flagPiece[BLACK] = KING;
         v->flagRegion[WHITE] = Rank4BB;
         v->flagRegion[BLACK] = Rank1BB;
         v->dropNoDoubled = NO_PIECE_TYPE;
@@ -1091,7 +1091,7 @@ namespace {
         v->doubleStep = false;
         v->castling = false;
         v->stalemateValue = -VALUE_MATE;
-        v->flagPiece = BREAKTHROUGH_PIECE;
+        v->flagPiece[WHITE] = v->flagPiece[BLACK] = BREAKTHROUGH_PIECE;
         v->flagRegion[WHITE] = Rank8BB;
         v->flagRegion[BLACK] = Rank1BB;
         return v;
@@ -1422,7 +1422,7 @@ namespace {
         v->doubleStep = false;
         v->castling = false;
         v->stalemateValue = -VALUE_MATE;
-        v->flagPiece = KNIGHT;
+        v->flagPiece[WHITE] = v->flagPiece[BLACK] = KNIGHT;
         v->flagRegion[WHITE] = make_bitboard(SQ_E5);
         v->flagRegion[BLACK] = make_bitboard(SQ_E5);
         v->flagMove = true;
index 891b039..3e5dcb5 100644 (file)
@@ -141,7 +141,7 @@ struct Variant {
   PieceSet extinctionPieceTypes = NO_PIECE_SET;
   int extinctionPieceCount = 0;
   int extinctionOpponentPieceCount = 0;
-  PieceType flagPiece = NO_PIECE_TYPE;
+  PieceType flagPiece[COLOR_NB] = {ALL_PIECES, ALL_PIECES};
   Bitboard flagRegion[COLOR_NB] = {};
   int flagPieceCount = 1;
   bool flagPieceBlockedWin = false;
@@ -323,7 +323,7 @@ struct Variant {
                     && checkmateValue == -VALUE_MATE
                     && stalemateValue == VALUE_DRAW
                     && !materialCounting
-                    && !flagPiece
+                    && !(flagRegion[WHITE] || flagRegion[BLACK])
                     && !mustCapture
                     && !checkCounting
                     && !makpongRule
index c882be3..b581fcc 100644 (file)
 # extinctionPieceTypes: list of piece types for extinction rules, e.g., pnbrq (* means all) (default: )
 # extinctionPieceCount: piece count at which the game is decided by extinction rule (default: 0)
 # extinctionOpponentPieceCount: opponent piece count required to adjudicate by extinction rule (default: 0)
-# flagPiece: piece type for capture the flag win rule [PieceType] (default: -)
+# flagPiece: piece type for capture the flag win rule [PieceType] (default: *)
+# flagPieceWhite: piece type for capture the flag win rule [PieceType] (default: *)
+# flagPieceBlack: piece type for capture the flag win rule [PieceType] (default: *)
+# flagRegion: target region for capture the flag win rule [Bitboard] (default: )
 # flagRegionWhite: white's target region for capture the flag win rule [Bitboard] (default: )
 # flagRegionBlack: black's target region for capture the flag win rule [Bitboard] (default: )
-# flagMove: black gets one more move after white captures the flag [bool] (default: false)
-# checkCounting: enable check count win rule (check count is communicated via FEN, see 3check) [bool] (default: false)
 # flagPieceCount: number of flag pieces that have to be in the flag zone [int] (default: 1)
-# flagPieceBlockedWin: for flagPieceCount > 1. if at least one piece in flag zone and all others occupied by opponent pieces, win. [bool] (default: false)
+# flagPieceBlockedWin: for flagPieceCount > 1, win if at least one flag piece in flag zone and all others occupied by pieces [bool] (default: false)
+# flagMove: the other side gets one more move after one reaches the flag zone [bool] (default: false)
+# checkCounting: enable check count win rule (check count is communicated via FEN, see 3check) [bool] (default: false)
 # connectN: number of aligned pieces for win [int] (default: 0)
 # materialCounting: enable material counting rules [MaterialCounting] (default: none)
 # countingRule: enable counting rules [CountingRule] (default: none)
@@ -310,8 +313,7 @@ blastOnCapture = true
 [atomic-giveaway-hill:giveaway]
 blastOnCapture = true
 flagPiece = k
-flagRegionWhite = d4 e4 d5 e5
-flagRegionBlack = d4 e4 d5 e5
+flagRegion = d4 e4 d5 e5
 
 # Crazyhouse with mandatory captures, using crazyhouse as a template
 [coffeehouse:crazyhouse]
index 8111ee7..718e072 100755 (executable)
@@ -186,6 +186,7 @@ if [[ $1 == "all" ||  $1 == "largeboard" ]]; then
   expect perft.exp janggi "fen 1n1kaabn1/cr2N4/5C1c1/p1pNp3p/9/9/P1PbP1P1P/3r1p3/4A4/R1BA1KB1R b - - 0 1" 4 76763 > /dev/null
   expect perft.exp janggi "fen 1Pbcka3/3nNn1c1/N2CaC3/1pB6/9/9/5P3/9/4K4/9 w - - 0 23" 4 151202 > /dev/null
   expect perft.exp jesonmor startpos 3 27960 > /dev/null
+  expect perft.exp jesonmor "fen nn1nnn1nn/9/3n1n3/9/9/9/3N1N3/9/NN1NNN1NN w - - 4 3" 3 37564 > /dev/null
 
   # non-chess
   expect perft.exp flipello10 startpos 7 55180 > /dev/null