From 05c5367700496da01ad3a1944fe2cd36117e3b46 Mon Sep 17 00:00:00 2001 From: ianfab Date: Sat, 11 Aug 2018 15:36:00 +0200 Subject: [PATCH] Add support for USI protocol 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 | 98 ++++++++++++++++++++++++++++++++--------------------- src/position.h | 2 +- src/uci.cpp | 28 ++++++++++----- src/uci.h | 2 +- src/ucioption.cpp | 1 + 5 files changed, 81 insertions(+), 50 deletions(-) diff --git a/src/position.cpp b/src/position.cpp index ecf2914..17a57eb 100644 --- a/src/position.cpp +++ b/src/position.cpp @@ -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(); diff --git a/src/position.h b/src/position.h index a5b18c5..650d8be 100644 --- a/src/position.h +++ b/src/position.h @@ -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; diff --git a/src/uci.cpp b/src/uci.cpp index e5c5f20..d17a467 100644 --- a/src/uci.cpp +++ b/src/uci.cpp @@ -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(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))]; diff --git a/src/uci.h b/src/uci.h index 9f5a910..408d191 100644 --- 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); diff --git a/src/ucioption.cpp b/src/ucioption.cpp index d20252e..cc1bf62 100644 --- a/src/ucioption.cpp +++ b/src/ucioption.cpp @@ -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"}); -- 1.7.0.4