protected override LeafEvaluationResult DoTryEvaluate(MCTSNode node) => default;
/// <summary> /// Initializes a specified EncodedPosition to reflect the a specified node's position. /// </summary> /// <param name="node"></param> /// <param name="boardsHistory"></param> internal unsafe void CalcRawPosition(MCTSNode node, ref EncodedPositionWithHistory boardsHistory) { Span <EncodedPositionBoard> destBoards = new Span <EncodedPositionBoard>(Unsafe.AsPointer(ref boardsHistory), EncodedPositionBoards.NUM_MOVES_HISTORY); // Now we fill in the history boards, extracted from up to 4 distinct places: // 1. the current node's board (always) // 2. possibly sequence of boards by ascending tree toward root // 3. possibly sequence of boards coming from the prior history that // this search was launched with (nodes before the root) // 4. possibly one or more fill-in boards, which can be // either zero (if history fill-in feature turned off) // otherwise the last actual board in the history repeated destBoards[0] = LC0BoardPosition; MCTSNode priorNode = node.Parent; // Ascend in tree copying positions from ancestors int nextBoardIndex = 1; while (nextBoardIndex < EncodedPositionBoards.NUM_MOVES_HISTORY && priorNode != null) { if (nextBoardIndex % 2 == 1) { destBoards[nextBoardIndex++] = priorNode.Annotation.LC0BoardPosition.ReversedAndFlipped; } else { destBoards[nextBoardIndex++] = priorNode.Annotation.LC0BoardPosition; } priorNode = priorNode.Parent; } // Boards from prior history int priorPositionsIndex = 0; while (nextBoardIndex < EncodedPositionBoards.NUM_MOVES_HISTORY && priorPositionsIndex < node.Tree.EncodedPriorPositions.Count) { if (nextBoardIndex % 2 == 1) { destBoards[nextBoardIndex++] = node.Tree.EncodedPriorPositions[priorPositionsIndex].ReversedAndFlipped; } else { destBoards[nextBoardIndex++] = node.Tree.EncodedPriorPositions[priorPositionsIndex]; } priorPositionsIndex++; } // Finally, set last boards either with repeated last position (if fill in) or zeros int indexBoardToRepeat = nextBoardIndex - 1; bool historyFillIn = node.Context.ParamsSearch.HistoryFillIn; while (nextBoardIndex < EncodedPositionBoards.NUM_MOVES_HISTORY) { if (historyFillIn) { destBoards[nextBoardIndex++] = destBoards[indexBoardToRepeat]; } else { destBoards[nextBoardIndex++] = default; } } boardsHistory.SetMiscInfo(new EncodedTrainingPositionMiscInfo(MiscInfo, default)); }
internal void DoVisitLeafNode(MCTSNode node, int numVisits) { ref MCTSNodeStruct nodeRef = ref node.Ref;
static void DumpNodeStr(PositionWithHistory priorMoves, MCTSNode node, int depth, int countTimesSeen, bool fullDetail) { node.Context.Tree.Annotate(node); char extraChar = ' '; if (node.Terminal == GameResult.Checkmate) { extraChar = 'C'; } else if (node.Terminal == GameResult.Draw) { extraChar = 'D'; } else if (countTimesSeen > 1) { extraChar = countTimesSeen > 9 ? '9' : countTimesSeen.ToString()[0]; } float multiplier = depth % 2 == 0 ? 1.0f : -1.0f; float pctOfVisits = node.IsRoot ? 100.0f : (100.0f * node.N / node.Parent.N); MCTSNode bestMove = null; // TODO: someday show this too MCTSNode nextBestMove = null; if (!node.IsRoot) { MCTSNode[] parentsChildrenSortedQ = node.ChildrenSorted(innerNode => - multiplier * (float)innerNode.Q); if (parentsChildrenSortedQ.Length > 0) { bestMove = parentsChildrenSortedQ[0]; } // if (parentsChildrenSortedQ.Length > 1) nextBestMove = parentsChildrenSortedQ[1]; } // Depth, move Console.Write($"{depth,3}. "); Console.Write(extraChar); Console.Write($" {node.NumPolicyMoves,3} "); Console.Write($"{node.Index,13:N0}"); string san = node.IsRoot ? "" : MGMoveConverter.ToMove(node.Annotation.PriorMoveMG).ToSAN(in node.Parent.Annotation.Pos); // string sanNextBest = node.IsRoot ? "" : MGMoveConverter.ToMove(nextBestMove.Annotation.PriorMoveMG).ToSAN(in node.Parent.Annotation.Pos); if (node.Annotation.Pos.MiscInfo.SideToMove == SideType.White) { Console.Write($" "); Console.Write($"{san,6}"); } else { Console.Write($"{san,6}"); Console.Write($" "); } // float diffBestNextBestQ = 0; // if (nextBestMove != null) diffBestNextBestQ = (float)(bestMove.Q - nextBestMove.Q); // Console.Write($"{ (nextBestMove?.Annotation == null ? "" : nextBestMove.Annotation.PriorMoveMG.ToString()),8}"); // Console.Write($"{diffBestNextBestQ,8:F2}"); Console.Write($"{node.N,13:N0} "); Console.Write($" {pctOfVisits,5:F0}%"); Console.Write($" {100.0 * node.P,6:F2}% "); DumpWithColor(multiplier * node.V, $" {multiplier * node.V,6:F3} ", -0.2f, 0.2f); // DumpWithColor(multiplier * node.VSecondary, $" {multiplier * node.VSecondary,6:F3} ", -0.2f, 0.2f); double q = multiplier * node.Q; DumpWithColor((float)q, $" {q,6:F3} ", -0.2f, 0.2f); // float qStdDev = MathF.Sqrt(node.Ref.VVariance); // if (float.IsNaN(qStdDev)) // Console.WriteLine("found negative var"); // Console.Write($" +/-{qStdDev,5:F2} "); Console.Write($" {node.WinP,5:F2}/{node.DrawP,5:F2}/{node.LossP,5:F2} "); Console.Write($" {node.WAvg,5:F2}/{node.DAvg,5:F2}/{node.LAvg,5:F2} "); // Console.Write($" {node.Ref.QUpdatesWtdAvg,5:F2} "); // Console.Write($" +/-:{MathF.Sqrt(node.Ref.QUpdatesWtdVariance),5:F2} "); // Console.Write($" {node.Ref.TrendBonusToP,5:F2} "); Console.Write($" {node.MPosition,3:F0} "); Console.Write($" {node.MAvg,3:F0} "); if (fullDetail) { int numPieces = node.Annotation.Pos.PieceCount; // Console.Write($" {PosStr(node.Annotation.Pos)} "); Console.Write($" {node.Annotation.Pos.FEN}"); } Console.WriteLine(); }
int NInFlightThisSelector(MCTSNode node) => SelectorID == 0 ? node.NInFlight : node.NInFlight2;
/// <summary> /// Takes any actions necessary upon visit to an inner note. /// </summary> /// <param name="node"></param> void DoVisitInnerNode(MCTSNode node) { InsureAnnotated(node); }
private async Task MCTSRoutine() { System.Action <string> print = (s) => Logger.Log(s); MOARandom r = new MOARandom(); IActor playerOne = null; IGameState game = null; switch (_testingGame) { case TestingGame.OX: playerOne = new OXActor(_startPlayerOne ? _playerTwo.Name : _playerOne.Name); game = new OXGameState(_playerOne.Name, _playerTwo.Name, playerOne as OXActor); break; case TestingGame.C4: playerOne = new C4Actor(_startPlayerOne ? _playerTwo.Name : _playerOne.Name); game = new C4GameState(_playerOne.Name, _playerTwo.Name, playerOne as C4Actor); break; case TestingGame.Pentagoesque: playerOne = new PentActor(_startPlayerOne ? _playerTwo.Name : _playerOne.Name); game = new PentGameState(_playerOne.Name, _playerTwo.Name, playerOne as PentActor); break; default: break; } _playerOne.Config = new MCTSConfig { MaxIterations = (uint)_playerOne.Iterations, Verbose = verbose, PrintFn = print, UCTK = uctExploration }; switch (_playerOne.Behaviour) { case PlayerBehaviour.MCTS: _playerOne.TreeRoot = new MCTSNode(0, null, null, game.ActorJustActed, game); break; case PlayerBehaviour.UCT: _playerOne.TreeRoot = new UCTNode(0, null, null, game.ActorJustActed, game, _playerOne.Config.UCTK); break; } _playerTwo.Config = new MCTSConfig { MaxIterations = (uint)_playerTwo.Iterations, Verbose = verbose, PrintFn = print, UCTK = uctExploration }; switch (_playerTwo.Behaviour) { case PlayerBehaviour.MCTS: _playerTwo.TreeRoot = new MCTSNode(0, null, null, game.ActorJustActed, game); break; case PlayerBehaviour.UCT: _playerTwo.TreeRoot = new UCTNode(0, null, null, game.ActorJustActed, game, _playerTwo.Config.UCTK); break; } while (!game.IsTerminal() && Application.isPlaying) { IAction action = null; Player actingPlayer = null; Player otherPlayer = null; if (game.ActorJustActed.Name == _playerOne.Name) { actingPlayer = _playerTwo; otherPlayer = _playerOne; } else { actingPlayer = _playerOne; otherPlayer = _playerTwo; } Logger.Log(GetType().Name, $"Player Acting: {actingPlayer.Name}, Behaviour: {actingPlayer.Behaviour}"); MCTSNode node = null; switch (actingPlayer.Behaviour) { case PlayerBehaviour.Random: action = game.GetRandomMove(); break; case PlayerBehaviour.MCTS: node = await MCTS.ProcessTree( actingPlayer.RetainTree?actingPlayer.TreeRoot : new MCTSNode(0, null, null, game.ActorJustActed, game), game, actingPlayer.Config ); action = node.IncomingAction; actingPlayer.TreeRoot = node.ConstructAsRoot(); break; case PlayerBehaviour.UCT: node = await MCTS.ProcessTree( actingPlayer.RetainTree?actingPlayer.TreeRoot : new UCTNode(0, null, null, game.ActorJustActed, game, actingPlayer.Config.UCTK), game, actingPlayer.Config ); action = node.IncomingAction; actingPlayer.TreeRoot = node.ConstructAsRoot(); break; default: break; } if (action != null) { game = action.DoAction(); Logger.Log(action.ASCIIRepresentation); otherPlayer.TreeRoot = otherPlayer.TreeRoot?.GetChildWithState(game)?.ConstructAsRoot(); } else { Logger.Log("STALLING!!"); break; } Logger.Log(game.ASCIIRepresentation); if (waitTime != 0f) { await Task.Delay((int)(waitTime * 1000)); } } Logger.LogFormat("{0} {1}", game.ActorJustActed.Name, game.GetResult(game.ActorJustActed)); }
/// <summary> /// Update the root node /// </summary> /// <param name="spot"></param> public override void UpdateLastMove(Spot spot) { root = root.NextRoot(spot); }
public MonteCarloAI(GlobalGame game, int turn, Color color, Sprite sprite, string name, float time) : base(game, turn, color, sprite, name) { root = new MCTSNode(game, null, null); this.time = Math.Max(float.Epsilon, time); }
/// <summary> /// Worker method that actually computes and updates the statistic. /// </summary> private void DoSetEarlyStopMoveSecondaryFlags() { if (Manager.Root.NumChildrenExpanded == 0) { return; } float aggressiveness = Context.ParamsSearch.MoveFutilityPruningAggressiveness; if (aggressiveness >= 1.5f) { throw new Exception("Ceres configuration error: maximum value of EarlyStopMoveSecondaryAggressiveness is 1.5."); } float MIN_BEST_N_FRAC_REQUIRED = ManagerChooseRootMove.MIN_FRAC_N_REQUIRED_MIN; // Calibrate aggressiveness such that : // - at maximum value of 1.0 we assume 50% visits go to second best move(s) // - at reasonable default value 0.5 we assume 75% of visits to go second best move(s) float aggressivenessMultiplier = 1.0f / (1.0f - aggressiveness * 0.5f); int?numRemainingSteps = Manager.EstimatedNumVisitsRemaining(); // Can't make any determiniation if we can't estimate how many steps left if (numRemainingSteps is null) { return; } int numStepsTotal = (int)(numRemainingSteps / Manager.FractionSearchRemaining); MCTSNode[] nodesSortedN = Root.ChildrenSorted(n => - n.N); MCTSNode bestNNode = nodesSortedN[0]; int minN = Root.N / 5;// (int)(bestQNode.N * MIN_BEST_N_FRAC_REQUIRED); MCTSNode[] nodesSortedQ = Root.ChildrenSorted(n => n.N < minN ? int.MaxValue : (float)n.Q); MCTSNode bestQNode = nodesSortedQ[0]; float bestQ = (float)bestQNode.Q; ManagerChooseRootMove bestMoveChoser = new(Context.Root, false, Context.ParamsSearch.MLHBonusFactor); Span <MCTSNodeStructChild> children = Root.Ref.Children; for (int i = 0; i < Root.Ref.NumChildrenExpanded; i++) { // Never shut down second best move unless the whole search is eligible to shut down if (nodesSortedN.Length > 1) { MCTSNode secondBestMove = Context.ParamsSearch.BestMoveMode == ParamsSearch.BestMoveModeEnum.TopN ? nodesSortedN[1] : nodesSortedQ[1]; bool isSecondBestMove = children[i].Move == secondBestMove.PriorMove; if (isSecondBestMove && !Context.ParamsSearch.FutilityPruningStopSearchEnabled && Context.RootMovesPruningStatus[i] != MCTSFutilityPruningStatus.PrunedDueToSearchMoves) { Context.RootMovesPruningStatus[i] = MCTSFutilityPruningStatus.NotPruned; continue; } } float earlyStopGapRaw; if (Context.ParamsSearch.BestMoveMode == ParamsSearch.BestMoveModeEnum.TopN) { earlyStopGapRaw = nodesSortedN[0].N - children[i].N; } else { int minNRequired = (int)(bestQNode.N * MIN_BEST_N_FRAC_REQUIRED); earlyStopGapRaw = minNRequired - children[i].N; } float earlyStopGapAdjusted = earlyStopGapRaw * aggressivenessMultiplier; bool earlyStop = earlyStopGapAdjusted > numRemainingSteps; if (Context.RootMovesPruningStatus[i] == MCTSFutilityPruningStatus.NotPruned && earlyStop) { #if NOT_HELPFUL // Never shutdown nodes getting large fraction of all visits float fracVisitsThisMoveRunningAverage = Context.RootMoveTracker != null ? Context.RootMoveTracker.RunningFractionVisits[i] : 0; const float THRESHOLD_VISIT_FRACTION_DO_NOT_SHUTDOWN_CHILD = 0.20f; bool shouldVeto = (fracVisitsThisMoveRunningAverage > THRESHOLD_VISIT_FRACTION_DO_NOT_SHUTDOWN_CHILD); if (Context.ParamsSearch.TestFlag && nodesSortedN[0] != Context.Root.ChildAtIndex(i) && NumberOfNotShutdownChildren() > 1 && shouldVeto) { MCTSEventSource.TestCounter1++; continue; } else #endif Context.RootMovesPruningStatus[i] = MCTSFutilityPruningStatus.PrunedDueToFutility; if (MCTSDiagnostics.DumpSearchFutilityShutdown) { Console.WriteLine(); Console.WriteLine($"\r\nShutdown {children[i].Move} [{children[i].N}] at root N {Context.Root.N} with remaning {numRemainingSteps}" + $" due to raw gapN {earlyStopGapRaw} adusted to {earlyStopGapAdjusted} in mode {Context.ParamsSearch.BestMoveMode} aggmult {aggressivenessMultiplier}"); DumpDiagnosticsMoveShutdown(); } } // Console.WriteLine(i + $" EarlyStopMoveSecondary(simple) gap={gapToBest} adjustedGap={inflatedGap} remaining={numRemainingSteps} "); } // TODO: log this //Console.WriteLine($"{Context.RemainingTime,5:F2}sec remains at N={Root.N}, setting minN to {minN} number still considered {count} " + // $"using EstimatedNPS {Context.EstimatedNPS} with nodes remaining {Context.EstimatedNumStepsRemaining()} " + // statsStr); }
/// <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}"); } }