mirror of
https://github.com/HChaZZY/Stockfish.git
synced 2025-12-21 17:46:26 +08:00
This is another refactor which aims to decouple uci from stockfish. A new engine class manages all engine related logic and uci is a "small" wrapper around it. In the future we should also try to remove the need for the Position object in the uci and replace the options with an actual options struct instead of using a map. Also convert the std::string's in the Info structs a string_view. closes #5147 No functional change
418 lines
13 KiB
C++
418 lines
13 KiB
C++
/*
|
|
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/>.
|
|
*/
|
|
|
|
#include "uci.h"
|
|
|
|
#include <algorithm>
|
|
#include <cassert>
|
|
#include <cctype>
|
|
#include <cmath>
|
|
#include <cstdint>
|
|
#include <cstdlib>
|
|
#include <deque>
|
|
#include <memory>
|
|
#include <optional>
|
|
#include <sstream>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include "benchmark.h"
|
|
#include "engine.h"
|
|
#include "evaluate.h"
|
|
#include "movegen.h"
|
|
#include "nnue/network.h"
|
|
#include "nnue/nnue_common.h"
|
|
#include "perft.h"
|
|
#include "position.h"
|
|
#include "search.h"
|
|
#include "syzygy/tbprobe.h"
|
|
#include "types.h"
|
|
#include "ucioption.h"
|
|
|
|
namespace Stockfish {
|
|
|
|
constexpr auto StartFEN = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
|
|
constexpr int MaxHashMB = Is64Bit ? 33554432 : 2048;
|
|
|
|
|
|
UCIEngine::UCIEngine(int argc, char** argv) :
|
|
engine(argv[0]),
|
|
cli(argc, argv) {
|
|
|
|
auto& options = engine.get_options();
|
|
|
|
options["Debug Log File"] << Option("", [](const Option& o) { start_logger(o); });
|
|
|
|
options["Threads"] << Option(1, 1, 1024, [this](const Option&) { engine.resize_threads(); });
|
|
|
|
options["Hash"] << Option(16, 1, MaxHashMB, [this](const Option& o) { engine.set_tt_size(o); });
|
|
|
|
options["Clear Hash"] << Option([this](const Option&) { engine.search_clear(); });
|
|
options["Ponder"] << Option(false);
|
|
options["MultiPV"] << Option(1, 1, MAX_MOVES);
|
|
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& o) { engine.load_big_network(o); });
|
|
options["EvalFileSmall"] << Option(EvalFileDefaultNameSmall,
|
|
[this](const Option& o) { engine.load_small_network(o); });
|
|
|
|
engine.load_networks();
|
|
engine.resize_threads();
|
|
engine.search_clear(); // After threads are up
|
|
}
|
|
|
|
void UCIEngine::loop() {
|
|
|
|
Position pos;
|
|
std::string token, cmd;
|
|
StateListPtr states(new std::deque<StateInfo>(1));
|
|
|
|
pos.set(StartFEN, false, &states->back());
|
|
|
|
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";
|
|
|
|
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")
|
|
engine.stop();
|
|
|
|
// 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")
|
|
engine.set_ponderhit(false);
|
|
|
|
else if (token == "uci")
|
|
sync_cout << "id name " << engine_info(true) << "\n"
|
|
<< engine.get_options() << "\nuciok" << sync_endl;
|
|
|
|
else if (token == "setoption")
|
|
setoption(is);
|
|
else if (token == "go")
|
|
go(pos, is);
|
|
else if (token == "position")
|
|
position(is);
|
|
else if (token == "ucinewgame")
|
|
engine.search_clear();
|
|
else if (token == "isready")
|
|
sync_cout << "readyok" << sync_endl;
|
|
|
|
// 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);
|
|
else if (token == "d")
|
|
sync_cout << pos << sync_endl;
|
|
else if (token == "eval")
|
|
engine.trace_eval();
|
|
else if (token == "compiler")
|
|
sync_cout << compiler_info() << sync_endl;
|
|
else if (token == "export_net")
|
|
{
|
|
std::pair<std::optional<std::string>, std::string> files[2];
|
|
|
|
if (is >> std::skipws >> files[0].second)
|
|
files[0].first = files[0].second;
|
|
|
|
if (is >> std::skipws >> files[1].second)
|
|
files[1].first = files[1].second;
|
|
|
|
engine.save_network(files);
|
|
}
|
|
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;
|
|
|
|
} while (token != "quit" && cli.argc == 1); // The command-line arguments are one-shot
|
|
}
|
|
|
|
Search::LimitsType UCIEngine::parse_limits(const Position& pos, std::istream& is) {
|
|
Search::LimitsType limits;
|
|
std::string token;
|
|
|
|
limits.startTime = now(); // The search starts as early as possible
|
|
|
|
while (is >> token)
|
|
if (token == "searchmoves") // Needs to be the last command on the line
|
|
while (is >> token)
|
|
limits.searchmoves.push_back(to_move(pos, token));
|
|
|
|
else if (token == "wtime")
|
|
is >> limits.time[WHITE];
|
|
else if (token == "btime")
|
|
is >> limits.time[BLACK];
|
|
else if (token == "winc")
|
|
is >> limits.inc[WHITE];
|
|
else if (token == "binc")
|
|
is >> limits.inc[BLACK];
|
|
else if (token == "movestogo")
|
|
is >> limits.movestogo;
|
|
else if (token == "depth")
|
|
is >> limits.depth;
|
|
else if (token == "nodes")
|
|
is >> limits.nodes;
|
|
else if (token == "movetime")
|
|
is >> limits.movetime;
|
|
else if (token == "mate")
|
|
is >> limits.mate;
|
|
else if (token == "perft")
|
|
is >> limits.perft;
|
|
else if (token == "infinite")
|
|
limits.infinite = 1;
|
|
else if (token == "ponder")
|
|
limits.ponderMode = true;
|
|
|
|
return limits;
|
|
}
|
|
|
|
void UCIEngine::go(Position& pos, std::istringstream& is) {
|
|
|
|
Search::LimitsType limits = parse_limits(pos, is);
|
|
engine.go(limits);
|
|
}
|
|
|
|
void UCIEngine::bench(Position& pos, std::istream& args) {
|
|
std::string token;
|
|
uint64_t num, nodes = 0, cnt = 1;
|
|
|
|
std::vector<std::string> list = setup_bench(pos, args);
|
|
|
|
num = count_if(list.begin(), list.end(),
|
|
[](const std::string& s) { return s.find("go ") == 0 || s.find("eval") == 0; });
|
|
|
|
TimePoint elapsed = now();
|
|
|
|
for (const auto& cmd : list)
|
|
{
|
|
std::istringstream is(cmd);
|
|
is >> std::skipws >> token;
|
|
|
|
if (token == "go" || token == "eval")
|
|
{
|
|
std::cerr << "\nPosition: " << cnt++ << '/' << num << " (" << pos.fen() << ")"
|
|
<< std::endl;
|
|
if (token == "go")
|
|
{
|
|
go(pos, is);
|
|
engine.wait_for_search_finished();
|
|
nodes += engine.nodes_searched();
|
|
}
|
|
else
|
|
engine.trace_eval();
|
|
}
|
|
else if (token == "setoption")
|
|
setoption(is);
|
|
else if (token == "position")
|
|
position(is);
|
|
else if (token == "ucinewgame")
|
|
{
|
|
engine.search_clear(); // search_clear may take a while
|
|
elapsed = now();
|
|
}
|
|
}
|
|
|
|
elapsed = now() - elapsed + 1; // Ensure positivity to avoid a 'divide by zero'
|
|
|
|
dbg_print();
|
|
|
|
std::cerr << "\n==========================="
|
|
<< "\nTotal time (ms) : " << elapsed << "\nNodes searched : " << nodes
|
|
<< "\nNodes/second : " << 1000 * nodes / elapsed << std::endl;
|
|
}
|
|
|
|
|
|
void UCIEngine::setoption(std::istringstream& is) {
|
|
engine.wait_for_search_finished();
|
|
engine.get_options().setoption(is);
|
|
}
|
|
|
|
void UCIEngine::position(std::istringstream& is) {
|
|
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;
|
|
|
|
std::vector<std::string> moves;
|
|
|
|
while (is >> token)
|
|
{
|
|
moves.push_back(token);
|
|
}
|
|
|
|
engine.set_position(fen, moves);
|
|
}
|
|
|
|
namespace {
|
|
|
|
struct WinRateParams {
|
|
double a;
|
|
double b;
|
|
};
|
|
|
|
WinRateParams win_rate_params(const Position& pos) {
|
|
|
|
int material = pos.count<PAWN>() + 3 * pos.count<KNIGHT>() + 3 * pos.count<BISHOP>()
|
|
+ 5 * pos.count<ROOK>() + 9 * pos.count<QUEEN>();
|
|
|
|
// The fitted model only uses data for material counts in [10, 78], and is anchored at count 58.
|
|
double m = std::clamp(material, 10, 78) / 58.0;
|
|
|
|
// Return a = p_a(material) and b = p_b(material), see github.com/official-stockfish/WDL_model
|
|
constexpr double as[] = {-185.71965483, 504.85014385, -438.58295743, 474.04604627};
|
|
constexpr double bs[] = {89.23542728, -137.02141296, 73.28669021, 47.53376190};
|
|
|
|
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];
|
|
|
|
return {a, b};
|
|
}
|
|
|
|
// The win rate model is 1 / (1 + exp((a - eval) / b)), where a = p_a(material) and b = p_b(material).
|
|
// It fits the LTC fishtest statistics rather accurately.
|
|
int win_rate_model(Value v, const Position& pos) {
|
|
|
|
auto [a, b] = win_rate_params(pos);
|
|
|
|
// Return the win rate in per mille units, rounded to the nearest integer.
|
|
return int(0.5 + 1000 / (1 + std::exp((a - double(v)) / b)));
|
|
}
|
|
}
|
|
|
|
std::string UCIEngine::to_score(Value v, const Position& pos) {
|
|
assert(-VALUE_INFINITE < v && v < VALUE_INFINITE);
|
|
|
|
std::stringstream ss;
|
|
|
|
if (std::abs(v) < VALUE_TB_WIN_IN_MAX_PLY)
|
|
ss << "cp " << to_cp(v, pos);
|
|
else if (std::abs(v) <= VALUE_TB)
|
|
{
|
|
const int ply = VALUE_TB - std::abs(v); // recompute ss->ply
|
|
ss << "cp " << (v > 0 ? 20000 - ply : -20000 + ply);
|
|
}
|
|
else
|
|
ss << "mate " << (v > 0 ? VALUE_MATE - v + 1 : -VALUE_MATE - v) / 2;
|
|
|
|
return ss.str();
|
|
}
|
|
|
|
// Turns a Value to an integer centipawn number,
|
|
// without treatment of mate and similar special scores.
|
|
int UCIEngine::to_cp(Value v, const Position& pos) {
|
|
|
|
// In general, the score can be defined via the the WDL as
|
|
// (log(1/L - 1) - log(1/W - 1)) / ((log(1/L - 1) + log(1/W - 1))
|
|
// Based on our win_rate_model, this simply yields v / a.
|
|
|
|
auto [a, b] = win_rate_params(pos);
|
|
|
|
return std::round(100 * int(v) / a);
|
|
}
|
|
|
|
std::string UCIEngine::wdl(Value v, const Position& pos) {
|
|
std::stringstream ss;
|
|
|
|
int wdl_w = win_rate_model(v, pos);
|
|
int wdl_l = win_rate_model(-v, pos);
|
|
int wdl_d = 1000 - wdl_w - wdl_l;
|
|
ss << " wdl " << wdl_w << " " << wdl_d << " " << wdl_l;
|
|
|
|
return ss.str();
|
|
}
|
|
|
|
std::string UCIEngine::square(Square s) {
|
|
return std::string{char('a' + file_of(s)), char('1' + rank_of(s))};
|
|
}
|
|
|
|
std::string UCIEngine::move(Move m, bool chess960) {
|
|
if (m == Move::none())
|
|
return "(none)";
|
|
|
|
if (m == Move::null())
|
|
return "0000";
|
|
|
|
Square from = m.from_sq();
|
|
Square to = m.to_sq();
|
|
|
|
if (m.type_of() == CASTLING && !chess960)
|
|
to = make_square(to > from ? FILE_G : FILE_C, rank_of(from));
|
|
|
|
std::string move = square(from) + square(to);
|
|
|
|
if (m.type_of() == PROMOTION)
|
|
move += " pnbrqk"[m.promotion_type()];
|
|
|
|
return move;
|
|
}
|
|
|
|
|
|
Move UCIEngine::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 == move(m, pos.is_chess960()))
|
|
return m;
|
|
|
|
return Move::none();
|
|
}
|
|
|
|
} // namespace Stockfish
|