diff --git a/.gitignore b/.gitignore index ffec2f7cda..252f297704 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,5 @@ subprojects/* !subprojects/*.wrap lc0.xcodeproj/ *.swp +.vs/ +build-cuda.cmd diff --git a/src/chess/callbacks.h b/src/chess/callbacks.h index 79ebf2ff8b..579a7dd09b 100644 --- a/src/chess/callbacks.h +++ b/src/chess/callbacks.h @@ -69,6 +69,8 @@ struct ThinkingInfo { int hashfull = -1; // Win in centipawns. optional score; + // Distance to Mate + optional mate; // Number of successful TB probes (not the same as playouts ending in TB hit). int tb_hits = -1; // Best line found. Moves are from perspective of white player. diff --git a/src/chess/position.h b/src/chess/position.h index 922727e64a..519871e08a 100644 --- a/src/chess/position.h +++ b/src/chess/position.h @@ -82,6 +82,7 @@ class Position { }; enum class GameResult { UNDECIDED, WHITE_WON, DRAW, BLACK_WON }; +enum class CertaintyTrigger { NONE, TB_HIT, TWO_FOLD, TERMINAL, NORMAL }; class PositionHistory { public: diff --git a/src/chess/uciloop.cc b/src/chess/uciloop.cc index 9d00185c01..1e6f09bdea 100644 --- a/src/chess/uciloop.cc +++ b/src/chess/uciloop.cc @@ -247,7 +247,13 @@ void UciLoop::SendInfo(const std::vector& infos) { if (info.seldepth >= 0) res += " seldepth " + std::to_string(info.seldepth); if (info.time >= 0) res += " time " + std::to_string(info.time); if (info.nodes >= 0) res += " nodes " + std::to_string(info.nodes); - if (info.score) res += " score cp " + std::to_string(*info.score); + + // If mate display mate, otherwise if score display score. + if (info.mate) { + res += " score mate " + std::to_string(*info.mate); + } else if (info.score) { + res += " score cp " + std::to_string(*info.score); + } if (info.hashfull >= 0) res += " hashfull " + std::to_string(info.hashfull); if (info.nps >= 0) res += " nps " + std::to_string(info.nps); if (info.tb_hits >= 0) res += " tbhits " + std::to_string(info.tb_hits); diff --git a/src/chess/uciloop.h b/src/chess/uciloop.h index 4224dc7664..2075bf1177 100644 --- a/src/chess/uciloop.h +++ b/src/chess/uciloop.h @@ -82,6 +82,7 @@ class UciLoop { virtual void CmdStop() { throw Exception("Not supported"); } virtual void CmdPonderHit() { throw Exception("Not supported"); } virtual void CmdStart() { throw Exception("Not supported"); } + virtual void CmdStats() { throw Exception("Not supported"); } private: bool DispatchCommand( diff --git a/src/engine.cc b/src/engine.cc index 6d160f8fcd..5759bac70f 100644 --- a/src/engine.cc +++ b/src/engine.cc @@ -349,6 +349,7 @@ void EngineController::Go(const GoParams& params) { if (info.multipv <= 1) { ponder_info = info; if (ponder_info.score) ponder_info.score = -*ponder_info.score; + if (ponder_info.mate) ponder_info.mate = -*ponder_info.mate; if (ponder_info.depth > 1) ponder_info.depth--; if (ponder_info.seldepth > 1) ponder_info.seldepth--; ponder_info.pv.clear(); @@ -455,4 +456,6 @@ void EngineLoop::CmdPonderHit() { engine_.PonderHit(); } void EngineLoop::CmdStop() { engine_.Stop(); } + + } // namespace lczero diff --git a/src/engine.h b/src/engine.h index 89ad6d8171..386bb7db6e 100644 --- a/src/engine.h +++ b/src/engine.h @@ -77,7 +77,8 @@ class EngineController { void PonderHit(); // Must not block. void Stop(); - + // Prints verbose move stats of current root + SearchLimits PopulateSearchLimits(int ply, bool is_black, const GoParams& params, std::chrono::steady_clock::time_point start_time); diff --git a/src/mcts/node.cc b/src/mcts/node.cc index d968377be2..9c96cba8a3 100644 --- a/src/mcts/node.cc +++ b/src/mcts/node.cc @@ -28,9 +28,11 @@ #include "mcts/node.h" #include +#include #include #include #include +#include #include #include #include @@ -160,9 +162,58 @@ float Edge::GetP() const { return ret; } +void Edge::MakeTerminal(GameResult result) { + certainty_state_ |= kTerminalMask | kCertainMask | kUpperBound | kLowerBound; + certainty_state_ &= kGameResultClear; + if (result == GameResult::WHITE_WON) { + certainty_state_ |= kGameResultWin; + } else if (result == GameResult::BLACK_WON) { + certainty_state_ |= kGameResultLoss; + } +} + +void Edge::MakeCertain(GameResult result, CertaintyTrigger trigger) { + certainty_state_ |= kCertainMask | kUpperBound | kLowerBound; + certainty_state_ &= kGameResultClear; + if (result == GameResult::WHITE_WON) { + certainty_state_ |= kGameResultWin; + } else if (result == GameResult::BLACK_WON) { + certainty_state_ |= kGameResultLoss; + } + if (trigger == CertaintyTrigger::TB_HIT) certainty_state_ |= kTBHit; + if (trigger == CertaintyTrigger::TWO_FOLD) certainty_state_ |= kTwoFold; +} + +void Edge::MakeCertain(int q, CertaintyTrigger trigger) { + certainty_state_ |= kCertainMask | kUpperBound | kLowerBound; + certainty_state_ &= kGameResultClear; + if (q == 1) { + certainty_state_ |= kGameResultWin; + } else if (q == -1) { + certainty_state_ |= kGameResultLoss; + } + if (trigger == CertaintyTrigger::TB_HIT) certainty_state_ |= kTBHit; + if (trigger == CertaintyTrigger::TWO_FOLD) certainty_state_ |= kTwoFold; +} +void Edge::SetEQ(int eq) { + certainty_state_ &= kGameResultClear; + if (eq == 1) { + certainty_state_ |= kGameResultWin; + } else if (eq == -1) { + certainty_state_ |= kGameResultLoss; + } +} + +int Edge::GetEQ() const { + if (certainty_state_ & kGameResultLoss) return -1; + if (certainty_state_ & kGameResultWin) return 1; + return 0; +} + std::string Edge::DebugString() const { std::ostringstream oss; - oss << "Move: " << move_.as_string() << " p_: " << p_ << " GetP: " << GetP(); + oss << "Move: " << move_.as_string() << " p_: " << p_ << " GetP: " << GetP() + << " Certainty:" << std::bitset<8>(certainty_state_); return oss.str(); } @@ -197,36 +248,50 @@ void Node::CreateEdges(const MoveList& moves) { Node::ConstIterator Node::Edges() const { return {edges_, &child_}; } Node::Iterator Node::Edges() { return {edges_, &child_}; } +// Recalculate n_ from real children visits. +// This is needed if a node was proved to be certain in a prior +// search and later gets to be root of search. +void Node::RecomputeNfromChildren() { + if (n_ > 1) { + uint32_t visits = 1; + for (const auto& child : Edges()) visits += child.GetN(); + n_ = visits; + } + assert(n_in_flight_ == 0); +} + float Node::GetVisitedPolicy() const { return visited_policy_; } +float Node::GetQ() const { + if (parent_) { + auto edge = parent_->GetEdgeToNode(this); + if (edge->IsCertain()) return (float)edge->GetEQ(); + } + return q_; +} + Edge* Node::GetEdgeToNode(const Node* node) const { assert(node->parent_ == this); assert(node->index_ < edges_.size()); return &edges_[node->index_]; } -Edge* Node::GetOwnEdge() const { return GetParent()->GetEdgeToNode(this); } +Edge* Node::GetOwnEdge() const { + if (GetParent()) { + return GetParent()->GetEdgeToNode(this); + } else + return nullptr; +} std::string Node::DebugString() const { std::ostringstream oss; - oss << " Term:" << is_terminal_ << " This:" << this << " Parent:" << parent_ - << " Index:" << index_ << " Child:" << child_.get() - << " Sibling:" << sibling_.get() << " Q:" << q_ << " N:" << n_ - << " N_:" << n_in_flight_ << " Edges:" << edges_.size(); + oss << " This:" << this << " Parent:" << parent_ << " Index:" << index_ + << " Child:" << child_.get() << " Sibling:" << sibling_.get() + << " Q:" << q_ << " N:" << n_ << " N_:" << n_in_flight_ + << " Edges:" << edges_.size(); return oss.str(); } -void Node::MakeTerminal(GameResult result) { - is_terminal_ = true; - if (result == GameResult::DRAW) { - q_ = 0.0f; - } else if (result == GameResult::WHITE_WON) { - q_ = 1.0f; - } else if (result == GameResult::BLACK_WON) { - q_ = -1.0f; - } -} - bool Node::TryStartScoreUpdate() { if (n_ == 0 && n_in_flight_ > 0) return false; ++n_in_flight_; @@ -373,6 +438,8 @@ void NodeTree::MakeMove(Move move) { current_head_->ReleaseChildrenExceptOne(new_head); current_head_ = new_head ? new_head : current_head_->CreateSingleChildNode(move); + if (current_head_->GetParent()) current_head_->GetOwnEdge()->ClearCertaintyState(); + current_head_->RecomputeNfromChildren(); history_.Append(move); } @@ -417,8 +484,12 @@ bool NodeTree::ResetToPosition(const std::string& starting_fen, // previously trimmed; we need to reset current_head_ in that case. // Also, if the current_head_ is terminal, reset that as well to allow forced // analysis of WDL hits, or possibly 3 fold or 50 move "draws", etc. - if (!seen_old_head || current_head_->IsTerminal()) TrimTreeAtHead(); + if (!seen_old_head) TrimTreeAtHead(); + // Certainty Propagation: No need to trim the head, just resetting certainty + // state except bounds, and recomputing N should suffices even with WDL hits. + if (current_head_->GetParent()) current_head_->GetOwnEdge()->ClearCertaintyState(); + current_head_->RecomputeNfromChildren(); return seen_old_head; } diff --git a/src/mcts/node.h b/src/mcts/node.h index 26d020b2ef..053f91b96f 100644 --- a/src/mcts/node.h +++ b/src/mcts/node.h @@ -86,6 +86,69 @@ class Edge { float GetP() const; void SetP(float val); + void MakeTerminal(GameResult result); + + // Sets flags for certainty, trigger of certainty and result by GameResult. + void MakeCertain(GameResult result, CertaintyTrigger trigger); + + // Sets flags for certainty, trigger of certainty and result (by Q). + void MakeCertain(int q, CertaintyTrigger trigger); + + // Sets edge-Q: win = 1; draw = 0; loss = -1. + void SetEQ(int eq); + // Clears Certainty but keeps bounds. + void ClearCertaintyState() { + (certainty_state_ & kCertainMask) ? certainty_state_ = 0 + : certainty_state_ &= kClearKeepBounds; + }; + // Sets (U)pper and (L)ower bounds. + void UBound(int eq) { + certainty_state_ |= kUpperBound; + SetEQ(eq); + }; + void LBound(int eq) { + certainty_state_ |= kLowerBound; + SetEQ(eq); + }; + + // Returns true if only upper bounded. + bool IsOnlyUBounded() { + return (certainty_state_ & kUpperBound) && + !(certainty_state_ & kLowerBound); + }; + + // Returns true if only lower bounded. + bool IsOnlyLBounded() { + return !(certainty_state_ & kUpperBound) && + (certainty_state_ & kLowerBound); + }; + bool IsTerminal() const { return certainty_state_ & kTerminalMask; }; + bool IsCertain() const { return certainty_state_ & kCertainMask; }; + bool IsCertainWin() const { + return ((certainty_state_ & kCertainMask) && + (certainty_state_ & kGameResultWin)); + }; + bool IsCertainLoss() const { + return ((certainty_state_ & kCertainMask) && + (certainty_state_ & kGameResultLoss)); + }; + bool IsCertainDraw() const { + return ((certainty_state_ & kCertainMask) && + !(certainty_state_ & ~kGameResultClear)); + }; + + // Returns true if certainty prove based on a TB hit. + bool IsPropagatedTBHit() const { return certainty_state_ & kTBHit; }; + + // Query bounds. + bool IsUBounded() const { return certainty_state_ & kUpperBound; }; + bool IsLBounded() const { return certainty_state_ & kLowerBound; }; + + // Return all stats flags. + uint8_t GetCertaintyStatus() const { return certainty_state_; }; + + int GetEQ() const; + // Debug information about the edge. std::string DebugString() const; @@ -101,6 +164,28 @@ class Edge { // network; compressed to a 16 bit format (5 bits exp, 11 bits significand). uint16_t p_ = 0; + // Certainty Propagation attaches a number of flags to each edge. + // kTerminalMask -> edge is terminal. + // kCertainMask -> edge is certain. + // kUpperBound -> edge is upper bounded. + // kLowerBound -> edge is lower bounded. + // kTBHit -> edge is certain because of a TB hit. + // kTwoFold -> edge is certain because of a two-fold. + // kGameResultWin -> edge is a proven win. + // kGameResultLoss -> edge is a proven loss. + uint8_t certainty_state_ = 0; + enum Masks : uint8_t { + kTerminalMask = 0b00000001, + kCertainMask = 0b00000010, + kUpperBound = 0b00000100, + kLowerBound = 0b00001000, + kTBHit = 0b00010000, + kTwoFold = 0b00100000, + kGameResultWin = 0b01000000, + kGameResultLoss = 0b10000000, + kGameResultClear = 0b00111111, + kClearKeepBounds = 0b00001100, + }; friend class EdgeList; }; @@ -144,6 +229,9 @@ class Node { // Returns whether a node has children. bool HasChildren() const { return edges_; } + // Recalculate n_ from real children visits. + void RecomputeNfromChildren(); + // Returns sum of policy priors which have had at least one playout. float GetVisitedPolicy() const; uint32_t GetN() const { return n_; } @@ -152,15 +240,46 @@ class Node { // Returns n = n_if_flight. int GetNStarted() const { return n_ + n_in_flight_; } // Returns node eval, i.e. average subtree V for non-terminal node and -1/0/1 - // for terminal nodes. - float GetQ() const { return q_; } + // for terminal nodes (read from edge). + float GetQ() const; // Returns whether the node is known to be draw/lose/win. - bool IsTerminal() const { return is_terminal_; } + bool IsTerminal() const { + return GetOwnEdge() ? GetOwnEdge()->IsTerminal() : false; + } + bool IsCertain() const { + return GetOwnEdge() ? GetOwnEdge()->IsCertain() : false; + } + + // Sets bounds. + void UBound(int eq) const { + if (GetOwnEdge()) GetOwnEdge()->UBound(eq); + } + void LBound(int eq) const { + if (GetOwnEdge()) GetOwnEdge()->LBound(eq); + } + // Queries bounds. + + bool IsBounded() const { + return GetOwnEdge() + ? GetOwnEdge()->IsLBounded() || GetOwnEdge()->IsUBounded() + : false; + } + bool IsOnlyUBounded() const { + return GetOwnEdge() ? GetOwnEdge()->IsOnlyUBounded() : false; + } uint16_t GetNumEdges() const { return edges_.size(); } - // Makes the node terminal and sets it's score. - void MakeTerminal(GameResult result); + // Makes the node terminal or certain and sets it's score. + void MakeTerminal(GameResult result) { + if (GetOwnEdge()) GetOwnEdge()->MakeTerminal(result); + } + void MakeCertain(GameResult result, CertaintyTrigger trigger) { + if (GetOwnEdge()) GetOwnEdge()->MakeCertain(result, trigger); + } + void MakeCertain(int q, CertaintyTrigger trigger) { + if (GetOwnEdge()) GetOwnEdge()->MakeCertain(q, trigger); + } // If this node is not in the process of being expanded by another thread // (which can happen only if n==0 and n-in-flight==1), mark the node as @@ -249,7 +368,8 @@ class Node { EdgeList edges_; // 8 byte fields. - // Pointer to a parent node. nullptr for the root. + // Pointer to a parent node. nullptr for the root of tree, + // Note: root of tree might not be search->root_node_. Node* parent_ = nullptr; // Pointer to a first child. nullptr for a leaf node. std::unique_ptr child_; @@ -281,10 +401,6 @@ class Node { // Index of this node is parent's edge list. uint16_t index_; - // 1 byte fields. - // Whether or not this node end game (with a winning of either sides or draw). - bool is_terminal_ = false; - // TODO(mooskagh) Unfriend NodeTree. friend class NodeTree; friend class Edge_Iterator; @@ -332,19 +448,35 @@ class EdgeAndNode { float GetQ(float default_q) const { return (node_ && node_->GetN() > 0) ? node_->GetQ() : default_q; } - // N-related getters, from Node (if exists). + + // Gets the edges Q, if edge is certain this + // is the proven game result (-1, 0, +1). + int GetEQ() const { return edge_->GetEQ(); } + + // N-related getters, from node (if exists). uint32_t GetN() const { return node_ ? node_->GetN() : 0; } int GetNStarted() const { return node_ ? node_->GetNStarted() : 0; } uint32_t GetNInFlight() const { return node_ ? node_->GetNInFlight() : 0; } - // Whether the node is known to be terminal. - bool IsTerminal() const { return node_ ? node_->IsTerminal() : false; } - // Edge related getters. float GetP() const { return edge_->GetP(); } Move GetMove(bool flip = false) const { return edge_ ? edge_->GetMove(flip) : Move(); } + bool IsTerminal() const { return edge_->IsTerminal(); } + bool IsCertain() const { return edge_->IsCertain(); } + bool IsCertainWin() const { return edge_->IsCertainWin(); } + + // Queries bounds. + bool IsUBounded() const { return edge_->IsUBounded(); } + bool IsLBounded() const { return edge_->IsLBounded(); } + + // Sets bounds. + void UBound(int eq) { edge_->UBound(eq); } + void LBound(int eq) { edge_->LBound(eq); } + + // Queries if edge's certainty is based on a TB hit. + bool IsPropagatedTBHit() const { return edge_->IsPropagatedTBHit(); } // Returns U = numerator * p / N. // Passed numerator is expected to be equal to (cpuct * sqrt(N[parent])). diff --git a/src/mcts/params.cc b/src/mcts/params.cc index 85dcb5ff5e..1cd4525c04 100644 --- a/src/mcts/params.cc +++ b/src/mcts/params.cc @@ -161,6 +161,17 @@ const OptionId SearchParams::kHistoryFillId{ "exist, but they can be synthesized. This parameter defines when to " "synthesize them (always, never, or only at non-standard fen position)."}; +const OptionId SearchParams::kCertaintyPropagationId{ + "certainty-propagation", "CertaintyPropagation", + "Propagates certain scores more efficiently in the search tree, " + "proves and displays mates. Uses two-fold draw scoring."}; + +const OptionId SearchParams::kCertaintyPropagationDepthId{ + "certainty-propagation-depth", "CertaintyPropagationDepth", + "Depth of look-ahead-search for certainty propagation. " + "Warning, settings larger than 1 are currently not " + "recommended."}; + void SearchParams::Populate(OptionsParser* options) { // Here the uci optimized defaults" are set. // Many of them are overridden with training specific values in tournament.cc. @@ -193,8 +204,12 @@ void SearchParams::Populate(OptionsParser* options) { options->Add(kScoreTypeId, score_type) = "centipawn"; std::vector history_fill_opt{"no", "fen_only", "always"}; options->Add(kHistoryFillId, history_fill_opt) = "fen_only"; + options->Add(kCertaintyPropagationId) = false; + options->Add(kCertaintyPropagationDepthId, 0, 4) = 1; } + + SearchParams::SearchParams(const OptionsDict& options) : options_(options), kCpuct(options.Get(kCpuctId.GetId())), @@ -211,6 +226,8 @@ SearchParams::SearchParams(const OptionsDict& options) kMaxCollisionEvents(options.Get(kMaxCollisionEventsId.GetId())), kMaxCollisionVisits(options.Get(kMaxCollisionVisitsId.GetId())), kOutOfOrderEval(options.Get(kOutOfOrderEvalId.GetId())), + kCertaintyPropagation(options.Get(kCertaintyPropagationId.GetId())), + kCertaintyPropagationDepth(options.Get(kCertaintyPropagationDepthId.GetId())), kHistoryFill( EncodeHistoryFill(options.Get(kHistoryFillId.GetId()))), kMiniBatchSize(options.Get(kMiniBatchSizeId.GetId())){ diff --git a/src/mcts/params.h b/src/mcts/params.h index bcbe780f46..ffb3bfdf1b 100644 --- a/src/mcts/params.h +++ b/src/mcts/params.h @@ -88,6 +88,8 @@ class SearchParams { return options_.Get(kScoreTypeId.GetId()); } FillEmptyHistory GetHistoryFill() const { return kHistoryFill; } + bool GetCertaintyPropagation() const { return kCertaintyPropagation; } + int GetCertaintyPropagationDepth() const { return kCertaintyPropagationDepth; } // Search parameter IDs. static const OptionId kMiniBatchSizeId; @@ -115,6 +117,8 @@ class SearchParams { static const OptionId kMultiPvId; static const OptionId kScoreTypeId; static const OptionId kHistoryFillId; + static const OptionId kCertaintyPropagationId; + static const OptionId kCertaintyPropagationDepthId; private: const OptionsDict& options_; @@ -137,6 +141,8 @@ class SearchParams { const int kMaxCollisionEvents; const int kMaxCollisionVisits; const bool kOutOfOrderEval; + const bool kCertaintyPropagation; + const int kCertaintyPropagationDepth; const FillEmptyHistory kHistoryFill; const int kMiniBatchSize; }; diff --git a/src/mcts/search.cc b/src/mcts/search.cc index d189d2b266..08844c421a 100644 --- a/src/mcts/search.cc +++ b/src/mcts/search.cc @@ -28,6 +28,7 @@ #include "mcts/search.h" #include +#include #include #include #include @@ -118,18 +119,21 @@ void Search::SendUciInfo() REQUIRES(nodes_mutex_) { common_info.nps = common_info.time ? (total_playouts_ * 1000 / common_info.time) : 0; common_info.tb_hits = tb_hits_.load(std::memory_order_acquire); - int multipv = 0; for (const auto& edge : edges) { + float score = (edge.HasNode() && edge.node()->GetParent()) + ? edge.GetQ(-edge.node()->GetParent()->GetQ()) + : edge.GetQ(0); + ++multipv; uci_infos.emplace_back(common_info); auto& uci_info = uci_infos.back(); if (score_type == "centipawn") { - uci_info.score = 290.680623072 * tan(1.548090806 * edge.GetQ(0)); + uci_info.score = 290.680623072 * tan(1.548090806 * score); } else if (score_type == "win_percentage") { - uci_info.score = edge.GetQ(0) * 5000 + 5000; + uci_info.score = score * 5000 + 5000; } else if (score_type == "Q") { - uci_info.score = edge.GetQ(0) * 10000; + uci_info.score = score * 10000; } if (params_.GetMultiPv() > 1) uci_info.multipv = multipv; bool flip = played_history_.IsBlackToMove(); @@ -138,6 +142,23 @@ void Search::SendUciInfo() REQUIRES(nodes_mutex_) { uci_info.pv.push_back(iter.GetMove(flip)); if (!iter.node()) break; // Last edge was dangling, cannot continue. } + + // Mate display if certain win (or loss) with distance to mate set to + // length of pv (average mate) + NegaBoundSearch depth. + // If win is based on propagated TB bit, length of mate is + // adjusted by +1000; For root filtered TB moves +500. + if (params_.GetCertaintyPropagation()) { + if (edge.IsCertain() && edge.GetEQ() != 0) + uci_info.mate = edge.GetEQ() * ((uci_info.pv.size() + 1) / 2 + + params_.GetCertaintyPropagationDepth() + + (edge.IsPropagatedTBHit() ? 1000 : 0)); + else if (root_syzygy_rank_) { + int sign = (root_syzygy_rank_ - 1 > 0) - (root_syzygy_rank_ - 1 < 0); + if (sign) { + uci_info.mate = sign * (-500 + abs(root_syzygy_rank_)); + } else uci_info.score = 0; + } + } } if (!uci_infos.empty()) last_outputted_uci_info_ = uci_infos.front(); @@ -247,8 +268,8 @@ std::vector Search::GetVerboseStats(Node* node, oss << "(V: "; optional v; - if (edge.IsTerminal()) { - v = edge.node()->GetQ(); + if (edge.IsCertain()) { + v = (float)edge.edge()->GetEQ(); } else { NNCacheLock nneval = GetCachedNNEval(edge.node()); if (nneval) v = -nneval->q; @@ -260,7 +281,8 @@ std::vector Search::GetVerboseStats(Node* node, } oss << ") "; - if (edge.IsTerminal()) oss << "(T) "; + oss << " C:" << std::bitset<8>(edge.edge()->GetCertaintyStatus()); + infos.emplace_back(oss.str()); } return infos; @@ -442,21 +464,27 @@ std::int64_t Search::GetTotalPlayouts() const { return total_playouts_; } -bool Search::PopulateRootMoveLimit(MoveList* root_moves) const { +int Search::PopulateRootMoveLimit(MoveList* root_moves) const { // Search moves overrides tablebase. if (!limits_.searchmoves.empty()) { *root_moves = limits_.searchmoves; - return false; + return 0; } + + // Syzygy root_probe returns best_rank for proper eval if + // moves are syzygy root filtered. auto board = played_history_.Last().GetBoard(); if (!syzygy_tb_ || !board.castlings().no_legal_castle() || (board.ours() | board.theirs()).count() > syzygy_tb_->max_cardinality()) { - return false; + return 0; } - return syzygy_tb_->root_probe(played_history_.Last(), - played_history_.DidRepeatSinceLastZeroingMove(), - root_moves) || - syzygy_tb_->root_probe_wdl(played_history_.Last(), root_moves); + + int best_rank = syzygy_tb_->root_probe( + played_history_.Last(), played_history_.DidRepeatSinceLastZeroingMove(), + root_moves); + if (!best_rank) + best_rank = syzygy_tb_->root_probe_wdl(played_history_.Last(), root_moves); + return best_rank; } // Computes the best move, maybe with temperature (according to the settings). @@ -496,11 +524,14 @@ std::vector Search::GetBestChildrenNoTemperature(Node* parent, PopulateRootMoveLimit(&root_limit); } // Best child is selected using the following criteria: + // with Certainty Propagation: + // * Prefer certain wins, avoid certain losses + // Otherwise: // * Largest number of playouts. // * If two nodes have equal number: // * If that number is 0, the one with larger prior wins. // * If that number is larger than 0, the one with larger eval wins. - using El = std::tuple; + using El = std::tuple; std::vector edges; for (auto edge : parent->Edges()) { if (parent == root_node_ && !root_limit.empty() && @@ -508,19 +539,37 @@ std::vector Search::GetBestChildrenNoTemperature(Node* parent, root_limit.end()) { continue; } - edges.emplace_back(edge.GetN(), edge.GetQ(0), edge.GetP(), edge); + edges.emplace_back((params_.GetCertaintyPropagation()) + ? edge.edge()->GetEQ() * (edge.IsTerminal()+1) + : 0, + edge.GetN(), edge.GetQ(0), edge.GetP(), edge); + } + // In case of certain draws, insert + // these draws at first N (descending) where Q<=0. + if (params_.GetCertaintyPropagation()) { + std::partial_sort(edges.begin(), edges.end(), edges.end(), + std::greater()); + // largest N with Q >= 0 + uint64_t largest_N = 0; + for (auto it = edges.begin(); it != edges.end(); ++it) { + if (std::get<2>(*it) <= 0.0f && largest_N == 0) + largest_N = std::get<1>(*it); + if (std::get<4>(*it).edge()->IsCertainDraw() && largest_N > 0) + std::get<1>(*it) = largest_N; + } } + // Final sort pass auto middle = (static_cast(edges.size()) > count) ? edges.begin() + count : edges.end(); std::partial_sort(edges.begin(), middle, edges.end(), std::greater()); std::vector res; std::transform(edges.begin(), middle, std::back_inserter(res), - [](const El& x) { return std::get<3>(x); }); + [](const El& x) { return std::get<4>(x); }); return res; } -// Returns a child with most visits. +// Returns best child. EdgeAndNode Search::GetBestChildNoTemperature(Node* parent) const { auto res = GetBestChildrenNoTemperature(parent, 1); return res.empty() ? EdgeAndNode() : res.front(); @@ -719,8 +768,10 @@ void SearchWorker::InitializeIteration( if (!root_move_filter_populated_) { root_move_filter_populated_ = true; - if (search_->PopulateRootMoveLimit(&root_move_filter_)) { + int best_rank = search_->PopulateRootMoveLimit(&root_move_filter_); + if (best_rank) { search_->tb_hits_.fetch_add(1, std::memory_order_acq_rel); + search_->root_syzygy_rank_ = best_rank; } } } @@ -765,7 +816,7 @@ void SearchWorker::GatherMinibatch() { ExtendNode(node); // Only send non-terminal nodes to a neural network. - if (!node->IsTerminal()) { + if (!node->IsCertain()) { picked_node.nn_queried = true; picked_node.is_cache_hit = AddNodeToComputation(node, true); } @@ -856,20 +907,18 @@ SearchWorker::NodeToProcess SearchWorker::PickNodeToExtend( } return NodeToProcess::Collision(node, depth, collision_limit); } - // Either terminal or unexamined leaf node -- the end of this playout. - if (!node->HasChildren()) { - if (node->IsTerminal()) { - return NodeToProcess::TerminalHit(node, depth, 1); - } else { - return NodeToProcess::Extension(node, depth); - } + // Either terminal/certain or unexamined leaf node -- the end of this playout. + if (node->IsCertain()) { + return NodeToProcess::TerminalHit(node, depth, 1); + } else if (!node->HasChildren()) { + return NodeToProcess::Extension(node, depth); } Node* possible_shortcut_child = node->GetCachedBestChild(); if (possible_shortcut_child) { // Add two here to reverse the conservatism that goes into calculating the // remaining cache visits. collision_limit = - std::min(collision_limit, node->GetRemainingCacheVisits() + 2); + std::min(collision_limit, node->GetRemainingCacheVisits() + 2); is_root_node = false; node = possible_shortcut_child; node_already_updated = true; @@ -886,6 +935,7 @@ SearchWorker::NodeToProcess SearchWorker::PickNodeToExtend( float second_best = std::numeric_limits::lowest(); int possible_moves = 0; const float fpu = GetFpu(params_, node, is_root_node); + bool parent_upperbounded = node->IsOnlyUBounded(); for (auto child : node->Edges()) { if (is_root_node) { // If there's no chance to catch up to the current best node with @@ -897,6 +947,18 @@ SearchWorker::NodeToProcess SearchWorker::PickNodeToExtend( search_->remaining_playouts_ < best_node_n - child.GetN()) { continue; } + // If CertaintyPropagation >= 1 play certain win and don't search other + // moves at root. If search limit infinite continue searching other + // moves. + if (params_.GetCertaintyPropagation() && + child.edge()->IsCertainWin()) { + if (!search_->limits_.infinite) { + best_edge = child; + possible_moves = 1; + break; + } else if (search_->current_best_edge_ == child && possible_moves > 0) + continue; + } // If root move filter exists, make sure move is in the list. if (!root_move_filter_.empty() && std::find(root_move_filter_.begin(), root_move_filter_.end(), @@ -906,6 +968,19 @@ SearchWorker::NodeToProcess SearchWorker::PickNodeToExtend( ++possible_moves; } float Q = child.GetQ(fpu); + + // Certainty Propagation. Avoid suboptimal childs. + if (params_.GetCertaintyPropagation()) { + // Prefers lower bounded childs over drawing children. + if (child.edge()->IsOnlyLBounded() && child.GetQ(0) <= 0.0f) Q = 0.01f; + // Perfers drawing children over upper bounded childs. + if (child.edge()->IsOnlyUBounded() && child.GetQ(0) >= 0.0f) Q = -0.01f; + // Penalize exploring suboptimal childs throughout the tree. + if (parent_upperbounded) { + if (child.edge()->IsOnlyUBounded()) Q -= child.GetN() * 0.1f; + } + } + const float score = child.GetU(puct_mult) + Q; if (score > best) { second_best = best; @@ -942,36 +1017,19 @@ SearchWorker::NodeToProcess SearchWorker::PickNodeToExtend( } } -void SearchWorker::ExtendNode(Node* node) { - // Initialize position sequence with pre-move position. - history_.Trim(search_->played_history_.GetLength()); - std::vector to_add; - // Could instead reserve one more than the difference between history_.size() - // and history_.capacity(). - to_add.reserve(60); - Node* cur = node; - while (cur != search_->root_node_) { - Node* prev = cur->GetParent(); - to_add.push_back(prev->GetEdgeToNode(cur)->GetMove()); - cur = prev; - } - for (int i = to_add.size() - 1; i >= 0; i--) { - history_.Append(to_add[i]); - } - - // We don't need the mutex because other threads will see that N=0 and - // N-in-flight=1 and will not touch this node. - const auto& board = history_.Last().GetBoard(); - auto legal_moves = board.GenerateLegalMoves(); - +void SearchWorker::EvalPosition(Node* node, MoveList& legal_moves, + const ChessBoard& board, GameResult& result, + CertaintyTrigger& trigger) { // Check whether it's a draw/lose by position. Importantly, we must check // these before doing the by-rule checks below. if (legal_moves.empty()) { // Could be a checkmate or a stalemate if (board.IsUnderCheck()) { - node->MakeTerminal(GameResult::WHITE_WON); + result = GameResult::WHITE_WON; + trigger = CertaintyTrigger::TERMINAL; } else { - node->MakeTerminal(GameResult::DRAW); + result = GameResult::DRAW; + trigger = CertaintyTrigger::TERMINAL; } return; } @@ -980,22 +1038,33 @@ void SearchWorker::ExtendNode(Node* node) { // if they are root, then thinking about them is the point. if (node != search_->root_node_) { if (!board.HasMatingMaterial()) { - node->MakeTerminal(GameResult::DRAW); + result = GameResult::DRAW; + trigger = CertaintyTrigger::TERMINAL; return; } if (history_.Last().GetNoCaptureNoPawnPly() >= 100) { - node->MakeTerminal(GameResult::DRAW); + result = GameResult::DRAW; + trigger = CertaintyTrigger::TERMINAL; return; } if (history_.Last().GetRepetitions() >= 2) { - node->MakeTerminal(GameResult::DRAW); + result = GameResult::DRAW; + trigger = CertaintyTrigger::TERMINAL; + return; + } + + if ((history_.Last().GetRepetitions() >= 1) && + params_.GetCertaintyPropagation()) { + result = GameResult::DRAW; + trigger = CertaintyTrigger::TWO_FOLD; return; } // Neither by-position or by-rule termination, but maybe it's a TB position. - if (search_->syzygy_tb_ && board.castlings().no_legal_castle() && + if (!search_->root_syzygy_rank_ && search_->syzygy_tb_ && + board.castlings().no_legal_castle() && history_.Last().GetNoCaptureNoPawnPly() == 0 && (board.ours() | board.theirs()).count() <= search_->syzygy_tb_->max_cardinality()) { @@ -1006,20 +1075,143 @@ void SearchWorker::ExtendNode(Node* node) { if (state != FAIL) { // If the colors seem backwards, check the checkmate check above. if (wdl == WDL_WIN) { - node->MakeTerminal(GameResult::BLACK_WON); + result = GameResult::BLACK_WON; + trigger = CertaintyTrigger::TB_HIT; } else if (wdl == WDL_LOSS) { - node->MakeTerminal(GameResult::WHITE_WON); + result = GameResult::WHITE_WON; + trigger = CertaintyTrigger::TB_HIT; } else { // Cursed wins and blessed losses count as draws. - node->MakeTerminal(GameResult::DRAW); + result = GameResult::DRAW; + trigger = CertaintyTrigger::NORMAL; } search_->tb_hits_.fetch_add(1, std::memory_order_acq_rel); return; } } } +} + +void SearchWorker::ExtendNode(Node* node) { + // Initialize position sequence with pre-move position. + history_.Trim(search_->played_history_.GetLength()); + std::vector to_add; + // Could instead reserve one more than the difference between history_.size() + // and history_.capacity(). + to_add.reserve(60); + Node* cur = node; + while (cur != search_->root_node_) { + Node* prev = cur->GetParent(); + to_add.push_back(prev->GetEdgeToNode(cur)->GetMove()); + cur = prev; + } + for (int i = to_add.size() - 1; i >= 0; i--) { + history_.Append(to_add[i]); + } + // We don't need the mutex because other threads will see that N=0 and + // N-in-flight=1 and will not touch this node. + const auto& board = history_.Last().GetBoard(); + auto legal_moves = board.GenerateLegalMoves(); + GameResult result = GameResult::UNDECIDED; + CertaintyTrigger trigger = CertaintyTrigger::NONE; + + EvalPosition(node, legal_moves, board, result, trigger); + + if (trigger != CertaintyTrigger::NONE) { + if (trigger == CertaintyTrigger::TERMINAL) + node->MakeTerminal(result); + else + node->MakeCertain(result, trigger); + return; + } // Add legal moves as edges of this node. node->CreateEdges(legal_moves); + + // Certainty Propagation: + // Look-ahead-search of moves to establish bounds and/or certainty. + // TODO: Hashtable, Move-Gen speed, move ordering. + if (params_.GetCertaintyPropagation() && + params_.GetCertaintyPropagationDepth() > 0) { + int node_lowerbound = -1; + int node_upperbound = -1; + bool based_on_propagated_tbhit = false; + for (auto iter : node->Edges()) { + // Eval each edge: Append -> Search -> Pop + history_.Append(iter.GetMove()); + struct Bounds bounds = SearchWorker::NegaBoundSearch( + params_.GetCertaintyPropagationDepth() - 1, -1, 1); + history_.Pop(); + if (bounds.lowerbound == bounds.upperbound) { + iter.edge()->MakeCertain(-bounds.lowerbound, + bounds.based_on_tbhit + ? CertaintyTrigger::TB_HIT + : CertaintyTrigger::NORMAL); + based_on_propagated_tbhit |= bounds.based_on_tbhit; + } else { + if (bounds.lowerbound > -1) iter.edge()->UBound(-bounds.lowerbound); + if (bounds.upperbound < 1) iter.edge()->LBound(-bounds.upperbound); + } + if (-bounds.upperbound > node_lowerbound) + node_lowerbound = -bounds.upperbound; + if (-bounds.lowerbound > node_upperbound) + node_upperbound = -bounds.lowerbound; + } + if (node != search_->root_node_) { + if (node_lowerbound == node_upperbound) { + node->MakeCertain(-node_lowerbound, based_on_propagated_tbhit + ? CertaintyTrigger::TB_HIT + : CertaintyTrigger::NORMAL); + } else { + if (node_lowerbound > -1) node->UBound(-node_lowerbound); + if (node_upperbound < 1) node->LBound(-node_upperbound); + } + } + } +} + +struct SearchWorker::Bounds SearchWorker::NegaBoundSearch(int depth, + int lowerbound, + int upperbound) { + struct Bounds returnbound; + const auto& board = history_.Last().GetBoard(); + auto legal_moves_child = board.GenerateLegalMoves(); + GameResult result = GameResult::UNDECIDED; + CertaintyTrigger trigger = CertaintyTrigger::NONE; + EvalPosition(nullptr, legal_moves_child, board, result, trigger); + + if (trigger != CertaintyTrigger::NONE) { + returnbound.based_on_tbhit |= (trigger == CertaintyTrigger::TB_HIT); + int score = (result == GameResult::WHITE_WON) + ? -1 + : (result == GameResult::BLACK_WON ? 1 : 0); + returnbound.lowerbound = score; + returnbound.upperbound = score; + return returnbound; + } + // Singular-extend positions with only one legal move. + if ((depth == 0 && legal_moves_child.size() != 1) || depth < -1) { + returnbound.lowerbound = -1; + returnbound.upperbound = 1; + return returnbound; + } + + int myupperbound = -1; + for (auto iter : legal_moves_child) { + history_.Append(iter); + // Call recursive NegaBoundSearch with bounds reversed for opponent. + struct Bounds bound = NegaBoundSearch(depth - 1, -upperbound, -lowerbound); + int rlower = -bound.upperbound; + int rupper = -bound.lowerbound; + returnbound.based_on_tbhit |= bound.based_on_tbhit; + history_.Pop(); + if (rlower >= lowerbound) lowerbound = rlower; + if (rupper >= myupperbound) myupperbound = rupper; + // Alpha-Bound cutoff. + if (lowerbound >= upperbound) break; + } + returnbound.lowerbound = lowerbound; + returnbound.upperbound = myupperbound; + return returnbound; } // Returns whether node was already in cache. @@ -1062,7 +1254,9 @@ void SearchWorker::MaybePrefetchIntoCache() { // TODO(mooskagh) Remove prefetch into cache if node collisions work well. // If there are requests to NN, but the batch is not full, try to prefetch // nodes which are likely useful in future. + // TODO(Videodr0me) Maybe use bounds here to more efficiently select nodes. if (search_->stop_.load(std::memory_order_acquire)) return; + if (computation_->GetCacheMisses() > 0 && computation_->GetCacheMisses() < params_.GetMaxPrefetchBatch()) { history_.Trim(search_->played_history_.GetLength()); @@ -1092,8 +1286,8 @@ int SearchWorker::PrefetchIntoCache(Node* node, int budget) { assert(node); // n = 0 and n_in_flight_ > 0, that means the node is being extended. if (node->GetN() == 0) return 0; - // The node is terminal; don't prefetch it. - if (node->IsTerminal()) return 0; + // The node is certain; don't prefetch it. + if (node->IsCertain()) return 0; // Populate all subnodes and their scores. typedef std::pair ScoredEdge; @@ -1172,8 +1366,8 @@ void SearchWorker::FetchSingleNodeResult(NodeToProcess* node_to_process, int idx_in_computation) { Node* node = node_to_process->node; if (!node_to_process->nn_queried) { - // Terminal nodes don't involve the neural NetworkComputation, nor do - // they require any further processing after value retrieval. + // Terminal or certain nodes don't involve the neural NetworkComputation, + // nor do they require any further processing after value retrieval. node_to_process->v = node->GetQ(); return; } @@ -1231,16 +1425,70 @@ void SearchWorker::DoBackupUpdateSingleNode( // Backup V value up to a root. After 1 visit, V = Q. float v = node_to_process.v; + bool origin_bounded = node->IsBounded(); for (Node* n = node; n != search_->root_node_->GetParent(); n = n->GetParent()) { + // Certainty Propagation: + // If update could affect bounds (origin_bounded), + // check all childs, and update bounds/certainty. + float prev_q = -100.0f; + if (params_.GetCertaintyPropagation() && n != node && (origin_bounded) && + !n->IsCertain()) { + bool based_on_propagated_tbhit = false; + int lower_bound = -1; + int upper_bound = -1; + for (auto iter : n->Edges()) { + if (iter.IsLBounded() && iter.GetEQ() > lower_bound) + lower_bound = iter.GetEQ(); + if (iter.IsUBounded() && iter.GetEQ() > upper_bound) + upper_bound = iter.GetEQ(); + // Only checking !UBounded so that lower bounded + // edges, also get the correct upper_bound. + if (!iter.IsUBounded()) upper_bound = 1; + if (lower_bound == upper_bound && lower_bound == 1) { + based_on_propagated_tbhit = iter.IsPropagatedTBHit(); + break; + } + based_on_propagated_tbhit |= iter.IsPropagatedTBHit(); + } + // Exact scores are certain and propagate certainty. + // Inexact scores propagate their bounds. + if (lower_bound == upper_bound) { + if (n != search_->root_node_) { + prev_q = n->GetQ(); + n->MakeCertain(-lower_bound, based_on_propagated_tbhit + ? CertaintyTrigger::TB_HIT + : CertaintyTrigger::NORMAL); + v = (float)-lower_bound; + } + } else { + if (lower_bound > -1) n->UBound(-lower_bound); + if (upper_bound < 1) n->LBound(-upper_bound); + } + } + + // Certainty propagation: reduce error by keeping score in proven bounds. + if (params_.GetCertaintyPropagation() && n->GetParent() && + !n->IsCertain()) { + if (n->GetOwnEdge()->IsUBounded() && v > 0.0f) v = 0.00f; + if (n->GetOwnEdge()->IsLBounded() && v < 0.0f) v = 0.00f; + } + n->FinalizeScoreUpdate(v, node_to_process.multivisit); + + // Certainty propagation: adjust Qs along the path as if all visits already + // had propagated the certain result. + if (params_.GetCertaintyPropagation() && (prev_q != -100.0f) && + (prev_q != v) && n->IsCertain()) + v = v + (v - prev_q) * (n->GetN() - 1); + // Q will be flipped for opponent. v = -v; - // Update the stats. - // Best move. + // Update best move if new N > best N or + // if the node is a certain child of root. if (n->GetParent() == search_->root_node_ && - search_->current_best_edge_.GetN() <= n->GetN()) { + (search_->current_best_edge_.GetN() <= n->GetN() || n->IsCertain())) { search_->current_best_edge_ = search_->GetBestChildNoTemperature(search_->root_node_); } diff --git a/src/mcts/search.h b/src/mcts/search.h index 14b5bcd51f..834a13884c 100644 --- a/src/mcts/search.h +++ b/src/mcts/search.h @@ -30,6 +30,7 @@ #include #include #include +#include "chess/board.h" #include "chess/callbacks.h" #include "chess/uciloop.h" #include "mcts/node.h" @@ -116,15 +117,15 @@ class Search { void SendUciInfo(); // Requires nodes_mutex_ to be held. // Sets stop to true and notifies watchdog thread. void FireStopInternal(); - void SendMovesStats() const; // Function which runs in a separate thread and watches for time and // uci `stop` command; void WatchdogThread(); // Populates the given list with allowed root moves. - // Returns true if the population came from tablebase. - bool PopulateRootMoveLimit(MoveList* root_moves) const; + // Returns best_rank != 0 if the population came from tablebase. + // 1 for draw, > 1 for win and < 1 for loss + int PopulateRootMoveLimit(MoveList* root_moves) const; // Returns verbose information about given node, as vector of strings. std::vector GetVerboseStats(Node* node, @@ -179,6 +180,7 @@ class Search { // Cummulative depth of all paths taken in PickNodetoExtend. uint64_t cum_depth_ GUARDED_BY(nodes_mutex_) = 0; std::atomic tb_hits_{0}; + std::atomic root_syzygy_rank_{0}; BestMoveInfo::Callback best_move_callback_; ThinkingInfo::Callback info_callback_; @@ -239,12 +241,16 @@ class SearchWorker { void UpdateCounters(); private: + struct Bounds { + int lowerbound; + int upperbound; + bool based_on_tbhit = false; + }; + struct NodeToProcess { - bool IsExtendable() const { return !is_collision && !node->IsTerminal(); } + bool IsExtendable() const { return !is_collision && !node->IsCertain(); } bool IsCollision() const { return is_collision; } - bool CanEvalOutOfOrder() const { - return is_cache_hit || node->IsTerminal(); - } + bool CanEvalOutOfOrder() const { return is_cache_hit || node->IsCertain(); } // The node to extend. Node* node; @@ -277,7 +283,10 @@ class SearchWorker { }; NodeToProcess PickNodeToExtend(int collision_limit); + void EvalPosition(Node* node, MoveList& legal_moves, const ChessBoard& board, + GameResult& result, CertaintyTrigger& trigger); void ExtendNode(Node* node); + struct Bounds NegaBoundSearch(int depth, int lowerbound, int upperbound); bool AddNodeToComputation(Node* node, bool add_if_cached); int PrefetchIntoCache(Node* node, int budget); void FetchSingleNodeResult(NodeToProcess* node_to_process, diff --git a/src/selfplay/loop.cc b/src/selfplay/loop.cc index 5a161151d2..1614fef2c4 100644 --- a/src/selfplay/loop.cc +++ b/src/selfplay/loop.cc @@ -24,6 +24,9 @@ terms of the respective license agreement, the licensors of this Program grant you additional permission to convey the resulting work. */ +#include +#include +#include #include "selfplay/loop.h" #include "selfplay/tournament.h" @@ -123,13 +126,56 @@ void SelfPlayLoop::CmdSetOption(const std::string& name, } void SelfPlayLoop::SendTournament(const TournamentInfo& info) { + const int winp1 = info.results[0][0] + info.results[0][1]; + const int loosep1 = info.results[2][0] + info.results[2][1]; + const int draws = info.results[1][0] + info.results[1][1]; + + // Initialize variables + float percentage = -1; + float elo = 10000; + float los = 10000; + + // Only caculate percentage if any games at all (avoid divide by 0). + if ((winp1 + loosep1 + draws) > 0) + percentage = + (static_cast(draws) / 2 + winp1) / (winp1 + loosep1 + draws); + + // Calculate elo and los if percentage strictly between 0 and 1 (avoids divide + // by 0 or overflow). + if ((percentage < 1) && (percentage > 0)) + elo = -400 * log(1 / percentage - 1) / log(10); + if ((winp1 + loosep1) > 0) + los = .5 + + .5 * std::erf((winp1 - loosep1) / std::sqrt(2.0 * (winp1 + loosep1))); + std::string res = "tournamentstatus"; if (info.finished) res += " final"; - res += " win " + std::to_string(info.results[0][0]) + " " + - std::to_string(info.results[0][1]); - res += " lose " + std::to_string(info.results[2][0]) + " " + - std::to_string(info.results[2][1]); - res += " draw " + std::to_string(info.results[1][0]) + " " + + res += " P1: +" + std::to_string(winp1) + " -" + std::to_string(loosep1) + + " =" + std::to_string(draws); + + if (percentage > 0) { + std::ostringstream oss; + oss << std::fixed << std::setw(5) << std::setprecision(2) + << (percentage * 100) << "%"; + res += " Win: " + oss.str(); + } + if (elo < 10000) { + std::ostringstream oss; + oss << std::fixed << std::setw(5) << std::setprecision(2) << (elo); + res += " Elo: " + oss.str(); + } + if (los < 10000) { + std::ostringstream oss; + oss << std::fixed << std::setw(5) << std::setprecision(2) << (los * 100) + << "%"; + res += " LOS: " + oss.str(); + } + res += " P1-W: +" + std::to_string(info.results[0][0]) + " -" + + std::to_string(info.results[2][0]) + " =" + + std::to_string(info.results[1][0]); + // Might be redundant to also list P1-B: + res += " P1-B: +" + std::to_string(info.results[0][1]) + " -" + + std::to_string(info.results[2][1]) + " =" + std::to_string(info.results[1][1]); SendResponse(res); } diff --git a/src/syzygy/syzygy.cc b/src/syzygy/syzygy.cc index 72a69b3b73..aba08a6439 100644 --- a/src/syzygy/syzygy.cc +++ b/src/syzygy/syzygy.cc @@ -1616,9 +1616,9 @@ int SyzygyTablebase::probe_dtz(const Position& pos, ProbeState* result) { } // Use the DTZ tables to rank root moves. -// -// A return value false indicates that not all probes were successful. -bool SyzygyTablebase::root_probe(const Position& pos, bool has_repeated, +// A return value 0 indicates that not all probes were successful. +// Otherwise best rank is returned, with rank 1 = draw. +int SyzygyTablebase::root_probe(const Position& pos, bool has_repeated, std::vector* safe_moves) { ProbeState result; auto root_moves = pos.GetBoard().GenerateLegalMoves(); @@ -1648,14 +1648,14 @@ bool SyzygyTablebase::root_probe(const Position& pos, bool has_repeated, next_pos.GetBoard().GenerateLegalMoves().size() == 0) { dtz = 1; } - if (result == FAIL) return false; + if (result == FAIL) return 0; // Better moves are ranked higher. Certain wins are ranked equally. // Losing moves are ranked equally unless a 50-move draw is in sight. int r = dtz > 0 ? (dtz + cnt50 <= 99 && !rep ? 1000 : 1000 - (dtz + cnt50)) : dtz < 0 ? (-dtz * 2 + cnt50 < 100 ? -1000 : -1000 + (-dtz + cnt50)) - : 0; + : 1; if (r > best_rank) best_rank = r; ranks.push_back(r); } @@ -1667,16 +1667,17 @@ bool SyzygyTablebase::root_probe(const Position& pos, bool has_repeated, } counter++; } - return true; + return best_rank; } // Use the WDL tables to rank root moves. // This is a fallback for the case that some or all DTZ tables are missing. // -// A return value false indicates that not all probes were successful. -bool SyzygyTablebase::root_probe_wdl(const Position& pos, +// A return value 0 indicates that not all probes were successful. +// Otherwise best rank is returned with rank 1 = draw. +int SyzygyTablebase::root_probe_wdl(const Position& pos, std::vector* safe_moves) { - static const int WDL_to_rank[] = {-1000, -899, 0, 899, 1000}; + static const int WDL_to_rank[] = {-1000, -899, 1, 899, 1000}; auto root_moves = pos.GetBoard().GenerateLegalMoves(); ProbeState result; std::vector ranks; @@ -1686,7 +1687,7 @@ bool SyzygyTablebase::root_probe_wdl(const Position& pos, for (auto& m : root_moves) { Position nextPos = Position(pos, m); WDLScore wdl = static_cast(-probe_wdl(nextPos, &result)); - if (result == FAIL) return false; + if (result == FAIL) return 0; ranks.push_back(WDL_to_rank[wdl + 2]); if (ranks.back() > best_rank) best_rank = ranks.back(); } @@ -1698,6 +1699,6 @@ bool SyzygyTablebase::root_probe_wdl(const Position& pos, } counter++; } - return true; + return best_rank; } } // namespace lczero diff --git a/src/syzygy/syzygy.h b/src/syzygy/syzygy.h index 521c0b46d2..0ccc4e9809 100644 --- a/src/syzygy/syzygy.h +++ b/src/syzygy/syzygy.h @@ -87,16 +87,17 @@ class SyzygyTablebase { // has_repeated should be whether there are any repeats since last 50 move // counter reset. // Thread safe. - // Returns false if the position is not in the tablebase. + // Returns 0 if the position is not in the tablebase, and best rank + // if found (1 = draw, > 1 for wins, < 1 for losses) // Safe moves are added to the safe_moves output paramater. - bool root_probe(const Position& pos, bool has_repeated, + int root_probe(const Position& pos, bool has_repeated, std::vector* safe_moves); // Probes WDL tables to determine which moves might be on the optimal play // path. If 50 move ply counter is non-zero some (or maybe even all) of the // returned safe moves in a 'winning' position, may actually be draws. // Returns false if the position is not in the tablebase. // Safe moves are added to the safe_moves output paramater. - bool root_probe_wdl(const Position& pos, std::vector* safe_moves); + int root_probe_wdl(const Position& pos, std::vector* safe_moves); private: template