/// <summary> /// Actually runs a search with specified limits. /// </summary> /// <param name="searchLimit"></param> /// <returns></returns> private GameEngineSearchResultCeres RunSearch(SearchLimit searchLimit) { DateTime lastInfoUpdate = DateTime.Now; int numUpdatesSent = 0; MCTSManager.MCTSProgressCallback callback = (manager) => { curManager = manager; DateTime now = DateTime.Now; float timeSinceLastUpdate = (float)(now - lastInfoUpdate).TotalSeconds; bool isFirstUpdate = numUpdatesSent == 0; float UPDATE_INTERVAL_SECONDS = isFirstUpdate ? 0.1f : 0.5f; if (curManager != null && timeSinceLastUpdate > UPDATE_INTERVAL_SECONDS && curManager.Root.N > 0) { Send(UCIInfoString(curManager)); numUpdatesSent++; lastInfoUpdate = now; } }; GameEngineCeresInProcess.ProgressCallback callbackPlain = obj => callback((MCTSManager)obj); // use this? movesSinceNewGame // Search from this position (possibly with tree reuse) GameEngineSearchResultCeres result = CeresEngine.Search(curPositionAndMoves, searchLimit, gameMoveHistory, callbackPlain) as GameEngineSearchResultCeres; GameMoveStat moveStat = new GameMoveStat(gameMoveHistory.Count, curPositionAndMoves.FinalPosition.MiscInfo.SideToMove, result.ScoreQ, result.ScoreCentipawns, float.NaN, //engine1.CumulativeSearchTimeSeconds, curPositionAndMoves.FinalPosition.PieceCount, result.MAvg, result.FinalN, result.FinalN - result.StartingN, searchLimit, (float)result.TimingStats.ElapsedTimeSecs); gameMoveHistory.Add(moveStat); if (SearchFinishedEvent != null) { SearchFinishedEvent(result.Search.Manager); } // Send the final info string (unless this was an instamove). Send(UCIInfoString(result.Search.Manager, result.Search.BestMoveRoot)); // Send the best move Send("bestmove " + result.Search.BestMove.MoveStr(MGMoveNotationStyle.LC0Coordinate)); if (debug) { Send("info string " + result.Search.BestMoveRoot.BestMoveInfo(false)); } return(result); }
/// <summary> /// Handles special case that move was selected immediately at root from tablebase. /// </summary> /// <param name="manager"></param> /// <param name="searchRootNode"></param> /// <param name="scoreAsQ"></param> /// <returns></returns> static string OutputUCIInfoTablebaseImmediate(MCTSManager manager, MCTSNode searchRootNode, bool scoreAsQ) { GameResult result = searchRootNode.Ref.Terminal; string scoreStr; if (result == GameResult.Checkmate) { scoreStr = scoreAsQ ? "1.0" : "9999"; } else if (result == GameResult.Draw) { scoreStr = "0"; } else { // TODO: cleanup, see comment in MCTSManager.TrySetImmediateBestMove // explaining special meeting of Unknown status to actually mean loss. scoreStr = scoreAsQ ? "-1.0" : "-9999"; } string moveStr = manager.TablebaseImmediateBestMove.MoveStr(MGMoveNotationStyle.LC0Coordinate); string str = $"info depth 1 seldepth 1 time 0 nodes 1 score cp {scoreStr} pv {moveStr}"; return(str); }
public MCTSSearchFlow(MCTSManager manager, MCTSIterator context) { Manager = manager; Context = context; int numSelectors = context.ParamsSearch.Execution.FlowDirectOverlapped ? 2 : 1; batchingManagers = new MCTSBatchParamsManager[numSelectors]; for (int i = 0; i < numSelectors; i++) { batchingManagers[i] = new MCTSBatchParamsManager(manager.Context.ParamsSelect.UseDynamicVLoss); } bool shouldCache = context.EvaluatorDef.CacheMode != Chess.PositionEvalCaching.PositionEvalCache.CacheMode.None; string instanceID = "0"; //Params.Evaluator1 : Params.Evaluator2; const bool LOW_PRIORITY_PRIMARY = false; LeafEvaluatorNN nodeEvaluator1 = new LeafEvaluatorNN(context.EvaluatorDef, context.NNEvaluators.Evaluator1, shouldCache, LOW_PRIORITY_PRIMARY, context.Tree.PositionCache, null);// context.ParamsNN.DynamicNNSelectorFunc); BlockNNEval1 = new MCTSNNEvaluator(nodeEvaluator1, true); if (context.ParamsSearch.Execution.FlowDirectOverlapped) { // Create a second evaluator (configured like the first) on which to do overlapping. LeafEvaluatorNN nodeEvaluator2 = new LeafEvaluatorNN(context.EvaluatorDef, context.NNEvaluators.Evaluator2, shouldCache, false, context.Tree.PositionCache, null);// context.ParamsNN.DynamicNNSelectorFunc); BlockNNEval2 = new MCTSNNEvaluator(nodeEvaluator2, true); } if (context.EvaluatorDef.SECONDARY_NETWORK_ID != null) { throw new NotImplementedException(); //NodeEvaluatorNN nodeEvaluatorSecondary = new NodeEvaluatorNN(context.EvaluatorDef, context.ParamsNN.Evaluators.EvaluatorSecondary, false, false, null, null); //BlockNNEvalSecondaryNet = new MCTSNNEvaluate(nodeEvaluatorSecondary, false); } BlockApply = new MCTSApply(context.FirstMoveSampler); if (context.ParamsSearch.Execution.RootPreloadDepth > 0) { rootPreloader = new MCTSRootPreloader(); } if (context.ParamsSearch.Execution.SmartSizeBatches) { context.NNEvaluators.CalcStatistics(true, 1f); } }
private void TryAddSupplementalNodes(MCTSManager manager, int maxNodes, MCTSNodesSelectedSet selectedNodes, ILeafSelector selector) { foreach ((MCTSNode parentNode, int selectorID, int childIndex) in ((LeafSelectorMulti)selector).supplementalCandidates) // TODO: remove cast { if (childIndex <= parentNode.NumChildrenExpanded - 1) { // This child was already selected as part of the normal leaf gathering process. continue; } else { MCTSEventSource.TestCounter1++; // Record visit to this child in the parent (also increments the child NInFlight counter) parentNode.UpdateRecordVisitsToChild(selectorID, childIndex, 1); MCTSNode node = parentNode.CreateChild(childIndex); ((LeafSelectorMulti)selector).DoVisitLeafNode(node, 1);// TODO: remove cast if (!parentNode.IsRoot) { if (selectorID == 0) { parentNode.Parent.Ref.BackupIncrementInFlight(1, 0); } else { parentNode.Parent.Ref.BackupIncrementInFlight(0, 1); } } // Try to process this node int nodesBefore = selectedNodes.NodesNN.Count; selector.InsureAnnotated(node); selectedNodes.ProcessNode(node); bool wasSentToNN = selectedNodes.NodesNN.Count != nodesBefore; //if (wasSentToNN) MCTSEventSource.TestCounter2++; // dje: add counter? } } }
private void TryAddRootPreloadNodes(MCTSManager manager, int maxNodes, MCTSNodesSelectedSet selectedNodes, ILeafSelector selector) { if (rootPreloader == null) { return; } List <MCTSNode> rootPreloadNodes = rootPreloader.GetRootPreloadNodes(manager.Root, selector.SelectorID, maxNodes, MCTSRootPreloader.PRELOAD_MIN_P); if (rootPreloadNodes != null) { for (int i = 0; i < rootPreloadNodes.Count; i++) { MCTSNode node = rootPreloadNodes[i]; selector.InsureAnnotated(node); selectedNodes.ProcessNode(node); } } }
public static string UCIInfoString(MCTSManager manager, MCTSNode bestMoveRoot = null) { // If no override bestMoveRoot was specified // then it is assumed the move chosen was from the root (not an instamove) if (bestMoveRoot == null) { bestMoveRoot = manager.Root; } bool wasInstamove = manager.Root != bestMoveRoot; float elapsedTimeSeconds = wasInstamove ? 0 : (float)(DateTime.Now - manager.StartTimeThisSearch).TotalSeconds; float scoreCentipawn = MathF.Round(EncodedEvalLogistic.LogisticToCentipawn((float)bestMoveRoot.Q), 0); float nps = manager.NumStepsTakenThisSearch / elapsedTimeSeconds; SearchPrincipalVariation pv; using (new SearchContextExecutionBlock(manager.Context)) { pv = new SearchPrincipalVariation(bestMoveRoot); } //info depth 12 seldepth 27 time 30440 nodes 51100 score cp 105 hashfull 241 nps 1678 tbhits 0 pv e6c6 c5b4 d5e4 d1e1 int selectiveDepth = pv.Nodes.Count - 1; int depthOfBestMoveInTree = wasInstamove ? bestMoveRoot.Depth : 0; int depth = (int)MathF.Round(manager.Context.AvgDepth - depthOfBestMoveInTree, 0); if (wasInstamove) { // Note that the correct tablebase hits cannot be easily calculated and reported return($"info depth {depth} seldepth {selectiveDepth} time 0 " + $"nodes {bestMoveRoot.N:F0} score cp {scoreCentipawn:F0} tbhits {manager.CountTablebaseHits} nps 0 " + $"pv {pv.ShortStr()} string M= {bestMoveRoot.MAvg:F0} instamove"); } else { return($"info depth {depth} seldepth {selectiveDepth} time {elapsedTimeSeconds * 1000.0f:F0} " + $"nodes {manager.Root.N:F0} score cp {scoreCentipawn:F0} tbhits {manager.CountTablebaseHits} nps {nps:F0} " + $"pv {pv.ShortStr()} string M= {manager.Root.MAvg:F0}"); } }
void ProcessEPD(int epdNum, EPDEntry epd, bool outputDetail, ObjectPool <object> otherEngines) { UCISearchInfo otherEngineAnalysis2 = default; EPDEntry epdToUse = epd; Task RunNonCeres() { if (Def.ExternalEngineDef != null) { object engineObj = otherEngines.GetFromPool(); if (engineObj is LC0Engine) { LC0Engine le = (LC0Engine)engineObj; // Run test 2 first since that's the one we dump in detail, to avoid any possible caching effect from a prior run otherEngineAnalysis2 = le.AnalyzePositionFromFEN(epdToUse.FEN, epdToUse.StartMoves, Def.ExternalEngineDef.SearchLimit); // leelaAnalysis2 = le.AnalyzePositionFromFEN(epdToUse.FEN, new SearchLimit(SearchLimit.LimitType.NodesPerMove, 2)); // **** TEMP otherEngines.RestoreToPool(le); } else { UCIGameRunner runner = (engineObj is UCIGameRunner) ? (engineObj as UCIGameRunner) : (engineObj as GameEngineUCI).UCIRunner; string moveType = Def.ExternalEngineDef.SearchLimit.Type == SearchLimitType.NodesPerMove ? "nodes" : "movetime"; int moveValue = moveType == "nodes" ? (int)Def.ExternalEngineDef.SearchLimit.Value : (int)Def.ExternalEngineDef.SearchLimit.Value * 1000; runner.EvalPositionPrepare(); otherEngineAnalysis2 = runner.EvalPosition(epdToUse.FEN, epdToUse.StartMoves, moveType, moveValue, null); otherEngines.RestoreToPool(runner); // public UCISearchInfo EvalPosition(int engineNum, string fenOrPositionCommand, string moveType, int moveMetric, bool shouldCache = false) } } return(Task.CompletedTask); } bool EXTERNAL_CONCURRENT = numConcurrentSuiteThreads > 1; Task lzTask = EXTERNAL_CONCURRENT ? Task.Run(RunNonCeres) : RunNonCeres(); // Comptue search limit // If possible, adjust for the fact that LC0 "cheats" by going slightly over node budget SearchLimit ceresSearchLimit1 = Def.CeresEngine1Def.SearchLimit; SearchLimit ceresSearchLimit2 = Def.CeresEngine2Def?.SearchLimit; if (Def.CeresEngine1Def.SearchLimit.Type == SearchLimitType.NodesPerMove && otherEngineAnalysis2 != null && !Def.Engine1Def.SearchParams.FutilityPruningStopSearchEnabled) { if (Def.CeresEngine1Def.SearchLimit.Type == SearchLimitType.NodesPerMove) { ceresSearchLimit1 = new SearchLimit(SearchLimitType.NodesPerMove, otherEngineAnalysis2.Nodes); } if (Def.CeresEngine1Def.SearchLimit.Type == SearchLimitType.NodesPerMove) { ceresSearchLimit2 = new SearchLimit(SearchLimitType.NodesPerMove, otherEngineAnalysis2.Nodes); } } MCTSManager worker1, worker2 = default; MGMove bestMoveMG1, bestMoveMG2 = default; TimingStats stats1, stats2 = default; // Note that if we are running both Ceres1 and Ceres2 we alternate which search goes first. // This prevents any systematic difference/benefit that might come from order // (for example if we reuse position evaluations from the other tree, which can benefit only one of the two searches). if (epdNum % 2 == 0 || Def.CeresEngine2Def == null) { (worker1, bestMoveMG1, stats1) = MCTSLaunch.SearchOnFEN(evaluatorSet1, Def.Engine1Def.SelectParams, Def.Engine1Def.SearchParams, null, null, null, epdToUse.FEN, epd.StartMoves, MakeCeresSearchLimit(Def, otherEngineAnalysis2, ceresSearchLimit1), false, null, true, null); MCTSIterator shareContext = null; if (Def.RunCeres2Engine) { if (Def.Engine2Def.SearchParams.ReusePositionEvaluationsFromOtherTree) { shareContext = worker1.Context; } (worker2, bestMoveMG2, stats2) = MCTSLaunch.SearchOnFEN(evaluatorSet2, Def.Engine2Def.SelectParams, Def.Engine2Def.SearchParams, null, null, shareContext, epdToUse.FEN, epd.StartMoves, MakeCeresSearchLimit(Def, otherEngineAnalysis2, ceresSearchLimit2), false, null, true, null); } } else { (worker2, bestMoveMG2, stats2) = MCTSLaunch.SearchOnFEN(evaluatorSet2, Def.Engine2Def.SelectParams, Def.Engine2Def.SearchParams, null, null, null, epdToUse.FEN, epd.StartMoves, MakeCeresSearchLimit(Def, otherEngineAnalysis2, ceresSearchLimit2), false, null, true, null); MCTSIterator shareContext = null; if (Def.Engine1Def.SearchParams.ReusePositionEvaluationsFromOtherTree) { shareContext = worker2.Context; } (worker1, bestMoveMG1, stats1) = MCTSLaunch.SearchOnFEN(evaluatorSet1, Def.Engine1Def.SelectParams, Def.Engine1Def.SearchParams, null, null, shareContext, epdToUse.FEN, epd.StartMoves, MakeCeresSearchLimit(Def, otherEngineAnalysis2, ceresSearchLimit1), false, null, true, null); } // Wait for LZ analysis if (EXTERNAL_CONCURRENT) { lzTask.Wait(); } Move bestMoveOtherEngine = default; if (Def.ExternalEngineDef != null) { MGPosition thisPosX = PositionWithHistory.FromFENAndMovesUCI(epdToUse.FEN, epdToUse.StartMoves).FinalPosMG; MGMove lzMoveMG1 = MGMoveFromString.ParseMove(thisPosX, otherEngineAnalysis2.BestMove); bestMoveOtherEngine = MGMoveConverter.ToMove(lzMoveMG1); } Move bestMoveCeres1 = MGMoveConverter.ToMove(bestMoveMG1); Move bestMoveCeres2 = MGMoveConverter.ToMove(bestMoveMG2); char CorrectStr(Move move) => epdToUse.CorrectnessScore(move, 10) == 10 ? '+' : '.'; int scoreCeres1 = epdToUse.CorrectnessScore(bestMoveCeres1, 10); int scoreCeres2 = epdToUse.CorrectnessScore(bestMoveCeres2, 10); int scoreOtherEngine = epdToUse.CorrectnessScore(bestMoveOtherEngine, 10); SearchResultInfo result1 = new SearchResultInfo(worker1, bestMoveMG1); SearchResultInfo result2 = worker2 == null ? null : new SearchResultInfo(worker2, bestMoveMG2); accCeres1 += scoreCeres1; accCeres2 += scoreCeres2; // Accumulate how many nodes were required to find one of the correct moves // (in the cases where both succeeded) if (scoreCeres1 > 0 && (worker2 == null || scoreCeres2 > 0)) { accWCeres1 += (scoreCeres1 == 0) ? result1.N : result1.NumNodesWhenChoseTopNNode; if (worker2 != null) { accWCeres2 += (scoreCeres2 == 0) ? result2.N : result2.NumNodesWhenChoseTopNNode; } numSearchesBothFound++; } this.avgOther += scoreOtherEngine; numSearches++; float avgCeres1 = (float)accCeres1 / numSearches; float avgCeres2 = (float)accCeres2 / numSearches; float avgWCeres1 = (float)accWCeres1 / numSearchesBothFound; float avgWCeres2 = (float)accWCeres2 / numSearchesBothFound; float avgOther = (float)this.avgOther / numSearches; string MoveIfWrong(Move m) => m.IsNull || epdToUse.CorrectnessScore(m, 10) == 10 ? " " : m.ToString().ToLower(); int diff1 = scoreCeres1 - scoreOtherEngine; //NodeEvaluatorNeuralNetwork int evalNumBatches1 = result1.NumNNBatches; int evalNumPos1 = result1.NumNNNodes; int evalNumBatches2 = worker2 == null ? 0 : result2.NumNNBatches; int evalNumPos2 = worker2 == null ? 0 : result2.NumNNNodes; string correctMove = null; if (epdToUse.AMMoves != null) { correctMove = "-" + epdToUse.AMMoves[0]; } else if (epdToUse.BMMoves != null) { correctMove = epdToUse.BMMoves[0]; } float otherEngineTime = otherEngineAnalysis2 == null ? 0 : (float)otherEngineAnalysis2.EngineReportedSearchTime / 1000.0f; totalTimeOther += otherEngineTime; totalTimeCeres1 += (float)stats1.ElapsedTimeSecs; totalNodesOther += otherEngineAnalysis2 == null ? 0 : (int)otherEngineAnalysis2.Nodes; totalNodes1 += (int)result1.N; sumEvalNumPosOther += otherEngineAnalysis2 == null ? 0 : (int)otherEngineAnalysis2.Nodes; sumEvalNumBatches1 += evalNumBatches1; sumEvalNumPos1 += evalNumPos1; if (Def.RunCeres2Engine) { totalTimeCeres2 += (float)stats2.ElapsedTimeSecs; totalNodes2 += (int)result2.N; sumEvalNumBatches2 += evalNumBatches2; sumEvalNumPos2 += evalNumPos2; } float Adjust(int score, float frac) => score == 0 ? 0 : Math.Max(1.0f, MathF.Round(frac * 100.0f, 0)); string worker1PickedNonTopNMoveStr = result1.PickedNonTopNMoveStr; string worker2PickedNonTopNMoveStr = result2?.PickedNonTopNMoveStr; bool ex = otherEngineAnalysis2 != null; bool c2 = worker2 != null; Writer writer = new Writer(epdNum == 0); writer.Add("#", $"{epdNum,4}", 6); if (ex) { writer.Add("CEx", $"{avgOther,5:F2}", 7); } writer.Add("CC", $"{avgCeres1,5:F2}", 7); if (c2) { writer.Add("CC2", $"{avgCeres2,5:F2}", 7); } writer.Add("P", $" {0.001f * avgWCeres1,7:f2}", 9); if (c2) { writer.Add("P2", $" {0.001f * avgWCeres2,7:f2}", 9); } if (ex) { writer.Add("SEx", $"{scoreOtherEngine,3}", 5); } writer.Add("SC", $"{scoreCeres1,3}", 5); if (c2) { writer.Add("SC2", $"{scoreCeres2,3}", 5); } if (ex) { writer.Add("MEx", $"{otherEngineAnalysis2.BestMove,7}", 9); } writer.Add("MC", $"{worker1.BestMoveMG,7}", 9); if (c2) { writer.Add("MC2", $"{worker2.BestMoveMG,7}", 9); } writer.Add("Fr", $"{worker1PickedNonTopNMoveStr}{ 100.0f * result1.TopNNodeN / result1.N,3:F0}%", 9); if (c2) { writer.Add("Fr2", $"{worker2PickedNonTopNMoveStr}{ 100.0f * result2?.TopNNodeN / result2?.N,3:F0}%", 9); } writer.Add("Yld", $"{result1.NodeSelectionYieldFrac,6:f3}", 9); if (c2) { writer.Add("Yld2", $"{result2.NodeSelectionYieldFrac,6:f3}", 9); } // Search time if (ex) { writer.Add("TimeEx", $"{otherEngineTime,7:F2}", 9); } writer.Add("TimeC", $"{stats1.ElapsedTimeSecs,7:F2}", 9); if (c2) { writer.Add("TimeC2", $"{stats2.ElapsedTimeSecs,7:F2}", 9); } writer.Add("Dep", $"{result1.AvgDepth,5:f1}", 7); if (c2) { writer.Add("Dep2", $"{result2.AvgDepth,5:f1}", 7); } // Nodes if (ex) { writer.Add("NEx", $"{otherEngineAnalysis2.Nodes,12:N0}", 14); } writer.Add("Nodes", $"{result1.N,12:N0}", 14); if (c2) { writer.Add("Nodes2", $"{result2.N,12:N0}", 14); } // Fraction when chose top N writer.Add("Frac", $"{Adjust(scoreCeres1, result1.FractionNumNodesWhenChoseTopNNode),4:F0}", 6); if (c2) { writer.Add("Frac2", $"{Adjust(scoreCeres2, result2.FractionNumNodesWhenChoseTopNNode),4:F0}", 6); } // Score (Q) if (ex) { writer.Add("QEx", $"{otherEngineAnalysis2.ScoreLogistic,6:F3}", 8); } writer.Add("QC", $"{result1.Q,6:F3}", 8); if (c2) { writer.Add("QC2", $"{result2.Q,6:F3}", 8); } // Num batches&positions writer.Add("Batches", $"{evalNumBatches1,8:N0}", 10); writer.Add("NNEvals", $"{evalNumPos1,11:N0}", 13); if (c2) { writer.Add("Batches2", $"{evalNumBatches2,8:N0}", 10); writer.Add("NNEvals2", $"{evalNumPos2,11:N0}", 13); } // Tablebase hits writer.Add("TBase", $"{worker1.CountTablebaseHits,8:N0}", 10); if (c2) { writer.Add("TBase2", $"{worker2.CountTablebaseHits,8:N0}", 10); } // writer.Add("EPD", $"{epdToUse.ID,-30}", 32); if (outputDetail) { if (epdNum == 0) { Def.Output.WriteLine(writer.ids.ToString()); Def.Output.WriteLine(writer.dividers.ToString()); } Def.Output.WriteLine(writer.text.ToString()); } // MCTSNodeStorageSerialize.Save(worker1.Context.Store, @"c:\temp", "TESTSORE"); worker1?.Dispose(); if (!object.ReferenceEquals(worker1, worker2)) { worker2?.Dispose(); } }
/// <summary> /// Returns an UCI info string appropriate for a given search state. /// </summary> /// <param name="manager"></param> /// <param name="overrideRootMove"></param> /// <returns></returns> public static string UCIInfoString(MCTSManager manager, MCTSNode overrideRootMove = null, MCTSNode overrideBestMoveNodeAtRoot = null, int?multiPVIndex = null, bool useParentN = true, bool showWDL = false, bool scoreAsQ = false) { if (manager.TablebaseImmediateBestMove != default) { if (multiPVIndex.HasValue && multiPVIndex != 1) { return(null); } else { return(OutputUCIInfoTablebaseImmediate(manager, overrideRootMove ?? manager.Root, scoreAsQ)); } } bool wasInstamove = manager.Root != overrideRootMove; // If no override bestMoveRoot was specified // then it is assumed the move chosen was from the root (not an instamove) MCTSNode thisRootNode = overrideRootMove ?? manager.Root; if (thisRootNode.NumPolicyMoves == 0) { // Terminal position, nothing to output return(null); } float elapsedTimeSeconds = wasInstamove ? 0 : (float)(DateTime.Now - manager.StartTimeThisSearch).TotalSeconds; // Get the principal variation (the first move of which will be the best move) SearchPrincipalVariation pv; using (new SearchContextExecutionBlock(manager.Context)) { pv = new SearchPrincipalVariation(thisRootNode, overrideBestMoveNodeAtRoot); } MCTSNode bestMoveNode = pv.Nodes.Count > 1 ? pv.Nodes[1] : pv.Nodes[0]; // The score displayed corresponds to // the Q (average visit value) of the move to be made. float scoreToShow; if (scoreAsQ) { scoreToShow = MathF.Round((float)-bestMoveNode.Q * 1000, 0); } else { scoreToShow = MathF.Round(EncodedEvalLogistic.LogisticToCentipawn((float)-bestMoveNode.Q), 0); } float nps = manager.NumStepsTakenThisSearch / elapsedTimeSeconds; //info depth 12 seldepth 27 time 30440 nodes 51100 score cp 105 hashfull 241 nps 1678 tbhits 0 pv e6c6 c5b4 d5e4 d1e1 int selectiveDepth = pv.Nodes.Count; int depthOfBestMoveInTree = wasInstamove ? thisRootNode.Depth : 0; int depth = 1 + (int)MathF.Round(manager.Context.AvgDepth - depthOfBestMoveInTree, 0); string pvString = multiPVIndex.HasValue ? $"multipv {multiPVIndex} pv {pv.ShortStr()}" : $"pv {pv.ShortStr()}"; int n = thisRootNode.N; if (!useParentN && overrideBestMoveNodeAtRoot != null) { n = overrideBestMoveNodeAtRoot.N; } //score cp 27 wdl 384 326 290 string strWDL = ""; if (showWDL) { // Note that win and loss inverted to reverse perspective. strWDL = $" wdl {Math.Round(bestMoveNode.LossP * 1000)} " + $"{Math.Round(bestMoveNode.DrawP * 1000)} " + $"{Math.Round(bestMoveNode.WinP * 1000)}"; } if (wasInstamove) { // Note that the correct tablebase hits cannot be easily calculated and reported return($"info depth {depth} seldepth {selectiveDepth} time 0 " + $"nodes {n:F0} score cp {scoreToShow}{strWDL} tbhits {manager.CountTablebaseHits} nps 0 " + $"{pvString} string M= {thisRootNode.MAvg:F0} "); } else { return($"info depth {depth} seldepth {selectiveDepth} time {elapsedTimeSeconds * 1000.0f:F0} " + $"nodes {n:F0} score cp {scoreToShow}{strWDL} tbhits {manager.CountTablebaseHits} nps {nps:F0} " + $"{pvString} string M= {thisRootNode.MAvg:F0}"); } }