Beispiel #1
0
        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();
            }
        }
Beispiel #2
0
        /// <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));
        }
Beispiel #3
0
        /// <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);
        }
Beispiel #4
0
        // --------------------------------------------------------------------------------------------
        /// <summary>
        /// Converts from MGChessMove.
        /// TO DO: shouldn't this be in an Extension class instead?
        /// </summary>
        /// <param name="mgMove"></param>
        /// <returns></returns>
        public static Move ToMove(MGMove mgMove)
        {
            if (mgMove.CastleShort)
            {
                return(new Move(Move.MoveType.MoveCastleShort));
            }
            if (mgMove.CastleLong)
            {
                return(new Move(Move.MoveType.MoveCastleLong));
            }

            PieceType promoPiece = PieceType.None;

            if (mgMove.PromoteQueen)
            {
                promoPiece = PieceType.Queen;
            }
            else if (mgMove.PromoteRook)
            {
                promoPiece = PieceType.Rook;
            }
            else if (mgMove.PromoteBishop)
            {
                promoPiece = PieceType.Bishop;
            }
            else if (mgMove.PromoteKnight)
            {
                promoPiece = PieceType.Knight;
            }

            Square fromSquare = new Square((int)mgMove.FromSquareIndex, Square.SquareIndexType.BottomToTopRightToLeft);
            Square toSquare   = new Square((int)mgMove.ToSquareIndex, Square.SquareIndexType.BottomToTopRightToLeft);

            return(new Move(fromSquare, toSquare, promoPiece));
        }
Beispiel #5
0
 public void AppendMove(MGMove move)
 {
     Moves.Add(move);
     if (haveFinalized)
     {
         InitPositionsAndFinalPosMG();
     }
 }
Beispiel #6
0
        /// <summary>
        ///
        /// </summary>
        /// <param name="node"></param>
        /// <param name="parentAnnotation">optionally a precomputed parent annotation (otherwise computed)</param>
        /// <returns></returns>
        public unsafe void Annotate(MCTSNode node)
        {
            if (node.Annotation.IsInitialized)
            {
                return;
            }

            NumAnnotations++;

            // Get the position corresponding to this node
            MGPosition newPos;

            if (!node.IsRoot)
            {
                // Apply move for this node to the prior position
                node.Parent.Annotate();
                newPos = node.Parent.Annotation.PosMG;
                newPos.MakeMove(ConverterMGMoveEncodedMove.EncodedMoveToMGChessMove(node.PriorMove, in newPos, true));
            }
            else
            {
                newPos = PriorMoves.FinalPosMG;
            }

            MGMove priorMoveMG = default;

            if (!node.IsRoot)
            {
                priorMoveMG = ConverterMGMoveEncodedMove.EncodedMoveToMGChessMove(node.PriorMove, in node.Parent.Annotation.PosMG, true);
            }

            bool isRoot = node.IsRoot;

            Position newPosAsPos = newPos.ToPosition;

            // Create history, with prepended move representing this move
            Span <Position> posHistory = GetPriorHistoryPositions(node.Parent, in newPosAsPos, PosScratchBuffer,
                                                                  node.Context.ParamsSearch.DrawByRepetitionLookbackPlies, true);

            // Determine the set (possibly a subset) of positions over which to compute hash
            Span <Position> posHistoryForCaching  = posHistory;
            int             numCacheHashPositions = node.Context.EvaluatorDef.NumCacheHashPositions;

            if (posHistory.Length > numCacheHashPositions)
            {
                posHistoryForCaching = posHistory.Slice(posHistory.Length - numCacheHashPositions, numCacheHashPositions);
            }

            // Compute the actual hash
            ulong zobristHashForCaching = EncodedBoardZobrist.ZobristHash(posHistoryForCaching, node.Context.EvaluatorDef.HashMode);

            node.LastAccessedSequenceCounter = node.Context.Tree.SEQUENCE_COUNTER++;
            node.Annotation.PriorMoveMG      = priorMoveMG;

            node.Annotation.Pos   = posHistory[^ 1]; // this will have had its repetition count set
Beispiel #7
0
        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);
            }
        }
Beispiel #8
0
        internal static int FindMoveIndex(MGMove[] moves, MGMove move, int startIndex, int numMovesUsed)
        {
            byte moveToSquare = move.ToSquareIndex;

            for (int i = startIndex; i < numMovesUsed; i++)
            {
                if (moves[i].ToSquareIndex == moveToSquare)
                {
                    if (moves[i] == move)
                    {
                        return(i);
                    }
                }
            }
            return(-1);
        }
Beispiel #9
0
        /// <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);
        }
Beispiel #10
0
        public SuiteTestResultItem(MCTSManager manager, MGMove bestMove)
        {
            using (new SearchContextExecutionBlock(manager.Context))
            {
                if (manager.TablebaseImmediateBestMove != default(MGMove))
                {
                    Q                    = 1.0;
                    BestMove             = manager.TablebaseImmediateBestMove;
                    PickedNonTopNMoveStr = " ";
                }
                else
                {
                    Q             = manager.Root.Q;
                    UCIInfoString = UCIManager.UCIInfoString(manager);

                    // SearchPrincipalVariation pv1 = new SearchPrincipalVariation(worker1.Root);
                    BestMove = bestMove;
                    N        = manager.Context.Root.N;
                    NumNodesWhenChoseTopNNode = manager.NumNodesWhenChoseTopNNode;
                    NumNNBatches = manager.Context.NumNNBatches;
                    NumNNNodes   = manager.Context.NumNNNodes;

                    TopNNodeN = manager.TopNNode.N;
                    FractionNumNodesWhenChoseTopNNode = manager.FractionNumNodesWhenChoseTopNNode;
                    AvgDepth = manager.Context.AvgDepth;
                    MAvg     = manager.Context.Root.MAvg;
                    NodeSelectionYieldFrac = manager.Context.NodeSelectionYieldFrac;

                    // TODO: try removing this, clean up
                    try
                    {
                        PickedNonTopNMoveStr = !manager.Root.BestMove(false).Equals(manager.Root.ChildWithLargestValue(node => node.N)) ? "!" : " ";
                    }
                    catch (Exception excp)
                    {
                        // TODO: resolve this
                        PickedNonTopNMoveStr = "?";
                    }
                }
            }
        }
Beispiel #11
0
        /// <summary>
        /// Writes extensive descriptive information to a specified TextWriter,
        /// including verbose move statistics, principal variation, and move timing information.
        /// </summary>
        /// <param name="searchRootNode"></param>
        /// <param name="writer"></param>
        /// <param name="description"></param>
        public void DumpFullInfo(MGMove bestMove, MCTSNode searchRootNode = null, TextWriter writer = null, string description = null)
        {
            searchRootNode = searchRootNode ?? Root;
            writer         = writer ?? Console.Out;

            int moveIndex = searchRootNode.Tree.Store.Nodes.PriorMoves.Moves.Count;

            writer.WriteLine();
            writer.WriteLine("=================================================================================");
            writer.Write(DateTime.Now + " SEARCH RESULT INFORMATION,  Move = " + ((1 + moveIndex / 2)));
            writer.WriteLine($" Thread = {Thread.CurrentThread.ManagedThreadId}");
            if (description != null)
            {
                writer.WriteLine(description);
            }
            writer.WriteLine();

            writer.WriteLine("Tree root           : " + Context.Root);
            if (searchRootNode != Root)
            {
                writer.WriteLine("Search root         : " + searchRootNode);
            }
            writer.WriteLine();

            MCTSNode[] nodesSortedN = null;
            MCTSNode[] nodesSortedQ = null;

            string bestMoveInfo = "";

            if (searchRootNode.NumChildrenExpanded > 0 &&
                StopStatus != SearchStopStatus.TablebaseImmediateMove &&
                StopStatus != SearchStopStatus.OnlyOneLegalMove)
            {
                MCTSNode[] childrenSortedN = searchRootNode.ChildrenSorted(node => - node.N);
                MCTSNode[] childrenSortedQ = searchRootNode.ChildrenSorted(node => (float)node.Q);
                bool       isTopN          = childrenSortedN[0].Annotation.PriorMoveMG == bestMove;
                bool       isTopQ          = childrenSortedQ[0].Annotation.PriorMoveMG == bestMove;
                if (isTopN && isTopQ)
                {
                    bestMoveInfo = "(TopN and TopQ)";
                }
                else if (isTopN)
                {
                    bestMoveInfo = "(TopN)";
                }
                else if (isTopQ)
                {
                    bestMoveInfo = "(TopQ)";
                }
            }

            // Output position (with history) information.
            writer.WriteLine("Position            : " + searchRootNode.Annotation.Pos.FEN);
            writer.WriteLine("Tree root position  : " + Context.Tree.Store.Nodes.PriorMoves);
            writer.WriteLine("Search stop status  : " + StopStatus);
            writer.WriteLine("Best move selected  : " + bestMove.MoveStr(MGMoveNotationStyle.LC0Coordinate) + " " + bestMoveInfo);
            writer.WriteLine();

            using (new SearchContextExecutionBlock(Context))
            {
                string infoUpdate = UCIInfo.UCIInfoString(this, searchRootNode);
                writer.WriteLine(infoUpdate);

                writer.WriteLine();
                DumpTimeInfo(writer);

                writer.WriteLine();
                searchRootNode.Dump(1, 1, writer: writer);

                writer.WriteLine();
                MCTSPosTreeNodeDumper.DumpPV(searchRootNode, true, writer);
            }
        }
Beispiel #12
0
        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();
                                  }
        }
        /// <summary>
        /// Overriden virtual method which executes search.
        /// </summary>
        /// <param name="curPositionAndMoves"></param>
        /// <param name="searchLimit"></param>
        /// <param name="gameMoveHistory"></param>
        /// <param name="callback"></param>
        /// <returns></returns>
        protected override GameEngineSearchResult DoSearch(PositionWithHistory curPositionAndMoves,
                                                           SearchLimit searchLimit,
                                                           List <GameMoveStat> gameMoveHistory,
                                                           ProgressCallback callback,
                                                           bool verbose)
        {
            if (LastSearch != null && curPositionAndMoves.InitialPosMG != LastSearch.Manager.Context.StartPosAndPriorMoves.InitialPosMG)
            {
                throw new Exception("ResetGame must be called if not continuing same line");
            }

            MCTSearch searchResult;

            // Set up callback passthrough if provided
            MCTSManager.MCTSProgressCallback callbackMCTS = null;
            if (callback != null)
            {
                callbackMCTS = callbackContext => callback((MCTSManager)callbackContext);
            }

            // Possibly use the context of opponent to reuse position evaluations
            MCTSIterator shareContext = null;

            if (OpponentEngine is GameEngineCeresInProcess)
            {
                GameEngineCeresInProcess ceresOpponentEngine = OpponentEngine as GameEngineCeresInProcess;

                if (LastSearch is not null &&
                    LastSearch.Manager.Context.ParamsSearch.ReusePositionEvaluationsFromOtherTree &&
                    ceresOpponentEngine?.LastSearch.Manager != null &&
                    LeafEvaluatorReuseOtherTree.ContextsCompatibleForReuse(LastSearch.Manager.Context, ceresOpponentEngine.LastSearch.Manager.Context))
                {
                    shareContext = ceresOpponentEngine.LastSearch.Manager.Context;

                    // Clear any prior shared context from the shared context
                    // to prevent unlimited backward chaining (keeping unneeded prior contexts alive)
                    shareContext.ClearSharedContext();
                }
            }

            void InnerCallback(MCTSManager manager)
            {
                callbackMCTS?.Invoke(manager);
            }

            // Run the search
            searchResult = RunSearchPossiblyTreeReuse(shareContext, curPositionAndMoves, gameMoveHistory,
                                                      searchLimit, InnerCallback, verbose);

            int scoreCeresCP = (int)Math.Round(EncodedEvalLogistic.LogisticToCentipawn((float)searchResult.Manager.Root.Q), 0);

            MGMove bestMoveMG = searchResult.BestMove;

            int N = (int)searchResult.SearchRootNode.N;

            // Save (do not dispose) last search in case we can reuse it next time
            LastSearch = searchResult;

            isFirstMoveOfGame = false;

            // TODO is the RootNWhenSearchStarted correct because we may be following a continuation (BestMoveRoot)
            GameEngineSearchResultCeres result =
                new GameEngineSearchResultCeres(bestMoveMG.MoveStr(MGMoveNotationStyle.LC0Coordinate),
                                                (float)searchResult.SearchRootNode.Q, scoreCeresCP, searchResult.SearchRootNode.MAvg, searchResult.Manager.SearchLimit, default,