Support XBoard protocol (close #37)
authorFabian Fichter <ianfab@users.noreply.github.com>
Fri, 1 Nov 2019 20:36:31 +0000 (21:36 +0100)
committerFabian Fichter <ianfab@users.noreply.github.com>
Sun, 3 Nov 2019 16:29:18 +0000 (17:29 +0100)
src/search.cpp
src/thread.h
src/types.h
src/uci.cpp
src/uci.h
src/ucioption.cpp
src/variant.cpp

index af82089..15fd1e6 100644 (file)
@@ -228,9 +228,17 @@ void MainThread::search() {
   {
       rootMoves.emplace_back(MOVE_NONE);
       Value variantResult;
+      Value result =  rootPos.is_game_end(variantResult) ? variantResult
+                    : rootPos.checkers()                 ? rootPos.checkmate_value()
+                                                         : rootPos.stalemate_value();
+      if (Options["Protocol"] == "xboard")
+          sync_cout << (  result == VALUE_DRAW ? "1/2-1/2 {Draw}"
+                        : (rootPos.side_to_move() == BLACK ? -result : result) == VALUE_MATE ? "1-0 {White wins}"
+                        : "0-1 {Black wins}")
+                    << sync_endl;
+      else
       sync_cout << "info depth 0 score "
-                << UCI::value(  rootPos.is_game_end(variantResult) ? variantResult
-                              : rootPos.checkers() ? rootPos.checkmate_value() : rootPos.stalemate_value())
+                << UCI::value(result)
                 << sync_endl;
   }
   else
@@ -268,7 +276,7 @@ void MainThread::search() {
   if (Limits.npmsec)
       Time.availableNodes += Limits.inc[us] - Threads.nodes_searched();
 
-  Thread* bestThread = this;
+  bestThread = this;
 
   // Check if there are threads with a better score than main thread
   if (    Options["MultiPV"] == 1
@@ -307,6 +315,14 @@ void MainThread::search() {
   if (bestThread != this)
       sync_cout << UCI::pv(bestThread->rootPos, bestThread->completedDepth, -VALUE_INFINITE, VALUE_INFINITE) << sync_endl;
 
+  if (Options["Protocol"] == "xboard")
+  {
+      // Send move only when not in analyze mode and not at game end
+      if (!Options["UCI_AnalyseMode"] && rootMoves[0].pv[0] != MOVE_NONE)
+          sync_cout << "move " << UCI::move(rootPos, bestThread->rootMoves[0].pv[0]) << sync_endl;
+      return;
+  }
+
   sync_cout << "bestmove " << UCI::move(rootPos, bestThread->rootMoves[0].pv[0]);
 
   if (bestThread->rootMoves[0].pv.size() > 1 || bestThread->rootMoves[0].extract_ponder_from_tt(rootPos))
@@ -962,7 +978,7 @@ moves_loop: // When in check, search starts from here
 
       ss->moveCount = ++moveCount;
 
-      if (rootNode && thisThread == Threads.main() && Time.elapsed() > 3000)
+      if (rootNode && thisThread == Threads.main() && Time.elapsed() > 3000 && Options["Protocol"] != "xboard")
           sync_cout << "info depth " << depth
                     << " currmove " << UCI::move(pos, move)
                     << " currmovenumber " << moveCount + thisThread->pvIdx << sync_endl;
@@ -1769,6 +1785,21 @@ string UCI::pv(const Position& pos, Depth depth, Value alpha, Value beta) {
       if (ss.rdbuf()->in_avail()) // Not at first line
           ss << "\n";
 
+      if (Options["Protocol"] == "xboard")
+      {
+          ss << d << " "
+             << UCI::value(v) << " "
+             << elapsed / 10 << " "
+             << nodesSearched << " "
+             << rootMoves[i].selDepth << " "
+             << nodesSearched * 1000 / elapsed << " "
+             << tbHits << "\t";
+
+          for (Move m : rootMoves[i].pv)
+              ss << " " << UCI::move(pos, m);
+      }
+      else
+      {
       ss << "info"
          << " depth "    << d
          << " seldepth " << rootMoves[i].selDepth
@@ -1790,6 +1821,7 @@ string UCI::pv(const Position& pos, Depth depth, Value alpha, Value beta) {
 
       for (Move m : rootMoves[i].pv)
           ss << " " << UCI::move(pos, m);
+      }
   }
 
   return ss.str();
index 0517afc..418aeae 100644 (file)
@@ -90,6 +90,7 @@ struct MainThread : public Thread {
   int callsCnt;
   bool stopOnPonderhit;
   std::atomic_bool ponder;
+  Thread* bestThread; // to fetch best move when in XBoard mode
 };
 
 
@@ -109,9 +110,9 @@ struct ThreadPool : public std::vector<Thread*> {
 
   std::atomic_bool stop;
 
-private:
   StateListPtr setupStates;
 
+private:
   uint64_t accumulate(std::atomic<uint64_t> Thread::* member) const {
 
     uint64_t sum = 0;
index be57825..b7ccc21 100644 (file)
@@ -303,6 +303,7 @@ enum Value : int {
   VALUE_DRAW      = 0,
   VALUE_KNOWN_WIN = 10000,
   VALUE_MATE      = 32000,
+  XBOARD_VALUE_MATE = 100000,
   VALUE_INFINITE  = 32001,
   VALUE_NONE      = 32002,
 
index 21970f2..ed61569 100644 (file)
@@ -133,6 +133,12 @@ namespace {
     Threads.start_thinking(pos, states, limits, ponderMode);
   }
 
+  void xboard_go(Position& pos, Search::LimitsType limits, StateListPtr& states) {
+
+    limits.startTime = now(); // As early as possible!
+
+    Threads.start_thinking(pos, states, limits, false);
+  }
 
   // bench() is called when engine receives the "bench" command. Firstly
   // a list of UCI commands is setup according to bench parameters, then
@@ -185,6 +191,34 @@ namespace {
         Options["VariantPath"] = token;
   }
 
+  // do_move() is called when engine needs to change position state in XBoard mode.
+
+  void do_move(Position& pos, std::deque<Move>& moveList, StateListPtr& states, Move m) {
+
+    // transfer states back
+    if (Threads.setupStates.get())
+        states = std::move(Threads.setupStates);
+
+    if (m == MOVE_NONE)
+        return;
+    moveList.push_back(m);
+    states->emplace_back();
+    pos.do_move(m, states->back());
+  }
+
+  // undo_move() is called when engine needs to change position state in XBoard mode.
+
+  void undo_move(Position& pos, std::deque<Move>& moveList, StateListPtr& states) {
+
+    // transfer states back
+    if (Threads.setupStates.get())
+        states = std::move(Threads.setupStates);
+
+    pos.undo_move(moveList.back());
+    states->pop_back();
+    moveList.pop_back();
+  }
+
 } // namespace
 
 
@@ -206,6 +240,14 @@ void UCI::loop(int argc, char* argv[]) {
   for (int i = 1; i < argc; ++i)
       cmd += std::string(argv[i]) + " ";
 
+  // XBoard states
+  Color playColor = COLOR_NB;
+  bool move_after_search = false;
+  Search::LimitsType limits;
+  Search::LimitsType analysis_limits;
+  analysis_limits.infinite = 1;
+  std::deque<Move> moveList = std::deque<Move>();
+
   do {
       if (argc == 1 && !getline(cin, cmd)) // Block here waiting for input or EOF
           cmd = "quit";
@@ -226,12 +268,169 @@ void UCI::loop(int argc, char* argv[]) {
       else if (token == "ponderhit")
           Threads.main()->ponder = false; // Switch to normal search
 
-      else if (token == "uci" || token == "usi")
+      else if (token == "uci" || token == "usi" || token == "xboard")
       {
           Options["Protocol"] = token;
-          sync_cout << "id name " << engine_info(true)
-                    << "\n"       << Options
-                    << "\n" << token << "ok"  << sync_endl;
+          if (token != "xboard")
+              sync_cout << "id name " << engine_info(true)
+                          << "\n"       << Options
+                          << "\n" << token << "ok"  << sync_endl;
+      }
+
+      else if (Options["Protocol"] == "xboard")
+      {
+          if (move_after_search)
+          {
+              Threads.stop = true;
+              Threads.main()->wait_for_search_finished();
+              do_move(pos, moveList, states, Threads.main()->bestThread->rootMoves[0].pv[0]);
+              move_after_search = false;
+          }
+          if (token == "protover")
+          {
+              string vars = "chess";
+              for (string v : variants.get_keys())
+                  if (v != "chess")
+                      vars += "," + v;
+              sync_cout << "feature setboard=1 usermove=1 memory=1 smp=1 colors=0 draw=0 name=0 sigint=0 myname=Fairy-Stockfish variants=\"" << vars << "\""
+                        << Options << sync_endl
+                        << "feature done=1" << sync_endl;
+          }
+          else if (token == "accepted" || token == "rejected" || token == "result" || token == "?") {}
+          else if (token == "new")
+          {
+              is = istringstream("startpos");
+              position(pos, is, states);
+              // play second by default
+              playColor = ~pos.side_to_move();
+          }
+          else if (token == "variant")
+          {
+              if (is >> token)
+                  Options["UCI_Variant"] = token;
+              is = istringstream("startpos");
+              position(pos, is, states);
+          }
+          else if (token == "force")
+              playColor = COLOR_NB;
+          else if (token == "go")
+          {
+              playColor = pos.side_to_move();
+              xboard_go(pos, limits, states);
+              move_after_search = true;
+          }
+          else if (token == "level" || token == "st" || token == "sd" || token == "time" || token == "otim")
+          {
+              int num;
+              if (token == "level")
+              {
+                  // moves to go
+                  is >> limits.movestogo;
+                  // base time
+                  is >> token;
+                  size_t idx = token.find(":");
+                  if (idx != string::npos)
+                      num = std::stoi(token.substr(0, idx)) * 60 + std::stoi(token.substr(idx + 1));
+                  else
+                      num = std::stoi(token) * 60;
+                  limits.time[WHITE] = num * 1000;
+                  limits.time[BLACK] = num * 1000;
+                  // increment
+                  is >> num;
+                  limits.inc[WHITE] = num * 1000;
+                  limits.inc[BLACK] = num * 1000;
+              }
+              else if (token == "sd")
+                is >> limits.depth;
+              else if (token == "st")
+                is >> limits.movetime;
+              // Note: time/otim are in centi-, not milliseconds
+              else if (token == "time")
+              {
+                  is >> num;
+                  limits.time[playColor != COLOR_NB ? playColor : pos.side_to_move()] = num * 10;
+              }
+              else if (token == "otim")
+              {
+                  is >> num;
+                  limits.time[playColor != COLOR_NB ? ~playColor : ~pos.side_to_move()] = num * 10;
+              }
+          }
+          else if (token == "setboard")
+          {
+              std::getline(is, token);
+              is = istringstream("fen" + token);
+              position(pos, is, states);
+          }
+          else if (token == "cores")
+          {
+              if (is >> token)
+                  Options["Threads"] = token;
+          }
+          else if (token == "memory")
+          {
+              if (is >> token)
+                  Options["Hash"] = token;
+          }
+          else if (token == "hard" || token == "easy")
+              Options["Ponder"] = token == "hard";
+          else if (token == "option")
+          {
+              string name, value;
+              is.get();
+              std::getline(is, name, '=');
+              std::getline(is, value);
+              if (Options.count(name))
+                  Options[name] = value;
+          }
+          else if (token == "analyze")
+          {
+              Options["UCI_AnalyseMode"] = string("true");
+              xboard_go(pos, analysis_limits, states);
+          }
+          else if (token == "exit")
+          {
+              Threads.stop = true;
+              Threads.main()->wait_for_search_finished();
+              Options["UCI_AnalyseMode"] = string("false");
+          }
+          else if (token == "undo")
+          {
+              if (moveList.size())
+              {
+                  if (Options["UCI_AnalyseMode"])
+                  {
+                      Threads.stop = true;
+                      Threads.main()->wait_for_search_finished();
+                  }
+                  undo_move(pos, moveList, states);
+                  if (Options["UCI_AnalyseMode"])
+                      xboard_go(pos, analysis_limits, states);
+              }
+          }
+          else
+          {
+              // process move string
+              if (token == "usermove")
+                  is >> token;
+              if (Options["UCI_AnalyseMode"])
+              {
+                  Threads.stop = true;
+                  Threads.main()->wait_for_search_finished();
+              }
+              Move m;
+              if ((m = UCI::to_move(pos, token)) != MOVE_NONE)
+                  do_move(pos, moveList, states, m);
+              else
+                  sync_cout << "Error (unkown command): " << token << sync_endl;
+              if (Options["UCI_AnalyseMode"])
+                  xboard_go(pos, analysis_limits, states);
+              else if (pos.side_to_move() == playColor)
+              {
+                  xboard_go(pos, limits, states);
+                  move_after_search = true;
+              }
+          }
       }
 
       else if (token == "setoption")  setoption(is);
@@ -267,6 +466,14 @@ string UCI::value(Value v) {
 
   stringstream ss;
 
+  if (Options["Protocol"] == "xboard")
+  {
+      if (abs(v) < VALUE_MATE - MAX_PLY)
+          ss << v * 100 / PawnValueEg;
+      else
+          ss << (v > 0 ? XBOARD_VALUE_MATE + VALUE_MATE - v + 1 : -XBOARD_VALUE_MATE - VALUE_MATE - v - 1) / 2;
+  } else
+
   if (abs(v) < VALUE_MATE - MAX_PLY)
       ss << "cp " << v * 100 / PawnValueEg;
   else if (Options["Protocol"] == "usi")
@@ -288,6 +495,8 @@ std::string UCI::square(const Position& pos, Square s) {
                                   : std::string{ char('0' + (pos.max_file() - file_of(s) + 1) / 10),
                                                  char('0' + (pos.max_file() - file_of(s) + 1) % 10),
                                                  char('a' + pos.max_rank() - rank_of(s)) };
+  else if (Options["Protocol"] == "xboard" && pos.max_rank() == RANK_10)
+      return std::string{ char('a' + file_of(s)), char('0' + rank_of(s)) };
   else
       return rank_of(s) < RANK_10 ? std::string{ char('a' + file_of(s)), char('1' + (rank_of(s) % 10)) }
                                   : std::string{ char('a' + file_of(s)), char('0' + ((rank_of(s) + 1) / 10)),
index fe9f918..14d0a91 100644 (file)
--- a/src/uci.h
+++ b/src/uci.h
@@ -58,6 +58,7 @@ public:
   operator double() const;
   operator std::string() const;
   bool operator==(const char*) const;
+  bool operator!=(const char*) const;
   void set_combo(std::vector<std::string> newComboValues);
 
 private:
index 3fc8430..fbffb90 100644 (file)
@@ -42,6 +42,13 @@ namespace PSQT {
 
 namespace UCI {
 
+// standard variants of XBoard/WinBoard
+std::set<string> standard_variants = {
+    "normal", "fischerandom", "3check", "makruk", "shatranj",
+    "asean", "seirawan", "crazyhouse", "suicide", "giveaway", "losers",
+    "capablanca", "gothic", "janus", "caparandom", "grand", "shogi", "xiangqi"
+};
+
 /// 'On change' actions, triggered by an option's value change
 void on_clear_hash(const Option&) { Search::clear(); }
 void on_hash_size(const Option& o) { TT.resize(o); }
@@ -52,13 +59,24 @@ void on_variant_path(const Option& o) { variants.parse(o); Options["UCI_Variant"
 void on_variant_change(const Option &o) {
     const Variant* v = variants.find(o)->second;
     PSQT::init(v);
-    sync_cout << "info string variant " << (std::string)o
-              << " files " << v->maxFile + 1
-              << " ranks " << v->maxRank + 1
-              << " pocket " << (v->pieceDrops ? (v->pocketSize ? v->pocketSize : v->pieceTypes.size()) : 0)
-              << " template " << v->variantTemplate
-              << " startpos " << v->startFen
-              << sync_endl;
+    // Do not send setup command for known variants
+    if (standard_variants.find(o) != standard_variants.end())
+        return;
+    int pocketsize = v->pieceDrops ? (v->pocketSize ? v->pocketSize : v->pieceTypes.size()) : 0;
+    if (Options["Protocol"] == "xboard")
+        sync_cout << "setup (-) "
+                  << v->maxFile + 1 << "x" << v->maxRank + 1
+                  << "+" << pocketsize << "_" << v->variantTemplate
+                  << " " << v->startFen
+                  << sync_endl;
+    else
+        sync_cout << "info string variant " << (std::string)o
+                << " files " << v->maxFile + 1
+                << " ranks " << v->maxRank + 1
+                << " pocket " << pocketsize
+                << " template " << v->variantTemplate
+                << " startpos " << v->startFen
+                << sync_endl;
 }
 
 
@@ -77,7 +95,7 @@ void init(OptionsMap& o) {
   // at most 2^32 clusters.
   constexpr int MaxHashMB = Is64Bit ? 131072 : 2048;
 
-  o["Protocol"]              << Option("uci", {"uci", "usi"});
+  o["Protocol"]              << Option("uci", {"uci", "usi", "xboard"});
   o["Debug Log File"]        << Option("", on_logger);
   o["Contempt"]              << Option(24, -100, 100);
   o["Analysis Contempt"]     << Option("Both", {"Both", "Off", "White", "Black"});
@@ -109,6 +127,35 @@ void init(OptionsMap& o) {
 
 std::ostream& operator<<(std::ostream& os, const OptionsMap& om) {
 
+  if (Options["Protocol"] == "xboard")
+  {
+      for (size_t idx = 0; idx < om.size(); ++idx)
+          for (const auto& it : om)
+              if (it.second.idx == idx && it.first != "Protocol")
+              {
+                  const Option& o = it.second;
+                  os << "\nfeature option=\"" << it.first << " -" << o.type;
+
+                  if (o.type == "string" || o.type == "check" || o.type == "combo")
+                      os << " " << o.defaultValue;
+
+                  if (o.type == "combo")
+                      for (string value : o.comboValues)
+                          if (value != o.defaultValue)
+                              os << " /// " << value;
+
+                  if (o.type == "spin")
+                      os << " " << int(stof(o.defaultValue))
+                         << " " << o.min
+                         << " " << o.max;
+
+                  os << "\"";
+
+                  break;
+              }
+  }
+  else
+
   for (size_t idx = 0; idx < om.size(); ++idx)
       for (const auto& it : om)
           if (it.second.idx == idx)
@@ -168,6 +215,11 @@ bool Option::operator==(const char* s) const {
         && !CaseInsensitiveLess()(s, currentValue);
 }
 
+bool Option::operator!=(const char* s) const {
+  assert(type == "combo");
+  return !(*this == s);
+}
+
 
 /// operator<<() inits options and assigns idx in the correct printing order
 
index 1b2c696..67a0504 100644 (file)
@@ -41,6 +41,11 @@ namespace {
         v->endgameEval = true;
         return v;
     }
+    Variant* chess960_variant() {
+        Variant* v = chess_variant();
+        v->chess960 = true;
+        return v;
+    }
     Variant* fairy_variant() {
         Variant* v = chess_variant();
         v->add_piece(SILVER, 's');
@@ -659,7 +664,8 @@ namespace {
 void VariantMap::init() {
     // Add to UCI_Variant option
     add("chess", chess_variant());
-    add("standard", chess_variant());
+    add("normal", chess_variant());
+    add("fischerandom", chess960_variant());
     add("fairy", fairy_variant()); // fairy variant used for endgame code initialization
     add("makruk", makruk_variant());
     add("cambodian", cambodian_variant());