public void AppendMove(string moveStr) { MGPosition mgPos = MGPosition.FromPosition(FinalPosition); MGMove thisMove = MGMoveFromString.ParseMove(mgPos, moveStr); if (thisMove.IsNull) { throw new Exception("Unexpected null move"); } // Verify move is legal from this position MGMoveList moves = new MGMoveList(); MGMoveGen.GenerateMoves(in mgPos, moves); if (Array.IndexOf(moves.MovesArray, thisMove) == -1) { throw new Exception($"The move {moveStr} is not legal from position {FinalPosition.FEN}"); } Moves.Add(MGMoveFromString.ParseMove(mgPos, moveStr)); if (haveFinalized) { InitPositionsAndFinalPosMG(); } }
/// <summary> /// Constructs a new MGMoveSequence given a starting position (as a FEN) /// and an optional string containing a sequence of subsequent moves (in coordiante notation). /// </summary> /// <param name="fen"></param> /// <param name="movesStr"></param> /// <returns></returns> public static PositionWithHistory FromFENAndMovesUCI(string fen, string movesStr) { MGPosition mgPos = MGPosition.FromFEN(fen); PositionWithHistory ret = new PositionWithHistory(mgPos); if (movesStr != null && movesStr != "") { string[] parts = movesStr.Split(" "); for (int i = 0; i < parts.Length; i++) { string moveStr = parts[i]; if (moveStr != "") { MGMove mgMove = MGMoveFromString.ParseMove(mgPos, moveStr); ret.Moves.Add(mgMove); mgPos.MakeMove(mgMove); } } } return(ret); }
/// <summary> /// Runs CPU benchmark and outputs summary results, /// with an overall statistic provided (index to 100 on a Intel Skylake 6142). /// </summary> /// <returns>Relative CPU index (baseline 100)</returns> static int DumpCPUBenchmark() { Console.WriteLine("-----------------------------------------------------------------------------------"); Console.WriteLine("CPU BENCHMARK"); Position ps = Position.StartPosition; EncodedPositionBoard zb = default; MGMove nmove = ConverterMGMoveEncodedMove.EncodedMoveToMGChessMove(new EncodedMove("e2e4"), MGChessPositionConverter.MGChessPositionFromFEN(ps.FEN)); float ops1 = Benchmarking.DumpOperationTimeAndMemoryStats(() => MGPosition.FromPosition(ps), "MGPosition.FromPosition"); float ops2 = Benchmarking.DumpOperationTimeAndMemoryStats(() => MGChessPositionConverter.MGChessPositionFromFEN(ps.FEN), "MGChessPositionFromFEN"); float ops3 = Benchmarking.DumpOperationTimeAndMemoryStats(() => ConverterMGMoveEncodedMove.MGChessMoveToEncodedMove(nmove), "MGChessMoveToLZPositionMove"); float ops4 = Benchmarking.DumpOperationTimeAndMemoryStats(() => EncodedBoardZobrist.ZobristHash(zb), "ZobristHash"); // Performance metric is against a baseline system (Intel Skylake 6142) const float REFERENCE_BM1_OPS = 2160484; const float REFERENCE_BM2_OPS = 448074; const float REFERENCE_BM3_OPS = 157575582; const float REFERENCE_BM4_OPS = 112731351; float relative1 = ops1 / REFERENCE_BM1_OPS; float relative2 = ops2 / REFERENCE_BM2_OPS; float relative3 = ops3 / REFERENCE_BM3_OPS; float relative4 = ops4 / REFERENCE_BM4_OPS; float avg = StatUtils.Average(relative1, relative2, relative3, relative4); Console.WriteLine(); Console.WriteLine($"CERES CPU BENCHMARK SCORE: {avg*100,4:F0}"); return((int)MathF.Round(avg * 100, 0)); }
/// <summary> /// Constructs a test batch of specified size. /// </summary> /// <param name="evaluator"></param> /// <param name="count"></param> /// <param name="fen"></param> /// <returns></returns> public static EncodedPositionBatchFlat MakeTestBatch(NNEvaluator evaluator, int count, string fen = null) { EncodedPositionBatchFlat batch; if (fen == null) { fen = Position.StartPosition.FEN; } Position rawPos = Position.FromFEN(fen); MGPosition mgPos = MGPosition.FromPosition(rawPos); EncodedPositionWithHistory position = EncodedPositionWithHistory.FromFEN(fen); EncodedPositionWithHistory[] positions = new EncodedPositionWithHistory[count]; Array.Fill(positions, position); batch = new EncodedPositionBatchFlat(positions, count); bool hasPositions = evaluator.InputsRequired.HasFlag(NNEvaluator.InputTypes.Positions); bool hasMoves = evaluator.InputsRequired.HasFlag(NNEvaluator.InputTypes.Moves); bool hasHashes = evaluator.InputsRequired.HasFlag(NNEvaluator.InputTypes.Hashes); bool hasBoards = evaluator.InputsRequired.HasFlag(NNEvaluator.InputTypes.Boards); if (fen != null) { if (hasPositions) { batch.Positions = new MGPosition[count]; } if (hasHashes) { batch.PositionHashes = new ulong[count]; } if (hasMoves) { batch.Moves = new MGMoveList[count]; } for (int i = 0; i < count; i++) { if (hasPositions) { batch.Positions[i] = MGChessPositionConverter.MGChessPositionFromFEN(fen); } if (hasHashes) { batch.PositionHashes[i] = (ulong)i + (ulong)batch.Positions[i].GetHashCode(); } if (hasMoves) { MGMoveList moves = new MGMoveList(); MGMoveGen.GenerateMoves(in mgPos, moves); batch.Moves[i] = moves; } } } return(batch); }
MovesAndProbabilities(Position startPosition, float minProbability = 0.0f, int topN = int.MaxValue) { // (EncodedMove bestMoveEncoded, float probability) = result.Policy.PolicyInfoAtIndex(posIndex); MGPosition mgPos = MGPosition.FromPosition(in startPosition); foreach ((EncodedMove move, float probability) in ProbabilitySummary(minProbability, topN)) { MGMove mgMove = ConverterMGMoveEncodedMove.EncodedMoveToMGChessMove(move, in mgPos); Move moveRet = MGMoveConverter.ToMove(mgMove); yield return(moveRet, probability); } }
public static MGMove ToMGMove(MGPosition mgPos, EncodedMove encodedMove) { MGMoveList movesLegal = new MGMoveList(); MGMoveGen.GenerateMoves(in mgPos, movesLegal); int indexLegalMove = MoveInMGMovesArrayLocator.FindMoveInMGMoves(in mgPos, movesLegal.MovesArray, encodedMove, 0, movesLegal.NumMovesUsed, mgPos.BlackToMove); if (indexLegalMove == -1) { throw new Exception($"Move not found {encodedMove}"); } return(movesLegal.MovesArray[indexLegalMove]); // Move move = MGMoveConverter.ToMove(theMove); }
/// <summary> /// Returns the MGMove corresponding to a given Move. /// </summary> /// <param name="pos"></param> /// <param name="move"></param> /// <returns></returns> public static MGMove MGMoveFromPosAndMove(Position pos, Move move) { PositionWithMove moveAndPos = new PositionWithMove(pos, move); MGPosition mgPos = MGPosition.FromPosition(in moveAndPos.Position); MGMoveList moves = new MGMoveList(); MGMoveGen.GenerateMoves(in mgPos, moves); for (int i = 0; i < moves.NumMovesUsed; i++) { if (moves.MovesArray[i].EqualsMove(move)) { return(moves.MovesArray[i]); } } throw new Exception("Move not found"); }
/// <summary> /// Attempts to find a subnode by following specified moves from root. /// </summary> /// <param name="priorRoot"></param> /// <param name="movesMade"></param> /// <returns></returns> static MCTSNode FollowMovesToNode(MCTSNode priorRoot, IEnumerable <MGMove> movesMade) { PositionWithHistory startingPriorMove = priorRoot.Context.StartPosAndPriorMoves; MGPosition position = startingPriorMove.FinalPosMG; MCTSIterator context = priorRoot.Context; // Advance root node and update prior moves MCTSNode newRoot = priorRoot; foreach (MGMove moveMade in movesMade) { bool foundChild = false; // Find this new root node (after these moves) foreach (MCTSNodeStructChild child in newRoot.Ref.Children) { if (child.IsExpanded) { MGMove thisChildMove = ConverterMGMoveEncodedMove.EncodedMoveToMGChessMove(child.Move, in position); if (thisChildMove == moveMade) { // Advance new root to reflect this move newRoot = context.Tree.GetNode(child.ChildIndex, newRoot); // Advance position position.MakeMove(thisChildMove); // Done looking for match foundChild = true; break; } } } if (!foundChild) { return(null); } } // Found it return(newRoot); }
/// <summary> /// Returns the position at a specified index. /// </summary> /// <param name="index"></param> /// <returns></returns> public PositionWithHistory GetAtIndex(int index) { if (moveSequences != null) { return(moveSequences[index]); } if (fensAndMoves != null) { string fenAndMoves = fensAndMoves[index]; string[] parts = fenAndMoves.Split(" "); string fen = null; string moves = null; if (fenAndMoves.Contains("moves")) { int indexMoves = fenAndMoves.IndexOf("moves"); fen = fenAndMoves.Substring(0, indexMoves); moves = fenAndMoves.Substring(indexMoves + 6); } else { fen = fenAndMoves; } return(PositionWithHistory.FromFENAndMovesUCI(fen, moves)); } else if (openings != null) { PGNGame game = openings[index]; return(game.Moves); } else if (epds != null) { return(new PositionWithHistory(MGPosition.FromFEN(epds[index].FEN))); } else if (numGamesFromStartPosition > 0) { numGamesFromStartPosition--; return(PositionWithHistory.StartPosition); } else { throw new Exception("PositionsWithHistory exhausted set of game start positions."); } }
/// <summary> /// Generates the set of moves and positions possible as the next move in a position. /// </summary> /// <param name="startPos"></param> /// <param name="moveToIncludeFilter"></param> /// <returns></returns> public static IEnumerable <(MGMove, Position)> GenPositions(Position startPos, Predicate <MGMove> moveToIncludeFilter = null) { MGPosition posMG = MGChessPositionConverter.MGChessPositionFromFEN(startPos.FEN); // TODO: more efficient? MGMoveList moves = new MGMoveList(); MGMoveGen.GenerateMoves(in posMG, moves); for (int i = 0; i < moves.NumMovesUsed; i++) { // Only consider captures (would reduce number of pieces into range of tablebase) if (moveToIncludeFilter == null || moveToIncludeFilter(moves.MovesArray[i])) { // Make this move and get new Position MGPosition newPosMG = new MGPosition(posMG); newPosMG.MakeMove(moves.MovesArray[i]); Position newPos = MGChessPositionConverter.PositionFromMGChessPosition(in newPosMG); yield return(moves.MovesArray[i], newPos); } } }
public IEncodedPositionBatchFlat GetSubBatchCopied(int startIndex, int count) { float[] w = null; float[] l = null; if (W != null) { w = new float[count]; Array.Copy(W, startIndex, w, 0, count); } if (L != null) { l = new float[count]; Array.Copy(L, startIndex, l, 0, count); } byte[] posPlaneValuesEncoded = new byte[count * EncodedPositionWithHistory.NUM_PLANES_TOTAL]; Array.Copy(PosPlaneValues, startIndex * EncodedPositionWithHistory.NUM_PLANES_TOTAL, posPlaneValuesEncoded, 0, count * EncodedPositionWithHistory.NUM_PLANES_TOTAL); ulong[] posPlaneBitmaps = new ulong[count * EncodedPositionWithHistory.NUM_PLANES_TOTAL]; Array.Copy(PosPlaneBitmaps, startIndex * EncodedPositionWithHistory.NUM_PLANES_TOTAL, posPlaneBitmaps, 0, count * EncodedPositionWithHistory.NUM_PLANES_TOTAL); EncodedPositionBatchFlat ret = new EncodedPositionBatchFlat(posPlaneBitmaps, posPlaneValuesEncoded, w, l, null, count); if (Positions != null) { ulong[] hashes = new ulong[count]; MGPosition[] positionsMG = new MGPosition[count]; MGMoveList[] moves = new MGMoveList[count]; Array.Copy(PositionHashes, startIndex, hashes, 0, count); Array.Copy(Positions, startIndex, positionsMG, 0, count); Array.Copy(Moves, startIndex, moves, 0, count); ret.PositionHashes = hashes; ret.Positions = positionsMG; ret.Moves = moves; } return(ret); }
public PositionWithHistory(MGPosition initialPosMG, List <MGMove> moves = null) { InitialPosMG = initialPosMG; Moves = moves ?? new List <MGMove>(); }
/// <summary> /// /// </summary> /// <param name="movesMade"></param> /// <param name="thresholdFractionNodesRetained"></param> /// <returns></returns> public bool ResetRootAssumingMovesMade(IEnumerable <MGMove> movesMade, float thresholdFractionNodesRetained) { PositionWithHistory staringPriorMove = Nodes.PriorMoves; MGPosition position = Nodes.PriorMoves.FinalPosMG; ref MCTSNodeStruct priorRoot = ref RootNode;
public static MGMove ToMGMove(Position position, EncodedMove encodedMove) => ToMGMove(MGPosition.FromPosition(position), encodedMove);
public PositionWithHistory(PositionWithHistory copy) { InitialPosMG = copy.InitialPosMG; Moves = new List <MGMove>(copy.Moves); }
public PositionWithHistory(Position initialPos, List <MGMove> moves = null) { InitialPosMG = MGChessPositionConverter.MCChessPositionFromPosition(in initialPos); Moves = moves ?? new List <MGMove>(); }
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); } } PositionWithHistory pos = PositionWithHistory.FromFENAndMovesSAN(epdToUse.FEN, epdToUse.StartMoves); // TODO: should this be switched to GameEngineCeresInProcess? // 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). MCTSearch search1 = null; MCTSearch search2 = null; if (epdNum % 2 == 0 || Def.CeresEngine2Def == null) { search1 = new MCTSearch(); search1.Search(evaluatorSet1, Def.Engine1Def.SelectParams, Def.Engine1Def.SearchParams, null, null, null, pos, ceresSearchLimit1, false, DateTime.Now, null, null, true); MCTSIterator shareContext = null; if (Def.RunCeres2Engine) { if (Def.Engine2Def.SearchParams.ReusePositionEvaluationsFromOtherTree) { shareContext = search1.Manager.Context; } search2 = new MCTSearch(); search2.Search(evaluatorSet2, Def.Engine2Def.SelectParams, Def.Engine2Def.SearchParams, null, null, shareContext, pos, ceresSearchLimit2, false, DateTime.Now, null, null, true); } } else { search2 = new MCTSearch(); search2.Search(evaluatorSet2, Def.Engine2Def.SelectParams, Def.Engine2Def.SearchParams, null, null, null, pos, ceresSearchLimit2, false, DateTime.Now, null, null, true); MCTSIterator shareContext = null; if (Def.Engine1Def.SearchParams.ReusePositionEvaluationsFromOtherTree) { shareContext = search2.Manager.Context; } search1 = new MCTSearch(); search1.Search(evaluatorSet1, Def.Engine1Def.SelectParams, Def.Engine1Def.SearchParams, null, null, shareContext, pos, ceresSearchLimit1, false, DateTime.Now, null, null, true); } // 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(search1.BestMove); Move bestMoveCeres2 = search2 == null ? default : MGMoveConverter.ToMove(search2.BestMove); 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(search1.Manager, search1.BestMove); SearchResultInfo result2 = search2 == null ? null : new SearchResultInfo(search2.Manager, search2.BestMove); 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 && (search2 == null || scoreCeres2 > 0)) { accWCeres1 += (scoreCeres1 == 0) ? result1.N : result1.NumNodesWhenChoseTopNNode; if (search2 != 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 = search2 == null ? 0 : result2.NumNNBatches; int evalNumPos2 = search2 == 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)search1.TimingInfo.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)search2.TimingInfo.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 = search2 != 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", $"{search1.Manager.BestMoveMG,7}", 9); if (c2) { writer.Add("MC2", $"{search2.Manager.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", $"{search1.TimingInfo.ElapsedTimeSecs,7:F2}", 9); if (c2) { writer.Add("TimeC2", $"{search2.TimingInfo.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", $"{(search1.CountSearchContinuations > 0 ? 0 : search1.Manager.CountTablebaseHits),8:N0}", 10); if (c2) { writer.Add("TBase2", $"{(search2.CountSearchContinuations > 0 ? 0 : search2.Manager.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"); search1?.Manager?.Dispose(); if (!object.ReferenceEquals(search1?.Manager, search2?.Manager)) { search2?.Manager?.Dispose(); } }