Support virtual piece drops (#122)
authorFabian Fichter <ianfab@users.noreply.github.com>
Sun, 4 Apr 2021 09:08:48 +0000 (11:08 +0200)
committerFabian Fichter <ianfab@users.noreply.github.com>
Sun, 4 Apr 2021 10:17:59 +0000 (12:17 +0200)
Support negative piece counts for bughouse,
and allow virtual piece drops under certain conditions.
This enables the engine to consider the effect of future piece flows,
which is required for more sophisticated communication and strategy.

This significantly improves performance against human opponents,
with only a moderate regression in self-play.

src/evaluate.cpp
src/movegen.cpp
src/movepick.cpp
src/partner.cpp
src/partner.h
src/position.cpp
src/position.h
src/search.cpp
src/thread.cpp
src/types.h

index d3fce55..f7b78f3 100644 (file)
@@ -657,6 +657,11 @@ namespace {
         if (pt == pos.drop_no_doubled())
             score -= make_score(50, 20) * std::max(pos.count_with_hand(Us, pt) - pos.max_file() - 1, 0);
     }
+    else if (pos.count_in_hand(Us, pt) < 0)
+    {
+        // Penalize drops of virtual pieces
+        score += (PSQT::psq[make_piece(WHITE, pt)][SQ_NONE] + make_score(1000, 1000)) * pos.count_in_hand(Us, pt);
+    }
 
     return score;
   }
@@ -1581,6 +1586,10 @@ Value Eval::evaluate(const Position& pos) {
           v += pos.material_counting_result() / (10 * std::max(2 * pos.n_move_rule() - pos.rule50_count(), 1));
   }
 
+  // Guarantee evaluation does not hit the virtual win/loss range
+  if (pos.two_boards() && std::abs(v) >= VALUE_VIRTUAL_MATE_IN_MAX_PLY)
+      v += v > VALUE_ZERO ? MAX_PLY + 1 : -MAX_PLY - 1;
+
   // Guarantee evaluation does not hit the tablebase range
   v = std::clamp(v, VALUE_TB_LOSS_IN_MAX_PLY + 1, VALUE_TB_WIN_IN_MAX_PLY - 1);
 
index 5a473cf..c2cefbc 100644 (file)
@@ -70,9 +70,11 @@ namespace {
     return moveList;
   }
 
-  template<Color Us, bool Checks>
+  template<Color Us, GenType Type>
   ExtMove* generate_drops(const Position& pos, ExtMove* moveList, PieceType pt, Bitboard b) {
-    if (pos.count_in_hand(Us, pt) > 0)
+    assert(Type != CAPTURES);
+    // Do not generate virtual drops for perft and at root
+    if (pos.count_in_hand(Us, pt) > 0 || (Type != NON_EVASIONS && pos.two_boards() && pos.allow_virtual_drop(Us, pt)))
     {
         // Restrict to valid target
         b &= pos.drop_region(Us, pt);
@@ -81,12 +83,12 @@ namespace {
         if (pos.drop_promoted() && pos.promoted_piece_type(pt))
         {
             Bitboard b2 = b;
-            if (Checks)
+            if (Type == QUIET_CHECKS)
                 b2 &= pos.check_squares(pos.promoted_piece_type(pt));
             while (b2)
                 *moveList++ = make_drop(pop_lsb(&b2), pt, pos.promoted_piece_type(pt));
         }
-        if (Checks)
+        if (Type == QUIET_CHECKS || pos.count_in_hand(Us, pt) <= 0)
             b &= pos.check_squares(pt);
         while (b)
             *moveList++ = make_drop(pop_lsb(&b), pt, pt);
@@ -373,9 +375,9 @@ namespace {
         if (pt != PAWN && pt != KING)
             moveList = generate_moves<Checks>(pos, moveList, pt, piecesToMove, target);
     // generate drops
-    if (pos.piece_drops() && Type != CAPTURES && pos.count_in_hand(Us, ALL_PIECES) > 0)
+    if (pos.piece_drops() && Type != CAPTURES && (pos.count_in_hand(Us, ALL_PIECES) > 0 || pos.two_boards()))
         for (PieceType pt : pos.piece_types())
-            moveList = generate_drops<Us, Checks>(pos, moveList, pt, target & ~pos.pieces(~Us));
+            moveList = generate_drops<Us, Type>(pos, moveList, pt, target & ~pos.pieces(~Us));
 
     if (Type != QUIET_CHECKS && Type != EVASIONS && pos.count<KING>(Us))
     {
@@ -553,7 +555,7 @@ ExtMove* generate<LEGAL>(const Position& pos, ExtMove* moveList) {
   moveList = pos.checkers() ? generate<EVASIONS    >(pos, moveList)
                             : generate<NON_EVASIONS>(pos, moveList);
   while (cur != moveList)
-      if (!pos.legal(*cur))
+      if (!pos.legal(*cur) || pos.virtual_drop(*cur))
           *cur = (--moveList)->move;
       else
           ++cur;
index ac1fe6e..5718dc5 100644 (file)
@@ -163,7 +163,7 @@ top:
   case QSEARCH_TT:
   case PROBCUT_TT:
       ++stage;
-      assert(pos.legal(ttMove) == MoveList<LEGAL>(pos).contains(ttMove));
+      assert(pos.legal(ttMove) == MoveList<LEGAL>(pos).contains(ttMove) || pos.virtual_drop(ttMove));
       return ttMove;
 
   case CAPTURE_INIT:
index c53d069..3225255 100644 (file)
@@ -27,7 +27,7 @@
 PartnerHandler Partner; // Global object
 
 void PartnerHandler::reset() {
-    fast = sitRequested = partnerDead = weDead = weWin = false;
+    fast = sitRequested = partnerDead = weDead = weWin = weVirtualWin = weVirtualLoss = false;
     time = opptime = 0;
 }
 
index 8a78882..cf61c03 100644 (file)
@@ -42,7 +42,7 @@ struct PartnerHandler {
     void parse_ptell(std::istringstream& is, const Position& pos);
 
     std::atomic<bool> isFairy;
-    std::atomic<bool> fast, sitRequested, partnerDead, weDead, weWin;
+    std::atomic<bool> fast, sitRequested, partnerDead, weDead, weWin, weVirtualWin, weVirtualLoss;
     std::atomic<TimePoint> time, opptime;
     Move moveRequested;
 };
index 3157db0..cac4968 100644 (file)
@@ -1118,7 +1118,7 @@ bool Position::pseudo_legal(const Move m) const {
       return   piece_drops()
             && pc != NO_PIECE
             && color_of(pc) == us
-            && count_in_hand(us, in_hand_piece_type(m)) > 0
+            && (count_in_hand(us, in_hand_piece_type(m)) > 0 || (two_boards() && allow_virtual_drop(us, type_of(pc))))
             && (drop_region(us, type_of(pc)) & ~pieces() & to)
             && (   type_of(pc) == in_hand_piece_type(m)
                 || (drop_promoted() && type_of(pc) == promoted_piece_type(in_hand_piece_type(m))));
@@ -2443,6 +2443,19 @@ bool Position::is_immediate_game_end(Value& result, int ply) const {
       result = mate_in(ply);
       return true;
   }
+  // Failing to checkmate with virtual pieces is a loss
+  if (two_boards() && !checkers())
+  {
+      int virtualCount = 0;
+      for (PieceType pt : piece_types())
+          virtualCount += std::max(-count_in_hand(~sideToMove, pt), 0);
+
+      if (virtualCount > 0)
+      {
+          result = mate_in(ply);
+          return true;
+      }
+  }
 
   return false;
 }
index bc80fc8..d67b99a 100644 (file)
@@ -196,6 +196,7 @@ public:
   int count_in_hand(Color c, PieceType pt) const;
   int count_with_hand(Color c, PieceType pt) const;
   bool bikjang() const;
+  bool allow_virtual_drop(Color c, PieceType pt) const;
 
   // Position representation
   Bitboard pieces(PieceType pt = ALL_PIECES) const;
@@ -245,6 +246,7 @@ public:
   // Properties of moves
   bool legal(Move m) const;
   bool pseudo_legal(const Move m) const;
+  bool virtual_drop(Move m) const;
   bool capture(Move m) const;
   bool capture_or_promotion(Move m) const;
   bool gives_check(Move m) const;
@@ -792,6 +794,16 @@ inline Value Position::checkmate_value(int ply) const {
       // Niol
       return VALUE_DRAW;
   }
+  // Checkmate using virtual pieces
+  if (two_boards() && var->checkmateValue < VALUE_ZERO)
+  {
+      Value virtualMaterial = VALUE_ZERO;
+      for (PieceType pt : piece_types())
+          virtualMaterial += std::max(-count_in_hand(~sideToMove, pt), 0) * PieceValue[MG][pt];
+
+      if (virtualMaterial > 0)
+          return -VALUE_VIRTUAL_MATE + virtualMaterial / 20 + ply;
+  }
   // Return mate value
   return convert_mate_value(var->checkmateValue, ply);
 }
@@ -1166,6 +1178,11 @@ inline bool Position::capture(Move m) const {
   return (!empty(to_sq(m)) && type_of(m) != CASTLING && from_sq(m) != to_sq(m)) || type_of(m) == EN_PASSANT;
 }
 
+inline bool Position::virtual_drop(Move m) const {
+  assert(is_ok(m));
+  return type_of(m) == DROP && count_in_hand(side_to_move(), in_hand_piece_type(m)) <= 0;
+}
+
 inline Piece Position::captured_piece() const {
   return st->capturedPiece;
 }
@@ -1244,6 +1261,16 @@ inline bool Position::bikjang() const {
   return st->bikjang;
 }
 
+inline bool Position::allow_virtual_drop(Color c, PieceType pt) const {
+  assert(two_boards());
+  // Do we allow a virtual drop?
+  return pt != KING && (   count_in_hand(c, PAWN) >= -(pt == PAWN)
+                        && count_in_hand(c, KNIGHT) >= -(pt == PAWN)
+                        && count_in_hand(c, BISHOP) >= -(pt == PAWN)
+                        && count_in_hand(c, ROOK) >= 0
+                        && count_in_hand(c, QUEEN) >= 0);
+}
+
 inline Value Position::material_counting_result() const {
   auto weigth_count = [this](PieceType pt, int v){ return v * (count(WHITE, pt) - count(BLACK, pt)); };
   int materialCount;
index fbe2ad4..7b68b1a 100644 (file)
@@ -302,10 +302,25 @@ void MainThread::search() {
 
   if (Options["Protocol"] == "xboard")
   {
+      Move bestMove = bestThread->rootMoves[0].pv[0];
+      // Wait for virtual drop to become real
+      if (rootPos.two_boards() && rootPos.virtual_drop(bestMove))
+      {
+          Partner.ptell("fast");
+          while (!Threads.abort && !Partner.partnerDead && !Partner.fast && Limits.time[us] - Time.elapsed() > Partner.opptime)
+          {}
+          Partner.ptell("x");
+          // Find best real move
+          for (const auto& m : this->rootMoves)
+              if (!rootPos.virtual_drop(m.pv[0]))
+              {
+                  bestMove = m.pv[0];
+                  break;
+              }
+      }
       // Send move only when not in analyze mode and not at game end
       if (!Limits.infinite && !ponder && rootMoves[0].pv[0] != MOVE_NONE && !Threads.abort.exchange(true))
       {
-          Move bestMove = bestThread->rootMoves[0].pv[0];
           sync_cout << "move " << UCI::move(rootPos, bestMove) << sync_endl;
           if (XBoard::stateMachine->moveAfterSearch)
           {
@@ -573,30 +588,68 @@ void Thread::search() {
           // Update partner in bughouse variants
           if (completedDepth >= 8 && rootPos.two_boards() && Options["Protocol"] == "xboard")
           {
+              // Communicate clock times relevant for sitting decisions
               if (Limits.time[us])
                   Partner.ptell<FAIRY>("time " + std::to_string((Limits.time[us] - Time.elapsed()) / 10));
               if (Limits.time[~us])
                   Partner.ptell<FAIRY>("otim " + std::to_string(Limits.time[~us] / 10));
+              // We are dead and need to sit
               if (!Partner.weDead && bestValue <= VALUE_MATED_IN_MAX_PLY)
               {
                   Partner.ptell("dead");
                   Partner.weDead = true;
               }
+              // We were dead but are fine again
               else if (Partner.weDead && bestValue > VALUE_MATED_IN_MAX_PLY)
               {
                   Partner.ptell("x");
                   Partner.weDead = false;
               }
+              // We win by force, so partner should sit
               else if (!Partner.weWin && bestValue >= VALUE_MATE_IN_MAX_PLY && Limits.time[~us] < Partner.time)
               {
                   Partner.ptell("sit");
                   Partner.weWin = true;
               }
+              // We are no longer winning
               else if (Partner.weWin && (bestValue < VALUE_MATE_IN_MAX_PLY || Limits.time[~us] > Partner.time))
               {
                   Partner.ptell("x");
                   Partner.weWin = false;
               }
+              // We can win if partner delivers required material quickly
+              else if (  !Partner.weVirtualWin
+                       && bestValue >= VALUE_VIRTUAL_MATE_IN_MAX_PLY
+                       && bestValue <= VALUE_VIRTUAL_MATE
+                       && Limits.time[us] - Time.elapsed() > Partner.opptime)
+              {
+                  Partner.ptell("fast");
+                  Partner.weVirtualWin = true;
+              }
+              // Virtual mate is gone
+              else if (   Partner.weVirtualWin
+                       && (bestValue < VALUE_VIRTUAL_MATE_IN_MAX_PLY || bestValue > VALUE_VIRTUAL_MATE || Limits.time[us] - Time.elapsed() < Partner.opptime))
+              {
+                  Partner.ptell("slow");
+                  Partner.weVirtualWin = false;
+              }
+              // We need to survive a virtual mate and play fast
+              else if (  !Partner.weVirtualLoss
+                       && (bestValue <= -VALUE_VIRTUAL_MATE_IN_MAX_PLY && bestValue >= -VALUE_VIRTUAL_MATE)
+                       && Limits.time[~us] > Partner.time)
+              {
+                  Partner.ptell("sit");
+                  Partner.weVirtualLoss = true;
+                  Partner.fast = true;
+              }
+              // Virtual mate threat is over
+              else if (   Partner.weVirtualLoss
+                       && (bestValue > -VALUE_VIRTUAL_MATE_IN_MAX_PLY || bestValue < -VALUE_VIRTUAL_MATE || Limits.time[~us] < Partner.time))
+              {
+                  Partner.ptell("x");
+                  Partner.weVirtualLoss = false;
+                  Partner.fast = false;
+              }
           }
 
           // Stop the search if we have exceeded the totalTime
@@ -1965,7 +2018,7 @@ void MainThread::check_time() {
 
   if (   rootPos.two_boards()
       && Time.elapsed() < Limits.time[rootPos.side_to_move()] - 1000
-      && (Partner.sitRequested || (Partner.weDead && !Partner.partnerDead)))
+      && (Partner.sitRequested || (Partner.weDead && !Partner.partnerDead) || Partner.weVirtualWin))
       return;
 
   if (   (Limits.use_time_management() && (elapsed > Time.maximum() - 10 || stopOnPonderhit))
@@ -2017,8 +2070,10 @@ string UCI::pv(const Position& pos, Depth depth, Value alpha, Value beta) {
              << nodesSearched * 1000 / elapsed << " "
              << tbHits << "\t";
 
-          for (Move m : rootMoves[i].pv)
-              ss << " " << UCI::move(pos, m);
+          // Do not print PVs with virtual drops in bughouse variants
+          if (!pos.two_boards())
+              for (Move m : rootMoves[i].pv)
+                  ss << " " << UCI::move(pos, m);
       }
       else
       {
index ec9abce..7780d5e 100644 (file)
@@ -20,6 +20,7 @@
 
 #include <algorithm> // For std::count
 #include "movegen.h"
+#include "partner.h"
 #include "search.h"
 #include "thread.h"
 #include "uci.h"
@@ -188,6 +189,23 @@ void ThreadPool::start_thinking(Position& pos, StateListPtr& states,
           && (limits.banmoves.empty() || !std::count(limits.banmoves.begin(), limits.banmoves.end(), m)))
           rootMoves.emplace_back(m);
 
+  // Add virtual drops
+  if (pos.two_boards() && Partner.opptime && limits.time[pos.side_to_move()] > Partner.opptime + 1000)
+  {
+      if (pos.checkers())
+      {
+          for (const auto& m : MoveList<EVASIONS>(pos))
+              if (pos.virtual_drop(m) && pos.legal(m))
+                  rootMoves.emplace_back(m);
+      }
+      else
+      {
+          for (const auto& m : MoveList<QUIETS>(pos))
+              if (pos.virtual_drop(m) && pos.legal(m))
+                  rootMoves.emplace_back(m);
+      }
+  }
+
   if (!rootMoves.empty())
       Tablebases::rank_root_moves(pos, rootMoves);
 
index 5f501bc..503e340 100644 (file)
@@ -324,6 +324,8 @@ enum Value : int {
   VALUE_KNOWN_WIN = 10000,
   VALUE_MATE      = 32000,
   XBOARD_VALUE_MATE = 200000,
+  VALUE_VIRTUAL_MATE = 3000,
+  VALUE_VIRTUAL_MATE_IN_MAX_PLY = VALUE_VIRTUAL_MATE - MAX_PLY,
   VALUE_INFINITE  = 32001,
   VALUE_NONE      = 32002,