X-Git-Url: https://git.sesse.net/?p=stockfish;a=blobdiff_plain;f=src%2Fsearch.cpp;h=cf380b3135b190b4c808eafda1ec23934cf08a47;hp=3c638ca057f5e1c3c515498e005cecf88a111c8e;hb=0a74c16ffe5474f126cfac71e3f85394771ebbbd;hpb=66c5eaebd8c3ecc00a21a9c7f214cb59db4ce4e5 diff --git a/src/search.cpp b/src/search.cpp index 3c638ca0..cf380b31 100644 --- a/src/search.cpp +++ b/src/search.cpp @@ -75,9 +75,6 @@ namespace { int FutilityMoveCounts[2][16]; // [improving][depth] int Reductions[2][2][64][64]; // [pv][improving][depth][moveNumber] - // Threshold used for countermoves based pruning - const int CounterMovePruneThreshold = 0; - template Depth reduction(bool i, Depth d, int mn) { return Reductions[PvNode][i][std::min(d / ONE_PLY, 63)][std::min(mn, 63)] * ONE_PLY; } @@ -90,56 +87,15 @@ namespace { // Skill structure is used to implement strength limit struct Skill { - Skill(int l) : level(l) {} + explicit Skill(int l) : level(l) {} bool enabled() const { return level < 20; } bool time_to_pick(Depth depth) const { return depth / ONE_PLY == 1 + level; } - Move best_move(size_t multiPV) { return best ? best : pick_best(multiPV); } Move pick_best(size_t multiPV); int level; Move best = MOVE_NONE; }; - // EasyMoveManager structure is used to detect an 'easy move'. When the PV is stable - // across multiple search iterations, we can quickly return the best move. - struct EasyMoveManager { - - void clear() { - stableCnt = 0; - expectedPosKey = 0; - pv[0] = pv[1] = pv[2] = MOVE_NONE; - } - - Move get(Key key) const { - return expectedPosKey == key ? pv[2] : MOVE_NONE; - } - - void update(Position& pos, const std::vector& newPv) { - - assert(newPv.size() >= 3); - - // Keep track of how many times in a row the 3rd ply remains stable - stableCnt = (newPv[2] == pv[2]) ? stableCnt + 1 : 0; - - if (!std::equal(newPv.begin(), newPv.begin() + 3, pv)) - { - std::copy(newPv.begin(), newPv.begin() + 3, pv); - - StateInfo st[2]; - pos.do_move(newPv[0], st[0]); - pos.do_move(newPv[1], st[1]); - expectedPosKey = pos.key(); - pos.undo_move(newPv[1]); - pos.undo_move(newPv[0]); - } - } - - int stableCnt; - Key expectedPosKey; - Move pv[3]; - }; - - EasyMoveManager EasyMove; Value DrawValue[COLOR_NB]; template @@ -153,6 +109,34 @@ namespace { void update_pv(Move* pv, Move move, Move* childPv); void update_continuation_histories(Stack* ss, Piece pc, Square to, int bonus); void update_stats(const Position& pos, Stack* ss, Move move, Move* quiets, int quietsCnt, int bonus); + void update_capture_stats(const Position& pos, Move move, Move* captures, int captureCnt, int bonus); + bool pv_is_draw(Position& pos); + + // perft() is our utility to verify move generation. All the leaf nodes up + // to the given depth are generated and counted, and the sum is returned. + template + uint64_t perft(Position& pos, Depth depth) { + + StateInfo st; + uint64_t cnt, nodes = 0; + const bool leaf = (depth == 2 * ONE_PLY); + + for (const auto& m : MoveList(pos)) + { + if (Root && depth <= ONE_PLY) + cnt = 1, nodes++; + else + { + pos.do_move(m, st); + cnt = leaf ? MoveList(pos).size() : perft(pos, depth - ONE_PLY); + nodes += cnt; + pos.undo_move(m); + } + if (Root) + sync_cout << UCI::move(m, pos.is_chess960()) << ": " << cnt << sync_endl; + } + return nodes; + } } // namespace @@ -193,56 +177,26 @@ void Search::clear() { TT.clear(); for (Thread* th : Threads) - { - th->counterMoves.fill(MOVE_NONE); - th->mainHistory.fill(0); - - for (auto& to : th->contHistory) - for (auto& h : to) - h.fill(0); - - th->contHistory[NO_PIECE][0].fill(CounterMovePruneThreshold - 1); - } + th->clear(); Threads.main()->callsCnt = 0; Threads.main()->previousScore = VALUE_INFINITE; + Threads.main()->previousTimeReduction = 1; } -/// Search::perft() is our utility to verify move generation. All the leaf nodes -/// up to the given depth are generated and counted, and the sum is returned. -template -uint64_t Search::perft(Position& pos, Depth depth) { - - StateInfo st; - uint64_t cnt, nodes = 0; - const bool leaf = (depth == 2 * ONE_PLY); - - for (const auto& m : MoveList(pos)) - { - if (Root && depth <= ONE_PLY) - cnt = 1, nodes++; - else - { - pos.do_move(m, st); - cnt = leaf ? MoveList(pos).size() : perft(pos, depth - ONE_PLY); - nodes += cnt; - pos.undo_move(m); - } - if (Root) - sync_cout << UCI::move(m, pos.is_chess960()) << ": " << cnt << sync_endl; - } - return nodes; -} - -template uint64_t Search::perft(Position&, Depth); - - /// MainThread::search() is called by the main thread when the program receives /// the UCI 'go' command. It searches from the root position and outputs the "bestmove". void MainThread::search() { + if (Limits.perft) + { + nodes = perft(rootPos, Limits.perft * ONE_PLY); + 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(); @@ -253,7 +207,7 @@ void MainThread::search() { if (rootMoves.empty()) { - rootMoves.push_back(RootMove(MOVE_NONE)); + rootMoves.emplace_back(MOVE_NONE); sync_cout << "info depth 0 score " << UCI::value(rootPos.checkers() ? -VALUE_MATE : VALUE_DRAW) << sync_endl; @@ -267,23 +221,18 @@ void MainThread::search() { Thread::search(); // Let's start searching! } - // 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(); - // 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, // 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 (which also raises Threads.stop). - if (!Threads.stop && (Threads.ponder || Limits.infinite)) - { - Threads.stopOnPonderhit = true; - wait(Threads.stop); - } + Threads.stopOnPonderhit = true; - // Stop the threads if not already stopped + while (!Threads.stop && (Threads.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; // Wait until all threads have finished @@ -291,10 +240,14 @@ void MainThread::search() { if (th != this) th->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(); + // Check if there are threads with a better score than main thread Thread* bestThread = this; - if ( !this->easyMovePlayed - && Options["MultiPV"] == 1 + if ( Options["MultiPV"] == 1 && !Limits.depth && !Skill(Options["Skill Level"]).enabled() && rootMoves[0].pv[0] != MOVE_NONE) @@ -304,7 +257,9 @@ void MainThread::search() { Depth depthDiff = th->completedDepth - bestThread->completedDepth; Value scoreDiff = th->rootMoves[0].score - bestThread->rootMoves[0].score; - if (scoreDiff > 0 && depthDiff >= 0) + // Select the thread with the best score, always if it is a mate + if ( scoreDiff > 0 + && (depthDiff >= 0 || th->rootMoves[0].score >= VALUE_MATE_IN_MAX_PLY)) bestThread = th; } } @@ -330,10 +285,12 @@ void MainThread::search() { void Thread::search() { - Stack stack[MAX_PLY+7], *ss = stack+4; // To allow referencing (ss-4) and (ss+2) + Stack stack[MAX_PLY+7], *ss = stack+4; // To reference from (ss-4) to (ss+2) Value bestValue, alpha, beta, delta; - Move easyMove = MOVE_NONE; + Move lastBestMove = MOVE_NONE; + Depth lastBestMoveDepth = DEPTH_ZERO; MainThread* mainThread = (this == Threads.main() ? Threads.main() : nullptr); + double timeReduction = 1.0; std::memset(ss-4, 0, 7 * sizeof(Stack)); for (int i = 4; i > 0; i--) @@ -341,13 +298,10 @@ void Thread::search() { bestValue = delta = alpha = -VALUE_INFINITE; beta = VALUE_INFINITE; - completedDepth = DEPTH_ZERO; if (mainThread) { - easyMove = EasyMove.get(rootPos.key()); - EasyMove.clear(); - mainThread->easyMovePlayed = mainThread->failedLow = false; + mainThread->failedLow = false; mainThread->bestMoveChanges = 0; } @@ -452,22 +406,18 @@ void Thread::search() { // Sort the PV lines searched so far and update the GUI std::stable_sort(rootMoves.begin(), rootMoves.begin() + PVIdx + 1); - if (!mainThread) - continue; - - if (Threads.stop || PVIdx + 1 == multiPV || Time.elapsed() > 3000) + if ( mainThread + && (Threads.stop || PVIdx + 1 == multiPV || Time.elapsed() > 3000)) sync_cout << UCI::pv(rootPos, rootDepth, alpha, beta) << sync_endl; } if (!Threads.stop) completedDepth = rootDepth; - if (!mainThread) - continue; - - // If 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); + if (rootMoves[0].pv[0] != lastBestMove) { + lastBestMove = rootMoves[0].pv[0]; + lastBestMoveDepth = rootDepth; + } // Have we found a "mate in x"? if ( Limits.mate @@ -475,27 +425,42 @@ void Thread::search() { && VALUE_MATE - bestValue <= 2 * Limits.mate) Threads.stop = true; + if (!mainThread) + continue; + + // If 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); + // Do we have time for the next iteration? Can we stop searching now? if (Limits.use_time_management()) { if (!Threads.stop && !Threads.stopOnPonderhit) { // Stop the search if only one legal move is available, or if all - // of the available time has been used, or if we matched an easyMove - // from the previous search and just did a fast verification. + // of the available time has been used const int F[] = { mainThread->failedLow, bestValue - mainThread->previousScore }; - int improvingFactor = std::max(229, std::min(715, 357 + 119 * F[0] - 6 * F[1])); - double unstablePvFactor = 1 + mainThread->bestMoveChanges; - bool doEasyMove = rootMoves[0].pv[0] == easyMove - && mainThread->bestMoveChanges < 0.03 - && Time.elapsed() > Time.optimum() * 5 / 44; + Color us = rootPos.side_to_move(); + bool thinkHard = DrawValue[us] == bestValue + && Limits.time[us] - Time.elapsed() > Limits.time[~us] + && ::pv_is_draw(rootPos); + + double unstablePvFactor = 1 + mainThread->bestMoveChanges + thinkHard; + + // if the bestMove is stable over several iterations, reduce time for this move, + // the longer the move has been stable, the more. + // Use part of the gained time from a previous stable move for the current move. + timeReduction = 1; + for (int i : {3, 4, 5}) + if (lastBestMoveDepth * i < completedDepth && !thinkHard) + timeReduction *= 1.3; + unstablePvFactor *= std::pow(mainThread->previousTimeReduction, 0.51) / timeReduction; if ( rootMoves.size() == 1 - || Time.elapsed() > Time.optimum() * unstablePvFactor * improvingFactor / 628 - || (mainThread->easyMovePlayed = doEasyMove, doEasyMove)) + || Time.elapsed() > Time.optimum() * unstablePvFactor * improvingFactor / 628) { // If we are allowed to ponder do not stop the search now but // keep pondering until the GUI sends "ponderhit" or "stop". @@ -505,26 +470,18 @@ void Thread::search() { Threads.stop = true; } } - - if (rootMoves[0].pv.size() >= 3) - EasyMove.update(rootPos, rootMoves[0].pv); - else - EasyMove.clear(); } } if (!mainThread) return; - // Clear any candidate easy move that wasn't stable for the last search - // iterations; the second condition prevents consecutive fast moves. - if (EasyMove.stableCnt < 6 || mainThread->easyMovePlayed) - EasyMove.clear(); + mainThread->previousTimeReduction = timeReduction; // If skill level is enabled, swap best PV line with the sub-optimal one if (skill.enabled()) - std::swap(rootMoves[0], *std::find(rootMoves.begin(), - rootMoves.end(), skill.best_move(multiPV))); + std::swap(rootMoves[0], *std::find(rootMoves.begin(), rootMoves.end(), + skill.best ? skill.best : skill.pick_best(multiPV))); } @@ -536,7 +493,7 @@ namespace { Value search(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth, bool cutNode, bool skipEarlyPruning) { const bool PvNode = NT == PV; - const bool rootNode = PvNode && (ss-1)->ply == 0; + const bool rootNode = PvNode && ss->ply == 0; assert(-VALUE_INFINITE <= alpha && alpha < beta && beta <= VALUE_INFINITE); assert(PvNode || (alpha == beta - 1)); @@ -544,7 +501,7 @@ namespace { assert(!(PvNode && cutNode)); assert(depth / ONE_PLY * ONE_PLY == depth); - Move pv[MAX_PLY+1], quietsSearched[64]; + Move pv[MAX_PLY+1], capturesSearched[32], quietsSearched[64]; StateInfo st; TTEntry* tte; Key posKey; @@ -552,25 +509,24 @@ namespace { Depth extension, newDepth; Value bestValue, value, ttValue, eval; bool ttHit, inCheck, givesCheck, singularExtensionNode, improving; - bool captureOrPromotion, doFullDepthSearch, moveCountPruning, skipQuiets, ttCapture; + bool captureOrPromotion, doFullDepthSearch, moveCountPruning, skipQuiets, ttCapture, pvExact; Piece movedPiece; - int moveCount, quietCount; + int moveCount, captureCount, quietCount; // Step 1. Initialize node Thread* thisThread = pos.this_thread(); inCheck = pos.checkers(); - moveCount = quietCount = ss->moveCount = 0; + moveCount = captureCount = quietCount = ss->moveCount = 0; ss->statScore = 0; bestValue = -VALUE_INFINITE; - ss->ply = (ss-1)->ply + 1; // Check for the available remaining time if (thisThread == Threads.main()) static_cast(thisThread)->check_time(); - // Used to send selDepth info to GUI - if (PvNode && thisThread->selDepth < ss->ply) - thisThread->selDepth = ss->ply; + // Used to send selDepth info to GUI (selDepth counts from 1, ply from 0) + if (PvNode && thisThread->selDepth < ss->ply + 1) + thisThread->selDepth = ss->ply + 1; if (!rootNode) { @@ -593,6 +549,7 @@ namespace { assert(0 <= ss->ply && ss->ply < MAX_PLY); + (ss+1)->ply = ss->ply + 1; ss->currentMove = (ss+1)->excludedMove = bestMove = MOVE_NONE; ss->contHistory = &thisThread->contHistory[NO_PIECE][0]; (ss+2)->killers[0] = (ss+2)->killers[1] = MOVE_NONE; @@ -623,6 +580,8 @@ namespace { { if (!pos.capture_or_promotion(ttMove)) update_stats(pos, ss, ttMove, nullptr, 0, stat_bonus(depth)); + else + update_capture_stats(pos, ttMove, nullptr, 0, stat_bonus(depth)); // Extra penalty for a quiet TT move in previous ply when it gets refuted if ((ss-1)->moveCount == 1 && !pos.captured_piece()) @@ -658,8 +617,8 @@ namespace { int drawScore = TB::UseRule50 ? 1 : 0; - value = v < -drawScore ? -VALUE_MATE + MAX_PLY + ss->ply - : v > drawScore ? VALUE_MATE - MAX_PLY - ss->ply + value = v < -drawScore ? -VALUE_MATE + MAX_PLY + ss->ply + 1 + : v > drawScore ? VALUE_MATE - MAX_PLY - ss->ply - 1 : VALUE_DRAW + 2 * v * drawScore; tte->save(posKey, value_to_tt(value, ss->ply), BOUND_EXACT, @@ -727,7 +686,7 @@ namespace { // Step 8. Null move search with verification search (is omitted in PV nodes) if ( !PvNode && eval >= beta - && (ss->staticEval >= beta - 35 * (depth / ONE_PLY - 6) || depth >= 13 * ONE_PLY) + && ss->staticEval >= beta - 36 * depth / ONE_PLY + 225 && pos.non_pawn_material(pos.side_to_move())) { @@ -773,7 +732,7 @@ namespace { assert(is_ok((ss-1)->currentMove)); - MovePicker mp(pos, ttMove, rbeta - ss->staticEval); + MovePicker mp(pos, ttMove, rbeta - ss->staticEval, &thisThread->captureHistory); while ((move = mp.next_move()) != MOVE_NONE) if (pos.legal(move)) @@ -807,7 +766,7 @@ moves_loop: // When in check search starts from here const PieceToHistory* contHist[] = { (ss-1)->contHistory, (ss-2)->contHistory, nullptr, (ss-4)->contHistory }; Move countermove = thisThread->counterMoves[pos.piece_on(prevSq)][prevSq]; - MovePicker mp(pos, ttMove, depth, &thisThread->mainHistory, contHist, countermove, ss->killers); + MovePicker mp(pos, ttMove, depth, &thisThread->mainHistory, &thisThread->captureHistory, contHist, countermove, ss->killers); value = bestValue; // Workaround a bogus 'uninitialized' warning under gcc improving = ss->staticEval >= (ss-2)->staticEval /* || ss->staticEval == VALUE_NONE Already implicit in the previous condition */ @@ -822,6 +781,7 @@ moves_loop: // When in check search starts from here && tte->depth() >= depth - 3 * ONE_PLY; skipQuiets = false; ttCapture = false; + pvExact = PvNode && ttHit && tte->bound() == BOUND_EXACT; // Step 11. Loop through moves // Loop through all pseudo-legal moves until no moves remain or a beta cutoff occurs @@ -962,6 +922,14 @@ moves_loop: // When in check search starts from here r -= r ? ONE_PLY : DEPTH_ZERO; else { + // Decrease reduction if opponent's move count is high + if ((ss-1)->moveCount > 15) + r -= ONE_PLY; + + // Decrease reduction for exact PV nodes + if (pvExact) + r -= ONE_PLY; + // Increase reduction if ttMove is a capture if (ttCapture) r += ONE_PLY; @@ -984,10 +952,10 @@ moves_loop: // When in check search starts from here - 4000; // Decrease/increase reduction by comparing opponent's stat score - if (ss->statScore > 0 && (ss-1)->statScore < 0) + if (ss->statScore >= 0 && (ss-1)->statScore < 0) r -= ONE_PLY; - else if (ss->statScore < 0 && (ss-1)->statScore > 0) + else if ((ss-1)->statScore >= 0 && ss->statScore < 0) r += ONE_PLY; // Decrease/increase reduction for moves with a good/bad history @@ -1089,6 +1057,8 @@ moves_loop: // When in check search starts from here if (!captureOrPromotion && move != bestMove && quietCount < 64) quietsSearched[quietCount++] = move; + else if (captureOrPromotion && move != bestMove && captureCount < 32) + capturesSearched[captureCount++] = move; } // The following condition would detect a stop only after move loop has been @@ -1114,6 +1084,8 @@ moves_loop: // When in check search starts from here // Quiet best move: update move sorting heuristics if (!pos.capture_or_promotion(bestMove)) update_stats(pos, ss, bestMove, quietsSearched, quietCount, stat_bonus(depth)); + else + update_capture_stats(pos, bestMove, capturesSearched, captureCount, stat_bonus(depth)); // Extra penalty for a quiet TT move in previous ply when it gets refuted if ((ss-1)->moveCount == 1 && !pos.captured_piece()) @@ -1169,7 +1141,7 @@ moves_loop: // When in check search starts from here } ss->currentMove = bestMove = MOVE_NONE; - ss->ply = (ss-1)->ply + 1; + (ss+1)->ply = ss->ply + 1; moveCount = 0; // Check for an instant draw or if the maximum ply has been reached @@ -1184,7 +1156,6 @@ moves_loop: // When in check search starts from here // only two types of depth in TT: DEPTH_QS_CHECKS or DEPTH_QS_NO_CHECKS. ttDepth = InCheck || depth >= DEPTH_QS_CHECKS ? DEPTH_QS_CHECKS : DEPTH_QS_NO_CHECKS; - // Transposition table lookup posKey = pos.key(); tte = TT.probe(posKey, ttHit); @@ -1243,8 +1214,7 @@ moves_loop: // When in check search starts from here // to search the moves. Because the depth is <= 0 here, only captures, // queen promotions and checks (only if depth >= DEPTH_QS_CHECKS) will // be generated. - const PieceToHistory* contHist[4] = {}; - MovePicker mp(pos, ttMove, depth, &pos.this_thread()->mainHistory, contHist, to_sq((ss-1)->currentMove)); + MovePicker mp(pos, ttMove, depth, &pos.this_thread()->mainHistory, &pos.this_thread()->captureHistory, to_sq((ss-1)->currentMove)); // Loop through the moves until no moves remain or a beta cutoff occurs while ((move = mp.next_move()) != MOVE_NONE) @@ -1399,6 +1369,26 @@ moves_loop: // When in check search starts from here } + // update_capture_stats() updates move sorting heuristics when a new capture best move is found + + void update_capture_stats(const Position& pos, Move move, + Move* captures, int captureCnt, int bonus) { + + CapturePieceToHistory& captureHistory = pos.this_thread()->captureHistory; + Piece moved_piece = pos.moved_piece(move); + PieceType captured = type_of(pos.piece_on(to_sq(move))); + captureHistory.update(moved_piece,to_sq(move), captured, bonus); + + // Decrease all the other played capture moves + for (int i = 0; i < captureCnt; ++i) + { + moved_piece = pos.moved_piece(captures[i]); + captured = type_of(pos.piece_on(to_sq(captures[i]))); + captureHistory.update(moved_piece, to_sq(captures[i]), captured, -bonus); + } + } + + // update_stats() updates move sorting heuristics when a new quiet best move is found void update_stats(const Position& pos, Stack* ss, Move move, @@ -1430,6 +1420,24 @@ moves_loop: // When in check search starts from here } + // Is the PV leading to a draw position? Assumes all pv moves are legal + bool pv_is_draw(Position& pos) { + + StateInfo st[MAX_PLY]; + auto& pv = pos.this_thread()->rootMoves[0].pv; + + for (size_t i = 0; i < pv.size(); ++i) + pos.do_move(pv[i], st[i]); + + bool isDraw = pos.is_draw(pv.size()); + + for (size_t i = pv.size(); i > 0; --i) + pos.undo_move(pv[i-1]); + + return isDraw; + } + + // When playing with strength handicap, choose best move among a set of RootMoves // using a statistical rule dependent on 'level'. Idea by Heinz van Saanen. @@ -1453,7 +1461,7 @@ moves_loop: // When in check search starts from here int push = ( weakness * int(topScore - rootMoves[i].score) + delta * (rng.rand() % weakness)) / 128; - if (rootMoves[i].score + push > maxScore) + if (rootMoves[i].score + push >= maxScore) { maxScore = rootMoves[i].score + push; best = rootMoves[i].pv[0]; @@ -1492,7 +1500,7 @@ moves_loop: // When in check search starts from here if (Threads.ponder) return; - if ( (Limits.use_time_management() && elapsed > Time.maximum() - 10) + if ( (Limits.use_time_management() && elapsed > Time.maximum()) || (Limits.movetime && elapsed >= Limits.movetime) || (Limits.nodes && Threads.nodes_searched() >= (uint64_t)Limits.nodes)) Threads.stop = true; @@ -1591,6 +1599,10 @@ void Tablebases::filter_root_moves(Position& pos, Search::RootMoves& rootMoves) ProbeDepth = Options["SyzygyProbeDepth"] * ONE_PLY; Cardinality = Options["SyzygyProbeLimit"]; + // Don't filter any moves if the user requested analysis on multiple + if (Options["MultiPV"] != 1) + return; + // Skip TB probing when no TB found: !TBLargest -> !TB::Cardinality if (Cardinality > MaxCardinality) {