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))
#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).
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"
// 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> {
(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.";
"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;
// 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());
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;
// 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);
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, ...]";
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
#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
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"
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;
}
#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 = "\\";
binaryDirectory.replace(0, 1, workingDirectory);
}
-
-} // namespace CommandLine
-
} // namespace Stockfish
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
#include <fstream>
#include <iomanip>
#include <iostream>
+#include <optional>
#include <sstream>
#include <string_view>
+#include <type_traits>
#include <unordered_map>
#include "../evaluate.h"
AlignedPtr<Network<TransformedFeatureDimensionsSmall, L2Small, L3Small>> networkSmall[LayerStacks];
// Evaluation function file names
-std::string fileName[2];
-std::string netDescription[2];
namespace Detail {
}
// 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;
}
// 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;
// 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;
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. "
}
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";
#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 {
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
#include "position.h"
#include <algorithm>
-#include <atomic>
#include <cassert>
#include <cctype>
#include <cstddef>
#include "movegen.h"
#include "nnue/nnue_common.h"
#include "syzygy/tbprobe.h"
-#include "thread.h"
#include "tt.h"
#include "uci.h"
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);
// 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.
// 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());
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);
}
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
// 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);
st->key ^= Zobrist::side;
++st->rule50;
- prefetch(TT.first_entry(key()));
+ prefetch(tt.first_entry(key()));
st->pliesFromNull = 0;
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());
}
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.
// 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();
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;
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
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;
int castlingRightsMask[SQUARE_NB];
Square castlingRookSquare[CASTLING_RIGHT_NB];
Bitboard castlingPath[CASTLING_RIGHT_NB];
- Thread* thisThread;
StateInfo* st;
int gamePly;
Color sideToMove;
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;
#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;
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;
}
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.
}
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.
} // 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())
{
}
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)
ss->pv = pv;
- bestValue = -VALUE_INFINITE;
+ iterBestValue = -VALUE_INFINITE;
if (mainThread)
{
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.
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)
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)
{
// 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
// 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
// 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)
}
// 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;
}
// 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;
// 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;
}
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();
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)
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
// 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()
{
// 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).
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)
{
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;
}
}
}
- CapturePieceToHistory& captureHistory = thisThread->captureHistory;
Value unadjustedStaticEval = VALUE_NONE;
// 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);
}
else
{
- unadjustedStaticEval = ss->staticEval = eval = evaluate(pos);
+ unadjustedStaticEval = ss->staticEval = eval = evaluate(pos, *thisThread);
Value newEval =
ss->staticEval
// 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)
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);
{
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))
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
{
// 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;
}
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;
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;
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.
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;
}
// 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;
}
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;
&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)
// 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)
// 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)
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))
// 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;
// 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;
}
ss->pv[0] = Move::none();
}
- Thread* thisThread = pos.this_thread();
+ Worker* thisThread = this;
bestMove = Move::none();
ss->inCheck = pos.checkers();
moveCount = 0;
// 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);
// 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();
{
// 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
{
// 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
{
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;
}
}
// 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;
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);
// 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);
}
+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.
// 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,
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;
: 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);
}
// 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)
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;
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)
{
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);
return false;
pos.do_move(pv[0], st);
- TTEntry* tte = TT.probe(pos.key(), ttHit);
+ TTEntry* tte = tt.probe(pos.key(), ttHit);
if (ttHit)
{
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
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"]);
}
}
#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
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 {
// 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
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
#include "../position.h"
#include "../search.h"
#include "../types.h"
-#include "../uci.h"
#ifndef _WIN32
#include <fcntl.h>
// 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;
// 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)
// 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};
StateInfo st;
WDLScore wdl;
- bool rule50 = Options["Syzygy50MoveRule"];
// Probe and rank each move
for (auto& m : rootMoves)
namespace Stockfish {
class Position;
+class OptionsMap;
}
namespace Stockfish::Tablebases {
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
#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();
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();
// 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)
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);
}
}
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))
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.
// 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 {
// 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;
// Start non-main threads
-
+// Will be invoked by main thread after it has started searching
void ThreadPool::start_searching() {
for (Thread* th : threads)
#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;
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
#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); }
};
#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.
// 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
maximumTime =
TimePoint(std::min(0.84 * limits.time[us] - moveOverhead, maxScale * optimumTime)) - 10;
- if (Options["Ponder"])
+ if (options["Ponder"])
optimumTime += optimumTime / 4;
}
#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
#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)
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);
}
// 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);
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));
});
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;
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
#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) {
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)
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 << ","
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
namespace Stockfish {
+class OptionsMap;
+
using Range = std::pair<int, int>; // Option's min-max values
using RangeFun = Range(int);
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();
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()
#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;
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];
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;
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);
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'
<< "\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
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)";
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()];
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();
#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
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"),
// 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();
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;
+}
+}
--- /dev/null
+/*
+ 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