From b9f3273af12266b81c00993a360f5f544c345ce9 Mon Sep 17 00:00:00 2001 From: "Robert Nurnberg @ elitebook" Date: Thu, 2 Apr 2026 20:22:32 +0200 Subject: [PATCH] Align ponder move and final PV output In master for various reasons the given ponder move may get out of sync with (the second move of) the last PV sent to the GUI: 1. If a ponder move is extracted from the TT despite bestmove leading to a game ending draw. 2. If a ponder move is extracted from the TT from a fail high/low, since after the ponder move extraction no new PV is sent to the GUI. 3. If `syzygy_extend_pv()` from within the call to `pv()` changes the PV after the ponder move has been selected. 4. If the PV roll-back to protect mated-in scores differs in the second move from the final PV sent to the GUI. This PR keeps ponder move and the last printed PV in sync. Fixes #6676. closes https://github.com/official-stockfish/Stockfish/pull/6679 No functional change --- src/search.cpp | 53 ++++++++++++++++++++++++-------------------------- 1 file changed, 25 insertions(+), 28 deletions(-) diff --git a/src/search.cpp b/src/search.cpp index 2def21d02..d1aaab1ad 100644 --- a/src/search.cpp +++ b/src/search.cpp @@ -239,14 +239,18 @@ void Search::Worker::start_searching() { 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) + std::string ponder; + bool extractedPonder = false; + + if (bestThread->rootMoves[0].pv.size() == 1) + extractedPonder = bestThread->rootMoves[0].extract_ponder_from_tt(tt, rootPos); + + // Send again PV info if we have a new best thread or extracted a ponder move. + if (bestThread != this || extractedPonder) main_manager()->pv(*bestThread, threads, tt, bestThread->completedDepth); - std::string ponder; - - if (bestThread->rootMoves[0].pv.size() > 1 - || bestThread->rootMoves[0].extract_ponder_from_tt(tt, rootPos)) + // In rare cases, pv() may change the ponder move through syzygy_extend_pv(). + if (bestThread->rootMoves[0].pv.size() > 1) ponder = UCIEngine::move(bestThread->rootMoves[0].pv[1], rootPos.is_chess960()); auto bestmove = UCIEngine::move(bestThread->rootMoves[0].pv[0], rootPos.is_chess960()); @@ -262,9 +266,7 @@ void Search::Worker::iterative_deepening() { Move pv[MAX_PLY + 1]; - Depth lastBestMoveDepth = 0; - Value lastBestScore = -VALUE_INFINITE; - std::vector lastBestPV; + Depth lastBestMoveDepth = 0; Value alpha, beta; Value bestValue = -VALUE_INFINITE; @@ -428,8 +430,7 @@ void Search::Worker::iterative_deepening() { // PV that cannot be trusted, i.e. it can be delayed or refuted if we // would have had time to fully search other root-moves. Thus here we // suppress any exact mated-in/TB loss output and, if we do, below pick - // the score/PV from the previously completed iteration with the most - // recent bestmove change. + // the score/PV from the previous iteration. && !(threads.stop && is_loss(rootMoves[0].uciScore) && rootMoves[0].score == rootMoves[0].uciScore)) main_manager()->pv(*this, threads, tt, rootDepth); @@ -440,7 +441,11 @@ void Search::Worker::iterative_deepening() { if (!threads.stop) { - completedDepth = rootDepth; + completedDepth = rootDepth; + + if (lastIterationPV.empty() || rootMoves[0].pv[0] != lastIterationPV[0]) + lastBestMoveDepth = rootDepth; + lastIterationPV = rootMoves[0].pv; } @@ -451,14 +456,12 @@ void Search::Worker::iterative_deepening() { { // Bring the last best move to the front for best thread selection. // For an aborted d1 search we label the loss score as inexact. - if (!lastBestPV.empty()) + if (!lastIterationPV.empty()) { - Utility::move_to_front(rootMoves, - [&lastBestPV = std::as_const(lastBestPV)](const auto& rm) { - return rm == lastBestPV[0]; - }); - rootMoves[0].pv = lastBestPV; - rootMoves[0].score = rootMoves[0].uciScore = lastBestScore; + Utility::move_to_front(rootMoves, [&lastPV = std::as_const(lastIterationPV)]( + const auto& rm) { return rm == lastPV[0]; }); + rootMoves[0].pv = lastIterationPV; + rootMoves[0].score = rootMoves[0].uciScore = rootMoves[0].previousScore; } else { @@ -468,12 +471,6 @@ void Search::Worker::iterative_deepening() { main_manager()->pv(*this, threads, tt, rootDepth); } } - else if (lastBestPV.empty() || rootMoves[0].pv[0] != lastBestPV[0]) - { - lastBestPV = rootMoves[0].pv; - lastBestScore = rootMoves[0].score; - lastBestMoveDepth = rootDepth; - } // Have we found a "mate in x" after a completed iteration? if (limits.mate && !threads.stop @@ -2223,10 +2220,10 @@ bool RootMove::extract_ponder_from_tt(const TranspositionTable& tt, Position& po pos.do_move(pv[0], st, &tt); - auto [ttHit, ttData, ttWriter] = tt.probe(pos.key()); - if (ttHit) + if (!pos.is_draw(1)) { - if (MoveList(pos).contains(ttData.move)) + auto [ttHit, ttData, ttWriter] = tt.probe(pos.key()); + if (ttHit && MoveList(pos).contains(ttData.move)) pv.push_back(ttData.move); }