]> git.sesse.net Git - stockfish/commitdiff
Refactor global variables
authorDisservin <disservin.social@gmail.com>
Mon, 8 Jan 2024 18:48:46 +0000 (19:48 +0100)
committerDisservin <disservin.social@gmail.com>
Sat, 13 Jan 2024 18:40:53 +0000 (19:40 +0100)
This aims to remove some of the annoying global structure which Stockfish has.

Overall there is no major elo regression to be expected.

Non regression SMP STC (paused, early version):
https://tests.stockfishchess.org/tests/view/65983d7979aa8af82b9608f1
LLR: 0.23 (-2.94,2.94) <-1.75,0.25>
Total: 76232 W: 19035 L: 19096 D: 38101
Ptnml(0-2): 92, 8735, 20515, 8690, 84

Non regression STC (early version):
https://tests.stockfishchess.org/tests/view/6595b3a479aa8af82b95da7f
LLR: 2.93 (-2.94,2.94) <-1.75,0.25>
Total: 185344 W: 47027 L: 46972 D: 91345
Ptnml(0-2): 571, 21285, 48943, 21264, 609

Non regression SMP STC:
https://tests.stockfishchess.org/tests/view/65a0715c79aa8af82b96b7e4
LLR: 2.94 (-2.94,2.94) <-1.75,0.25>
Total: 142936 W: 35761 L: 35662 D: 71513
Ptnml(0-2): 209, 16400, 38135, 16531, 193

These global structures/variables add hidden dependencies and allow data
to be mutable from where it shouldn't it be (i.e. options). They also
prevent Stockfish from internal selfplay, which would be a nice thing to
be able to do, i.e. instantiate two Stockfish instances and let them
play against each other. It will also allow us to make Stockfish a
library, which can be easier used on other platforms.

For consistency with the old search code, `thisThread` has been kept,
even though it is not strictly necessary anymore. This the first major
refactor of this kind (in recent time), and future changes are required,
to achieve the previously described goals. This includes cleaning up the
dependencies, transforming the network to be self contained and coming
up with a plan to deal with proper tablebase memory management (see
comments for more information on this).

The removal of these global structures has been discussed in parts with
Vondele and Sopel.

closes https://github.com/official-stockfish/Stockfish/pull/4968

No functional change

27 files changed:
src/Makefile
src/evaluate.cpp
src/evaluate.h
src/main.cpp
src/misc.cpp
src/misc.h
src/nnue/evaluate_nnue.cpp
src/nnue/evaluate_nnue.h
src/position.cpp
src/position.h
src/search.cpp
src/search.h
src/syzygy/tbprobe.cpp
src/syzygy/tbprobe.h
src/thread.cpp
src/thread.h
src/thread_win32_osx.h
src/timeman.cpp
src/timeman.h
src/tt.cpp
src/tt.h
src/tune.cpp
src/tune.h
src/uci.cpp
src/uci.h
src/ucioption.cpp
src/ucioption.h [new file with mode: 0644]

index e6de514e568b40f8a2ada262973fe3b56416d09a..9680ca7feff269accd529270de4d502dea3a7bb7 100644 (file)
@@ -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))
 
index e220b92a7fcf9cec6a7d933dfeef1143d0aba6e3..3e067e4c44798ae0d824d74286a28c46c93e41d4 100644 (file)
@@ -25,6 +25,7 @@
 #include <fstream>
 #include <iomanip>
 #include <iostream>
+#include <optional>
 #include <sstream>
 #include <unordered_map>
 #include <vector>
 #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"
+#include "ucioption.h"
 
 // Macro to embed the default efficiently updatable neural network (NNUE) file
 // data in the engine binary (using incbin.h, by Dale Weiler).
@@ -62,10 +64,6 @@ namespace Stockfish {
 
 namespace Eval {
 
-std::unordered_map<NNUE::NetSize, EvalFile> 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,38 +72,45 @@ std::unordered_map<NNUE::NetSize, EvalFile> 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() {
+NNUE::EvalFiles NNUE::load_networks(const std::string& rootDirectory,
+                                    const OptionsMap&  options,
+                                    NNUE::EvalFiles    evalFiles) {
 
-    for (auto& [netSize, evalFile] : EvalFiles)
+    for (auto& [netSize, evalFile] : evalFiles)
     {
         // Replace with
-        // Options[evalFile.option_name]
+        // options[evalFile.optionName]
         // once fishtest supports the uci option EvalFileSmall
         std::string user_eval_file =
-          netSize == Small ? evalFile.default_name : Options[evalFile.option_name];
+          netSize == Small ? evalFile.defaultName : options[evalFile.optionName];
 
         if (user_eval_file.empty())
-            user_eval_file = evalFile.default_name;
+            user_eval_file = evalFile.defaultName;
 
 #if defined(DEFAULT_NNUE_DIRECTORY)
-        std::vector<std::string> dirs = {"<internal>", "", CommandLine::binaryDirectory,
+        std::vector<std::string> dirs = {"<internal>", "", rootDirectory,
                                          stringify(DEFAULT_NNUE_DIRECTORY)};
 #else
-        std::vector<std::string> dirs = {"<internal>", "", CommandLine::binaryDirectory};
+        std::vector<std::string> dirs = {"<internal>", "", rootDirectory};
 #endif
 
         for (const std::string& directory : dirs)
         {
-            if (evalFile.selected_name != user_eval_file)
+            if (evalFile.current != user_eval_file)
             {
                 if (directory != "<internal>")
                 {
                     std::ifstream stream(directory + user_eval_file, std::ios::binary);
-                    if (NNUE::load_eval(user_eval_file, stream, netSize))
-                        evalFile.selected_name = user_eval_file;
+                    auto          description = NNUE::load_eval(stream, netSize);
+
+                    if (description.has_value())
+                    {
+                        evalFile.current        = user_eval_file;
+                        evalFile.netDescription = description.value();
+                    }
                 }
 
-                if (directory == "<internal>" && user_eval_file == evalFile.default_name)
+                if (directory == "<internal>" && user_eval_file == evalFile.defaultName)
                 {
                     // C++ way to prepare a buffer for a memory stream
                     class MemoryBuffer: public std::basic_streambuf<char> {
@@ -124,28 +129,36 @@ void NNUE::init() {
                     (void) gEmbeddedNNUESmallEnd;
 
                     std::istream stream(&buffer);
-                    if (NNUE::load_eval(user_eval_file, stream, netSize))
-                        evalFile.selected_name = user_eval_file;
+                    auto         description = NNUE::load_eval(stream, netSize);
+
+                    if (description.has_value())
+                    {
+                        evalFile.current        = user_eval_file;
+                        evalFile.netDescription = description.value();
+                    }
                 }
             }
         }
     }
+
+    return evalFiles;
 }
 
 // Verifies that the last net used was loaded successfully
-void NNUE::verify() {
+void NNUE::verify(const OptionsMap&                                        options,
+                  const std::unordered_map<Eval::NNUE::NetSize, EvalFile>& evalFiles) {
 
-    for (const auto& [netSize, evalFile] : EvalFiles)
+    for (const auto& [netSize, evalFile] : evalFiles)
     {
         // Replace with
-        // Options[evalFile.option_name]
+        // options[evalFile.optionName]
         // once fishtest supports the uci option EvalFileSmall
         std::string user_eval_file =
-          netSize == Small ? evalFile.default_name : Options[evalFile.option_name];
+          netSize == Small ? evalFile.defaultName : options[evalFile.optionName];
         if (user_eval_file.empty())
-            user_eval_file = evalFile.default_name;
+            user_eval_file = evalFile.defaultName;
 
-        if (evalFile.selected_name != user_eval_file)
+        if (evalFile.current != user_eval_file)
         {
             std::string msg1 =
               "Network evaluation parameters compatible with the engine must be available.";
@@ -155,7 +168,7 @@ void NNUE::verify() {
                                "including the directory name, to the network file.";
             std::string msg4 = "The default net can be downloaded from: "
                                "https://tests.stockfishchess.org/api/nn/"
-                             + evalFile.default_name;
+                             + evalFile.defaultName;
             std::string msg5 = "The engine will be terminated now.";
 
             sync_cout << "info string ERROR: " << msg1 << sync_endl;
@@ -183,7 +196,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 +217,7 @@ Value Eval::evaluate(const Position& pos) {
         Value nnue = smallNet ? NNUE::evaluate<NNUE::Small>(pos, true, &nnueComplexity)
                               : NNUE::evaluate<NNUE::Big>(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 +240,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 +262,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, ...]";
index 79b77192a24586a201c0f55c5ce14c8aeb430f77..8a9d6fc7c87046cbb8bd49255df019bc75a89d91 100644 (file)
 namespace Stockfish {
 
 class Position;
+class OptionsMap;
+
+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,22 +46,27 @@ Value evaluate(const Position& pos);
 #define EvalFileDefaultNameBig "nn-baff1edbea57.nnue"
 #define EvalFileDefaultNameSmall "nn-baff1ede1f90.nnue"
 
+struct EvalFile {
+    // UCI option name
+    std::string optionName;
+    // Default net name, will use one of the macros above
+    std::string defaultName;
+    // Selected net name, either via uci option or default
+    std::string current;
+    // Net description extracted from the net file
+    std::string netDescription;
+};
+
 namespace NNUE {
 
 enum NetSize : int;
 
-void init();
-void verify();
+using EvalFiles = std::unordered_map<Eval::NNUE::NetSize, EvalFile>;
 
-}  // namespace NNUE
+EvalFiles load_networks(const std::string&, const OptionsMap&, EvalFiles);
+void      verify(const OptionsMap&, const EvalFiles&);
 
-struct EvalFile {
-    std::string option_name;
-    std::string default_name;
-    std::string selected_name;
-};
-
-extern std::unordered_map<NNUE::NetSize, EvalFile> EvalFiles;
+}  // namespace NNUE
 
 }  // namespace Eval
 
index 78b3f54d9194a0c1f6126cd4ceb7d22ed2f71ace..de07d6a8738d25c0a0c79a4b4cf91e6d37af2dda 100644 (file)
   along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-#include <cstddef>
 #include <iostream>
+#include <unordered_map>
 
 #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"
@@ -35,17 +33,16 @@ int main(int argc, char* argv[]) {
 
     std::cout << engine_info() << std::endl;
 
-    CommandLine::init(argc, argv);
-    UCI::init(Options);
-    Tune::init();
     Bitboards::init();
     Position::init();
-    Threads.set(size_t(Options["Threads"]));
-    Search::clear();  // After threads are up
-    Eval::NNUE::init();
 
-    UCI::loop(argc, argv);
+    UCI uci(argc, argv);
+
+    Tune::init(uci.options);
+
+    uci.evalFiles = Eval::NNUE::load_networks(uci.workingDirectory(), uci.options, uci.evalFiles);
+
+    uci.loop();
 
-    Threads.set(0);
     return 0;
 }
index 9350a4830df369a9f0b6c91383b6b9b3c17624e2..4885a5cd35cf54ad54349bd45bcb2204818adcf9 100644 (file)
@@ -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
index ca6cc16639f209f79c89690784bfe5cfc6e0a750..994f551d5ff304d1f619058c2f76d6c5901e4632 100644 (file)
@@ -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, char**);
+
+    int    argc;
+    char** argv;
+
+    std::string binaryDirectory;   // path of the executable directory
+    std::string workingDirectory;  // path of the working directory
+};
 
 }  // namespace Stockfish
 
index 86fe523050ae8c3f7287301b87cd926089535cbf..d4a4dbe4c458d2b19b6e7c6a74598f1a62c5eda3 100644 (file)
 #include <fstream>
 #include <iomanip>
 #include <iostream>
+#include <optional>
 #include <sstream>
 #include <string_view>
+#include <type_traits>
 #include <unordered_map>
 
 #include "../evaluate.h"
@@ -51,8 +53,6 @@ AlignedPtr<Network<TransformedFeatureDimensionsBig, L2Big, L3Big>>       network
 AlignedPtr<Network<TransformedFeatureDimensionsSmall, L2Small, L3Small>> networkSmall[LayerStacks];
 
 // Evaluation function file names
-std::string fileName[2];
-std::string netDescription[2];
 
 namespace Detail {
 
@@ -136,10 +136,10 @@ static bool write_header(std::ostream& stream, std::uint32_t hashValue, const st
 }
 
 // Read network parameters
-static bool read_parameters(std::istream& stream, NetSize netSize) {
+static bool read_parameters(std::istream& stream, NetSize netSize, std::string& netDescription) {
 
     std::uint32_t hashValue;
-    if (!read_header(stream, &hashValue, &netDescription[netSize]))
+    if (!read_header(stream, &hashValue, &netDescription))
         return false;
     if (hashValue != HashValue[netSize])
         return false;
@@ -158,9 +158,10 @@ static bool read_parameters(std::istream& stream, NetSize netSize) {
 }
 
 // Write network parameters
-static bool write_parameters(std::ostream& stream, NetSize netSize) {
+static bool
+write_parameters(std::ostream& stream, NetSize netSize, const std::string& netDescription) {
 
-    if (!write_header(stream, HashValue[netSize], netDescription[netSize]))
+    if (!write_header(stream, HashValue[netSize], netDescription))
         return false;
     if (netSize == Big && !Detail::write_parameters(stream, *featureTransformerBig))
         return false;
@@ -424,24 +425,30 @@ std::string trace(Position& pos) {
 
 
 // Load eval, from a file stream or a memory stream
-bool load_eval(const std::string name, std::istream& stream, NetSize netSize) {
+std::optional<std::string> load_eval(std::istream& stream, NetSize netSize) {
 
     initialize(netSize);
-    fileName[netSize] = name;
-    return read_parameters(stream, netSize);
+    std::string netDescription;
+    return read_parameters(stream, netSize, netDescription) ? std::make_optional(netDescription)
+                                                            : std::nullopt;
 }
 
 // Save eval, to a file stream or a memory stream
-bool save_eval(std::ostream& stream, NetSize netSize) {
+bool save_eval(std::ostream&      stream,
+               NetSize            netSize,
+               const std::string& name,
+               const std::string& netDescription) {
 
-    if (fileName[netSize].empty())
+    if (name.empty() || name == "None")
         return false;
 
-    return write_parameters(stream, netSize);
+    return write_parameters(stream, netSize, netDescription);
 }
 
 // Save eval, to a file given by its name
-bool save_eval(const std::optional<std::string>& filename, NetSize netSize) {
+bool save_eval(const std::optional<std::string>&                              filename,
+               NetSize                                                        netSize,
+               const std::unordered_map<Eval::NNUE::NetSize, Eval::EvalFile>& evalFiles) {
 
     std::string actualFilename;
     std::string msg;
@@ -450,7 +457,7 @@ bool save_eval(const std::optional<std::string>& filename, NetSize netSize) {
         actualFilename = filename.value();
     else
     {
-        if (EvalFiles.at(netSize).selected_name
+        if (evalFiles.at(netSize).current
             != (netSize == Small ? EvalFileDefaultNameSmall : EvalFileDefaultNameBig))
         {
             msg = "Failed to export a net. "
@@ -463,7 +470,8 @@ bool save_eval(const std::optional<std::string>& filename, NetSize netSize) {
     }
 
     std::ofstream stream(actualFilename, std::ios_base::binary);
-    bool          saved = save_eval(stream, netSize);
+    bool          saved = save_eval(stream, netSize, evalFiles.at(netSize).current,
+                                    evalFiles.at(netSize).netDescription);
 
     msg = saved ? "Network saved successfully to " + actualFilename : "Failed to export a net";
 
index fabfb5693f936c2dc8a9dd7bad72d059c6662dc6..ea88f8902279686b7dadf0ccf4db24d21921ec2e 100644 (file)
 #include <memory>
 #include <optional>
 #include <string>
+#include <unordered_map>
 
 #include "../misc.h"
+#include "../types.h"
 #include "nnue_architecture.h"
 #include "nnue_feature_transformer.h"
-#include "../types.h"
 
 namespace Stockfish {
 class Position;
+
+namespace Eval {
+struct EvalFile;
+}
+
 }
 
 namespace Stockfish::Eval::NNUE {
@@ -73,9 +79,14 @@ template<NetSize Net_Size>
 Value evaluate(const Position& pos, bool adjusted = false, int* complexity = nullptr);
 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<std::string>& filename, NetSize netSize);
+std::optional<std::string> load_eval(std::istream& stream, NetSize netSize);
+bool                       save_eval(std::ostream&      stream,
+                                     NetSize            netSize,
+                                     const std::string& name,
+                                     const std::string& netDescription);
+bool                       save_eval(const std::optional<std::string>& filename,
+                                     NetSize                           netSize,
+                                     const std::unordered_map<Eval::NNUE::NetSize, Eval::EvalFile>&);
 
 }  // namespace Stockfish::Eval::NNUE
 
index ddc31888422915ffd72622f23e637072e5507379..6202381d072efb14f7e60ad23ffc080297f2493a 100644 (file)
@@ -19,7 +19,6 @@
 #include "position.h"
 
 #include <algorithm>
-#include <atomic>
 #include <cassert>
 #include <cctype>
 #include <cstddef>
@@ -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());
 }
index 34b53f4a55821321f4c613c639c643e392a69de7..7ce3556f0e91e63913c495965afcc89e696e88b7 100644 (file)
@@ -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<std::deque<StateInfo>>;
 // 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;
index e93b12d159802a2c5d5fc641265fde141762333d..5530d125f15cb2108900ba6a470e8ec25e184e84 100644 (file)
@@ -27,8 +27,6 @@
 #include <cstring>
 #include <initializer_list>
 #include <iostream>
-#include <sstream>
-#include <string>
 #include <utility>
 
 #include "bitboard.h"
 #include "timeman.h"
 #include "tt.h"
 #include "uci.h"
+#include "ucioption.h"
 
 namespace Stockfish {
 
-namespace Search {
-
-LimitsType Limits;
-}
-
 namespace Tablebases {
 
 int   Cardinality;
@@ -62,33 +56,17 @@ Depth ProbeDepth;
 
 namespace TB = Tablebases;
 
-using std::string;
 using Eval::evaluate;
 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) {
     return ((116 - 44 * noTtCutNode) * (d - improving));
 }
 
-// Reductions lookup table initialized at startup
-int Reductions[MAX_MOVES];  // [depth or moveNumber]
-
-Depth reduction(bool i, Depth d, int mn, int delta, int rootDelta) {
-    int reductionScale = Reductions[d] * Reductions[mn];
-    return (reductionScale + 1346 - int(delta) * 896 / int(rootDelta)) / 1024
-         + (!i && reductionScale > 880);
-}
-
 constexpr int futility_move_count(bool improving, Depth depth) {
     return improving ? (3 + depth * depth) : (3 + depth * depth) / 2;
 }
@@ -105,9 +83,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 +103,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<NodeType nodeType>
-Value search(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth, bool cutNode);
-
-template<NodeType nodeType>
-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.
@@ -187,42 +159,35 @@ uint64_t perft(Position& pos, Depth depth) {
 }  // namespace
 
 
-// Called at startup to initialize various lookup tables
-void Search::init() {
-
-    for (int i = 1; i < MAX_MOVES; ++i)
-        Reductions[i] = int((20.37 + std::log(Threads.size()) / 2) * std::log(i));
+Search::Worker::Worker(SharedState&                    sharedState,
+                       std::unique_ptr<ISearchManager> sm,
+                       size_t                          thread_id) :
+    // Unpack the SharedState struct into member variables
+    thread_idx(thread_id),
+    manager(std::move(sm)),
+    options(sharedState.options),
+    threads(sharedState.threads),
+    tt(sharedState.tt) {
+    clear();
 }
 
+void Search::Worker::start_searching() {
+    // Non-main threads go directly to iterative_deepening()
+    if (!is_mainthread())
+    {
+        iterative_deepening();
+        return;
+    }
 
-// 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
-}
-
-
-// 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<true>(rootPos, Limits.perft);
+        nodes = perft<true>(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();
+    main_manager()->tm.init(limits, rootPos.side_to_move(), rootPos.game_ply(), options);
+    tt.new_search();
 
     if (rootMoves.empty())
     {
@@ -232,73 +197,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.advance_nodes_time(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 +280,7 @@ void Thread::search() {
 
     ss->pv = pv;
 
-    bestValue = -VALUE_INFINITE;
+    iterBestValue = -VALUE_INFINITE;
 
     if (mainThread)
     {
@@ -325,8 +292,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 +305,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 +320,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 +357,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<Root>(rootPos, ss, alpha, beta, adjustedDepth, false);
+                iterBestValue = search<Root>(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 +370,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 +409,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 +428,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,16 +494,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
+    for (int i = 1; i < MAX_MOVES; ++i)
+        reductions[i] = int((20.37 + std::log(size_t(options["Threads"])) / 2) * std::log(i));
+}
+
+
+// Main search function for both PV and non-PV nodes.
 template<NodeType nodeType>
-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;
@@ -539,7 +534,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(this->nodes);
         if (alpha >= beta)
             return alpha;
     }
@@ -564,7 +559,7 @@ 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();
+    Worker* thisThread = this;
     ss->inCheck        = pos.checkers();
     priorCapture       = pos.captured_piece();
     Color us           = pos.side_to_move();
@@ -573,8 +568,8 @@ Value search(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth, boo
     maxValue                                              = VALUE_INFINITE;
 
     // Check for the available remaining time
-    if (thisThread == Threads.main())
-        static_cast<MainThread*>(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 +578,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, *thisThread)
+                                                        : 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 +609,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 +633,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 +671,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<MainThread*>(thisThread)->callsCnt = 0;
+            if (is_mainthread())
+                main_manager()->callsCnt = 0;
 
             if (err != TB::ProbeState::FAIL)
             {
@@ -699,7 +694,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 +711,6 @@ Value search(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth, boo
         }
     }
 
-    CapturePieceToHistory& captureHistory = thisThread->captureHistory;
 
     Value unadjustedStaticEval = VALUE_NONE;
 
@@ -739,7 +734,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, *thisThread);
         else if (PvNode)
             Eval::NNUE::hint_common_parent_position(pos);
 
@@ -757,7 +752,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, *thisThread);
 
         Value newEval =
           ss->staticEval
@@ -769,7 +764,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 +822,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<NonPV>(pos, ss + 1, -beta, -beta + 1, depth - R, !cutNode);
 
@@ -885,7 +880,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 +888,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 +912,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 +940,8 @@ moves_loop:  // When in check, search starts here
     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 +974,8 @@ moves_loop:  // When in check, search starts here
 
         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;
@@ -995,7 +992,7 @@ moves_loop:  // When in check, search starts here
 
         int delta = beta - alpha;
 
-        Depth r = reduction(improving, depth, moveCount, delta, thisThread->rootDelta);
+        Depth r = reduction(improving, depth, moveCount, delta);
 
         // Step 14. Pruning at shallow depth (~120 Elo).
         // Depth conditions are important for mate finding.
@@ -1016,7 +1013,8 @@ moves_loop:  // When in check, search starts here
                     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 +1133,8 @@ moves_loop:  // When in check, search starts here
 
             // 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 +1144,7 @@ moves_loop:  // When in check, search starts here
         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 +1152,7 @@ moves_loop:  // When in check, search starts here
           &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 +1268,7 @@ moves_loop:  // When in check, search starts here
         // 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 +1370,8 @@ moves_loop:  // When in check, search starts here
 
     // 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 +1399,7 @@ moves_loop:  // When in check, search starts here
                   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,7 +1421,7 @@ moves_loop:  // When in check, search starts here
 // function with zero depth, or recursively with further decreasing depth per call.
 // (~155 Elo)
 template<NodeType nodeType>
-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;
@@ -1435,7 +1434,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(this->nodes);
         if (alpha >= beta)
             return alpha;
     }
@@ -1460,7 +1459,7 @@ Value qsearch(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth) {
         ss->pv[0]    = Move::none();
     }
 
-    Thread* thisThread = pos.this_thread();
+    Worker* thisThread = this;
     bestMove           = Move::none();
     ss->inCheck        = pos.checkers();
     moveCount          = 0;
@@ -1471,7 +1470,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, *thisThread) : VALUE_DRAW;
 
     assert(0 <= ss->ply && ss->ply < MAX_PLY);
 
@@ -1480,7 +1479,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 +1501,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, *thisThread);
 
             Value newEval =
               ss->staticEval
@@ -1522,7 +1521,8 @@ 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, *thisThread)
+                                                    : -(ss - 1)->staticEval;
 
             Value newEval =
               ss->staticEval
@@ -1539,7 +1539,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 +1632,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 +1643,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<nodeType>(pos, ss + 1, -beta, -alpha, depth - 1);
         pos.undo_move(move);
@@ -1686,7 +1687,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 +1695,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 +1761,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 +1773,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 +1786,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 +1844,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 +1854,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 +1898,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 +1923,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 +1947,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 +1960,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 +1981,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"]);
         }
     }
 
index 72e275d36f7c2605d236aa8c976c763dc58813cb..48a5630cddb47527e8cb415f332660dd4cb9d30c 100644 (file)
 #ifndef SEARCH_H_INCLUDED
 #define SEARCH_H_INCLUDED
 
+#include <atomic>
+#include <cassert>
+#include <cstddef>
 #include <cstdint>
+#include <memory>
 #include <vector>
 
 #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 +79,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 {
@@ -85,7 +103,6 @@ using RootMoves = std::vector<RootMove>;
 
 // LimitsType struct stores information sent by GUI about available time to
 // search the current move, maximum depth/time, or if we are in analysis mode.
-
 struct LimitsType {
 
     // Init explicitly due to broken value-initialization of non POD in MSVC
@@ -103,10 +120,136 @@ 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 SharedState {
+    SharedState(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, implement a common interface
+// for the SearchManagers. A Null Object will be given to
+// non-mainthread workers.
+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 {}
+};
+
+// Search::Worker is the class that does the actual search.
+// It is instantiated once per thread, and it is responsible for keeping track
+// of the search history, and storing data required for the search.
+class Worker {
+   public:
+    Worker(SharedState&, std::unique_ptr<ISearchManager>, size_t);
+
+    // 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<NodeType nodeType>
+    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<NodeType nodeType>
+    Value qsearch(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth = 0);
+
+    Depth reduction(bool i, Depth d, int mn, int delta) {
+        int reductionScale = reductions[d] * reductions[mn];
+        return (reductionScale + 1346 - int(delta) * 896 / int(rootDelta)) / 1024
+             + (!i && reductionScale > 880);
+    }
+
+    // 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<SearchManager*>(manager.get());
+    }
+
+    LimitsType limits;
+
+    size_t                pvIdx, pvLast;
+    std::atomic<uint64_t> nodes, tbHits, bestMoveChanges;
+    int                   selDepth, nmpMinPly;
+
+    Position  rootPos;
+    StateInfo rootState;
+    RootMoves rootMoves;
+    Depth     rootDepth, completedDepth;
+    Value     rootDelta;
+
+    size_t thread_idx;
+
+    // Reductions lookup table initialized at startup
+    int reductions[MAX_MOVES];  // [depth or moveNumber]
+
+    // The main thread has a SearchManager, the others have a NullSearchManager
+    std::unique_ptr<ISearchManager> manager;
+
+    const OptionsMap&   options;
+    ThreadPool&         threads;
+    TranspositionTable& tt;
+
+    friend class Stockfish::ThreadPool;
+    friend class Stockfish::UCI;
+    friend class SearchManager;
+};
+
 
 }  // namespace Search
 
index 91013dcac2833daebc633462a3f8efb4bf4ce500..6f30bf6b1f7c7704a7e7b0fb89470088686c6f17 100644 (file)
@@ -42,7 +42,6 @@
 #include "../position.h"
 #include "../search.h"
 #include "../types.h"
-#include "../uci.h"
 
 #ifndef _WIN32
     #include <fcntl.h>
@@ -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)
index cc8eb0d4d707b97fbd8c22b54153399e35f1addd..d7b412a10e5295c1820a7eaeac03d11b47dc05e0 100644 (file)
@@ -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
 
index 01ccd4fc565e37c2a67b963db0653b4b88b0bea4..a512c0a52b86707828d4cfb3a673fc630220d369 100644 (file)
@@ -23,9 +23,8 @@
 #include <cmath>
 #include <cstdlib>
 #include <deque>
-#include <initializer_list>
-#include <unordered_map>
 #include <memory>
+#include <unordered_map>
 #include <utility>
 
 #include "evaluate.h"
 #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::SharedState&                    sharedState,
+               std::unique_ptr<Search::ISearchManager> sm,
+               size_t                                  n) :
+    worker(std::make_unique<Search::Worker>(sharedState, std::move(sm), n)),
     idx(n),
+    nthreads(sharedState.options["Threads"]),
     stdThread(&Thread::idle_loop, this) {
 
     wait_for_search_finished();
@@ -62,24 +64,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 +92,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 +107,41 @@ 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::SharedState sharedState) {
 
     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 = sharedState.options["Threads"];
+
     if (requested > 0)  // create new thread(s)
     {
-        threads.push_back(new MainThread(0));
+        threads.push_back(new Thread(
+          sharedState, std::unique_ptr<Search::ISearchManager>(new Search::SearchManager()), 0));
+
 
         while (threads.size() < requested)
-            threads.push_back(new Thread(threads.size()));
+            threads.push_back(new Thread(
+              sharedState, std::unique_ptr<Search::ISearchManager>(new Search::NullSearchManager()),
+              threads.size()));
         clear();
 
-        // Reallocate the hash with the new threadpool size
-        TT.resize(size_t(Options["Hash"]));
+        main_thread()->wait_for_search_finished();
 
-        // Init thread number dependent search params.
-        Search::init();
+        // Reallocate the hash with the new threadpool size
+        sharedState.tt.resize(sharedState.options["Hash"], requested);
     }
 }
 
@@ -161,28 +150,31 @@ 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.clear();
 }
 
 
 // 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()->wait_for_search_finished();
+    main_manager()->stopOnPonderhit = stop = false;
+    main_manager()->ponder                 = ponderMode;
+
+    increaseDepth = true;
 
-    main()->stopOnPonderhit = stop = false;
-    increaseDepth                  = true;
-    main()->ponder                 = ponderMode;
-    Search::Limits                 = limits;
     Search::RootMoves rootMoves;
 
     for (const auto& m : MoveList<LEGAL>(pos))
@@ -191,7 +183,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 +199,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 +220,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;
@@ -257,7 +253,7 @@ Thread* ThreadPool::get_best_thread() const {
 
 
 // Start non-main threads
-
+// Will be invoked by main thread after it has started searching
 void ThreadPool::start_searching() {
 
     for (Thread* th : threads)
index 7db7c15905b7c5a6fb1e36eedf0c804c59141eee..6575b14e6381b3ef4e4fe01668cfaacec899a06b 100644 (file)
 #include <condition_variable>
 #include <cstddef>
 #include <cstdint>
+#include <memory>
 #include <mutex>
 #include <vector>
 
-#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::SharedState&, std::unique_ptr<Search::ISearchManager>, 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<uint64_t> 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<Search::Worker> 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<MainThread*>(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::SharedState);
+
+    Search::SearchManager* main_manager() const {
+        return static_cast<Search::SearchManager*>(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<Thread*> threads;
 
-    uint64_t accumulate(std::atomic<uint64_t> Thread::*member) const {
+    uint64_t accumulate(std::atomic<uint64_t> 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).load(std::memory_order_relaxed);
         return sum;
     }
 };
 
-extern ThreadPool Threads;
-
 }  // namespace Stockfish
 
 #endif  // #ifndef THREAD_H_INCLUDED
index 4bc62d678ad05036b4f9e0bfa16eb44560906507..8d424b72a5044e122f7d73e2d7a4edcb72024b4e 100644 (file)
 #if defined(__APPLE__) || defined(__MINGW32__) || defined(__MINGW64__) || defined(USE_PTHREADS)
 
     #include <pthread.h>
+    #include <functional>
 
 namespace Stockfish {
 
-static const size_t TH_STACK_SIZE = 8 * 1024 * 1024;
-
-template<class T, class P = std::pair<T*, void (T::*)()>>
-void* start_routine(void* ptr) {
-    P* p = reinterpret_cast<P*>(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<std::function<void()>*>(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<class T, class P = std::pair<T*, void (T::*)()>>
-    explicit NativeThread(void (T::*fun)(), T* obj) {
+    template<class Function, class... Args>
+    explicit NativeThread(Function&& fun, Args&&... args) {
+        auto func = new std::function<void()>(
+          std::bind(std::forward<Function>(fun), std::forward<Args>(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<T>, new P(obj, fun));
+        pthread_create(&thread, attr, start_routine, func);
     }
+
     void join() { pthread_join(thread, nullptr); }
 };
 
index 77db2f621dfb3048b76ec7e7e888f91a91e8d64f..121f8edb98547e9d82e6b03c8222ae89b7816c4c 100644 (file)
 #include "timeman.h"
 
 #include <algorithm>
+#include <cassert>
 #include <cmath>
+#include <cstdint>
 
 #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;
+}
+
+void TimeManagement::clear() {
+    availableNodes = 0;  // When in 'nodes as time' mode
+}
+
+void TimeManagement::advance_nodes_time(std::int64_t nodes) {
+    assert(useNodesTime);
+    availableNodes += nodes;
+}
 
 // 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 +71,8 @@ void TimeManagement::init(Search::LimitsType& limits, Color us, int ply) {
     // must be much lower than the real engine speed.
     if (npmsec)
     {
+        useNodesTime = true;
+
         if (!availableNodes)                            // Only once at game start
             availableNodes = npmsec * limits.time[us];  // Time is in msec
 
@@ -100,7 +119,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;
 }
 
index 0509158c3f429ae1a7be894dcd9eefbb1664f200..b07712a25c228ae87cc3ca44a20068dae92cc4c7 100644 (file)
 #ifndef TIMEMAN_H_INCLUDED
 #define TIMEMAN_H_INCLUDED
 
+#include <cstddef>
 #include <cstdint>
 
 #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
+    void clear();
+    void advance_nodes_time(std::int64_t nodes);
 
    private:
     TimePoint startTime;
     TimePoint optimumTime;
     TimePoint maximumTime;
-};
 
-extern TimeManagement Time;
+    std::int64_t availableNodes = 0;      // When in 'nodes as time' mode
+    bool         useNodesTime   = false;  // True if we are in 'nodes as time' mode
+};
 
 }  // namespace Stockfish
 
index 2e3f7d32835560d7767754ad7b74dd400c1e99c8..f3f58979d1b4520fa347a472bd3f469ab5408870 100644 (file)
 #include <vector>
 
 #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 threadCount) {
     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(threadCount);
 }
 
 
 // Initializes the entire transposition table to zero,
 // in a multi-threaded way.
-void TranspositionTable::clear() {
-
+void TranspositionTable::clear(size_t threadCount) {
     std::vector<std::thread> threads;
 
-    for (size_t idx = 0; idx < size_t(Options["Threads"]); ++idx)
+    for (size_t idx = 0; idx < size_t(threadCount); ++idx)
     {
-        threads.emplace_back([this, idx]() {
+        threads.emplace_back([this, idx, threadCount]() {
             // Thread binding gives faster search on systems with a first-touch policy
-            if (Options["Threads"] > 8)
+            if (threadCount > 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 / threadCount), start = size_t(stride * idx),
+                         len = idx != size_t(threadCount) - 1 ? stride : clusterCount - start;
 
             std::memset(&table[start], 0, len * sizeof(Cluster));
         });
index 61e854c1af2a8afb1bb65a8502a870156d03d98b..4115ee7ae5186ecdc9fffcc19cabb09bad05a74d 100644 (file)
--- 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 threadCount);
+    void     clear(size_t threadCount);
 
     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
index 1dddca0c37da070a6984484e3cd62439ae1cc0ac..88b3b7912d5967374477388f5b3a4dabc6862225 100644 (file)
 #include <sstream>
 #include <string>
 
-#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<std::string, int> 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<int>::init_option() {
-    make_option(name, value, range);
+    make_option(options, name, value, range);
 }
 
 template<>
 void Tune::Entry<int>::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
index 170570012257d46cdd08a4578046519ad46a7c9c..b88c085fd4b37ff0b4617e46783d638cfb117ef8 100644 (file)
@@ -28,6 +28,8 @@
 
 namespace Stockfish {
 
+class OptionsMap;
+
 using Range    = std::pair<int, int>;  // 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()
index be902277984ebb3412d342f804e003da9a579e76..82fb25c1d27b2c7361b7a9a73372ad5cb0d7bce3 100644 (file)
 #include <cassert>
 #include <cctype>
 #include <cmath>
-#include <cstdint>
 #include <cstdlib>
 #include <deque>
-#include <iostream>
 #include <memory>
 #include <optional>
 #include <sstream>
-#include <string>
 #include <vector>
 
 #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<StateInfo>(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) {
+
+    evalFiles = {{Eval::NNUE::Big, {"EvalFile", EvalFileDefaultNameBig, "None", ""}},
+                 {Eval::NNUE::Small, {"EvalFileSmall", EvalFileDefaultNameSmall, "None", ""}}};
+
+
+    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("<empty>", [](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&) {
+        evalFiles = Eval::NNUE::load_networks(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<StateInfo>(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<std::string> 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 +182,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 +209,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 +237,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 +249,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 +263,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<StateInfo>(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<StateInfo>(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<std::string> 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<StateInfo>(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 <x>    The score from the engine's point of view in centipawns.
-// mate <y>  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 +334,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 +351,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 +359,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<LEGAL>(pos))
-        if (str == UCI::move(m, pos.is_chess960()))
+        if (str == move(m, pos.is_chess960()))
             return m;
 
     return Move::none();
index d249da7442bf6e7732a405ff1c123c6dfd6983dc..cd113b1ad29a73318ff7ceb57634d03a3f74fb87 100644 (file)
--- a/src/uci.h
+++ b/src/uci.h
 #ifndef UCI_H_INCLUDED
 #define UCI_H_INCLUDED
 
-#include <cstddef>
-#include <iosfwd>
-#include <map>
+#include <cstdint>
+#include <iostream>
 #include <string>
+#include <unordered_map>
 
-#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 Eval::NNUE {
+enum NetSize : int;
+}
 
-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<std::string, Option, CaseInsensitiveLess>;
+    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;
 
-   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<Eval::NNUE::NetSize, Eval::EvalFile> evalFiles;
 
    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
index f8cbcc53077faf77cf5131ddad395b0b6483ea80..c7de7e3f7f2109f24ca0abcc24a5219c3470d729 100644 (file)
   along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
+#include "ucioption.h"
+
 #include <algorithm>
 #include <cassert>
 #include <cctype>
-#include <cstddef>
-#include <iosfwd>
-#include <istream>
-#include <map>
-#include <ostream>
+#include <iostream>
 #include <sstream>
-#include <string>
+#include <utility>
 
-#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 {
+bool CaseInsensitiveLess::operator()(const std::string& s1, const std::string& s2) const {
 
-// '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 {
-
-    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("<empty>", 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);
-}
+    is >> token;  // Consume the "name" token
 
+    // Read the option name (can contain spaces)
+    while (is >> token && token != "value")
+        name += (name.empty() ? "" : " ") + token;
 
-// 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) {
+    // Read the option value (can contain spaces)
+    while (is >> token)
+        value += (value.empty() ? "" : " ") + token;
 
-    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;
-
-                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;
-            }
+    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;
+}
+}
diff --git a/src/ucioption.h b/src/ucioption.h
new file mode 100644 (file)
index 0000000..b575d16
--- /dev/null
@@ -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 <http://www.gnu.org/licenses/>.
+*/
+
+#ifndef UCIOPTION_H_INCLUDED
+#define UCIOPTION_H_INCLUDED
+
+#include <cstddef>
+#include <functional>
+#include <iosfwd>
+#include <map>
+#include <string>
+
+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<std::string, Option, CaseInsensitiveLess>;
+
+    OptionsStore options_map;
+};
+
+// The Option class implements each option as specified by the UCI protocol
+class Option {
+   public:
+    using OnChange = std::function<void(const Option&)>;
+
+    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  // #ifndef UCIOPTION_H_INCLUDED