From: Fabian Fichter Date: Fri, 1 Nov 2019 20:36:31 +0000 (+0100) Subject: Support XBoard protocol (close #37) X-Git-Url: http://winboard.nl/cgi-bin?a=commitdiff_plain;h=8a1bf6c8274d44d40e36880b54ebcd25f6480a09;p=fairystockfish.git Support XBoard protocol (close #37) --- diff --git a/src/search.cpp b/src/search.cpp index af82089..15fd1e6 100644 --- a/src/search.cpp +++ b/src/search.cpp @@ -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(); diff --git a/src/thread.h b/src/thread.h index 0517afc..418aeae 100644 --- a/src/thread.h +++ b/src/thread.h @@ -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 { std::atomic_bool stop; -private: StateListPtr setupStates; +private: uint64_t accumulate(std::atomic Thread::* member) const { uint64_t sum = 0; diff --git a/src/types.h b/src/types.h index be57825..b7ccc21 100644 --- a/src/types.h +++ b/src/types.h @@ -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, diff --git a/src/uci.cpp b/src/uci.cpp index 21970f2..ed61569 100644 --- a/src/uci.cpp +++ b/src/uci.cpp @@ -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& 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& 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 moveList = std::deque(); + 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)), diff --git a/src/uci.h b/src/uci.h index fe9f918..14d0a91 100644 --- 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 newComboValues); private: diff --git a/src/ucioption.cpp b/src/ucioption.cpp index 3fc8430..fbffb90 100644 --- a/src/ucioption.cpp +++ b/src/ucioption.cpp @@ -42,6 +42,13 @@ namespace PSQT { namespace UCI { +// standard variants of XBoard/WinBoard +std::set 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 diff --git a/src/variant.cpp b/src/variant.cpp index 1b2c696..67a0504 100644 --- a/src/variant.cpp +++ b/src/variant.cpp @@ -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());