Add support for USI protocol
authorianfab <ianfab@users.noreply.github.com>
Sat, 11 Aug 2018 13:36:00 +0000 (15:36 +0200)
committerianfab <ianfab@users.noreply.github.com>
Sat, 11 Aug 2018 18:46:11 +0000 (20:46 +0200)
Specification: http://hgm.nubati.net/usi.html

- Switch protocol when receiving uci/usi command,
  or when option "Protocol" is changed.
- Support parsing of SFEN, but internally use FEN.
- Use shogi coordinates (e.g., 3e3d) for all moves,
  and notation with "*" for piece drops.
- Specify mate distance in ply instead of fullmoves.
- Todo: Adapt option names to be compliant with USI
  - No whitespaces.
  - Standard options should be prefixed by "USI_".

src/position.cpp
src/position.h
src/uci.cpp
src/uci.h
src/ucioption.cpp

index ecf2914..17a57eb 100644 (file)
@@ -114,7 +114,7 @@ std::ostream& operator<<(std::ostream& os, const Position& pos) {
      << std::setfill(' ') << std::dec << "\nCheckers: ";
 
   for (Bitboard b = pos.checkers(); b; )
-      os << UCI::square(pop_lsb(&b)) << " ";
+      os << UCI::square(pos, pop_lsb(&b)) << " ";
 
   if (    int(Tablebases::MaxCardinality) >= popcount(pos.pieces())
       && !pos.can_castle(ANY_CASTLING))
@@ -216,7 +216,7 @@ void Position::init() {
 /// This function is not very robust - make sure that input FENs are correct,
 /// this is assumed to be the responsibility of the GUI.
 
-Position& Position::set(const Variant* v, const string& fenStr, bool isChess960, StateInfo* si, Thread* th) {
+Position& Position::set(const Variant* v, const string& fenStr, bool isChess960, StateInfo* si, Thread* th, bool sfen) {
 /*
    A FEN string defines a particular position using only the ASCII character set.
 
@@ -310,54 +310,57 @@ Position& Position::set(const Variant* v, const string& fenStr, bool isChess960,
   // 2. Active color
   ss >> token;
   sideToMove = (token == 'w' ? WHITE : BLACK);
+  // Invert side to move for SFEN
+  if (sfen)
+      sideToMove = ~sideToMove;
   ss >> token;
 
   // 3-4. Skip parsing castling and en passant flags if not present
   st->epSquare = SQ_NONE;
-  if (!isdigit(ss.peek()))
+  if (!isdigit(ss.peek()) && !sfen)
   {
-  // 3. Castling availability. Compatible with 3 standards: Normal FEN standard,
-  // Shredder-FEN that uses the letters of the columns on which the rooks began
-  // the game instead of KQkq and also X-FEN standard that, in case of Chess960,
-  // if an inner rook is associated with the castling right, the castling tag is
-  // replaced by the file letter of the involved rook, as for the Shredder-FEN.
-  while ((ss >> token) && !isspace(token))
-  {
-      Square rsq;
-      Color c = islower(token) ? BLACK : WHITE;
-      Piece rook = make_piece(c, ROOK);
+      // 3. Castling availability. Compatible with 3 standards: Normal FEN standard,
+      // Shredder-FEN that uses the letters of the columns on which the rooks began
+      // the game instead of KQkq and also X-FEN standard that, in case of Chess960,
+      // if an inner rook is associated with the castling right, the castling tag is
+      // replaced by the file letter of the involved rook, as for the Shredder-FEN.
+      while ((ss >> token) && !isspace(token))
+      {
+          Square rsq;
+          Color c = islower(token) ? BLACK : WHITE;
+          Piece rook = make_piece(c, ROOK);
 
-      token = char(toupper(token));
+          token = char(toupper(token));
 
-      if (token == 'K')
-          for (rsq = relative_square(c, SQ_H1); piece_on(rsq) != rook; --rsq) {}
+          if (token == 'K')
+              for (rsq = relative_square(c, SQ_H1); piece_on(rsq) != rook; --rsq) {}
 
-      else if (token == 'Q')
-          for (rsq = relative_square(c, SQ_A1); piece_on(rsq) != rook; ++rsq) {}
+          else if (token == 'Q')
+              for (rsq = relative_square(c, SQ_A1); piece_on(rsq) != rook; ++rsq) {}
 
-      else if (token >= 'A' && token <= 'H')
-          rsq = make_square(File(token - 'A'), relative_rank(c, RANK_1));
+          else if (token >= 'A' && token <= 'H')
+              rsq = make_square(File(token - 'A'), relative_rank(c, RANK_1));
 
-      else
-          continue;
+          else
+              continue;
 
-      set_castling_right(c, rsq);
-  }
+          set_castling_right(c, rsq);
+      }
 
-  // 4. En passant square. Ignore if no pawn capture is possible
-  if (   ((ss >> col) && (col >= 'a' && col <= 'h'))
-      && ((ss >> row) && (row == '3' || row == '6')))
-  {
-      st->epSquare = make_square(File(col - 'a'), Rank(row - '1'));
+      // 4. En passant square. Ignore if no pawn capture is possible
+      if (   ((ss >> col) && (col >= 'a' && col <= 'h'))
+          && ((ss >> row) && (row == '3' || row == '6')))
+      {
+          st->epSquare = make_square(File(col - 'a'), Rank(row - '1'));
 
-      if (   !(attackers_to(st->epSquare) & pieces(sideToMove, PAWN))
-          || !(pieces(~sideToMove, PAWN) & (st->epSquare + pawn_push(~sideToMove))))
-          st->epSquare = SQ_NONE;
-  }
+          if (   !(attackers_to(st->epSquare) & pieces(sideToMove, PAWN))
+              || !(pieces(~sideToMove, PAWN) & (st->epSquare + pawn_push(~sideToMove))))
+              st->epSquare = SQ_NONE;
+      }
   }
 
   // Check counter for nCheck
-  ss >> std::skipws >> token;
+  ss >> std::skipws >> token >> std::noskipws;
 
   if (max_check_count() && ss.peek() == '+')
   {
@@ -369,11 +372,28 @@ Position& Position::set(const Variant* v, const string& fenStr, bool isChess960,
       ss.putback(token);
 
   // 5-6. Halfmove clock and fullmove number
-  ss >> std::skipws >> st->rule50 >> gamePly;
+  if (sfen)
+  {
+      // Pieces in hand for SFEN
+      while ((ss >> token) && !isspace(token))
+      {
+          if (token == '-')
+              continue;
+          else if ((idx = piece_to_char().find(token)) != string::npos)
+              add_to_hand(color_of(Piece(idx)), type_of(Piece(idx)));
+      }
+      // Move count is in ply for SFEN
+      ss >> std::skipws >> gamePly;
+      gamePly = std::max(gamePly - 1, 0);
+  }
+  else
+  {
+      ss >> std::skipws >> st->rule50 >> gamePly;
 
-  // Convert from fullmove starting from 1 to gamePly starting from 0,
-  // handle also common incorrect FEN with fullmove = 0.
-  gamePly = std::max(2 * (gamePly - 1), 0) + (sideToMove == BLACK);
+      // Convert from fullmove starting from 1 to gamePly starting from 0,
+      // handle also common incorrect FEN with fullmove = 0.
+      gamePly = std::max(2 * (gamePly - 1), 0) + (sideToMove == BLACK);
+  }
 
   chess960 = isChess960;
   thisThread = th;
@@ -588,7 +608,7 @@ const string Position::fen() const {
   if (max_check_count())
       ss << " " << (max_check_count() - st->checksGiven[WHITE]) << "+" << (max_check_count() - st->checksGiven[BLACK]);
 
-  ss << (ep_square() == SQ_NONE ? " - " : " " + UCI::square(ep_square()) + " ")
+  ss << (ep_square() == SQ_NONE ? " - " : " " + UCI::square(*this, ep_square()) + " ")
      << st->rule50 << " " << 1 + (gamePly - (sideToMove == BLACK)) / 2;
 
   return ss.str();
index a5b18c5..650d8be 100644 (file)
@@ -83,7 +83,7 @@ public:
   Position& operator=(const Position&) = delete;
 
   // FEN string input/output
-  Position& set(const Variant* v, const std::string& fenStr, bool isChess960, StateInfo* si, Thread* th);
+  Position& set(const Variant* v, const std::string& fenStr, bool isChess960, StateInfo* si, Thread* th, bool sfen = false);
   Position& set(const std::string& code, Color c, StateInfo* si);
   const std::string fen() const;
 
index e5c5f20..d17a467 100644 (file)
@@ -50,20 +50,22 @@ namespace {
     string token, fen;
 
     is >> token;
+    // Parse as SFEN if specified
+    bool sfen = token == "sfen";
 
     if (token == "startpos")
     {
         fen = variants.find(Options["UCI_Variant"])->second->startFen;
         is >> token; // Consume "moves" token if any
     }
-    else if (token == "fen")
+    else if (token == "fen" || token == "sfen")
         while (is >> token && token != "moves")
             fen += token + " ";
     else
         return;
 
     states = StateListPtr(new std::deque<StateInfo>(1)); // Drop old and create a new one
-    pos.set(variants.find(Options["UCI_Variant"])->second, fen, Options["UCI_Chess960"], &states->back(), Threads.main());
+    pos.set(variants.find(Options["UCI_Variant"])->second, fen, Options["UCI_Chess960"], &states->back(), Threads.main(), sfen);
 
     // Parse move list (if any)
     while (is >> token && (m = UCI::to_move(pos, token)) != MOVE_NONE)
@@ -217,15 +219,18 @@ void UCI::loop(int argc, char* argv[]) {
       else if (token == "ponderhit")
           Threads.ponder = false; // Switch to normal search
 
-      else if (token == "uci")
+      else if (token == "uci" || token == "usi")
+      {
+          Options["Protocol"] = token;
           sync_cout << "id name " << engine_info(true)
                     << "\n"       << Options
-                    << "\nuciok"  << sync_endl;
+                    << "\n" << token << "ok"  << sync_endl;
+      }
 
       else if (token == "setoption")  setoption(is);
       else if (token == "go")         go(pos, is, states);
       else if (token == "position")   position(pos, is, states);
-      else if (token == "ucinewgame") Search::clear();
+      else if (token == "ucinewgame" || token == "usinewgame") Search::clear();
       else if (token == "isready")    sync_cout << "readyok" << sync_endl;
 
       // Additional custom non-UCI commands, mainly for debugging
@@ -255,6 +260,9 @@ string UCI::value(Value v) {
 
   if (abs(v) < VALUE_MATE - MAX_PLY)
       ss << "cp " << v * 100 / PawnValueEg;
+  else if (Options["Protocol"] == "usi")
+      // In USI, mate distance is given in ply
+      ss << "mate " << (v > 0 ? VALUE_MATE - v : -VALUE_MATE - v);
   else
       ss << "mate " << (v > 0 ? VALUE_MATE - v + 1 : -VALUE_MATE - v - 1) / 2;
 
@@ -264,8 +272,9 @@ string UCI::value(Value v) {
 
 /// UCI::square() converts a Square to a string in algebraic notation (g1, a7, etc.)
 
-std::string UCI::square(Square s) {
-  return std::string{ char('a' + file_of(s)), char('1' + rank_of(s)) };
+std::string UCI::square(const Position& pos, Square s) {
+  return Options["Protocol"] == "usi" ? std::string{ char('1' + pos.max_file() - file_of(s)), char('a' + pos.max_rank() - rank_of(s)) }
+                                      : std::string{ char('a' + file_of(s)), char('1' + rank_of(s)) };
 }
 
 
@@ -288,8 +297,9 @@ string UCI::move(const Position& pos, Move m) {
   if (type_of(m) == CASTLING && !pos.is_chess960())
       to = make_square(to > from ? FILE_G : FILE_C, rank_of(from));
 
-  string move = (type_of(m) == DROP ? std::string{pos.piece_to_char()[type_of(pos.moved_piece(m))], '@'}
-                                    : UCI::square(from)) + UCI::square(to);
+  string move = (type_of(m) == DROP ? std::string{pos.piece_to_char()[type_of(pos.moved_piece(m))],
+                                                  Options["Protocol"] == "usi" ? '*' : '@'}
+                                    : UCI::square(pos, from)) + UCI::square(pos, to);
 
   if (type_of(m) == PROMOTION)
       move += pos.piece_to_char()[make_piece(BLACK, promotion_type(m))];
index 9f5a910..408d191 100644 (file)
--- a/src/uci.h
+++ b/src/uci.h
@@ -72,7 +72,7 @@ private:
 void init(OptionsMap&);
 void loop(int argc, char* argv[]);
 std::string value(Value v);
-std::string square(Square s);
+std::string square(const Position& pos, Square s);
 std::string move(const Position& pos, Move m);
 std::string pv(const Position& pos, Depth depth, Value alpha, Value beta);
 Move to_move(const Position& pos, std::string& str);
index d20252e..cc1bf62 100644 (file)
@@ -62,6 +62,7 @@ void init(OptionsMap& o) {
   // at most 2^32 clusters.
   constexpr int MaxHashMB = Is64Bit ? 131072 : 2048;
 
+  o["Protocol"]              << Option("uci", {"uci", "usi"});
   o["Debug Log File"]        << Option("", on_logger);
   o["Contempt"]              << Option(21, -100, 100);
   o["Analysis Contempt"]     << Option("Both", {"Both", "Off", "White", "Black"});