From cb95027b997dd1a39ab604eb55516edfc9ba53c2 Mon Sep 17 00:00:00 2001 From: Ciekce <44617491+Ciekce@users.noreply.github.com> Date: Sun, 19 Jul 2026 12:41:08 +0100 Subject: [PATCH 1/4] rootmoves --- src/search.cpp | 147 +++++++++++++++++++++++++++++++++++++++++-------- src/search.hpp | 47 +++++++++++++++- 2 files changed, 169 insertions(+), 25 deletions(-) diff --git a/src/search.cpp b/src/search.cpp index bb0994cf..1de28005 100644 --- a/src/search.cpp +++ b/src/search.cpp @@ -99,7 +99,7 @@ Value Searcher::wait_for_score() { // Protect the read of root_score with a unique_lock. std::unique_lock lock_guard{mutex}; // Return the final score from the main thread's search. - return m_workers[0]->get_thread_data().root_score; + return m_workers[0]->get_thread_data().root_score(); } void Searcher::initialize(size_t thread_count) { @@ -236,14 +236,23 @@ Move Worker::iterative_deepening(const Position& root_position) { constexpr usize SS_PADDING = 2; std::array ss; - Depth last_search_depth = 0; - Depth last_seldepth = 0; - Value last_search_score = -VALUE_INF; Value base_search_score = -VALUE_INF; - Move last_best_move = Move::none(); - PV last_pv{}; const auto print_info_line = [&] { + const auto& root_move = m_td.pv_move(); + + auto score = root_move.display_score; + + auto upperbound = root_move.upperbound; + auto lowerbound = root_move.lowerbound; + + if (root_move.score == -VALUE_INF) { + score = root_move.previous_score; + + upperbound = false; + lowerbound = false; + } + // Lambda to convert internal units score to uci score. TODO: add eval rescaling here once we get one auto format_score = [](Value score) { if (score < -VALUE_WIN && score > -VALUE_MATED) { @@ -258,27 +267,47 @@ Move Worker::iterative_deepening(const Position& root_position) { // Get current time auto curr_time = time::Clock::now(); - std::cout << std::dec << "info depth " << last_search_depth << " seldepth " << last_seldepth - << " score " << format_score(last_search_score) << " nodes " - << m_searcher.node_count() << " nps " + std::cout << std::dec << "info depth " << root_move.searched_depth << " seldepth " + << root_move.seldepth << " score " << format_score(score); + if (upperbound) { + std::cout << " upperbound"; + } + if (lowerbound) { + std::cout << " lowerbound"; + } + std::cout << " nodes " << m_searcher.node_count() << " nps " << time::nps(m_searcher.node_count(), curr_time - m_search_start); - if (last_search_depth >= 16) { + if (root_move.searched_depth >= 16) { std::cout << " hashfull " << m_searcher.tt.hashfull(); } std::cout << " time " << time::cast(curr_time - m_search_start).count() - << " pv " << last_pv << std::endl; + << " pv " << root_move.pv << std::endl; }; + init_root_moves(root_position); + + m_pv_idx = 0; + + m_pv_start = 0; + m_pv_end = m_td.root_moves.size(); + m_node_counts.fill(0); for (Depth search_depth = 1; search_depth < MAX_PLY; search_depth++) { // Call search - m_seldepth = 0; + m_root_depth = search_depth; + m_seldepth = 0; + + for (auto& root_move : m_td.root_moves) { + root_move.previous_score = root_move.score; + } + Value alpha = -VALUE_INF, beta = VALUE_INF; Value delta = tuned::asp_window_delta; if (search_depth >= 5) { - alpha = last_search_score - delta; - beta = last_search_score + delta; + const auto window_score = m_td.pv_move().window_score; + alpha = window_score - delta; + beta = window_score + delta; } Value score = -VALUE_INF; @@ -292,6 +321,13 @@ Move Worker::iterative_deepening(const Position& root_position) { score = search(root_position, &ss[SS_PADDING], alpha, beta, asp_window_depth, 0, false); + // Sort the PVs searched so far. Effectively, find the current best PV and move it to the front. + std::stable_sort(m_td.root_moves.begin() + static_cast(m_pv_idx), + m_td.root_moves.begin() + static_cast(m_pv_end), + [](const RootMove& a, const RootMove& b) { + return a.score > b.score; + }); + if (m_stopped) { break; } @@ -311,26 +347,29 @@ Move Worker::iterative_deepening(const Position& root_position) { delta += delta; } - // If m_stopped is true, then the search exited early. Discard the results for this depth. + + // Sort all PVs that have been searched, either partially or fully + std::stable_sort(m_td.root_moves.begin() + static_cast(m_pv_start), + m_td.root_moves.begin() + static_cast(m_pv_idx) + 1, + [](const RootMove& a, const RootMove& b) { + return a.score > b.score; + }); + + // If m_stopped is true, then the search exited early. + // It is safe to use results from this partial depth, as the previous depth's best move is always searched first. if (m_stopped) { break; } - // Store information only if the last iterative deepening search completed - last_search_depth = search_depth; - last_seldepth = m_seldepth; - last_search_score = score; - last_pv = ss[SS_PADDING].pv; - last_best_move = last_pv.first_move(); base_search_score = search_depth == 1 ? score : base_search_score; - m_td.root_score = last_search_score; - // Check depth limit if (IS_MAIN && search_depth >= m_search_limits.depth_limit) { break; } + const auto last_best_move = m_td.pv_move().pv.first_move(); + const auto total_nodes = std::reduce(std::begin(m_node_counts), std::end(m_node_counts), 0); const auto best_move_nodes = m_node_counts[last_best_move.from_to()]; const auto nodes_tm_fraction = @@ -371,7 +410,7 @@ Move Worker::iterative_deepening(const Position& root_position) { print_info_line(); } - return last_best_move; + return m_td.pv_move().pv.first_move(); } template @@ -827,6 +866,35 @@ Value Worker::search( return 0; } + if (ROOT_NODE) { + auto& root_move = find_root_move(m); + + root_move.window_score = value; + + if (moves_played == 1 || value > alpha) { + root_move.searched_depth = m_root_depth; + root_move.seldepth = m_seldepth; + + root_move.score = value; + root_move.display_score = value; + + root_move.upperbound = false; + root_move.lowerbound = false; + + if (value <= alpha) { + root_move.display_score = alpha; + root_move.upperbound = true; + } else if (value >= beta) { + root_move.display_score = beta; + root_move.lowerbound = true; + } + + root_move.pv.set(m, (ss + 1)->pv); + } else { + root_move.score = -VALUE_INF; + } + } + if (value > best_value) { best_value = value; @@ -1076,5 +1144,36 @@ Value Worker::adj_shuffle(const Position& pos, Value value) { return value; } + +void Worker::init_root_moves(const Position& root_position) { + m_td.root_moves.clear(); + m_td.root_moves.reserve(256); + + MoveGen movegen{root_position}; + + MoveList noisy{}; + MoveList quiet{}; + + movegen.generate_moves(noisy, quiet); + + const auto insert_root_moves = [&](const MoveList& moves) { + for (const auto move : moves) { + m_td.root_moves.emplace_back(move); + } + }; + + insert_root_moves(noisy); + insert_root_moves(quiet); +} + +RootMove& Worker::find_root_move(Move move) { + for (auto& root_move : m_td.root_moves) { + if (root_move.pv.first_move() == move) { + return root_move; + } + } + + assert(false); +} } // namespace Search } // namespace Clockwork diff --git a/src/search.hpp b/src/search.hpp index e699c83a..310ca1a6 100644 --- a/src/search.hpp +++ b/src/search.hpp @@ -45,6 +45,11 @@ struct PV { m_pv.clear(); } + void set(Move move) { + m_pv.clear(); + m_pv.push_back(move); + } + void set(Move move, const PV& child_pv_line) { m_pv.clear(); m_pv.push_back(move); @@ -78,10 +83,30 @@ struct SearchLimits { Depth depth_limit; }; +struct RootMove { + explicit RootMove(Move move) { + pv.set(move); + } + + Value score = -VALUE_INF; + Value window_score = -VALUE_INF; + Value previous_score = -VALUE_INF; + Value display_score = -VALUE_INF; + + bool upperbound = false; + bool lowerbound = false; + + PV pv; + + Depth searched_depth = 1; + Depth seldepth = 0; +}; + struct ThreadData { History history; std::vector psqt_states; - Value root_score; + + std::vector root_moves; PsqtState& push_psqt_state() { psqt_states.push_back(psqt_states.back()); @@ -91,6 +116,18 @@ struct ThreadData { void pop_psqt_state() { psqt_states.pop_back(); } + + RootMove& pv_move() { + return root_moves[0]; + } + + const RootMove& pv_move() const { + return root_moves[0]; + } + + Value root_score() const { + return pv_move().score; + } }; class Searcher { @@ -179,9 +216,13 @@ class alignas(128) Worker { ThreadType m_thread_type; SearchLimits m_search_limits; ThreadData m_td; + usize m_pv_idx; + usize m_pv_start; + usize m_pv_end; std::atomic m_stopped; std::atomic m_exiting; std::array m_node_counts; + Depth m_root_depth; Depth m_seldepth; bool m_in_nmp_verification = false; @@ -195,6 +236,10 @@ class alignas(128) Worker { Value evaluate(const Position& pos); Value adj_shuffle(const Position& pos, Value value); bool check_tm_hard_limit(); + + void init_root_moves(const Position& root_position); + + RootMove& find_root_move(Move move); }; } // namespace Search From 351cacd32ceb67a9724023f9457c897c0d174904 Mon Sep 17 00:00:00 2001 From: Ciekce <44617491+Ciekce@users.noreply.github.com> Date: Sun, 19 Jul 2026 15:05:36 +0100 Subject: [PATCH 2/4] multipv! Bench: 12898941 --- src/search.cpp | 214 +++++++++++++++++++++++++++---------------------- src/search.hpp | 26 +++++- src/uci.cpp | 14 +++- src/uci.hpp | 5 +- 4 files changed, 156 insertions(+), 103 deletions(-) diff --git a/src/search.cpp b/src/search.cpp index 1de28005..6c8fa6a9 100644 --- a/src/search.cpp +++ b/src/search.cpp @@ -238,56 +238,8 @@ Move Worker::iterative_deepening(const Position& root_position) { Value base_search_score = -VALUE_INF; - const auto print_info_line = [&] { - const auto& root_move = m_td.pv_move(); - - auto score = root_move.display_score; - - auto upperbound = root_move.upperbound; - auto lowerbound = root_move.lowerbound; - - if (root_move.score == -VALUE_INF) { - score = root_move.previous_score; - - upperbound = false; - lowerbound = false; - } - - // Lambda to convert internal units score to uci score. TODO: add eval rescaling here once we get one - auto format_score = [](Value score) { - if (score < -VALUE_WIN && score > -VALUE_MATED) { - return "mate " + std::to_string(-(VALUE_MATED + score + 1) / 2); - } - if (score > VALUE_WIN && score < VALUE_MATED) { - return "mate " + std::to_string((VALUE_MATED + 1 - score) / 2); - } - return "cp " + std::to_string(score / 4); - }; - - // Get current time - auto curr_time = time::Clock::now(); - - std::cout << std::dec << "info depth " << root_move.searched_depth << " seldepth " - << root_move.seldepth << " score " << format_score(score); - if (upperbound) { - std::cout << " upperbound"; - } - if (lowerbound) { - std::cout << " lowerbound"; - } - std::cout << " nodes " << m_searcher.node_count() << " nps " - << time::nps(m_searcher.node_count(), curr_time - m_search_start); - if (root_move.searched_depth >= 16) { - std::cout << " hashfull " << m_searcher.tt.hashfull(); - } - std::cout << " time " << time::cast(curr_time - m_search_start).count() - << " pv " << root_move.pv << std::endl; - }; - init_root_moves(root_position); - m_pv_idx = 0; - m_pv_start = 0; m_pv_end = m_td.root_moves.size(); @@ -296,34 +248,65 @@ Move Worker::iterative_deepening(const Position& root_position) { for (Depth search_depth = 1; search_depth < MAX_PLY; search_depth++) { // Call search m_root_depth = search_depth; - m_seldepth = 0; for (auto& root_move : m_td.root_moves) { root_move.previous_score = root_move.score; } - Value alpha = -VALUE_INF, beta = VALUE_INF; - Value delta = tuned::asp_window_delta; - if (search_depth >= 5) { - const auto window_score = m_td.pv_move().window_score; - alpha = window_score - delta; - beta = window_score + delta; - } - Value score = -VALUE_INF; + for (m_pv_idx = 0; m_pv_idx < m_multipv; ++m_pv_idx) { + m_seldepth = 0; + + const auto& root_move = m_td.root_moves[m_pv_idx]; + + Value alpha = -VALUE_INF, beta = VALUE_INF; + Value delta = tuned::asp_window_delta; + if (search_depth >= 5) { + alpha = root_move.window_score - delta; + beta = root_move.window_score + delta; + } + + Value score = -VALUE_INF; + + int fail_high_reduction = 0; + + while (true) { + int asp_window_depth = search_depth - fail_high_reduction; - int fail_high_reduction = 0; + alpha = std::max(-VALUE_INF, alpha); + beta = std::min(VALUE_INF, beta); + score = search(root_position, &ss[SS_PADDING], alpha, beta, + asp_window_depth, 0, false); - while (true) { - int asp_window_depth = search_depth - fail_high_reduction; + // Sort the PVs searched so far. Effectively, find the current best PV and move it to the front. + std::stable_sort(m_td.root_moves.begin() + static_cast(m_pv_idx), + m_td.root_moves.begin() + static_cast(m_pv_end), + [](const RootMove& a, const RootMove& b) { + return a.score > b.score; + }); + + if (m_stopped) { + break; + } + + if (score <= alpha) { + beta = (alpha + beta) / 2; + alpha = score - delta; + fail_high_reduction = 0; + } else if (score >= beta) { + beta = score + delta; + if (fail_high_reduction < 3) { + ++fail_high_reduction; + } + } else { + break; + } - alpha = std::max(-VALUE_INF, alpha); - beta = std::min(VALUE_INF, beta); - score = search(root_position, &ss[SS_PADDING], alpha, beta, - asp_window_depth, 0, false); + delta += delta; + } - // Sort the PVs searched so far. Effectively, find the current best PV and move it to the front. - std::stable_sort(m_td.root_moves.begin() + static_cast(m_pv_idx), - m_td.root_moves.begin() + static_cast(m_pv_end), + // Sort all PVs that have been searched, either partially or fully + std::stable_sort(m_td.root_moves.begin() + static_cast(m_pv_start), + m_td.root_moves.begin() + static_cast(m_pv_idx) + 1, [](const RootMove& a, const RootMove& b) { return a.score > b.score; }); @@ -331,36 +314,17 @@ Move Worker::iterative_deepening(const Position& root_position) { if (m_stopped) { break; } - - if (score <= alpha) { - beta = (alpha + beta) / 2; - alpha = score - delta; - fail_high_reduction = 0; - } else if (score >= beta) { - beta = score + delta; - if (fail_high_reduction < 3) { - ++fail_high_reduction; - } - } else { - break; - } - - delta += delta; } - // Sort all PVs that have been searched, either partially or fully - std::stable_sort(m_td.root_moves.begin() + static_cast(m_pv_start), - m_td.root_moves.begin() + static_cast(m_pv_idx) + 1, - [](const RootMove& a, const RootMove& b) { - return a.score > b.score; - }); - // If m_stopped is true, then the search exited early. // It is safe to use results from this partial depth, as the previous depth's best move is always searched first. if (m_stopped) { break; } + const auto& pv_move = m_td.pv_move(); + const auto score = pv_move.score; + base_search_score = search_depth == 1 ? score : base_search_score; // Check depth limit @@ -368,7 +332,7 @@ Move Worker::iterative_deepening(const Position& root_position) { break; } - const auto last_best_move = m_td.pv_move().pv.first_move(); + const auto last_best_move = pv_move.pv.first_move(); const auto total_nodes = std::reduce(std::begin(m_node_counts), std::end(m_node_counts), 0); const auto best_move_nodes = m_node_counts[last_best_move.from_to()]; @@ -400,14 +364,14 @@ Move Worker::iterative_deepening(const Position& root_position) { } if (IS_MAIN && !m_searcher.settings.silent) { - print_info_line(); + print_info_lines(); } } // Print last info line // This ensures we output our last value of search_nodes before termination, allowing for accurate search reproduction. if (IS_MAIN && !m_searcher.settings.silent) { - print_info_line(); + print_info_lines(); } return m_td.pv_move().pv.first_move(); @@ -641,6 +605,10 @@ Value Worker::search( // Iterate over the move list for (Move m = moves.next(); m != Move::none(); m = moves.next()) { + if (ROOT_NODE && !is_legal_root_move(m)) { + continue; + } + if (m == ss->excluded_move) { continue; } @@ -867,7 +835,7 @@ Value Worker::search( } if (ROOT_NODE) { - auto& root_move = find_root_move(m); + auto& root_move = get_root_move(m); root_move.window_score = value; @@ -1164,16 +1132,66 @@ void Worker::init_root_moves(const Position& root_position) { insert_root_moves(noisy); insert_root_moves(quiet); + + m_multipv = std::min(m_searcher.settings.multipv, m_td.root_moves.size()); } -RootMove& Worker::find_root_move(Move move) { - for (auto& root_move : m_td.root_moves) { - if (root_move.pv.first_move() == move) { - return root_move; +void Worker::print_info_line(usize pv_idx) { + const auto& root_move = m_td.root_moves[pv_idx]; + + auto score = root_move.display_score; + + auto upperbound = root_move.upperbound; + auto lowerbound = root_move.lowerbound; + + if (root_move.score == -VALUE_INF) { + score = root_move.previous_score; + + upperbound = false; + lowerbound = false; + } + + // Lambda to convert internal units score to uci score. TODO: add eval rescaling here once we get one + auto format_score = [](Value score) { + if (score < -VALUE_WIN && score > -VALUE_MATED) { + return "mate " + std::to_string(-(VALUE_MATED + score + 1) / 2); } + if (score > VALUE_WIN && score < VALUE_MATED) { + return "mate " + std::to_string((VALUE_MATED + 1 - score) / 2); + } + return "cp " + std::to_string(score / 4); + }; + + // Get current time + auto curr_time = time::Clock::now(); + + std::cout << std::dec << "info"; + + if (m_searcher.settings.multipv > 1) { + std::cout << " multipv " << (pv_idx + 1); } - assert(false); + std::cout << " depth " << root_move.searched_depth << " seldepth " << root_move.seldepth + << " score " << format_score(score); + if (upperbound) { + std::cout << " upperbound"; + } + if (lowerbound) { + std::cout << " lowerbound"; + } + std::cout << " nodes " << m_searcher.node_count() << " nps " + << time::nps(m_searcher.node_count(), curr_time - m_search_start); + if (root_move.searched_depth >= 16) { + std::cout << " hashfull " << m_searcher.tt.hashfull(); + } + std::cout << " time " << time::cast(curr_time - m_search_start).count() + << " pv " << root_move.pv << std::endl; +} + +void Worker::print_info_lines() { + for (usize pv_idx = 0; pv_idx < m_multipv; ++pv_idx) { + print_info_line(pv_idx); + } } } // namespace Search } // namespace Clockwork diff --git a/src/search.hpp b/src/search.hpp index 310ca1a6..18c10728 100644 --- a/src/search.hpp +++ b/src/search.hpp @@ -27,6 +27,7 @@ struct SearchSettings { i64 move_time = -1; u64 hard_nodes = 0; u64 soft_nodes = 0; + usize multipv = 1; bool silent = false; bool datagen = false; }; @@ -215,6 +216,7 @@ class alignas(128) Worker { std::thread m_thread; ThreadType m_thread_type; SearchLimits m_search_limits; + usize m_multipv; ThreadData m_td; usize m_pv_idx; usize m_pv_start; @@ -239,7 +241,29 @@ class alignas(128) Worker { void init_root_moves(const Position& root_position); - RootMove& find_root_move(Move move); + void print_info_lines(); + void print_info_line(usize pv_idx); + + RootMove& get_root_move(Move move) { + for (auto& root_move : m_td.root_moves) { + if (root_move.pv.first_move() == move) { + return root_move; + } + } + + assert(false); + } + + bool is_legal_root_move(Move move) const { + for (usize i = m_pv_idx; i < m_td.root_moves.size(); ++i) { + const auto& root_move = m_td.root_moves[i]; + if (root_move.pv.first_move() == move) { + return true; + } + } + + return false; + } }; } // namespace Search diff --git a/src/uci.cpp b/src/uci.cpp index b4f5b774..466aa747 100644 --- a/src/uci.cpp +++ b/src/uci.cpp @@ -28,6 +28,7 @@ namespace Clockwork::UCI { constexpr std::string_view STARTPOS{"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"}; constexpr usize MAX_HASH = 268435456; constexpr usize MAX_THREADS = 1024; +constexpr usize MAX_MULTIPV = 256; UCIHandler::UCIHandler() : m_position(*Position::parse(STARTPOS)) { @@ -63,6 +64,7 @@ void UCIHandler::execute_command(const std::string& line) { std::cout << "option name UseSoftNodes type check default false\n"; std::cout << "option name Threads type spin default 1 min 1 max " << MAX_THREADS << "\n"; std::cout << "option name Hash type spin default 16 min 1 max " << MAX_HASH << "\n"; + std::cout << "option name MultiPV type spin default 1 min 1 max " << MAX_MULTIPV << "\n"; tuned::uci_print_tunable_options(); std::cout << "uciok" << std::endl; } else if (command == "ucinewgame") { @@ -129,7 +131,8 @@ void UCIHandler::handle_debug(std::istringstream&) { void UCIHandler::handle_go(std::istringstream& is) { // Clear any previous settings - settings = {}; + settings = {}; + settings.multipv = m_multipv; std::string token; while (is >> token) { if (token == "depth") { @@ -262,12 +265,19 @@ void UCIHandler::handle_setoption(std::istringstream& is) { } } else if (name == "Threads") { if (auto value = parse_number(value_str)) { - size_t thread_count = std::clamp(*value, 1, MAX_THREADS); + usize thread_count = std::clamp(*value, 1, MAX_THREADS); searcher.initialize(thread_count); searcher.set_position(m_position, m_repetition_info); } else { std::cout << "Invalid value " << value_str << std::endl; } + } else if (name == "MultiPV") { + if (auto value = parse_number(value_str)) { + usize multipv = std::clamp(*value, 1, MAX_MULTIPV); + m_multipv = multipv; + } else { + std::cout << "Invalid value " << value_str << std::endl; + } } else if (name == "UseSoftNodes") { if (value_str == "true") { m_use_soft_nodes = true; diff --git a/src/uci.hpp b/src/uci.hpp index 9c9a9bcb..3cd75992 100644 --- a/src/uci.hpp +++ b/src/uci.hpp @@ -22,8 +22,9 @@ class UCIHandler { Position m_position; RepetitionInfo m_repetition_info; // move this somewhere else later - TT m_tt; - bool m_use_soft_nodes = false; + TT m_tt; + bool m_use_soft_nodes = false; + usize m_multipv = 1; Search::Searcher searcher; From fa3529b21929f127a237a33e86e43e63cf2c099e Mon Sep 17 00:00:00 2001 From: Ciekce <44617491+Ciekce@users.noreply.github.com> Date: Sun, 19 Jul 2026 15:09:54 +0100 Subject: [PATCH 3/4] actually handle this I suppose Bench: 12898941 --- src/search.hpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/search.hpp b/src/search.hpp index 18c10728..1d5fe634 100644 --- a/src/search.hpp +++ b/src/search.hpp @@ -251,7 +251,8 @@ class alignas(128) Worker { } } - assert(false); + assert(false && "Failed to find root move"); + std::terminate(); } bool is_legal_root_move(Move move) const { From 8685907875b40fd4fc9f432e1486ea67158a99cd Mon Sep 17 00:00:00 2001 From: Ciekce <44617491+Ciekce@users.noreply.github.com> Date: Sun, 19 Jul 2026 15:49:57 +0100 Subject: [PATCH 4/4] fix Bench: 12898941 --- src/search.cpp | 41 ++++++++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/src/search.cpp b/src/search.cpp index 6c8fa6a9..75cee3df 100644 --- a/src/search.cpp +++ b/src/search.cpp @@ -461,6 +461,11 @@ Value Worker::search( ttpv |= tt_data->ttpv(); } + // Ensure the correct move is searched first if pv_idx > 0. + const auto tt_move = ROOT_NODE && m_root_depth > 1 ? m_td.root_moves[m_pv_idx].pv.first_move() + : tt_data ? tt_data->move + : Move::none(); + bool is_in_check = pos.is_in_check(); bool improving = false; Value correction = 0; @@ -479,8 +484,7 @@ Value Worker::search( } // Internal Iterative Reductions - if ((PV_NODE || cutnode) && depth >= 8 && !excluded - && (!tt_data || tt_data->move == Move::none())) { + if ((PV_NODE || cutnode) && depth >= 8 && !excluded && (!tt_data || tt_move == Move::none())) { depth--; } @@ -558,8 +562,7 @@ Value Worker::search( const Depth probcut_depth = std::clamp(depth - 4, 1, depth - 1); if (!tt_data || tt_data->depth + 3 < depth || tt_data->score >= probcut_beta) { - MovePicker moves{pos, m_td.history, tt_data ? tt_data->move : Move::none(), - tuned::probcut_see}; + MovePicker moves{pos, m_td.history, tt_move, tuned::probcut_see}; for (Move m = moves.next(); m != Move::none(); m = moves.next()) { @@ -589,7 +592,7 @@ Value Worker::search( } } - MovePicker moves{pos, m_td.history, tt_data ? tt_data->move : Move::none(), ply, ss}; + MovePicker moves{pos, m_td.history, tt_move, ply, ss}; Move best_move = Move::none(); Value best_value = -VALUE_INF; i32 moves_played = 0; @@ -648,9 +651,9 @@ Value Worker::search( // Singular extensions int extension = 0; - if (!ROOT_NODE && tt_data && m == tt_data->move && !excluded - && depth >= tuned::sing_min_depth && is_valid_score(tt_data->score) - && !is_mate_score(tt_data->score) && tt_data->depth >= depth - tuned::sing_depth_margin + if (!ROOT_NODE && tt_data && m == tt_move && !excluded && depth >= tuned::sing_min_depth + && is_valid_score(tt_data->score) && !is_mate_score(tt_data->score) + && tt_data->depth >= depth - tuned::sing_depth_margin && tt_data->bound() != Bound::Upper) { Value singular_beta = tt_data->score - depth * tuned::sing_beta_margin / 64; int singular_depth = depth / 2; @@ -755,7 +758,7 @@ Value Worker::search( if (cutnode) { reduction += tuned::lmr_cutnode_red; // If there is no available tt move, increase reduction - if (!tt_data || tt_data->move == Move::none()) { + if (!tt_data || tt_move == Move::none()) { reduction += tuned::lmr_no_tt_red; } } @@ -768,7 +771,7 @@ Value Worker::search( reduction += tuned::lmr_ttpv_fail_low; } - if (tt_data && tt_data->move.is_capture() && !m.is_capture()) { + if (tt_data && tt_move.is_capture() && !m.is_capture()) { reduction += tuned::lmr_tt_capture_red; } @@ -923,13 +926,17 @@ Value Worker::search( } if (!excluded) { - Bound bound = best_value >= beta ? Bound::Lower - : best_move != Move::none() ? Bound::Exact - : Bound::Upper; - Move tt_move = best_move != Move::none() ? best_move - : tt_data ? tt_data->move - : Move::none(); - m_searcher.tt.store(pos, ply, raw_eval, tt_move, best_value, depth, ttpv, bound); + Bound bound = best_value >= beta ? Bound::Lower + : best_move != Move::none() ? Bound::Exact + : Bound::Upper; + + // Don't overwrite PV 0's tt entry (the actual best move) in a search where it was excluded. + if (!ROOT_NODE || m_pv_idx == 0) { + Move new_tt_move = best_move != Move::none() ? best_move + : tt_data ? tt_data->move + : Move::none(); + m_searcher.tt.store(pos, ply, raw_eval, new_tt_move, best_value, depth, ttpv, bound); + } // Update to correction history. if (!is_in_check