X-Git-Url: https://git.sesse.net/?p=stockfish;a=blobdiff_plain;f=src%2Fsearch.cpp;h=2f83f4f45b017fa7a9bfde67c155c28dbcc431a4;hp=3b3c0f2aeb0deb9f84b19782468669054c943970;hb=615d98da2447e79ceceae205e0cd4e878115acc3;hpb=383b12e1a5cc03a122e9a071eebde87eac85b116 diff --git a/src/search.cpp b/src/search.cpp index 3b3c0f2a..2f83f4f4 100644 --- a/src/search.cpp +++ b/src/search.cpp @@ -1,8 +1,6 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 - Copyright (C) 2004-2008 Tord Romstad (Glaurung author) - Copyright (C) 2008-2015 Marco Costalba, Joona Kiiski, Tord Romstad - Copyright (C) 2015-2020 Marco Costalba, Joona Kiiski, Gary Linscott, Tord Romstad + Copyright (C) 2004-2020 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 @@ -65,9 +63,9 @@ namespace { constexpr uint64_t TtHitAverageResolution = 1024; // Razor and futility margins - constexpr int RazorMargin = 531; + constexpr int RazorMargin = 527; Value futility_margin(Depth d, bool improving) { - return Value(217 * (d - improving)); + return Value(227 * (d - improving)); } // Reductions lookup table, initialized at startup @@ -75,16 +73,16 @@ namespace { Depth reduction(bool i, Depth d, int mn) { int r = Reductions[d] * Reductions[mn]; - return (r + 511) / 1024 + (!i && r > 1007); + return (r + 570) / 1024 + (!i && r > 1018); } constexpr int futility_move_count(bool improving, Depth depth) { - return (4 + depth * depth) / (2 - improving); + return (3 + depth * depth) / (2 - improving); } // History and stats update bonus, based on depth int stat_bonus(Depth d) { - return d > 15 ? -8 : 19 * d * d + 155 * d - 132; + return d > 15 ? 27 : 17 * d * d + 133 * d - 134; } // Add a small random component to draw evaluations to avoid 3fold-blindness @@ -227,6 +225,8 @@ void MainThread::search() { Time.init(Limits, us, rootPos.game_ply()); TT.new_search(); + Eval::verify_NNUE(); + if (rootMoves.empty()) { rootMoves.emplace_back(MOVE_NONE); @@ -236,14 +236,8 @@ void MainThread::search() { } else { - for (Thread* th : Threads) - { - th->bestMoveChanges = 0; - if (th != this) - th->start_searching(); - } - - Thread::search(); // Let's start searching! + Threads.start_searching(); // start non-main threads + Thread::search(); // main thread start searching } // When we reach the maximum depth, we can arrive here without a raise of @@ -260,9 +254,7 @@ void MainThread::search() { Threads.stop = true; // Wait until all threads have finished - for (Thread* th : Threads) - if (th != this) - th->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. @@ -271,37 +263,11 @@ void MainThread::search() { Thread* bestThread = this; - // Check if there are threads with a better score than main thread - if ( int(Options["MultiPV"]) == 1 + if ( int(Options["MultiPV"]) == 1 && !Limits.depth && !(Skill(Options["Skill Level"]).enabled() || int(Options["UCI_LimitStrength"])) - && rootMoves[0].pv[0] != MOVE_NONE) - { - std::map votes; - Value minScore = this->rootMoves[0].score; - - // Find minimum score - for (Thread* th: Threads) - minScore = std::min(minScore, th->rootMoves[0].score); - - // Vote according to score and depth, and select the best thread - for (Thread* th : Threads) - { - votes[th->rootMoves[0].pv[0]] += - (th->rootMoves[0].score - minScore + 14) * int(th->completedDepth); - - if (abs(bestThread->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) - 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]])) - bestThread = th; - } - } + && rootMoves[0].pv[0] != MOVE_NONE) + bestThread = Threads.get_best_thread(); bestPreviousScore = bestThread->rootMoves[0].score; @@ -437,12 +403,12 @@ void Thread::search() { if (rootDepth >= 4) { Value prev = rootMoves[pvIdx].previousScore; - delta = Value(21); + delta = Value(19); alpha = std::max(prev - delta,-VALUE_INFINITE); beta = std::min(prev + delta, VALUE_INFINITE); // Adjust contempt based on root move's previousScore (dynamic contempt) - int dct = ct + (102 - ct / 2) * prev / (abs(prev) + 157); + int dct = ct + (110 - ct / 2) * prev / (abs(prev) + 140); contempt = (us == WHITE ? make_score(dct, dct / 2) : -make_score(dct, dct / 2)); @@ -540,13 +506,13 @@ void Thread::search() { && !Threads.stop && !mainThread->stopOnPonderhit) { - double fallingEval = (332 + 6 * (mainThread->bestPreviousScore - bestValue) - + 6 * (mainThread->iterValue[iterIdx] - bestValue)) / 704.0; + double fallingEval = (296 + 6 * (mainThread->bestPreviousScore - bestValue) + + 6 * (mainThread->iterValue[iterIdx] - bestValue)) / 725.0; fallingEval = Utility::clamp(fallingEval, 0.5, 1.5); // If the bestMove is stable over several iterations, reduce time accordingly - timeReduction = lastBestMoveDepth + 9 < completedDepth ? 1.94 : 0.91; - double reduction = (1.41 + mainThread->previousTimeReduction) / (2.27 * timeReduction); + timeReduction = lastBestMoveDepth + 10 < completedDepth ? 1.92 : 0.95; + double reduction = (1.47 + mainThread->previousTimeReduction) / (2.22 * timeReduction); // Use part of the gained time from a previous stable move for the current move for (Thread* th : Threads) @@ -556,9 +522,11 @@ void Thread::search() { } double bestMoveInstability = 1 + totBestMoveChanges / Threads.size(); - // Stop the search if we have only one legal move, or if available time elapsed - if ( rootMoves.size() == 1 - || Time.elapsed() > Time.optimum() * fallingEval * reduction * bestMoveInstability) + double totalTime = rootMoves.size() == 1 ? 0 : + Time.optimum() * fallingEval * reduction * bestMoveInstability; + + // Stop the search if we have exceeded the totalTime, at least 1ms search + if (Time.elapsed() > totalTime) { // If we are allowed to ponder do not stop the search now but // keep pondering until the GUI sends "ponderhit" or "stop". @@ -569,7 +537,7 @@ void Thread::search() { } else if ( Threads.increaseDepth && !mainThread->ponder - && Time.elapsed() > Time.optimum() * fallingEval * reduction * bestMoveInstability * 0.6) + && Time.elapsed() > totalTime * 0.56) Threads.increaseDepth = false; else Threads.increaseDepth = true; @@ -628,9 +596,10 @@ namespace { Key posKey; Move ttMove, move, excludedMove, bestMove; Depth extension, newDepth; - Value bestValue, value, ttValue, eval, maxValue; + Value bestValue, value, ttValue, eval, maxValue, probcutBeta; bool ttHit, ttPv, formerPv, givesCheck, improving, didLMR, priorCapture; - bool captureOrPromotion, doFullDepthSearch, moveCountPruning, ttCapture, singularLMR; + bool captureOrPromotion, doFullDepthSearch, moveCountPruning, + ttCapture, singularQuietLMR; Piece movedPiece; int moveCount, captureCount, quietCount; @@ -658,7 +627,7 @@ namespace { || pos.is_draw(ss->ply) || ss->ply >= MAX_PLY) return (ss->ply >= MAX_PLY && !ss->inCheck) ? evaluate(pos) - : value_draw(pos.this_thread()); + : value_draw(pos.this_thread()); // 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 @@ -693,7 +662,7 @@ namespace { // search to overwrite a previous full search TT value, so we use a different // position key in case of an excluded move. excludedMove = ss->excludedMove; - posKey = pos.key() ^ Key(excludedMove << 16); // Isn't a very good hash + posKey = excludedMove == MOVE_NONE ? pos.key() : pos.key() ^ make_key(excludedMove); tte = TT.probe(posKey, ttHit); ttValue = ttHit ? value_from_tt(tte->value(), ss->ply, pos.rule50_count()) : VALUE_NONE; ttMove = rootNode ? thisThread->rootMoves[thisThread->pvIdx].pv[0] @@ -701,7 +670,11 @@ namespace { ttPv = PvNode || (ttHit && tte->is_pv()); formerPv = ttPv && !PvNode; - if (ttPv && depth > 12 && ss->ply - 1 < MAX_LPH && !pos.captured_piece() && is_ok((ss-1)->currentMove)) + if ( ttPv + && depth > 12 + && ss->ply - 1 < MAX_LPH + && !priorCapture + && is_ok((ss-1)->currentMove)) thisThread->lowPlyHistory[ss->ply - 1][from_to((ss-1)->currentMove)] << stat_bonus(depth - 5); // thisThread->ttHitAverage can be used to approximate the running average of ttHit @@ -798,9 +771,10 @@ namespace { // Step 6. Static evaluation of the position if (ss->inCheck) { + // Skip early pruning when in check ss->staticEval = eval = VALUE_NONE; improving = false; - goto moves_loop; // Skip early pruning when in check + goto moves_loop; } else if (ttHit) { @@ -850,10 +824,10 @@ namespace { // Step 9. Null move search with verification search (~40 Elo) if ( !PvNode && (ss-1)->currentMove != MOVE_NULL - && (ss-1)->statScore < 23397 + && (ss-1)->statScore < 23824 && eval >= beta && eval >= ss->staticEval - && ss->staticEval >= beta - 32 * depth - 30 * improving + 120 * ttPv + 292 + && ss->staticEval >= beta - 33 * depth - 33 * improving + 112 * ttPv + 311 && !excludedMove && pos.non_pawn_material(us) && (ss->ply >= thisThread->nmpMinPly || us != thisThread->nmpColor)) @@ -861,7 +835,7 @@ namespace { assert(eval - beta >= 0); // Null move dynamic reduction based on depth and value - Depth R = (854 + 68 * depth) / 258 + std::min(int(eval - beta) / 192, 3); + Depth R = (737 + 77 * depth) / 246 + std::min(int(eval - beta) / 192, 3); ss->currentMove = MOVE_NULL; ss->continuationHistory = &thisThread->continuationHistory[0][0][NO_PIECE][0]; @@ -897,23 +871,33 @@ namespace { } } + probcutBeta = beta + 176 - 49 * improving; + // Step 10. ProbCut (~10 Elo) // If we have a good enough capture and a reduced search returns a value // much above beta, we can (almost) safely prune the previous move. if ( !PvNode - && depth >= 5 - && abs(beta) < VALUE_TB_WIN_IN_MAX_PLY) + && depth > 4 + && abs(beta) < VALUE_TB_WIN_IN_MAX_PLY + && !( ttHit + && tte->depth() >= depth - 3 + && ttValue != VALUE_NONE + && ttValue < probcutBeta)) { - Value raisedBeta = beta + 189 - 45 * improving; - assert(raisedBeta < VALUE_INFINITE); - MovePicker mp(pos, ttMove, raisedBeta - ss->staticEval, &captureHistory); + if ( ttHit + && tte->depth() >= depth - 3 + && ttValue != VALUE_NONE + && ttValue >= probcutBeta + && ttMove + && pos.capture_or_promotion(ttMove)) + return probcutBeta; + + assert(probcutBeta < VALUE_INFINITE); + MovePicker mp(pos, ttMove, probcutBeta - ss->staticEval, &captureHistory); int probCutCount = 0; while ( (move = mp.next_move()) != MOVE_NONE - && probCutCount < 2 + 2 * cutNode - && !( move == ttMove - && tte->depth() >= depth - 4 - && ttValue < raisedBeta)) + && probCutCount < 2 + 2 * cutNode) if (move != excludedMove && pos.legal(move)) { assert(pos.capture_or_promotion(move)); @@ -931,16 +915,24 @@ namespace { pos.do_move(move, st); // Perform a preliminary qsearch to verify that the move holds - value = -qsearch(pos, ss+1, -raisedBeta, -raisedBeta+1); + value = -qsearch(pos, ss+1, -probcutBeta, -probcutBeta+1); // If the qsearch held, perform the regular search - if (value >= raisedBeta) - value = -search(pos, ss+1, -raisedBeta, -raisedBeta+1, depth - 4, !cutNode); + if (value >= probcutBeta) + value = -search(pos, ss+1, -probcutBeta, -probcutBeta+1, depth - 4, !cutNode); pos.undo_move(move); - if (value >= raisedBeta) + if (value >= probcutBeta) + { + if ( !(ttHit + && tte->depth() >= depth - 3 + && ttValue != VALUE_NONE)) + tte->save(posKey, value_to_tt(value, ss->ply), ttPv, + BOUND_LOWER, + depth - 3, move, ss->staticEval); return value; + } } } @@ -968,10 +960,10 @@ moves_loop: // When in check, search starts from here contHist, countermove, ss->killers, - depth > 12 ? ss->ply : MAX_PLY); + ss->ply); value = bestValue; - singularLMR = moveCountPruning = false; + singularQuietLMR = moveCountPruning = false; ttCapture = ttMove && pos.capture_or_promotion(ttMove); // Mark this node as being searched @@ -994,6 +986,10 @@ moves_loop: // When in check, search starts from here thisThread->rootMoves.begin() + thisThread->pvLast, move)) continue; + // Check for legality + if (!rootNode && !pos.legal(move)) + continue; + ss->moveCount = ++moveCount; if (rootNode && thisThread == Threads.main() && Time.elapsed() > 3000) @@ -1034,14 +1030,15 @@ moves_loop: // When in check, search starts from here // Futility pruning: parent node (~5 Elo) if ( lmrDepth < 6 && !ss->inCheck - && ss->staticEval + 235 + 172 * lmrDepth <= alpha + && ss->staticEval + 284 + 188 * lmrDepth <= alpha && (*contHist[0])[movedPiece][to_sq(move)] + (*contHist[1])[movedPiece][to_sq(move)] - + (*contHist[3])[movedPiece][to_sq(move)] < 27400) + + (*contHist[3])[movedPiece][to_sq(move)] + + (*contHist[5])[movedPiece][to_sq(move)] / 2 < 28388) continue; // Prune moves with negative SEE (~20 Elo) - if (!pos.see_ge(move, Value(-(32 - std::min(lmrDepth, 18)) * lmrDepth * lmrDepth))) + if (!pos.see_ge(move, Value(-(29 - std::min(lmrDepth, 17)) * lmrDepth * lmrDepth))) continue; } else @@ -1055,12 +1052,15 @@ moves_loop: // When in check, search starts from here // Futility pruning for captures if ( !givesCheck && lmrDepth < 6 + && !(PvNode && abs(bestValue) < 2) + && PieceValue[MG][type_of(movedPiece)] >= PieceValue[MG][type_of(pos.piece_on(to_sq(move)))] && !ss->inCheck - && ss->staticEval + 270 + 384 * lmrDepth + PieceValue[MG][type_of(pos.piece_on(to_sq(move)))] <= alpha) + && ss->staticEval + 267 + 391 * lmrDepth + + PieceValue[MG][type_of(pos.piece_on(to_sq(move)))] <= alpha) continue; // See based pruning - if (!pos.see_ge(move, Value(-194) * depth)) // (~25 Elo) + if (!pos.see_ge(move, Value(-202) * depth)) // (~25 Elo) continue; } } @@ -1071,7 +1071,7 @@ moves_loop: // When in check, search starts from here // search of (alpha-s, beta-s), and just one fails high on (alpha, beta), // then that move is singular and should be extended. To verify this we do // a reduced search on all the other moves but the ttMove and if the - // result is lower than ttValue minus a margin then we will extend the ttMove. + // result is lower than ttValue minus a margin, then we will extend the ttMove. if ( depth >= 6 && move == ttMove && !rootNode @@ -1091,7 +1091,7 @@ moves_loop: // When in check, search starts from here if (value < singularBeta) { extension = 1; - singularLMR = true; + singularQuietLMR = !ttCapture; } // Multi-cut pruning @@ -1102,8 +1102,8 @@ moves_loop: // When in check, search starts from here else if (singularBeta >= beta) return singularBeta; - // If the eval of ttMove is greater than beta we try also if there is an other move that - // pushes it over beta, if so also produce a cutoff + // If the eval of ttMove is greater than beta we try also if there is another + // move that pushes it over beta, if so also produce a cutoff. else if (ttValue >= beta) { ss->excludedMove = move; @@ -1135,25 +1135,12 @@ moves_loop: // When in check, search starts from here if (type_of(move) == CASTLING) extension = 1; - // Late irreversible move extension - if ( move == ttMove - && pos.rule50_count() > 80 - && (captureOrPromotion || type_of(movedPiece) == PAWN)) - extension = 2; - // Add extension to new depth newDepth += extension; // Speculative prefetch as early as possible prefetch(TT.first_entry(pos.key_after(move))); - // Check for legality just before making the move - if (!rootNode && !pos.legal(move)) - { - ss->moveCount = --moveCount; - continue; - } - // Update the current move (this must be done after singular extension search) ss->currentMove = move; ss->continuationHistory = &thisThread->continuationHistory[ss->inCheck] @@ -1173,15 +1160,22 @@ moves_loop: // When in check, search starts from here || moveCountPruning || ss->staticEval + PieceValue[EG][pos.captured_piece()] <= alpha || cutNode - || thisThread->ttHitAverage < 375 * TtHitAverageResolution * TtHitAverageWindow / 1024)) + || thisThread->ttHitAverage < 415 * TtHitAverageResolution * TtHitAverageWindow / 1024)) { Depth r = reduction(improving, depth, moveCount); + // Decrease reduction at non-check cut nodes for second move at low depths + if ( cutNode + && depth <= 10 + && moveCount <= 2 + && !ss->inCheck) + r--; + // Decrease reduction if the ttHit running average is large - if (thisThread->ttHitAverage > 500 * TtHitAverageResolution * TtHitAverageWindow / 1024) + if (thisThread->ttHitAverage > 473 * TtHitAverageResolution * TtHitAverageWindow / 1024) r--; - // Reduction if other threads are searching this position. + // Reduction if other threads are searching this position if (th.marked()) r++; @@ -1193,11 +1187,11 @@ moves_loop: // When in check, search starts from here r++; // Decrease reduction if opponent's move count is high (~5 Elo) - if ((ss-1)->moveCount > 14) + if ((ss-1)->moveCount > 13) r--; // Decrease reduction if ttMove has been singularly extended (~3 Elo) - if (singularLMR) + if (singularQuietLMR) r -= 1 + formerPv; if (!captureOrPromotion) @@ -1215,23 +1209,23 @@ moves_loop: // When in check, search starts from here // hence break make_move(). (~2 Elo) else if ( type_of(move) == NORMAL && !pos.see_ge(reverse_move(move))) - r -= 2 + ttPv; + r -= 2 + ttPv - (type_of(movedPiece) == PAWN); ss->statScore = thisThread->mainHistory[us][from_to(move)] + (*contHist[0])[movedPiece][to_sq(move)] + (*contHist[1])[movedPiece][to_sq(move)] + (*contHist[3])[movedPiece][to_sq(move)] - - 4926; + - 4826; // Decrease/increase reduction by comparing opponent's stat score (~10 Elo) - if (ss->statScore >= -102 && (ss-1)->statScore < -114) + if (ss->statScore >= -100 && (ss-1)->statScore < -112) r--; - else if ((ss-1)->statScore >= -116 && ss->statScore < -154) + else if ((ss-1)->statScore >= -125 && ss->statScore < -138) r++; // Decrease/increase reduction for moves with a good/bad history (~30 Elo) - r -= ss->statScore / 16434; + r -= ss->statScore / 14615; } else { @@ -1241,7 +1235,7 @@ moves_loop: // When in check, search starts from here // Unless giving check, this capture is likely bad if ( !givesCheck - && ss->staticEval + PieceValue[EG][pos.captured_piece()] + 200 * depth <= alpha) + && ss->staticEval + PieceValue[EG][pos.captured_piece()] + 211 * depth <= alpha) r++; } @@ -1318,7 +1312,7 @@ moves_loop: // When in check, search starts from here rm.pv.push_back(*m); // We record how often the best move has been changed in each - // iteration. This information is used for time management: When + // iteration. This information is used for time management: when // the best move changes frequently, we allocate some more time. if (moveCount > 1) ++thisThread->bestMoveChanges; @@ -1503,7 +1497,7 @@ moves_loop: // When in check, search starts from here if (PvNode && bestValue > alpha) alpha = bestValue; - futilityBase = bestValue + 154; + futilityBase = bestValue + 141; } const PieceToHistory* contHist[] = { (ss-1)->continuationHistory, (ss-2)->continuationHistory, @@ -1512,8 +1506,8 @@ moves_loop: // When in check, search starts from here // Initialize a MovePicker object for the current position, and prepare // 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. + // queen and checking knight promotions, and other checks(only if depth >= DEPTH_QS_CHECKS) + // will be generated. MovePicker mp(pos, ttMove, depth, &thisThread->mainHistory, &thisThread->captureHistory, contHist, @@ -1552,7 +1546,7 @@ moves_loop: // When in check, search starts from here } } - // Don't search moves with negative SEE values + // Do not search moves with negative SEE values if ( !ss->inCheck && !pos.see_ge(move)) continue; @@ -1599,7 +1593,7 @@ moves_loop: // When in check, search starts from here } } - // All legal moves have been searched. A special case: If we're in check + // All legal moves have been searched. A special case: if we're in check // and no legal moves were found, it is checkmate. if (ss->inCheck && bestValue == -VALUE_INFINITE) return mated_in(ss->ply); // Plies to mate from the root @@ -1616,7 +1610,7 @@ moves_loop: // When in check, search starts from here // value_to_tt() 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. + // "plies to mate from the current position". Standard scores are unchanged. // The function is called before storing a value in the transposition table. Value value_to_tt(Value v, int ply) { @@ -1628,11 +1622,11 @@ moves_loop: // When in check, search starts from here } - // value_from_tt() is the inverse of value_to_tt(): It adjusts a mate or TB score - // from the transposition table (which refers to the plies to mate/be mated - // from current position) to "plies to mate/be mated (TB win/loss) from the root". - // However, for mate scores, to avoid potentially false mate scores related to the 50 moves rule, - // and the graph history interaction, return an optimal TB score instead. + // value_from_tt() is the inverse of value_to_tt(): it adjusts a mate or TB score + // from the transposition table (which refers to the plies to mate/be mated from + // current position) to "plies to mate/be mated (TB win/loss) from the root". However, + // for mate scores, to avoid potentially false mate scores related to the 50 moves rule + // and the graph history interaction, we return an optimal TB score instead. Value value_from_tt(Value v, int ply, int r50c) { @@ -1753,8 +1747,8 @@ moves_loop: // When in check, search starts from here thisThread->counterMoves[pos.piece_on(prevSq)][prevSq] = move; } - if (depth > 12 && ss->ply < MAX_LPH) - thisThread->lowPlyHistory[ss->ply][from_to(move)] << stat_bonus(depth - 7); + if (depth > 11 && ss->ply < MAX_LPH) + thisThread->lowPlyHistory[ss->ply][from_to(move)] << stat_bonus(depth - 6); } // When playing with strength handicap, choose best move among a set of RootMoves @@ -1792,6 +1786,7 @@ moves_loop: // When in check, search starts from here } // namespace + /// MainThread::check_time() is used to print debug info and, more importantly, /// to detect when we are out of available time and thus stop the search. @@ -1860,6 +1855,9 @@ string UCI::pv(const Position& pos, Depth depth, Value alpha, Value beta) { << " multipv " << i + 1 << " score " << UCI::value(v); + if (Options["UCI_ShowWDL"]) + ss << UCI::wdl(v, pos.game_ply()); + if (!tb && i == pvIdx) ss << (v >= beta ? " lowerbound" : v <= alpha ? " upperbound" : "");