diff --git a/.github/workflows/stockfish.yml b/.github/workflows/stockfish.yml index e8db52351dd..962db3af844 100644 --- a/.github/workflows/stockfish.yml +++ b/.github/workflows/stockfish.yml @@ -11,6 +11,7 @@ on: branches: - master - tools + jobs: Prerelease: if: github.ref == 'refs/heads/master' diff --git a/.github/workflows/stockfish_format_check.yml b/.github/workflows/stockfish_format_check.yml index 7a47ab6f406..5db5e4303fe 100644 --- a/.github/workflows/stockfish_format_check.yml +++ b/.github/workflows/stockfish_format_check.yml @@ -11,6 +11,7 @@ on: paths: - '**.cpp' - '**.h' + jobs: Stockfish: name: clang-format check diff --git a/src/Makefile b/src/Makefile index e6de514e568..9680ca7feff 100644 --- a/src/Makefile +++ b/src/Makefile @@ -63,7 +63,7 @@ HEADERS = benchmark.h bitboard.h evaluate.h misc.h movegen.h movepick.h \ nnue/layers/sqr_clipped_relu.h nnue/nnue_accumulator.h nnue/nnue_architecture.h \ nnue/nnue_common.h nnue/nnue_feature_transformer.h position.h \ search.h syzygy/tbprobe.h thread.h thread_win32_osx.h timeman.h \ - tt.h tune.h types.h uci.h + tt.h tune.h types.h uci.h ucioption.h OBJS = $(notdir $(SRCS:.cpp=.o)) diff --git a/src/evaluate.cpp b/src/evaluate.cpp index e220b92a7fc..aa2eba05797 100644 --- a/src/evaluate.cpp +++ b/src/evaluate.cpp @@ -34,7 +34,7 @@ #include "nnue/evaluate_nnue.h" #include "nnue/nnue_architecture.h" #include "position.h" -#include "thread.h" +#include "search.h" #include "types.h" #include "uci.h" @@ -62,10 +62,6 @@ namespace Stockfish { namespace Eval { -std::unordered_map EvalFiles = { - {NNUE::Big, {"EvalFile", EvalFileDefaultNameBig, "None"}}, - {NNUE::Small, {"EvalFileSmall", EvalFileDefaultNameSmall, "None"}}}; - // Tries to load a NNUE network at startup time, or when the engine // receives a UCI command "setoption name EvalFile value nn-[a-z0-9]{12}.nnue" @@ -74,7 +70,9 @@ std::unordered_map EvalFiles = { // network may be embedded in the binary), in the active working directory and // in the engine directory. Distro packagers may define the DEFAULT_NNUE_DIRECTORY // variable to have the engine search in a special directory in their distro. -void NNUE::init() { +void NNUE::init(const std::string& binaryDirectory, + const OptionsMap& Options, + std::unordered_map& EvalFiles) { for (auto& [netSize, evalFile] : EvalFiles) { @@ -88,10 +86,10 @@ void NNUE::init() { user_eval_file = evalFile.default_name; #if defined(DEFAULT_NNUE_DIRECTORY) - std::vector dirs = {"", "", CommandLine::binaryDirectory, + std::vector dirs = {"", "", binaryDirectory, stringify(DEFAULT_NNUE_DIRECTORY)}; #else - std::vector dirs = {"", "", CommandLine::binaryDirectory}; + std::vector dirs = {"", "", binaryDirectory}; #endif for (const std::string& directory : dirs) @@ -133,7 +131,8 @@ void NNUE::init() { } // Verifies that the last net used was loaded successfully -void NNUE::verify() { +void NNUE::verify(const OptionsMap& Options, + const std::unordered_map& EvalFiles) { for (const auto& [netSize, evalFile] : EvalFiles) { @@ -183,7 +182,7 @@ int Eval::simple_eval(const Position& pos, Color c) { // Evaluate is the evaluator for the outer world. It returns a static evaluation // of the position from the point of view of the side to move. -Value Eval::evaluate(const Position& pos) { +Value Eval::evaluate(const Position& pos, const Search::Worker& workerThread) { assert(!pos.checkers()); @@ -204,7 +203,7 @@ Value Eval::evaluate(const Position& pos) { Value nnue = smallNet ? NNUE::evaluate(pos, true, &nnueComplexity) : NNUE::evaluate(pos, true, &nnueComplexity); - int optimism = pos.this_thread()->optimism[stm]; + int optimism = workerThread.optimism[stm]; // Blend optimism and eval with nnue complexity and material imbalance optimism += optimism * (nnueComplexity + std::abs(simpleEval - nnue)) / 512; @@ -227,16 +226,16 @@ Value Eval::evaluate(const Position& pos) { // a string (suitable for outputting to stdout) that contains the detailed // descriptions and values of each evaluation term. Useful for debugging. // Trace scores are from white's point of view -std::string Eval::trace(Position& pos) { +std::string Eval::trace(Position& pos, Search::Worker& workerThread) { if (pos.checkers()) return "Final evaluation: none (in check)"; // Reset any global variable used in eval - pos.this_thread()->bestValue = VALUE_ZERO; - pos.this_thread()->rootSimpleEval = VALUE_ZERO; - pos.this_thread()->optimism[WHITE] = VALUE_ZERO; - pos.this_thread()->optimism[BLACK] = VALUE_ZERO; + workerThread.iterBestValue = VALUE_ZERO; + workerThread.rootSimpleEval = VALUE_ZERO; + workerThread.optimism[WHITE] = VALUE_ZERO; + workerThread.optimism[BLACK] = VALUE_ZERO; std::stringstream ss; ss << std::showpoint << std::noshowpos << std::fixed << std::setprecision(2); @@ -249,7 +248,7 @@ std::string Eval::trace(Position& pos) { v = pos.side_to_move() == WHITE ? v : -v; ss << "NNUE evaluation " << 0.01 * UCI::to_cp(v) << " (white side)\n"; - v = evaluate(pos); + v = evaluate(pos, workerThread); v = pos.side_to_move() == WHITE ? v : -v; ss << "Final evaluation " << 0.01 * UCI::to_cp(v) << " (white side)"; ss << " [with scaled NNUE, ...]"; diff --git a/src/evaluate.h b/src/evaluate.h index 79b77192a24..7a36eb3d2b7 100644 --- a/src/evaluate.h +++ b/src/evaluate.h @@ -27,13 +27,16 @@ namespace Stockfish { class Position; +namespace Search { +class Worker; +} namespace Eval { -std::string trace(Position& pos); +std::string trace(Position& pos, Search::Worker& workerThread); int simple_eval(const Position& pos, Color c); -Value evaluate(const Position& pos); +Value evaluate(const Position& pos, const Search::Worker& workerThread); // The default net name MUST follow the format nn-[SHA256 first 12 digits].nnue // for the build process (profile-build and fishtest) to work. Do not change the @@ -41,21 +44,23 @@ Value evaluate(const Position& pos); #define EvalFileDefaultNameBig "nn-baff1edbea57.nnue" #define EvalFileDefaultNameSmall "nn-baff1ede1f90.nnue" +struct EvalFile { + std::string option_name; + std::string default_name; + std::string selected_name; +}; + namespace NNUE { enum NetSize : int; -void init(); -void verify(); +void init(const std::string& binaryDirector, + const OptionsMap& Options, + std::unordered_map&); +void verify(const OptionsMap& Options, const std::unordered_map&); } // namespace NNUE -struct EvalFile { - std::string option_name; - std::string default_name; - std::string selected_name; -}; - extern std::unordered_map EvalFiles; } // namespace Eval diff --git a/src/main.cpp b/src/main.cpp index 78b3f54d919..01a5bf87547 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -16,18 +16,16 @@ along with this program. If not, see . */ -#include #include #include "bitboard.h" #include "evaluate.h" #include "misc.h" #include "position.h" -#include "search.h" -#include "thread.h" #include "tune.h" #include "types.h" #include "uci.h" +#include "ucioption.h" using namespace Stockfish; @@ -35,17 +33,14 @@ int main(int argc, char* argv[]) { std::cout << engine_info() << std::endl; - CommandLine::init(argc, argv); - UCI::init(Options); - Tune::init(); + UCI uci(argc, argv); + + Tune::init(uci.options); Bitboards::init(); Position::init(); - Threads.set(size_t(Options["Threads"])); - Search::clear(); // After threads are up - Eval::NNUE::init(); + Eval::NNUE::init(uci.workingDirectory(), uci.options, uci.EvalFiles); - UCI::loop(argc, argv); + uci.loop(); - Threads.set(0); return 0; } diff --git a/src/misc.cpp b/src/misc.cpp index 9350a4830df..4885a5cd35c 100644 --- a/src/misc.cpp +++ b/src/misc.cpp @@ -721,17 +721,13 @@ void bindThisThread(size_t idx) { #define GETCWD getcwd #endif -namespace CommandLine { - -std::string argv0; // path+name of the executable binary, as given by argv[0] -std::string binaryDirectory; // path of the executable directory -std::string workingDirectory; // path of the working directory - -void init([[maybe_unused]] int argc, char* argv[]) { +CommandLine::CommandLine(int _argc, char** _argv) : + argc(_argc), + argv(_argv) { std::string pathSeparator; // Extract the path+name of the executable binary - argv0 = argv[0]; + std::string argv0 = argv[0]; #ifdef _WIN32 pathSeparator = "\\"; @@ -766,7 +762,4 @@ void init([[maybe_unused]] int argc, char* argv[]) { binaryDirectory.replace(0, 1, workingDirectory); } - -} // namespace CommandLine - } // namespace Stockfish diff --git a/src/misc.h b/src/misc.h index ca6cc16639f..74e11652259 100644 --- a/src/misc.h +++ b/src/misc.h @@ -176,12 +176,17 @@ namespace WinProcGroup { void bindThisThread(size_t idx); } -namespace CommandLine { -void init(int argc, char* argv[]); -extern std::string binaryDirectory; // path of the executable directory -extern std::string workingDirectory; // path of the working directory -} +struct CommandLine { + public: + CommandLine(int argc, char** argv); + + int argc; + char** argv; + + std::string binaryDirectory; // path of the executable directory + std::string workingDirectory; // path of the working directory +}; } // namespace Stockfish diff --git a/src/nnue/evaluate_nnue.cpp b/src/nnue/evaluate_nnue.cpp index 86fe523050a..db3ce452d4d 100644 --- a/src/nnue/evaluate_nnue.cpp +++ b/src/nnue/evaluate_nnue.cpp @@ -441,7 +441,9 @@ bool save_eval(std::ostream& stream, NetSize netSize) { } // Save eval, to a file given by its name -bool save_eval(const std::optional& filename, NetSize netSize) { +bool save_eval(const std::optional& filename, + NetSize netSize, + const std::unordered_map& EvalFiles) { std::string actualFilename; std::string msg; diff --git a/src/nnue/evaluate_nnue.h b/src/nnue/evaluate_nnue.h index fabfb5693f9..fe8566431d3 100644 --- a/src/nnue/evaluate_nnue.h +++ b/src/nnue/evaluate_nnue.h @@ -26,11 +26,13 @@ #include #include #include +#include +#include "../evaluate.h" #include "../misc.h" +#include "../types.h" #include "nnue_architecture.h" #include "nnue_feature_transformer.h" -#include "../types.h" namespace Stockfish { class Position; @@ -75,7 +77,9 @@ void hint_common_parent_position(const Position& pos); bool load_eval(const std::string name, std::istream& stream, NetSize netSize); bool save_eval(std::ostream& stream, NetSize netSize); -bool save_eval(const std::optional& filename, NetSize netSize); +bool save_eval(const std::optional& filename, + NetSize netSize, + const std::unordered_map&); } // namespace Stockfish::Eval::NNUE diff --git a/src/position.cpp b/src/position.cpp index ddc31888422..6202381d072 100644 --- a/src/position.cpp +++ b/src/position.cpp @@ -19,7 +19,6 @@ #include "position.h" #include -#include #include #include #include @@ -36,7 +35,6 @@ #include "movegen.h" #include "nnue/nnue_common.h" #include "syzygy/tbprobe.h" -#include "thread.h" #include "tt.h" #include "uci.h" @@ -87,7 +85,7 @@ std::ostream& operator<<(std::ostream& os, const Position& pos) { ASSERT_ALIGNED(&st, Eval::NNUE::CacheLineSize); Position p; - p.set(pos.fen(), pos.is_chess960(), &st, pos.this_thread()); + p.set(pos.fen(), pos.is_chess960(), &st); Tablebases::ProbeState s1, s2; Tablebases::WDLScore wdl = Tablebases::probe_wdl(p, &s1); int dtz = Tablebases::probe_dtz(p, &s2); @@ -160,7 +158,7 @@ void Position::init() { // Initializes the position object with the given FEN string. // 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 string& fenStr, bool isChess960, StateInfo* si, Thread* th) { +Position& Position::set(const string& fenStr, bool isChess960, StateInfo* si) { /* A FEN string defines a particular position using only the ASCII character set. @@ -286,8 +284,7 @@ Position& Position::set(const string& fenStr, bool isChess960, StateInfo* si, Th // handle also common incorrect FEN with fullmove = 0. gamePly = std::max(2 * (gamePly - 1), 0) + (sideToMove == BLACK); - chess960 = isChess960; - thisThread = th; + chess960 = isChess960; set_state(); assert(pos_is_ok()); @@ -388,7 +385,7 @@ Position& Position::set(const string& code, Color c, StateInfo* si) { string fenStr = "8/" + sides[0] + char(8 - sides[0].length() + '0') + "/8/8/8/8/" + sides[1] + char(8 - sides[1].length() + '0') + "/8 w - - 0 10"; - return set(fenStr, false, si, nullptr); + return set(fenStr, false, si); } @@ -667,7 +664,6 @@ void Position::do_move(Move m, StateInfo& newSt, bool givesCheck) { assert(m.is_ok()); assert(&newSt != st); - thisThread->nodes.fetch_add(1, std::memory_order_relaxed); Key k = st->key ^ Zobrist::side; // Copy some fields of the old state to our new StateInfo object except the @@ -959,7 +955,7 @@ void Position::do_castling(Color us, Square from, Square& to, Square& rfrom, Squ // Used to do a "null move": it flips // the side to move without executing any move on the board. -void Position::do_null_move(StateInfo& newSt) { +void Position::do_null_move(StateInfo& newSt, TranspositionTable& tt) { assert(!checkers()); assert(&newSt != st); @@ -982,7 +978,7 @@ void Position::do_null_move(StateInfo& newSt) { st->key ^= Zobrist::side; ++st->rule50; - prefetch(TT.first_entry(key())); + prefetch(tt.first_entry(key())); st->pliesFromNull = 0; @@ -1235,7 +1231,7 @@ void Position::flip() { std::getline(ss, token); // Half and full moves f += token; - set(f, is_chess960(), st, this_thread()); + set(f, is_chess960(), st); assert(pos_is_ok()); } diff --git a/src/position.h b/src/position.h index 34b53f4a558..7ce3556f0e9 100644 --- a/src/position.h +++ b/src/position.h @@ -32,6 +32,8 @@ namespace Stockfish { +class TranspositionTable; + // StateInfo struct stores information needed to restore a Position object to // its previous state when we retract a move. Whenever a move is made on the // board (by calling Position::do_move), a StateInfo object must be passed. @@ -75,8 +77,6 @@ using StateListPtr = std::unique_ptr>; // pieces, side to move, hash keys, castling info, etc. Important methods are // do_move() and undo_move(), used by the search to update node info when // traversing the search tree. -class Thread; - class Position { public: static void init(); @@ -86,7 +86,7 @@ class Position { Position& operator=(const Position&) = delete; // FEN string input/output - Position& set(const std::string& fenStr, bool isChess960, StateInfo* si, Thread* th); + Position& set(const std::string& fenStr, bool isChess960, StateInfo* si); Position& set(const std::string& code, Color c, StateInfo* si); std::string fen() const; @@ -139,7 +139,7 @@ class Position { void do_move(Move m, StateInfo& newSt); void do_move(Move m, StateInfo& newSt, bool givesCheck); void undo_move(Move m); - void do_null_move(StateInfo& newSt); + void do_null_move(StateInfo& newSt, TranspositionTable& tt); void undo_null_move(); // Static Exchange Evaluation @@ -152,16 +152,15 @@ class Position { Key pawn_key() const; // Other properties of the position - Color side_to_move() const; - int game_ply() const; - bool is_chess960() const; - Thread* this_thread() const; - bool is_draw(int ply) const; - bool has_game_cycle(int ply) const; - bool has_repeated() const; - int rule50_count() const; - Value non_pawn_material(Color c) const; - Value non_pawn_material() const; + Color side_to_move() const; + int game_ply() const; + bool is_chess960() const; + bool is_draw(int ply) const; + bool has_game_cycle(int ply) const; + bool has_repeated() const; + int rule50_count() const; + Value non_pawn_material(Color c) const; + Value non_pawn_material() const; // Position consistency check, for debugging bool pos_is_ok() const; @@ -194,7 +193,6 @@ class Position { int castlingRightsMask[SQUARE_NB]; Square castlingRookSquare[CASTLING_RIGHT_NB]; Bitboard castlingPath[CASTLING_RIGHT_NB]; - Thread* thisThread; StateInfo* st; int gamePly; Color sideToMove; @@ -328,8 +326,6 @@ inline bool Position::capture_stage(Move m) const { inline Piece Position::captured_piece() const { return st->capturedPiece; } -inline Thread* Position::this_thread() const { return thisThread; } - inline void Position::put_piece(Piece pc, Square s) { board[s] = pc; diff --git a/src/search.cpp b/src/search.cpp index e93b12d1598..5cd2361e6a8 100644 --- a/src/search.cpp +++ b/src/search.cpp @@ -27,7 +27,6 @@ #include #include #include -#include #include #include @@ -44,14 +43,10 @@ #include "timeman.h" #include "tt.h" #include "uci.h" +#include "ucioption.h" namespace Stockfish { -namespace Search { - -LimitsType Limits; -} - namespace Tablebases { int Cardinality; @@ -68,12 +63,6 @@ using namespace Search; namespace { -// Different node types, used as a template parameter -enum NodeType { - NonPV, - PV, - Root -}; // Futility margin Value futility_margin(Depth d, bool noTtCutNode, bool improving) { @@ -105,9 +94,7 @@ int stat_bonus(Depth d) { return std::min(268 * d - 352, 1153); } int stat_malus(Depth d) { return std::min(400 * d - 354, 1201); } // Add a small random component to draw evaluations to avoid 3-fold blindness -Value value_draw(const Thread* thisThread) { - return VALUE_DRAW - 1 + Value(thisThread->nodes & 0x2); -} +Value value_draw(size_t nodes) { return VALUE_DRAW - 1 + Value(nodes & 0x2); } // Skill structure is used to implement strength limit. If we have a UCI_Elo, // we convert it to an appropriate skill level, anchored to the Stash engine. @@ -127,34 +114,30 @@ struct Skill { } bool enabled() const { return level < 20.0; } bool time_to_pick(Depth depth) const { return depth == 1 + int(level); } - Move pick_best(size_t multiPV); + Move pick_best(const RootMoves&, size_t multiPV); double level; Move best = Move::none(); }; -template -Value search(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth, bool cutNode); - -template -Value qsearch(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth = 0); - Value value_to_tt(Value v, int ply); Value value_from_tt(Value v, int ply, int r50c); void update_pv(Move* pv, Move move, const Move* childPv); void update_continuation_histories(Stack* ss, Piece pc, Square to, int bonus); -void update_quiet_stats(const Position& pos, Stack* ss, Move move, int bonus); -void update_all_stats(const Position& pos, - Stack* ss, - Move bestMove, - Value bestValue, - Value beta, - Square prevSq, - Move* quietsSearched, - int quietCount, - Move* capturesSearched, - int captureCount, - Depth depth); +void update_quiet_stats( + const Position& pos, Stack* ss, Search::Worker& workerThread, Move move, int bonus); +void update_all_stats(const Position& pos, + Stack* ss, + Search::Worker& workerThread, + Move bestMove, + Value bestValue, + Value beta, + Square prevSq, + Move* quietsSearched, + int quietCount, + Move* capturesSearched, + int captureCount, + Depth depth); // Utility to verify move generation. All the leaf nodes up // to the given depth are generated and counted, and the sum is returned. @@ -188,41 +171,28 @@ uint64_t perft(Position& pos, Depth depth) { // Called at startup to initialize various lookup tables -void Search::init() { +void Search::init(int size) { for (int i = 1; i < MAX_MOVES; ++i) - Reductions[i] = int((20.37 + std::log(Threads.size()) / 2) * std::log(i)); -} - - -// Resets search state to its initial value -void Search::clear() { - - Threads.main()->wait_for_search_finished(); - - Time.availableNodes = 0; - TT.clear(); - Threads.clear(); - Tablebases::init(Options["SyzygyPath"]); // Free mapped files + Reductions[i] = int((20.37 + std::log(size) / 2) * std::log(i)); } +void Search::Worker::start_searching() { + // Non-main threads go directly to iterative_deepening() + if (thread_idx != 0) + { + iterative_deepening(); + return; + } -// Called when the program receives the UCI 'go' -// command. It searches from the root position and outputs the "bestmove". -void MainThread::search() { - - if (Limits.perft) + if (limits.perft) { - nodes = perft(rootPos, Limits.perft); + nodes = perft(rootPos, limits.perft); sync_cout << "\nNodes searched: " << nodes << "\n" << sync_endl; return; } - Color us = rootPos.side_to_move(); - Time.init(Limits, us, rootPos.game_ply()); - TT.new_search(); - - Eval::NNUE::verify(); + tt.new_search(); if (rootMoves.empty()) { @@ -232,73 +202,75 @@ void MainThread::search() { } else { - Threads.start_searching(); // start non-main threads - Thread::search(); // main thread start searching + threads.start_searching(); // start non-main threads + iterative_deepening(); // main thread start searching } // When we reach the maximum depth, we can arrive here without a raise of - // Threads.stop. However, if we are pondering or in an infinite search, + // threads.stop. However, if we are pondering or in an infinite search, // the UCI protocol states that we shouldn't print the best move before the // GUI sends a "stop" or "ponderhit" command. We therefore simply wait here // until the GUI sends one of those commands. - - while (!Threads.stop && (ponder || Limits.infinite)) + while (!threads.stop && (main_manager()->ponder || limits.infinite)) {} // Busy wait for a stop or a ponder reset // Stop the threads if not already stopped (also raise the stop if - // "ponderhit" just reset Threads.ponder). - Threads.stop = true; + // "ponderhit" just reset threads.ponder). + threads.stop = true; // Wait until all threads have finished - Threads.wait_for_search_finished(); + threads.wait_for_search_finished(); // When playing in 'nodes as time' mode, subtract the searched nodes from // the available ones before exiting. - if (Limits.npmsec) - Time.availableNodes += Limits.inc[us] - Threads.nodes_searched(); + if (limits.npmsec) + main_manager()->tm.availableNodes += + limits.inc[rootPos.side_to_move()] - threads.nodes_searched(); - Thread* bestThread = this; + Worker* bestThread = this; Skill skill = - Skill(Options["Skill Level"], Options["UCI_LimitStrength"] ? int(Options["UCI_Elo"]) : 0); + Skill(options["Skill Level"], options["UCI_LimitStrength"] ? int(options["UCI_Elo"]) : 0); - if (int(Options["MultiPV"]) == 1 && !Limits.depth && !skill.enabled() + if (int(options["MultiPV"]) == 1 && !limits.depth && !skill.enabled() && rootMoves[0].pv[0] != Move::none()) - bestThread = Threads.get_best_thread(); + bestThread = threads.get_best_thread()->worker.get(); - bestPreviousScore = bestThread->rootMoves[0].score; - bestPreviousAverageScore = bestThread->rootMoves[0].averageScore; + main_manager()->bestPreviousScore = bestThread->rootMoves[0].score; + main_manager()->bestPreviousAverageScore = bestThread->rootMoves[0].averageScore; // Send again PV info if we have a new best thread if (bestThread != this) - sync_cout << UCI::pv(bestThread->rootPos, bestThread->completedDepth) << sync_endl; + sync_cout << UCI::pv(*bestThread, main_manager()->tm.elapsed(threads.nodes_searched()), + threads.nodes_searched(), threads.tb_hits(), tt.hashfull(), + TB::RootInTB) + << sync_endl; sync_cout << "bestmove " << UCI::move(bestThread->rootMoves[0].pv[0], rootPos.is_chess960()); if (bestThread->rootMoves[0].pv.size() > 1 - || bestThread->rootMoves[0].extract_ponder_from_tt(rootPos)) + || bestThread->rootMoves[0].extract_ponder_from_tt(tt, rootPos)) std::cout << " ponder " << UCI::move(bestThread->rootMoves[0].pv[1], rootPos.is_chess960()); std::cout << sync_endl; } - // Main iterative deepening loop. It calls search() // repeatedly with increasing depth until the allocated thinking time has been // consumed, the user stops the search, or the maximum search depth is reached. -void Thread::search() { +void Search::Worker::iterative_deepening() { // Allocate stack with extra size to allow access from (ss - 7) to (ss + 2): // (ss - 7) is needed for update_continuation_histories(ss - 1) which accesses (ss - 6), // (ss + 2) is needed for initialization of cutOffCnt and killers. - Stack stack[MAX_PLY + 10], *ss = stack + 7; - Move pv[MAX_PLY + 1]; - Value alpha, beta; - Move lastBestMove = Move::none(); - Depth lastBestMoveDepth = 0; - MainThread* mainThread = (this == Threads.main() ? Threads.main() : nullptr); - double timeReduction = 1, totBestMoveChanges = 0; - Color us = rootPos.side_to_move(); - int delta, iterIdx = 0; + Stack stack[MAX_PLY + 10], *ss = stack + 7; + Move pv[MAX_PLY + 1]; + Value alpha, beta; + Move lastBestMove = Move::none(); + Depth lastBestMoveDepth = 0; + SearchManager* mainThread = (thread_idx == 0 ? main_manager() : nullptr); + double timeReduction = 1, totBestMoveChanges = 0; + Color us = rootPos.side_to_move(); + int delta, iterIdx = 0; std::memset(ss - 7, 0, 10 * sizeof(Stack)); for (int i = 7; i > 0; --i) @@ -313,7 +285,7 @@ void Thread::search() { ss->pv = pv; - bestValue = -VALUE_INFINITE; + iterBestValue = -VALUE_INFINITE; if (mainThread) { @@ -325,8 +297,8 @@ void Thread::search() { mainThread->iterValue[i] = mainThread->bestPreviousScore; } - size_t multiPV = size_t(Options["MultiPV"]); - Skill skill(Options["Skill Level"], Options["UCI_LimitStrength"] ? int(Options["UCI_Elo"]) : 0); + size_t multiPV = size_t(options["MultiPV"]); + Skill skill(options["Skill Level"], options["UCI_LimitStrength"] ? int(options["UCI_Elo"]) : 0); // When playing with strength handicap enable MultiPV search that we will // use behind-the-scenes to retrieve a set of possible moves. @@ -338,8 +310,8 @@ void Thread::search() { int searchAgainCounter = 0; // Iterative deepening loop until requested to stop or the target depth is reached - while (++rootDepth < MAX_PLY && !Threads.stop - && !(Limits.depth && mainThread && rootDepth > Limits.depth)) + while (++rootDepth < MAX_PLY && !threads.stop + && !(limits.depth && mainThread && rootDepth > limits.depth)) { // Age out PV variability metric if (mainThread) @@ -353,11 +325,11 @@ void Thread::search() { size_t pvFirst = 0; pvLast = 0; - if (!Threads.increaseDepth) + if (!threads.increaseDepth) searchAgainCounter++; // MultiPV loop. We perform a full root search for each PV line - for (pvIdx = 0; pvIdx < multiPV && !Threads.stop; ++pvIdx) + for (pvIdx = 0; pvIdx < multiPV && !threads.stop; ++pvIdx) { if (pvIdx == pvLast) { @@ -390,7 +362,7 @@ void Thread::search() { // for every four searchAgain steps (see issue #2717). Depth adjustedDepth = std::max(1, rootDepth - failedHighCnt - 3 * (searchAgainCounter + 1) / 4); - bestValue = Stockfish::search(rootPos, ss, alpha, beta, adjustedDepth, false); + iterBestValue = search(rootPos, ss, alpha, beta, adjustedDepth, false); // Bring the best move to the front. It is critical that sorting // is done with a stable algorithm because all the values but the @@ -403,29 +375,32 @@ void Thread::search() { // If search has been stopped, we break immediately. Sorting is // safe because RootMoves is still valid, although it refers to // the previous iteration. - if (Threads.stop) + if (threads.stop) break; // When failing high/low give some update (without cluttering // the UI) before a re-search. - if (mainThread && multiPV == 1 && (bestValue <= alpha || bestValue >= beta) - && Time.elapsed() > 3000) - sync_cout << UCI::pv(rootPos, rootDepth) << sync_endl; + if (mainThread && multiPV == 1 && (iterBestValue <= alpha || iterBestValue >= beta) + && mainThread->tm.elapsed(threads.nodes_searched()) > 3000) + sync_cout << UCI::pv(*this, mainThread->tm.elapsed(threads.nodes_searched()), + threads.nodes_searched(), threads.tb_hits(), tt.hashfull(), + TB::RootInTB) + << sync_endl; // In case of failing low/high increase aspiration window and // re-search, otherwise exit the loop. - if (bestValue <= alpha) + if (iterBestValue <= alpha) { beta = (alpha + beta) / 2; - alpha = std::max(bestValue - delta, -VALUE_INFINITE); + alpha = std::max(iterBestValue - delta, -VALUE_INFINITE); failedHighCnt = 0; if (mainThread) mainThread->stopOnPonderhit = false; } - else if (bestValue >= beta) + else if (iterBestValue >= beta) { - beta = std::min(bestValue + delta, int(VALUE_INFINITE)); + beta = std::min(iterBestValue + delta, int(VALUE_INFINITE)); ++failedHighCnt; } else @@ -439,11 +414,16 @@ void Thread::search() { // Sort the PV lines searched so far and update the GUI std::stable_sort(rootMoves.begin() + pvFirst, rootMoves.begin() + pvIdx + 1); - if (mainThread && (Threads.stop || pvIdx + 1 == multiPV || Time.elapsed() > 3000)) - sync_cout << UCI::pv(rootPos, rootDepth) << sync_endl; + if (mainThread + && (threads.stop || pvIdx + 1 == multiPV + || mainThread->tm.elapsed(threads.nodes_searched()) > 3000)) + sync_cout << UCI::pv(*this, mainThread->tm.elapsed(threads.nodes_searched()), + threads.nodes_searched(), threads.tb_hits(), tt.hashfull(), + TB::RootInTB) + << sync_endl; } - if (!Threads.stop) + if (!threads.stop) completedDepth = rootDepth; if (rootMoves[0].pv[0] != lastBestMove) @@ -453,60 +433,62 @@ void Thread::search() { } // Have we found a "mate in x"? - if (Limits.mate && bestValue >= VALUE_MATE_IN_MAX_PLY - && VALUE_MATE - bestValue <= 2 * Limits.mate) - Threads.stop = true; + if (limits.mate && iterBestValue >= VALUE_MATE_IN_MAX_PLY + && VALUE_MATE - iterBestValue <= 2 * limits.mate) + threads.stop = true; if (!mainThread) continue; // If the skill level is enabled and time is up, pick a sub-optimal best move if (skill.enabled() && skill.time_to_pick(rootDepth)) - skill.pick_best(multiPV); + skill.pick_best(rootMoves, multiPV); // Use part of the gained time from a previous stable move for the current move - for (Thread* th : Threads) + for (Thread* th : threads) { - totBestMoveChanges += th->bestMoveChanges; - th->bestMoveChanges = 0; + totBestMoveChanges += th->worker->bestMoveChanges; + th->worker->bestMoveChanges = 0; } // Do we have time for the next iteration? Can we stop searching now? - if (Limits.use_time_management() && !Threads.stop && !mainThread->stopOnPonderhit) + if (limits.use_time_management() && !threads.stop && !mainThread->stopOnPonderhit) { - double fallingEval = (66 + 14 * (mainThread->bestPreviousAverageScore - bestValue) - + 6 * (mainThread->iterValue[iterIdx] - bestValue)) + double fallingEval = (66 + 14 * (mainThread->bestPreviousAverageScore - iterBestValue) + + 6 * (mainThread->iterValue[iterIdx] - iterBestValue)) / 616.6; fallingEval = std::clamp(fallingEval, 0.51, 1.51); // If the bestMove is stable over several iterations, reduce time accordingly timeReduction = lastBestMoveDepth + 8 < completedDepth ? 1.56 : 0.69; double reduction = (1.4 + mainThread->previousTimeReduction) / (2.17 * timeReduction); - double bestMoveInstability = 1 + 1.79 * totBestMoveChanges / Threads.size(); + double bestMoveInstability = 1 + 1.79 * totBestMoveChanges / threads.size(); - double totalTime = Time.optimum() * fallingEval * reduction * bestMoveInstability; + double totalTime = + mainThread->tm.optimum() * fallingEval * reduction * bestMoveInstability; // Cap used time in case of a single legal move for a better viewer experience if (rootMoves.size() == 1) totalTime = std::min(500.0, totalTime); // Stop the search if we have exceeded the totalTime - if (Time.elapsed() > totalTime) + if (mainThread->tm.elapsed(threads.nodes_searched()) > totalTime) { // If we are allowed to ponder do not stop the search now but // keep pondering until the GUI sends "ponderhit" or "stop". if (mainThread->ponder) mainThread->stopOnPonderhit = true; else - Threads.stop = true; + threads.stop = true; } - else if (!mainThread->ponder && Time.elapsed() > totalTime * 0.50) - Threads.increaseDepth = false; + else if (!mainThread->ponder + && mainThread->tm.elapsed(threads.nodes_searched()) > totalTime * 0.50) + threads.increaseDepth = false; else - Threads.increaseDepth = true; + threads.increaseDepth = true; } - mainThread->iterValue[iterIdx] = bestValue; + mainThread->iterValue[iterIdx] = iterBestValue; iterIdx = (iterIdx + 1) & 3; } @@ -517,19 +499,34 @@ void Thread::search() { // If the skill level is enabled, swap the best PV line with the sub-optimal one if (skill.enabled()) - std::swap(rootMoves[0], *std::find(rootMoves.begin(), rootMoves.end(), - skill.best ? skill.best : skill.pick_best(multiPV))); + std::swap(rootMoves[0], + *std::find(rootMoves.begin(), rootMoves.end(), + skill.best ? skill.best : skill.pick_best(rootMoves, multiPV))); } +void Search::Worker::clear() { + counterMoves.fill(Move::none()); + mainHistory.fill(0); + captureHistory.fill(0); + pawnHistory.fill(0); + correctionHistory.fill(0); + + for (bool inCheck : {false, true}) + for (StatsType c : {NoCaptures, Captures}) + for (auto& to : continuationHistory[inCheck][c]) + for (auto& h : to) + h->fill(-71); +} -namespace { // Main search function for both PV and non-PV nodes template -Value search(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth, bool cutNode) { +Value Search::Worker::search( + Position& pos, Stack* ss, Value alpha, Value beta, Depth depth, bool cutNode) { - constexpr bool PvNode = nodeType != NonPV; - constexpr bool rootNode = nodeType == Root; + constexpr bool PvNode = nodeType != NonPV; + constexpr bool rootNode = nodeType == Root; + const auto thisThread = this; // To easier distinguish local from member variables // Dive into quiescence search when the depth reaches zero if (depth <= 0) @@ -539,7 +536,7 @@ Value search(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth, boo // if the opponent had an alternative move earlier to this position. if (!rootNode && alpha < VALUE_DRAW && pos.has_game_cycle(ss->ply)) { - alpha = value_draw(pos.this_thread()); + alpha = value_draw(thisThread->nodes); if (alpha >= beta) return alpha; } @@ -564,17 +561,16 @@ Value search(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth, boo int moveCount, captureCount, quietCount; // Step 1. Initialize node - Thread* thisThread = pos.this_thread(); - ss->inCheck = pos.checkers(); - priorCapture = pos.captured_piece(); - Color us = pos.side_to_move(); + ss->inCheck = pos.checkers(); + priorCapture = pos.captured_piece(); + Color us = pos.side_to_move(); moveCount = captureCount = quietCount = ss->moveCount = 0; bestValue = -VALUE_INFINITE; maxValue = VALUE_INFINITE; // Check for the available remaining time - if (thisThread == Threads.main()) - static_cast(thisThread)->check_time(); + if (is_mainthread()) + main_manager()->check_time(*this); // Used to send selDepth info to GUI (selDepth counts from 1, ply from 0) if (PvNode && thisThread->selDepth < ss->ply + 1) @@ -583,10 +579,10 @@ Value search(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth, boo if (!rootNode) { // Step 2. Check for aborted search and immediate draw - if (Threads.stop.load(std::memory_order_relaxed) || pos.is_draw(ss->ply) + if (threads.stop.load(std::memory_order_relaxed) || pos.is_draw(ss->ply) || ss->ply >= MAX_PLY) - return (ss->ply >= MAX_PLY && !ss->inCheck) ? evaluate(pos) - : value_draw(pos.this_thread()); + return (ss->ply >= MAX_PLY && !ss->inCheck) ? evaluate(pos, *this) + : value_draw(thisThread->nodes); // Step 3. Mate distance pruning. Even if we mate at the next move our score // would be at best mate_in(ss->ply + 1), but if alpha is already bigger because @@ -614,7 +610,7 @@ Value search(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth, boo // Step 4. Transposition table lookup. excludedMove = ss->excludedMove; posKey = pos.key(); - tte = TT.probe(posKey, ss->ttHit); + tte = tt.probe(posKey, ss->ttHit); ttValue = ss->ttHit ? value_from_tt(tte->value(), ss->ply, pos.rule50_count()) : VALUE_NONE; ttMove = rootNode ? thisThread->rootMoves[thisThread->pvIdx].pv[0] : ss->ttHit ? tte->move() @@ -638,7 +634,7 @@ Value search(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth, boo { // Bonus for a quiet ttMove that fails high (~2 Elo) if (!ttCapture) - update_quiet_stats(pos, ss, ttMove, stat_bonus(depth)); + update_quiet_stats(pos, ss, *this, ttMove, stat_bonus(depth)); // Extra penalty for early quiet moves of // the previous ply (~0 Elo on STC, ~2 Elo on LTC). @@ -676,8 +672,8 @@ Value search(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth, boo TB::WDLScore wdl = Tablebases::probe_wdl(pos, &err); // Force check of time on the next occasion - if (thisThread == Threads.main()) - static_cast(thisThread)->callsCnt = 0; + if (is_mainthread()) + main_manager()->callsCnt = 0; if (err != TB::ProbeState::FAIL) { @@ -699,7 +695,8 @@ Value search(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth, boo if (b == BOUND_EXACT || (b == BOUND_LOWER ? value >= beta : value <= alpha)) { tte->save(posKey, value_to_tt(value, ss->ply), ss->ttPv, b, - std::min(MAX_PLY - 1, depth + 6), Move::none(), VALUE_NONE); + std::min(MAX_PLY - 1, depth + 6), Move::none(), VALUE_NONE, + tt.generation()); return value; } @@ -715,7 +712,6 @@ Value search(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth, boo } } - CapturePieceToHistory& captureHistory = thisThread->captureHistory; Value unadjustedStaticEval = VALUE_NONE; @@ -739,7 +735,7 @@ Value search(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth, boo // Never assume anything about values stored in TT unadjustedStaticEval = ss->staticEval = eval = tte->eval(); if (eval == VALUE_NONE) - unadjustedStaticEval = ss->staticEval = eval = evaluate(pos); + unadjustedStaticEval = ss->staticEval = eval = evaluate(pos, *this); else if (PvNode) Eval::NNUE::hint_common_parent_position(pos); @@ -757,7 +753,7 @@ Value search(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth, boo } else { - unadjustedStaticEval = ss->staticEval = eval = evaluate(pos); + unadjustedStaticEval = ss->staticEval = eval = evaluate(pos, *this); Value newEval = ss->staticEval @@ -769,7 +765,7 @@ Value search(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth, boo // Static evaluation is saved as it was before adjustment by correction history tte->save(posKey, VALUE_NONE, ss->ttPv, BOUND_NONE, DEPTH_NONE, Move::none(), - unadjustedStaticEval); + unadjustedStaticEval, tt.generation()); } // Use static evaluation difference to improve quiet move ordering (~9 Elo) @@ -827,7 +823,7 @@ Value search(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth, boo ss->currentMove = Move::null(); ss->continuationHistory = &thisThread->continuationHistory[0][0][NO_PIECE][0]; - pos.do_null_move(st); + pos.do_null_move(st, tt); Value nullValue = -search(pos, ss + 1, -beta, -beta + 1, depth - R, !cutNode); @@ -885,7 +881,7 @@ Value search(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth, boo { assert(probCutBeta < VALUE_INFINITE && probCutBeta > beta); - MovePicker mp(pos, ttMove, probCutBeta - ss->staticEval, &captureHistory); + MovePicker mp(pos, ttMove, probCutBeta - ss->staticEval, &thisThread->captureHistory); while ((move = mp.next_move()) != Move::none()) if (move != excludedMove && pos.legal(move)) @@ -893,13 +889,14 @@ Value search(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth, boo assert(pos.capture_stage(move)); // Prefetch the TT entry for the resulting position - prefetch(TT.first_entry(pos.key_after(move))); + prefetch(tt.first_entry(pos.key_after(move))); ss->currentMove = move; ss->continuationHistory = - &thisThread + &this ->continuationHistory[ss->inCheck][true][pos.moved_piece(move)][move.to_sq()]; + thisThread->nodes.fetch_add(1, std::memory_order_relaxed); pos.do_move(move, st); // Perform a preliminary qsearch to verify that the move holds @@ -916,7 +913,7 @@ Value search(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth, boo { // Save ProbCut data into transposition table tte->save(posKey, value_to_tt(value, ss->ply), ss->ttPv, BOUND_LOWER, depth - 3, - move, unadjustedStaticEval); + move, unadjustedStaticEval, tt.generation()); return std::abs(value) < VALUE_TB_WIN_IN_MAX_PLY ? value - (probCutBeta - beta) : value; } @@ -944,8 +941,8 @@ Value search(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth, boo Move countermove = prevSq != SQ_NONE ? thisThread->counterMoves[pos.piece_on(prevSq)][prevSq] : Move::none(); - MovePicker mp(pos, ttMove, depth, &thisThread->mainHistory, &captureHistory, contHist, - &thisThread->pawnHistory, countermove, ss->killers); + MovePicker mp(pos, ttMove, depth, &thisThread->mainHistory, &thisThread->captureHistory, + contHist, &thisThread->pawnHistory, countermove, ss->killers); value = bestValue; moveCountPruning = singularQuietLMR = false; @@ -978,7 +975,8 @@ Value search(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth, boo ss->moveCount = ++moveCount; - if (rootNode && thisThread == Threads.main() && Time.elapsed() > 3000) + if (rootNode && is_mainthread() + && main_manager()->tm.elapsed(threads.nodes_searched()) > 3000) sync_cout << "info depth " << depth << " currmove " << UCI::move(move, pos.is_chess960()) << " currmovenumber " << moveCount + thisThread->pvIdx << sync_endl; @@ -1016,7 +1014,8 @@ Value search(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth, boo Piece capturedPiece = pos.piece_on(move.to_sq()); int futilityEval = ss->staticEval + 238 + 305 * lmrDepth + PieceValue[capturedPiece] - + captureHistory[movedPiece][move.to_sq()][type_of(capturedPiece)] / 7; + + thisThread->captureHistory[movedPiece][move.to_sq()][type_of(capturedPiece)] + / 7; if (futilityEval < alpha) continue; } @@ -1135,8 +1134,8 @@ Value search(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth, boo // Recapture extensions (~1 Elo) else if (PvNode && move == ttMove && move.to_sq() == prevSq - && captureHistory[movedPiece][move.to_sq()] - [type_of(pos.piece_on(move.to_sq()))] + && thisThread->captureHistory[movedPiece][move.to_sq()] + [type_of(pos.piece_on(move.to_sq()))] > 4146) extension = 1; } @@ -1146,7 +1145,7 @@ Value search(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth, boo ss->doubleExtensions = (ss - 1)->doubleExtensions + (extension == 2); // Speculative prefetch as early as possible - prefetch(TT.first_entry(pos.key_after(move))); + prefetch(tt.first_entry(pos.key_after(move))); // Update the current move (this must be done after singular extension search) ss->currentMove = move; @@ -1154,6 +1153,7 @@ Value search(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth, boo &thisThread->continuationHistory[ss->inCheck][capture][movedPiece][move.to_sq()]; // Step 16. Make the move + thisThread->nodes.fetch_add(1, std::memory_order_relaxed); pos.do_move(move, st, givesCheck); // Decrease reduction if position is or has been on the PV (~4 Elo) @@ -1269,7 +1269,7 @@ Value search(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth, boo // Finished searching the move. If a stop occurred, the return value of // the search cannot be trusted, and we return immediately without // updating best move, PV and TT. - if (Threads.stop.load(std::memory_order_relaxed)) + if (threads.stop.load(std::memory_order_relaxed)) return VALUE_ZERO; if (rootNode) @@ -1371,8 +1371,8 @@ Value search(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth, boo // If there is a move that produces search value greater than alpha we update the stats of searched moves else if (bestMove) - update_all_stats(pos, ss, bestMove, bestValue, beta, prevSq, quietsSearched, quietCount, - capturesSearched, captureCount, depth); + update_all_stats(pos, ss, *this, bestMove, bestValue, beta, prevSq, quietsSearched, + quietCount, capturesSearched, captureCount, depth); // Bonus for prior countermove that caused the fail low else if (!priorCapture && prevSq != SQ_NONE) @@ -1400,7 +1400,7 @@ Value search(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth, boo bestValue >= beta ? BOUND_LOWER : PvNode && bestMove ? BOUND_EXACT : BOUND_UPPER, - depth, bestMove, unadjustedStaticEval); + depth, bestMove, unadjustedStaticEval, tt.generation()); // Adjust correction history if (!ss->inCheck && (!bestMove || !pos.capture(bestMove)) @@ -1422,10 +1422,12 @@ Value search(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth, boo // function with zero depth, or recursively with further decreasing depth per call. // (~155 Elo) template -Value qsearch(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth) { +Value Search::Worker::qsearch(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth) { static_assert(nodeType != Root); - constexpr bool PvNode = nodeType == PV; + constexpr bool PvNode = nodeType == PV; + const auto thisThread = this; // To easier distinguish local from member variables + assert(alpha >= -VALUE_INFINITE && alpha < beta && beta <= VALUE_INFINITE); assert(PvNode || (alpha == beta - 1)); @@ -1435,7 +1437,7 @@ Value qsearch(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth) { // if the opponent had an alternative move earlier to this position. if (alpha < VALUE_DRAW && pos.has_game_cycle(ss->ply)) { - alpha = value_draw(pos.this_thread()); + alpha = value_draw(thisThread->nodes); if (alpha >= beta) return alpha; } @@ -1460,10 +1462,9 @@ Value qsearch(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth) { ss->pv[0] = Move::none(); } - Thread* thisThread = pos.this_thread(); - bestMove = Move::none(); - ss->inCheck = pos.checkers(); - moveCount = 0; + bestMove = Move::none(); + ss->inCheck = pos.checkers(); + moveCount = 0; // Used to send selDepth info to GUI (selDepth counts from 1, ply from 0) if (PvNode && thisThread->selDepth < ss->ply + 1) @@ -1471,7 +1472,7 @@ Value qsearch(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth) { // Step 2. Check for an immediate draw or maximum ply reached if (pos.is_draw(ss->ply) || ss->ply >= MAX_PLY) - return (ss->ply >= MAX_PLY && !ss->inCheck) ? evaluate(pos) : VALUE_DRAW; + return (ss->ply >= MAX_PLY && !ss->inCheck) ? evaluate(pos, *this) : VALUE_DRAW; assert(0 <= ss->ply && ss->ply < MAX_PLY); @@ -1480,7 +1481,7 @@ Value qsearch(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth) { // Step 3. Transposition table lookup posKey = pos.key(); - tte = TT.probe(posKey, ss->ttHit); + tte = tt.probe(posKey, ss->ttHit); ttValue = ss->ttHit ? value_from_tt(tte->value(), ss->ply, pos.rule50_count()) : VALUE_NONE; ttMove = ss->ttHit ? tte->move() : Move::none(); pvHit = ss->ttHit && tte->is_pv(); @@ -1502,7 +1503,7 @@ Value qsearch(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth) { { // Never assume anything about values stored in TT if ((unadjustedStaticEval = ss->staticEval = bestValue = tte->eval()) == VALUE_NONE) - unadjustedStaticEval = ss->staticEval = bestValue = evaluate(pos); + unadjustedStaticEval = ss->staticEval = bestValue = evaluate(pos, *this); Value newEval = ss->staticEval @@ -1522,7 +1523,7 @@ Value qsearch(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth) { { // In case of null move search, use previous static eval with a different sign unadjustedStaticEval = ss->staticEval = bestValue = - (ss - 1)->currentMove != Move::null() ? evaluate(pos) : -(ss - 1)->staticEval; + (ss - 1)->currentMove != Move::null() ? evaluate(pos, *this) : -(ss - 1)->staticEval; Value newEval = ss->staticEval @@ -1539,7 +1540,7 @@ Value qsearch(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth) { { if (!ss->ttHit) tte->save(posKey, value_to_tt(bestValue, ss->ply), false, BOUND_LOWER, DEPTH_NONE, - Move::none(), unadjustedStaticEval); + Move::none(), unadjustedStaticEval, tt.generation()); return bestValue; } @@ -1632,7 +1633,7 @@ Value qsearch(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth) { } // Speculative prefetch as early as possible - prefetch(TT.first_entry(pos.key_after(move))); + prefetch(tt.first_entry(pos.key_after(move))); // Update the current move ss->currentMove = move; @@ -1643,6 +1644,7 @@ Value qsearch(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth) { quietCheckEvasions += !capture && ss->inCheck; // Step 7. Make and search the move + thisThread->nodes.fetch_add(1, std::memory_order_relaxed); pos.do_move(move, st, givesCheck); value = -qsearch(pos, ss + 1, -beta, -alpha, depth - 1); pos.undo_move(move); @@ -1686,7 +1688,7 @@ Value qsearch(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth) { // Static evaluation is saved as it was before adjustment by correction history tte->save(posKey, value_to_tt(bestValue, ss->ply), pvHit, bestValue >= beta ? BOUND_LOWER : BOUND_UPPER, ttDepth, bestMove, - unadjustedStaticEval); + unadjustedStaticEval, tt.generation()); assert(bestValue > -VALUE_INFINITE && bestValue < VALUE_INFINITE); @@ -1694,6 +1696,7 @@ Value qsearch(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth) { } +namespace { // Adjusts a mate or TB score from "plies to mate from the root" // to "plies to mate from the current position". Standard scores are unchanged. // The function is called before storing a value in the transposition table. @@ -1759,6 +1762,7 @@ void update_pv(Move* pv, Move move, const Move* childPv) { // Updates stats at the end of search() when a bestMove is found void update_all_stats(const Position& pos, Stack* ss, + Search::Worker& workerThread, Move bestMove, Value bestValue, Value beta, @@ -1770,8 +1774,7 @@ void update_all_stats(const Position& pos, Depth depth) { Color us = pos.side_to_move(); - Thread* thisThread = pos.this_thread(); - CapturePieceToHistory& captureHistory = thisThread->captureHistory; + CapturePieceToHistory& captureHistory = workerThread.captureHistory; Piece moved_piece = pos.moved_piece(bestMove); PieceType captured; @@ -1784,19 +1787,19 @@ void update_all_stats(const Position& pos, : stat_bonus(depth); // smaller bonus // Increase stats for the best move in case it was a quiet move - update_quiet_stats(pos, ss, bestMove, bestMoveBonus); + update_quiet_stats(pos, ss, workerThread, bestMove, bestMoveBonus); int pIndex = pawn_structure_index(pos); - thisThread->pawnHistory[pIndex][moved_piece][bestMove.to_sq()] << quietMoveBonus; + workerThread.pawnHistory[pIndex][moved_piece][bestMove.to_sq()] << quietMoveBonus; // Decrease stats for all non-best quiet moves for (int i = 0; i < quietCount; ++i) { - thisThread - ->pawnHistory[pIndex][pos.moved_piece(quietsSearched[i])][quietsSearched[i].to_sq()] + workerThread + .pawnHistory[pIndex][pos.moved_piece(quietsSearched[i])][quietsSearched[i].to_sq()] << -quietMoveMalus; - thisThread->mainHistory[us][quietsSearched[i].from_to()] << -quietMoveMalus; + workerThread.mainHistory[us][quietsSearched[i].from_to()] << -quietMoveMalus; update_continuation_histories(ss, pos.moved_piece(quietsSearched[i]), quietsSearched[i].to_sq(), -quietMoveMalus); } @@ -1842,7 +1845,8 @@ void update_continuation_histories(Stack* ss, Piece pc, Square to, int bonus) { // Updates move sorting heuristics -void update_quiet_stats(const Position& pos, Stack* ss, Move move, int bonus) { +void update_quiet_stats( + const Position& pos, Stack* ss, Search::Worker& workerThread, Move move, int bonus) { // Update killers if (ss->killers[0] != move) @@ -1851,25 +1855,23 @@ void update_quiet_stats(const Position& pos, Stack* ss, Move move, int bonus) { ss->killers[0] = move; } - Color us = pos.side_to_move(); - Thread* thisThread = pos.this_thread(); - thisThread->mainHistory[us][move.from_to()] << bonus; + Color us = pos.side_to_move(); + workerThread.mainHistory[us][move.from_to()] << bonus; update_continuation_histories(ss, pos.moved_piece(move), move.to_sq(), bonus); // Update countermove history if (((ss - 1)->currentMove).is_ok()) { - Square prevSq = ((ss - 1)->currentMove).to_sq(); - thisThread->counterMoves[pos.piece_on(prevSq)][prevSq] = move; + Square prevSq = ((ss - 1)->currentMove).to_sq(); + workerThread.counterMoves[pos.piece_on(prevSq)][prevSq] = move; } } +} // When playing with strength handicap, choose the best move among a set of RootMoves // using a statistical rule dependent on 'level'. Idea by Heinz van Saanen. -Move Skill::pick_best(size_t multiPV) { - - const RootMoves& rootMoves = Threads.main()->rootMoves; - static PRNG rng(now()); // PRNG sequence should be non-deterministic +Move Skill::pick_best(const RootMoves& rootMoves, size_t multiPV) { + static PRNG rng(now()); // PRNG sequence should be non-deterministic // RootMoves are already sorted by score in descending order Value topScore = rootMoves[0].score; @@ -1897,23 +1899,20 @@ Move Skill::pick_best(size_t multiPV) { return best; } -} // namespace - // Used to print debug info and, more importantly, // to detect when we are out of available time and thus stop the search. -void MainThread::check_time() { - +void SearchManager::check_time(Search::Worker& worker) { if (--callsCnt > 0) return; // When using nodes, ensure checking rate is not lower than 0.1% of nodes - callsCnt = Limits.nodes ? std::min(512, int(Limits.nodes / 1024)) : 512; + callsCnt = worker.limits.nodes ? std::min(512, int(worker.limits.nodes / 1024)) : 512; static TimePoint lastInfoTime = now(); - TimePoint elapsed = Time.elapsed(); - TimePoint tick = Limits.startTime + elapsed; + TimePoint elapsed = tm.elapsed(worker.threads.nodes_searched()); + TimePoint tick = worker.limits.startTime + elapsed; if (tick - lastInfoTime >= 1000) { @@ -1925,72 +1924,18 @@ void MainThread::check_time() { if (ponder) return; - if ((Limits.use_time_management() && (elapsed > Time.maximum() || stopOnPonderhit)) - || (Limits.movetime && elapsed >= Limits.movetime) - || (Limits.nodes && Threads.nodes_searched() >= uint64_t(Limits.nodes))) - Threads.stop = true; + if ((worker.limits.use_time_management() && (elapsed > tm.maximum() || stopOnPonderhit)) + || (worker.limits.movetime && elapsed >= worker.limits.movetime) + || (worker.limits.nodes + && worker.threads.nodes_searched() >= uint64_t(worker.limits.nodes))) + worker.threads.stop = true; } - -// Formats PV information according to the UCI protocol. UCI requires -// that all (if any) unsearched PV lines are sent using a previous search score. -string UCI::pv(const Position& pos, Depth depth) { - - std::stringstream ss; - TimePoint elapsed = Time.elapsed() + 1; - const RootMoves& rootMoves = pos.this_thread()->rootMoves; - size_t pvIdx = pos.this_thread()->pvIdx; - size_t multiPV = std::min(size_t(Options["MultiPV"]), rootMoves.size()); - uint64_t nodesSearched = Threads.nodes_searched(); - uint64_t tbHits = Threads.tb_hits() + (TB::RootInTB ? rootMoves.size() : 0); - - for (size_t i = 0; i < multiPV; ++i) - { - bool updated = rootMoves[i].score != -VALUE_INFINITE; - - if (depth == 1 && !updated && i > 0) - continue; - - Depth d = updated ? depth : std::max(1, depth - 1); - Value v = updated ? rootMoves[i].uciScore : rootMoves[i].previousScore; - - if (v == -VALUE_INFINITE) - v = VALUE_ZERO; - - bool tb = TB::RootInTB && std::abs(v) <= VALUE_TB; - v = tb ? rootMoves[i].tbScore : v; - - if (ss.rdbuf()->in_avail()) // Not at first line - ss << "\n"; - - ss << "info" - << " depth " << d << " seldepth " << rootMoves[i].selDepth << " multipv " << i + 1 - << " score " << UCI::value(v); - - if (Options["UCI_ShowWDL"]) - ss << UCI::wdl(v, pos.game_ply()); - - if (i == pvIdx && !tb && updated) // tablebase- and previous-scores are exact - ss << (rootMoves[i].scoreLowerbound - ? " lowerbound" - : (rootMoves[i].scoreUpperbound ? " upperbound" : "")); - - ss << " nodes " << nodesSearched << " nps " << nodesSearched * 1000 / elapsed - << " hashfull " << TT.hashfull() << " tbhits " << tbHits << " time " << elapsed << " pv"; - - for (Move m : rootMoves[i].pv) - ss << " " << UCI::move(m, pos.is_chess960()); - } - - return ss.str(); -} - - // Called in case we have no ponder move before exiting the search, // for instance, in case we stop the search during a fail high at root. // We try hard to have a ponder move to return to the GUI, // otherwise in case of 'ponder on' we have nothing to think about. -bool RootMove::extract_ponder_from_tt(Position& pos) { +bool RootMove::extract_ponder_from_tt(const TranspositionTable& tt, Position& pos) { StateInfo st; ASSERT_ALIGNED(&st, Eval::NNUE::CacheLineSize); @@ -2003,7 +1948,7 @@ bool RootMove::extract_ponder_from_tt(Position& pos) { return false; pos.do_move(pv[0], st); - TTEntry* tte = TT.probe(pos.key(), ttHit); + TTEntry* tte = tt.probe(pos.key(), ttHit); if (ttHit) { @@ -2016,12 +1961,14 @@ bool RootMove::extract_ponder_from_tt(Position& pos) { return pv.size() > 1; } -void Tablebases::rank_root_moves(Position& pos, Search::RootMoves& rootMoves) { +void Tablebases::rank_root_moves(const OptionsMap& options, + Position& pos, + Search::RootMoves& rootMoves) { RootInTB = false; - UseRule50 = bool(Options["Syzygy50MoveRule"]); - ProbeDepth = int(Options["SyzygyProbeDepth"]); - Cardinality = int(Options["SyzygyProbeLimit"]); + UseRule50 = bool(options["Syzygy50MoveRule"]); + ProbeDepth = int(options["SyzygyProbeDepth"]); + Cardinality = int(options["SyzygyProbeLimit"]); bool dtz_available = true; // Tables with fewer pieces than SyzygyProbeLimit are searched with @@ -2035,13 +1982,13 @@ void Tablebases::rank_root_moves(Position& pos, Search::RootMoves& rootMoves) { if (Cardinality >= popcount(pos.pieces()) && !pos.can_castle(ANY_CASTLING)) { // Rank moves using DTZ tables - RootInTB = root_probe(pos, rootMoves); + RootInTB = root_probe(pos, rootMoves, options["Syzygy50MoveRule"]); if (!RootInTB) { // DTZ tables are missing; try to rank moves using WDL tables dtz_available = false; - RootInTB = root_probe_wdl(pos, rootMoves); + RootInTB = root_probe_wdl(pos, rootMoves, options["Syzygy50MoveRule"]); } } diff --git a/src/search.h b/src/search.h index 72e275d36f7..3288c114333 100644 --- a/src/search.h +++ b/src/search.h @@ -19,19 +19,38 @@ #ifndef SEARCH_H_INCLUDED #define SEARCH_H_INCLUDED +#include +#include +#include #include +#include +#include #include #include "misc.h" #include "movepick.h" +#include "position.h" +#include "timeman.h" #include "types.h" namespace Stockfish { -class Position; +// Different node types, used as a template parameter +enum NodeType { + NonPV, + PV, + Root +}; + +class TranspositionTable; +class ThreadPool; +class OptionsMap; +class UCI; namespace Search { +// Called at startup to initialize various lookup tables, after program startup +void init(int); // Stack struct keeps track of the information we need to remember from nodes // shallower and deeper in the tree during the search. Each search thread has @@ -61,7 +80,7 @@ struct RootMove { explicit RootMove(Move m) : pv(1, m) {} - bool extract_ponder_from_tt(Position& pos); + bool extract_ponder_from_tt(const TranspositionTable& tt, Position& pos); bool operator==(const Move& m) const { return pv[0] == m; } // Sort in descending order bool operator<(const RootMove& m) const { @@ -103,10 +122,128 @@ struct LimitsType { int64_t nodes; }; -extern LimitsType Limits; -void init(); -void clear(); +// The UCI stores the uci options, thread pool, and transposition table. +// This struct is used to easily forward data to the Search::Worker class. +struct ExternalShared { + ExternalShared(const OptionsMap& o, ThreadPool& tp, TranspositionTable& t) : + options(o), + threads(tp), + tt(t) {} + + const OptionsMap& options; + ThreadPool& threads; + TranspositionTable& tt; +}; + +class Worker; + +// Null Object Pattern +class ISearchManager { + public: + virtual ~ISearchManager() {} + virtual void check_time(Search::Worker&) = 0; +}; + +// SearchManager manages the search from the main thread. It is responsible for +// keeping track of the time, and storing data strictly related to the main thread. +class SearchManager: public ISearchManager { + public: + void check_time(Search::Worker& worker) override; + + Stockfish::TimeManagement tm; + int callsCnt; + std::atomic_bool ponder; + + double previousTimeReduction; + Value bestPreviousScore; + Value bestPreviousAverageScore; + Value iterValue[4]; + bool stopOnPonderhit; + + size_t id; +}; + +class NullSearchManager: public ISearchManager { + public: + void check_time(Search::Worker&) override {} +}; + +class Worker { + public: + Worker(ExternalShared& es, std::unique_ptr sm, size_t i) : + // Unpack the ExternalShared struct into member variables + options(es.options), + threads(es.threads), + tt(es.tt), + manager(std::move(sm)), + thread_idx(i) {} + + // Reset histories, usually before a new game + void clear(); + + // Called when the program receives the UCI 'go' + // command. It searches from the root position and outputs the "bestmove". + void start_searching(); + + bool is_mainthread() const { return thread_idx == 0; } + + // Public because evaluate uses this + Value iterBestValue, optimism[COLOR_NB]; + Value rootSimpleEval; + + // Public because they need to be updatable by the stats + CounterMoveHistory counterMoves; + ButterflyHistory mainHistory; + CapturePieceToHistory captureHistory; + ContinuationHistory continuationHistory[2][2]; + PawnHistory pawnHistory; + CorrectionHistory correctionHistory; + + private: + void iterative_deepening(); + + // Main search function for both PV and non-PV nodes + template + Value search(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth, bool cutNode); + + // Quiescence search function, which is called by the main search + template + Value qsearch(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth = 0); + + // Get a pointer to the search manager, only allowed to be called by the + // main thread. + SearchManager* main_manager() const { + assert(thread_idx == 0); + return static_cast(manager.get()); + } + + LimitsType limits; + + size_t pvIdx, pvLast; + std::atomic nodes, tbHits, bestMoveChanges; + int selDepth, nmpMinPly; + + Position rootPos; + StateInfo rootState; + RootMoves rootMoves; + Depth rootDepth, completedDepth; + Value rootDelta; + + const OptionsMap& options; + ThreadPool& threads; + TranspositionTable& tt; + + // The main thread has a SearchManager, the others have a NullSearchManager + std::unique_ptr manager; + + size_t thread_idx; + + friend class Stockfish::ThreadPool; + friend class Stockfish::UCI; + friend class SearchManager; +}; + } // namespace Search diff --git a/src/syzygy/tbprobe.cpp b/src/syzygy/tbprobe.cpp index 91013dcac28..6f30bf6b1f7 100644 --- a/src/syzygy/tbprobe.cpp +++ b/src/syzygy/tbprobe.cpp @@ -42,7 +42,6 @@ #include "../position.h" #include "../search.h" #include "../types.h" -#include "../uci.h" #ifndef _WIN32 #include @@ -1574,7 +1573,7 @@ int Tablebases::probe_dtz(Position& pos, ProbeState* result) { // Use the DTZ tables to rank root moves. // // A return value false indicates that not all probes were successful. -bool Tablebases::root_probe(Position& pos, Search::RootMoves& rootMoves) { +bool Tablebases::root_probe(Position& pos, Search::RootMoves& rootMoves, bool rule50) { ProbeState result = OK; StateInfo st; @@ -1585,7 +1584,7 @@ bool Tablebases::root_probe(Position& pos, Search::RootMoves& rootMoves) { // Check whether a position was repeated since the last zeroing move. bool rep = pos.has_repeated(); - int dtz, bound = Options["Syzygy50MoveRule"] ? (MAX_DTZ - 100) : 1; + int dtz, bound = rule50 ? (MAX_DTZ - 100) : 1; // Probe and rank each move for (auto& m : rootMoves) @@ -1647,7 +1646,7 @@ bool Tablebases::root_probe(Position& pos, Search::RootMoves& rootMoves) { // 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 Tablebases::root_probe_wdl(Position& pos, Search::RootMoves& rootMoves) { +bool Tablebases::root_probe_wdl(Position& pos, Search::RootMoves& rootMoves, bool rule50) { static const int WDL_to_rank[] = {-MAX_DTZ, -MAX_DTZ + 101, 0, MAX_DTZ - 101, MAX_DTZ}; @@ -1655,7 +1654,6 @@ bool Tablebases::root_probe_wdl(Position& pos, Search::RootMoves& rootMoves) { StateInfo st; WDLScore wdl; - bool rule50 = Options["Syzygy50MoveRule"]; // Probe and rank each move for (auto& m : rootMoves) diff --git a/src/syzygy/tbprobe.h b/src/syzygy/tbprobe.h index cc8eb0d4d70..d7b412a10e5 100644 --- a/src/syzygy/tbprobe.h +++ b/src/syzygy/tbprobe.h @@ -25,6 +25,7 @@ namespace Stockfish { class Position; +class OptionsMap; } namespace Stockfish::Tablebases { @@ -47,12 +48,13 @@ enum ProbeState { extern int MaxCardinality; + void init(const std::string& paths); WDLScore probe_wdl(Position& pos, ProbeState* result); int probe_dtz(Position& pos, ProbeState* result); -bool root_probe(Position& pos, Search::RootMoves& rootMoves); -bool root_probe_wdl(Position& pos, Search::RootMoves& rootMoves); -void rank_root_moves(Position& pos, Search::RootMoves& rootMoves); +bool root_probe(Position& pos, Search::RootMoves& rootMoves, bool rule50); +bool root_probe_wdl(Position& pos, Search::RootMoves& rootMoves, bool rule50); +void rank_root_moves(const OptionsMap& options, Position& pos, Search::RootMoves& rootMoves); } // namespace Stockfish::Tablebases diff --git a/src/thread.cpp b/src/thread.cpp index 01ccd4fc565..a3a5cdbe547 100644 --- a/src/thread.cpp +++ b/src/thread.cpp @@ -23,9 +23,8 @@ #include #include #include -#include -#include #include +#include #include #include "evaluate.h" @@ -33,18 +32,19 @@ #include "movegen.h" #include "search.h" #include "syzygy/tbprobe.h" +#include "timeman.h" #include "tt.h" -#include "uci.h" +#include "types.h" +#include "ucioption.h" namespace Stockfish { -ThreadPool Threads; // Global object - - // Constructor launches the thread and waits until it goes to sleep // in idle_loop(). Note that 'searching' and 'exit' should be already set. -Thread::Thread(size_t n) : +Thread::Thread(Search::ExternalShared& es, std::unique_ptr sm, size_t n) : + worker(std::make_unique(es, std::move(sm), n)), idx(n), + nthreads(es.options["Threads"]), stdThread(&Thread::idle_loop, this) { wait_for_search_finished(); @@ -62,24 +62,6 @@ Thread::~Thread() { stdThread.join(); } - -// Reset histories, usually before a new game -void Thread::clear() { - - counterMoves.fill(Move::none()); - mainHistory.fill(0); - captureHistory.fill(0); - pawnHistory.fill(0); - correctionHistory.fill(0); - - for (bool inCheck : {false, true}) - for (StatsType c : {NoCaptures, Captures}) - for (auto& to : continuationHistory[inCheck][c]) - for (auto& h : to) - h->fill(-71); -} - - // Wakes up the thread that will start the search void Thread::start_searching() { mutex.lock(); @@ -108,7 +90,7 @@ void Thread::idle_loop() { // some Windows NUMA hardware, for instance in fishtest. To make it simple, // just check if running threads are below a threshold, in this case, all this // NUMA machinery is not needed. - if (Options["Threads"] > 8) + if (nthreads > 8) WinProcGroup::bindThisThread(idx); while (true) @@ -123,36 +105,44 @@ void Thread::idle_loop() { lk.unlock(); - search(); + worker->start_searching(); } } // Creates/destroys threads to match the requested number. // Created and launched threads will immediately go to sleep in idle_loop. // Upon resizing, threads are recreated to allow for binding if necessary. -void ThreadPool::set(size_t requested) { +void ThreadPool::set(Search::ExternalShared&& es) { if (threads.size() > 0) // destroy any existing thread(s) { - main()->wait_for_search_finished(); + main_thread()->wait_for_search_finished(); while (threads.size() > 0) delete threads.back(), threads.pop_back(); } + const size_t requested = es.options["Threads"]; + if (requested > 0) // create new thread(s) { - threads.push_back(new MainThread(0)); + threads.push_back( + new Thread(es, std::unique_ptr(new Search::SearchManager()), 0)); + while (threads.size() < requested) - threads.push_back(new Thread(threads.size())); + threads.push_back(new Thread( + es, std::unique_ptr(new Search::NullSearchManager()), + threads.size())); clear(); + main_thread()->wait_for_search_finished(); + // Reallocate the hash with the new threadpool size - TT.resize(size_t(Options["Hash"])); + es.tt.resize(es.options["Hash"], requested); // Init thread number dependent search params. - Search::init(); + Search::init(requested); } } @@ -161,28 +151,32 @@ void ThreadPool::set(size_t requested) { void ThreadPool::clear() { for (Thread* th : threads) - th->clear(); + th->worker->clear(); - main()->callsCnt = 0; - main()->bestPreviousScore = VALUE_INFINITE; - main()->bestPreviousAverageScore = VALUE_INFINITE; - main()->previousTimeReduction = 1.0; + main_manager()->callsCnt = 0; + main_manager()->bestPreviousScore = VALUE_INFINITE; + main_manager()->bestPreviousAverageScore = VALUE_INFINITE; + main_manager()->previousTimeReduction = 1.0; + main_manager()->tm.availableNodes = 0; } // Wakes up main thread waiting in idle_loop() and // returns immediately. Main thread will wake up other threads and start the search. -void ThreadPool::start_thinking(Position& pos, - StateListPtr& states, - const Search::LimitsType& limits, - bool ponderMode) { +void ThreadPool::start_thinking(const OptionsMap& options, + Position& pos, + StateListPtr& states, + Search::LimitsType limits, + bool ponderMode) { + + main_thread()->wait_for_search_finished(); + + main_manager()->stopOnPonderhit = stop = false; + main_manager()->ponder = ponderMode; + main_manager()->tm.init(limits, pos.side_to_move(), pos.game_ply(), options); - main()->wait_for_search_finished(); + increaseDepth = true; - main()->stopOnPonderhit = stop = false; - increaseDepth = true; - main()->ponder = ponderMode; - Search::Limits = limits; Search::RootMoves rootMoves; for (const auto& m : MoveList(pos)) @@ -191,7 +185,7 @@ void ThreadPool::start_thinking(Position& pos, rootMoves.emplace_back(m); if (!rootMoves.empty()) - Tablebases::rank_root_moves(pos, rootMoves); + Tablebases::rank_root_moves(options, pos, rootMoves); // After ownership transfer 'states' becomes empty, so if we stop the search // and call 'go' again without setting a new position states.get() == nullptr. @@ -207,15 +201,17 @@ void ThreadPool::start_thinking(Position& pos, // since they are read-only. for (Thread* th : threads) { - th->nodes = th->tbHits = th->nmpMinPly = th->bestMoveChanges = 0; - th->rootDepth = th->completedDepth = 0; - th->rootMoves = rootMoves; - th->rootPos.set(pos.fen(), pos.is_chess960(), &th->rootState, th); - th->rootState = setupStates->back(); - th->rootSimpleEval = Eval::simple_eval(pos, pos.side_to_move()); + th->worker->limits = limits; + th->worker->nodes = th->worker->tbHits = th->worker->nmpMinPly = + th->worker->bestMoveChanges = 0; + th->worker->rootDepth = th->worker->completedDepth = 0; + th->worker->rootMoves = rootMoves; + th->worker->rootPos.set(pos.fen(), pos.is_chess960(), &th->worker->rootState); + th->worker->rootState = setupStates->back(); + th->worker->rootSimpleEval = Eval::simple_eval(pos, pos.side_to_move()); } - main()->start_searching(); + main_thread()->start_searching(); } Thread* ThreadPool::get_best_thread() const { @@ -226,30 +222,32 @@ Thread* ThreadPool::get_best_thread() const { // Find the minimum score of all threads for (Thread* th : threads) - minScore = std::min(minScore, th->rootMoves[0].score); + minScore = std::min(minScore, th->worker->rootMoves[0].score); // Vote according to score and depth, and select the best thread auto thread_value = [minScore](Thread* th) { - return (th->rootMoves[0].score - minScore + 14) * int(th->completedDepth); + return (th->worker->rootMoves[0].score - minScore + 14) * int(th->worker->completedDepth); }; for (Thread* th : threads) - votes[th->rootMoves[0].pv[0]] += thread_value(th); + votes[th->worker->rootMoves[0].pv[0]] += thread_value(th); for (Thread* th : threads) - if (std::abs(bestThread->rootMoves[0].score) >= VALUE_TB_WIN_IN_MAX_PLY) + if (std::abs(bestThread->worker->rootMoves[0].score) >= VALUE_TB_WIN_IN_MAX_PLY) { // Make sure we pick the shortest mate / TB conversion or stave off mate the longest - if (th->rootMoves[0].score > bestThread->rootMoves[0].score) + if (th->worker->rootMoves[0].score > bestThread->worker->rootMoves[0].score) bestThread = th; } - else if (th->rootMoves[0].score >= VALUE_TB_WIN_IN_MAX_PLY - || (th->rootMoves[0].score > VALUE_TB_LOSS_IN_MAX_PLY - && (votes[th->rootMoves[0].pv[0]] > votes[bestThread->rootMoves[0].pv[0]] - || (votes[th->rootMoves[0].pv[0]] == votes[bestThread->rootMoves[0].pv[0]] - && thread_value(th) * int(th->rootMoves[0].pv.size() > 2) + else if (th->worker->rootMoves[0].score >= VALUE_TB_WIN_IN_MAX_PLY + || (th->worker->rootMoves[0].score > VALUE_TB_LOSS_IN_MAX_PLY + && (votes[th->worker->rootMoves[0].pv[0]] + > votes[bestThread->worker->rootMoves[0].pv[0]] + || (votes[th->worker->rootMoves[0].pv[0]] + == votes[bestThread->worker->rootMoves[0].pv[0]] + && thread_value(th) * int(th->worker->rootMoves[0].pv.size() > 2) > thread_value(bestThread) - * int(bestThread->rootMoves[0].pv.size() > 2))))) + * int(bestThread->worker->rootMoves[0].pv.size() > 2))))) bestThread = th; return bestThread; diff --git a/src/thread.h b/src/thread.h index 7db7c15905b..1eecc5ee1cf 100644 --- a/src/thread.h +++ b/src/thread.h @@ -23,91 +23,76 @@ #include #include #include +#include #include #include -#include "movepick.h" #include "position.h" #include "search.h" #include "thread_win32_osx.h" -#include "types.h" namespace Stockfish { -// Thread class keeps together all the thread-related stuff. -class Thread { - - std::mutex mutex; - std::condition_variable cv; - size_t idx; - bool exit = false, searching = true; // Set before starting std::thread - NativeThread stdThread; +class OptionsMap; +using Value = int; +// Abstraction of a thread. It contains a pointer to the worker and a native thread. +// After construction, the native thread is started with idle_loop() +// waiting for a signal to start searching. +// When the signal is received, the thread starts searching and when +// the search is finished, it goes back to idle_loop() waiting for a new signal. +class Thread { public: - explicit Thread(size_t); + Thread(Search::ExternalShared&, std::unique_ptr, size_t); virtual ~Thread(); - virtual void search(); - void clear(); - void idle_loop(); - void start_searching(); - void wait_for_search_finished(); - size_t id() const { return idx; } - - size_t pvIdx, pvLast; - std::atomic nodes, tbHits, bestMoveChanges; - int selDepth, nmpMinPly; - Value bestValue; - - int optimism[COLOR_NB]; - - Position rootPos; - StateInfo rootState; - Search::RootMoves rootMoves; - Depth rootDepth, completedDepth; - int rootDelta; - Value rootSimpleEval; - CounterMoveHistory counterMoves; - ButterflyHistory mainHistory; - CapturePieceToHistory captureHistory; - ContinuationHistory continuationHistory[2][2]; - PawnHistory pawnHistory; - CorrectionHistory correctionHistory; -}; - - -// MainThread is a derived class specific for main thread -struct MainThread: public Thread { - using Thread::Thread; + void idle_loop(); + void start_searching(); + void wait_for_search_finished(); + size_t id() const { return idx; } - void search() override; - void check_time(); + std::unique_ptr worker; - double previousTimeReduction; - Value bestPreviousScore; - Value bestPreviousAverageScore; - Value iterValue[4]; - int callsCnt; - bool stopOnPonderhit; - std::atomic_bool ponder; + private: + std::mutex mutex; + std::condition_variable cv; + size_t idx, nthreads; + bool exit = false, searching = true; // Set before starting std::thread + NativeThread stdThread; }; // ThreadPool struct handles all the threads-related stuff like init, starting, // parking and, most importantly, launching a thread. All the access to threads // is done through this class. -struct ThreadPool { +class ThreadPool { - void start_thinking(Position&, StateListPtr&, const Search::LimitsType&, bool = false); - void clear(); - void set(size_t); + public: + ~ThreadPool() { + // destroy any existing thread(s) + if (threads.size() > 0) + { + main_thread()->wait_for_search_finished(); + + while (threads.size() > 0) + delete threads.back(), threads.pop_back(); + } + } - MainThread* main() const { return static_cast(threads.front()); } - uint64_t nodes_searched() const { return accumulate(&Thread::nodes); } - uint64_t tb_hits() const { return accumulate(&Thread::tbHits); } - Thread* get_best_thread() const; - void start_searching(); - void wait_for_search_finished() const; + void + start_thinking(const OptionsMap&, Position&, StateListPtr&, Search::LimitsType, bool = false); + void clear(); + void set(Search::ExternalShared&&); + + Search::SearchManager* main_manager() const { + return static_cast(main_thread()->worker.get()->manager.get()); + }; + Thread* main_thread() const { return threads.front(); } + uint64_t nodes_searched() const { return accumulate(&Search::Worker::nodes); } + uint64_t tb_hits() const { return accumulate(&Search::Worker::tbHits); } + Thread* get_best_thread() const; + void start_searching(); + void wait_for_search_finished() const; std::atomic_bool stop, increaseDepth; @@ -122,17 +107,15 @@ struct ThreadPool { StateListPtr setupStates; std::vector threads; - uint64_t accumulate(std::atomic Thread::*member) const { + uint64_t accumulate(std::atomic Search::Worker::*member) const { uint64_t sum = 0; for (Thread* th : threads) - sum += (th->*member).load(std::memory_order_relaxed); + sum += th->worker.get()->*member; return sum; } }; -extern ThreadPool Threads; - } // namespace Stockfish #endif // #ifndef THREAD_H_INCLUDED diff --git a/src/thread_win32_osx.h b/src/thread_win32_osx.h index 4bc62d678ad..8d424b72a50 100644 --- a/src/thread_win32_osx.h +++ b/src/thread_win32_osx.h @@ -30,31 +30,35 @@ #if defined(__APPLE__) || defined(__MINGW32__) || defined(__MINGW64__) || defined(USE_PTHREADS) #include + #include namespace Stockfish { -static const size_t TH_STACK_SIZE = 8 * 1024 * 1024; - -template> -void* start_routine(void* ptr) { - P* p = reinterpret_cast(ptr); - (p->first->*(p->second))(); // Call member function pointer - delete p; +// free function to be passed to pthread_create() +inline void* start_routine(void* ptr) { + auto func = reinterpret_cast*>(ptr); + (*func)(); // Call the function + delete func; return nullptr; } class NativeThread { - pthread_t thread; + static constexpr size_t TH_STACK_SIZE = 8 * 1024 * 1024; + public: - template> - explicit NativeThread(void (T::*fun)(), T* obj) { + template + explicit NativeThread(Function&& fun, Args&&... args) { + auto func = new std::function( + std::bind(std::forward(fun), std::forward(args)...)); + pthread_attr_t attr_storage, *attr = &attr_storage; pthread_attr_init(attr); pthread_attr_setstacksize(attr, TH_STACK_SIZE); - pthread_create(&thread, attr, start_routine, new P(obj, fun)); + pthread_create(&thread, attr, start_routine, func); } + void join() { pthread_join(thread, nullptr); } }; diff --git a/src/timeman.cpp b/src/timeman.cpp index 77db2f621df..e3c14bc6e7b 100644 --- a/src/timeman.cpp +++ b/src/timeman.cpp @@ -22,27 +22,34 @@ #include #include "search.h" -#include "uci.h" +#include "ucioption.h" namespace Stockfish { -TimeManagement Time; // Our global time management object + +TimePoint TimeManagement::optimum() const { return optimumTime; } +TimePoint TimeManagement::maximum() const { return maximumTime; } +TimePoint TimeManagement::elapsed(size_t nodes) const { + return useNodesTime ? TimePoint(nodes) : now() - startTime; +} // Called at the beginning of the search and calculates // the bounds of time allowed for the current game ply. We currently support: // 1) x basetime (+ z increment) // 2) x moves in y seconds (+ z increment) -void TimeManagement::init(Search::LimitsType& limits, Color us, int ply) { - +void TimeManagement::init(Search::LimitsType& limits, + Color us, + int ply, + const OptionsMap& options) { // If we have no time, no need to initialize TM, except for the start time, // which is used by movetime. startTime = limits.startTime; if (limits.time[us] == 0) return; - TimePoint moveOverhead = TimePoint(Options["Move Overhead"]); - TimePoint npmsec = TimePoint(Options["nodestime"]); + TimePoint moveOverhead = TimePoint(options["Move Overhead"]); + TimePoint npmsec = TimePoint(options["nodestime"]); // optScale is a percentage of available time to use for the current move. // maxScale is a multiplier applied to optimumTime. @@ -54,6 +61,8 @@ void TimeManagement::init(Search::LimitsType& limits, Color us, int ply) { // must be much lower than the real engine speed. if (npmsec) { + useNodesTime = limits.npmsec; + if (!availableNodes) // Only once at game start availableNodes = npmsec * limits.time[us]; // Time is in msec @@ -100,7 +109,7 @@ void TimeManagement::init(Search::LimitsType& limits, Color us, int ply) { maximumTime = TimePoint(std::min(0.84 * limits.time[us] - moveOverhead, maxScale * optimumTime)) - 10; - if (Options["Ponder"]) + if (options["Ponder"]) optimumTime += optimumTime / 4; } diff --git a/src/timeman.h b/src/timeman.h index 0509158c3f4..4ea5383aca5 100644 --- a/src/timeman.h +++ b/src/timeman.h @@ -20,34 +20,38 @@ #define TIMEMAN_H_INCLUDED #include +#include #include "misc.h" -#include "search.h" -#include "thread.h" #include "types.h" namespace Stockfish { +class OptionsMap; + +namespace Search { +struct LimitsType; +} + // The TimeManagement class computes the optimal time to think depending on // the maximum available time, the game move number, and other parameters. class TimeManagement { public: - void init(Search::LimitsType& limits, Color us, int ply); - TimePoint optimum() const { return optimumTime; } - TimePoint maximum() const { return maximumTime; } - TimePoint elapsed() const { - return Search::Limits.npmsec ? TimePoint(Threads.nodes_searched()) : now() - startTime; - } + void init(Search::LimitsType& limits, Color us, int ply, const OptionsMap& options); + + TimePoint optimum() const; + TimePoint maximum() const; + TimePoint elapsed(std::size_t nodes) const; - int64_t availableNodes; // When in 'nodes as time' mode + std::int64_t availableNodes = 0; // When in 'nodes as time' mode private: TimePoint startTime; TimePoint optimumTime; TimePoint maximumTime; -}; -extern TimeManagement Time; + bool useNodesTime = false; // True if we are in 'nodes as time' mode +}; } // namespace Stockfish diff --git a/src/tt.cpp b/src/tt.cpp index 2e3f7d32835..693568bfc34 100644 --- a/src/tt.cpp +++ b/src/tt.cpp @@ -26,16 +26,13 @@ #include #include "misc.h" -#include "thread.h" -#include "uci.h" namespace Stockfish { -TranspositionTable TT; // Our global transposition table - // Populates the TTEntry with a new node's data, possibly // overwriting an old position. The update is not atomic and can be racy. -void TTEntry::save(Key k, Value v, bool pv, Bound b, Depth d, Move m, Value ev) { +void TTEntry::save( + Key k, Value v, bool pv, Bound b, Depth d, Move m, Value ev, uint8_t generation8) { // Preserve any existing move for the same position if (m || uint16_t(k) != key16) @@ -49,7 +46,7 @@ void TTEntry::save(Key k, Value v, bool pv, Bound b, Depth d, Move m, Value ev) key16 = uint16_t(k); depth8 = uint8_t(d - DEPTH_OFFSET); - genBound8 = uint8_t(TT.generation8 | uint8_t(pv) << 2 | b); + genBound8 = uint8_t(generation8 | uint8_t(pv) << 2 | b); value16 = int16_t(v); eval16 = int16_t(ev); } @@ -59,10 +56,7 @@ void TTEntry::save(Key k, Value v, bool pv, Bound b, Depth d, Move m, Value ev) // Sets the size of the transposition table, // measured in megabytes. Transposition table consists of a power of 2 number // of clusters and each cluster consists of ClusterSize number of TTEntry. -void TranspositionTable::resize(size_t mbSize) { - - Threads.main()->wait_for_search_finished(); - +void TranspositionTable::resize(size_t mbSize, int thread_count) { aligned_large_pages_free(table); clusterCount = mbSize * 1024 * 1024 / sizeof(Cluster); @@ -74,28 +68,25 @@ void TranspositionTable::resize(size_t mbSize) { exit(EXIT_FAILURE); } - clear(); + clear(thread_count); } // Initializes the entire transposition table to zero, // in a multi-threaded way. -void TranspositionTable::clear() { - +void TranspositionTable::clear(size_t thread_count) { std::vector threads; - for (size_t idx = 0; idx < size_t(Options["Threads"]); ++idx) + for (size_t idx = 0; idx < size_t(thread_count); ++idx) { - threads.emplace_back([this, idx]() { + threads.emplace_back([this, idx, thread_count]() { // Thread binding gives faster search on systems with a first-touch policy - if (Options["Threads"] > 8) + if (thread_count > 8) WinProcGroup::bindThisThread(idx); // Each thread will zero its part of the hash table - const size_t stride = size_t(clusterCount / Options["Threads"]), - start = size_t(stride * idx), - len = - idx != size_t(Options["Threads"]) - 1 ? stride : clusterCount - start; + const size_t stride = size_t(clusterCount / thread_count), start = size_t(stride * idx), + len = idx != size_t(thread_count) - 1 ? stride : clusterCount - start; std::memset(&table[start], 0, len * sizeof(Cluster)); }); diff --git a/src/tt.h b/src/tt.h index 61e854c1af2..e82356dac23 100644 --- a/src/tt.h +++ b/src/tt.h @@ -45,7 +45,7 @@ struct TTEntry { Depth depth() const { return Depth(depth8 + DEPTH_OFFSET); } bool is_pv() const { return bool(genBound8 & 0x4); } Bound bound() const { return Bound(genBound8 & 0x3); } - void save(Key k, Value v, bool pv, Bound b, Depth d, Move m, Value ev); + void save(Key k, Value v, bool pv, Bound b, Depth d, Move m, Value ev, uint8_t generation8); private: friend class TranspositionTable; @@ -88,23 +88,23 @@ class TranspositionTable { void new_search() { generation8 += GENERATION_DELTA; } // Lower bits are used for other things TTEntry* probe(const Key key, bool& found) const; int hashfull() const; - void resize(size_t mbSize); - void clear(); + void resize(size_t mbSize, int thread_count); + void clear(size_t thread_count); TTEntry* first_entry(const Key key) const { return &table[mul_hi64(key, clusterCount)].entry[0]; } + uint8_t generation() const { return generation8; } + private: friend struct TTEntry; size_t clusterCount; - Cluster* table; - uint8_t generation8; // Size must be not bigger than TTEntry::genBound8 + Cluster* table = nullptr; + uint8_t generation8 = 0; // Size must be not bigger than TTEntry::genBound8 }; -extern TranspositionTable TT; - } // namespace Stockfish #endif // #ifndef TT_H_INCLUDED diff --git a/src/tune.cpp b/src/tune.cpp index 1dddca0c37d..88b3b7912d5 100644 --- a/src/tune.cpp +++ b/src/tune.cpp @@ -24,14 +24,15 @@ #include #include -#include "uci.h" +#include "ucioption.h" using std::string; namespace Stockfish { bool Tune::update_on_last; -const UCI::Option* LastOption = nullptr; +const Option* LastOption = nullptr; +OptionsMap* Tune::options; static std::map TuneResults; string Tune::next(string& names, bool pop) { @@ -53,13 +54,13 @@ string Tune::next(string& names, bool pop) { return name; } -static void on_tune(const UCI::Option& o) { +static void on_tune(const Option& o) { if (!Tune::update_on_last || LastOption == &o) Tune::read_options(); } -static void make_option(const string& n, int v, const SetRange& r) { +static void make_option(OptionsMap* options, const string& n, int v, const SetRange& r) { // Do not generate option when there is nothing to tune (ie. min = max) if (r(v).first == r(v).second) @@ -68,8 +69,8 @@ static void make_option(const string& n, int v, const SetRange& r) { if (TuneResults.count(n)) v = TuneResults[n]; - Options[n] << UCI::Option(v, r(v).first, r(v).second, on_tune); - LastOption = &Options[n]; + (*options)[n] << Option(v, r(v).first, r(v).second, on_tune); + LastOption = &((*options)[n]); // Print formatted parameters, ready to be copy-pasted in Fishtest std::cout << n << "," << v << "," << r(v).first << "," << r(v).second << "," @@ -79,13 +80,13 @@ static void make_option(const string& n, int v, const SetRange& r) { template<> void Tune::Entry::init_option() { - make_option(name, value, range); + make_option(options, name, value, range); } template<> void Tune::Entry::read_option() { - if (Options.count(name)) - value = int(Options[name]); + if (options->count(name)) + value = int((*options)[name]); } // Instead of a variable here we have a PostUpdate function: just call it diff --git a/src/tune.h b/src/tune.h index 17057001225..b88c085fd4b 100644 --- a/src/tune.h +++ b/src/tune.h @@ -28,6 +28,8 @@ namespace Stockfish { +class OptionsMap; + using Range = std::pair; // Option's min-max values using RangeFun = Range(int); @@ -151,7 +153,8 @@ class Tune { return instance().add(SetDefaultRange, names.substr(1, names.size() - 2), args...); // Remove trailing parenthesis } - static void init() { + static void init(OptionsMap& o) { + options = &o; for (auto& e : instance().list) e->init_option(); read_options(); @@ -160,7 +163,9 @@ class Tune { for (auto& e : instance().list) e->read_option(); } - static bool update_on_last; + + static bool update_on_last; + static OptionsMap* options; }; // Some macro magic :-) we define a dummy int variable that the compiler initializes calling Tune::add() diff --git a/src/uci.cpp b/src/uci.cpp index be902277984..ccd5dcc98cf 100644 --- a/src/uci.cpp +++ b/src/uci.cpp @@ -22,111 +22,152 @@ #include #include #include -#include #include #include -#include #include #include #include -#include #include #include "benchmark.h" #include "evaluate.h" -#include "misc.h" #include "movegen.h" #include "nnue/evaluate_nnue.h" #include "nnue/nnue_architecture.h" #include "position.h" #include "search.h" -#include "thread.h" +#include "syzygy/tbprobe.h" +#include "types.h" +#include "ucioption.h" namespace Stockfish { -namespace { - -// FEN string for the initial position in standard chess -const char* StartFEN = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"; - - -// Called when the engine receives the "position" UCI command. -// It sets up the position that is described in the given FEN string ("fen") or -// the initial position ("startpos") and then makes the moves given in the following -// move list ("moves"). -void position(Position& pos, std::istringstream& is, StateListPtr& states) { - - Move m; - std::string token, fen; - - is >> token; - - if (token == "startpos") - { - fen = StartFEN; - is >> token; // Consume the "moves" token, if any - } - else if (token == "fen") - while (is >> token && token != "moves") - fen += token + " "; - else - return; - - states = StateListPtr(new std::deque(1)); // Drop the old state and create a new one - pos.set(fen, Options["UCI_Chess960"], &states->back(), Threads.main()); - - // Parse the move list, if any - while (is >> token && (m = UCI::to_move(pos, token)) != Move::none()) - { - states->emplace_back(); - pos.do_move(m, states->back()); - } +constexpr auto StartFEN = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"; +constexpr int NormalizeToPawnValue = 328; +constexpr int MaxHashMB = Is64Bit ? 33554432 : 2048; + +UCI::UCI(int argc, char** argv) : + cli(argc, argv) { + + options["Debug Log File"] << Option("", [](const Option& o) { start_logger(o); }); + + options["Threads"] << Option(1, 1, 1024, [this](const Option&) { + threads.set({options, threads, tt}); + }); + + options["Hash"] << Option(16, 1, MaxHashMB, [this](const Option& o) { + threads.main_thread()->wait_for_search_finished(); + tt.resize(o, options["Threads"]); + }); + + options["Clear Hash"] << Option(true, [this](const Option&) { search_clear(); }); + options["Ponder"] << Option(false); + options["MultiPV"] << Option(1, 1, 500); + options["Skill Level"] << Option(20, 0, 20); + options["Move Overhead"] << Option(10, 0, 5000); + options["nodestime"] << Option(0, 0, 10000); + options["UCI_Chess960"] << Option(false); + options["UCI_LimitStrength"] << Option(false); + options["UCI_Elo"] << Option(1320, 1320, 3190); + options["UCI_ShowWDL"] << Option(false); + options["SyzygyPath"] << Option("", [](const Option& o) { Tablebases::init(o); }); + options["SyzygyProbeDepth"] << Option(1, 1, 100); + options["Syzygy50MoveRule"] << Option(true); + options["SyzygyProbeLimit"] << Option(7, 0, 7); + options["EvalFile"] << Option(EvalFileDefaultNameBig, [this](const Option&) { + Eval::NNUE::init(cli.binaryDirectory, options, EvalFiles); + }); + + threads.set({options, threads, tt}); + + search_clear(); // After threads are up } -// Prints the evaluation of the current position, -// consistent with the UCI options set so far. -void trace_eval(Position& pos) { +void UCI::loop() { + Position pos; + std::string token, cmd; StateListPtr states(new std::deque(1)); - Position p; - p.set(pos.fen(), Options["UCI_Chess960"], &states->back(), Threads.main()); - Eval::NNUE::verify(); + pos.set(StartFEN, false, &states->back()); - sync_cout << "\n" << Eval::trace(p) << sync_endl; -} + for (int i = 1; i < cli.argc; ++i) + cmd += std::string(cli.argv[i]) + " "; + do + { + if (cli.argc == 1 + && !getline(std::cin, cmd)) // Wait for an input or an end-of-file (EOF) indication + cmd = "quit"; -// Called when the engine receives the "setoption" UCI command. -// The function updates the UCI option ("name") to the given value ("value"). + std::istringstream is(cmd); -void setoption(std::istringstream& is) { + token.clear(); // Avoid a stale if getline() returns nothing or a blank line + is >> std::skipws >> token; - Threads.main()->wait_for_search_finished(); + if (token == "quit" || token == "stop") + threads.stop = true; - std::string token, name, value; + // The GUI sends 'ponderhit' to tell that the user has played the expected move. + // So, 'ponderhit' is sent if pondering was done on the same move that the user + // has played. The search should continue, but should also switch from pondering + // to the normal search. + else if (token == "ponderhit") + threads.main_manager()->ponder = false; // Switch to the normal search - is >> token; // Consume the "name" token + else if (token == "uci") + sync_cout << "id name " << engine_info(true) << "\n" + << options << "\nuciok" << sync_endl; - // Read the option name (can contain spaces) - while (is >> token && token != "value") - name += (name.empty() ? "" : " ") + token; + 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 == "isready") + sync_cout << "readyok" << sync_endl; - // Read the option value (can contain spaces) - while (is >> token) - value += (value.empty() ? "" : " ") + token; + // Add custom non-UCI commands, mainly for debugging purposes. + // These commands must not be used during a search! + else if (token == "flip") + pos.flip(); + else if (token == "bench") + bench(pos, is, states); + else if (token == "d") + sync_cout << pos << sync_endl; + else if (token == "eval") + trace_eval(pos); + else if (token == "compiler") + sync_cout << compiler_info() << sync_endl; + else if (token == "export_net") + { + std::optional filename; + std::string f; + if (is >> std::skipws >> f) + filename = f; + Eval::NNUE::save_eval(filename, Eval::NNUE::Big, EvalFiles); + } + else if (token == "--help" || token == "help" || token == "--license" || token == "license") + sync_cout + << "\nStockfish is a powerful chess engine for playing and analyzing." + "\nIt is released as free software licensed under the GNU GPLv3 License." + "\nStockfish is normally used with a graphical user interface (GUI) and implements" + "\nthe Universal Chess Interface (UCI) protocol to communicate with a GUI, an API, etc." + "\nFor any further information, visit https://github.com/official-stockfish/Stockfish#readme" + "\nor read the corresponding README.md and Copying.txt files distributed along with this program.\n" + << sync_endl; + else if (!token.empty() && token[0] != '#') + sync_cout << "Unknown command: '" << cmd << "'. Type help for more information." + << sync_endl; - if (Options.count(name)) - Options[name] = value; - else - sync_cout << "No such option: " << name << sync_endl; + } while (token != "quit" && cli.argc == 1); // The command-line arguments are one-shot } +void UCI::go(Position& pos, std::istringstream& is, StateListPtr& states) { -// Called when the engine receives the "go" UCI command. The function sets the -// thinking time and other parameters from the input string then stars with a search - -void go(Position& pos, std::istringstream& is, StateListPtr& states) { Search::LimitsType limits; std::string token; @@ -137,7 +178,7 @@ void go(Position& pos, std::istringstream& is, StateListPtr& states) { while (is >> token) if (token == "searchmoves") // Needs to be the last command on the line while (is >> token) - limits.searchmoves.push_back(UCI::to_move(pos, token)); + limits.searchmoves.push_back(to_move(pos, token)); else if (token == "wtime") is >> limits.time[WHITE]; @@ -164,16 +205,12 @@ void go(Position& pos, std::istringstream& is, StateListPtr& states) { else if (token == "ponder") ponderMode = true; - Threads.start_thinking(pos, states, limits, ponderMode); -} - - -// Called when the engine receives the "bench" command. -// First, a list of UCI commands is set up according to the bench -// parameters, then it is run one by one, printing a summary at the end. + Eval::NNUE::verify(options, EvalFiles); -void bench(Position& pos, std::istream& args, StateListPtr& states) { + threads.start_thinking(options, pos, states, limits, ponderMode); +} +void UCI::bench(Position& pos, std::istream& args, StateListPtr& states) { std::string token; uint64_t num, nodes = 0, cnt = 1; @@ -196,8 +233,8 @@ void bench(Position& pos, std::istream& args, StateListPtr& states) { if (token == "go") { go(pos, is, states); - Threads.main()->wait_for_search_finished(); - nodes += Threads.nodes_searched(); + threads.main_thread()->wait_for_search_finished(); + nodes += threads.nodes_searched(); } else trace_eval(pos); @@ -208,9 +245,9 @@ void bench(Position& pos, std::istream& args, StateListPtr& states) { position(pos, is, states); else if (token == "ucinewgame") { - Search::clear(); + search_clear(); // Search::clear() may take a while elapsed = now(); - } // Search::clear() may take a while + } } elapsed = now() - elapsed + 1; // Ensure positivity to avoid a 'divide by zero' @@ -222,141 +259,66 @@ void bench(Position& pos, std::istream& args, StateListPtr& states) { << "\nNodes/second : " << 1000 * nodes / elapsed << std::endl; } -// The win rate model returns the probability of winning (in per mille units) given an -// eval and a game ply. It fits the LTC fishtest statistics rather accurately. -int win_rate_model(Value v, int ply) { - - // The model only captures up to 240 plies, so limit the input and then rescale - double m = std::min(240, ply) / 64.0; - - // The coefficients of a third-order polynomial fit is based on the fishtest data - // for two parameters that need to transform eval to the argument of a logistic - // function. - constexpr double as[] = {0.38036525, -2.82015070, 23.17882135, 307.36768407}; - constexpr double bs[] = {-2.29434733, 13.27689788, -14.26828904, 63.45318330}; - - // Enforce that NormalizeToPawnValue corresponds to a 50% win rate at ply 64 - static_assert(UCI::NormalizeToPawnValue == int(as[0] + as[1] + as[2] + as[3])); +void UCI::trace_eval(Position& pos) { + StateListPtr states(new std::deque(1)); + Position p; + p.set(pos.fen(), options["UCI_Chess960"], &states->back()); - double a = (((as[0] * m + as[1]) * m + as[2]) * m) + as[3]; - double b = (((bs[0] * m + bs[1]) * m + bs[2]) * m) + bs[3]; + Eval::NNUE::verify(options, EvalFiles); - // Transform the eval to centipawns with limited range - double x = std::clamp(double(v), -4000.0, 4000.0); - - // Return the win rate in per mille units, rounded to the nearest integer - return int(0.5 + 1000 / (1 + std::exp((a - x) / b))); + sync_cout << "\n" << Eval::trace(p, *threads.main_thread()->worker.get()) << sync_endl; } -} // namespace +void UCI::search_clear() { + threads.main_thread()->wait_for_search_finished(); + tt.clear(options["Threads"]); + threads.clear(); + Tablebases::init(options["SyzygyPath"]); // Free mapped files +} -// Waits for a command from the stdin, parses it, and then calls the appropriate -// function. It also intercepts an end-of-file (EOF) indication from the stdin to ensure a -// graceful exit if the GUI dies unexpectedly. When called with some command-line arguments, -// like running 'bench', the function returns immediately after the command is executed. -// In addition to the UCI ones, some additional debug commands are also supported. -void UCI::loop(int argc, char* argv[]) { - - Position pos; - std::string token, cmd; - StateListPtr states(new std::deque(1)); +void UCI::setoption(std::istringstream& is) { + threads.main_thread()->wait_for_search_finished(); + options.setoption(is); +} - pos.set(StartFEN, false, &states->back(), Threads.main()); +void UCI::position(Position& pos, std::istringstream& is, StateListPtr& states) { + Move m; + std::string token, fen; - for (int i = 1; i < argc; ++i) - cmd += std::string(argv[i]) + " "; + is >> token; - do + if (token == "startpos") { - if (argc == 1 - && !getline(std::cin, cmd)) // Wait for an input or an end-of-file (EOF) indication - cmd = "quit"; - - std::istringstream is(cmd); - - token.clear(); // Avoid a stale if getline() returns nothing or a blank line - is >> std::skipws >> token; - - if (token == "quit" || token == "stop") - Threads.stop = true; - - // The GUI sends 'ponderhit' to tell that the user has played the expected move. - // So, 'ponderhit' is sent if pondering was done on the same move that the user - // has played. The search should continue, but should also switch from pondering - // to the normal search. - else if (token == "ponderhit") - Threads.main()->ponder = false; // Switch to the normal search - - else if (token == "uci") - sync_cout << "id name " << engine_info(true) << "\n" - << Options << "\nuciok" << 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 == "isready") - sync_cout << "readyok" << sync_endl; + fen = StartFEN; + is >> token; // Consume the "moves" token, if any + } + else if (token == "fen") + while (is >> token && token != "moves") + fen += token + " "; + else + return; - // Add custom non-UCI commands, mainly for debugging purposes. - // These commands must not be used during a search! - else if (token == "flip") - pos.flip(); - else if (token == "bench") - bench(pos, is, states); - else if (token == "d") - sync_cout << pos << sync_endl; - else if (token == "eval") - trace_eval(pos); - else if (token == "compiler") - sync_cout << compiler_info() << sync_endl; - else if (token == "export_net") - { - std::optional filename; - std::string f; - if (is >> std::skipws >> f) - filename = f; - Eval::NNUE::save_eval(filename, Eval::NNUE::Big); - } - else if (token == "--help" || token == "help" || token == "--license" || token == "license") - sync_cout - << "\nStockfish is a powerful chess engine for playing and analyzing." - "\nIt is released as free software licensed under the GNU GPLv3 License." - "\nStockfish is normally used with a graphical user interface (GUI) and implements" - "\nthe Universal Chess Interface (UCI) protocol to communicate with a GUI, an API, etc." - "\nFor any further information, visit https://github.com/official-stockfish/Stockfish#readme" - "\nor read the corresponding README.md and Copying.txt files distributed along with this program.\n" - << sync_endl; - else if (!token.empty() && token[0] != '#') - sync_cout << "Unknown command: '" << cmd << "'. Type help for more information." - << sync_endl; + states = StateListPtr(new std::deque(1)); // Drop the old state and create a new one + pos.set(fen, options["UCI_Chess960"], &states->back()); - } while (token != "quit" && argc == 1); // The command-line arguments are one-shot + // Parse the move list, if any + while (is >> token && (m = to_move(pos, token)) != Move::none()) + { + states->emplace_back(); + pos.do_move(m, states->back()); + } } +int UCI::to_cp(Value v) { return 100 * v / NormalizeToPawnValue; } -// Turns a Value to an integer centipawn number, -// without treatment of mate and similar special scores. -int UCI::to_cp(Value v) { return 100 * v / UCI::NormalizeToPawnValue; } - -// Converts a Value to a string by adhering to the UCI protocol specification: -// -// cp The score from the engine's point of view in centipawns. -// mate Mate in 'y' moves (not plies). If the engine is getting mated, -// uses negative values for 'y'. std::string UCI::value(Value v) { - assert(-VALUE_INFINITE < v && v < VALUE_INFINITE); std::stringstream ss; if (std::abs(v) < VALUE_TB_WIN_IN_MAX_PLY) - ss << "cp " << UCI::to_cp(v); + ss << "cp " << to_cp(v); else if (std::abs(v) <= VALUE_TB) { const int ply = VALUE_TB - std::abs(v); // recompute ss->ply @@ -368,34 +330,11 @@ std::string UCI::value(Value v) { return ss.str(); } - -// Reports the win-draw-loss (WDL) statistics given an evaluation -// and a game ply based on the data gathered for fishtest LTC games. -std::string UCI::wdl(Value v, int ply) { - - std::stringstream ss; - - int wdl_w = win_rate_model(v, ply); - int wdl_l = win_rate_model(-v, ply); - int wdl_d = 1000 - wdl_w - wdl_l; - ss << " wdl " << wdl_w << " " << wdl_d << " " << wdl_l; - - return ss.str(); -} - - -// 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))}; } - -// Converts a Move to a string in coordinate notation (g1f3, a7a8q). -// The only special case is castling where the e1g1 notation is printed in -// standard chess mode and in e1h1 notation it is printed in Chess960 mode. -// Internally, all castling moves are always encoded as 'king captures rook'. std::string UCI::move(Move m, bool chess960) { - if (m == Move::none()) return "(none)"; @@ -408,7 +347,7 @@ std::string UCI::move(Move m, bool chess960) { if (m.type_of() == CASTLING && !chess960) to = make_square(to > from ? FILE_G : FILE_C, rank_of(from)); - std::string move = UCI::square(from) + UCI::square(to); + std::string move = square(from) + square(to); if (m.type_of() == PROMOTION) move += " pnbrqk"[m.promotion_type()]; @@ -416,16 +355,108 @@ std::string UCI::move(Move m, bool chess960) { return move; } +std::string UCI::pv(const Search::Worker& workerThread, + TimePoint elapsed, + uint64_t nodesSearched, + uint64_t tb_hits, + int hashfull, + bool rootInTB) { + std::stringstream ss; + TimePoint time = elapsed + 1; + const auto& rootMoves = workerThread.rootMoves; + const auto& depth = workerThread.completedDepth; + const auto& pos = workerThread.rootPos; + size_t pvIdx = workerThread.pvIdx; + size_t multiPV = std::min(size_t(workerThread.options["MultiPV"]), rootMoves.size()); + uint64_t tbHits = tb_hits + (rootInTB ? rootMoves.size() : 0); -// Converts a string representing a move in coordinate notation -// (g1f3, a7a8q) to the corresponding legal Move, if any. -Move UCI::to_move(const Position& pos, std::string& str) { + for (size_t i = 0; i < multiPV; ++i) + { + bool updated = rootMoves[i].score != -VALUE_INFINITE; + + if (depth == 1 && !updated && i > 0) + continue; + + Depth d = updated ? depth : std::max(1, depth - 1); + Value v = updated ? rootMoves[i].uciScore : rootMoves[i].previousScore; + + if (v == -VALUE_INFINITE) + v = VALUE_ZERO; + + bool tb = rootInTB && std::abs(v) <= VALUE_TB; + v = tb ? rootMoves[i].tbScore : v; + + if (ss.rdbuf()->in_avail()) // Not at first line + ss << "\n"; + + ss << "info" + << " depth " << d << " seldepth " << rootMoves[i].selDepth << " multipv " << i + 1 + << " score " << value(v); + + if (workerThread.options["UCI_ShowWDL"]) + ss << wdl(v, pos.game_ply()); + + if (i == pvIdx && !tb && updated) // tablebase- and previous-scores are exact + ss << (rootMoves[i].scoreLowerbound + ? " lowerbound" + : (rootMoves[i].scoreUpperbound ? " upperbound" : "")); + + ss << " nodes " << nodesSearched << " nps " << nodesSearched * 1000 / time << " hashfull " + << hashfull << " tbhits " << tbHits << " time " << time << " pv"; + + for (Move m : rootMoves[i].pv) + ss << " " << move(m, pos.is_chess960()); + } + + return ss.str(); +} + +namespace { +// The win rate model returns the probability of winning (in per mille units) given an +// eval and a game ply. It fits the LTC fishtest statistics rather accurately. +int win_rate_model(Value v, int ply) { + + // The model only captures up to 240 plies, so limit the input and then rescale + double m = std::min(240, ply) / 64.0; + + // The coefficients of a third-order polynomial fit is based on the fishtest data + // for two parameters that need to transform eval to the argument of a logistic + // function. + constexpr double as[] = {0.38036525, -2.82015070, 23.17882135, 307.36768407}; + constexpr double bs[] = {-2.29434733, 13.27689788, -14.26828904, 63.45318330}; + + // Enforce that NormalizeToPawnValue corresponds to a 50% win rate at ply 64 + static_assert(NormalizeToPawnValue == int(as[0] + as[1] + as[2] + as[3])); + + double a = (((as[0] * m + as[1]) * m + as[2]) * m) + as[3]; + double b = (((bs[0] * m + bs[1]) * m + bs[2]) * m) + bs[3]; + + // Transform the eval to centipawns with limited range + double x = std::clamp(double(v), -4000.0, 4000.0); + + // Return the win rate in per mille units, rounded to the nearest integer + return int(0.5 + 1000 / (1 + std::exp((a - x) / b))); +} +} + +std::string UCI::wdl(Value v, int ply) { + std::stringstream ss; + + int wdl_w = win_rate_model(v, ply); + int wdl_l = win_rate_model(-v, ply); + int wdl_d = 1000 - wdl_w - wdl_l; + ss << " wdl " << wdl_w << " " << wdl_d << " " << wdl_l; + + return ss.str(); +} + +Move UCI::to_move(const Position& pos, std::string& str) { if (str.length() == 5) str[4] = char(tolower(str[4])); // The promotion piece character must be lowercased for (const auto& m : MoveList(pos)) - if (str == UCI::move(m, pos.is_chess960())) + if (str == move(m, pos.is_chess960())) return m; return Move::none(); diff --git a/src/uci.h b/src/uci.h index d249da7442b..035d012492f 100644 --- a/src/uci.h +++ b/src/uci.h @@ -19,77 +19,72 @@ #ifndef UCI_H_INCLUDED #define UCI_H_INCLUDED -#include -#include -#include +#include +#include #include +#include -#include "types.h" +#include "evaluate.h" +#include "misc.h" +#include "position.h" +#include "thread.h" +#include "tt.h" + +#include "ucioption.h" namespace Stockfish { -class Position; -namespace UCI { +namespace Search { +class Worker; +} -// Normalizes the internal value as reported by evaluate or search -// to the UCI centipawn result used in output. This value is derived from -// the win_rate_model() such that Stockfish outputs an advantage of -// "100 centipawns" for a position if the engine has a 50% probability to win -// from this position in self-play at fishtest LTC time control. -const int NormalizeToPawnValue = 328; +class Move; +enum Square : int; +using Value = int; -class Option; +class UCI { + public: + UCI(int argc, char** argv); -// Define a custom comparator, because the UCI options should be case-insensitive -struct CaseInsensitiveLess { - bool operator()(const std::string&, const std::string&) const; -}; + void loop(); -// The options container is defined as a std::map -using OptionsMap = std::map; + static int to_cp(Value v); + static std::string value(Value v); + static std::string square(Square s); + static std::string move(Move m, bool chess960); + static std::string pv(const Search::Worker& workerThread, + TimePoint elapsed, + uint64_t nodesSearched, + uint64_t tb_hits, + int hashfull, + bool rootInTB); + static std::string wdl(Value v, int ply); + static Move to_move(const Position& pos, std::string& str); -// The Option class implements each option as specified by the UCI protocol -class Option { + const std::string& workingDirectory() const { return cli.workingDirectory; } - using OnChange = void (*)(const Option&); + OptionsMap options; + std::string currentEvalFileName = "None"; - public: - Option(OnChange = nullptr); - Option(bool v, OnChange = nullptr); - Option(const char* v, OnChange = nullptr); - Option(double v, int minv, int maxv, OnChange = nullptr); - Option(const char* v, const char* cur, OnChange = nullptr); - - Option& operator=(const std::string&); - void operator<<(const Option&); - operator int() const; - operator std::string() const; - bool operator==(const char*) const; + std::unordered_map EvalFiles = { + {Eval::NNUE::Big, {"EvalFile", EvalFileDefaultNameBig, "None"}}, + {Eval::NNUE::Small, {"EvalFileSmall", EvalFileDefaultNameSmall, "None"}}}; private: - friend std::ostream& operator<<(std::ostream&, const OptionsMap&); - - std::string defaultValue, currentValue, type; - int min, max; - size_t idx; - OnChange on_change; + TranspositionTable tt; + ThreadPool threads; + CommandLine cli; + + void go(Position& pos, std::istringstream& is, StateListPtr& states); + void bench(Position& pos, std::istream& args, StateListPtr& states); + void position(Position& pos, std::istringstream& is, StateListPtr& states); + void trace_eval(Position& pos); + void search_clear(); + void setoption(std::istringstream& is); }; -void init(OptionsMap&); -void loop(int argc, char* argv[]); -int to_cp(Value v); -std::string value(Value v); -std::string square(Square s); -std::string move(Move m, bool chess960); -std::string pv(const Position& pos, Depth depth); -std::string wdl(Value v, int ply); -Move to_move(const Position& pos, std::string& str); - -} // namespace UCI - -extern UCI::OptionsMap Options; - } // namespace Stockfish -#endif // #ifndef UCI_H_INCLUDED + +#endif \ No newline at end of file diff --git a/src/ucioption.cpp b/src/ucioption.cpp index f8cbcc53077..eefb4aa8124 100644 --- a/src/ucioption.cpp +++ b/src/ucioption.cpp @@ -16,104 +16,53 @@ along with this program. If not, see . */ +#include "ucioption.h" + #include #include #include -#include -#include -#include -#include -#include +#include #include -#include +#include -#include "evaluate.h" #include "misc.h" -#include "search.h" -#include "syzygy/tbprobe.h" -#include "thread.h" -#include "tt.h" -#include "types.h" -#include "uci.h" - -using std::string; namespace Stockfish { -UCI::OptionsMap Options; // Global object - -namespace UCI { - -// 'On change' actions, triggered by an option's value change -static void on_clear_hash(const Option&) { Search::clear(); } -static void on_hash_size(const Option& o) { TT.resize(size_t(o)); } -static void on_logger(const Option& o) { start_logger(o); } -static void on_threads(const Option& o) { Threads.set(size_t(o)); } -static void on_tb_path(const Option& o) { Tablebases::init(o); } -static void on_eval_file(const Option&) { Eval::NNUE::init(); } - -// Our case insensitive less() function as required by UCI protocol -bool CaseInsensitiveLess::operator()(const string& s1, const string& s2) const { +bool CaseInsensitiveLess::operator()(const std::string& s1, const std::string& s2) const { - return std::lexicographical_compare(s1.begin(), s1.end(), s2.begin(), s2.end(), - [](char c1, char c2) { return tolower(c1) < tolower(c2); }); + return std::lexicographical_compare( + s1.begin(), s1.end(), s2.begin(), s2.end(), + [](char c1, char c2) { return std::tolower(c1) < std::tolower(c2); }); } +void OptionsMap::setoption(std::istringstream& is) { + std::string token, name, value; -// Initializes the UCI options to their hard-coded default values -void init(OptionsMap& o) { - - constexpr int MaxHashMB = Is64Bit ? 33554432 : 2048; - - o["Debug Log File"] << Option("", on_logger); - o["Threads"] << Option(1, 1, 1024, on_threads); - o["Hash"] << Option(16, 1, MaxHashMB, on_hash_size); - o["Clear Hash"] << Option(on_clear_hash); - o["Ponder"] << Option(false); - o["MultiPV"] << Option(1, 1, MAX_MOVES); - o["Skill Level"] << Option(20, 0, 20); - o["Move Overhead"] << Option(10, 0, 5000); - o["nodestime"] << Option(0, 0, 10000); - o["UCI_Chess960"] << Option(false); - o["UCI_LimitStrength"] << Option(false); - o["UCI_Elo"] << Option(1320, 1320, 3190); - o["UCI_ShowWDL"] << Option(false); - o["SyzygyPath"] << Option("", on_tb_path); - o["SyzygyProbeDepth"] << Option(1, 1, 100); - o["Syzygy50MoveRule"] << Option(true); - o["SyzygyProbeLimit"] << Option(7, 0, 7); - o["EvalFile"] << Option(EvalFileDefaultNameBig, on_eval_file); - // Enable this after fishtest workers support EvalFileSmall - // o["EvalFileSmall"] << Option(EvalFileDefaultNameSmall, on_eval_file); -} - - -// Used to print all the options default values in chronological -// insertion order (the idx field) and in the format defined by the UCI protocol. -std::ostream& operator<<(std::ostream& os, const OptionsMap& om) { - - for (size_t idx = 0; idx < om.size(); ++idx) - for (const auto& it : om) - if (it.second.idx == idx) - { - const Option& o = it.second; - os << "\noption name " << it.first << " type " << o.type; + is >> token; // Consume the "name" token - if (o.type == "string" || o.type == "check" || o.type == "combo") - os << " default " << o.defaultValue; + // Read the option name (can contain spaces) + while (is >> token && token != "value") + name += (name.empty() ? "" : " ") + token; - if (o.type == "spin") - os << " default " << int(stof(o.defaultValue)) << " min " << o.min << " max " - << o.max; + // Read the option value (can contain spaces) + while (is >> token) + value += (value.empty() ? "" : " ") + token; - break; - } + if (options_map.count(name)) + options_map[name] = value; + else + sync_cout << "No such option: " << name << sync_endl; +} - return os; +Option OptionsMap::operator[](const std::string& name) const { + auto it = options_map.find(name); + return it != options_map.end() ? it->second : Option(); } +Option& OptionsMap::operator[](const std::string& name) { return options_map[name]; } -// Option class constructors and conversion operators +std::size_t OptionsMap::count(const std::string& name) const { return options_map.count(name); } Option::Option(const char* v, OnChange f) : type("string"), @@ -184,19 +133,19 @@ void Option::operator<<(const Option& o) { // Updates currentValue and triggers on_change() action. It's up to // the GUI to check for option's limits, but we could receive the new value // from the user by console window, so let's check the bounds anyway. -Option& Option::operator=(const string& v) { +Option& Option::operator=(const std::string& v) { assert(!type.empty()); if ((type != "button" && type != "string" && v.empty()) || (type == "check" && v != "true" && v != "false") - || (type == "spin" && (stof(v) < min || stof(v) > max))) + || (type == "spin" && (std::stof(v) < min || std::stof(v) > max))) return *this; if (type == "combo") { OptionsMap comboMap; // To have case insensitive compare - string token; + std::string token; std::istringstream ss(defaultValue); while (ss >> token) comboMap[token] << Option(); @@ -213,6 +162,24 @@ Option& Option::operator=(const string& v) { return *this; } -} // namespace UCI +std::ostream& operator<<(std::ostream& os, const OptionsMap& om) { + for (size_t idx = 0; idx < om.options_map.size(); ++idx) + for (const auto& it : om.options_map) + if (it.second.idx == idx) + { + const Option& o = it.second; + os << "\noption name " << it.first << " type " << o.type; + + if (o.type == "string" || o.type == "check" || o.type == "combo") + os << " default " << o.defaultValue; + + if (o.type == "spin") + os << " default " << int(stof(o.defaultValue)) << " min " << o.min << " max " + << o.max; + + break; + } -} // namespace Stockfish + return os; +} +} \ No newline at end of file diff --git a/src/ucioption.h b/src/ucioption.h new file mode 100644 index 00000000000..b7b4bdec48d --- /dev/null +++ b/src/ucioption.h @@ -0,0 +1,81 @@ +/* + Stockfish, a UCI chess playing engine derived from Glaurung 2.1 + Copyright (C) 2004-2024 The Stockfish developers (see AUTHORS file) + + Stockfish is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Stockfish is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef UCIOPTION_H_INCLUDED +#define UCIOPTION_H_INCLUDED + +#include +#include +#include +#include +#include + +namespace Stockfish { +// Define a custom comparator, because the UCI options should be case-insensitive +struct CaseInsensitiveLess { + bool operator()(const std::string&, const std::string&) const; +}; + +class Option; + +class OptionsMap { + public: + void setoption(std::istringstream&); + + friend std::ostream& operator<<(std::ostream&, const OptionsMap&); + + Option operator[](const std::string&) const; + Option& operator[](const std::string&); + + std::size_t count(const std::string&) const; + + private: + // The options container is defined as a std::map + using OptionsStore = std::map; + + OptionsStore options_map; +}; + +// The Option class implements each option as specified by the UCI protocol +class Option { + public: + using OnChange = std::function; + + Option(OnChange = nullptr); + Option(bool v, OnChange = nullptr); + Option(const char* v, OnChange = nullptr); + Option(double v, int minv, int maxv, OnChange = nullptr); + Option(const char* v, const char* cur, OnChange = nullptr); + + Option& operator=(const std::string&); + void operator<<(const Option&); + operator int() const; + operator std::string() const; + bool operator==(const char*) const; + + friend std::ostream& operator<<(std::ostream&, const OptionsMap&); + + private: + std::string defaultValue, currentValue, type; + int min, max; + size_t idx; + OnChange on_change; +}; + +} +#endif \ No newline at end of file