Exemple #1
0
        /// <summary>
        /// Returns limits to be used for the special case of first move of a game
        /// (more time is allocated because there is no tree use available).
        /// </summary>
        /// <param name="inputs"></param>
        /// <param name="estNumMovesLeftHard"></param>
        /// <returns></returns>
        ManagerGameLimitOutputs MoveTimeForFirstMove(ManagerGameLimitInputs inputs, float estNumMovesLeftHard)
        {
            float fractionFirstMove = 0.05f;
            float targetUnits       = inputs.RemainingFixedSelf * fractionFirstMove;

            return(new ManagerGameLimitOutputs(new SearchLimit(inputs.TargetLimitType, targetUnits)));
        }
Exemple #2
0
        /// Determines how much time or nodes resource to
        /// allocate to the the current move in a game subject to
        /// a limit on total numbrer of time or nodes over
        /// some number of moves (or possibly all moves).
        public ManagerGameLimitOutputs ComputeMoveAllocation(ManagerGameLimitInputs inputs)
        {
            if (inputs.MaxMovesToGo.HasValue && inputs.MaxMovesToGo < 2)
            {
                return(new ManagerGameLimitOutputs(new SearchLimit(inputs.TargetLimitType,
                                                                   inputs.RemainingFixedSelf * 0.99f)));
            }

            ManagerGameLimitOutputs outputs = DoComputeMoveTime(inputs);

            return(outputs);
        }
Exemple #3
0
        /// Determines how much time or nodes resource to
        /// allocate to the the current move in a game subject to
        /// a limit on total numbrer of time or nodes over
        /// some number of moves (or possibly all moves).
        public ManagerGameLimitOutputs ComputeMoveAllocation(ManagerGameLimitInputs inputs)
        {
            if (inputs.MaxMovesToGo.HasValue && inputs.MaxMovesToGo < 2)
            {
                return(new ManagerGameLimitOutputs(new SearchLimit(inputs.TargetLimitType,
                                                                   inputs.RemainingFixedSelf * 0.99f)));
            }

            return(new ManagerGameLimitOutputs(new SearchLimit(inputs.TargetLimitType,
                                                               inputs.RemainingFixedSelf * FRACTION_PER_MOVE +
                                                               inputs.IncrementSelf)));
        }
Exemple #4
0
        float EstimatedNumDifficultMovesToGo(ManagerGameLimitInputs inputs)
        {
            int numPieces = inputs.StartPos.PieceCount;

            // On estimate of the number of moves left comes from the piece count.
            // This method is likely imprecise, but is always available.
            // We subtract 6 from number of pieces since game is mostly or completely
            // over when 6 pieces are recahed (either via tablebases or very simple play).
            float estNumMovesLeftPieces = Math.Max(5, (numPieces - 6) * 2);

#if NOT
            // Attempts at using MLH were not immediately successful.
            float newM = inputs.Manager.Root.MAvg;

            // As a second estimate, use the output of the moves left head estimate
            float estNumMovesLeftMLH = (newM / 2);
            if (estNumMovesLeftMLH > 50)
            {
                // Shrink outlier very large estimates (more than 50 moves to go)
                estNumMovesLeftMLH = 50 + MathF.Sqrt(estNumMovesLeftMLH - 50);
            }
// Compute the aggregate estimate (equal weight combo from both if available)
            //      if (inputs.RootN == 0 || float.IsNaN(estNumMovesLeftMLH))
            //      else
            //        estNumMovesLeft = Stats.Average(estNumMovesLeftPieces, estNumMovesLeftMLH);
#endif

            float estNumMovesLeft = estNumMovesLeftPieces;

            // We assume that not all of these moves will be "hard",
            // e.g. at end of game the moves may become easier
            // due to tablebases or more transpositions available,
            // or narrower search trees due to simpler positions.
            const float MULTIPLIER = 0.65f;

            if (inputs.MaxMovesToGo.HasValue)
            {
                // Never assume more than 90% of remaining moves are hard
                float maxFromToGo = inputs.MaxMovesToGo.Value * 0.9f;
                float maxDefault  = MULTIPLIER * estNumMovesLeft;
                return(Math.Min(maxDefault, maxFromToGo));
            }
            else
            {
                return(MULTIPLIER * estNumMovesLeft);
            }
        }
Exemple #5
0
        /// <summary>
        /// Main algorithm for determining amount of resources to allocate to this node.
        /// </summary>
        /// <param name="inputs"></param>
        /// <returns></returns>
        ManagerGameLimitOutputs DoComputeMoveTime(ManagerGameLimitInputs inputs)
        {
            // Use more frontloading of early stopping enabled so we don't understoot in time spent
            float frontloadingAggressivemessMult = inputs.SearchParams.FutilityPruningStopSearchEnabled
                                             ? 1.5f
                                             : 1.3f;

            frontloadingAggressivemessMult *= inputs.SearchParams.GameLimitUsageAggressiveness;

            float estNumMovesLeftHard = EstimatedNumDifficultMovesToGo(inputs);

            // Special handling of first move
            if (inputs.IsFirstMoveOfGame)
            {
                return(MoveTimeForFirstMove(inputs, estNumMovesLeftHard));
            }

            // When we are behind then it's worth taking a gamble and using more time
            // but when we are ahead, take a little less time to be sure we don't err in time pressure.
            // Testing suggests this feature is helpful (circa 10 elo?)
            float winningnessMultiplier = inputs.RootQ switch
            {
Exemple #6
0
        /// <summary>
        /// Determines what fraction of the base move should
        /// be consumed for this move.
        /// </summary>
        /// <param name="inputs"></param>
        /// <returns></returns>
        float FractionOfBasePerMoveToUse(ManagerGameLimitInputs inputs)
        {
            float       factorLargeIncrement           = 1.0f;
            const float MAX_LARGE_INCREMENT_MULTIPLIER = 2.0f;

            if (inputs.IncrementSelf > 0)
            {
                float estBasePerMove = inputs.RemainingFixedSelf / 30;
                float incrementRelativeToBasePerMove = inputs.IncrementSelf / estBasePerMove;

                // The more increment relative to base time, the more aggressively the base time is used up
                // (because we don't have to worry about running out of time if game runs to many moves).
                factorLargeIncrement = 1.0f + incrementRelativeToBasePerMove;

                // Don't allow multiplier to be too large, since it ultimately has an exponentially increasing impact.
                factorLargeIncrement = MathF.Min(MAX_LARGE_INCREMENT_MULTIPLIER, factorLargeIncrement);
            }

            // When we are behind then it's worth taking a gamble and using more time
            // but when we are ahead, take a little less time to be sure we don't err in time pressure.
            float factorWinningness = inputs.RootQ switch
            {
Exemple #7
0
        /// <summary>
        /// Determines what fraction of the base move should
        /// be consumed for this move.
        /// </summary>
        /// <param name="inputs"></param>
        /// <returns></returns>
        float FractionOfBasePerMoveToUse(ManagerGameLimitInputs inputs)
        {
            float factorLargeIncrement = 1.0f;

            if (inputs.IncrementSelf > 0)
            {
                // With increasingly significant increment,
                // frontload the use of base time more aggressively
                // because we don't have to worry about
                // "running out of time."
                const float MAX_LARGE_INCREMENT_MULTIPLIER = 2.5f;
                float       estBasePerMove = inputs.RemainingFixedSelf / 30;
                float       incrementRelativeToBasePerMove = inputs.IncrementSelf / estBasePerMove;
                if (incrementRelativeToBasePerMove > 0.2)
                {
                    factorLargeIncrement = 1.0f + 0.5f * ((incrementRelativeToBasePerMove - 0.2f) / 0.3f);
                    factorLargeIncrement = MathF.Min(MAX_LARGE_INCREMENT_MULTIPLIER, factorLargeIncrement);
                }
            }

            // When we are behind then it's worth taking a gamble and using more time
            // but when we are ahead, take a little less time to be sure we don't err in time pressure.
            float factorWinningness = inputs.RootQ switch
            {
Exemple #8
0
        /// <summary>
        ///
        /// </summary>
        /// <param name="store"></param>
        /// <param name="reuseOtherContextForEvaluatedNodes"></param>
        /// <param name="reusePositionCache"></param>
        /// <param name="reuseTranspositionRoots"></param>
        /// <param name="nnEvaluators"></param>
        /// <param name="searchParams"></param>
        /// <param name="childSelectParams"></param>
        /// <param name="searchLimit"></param>
        /// <param name="paramsSearchExecutionPostprocessor"></param>
        /// <param name="limitManager"></param>
        /// <param name="startTime"></param>
        /// <param name="priorManager"></param>
        /// <param name="gameMoveHistory"></param>
        /// <param name="isFirstMoveOfGame"></param>
        public MCTSManager(MCTSNodeStore store,
                           MCTSIterator reuseOtherContextForEvaluatedNodes,
                           PositionEvalCache reusePositionCache,
                           TranspositionRootsDict reuseTranspositionRoots,
                           NNEvaluatorSet nnEvaluators,
                           ParamsSearch searchParams,
                           ParamsSelect childSelectParams,
                           SearchLimit searchLimit,
                           ParamsSearchExecutionModifier paramsSearchExecutionPostprocessor,
                           IManagerGameLimit limitManager,
                           DateTime startTime,
                           MCTSManager priorManager,
                           List <GameMoveStat> gameMoveHistory,
                           bool isFirstMoveOfGame)
        {
            StartTimeThisSearch                = startTime;
            RootNWhenSearchStarted             = store.Nodes.nodes[store.RootIndex.Index].N;
            ParamsSearchExecutionPostprocessor = paramsSearchExecutionPostprocessor;
            IsFirstMoveOfGame = isFirstMoveOfGame;

            PriorMoveStats = new List <GameMoveStat>();

            // Make our own copy of move history.
            if (gameMoveHistory != null)
            {
                PriorMoveStats.AddRange(gameMoveHistory);
            }

            // Possibly convert time limit per game into time for this move.
            if (searchLimit.IsPerGameLimit)
            {
                SearchLimitType type = searchLimit.Type == SearchLimitType.SecondsForAllMoves
                                                       ? SearchLimitType.SecondsPerMove
                                                       : SearchLimitType.NodesPerMove;
                float rootQ = priorManager == null ? float.NaN : (float)store.RootNode.Q;


                limitsManagerInputs = new(store.Nodes.PriorMoves.FinalPosition,
                                          searchParams, PriorMoveStats,
                                          type, store.RootNode.N, rootQ,
                                          searchLimit.Value, searchLimit.ValueIncrement,
                                          float.NaN, float.NaN,
                                          maxMovesToGo : searchLimit.MaxMovesToGo,
                                          isFirstMoveOfGame : isFirstMoveOfGame);

                ManagerGameLimitOutputs timeManagerOutputs = limitManager.ComputeMoveAllocation(limitsManagerInputs);
                SearchLimit = timeManagerOutputs.LimitTarget;
            }
            else
            {
                SearchLimit = searchLimit;
            }

            // Possibly autoselect new optimal parameters
            ParamsSearchExecutionChooser paramsChooser = new ParamsSearchExecutionChooser(nnEvaluators.EvaluatorDef,
                                                                                          searchParams, childSelectParams, searchLimit);

            // TODO: technically this is overwriting the params belonging to the prior search, that's ugly (but won't actually cause a problem)
            paramsChooser.ChooseOptimal(searchLimit.EstNumNodes(50_000, false), paramsSearchExecutionPostprocessor); // TODO: make 50_000 smarter


            int estNumNodes = EstimatedNumSearchNodesForEvaluator(searchLimit, nnEvaluators);

            // Adjust the nodes estimate if we are continuing an existing search
            if (searchLimit.Type == SearchLimitType.NodesPerMove && RootNWhenSearchStarted > 0)
            {
                estNumNodes = Math.Max(0, estNumNodes - RootNWhenSearchStarted);
            }
            Context = new MCTSIterator(store, reuseOtherContextForEvaluatedNodes, reusePositionCache, reuseTranspositionRoots,
                                       nnEvaluators, searchParams, childSelectParams, searchLimit, estNumNodes);
            ThreadSearchContext = Context;

            TerminationManager = new MCTSFutilityPruning(this, Context);
            LimitManager       = limitManager;

            CeresEnvironment.LogInfo("MCTS", "Init", $"SearchManager created for store {store}", InstanceID);
        }