Beispiel #1
0
        private static List <bool> FindMaskForDepth(FastWorld w, int ownIndex, int depth)
        {
            // Initially assume all masked
            var result = Extensions.FilledList(w.Snakes.Count, true);

            // Want at most this distance to any part of each snake to fully simulate it
            var maximumDesiredDistance = depth * 2;

            Coord ownHead = w.Snakes[ownIndex].Head;

            for (int i = 0; i < w.Snakes.Count; ++i)
            {
                // Self is never masked
                if (i == ownIndex)
                {
                    result[i] = false;
                    continue;
                }

                // What is dead may never die
                if (!w.Snakes[i].Alive)
                {
                    continue;
                }

                result[i] = !HasCloserPart(w.Snakes[i], ownHead, maximumDesiredDistance);
            }

            return(result);
        }
Beispiel #2
0
        private static List <int> CalculateSteppingOrdering(FastWorld w, int ownIndex, List <bool> reflexMask)
        {
            var result = new List <int>();

            // Always evaluate self out most
            result.Add(ownIndex);

            for (int i = 0; i < w.Snakes.Count; ++i)
            {
                // Ignore reflex based since they can't base their decision on other snakes anyways
                if (reflexMask[i])
                {
                    continue;
                }

                // Ignore self
                if (i == ownIndex)
                {
                    continue;
                }

                // Add fully simulated other snakes
                // TODO: Could order by distance to player, i.e. the closest snake makes the best move
                result.Add(i);
            }

            return(result);
        }
        public void End(GameState s)
        {
            FastWorld w        = FastWorld.FromApiModel(s);
            int       ownIndex = w.FindSnakeIndexForHead(s.You.Head);

            innerController.End(w, ownIndex);
        }
        public Direction Move(GameState s)
        {
            FastWorld w        = FastWorld.FromApiModel(s);
            int       ownIndex = w.FindSnakeIndexForHead(s.You.Head);

            return(innerController.Move(w, ownIndex, SearchLimit));
        }
        private Result BestFixedDepth(Configuration c, FastWorld w)
        {
            Debug.Assert(c.SearchLimit.LimitType == LimitType.Depth);

            var tuple = Search(c, w, c.SearchLimit.Limit);

            return(new Result(tuple.Item2, tuple.Item1, c.SearchLimit.Limit));
        }
Beispiel #6
0
        public static FillResult CountBasicFloodFill(FastWorld board, Coord start)
        {
            var visited    = new int[board.Height, board.Width];
            var queue      = new Queue <BasicFillItem>();
            int count      = 0;
            int blackCount = 0;
            int whiteCount = 0;

            queue.Enqueue(new BasicFillItem(start, 0));

            while (queue.TryDequeue(out BasicFillItem current))
            {
                // Iterate all possible moves from current position
                for (int i = 0; i < Moves.Length; ++i)
                {
                    var next         = current.Coord + Util.Moves[i];
                    var nextDistance = current.Distance + 1;

                    // Skip out of bounds
                    if (next.X < 0 || next.Y < 0 || next.X >= board.Width || next.Y >= board.Height)
                    {
                        continue;
                    }

                    // Skip already visited
                    if (visited[next.Y, next.X] > 0)
                    {
                        continue;
                    }
                    // Fill neighbour increasing distance and add neighbour to queue
                    visited[next.Y, next.X] = nextDistance;

                    // Skip unpassable
                    var occupant = board.fields[next.Y, next.X].occupant;

                    if (occupant != FastWorld.Occupant.Empty && occupant != FastWorld.Occupant.Fruit)
                    {
                        continue;
                    }

                    queue.Enqueue(new BasicFillItem(next, nextDistance));

                    // Check parity to determine whether on white or black chessboard tile
                    if (next.IsChessBoardWhite)
                    {
                        ++whiteCount;
                    }
                    else
                    {
                        ++blackCount;
                    }

                    ++count;
                }
            }

            return(new FillResult(count, whiteCount, blackCount, visited));
        }
Beispiel #7
0
 public static Result Search(Configuration c, FastWorld w, int ownIndex)
 {
     if (c.SearchLimit.LimitType == DeepeningSearch.LimitType.Depth)
     {
         return(SearchSelectingStrategy(c, w, ownIndex, c.SearchLimit.Limit, new DeepeningSearch.Stop()));
     }
     else
     {
         return(BestFixedTime(c, w, ownIndex));
     }
 }
        public string Start(GameState s)
        {
            FastWorld w        = FastWorld.FromApiModel(s);
            int       ownIndex = w.FindSnakeIndexForHead(s.You.Head);

            innerController.Start(w, ownIndex);

            // For color just take hash of inner type name
            var hash = sha256(innerController.GetType().FullName);

            return(String.Format("#{0:X2}{1:X2}{2:X2}", hash[0], hash[1], hash[2]));
        }
        public CachedMetricState(FastWorld world, int ownIndex, int enemyIndex)
        {
            this.World      = world;
            this.OwnIndex   = ownIndex;
            this.EnemyIndex = enemyIndex;

            this.ownSnake   = world.Snakes[ownIndex];
            this.enemySnake = world.Snakes[enemyIndex];

            this.adversarialFill = new Lazy <Util.AdversarialFillResult>(() => Util.GenerateFloodFillBoard(world));
            this.ownFill         = new Lazy <Util.FillResult>(() => Util.CountBasicFloodFill(world, ownSnake.Head));
            this.enemyFill       = new Lazy <Util.FillResult>(() => Util.CountBasicFloodFill(world, enemySnake.Head));
        }
        public Result Best(Configuration c, FastWorld w)
        {
            switch (c.SearchLimit.LimitType)
            {
            case LimitType.Depth:
                return(BestFixedDepth(c, w));

            case LimitType.Milliseconds:
                return(BestFixedTime(c, w));

            default:
                throw new ArgumentException();
            }
        }
        public float Score(FastWorld w, int ownIndex, Util.FillResult fill)
        {
            int maxLength  = 0;
            int sumLength  = 0;
            int otherCount = 0;

            for (int i = 0; i < w.Snakes.Count; ++i)
            {
                if (i == ownIndex)
                {
                    continue;
                }
                if (!w.Snakes[i].Alive)
                {
                    continue;
                }

                sumLength  += w.Snakes[i].Length;
                otherCount += 1;

                if (w.Snakes[i].Length > maxLength)
                {
                    maxLength = w.Snakes[i].Length;
                }
            }

            float averageLength = sumLength / (float)otherCount;

            float interpLength = 0.9f * maxLength + 0.1f * averageLength;

            float deltaAdvantage = w.Snakes[ownIndex].Length - interpLength;

            float deltaMetric      = ScoreDeltaAdvantage(deltaAdvantage);
            float deltaOnEatMetric = ScoreDeltaAdvantage(deltaAdvantage + 1);

            // If food distance metric is 1, want to be exactly as good as eating one
            // fruit. Since food distance metric is always less than 1, ensures that eating
            // is always better than guarding with proximity of 1.
            float foodMetricMultiplier = (deltaOnEatMetric - deltaMetric) * FruitMultiplierFactor;

            float score = deltaMetric + FoodDistanceMetric(w, ownIndex, fill) * foodMetricMultiplier;

            Debug.Assert(!float.IsNaN(score));

            return(score);
        }
        public Direction Move(FastWorld w, int ownIndex, DeepeningSearch.SearchLimit limit)
        {
            var watch = Stopwatch.StartNew();

            var conf = new MaxN.Configuration
            {
                MaxNHeuristicProducer      = this.MaxNHeuristicProducer,
                AlphaBetaFallbackHeuristic = this.AlphaBetaHeuristicProducer,
                SearchLimit = limit
            };

            var result = MaxN.Search(conf, w, ownIndex);

            // Console.Error.WriteLine($"{ownIndex} TURN {w.Turn:D3} NMax done {watch.Elapsed.TotalMilliseconds} ms, {result}");
            Trace.Assert(result != null);
            return(result.Move);
        }
        private Result BestFixedTime(Configuration c, FastWorld w)
        {
            Debug.Assert(c.SearchLimit.LimitType == LimitType.Milliseconds);

            // Keep in mind on thread safety:
            // The thread that calls Abort might block if the thread that is being aborted is in a protected
            // region of code, such as a catch block, finally block, or constrained execution region. If the
            // thread that calls Abort holds a lock that the aborted thread requires, a deadlock can occur.
            // https://docs.microsoft.com/en-us/dotnet/api/system.threading.thread.abort?view=netframework-4.7.2

            // Always reset stop latch first
            c.Stop = new Stop();

            var    myLock = new object();
            Result best   = null;

            Thread t = new Thread(() => {
                try {
                    for (int i = 1; ; ++i)
                    {
                        var current = Search(c, w, i);

                        lock (myLock) {
                            best = new Result(current.Item2, current.Item1, i);
                        }
                    }
                }
                catch (StopSearchException) {
                    // Perfectly normal to throw this exception upon stopping deepening
                    return;
                }
            });

            t.Start();

            Thread.Sleep(c.SearchLimit.Limit);

            c.Stop.RequestStop();

            // Should be no more contention since t is aborted and joined
            // But I guess it doesn't hurt
            lock (myLock) {
                return(best);
            }
        }
Beispiel #14
0
        /// <summary>
        /// More intelligent reflex based evasion. Scores all 4 available actions
        /// without simulating the next step, returning the best choice.
        /// This function is supposed to be very fast.
        /// </summary>
        /// <param name="board">Current world</param>
        /// <param name="index">Index of snake to simulate</param>
        /// <returns></returns>
        public static Direction ImprovedReflexBasedEvade(FastWorld board, int index)
        {
            Direction best      = Direction.North;
            float     bestScore = float.NegativeInfinity;

            for (int i = 0; i < Directions.Length; ++i)
            {
                var d = Directions[i];

                float score = ReflexEvasionHeuristic.Score(board, index, d);

                if (score > bestScore)
                {
                    bestScore = score;
                    best      = d;
                }
            }

            return(best);
        }
        private float FoodDistanceMetric(FastWorld w, int ownIndex, Util.FillResult fill)
        {
            int bestOwnDistance = int.MaxValue;

            foreach (var fruit in w.Fruits)
            {
                var dist = fill.Distances[fruit.Y, fruit.X];

                if (dist <= 0)
                {
                    continue;
                }

                bestOwnDistance = Math.Min(bestOwnDistance, dist);
            }

            if (bestOwnDistance == int.MaxValue)
            {
                return(0);
            }

            return((float)Math.Exp(-FoodDistanceDecay * bestOwnDistance));
        }
Beispiel #16
0
        private static Result SearchSelectingStrategy(Configuration c, FastWorld w, int ownIndex, int depth, DeepeningSearch.Stop stop)
        {
            var reflexMask     = c.ReflexMask ?? FindMaskForDepth(w, ownIndex, depth);
            var simulatedCount = reflexMask.Count(el => el == false);
            var ordering       = MaxN.CalculateSteppingOrdering(w, ownIndex, reflexMask);

            if (simulatedCount == 2 && c.AlphaBetaFallbackHeuristic != null)
            {
                // If alpha beta fallback heuristic is provided and we have two simulated snakes, use alpha beta search
                var abConf = new DeepeningSearch.Configuration(c.AlphaBetaFallbackHeuristic(ordering[0], ordering[1]), ordering[0], ordering[1]);
                abConf.Stop           = stop;
                var(direction, score) = new AlphaBeta().Search(abConf, w, depth);
                return(new Result(new DeepeningSearch.Result(score, direction, depth)));
            }
            else
            {
                var internalConfiguration = new InternalConfiguration {
                    ReflexMask  = reflexMask,
                    Depth       = depth,
                    OwnIndex    = ownIndex,
                    StopHandler = stop
                };

                var(scoresAbs, scoresRel, directions) = PseudoPlyStep(w, internalConfiguration, c.MaxNHeuristicProducer(ownIndex), 0, 0, ordering, null);

                return(new Result(
                           internalConfiguration.steps,
                           internalConfiguration.ReflexMask.Count(el => el == false),
                           internalConfiguration.Depth,
                           directions[ownIndex],
                           scoresRel[ownIndex],
                           scoresAbs,
                           scoresRel,
                           directions
                           ));
            }
        }
Beispiel #17
0
 public override Tuple <Direction, float> Search(Configuration c, FastWorld root, int depth)
 {
     return(BestWithHeuristic(c, root, depth, 0, float.NegativeInfinity, float.PositiveInfinity));
 }
Beispiel #18
0
        public static AdversarialFillResult GenerateFloodFillBoard(FastWorld board)
        {
            var fillBoard = new FillTile[board.Height, board.Width];

            // Count true empty tiles filled first by snake
            var emptyTileFillCounts = Extensions.FilledList(board.Snakes.Count, 0);

            var queue = new Queue <FillItem>();
            var list  = new List <FillItem>();

            // Fill heads as ours
            for (int i = 0; i < board.Snakes.Count; ++i)
            {
                list.Add(new FillItem(board.Snakes[i].index, 0, board.Snakes[i].Head));
            }

            // Sort by length to ensure longest snake can expand most freely
            list.Sort((a, b) => board.Snakes[b.Snake].MaxLength.CompareTo(board.Snakes[a.Snake].MaxLength));

            // Add initial head position to flood fill queue
            for (int i = 0; i < list.Count; ++i)
            {
                queue.Enqueue(list[i]);
            }

            while (queue.TryDequeue(out FillItem current))
            {
                // Iterate all possible moves from current position
                for (int i = 0; i < Moves.Length; ++i)
                {
                    var next = current.Coord + Util.Moves[i];

                    // Skip out of bounds
                    if (next.X < 0 || next.Y < 0 || next.X >= board.Width || next.Y >= board.Height)
                    {
                        continue;
                    }

                    // Skip already filled neighbours
                    if (fillBoard[next.Y, next.X].Snake != null)
                    {
                        continue;
                    }

                    // Fill neighbour increasing distance and add neighbour to queue
                    fillBoard[next.Y, next.X] = new FillTile(current.Snake, current.Distance + 1);

                    // Now skip if blocked on original board, because we cant continue filling from here
                    // Still want to set fill tile earlier to determine if we can reach this snake part first
                    if (board[next].occupant == FastWorld.Occupant.Snake)
                    {
                        continue;
                    }

                    emptyTileFillCounts[current.Snake]++;

                    queue.Enqueue(new FillItem(current.Snake, current.Distance + 1, next));
                }
            }

            return(new AdversarialFillResult(fillBoard, emptyTileFillCounts));
        }
 public abstract Tuple <Direction, float> Search(Configuration c, FastWorld root, int depth);
Beispiel #20
0
        private static Tuple <List <float>, List <float>, List <Direction> > PseudoPlyStep(FastWorld w, InternalConfiguration c, IMultiHeuristic heuristic, int currentDepth, int currentPlyDepth, List <int> steppingOrder, List <Direction> moves)
        {
            // Immediately check stop search on each draw (but not ply)
            if (c.StopHandler.StopRequested)
            {
                throw new DeepeningSearch.StopSearchException();
            }

            if (currentPlyDepth == steppingOrder.Count)
            {
                // Increase diagnostic step counter
                c.steps++;

                // All plys played, perform move
                var newW = w.Clone() as FastWorld;
                newW.UpdateMovementTick(moves);

                var cachedMetricState = new CachedMultiMetricState(newW);

                int nextDepth = currentDepth + 1;

                var          oldMoves = new List <Direction>(moves);
                List <float> phiAbs, phiRel;

                if (nextDepth == c.Depth || heuristic.IsTerminal(cachedMetricState))
                {
                    (phiAbs, phiRel) = ScoreAdvantage(cachedMetricState, c, heuristic);
                }
                else
                {
                    (phiAbs, phiRel, _) = PseudoPlyStep(newW, c, heuristic, nextDepth, 0, steppingOrder, null);
                }

                return(Tuple.Create(phiAbs, phiRel, oldMoves));
            }
            else if (currentPlyDepth == 0)
            {
                // First to play on this draw is responsible for setting up move cache by definition
                // Must create new move list to not mess with parent plys (and because by definition we did not receive the parent move list)
                moves = new List <Direction>(w.Snakes.Count);

                for (int i = 0; i < w.Snakes.Count; ++i)
                {
                    if (w.Snakes[i].Alive && c.ReflexMask[i])
                    {
                        moves.Add(Util.ImprovedReflexBasedEvade(w, i));
                    }
                    else
                    {
                        moves.Add(Direction.North);
                    }
                }
            }

            int ownIndex = steppingOrder[currentPlyDepth];

            float            alpha     = float.NegativeInfinity;
            List <float>     phiRelMax = null;
            List <float>     phiAbsMax = null;
            List <Direction> dirMax    = null;

            for (int i = 0; i < Util.Directions.Length; ++i)
            {
                // Must always evaluate at least one move, even if it is known to be deadly,
                // to have correct heuristics bounds
                bool mustEvaluate = phiRelMax == null && i == Util.Directions.Length - 1;

                // Skip certainly deadly moves if we do not have to evaluate
                if (!mustEvaluate && w.CertainlyDeadly(ownIndex, Util.Directions[i]))
                {
                    continue;
                }

                moves[ownIndex] = Util.Directions[i];

                var(phiStarAbs, phiStarRel, directions) = PseudoPlyStep(w, c, heuristic, currentDepth, currentPlyDepth + 1, steppingOrder, moves);
                var phiStarP = phiStarRel[ownIndex];

                if (alpha < phiStarP)
                {
                    alpha     = phiStarP;
                    phiRelMax = phiStarRel;
                    phiAbsMax = phiStarAbs;
                    dirMax    = directions;
                }

                Debug.Assert(alpha != float.NegativeInfinity);
            }

            Trace.Assert(phiRelMax != null);
            Trace.Assert(dirMax != null);

            return(Tuple.Create(phiAbsMax, phiRelMax, dirMax));
        }
Beispiel #21
0
 public CachedMultiMetricState(FastWorld w)
 {
     World           = w;
     adversarialFill = new Lazy <Util.AdversarialFillResult>(() => Util.GenerateFloodFillBoard(World));
 }
Beispiel #22
0
        public static float Score(FastWorld w, int ownIndex, Direction move)
        {
            // If known to be deadly return minimum
            if (w.CertainlyDeadly(ownIndex, move))
            {
                return(-1000.0f);
            }

            var newHead = w.Snakes[ownIndex].Head.Advanced(move);

            float enemyCollisionScore = 0.0f;

            // If stepping on snake, punish badly
            // This always means stepping on another snake, as
            // CertainlyDeadly already checks self
            if (w[newHead].occupant == FastWorld.Occupant.Snake)
            {
                enemyCollisionScore -= 1.0f;
            }

            float fruitScore = 0.0f;

            // Reward stepping on fruit
            if (w[newHead].occupant == FastWorld.Occupant.Fruit)
            {
                fruitScore = 1.0f;
            }

            float potentialCollisionScore = 0.0f;

            // Punish proximity to enemy that may kill us
            for (int i = 0; i < w.Snakes.Count; ++i)
            {
                if (!w.Snakes[i].Alive || i == ownIndex)
                {
                    continue;
                }

                var manhattan = Coord.ManhattanDistance(w.Snakes[i].Head, newHead);

                if (manhattan == 1)
                {
                    // TODO: Delta does not take into account growing yet
                    var delta = w.Snakes[ownIndex].Length - w.Snakes[i].Length;
                    if (delta == 0)
                    {
                        // Punish potential tie collision
                        potentialCollisionScore -= 0.5f;
                    }
                    else if (delta > 0)
                    {
                        // Reward potential collision with smaller snake
                        // less than potential tie
                        potentialCollisionScore += 0.3f;
                    }
                    else
                    {
                        // Punish potential collision with larger snake
                        potentialCollisionScore -= 1.0f;
                    }
                }
            }

            // Asume holding current direction
            float holdScore = move == w.Snakes[ownIndex].LastDirection ? 1.0f : 0.0f;

            return(holdScore * 1.0f
                   + fruitScore * 3.0f
                   + potentialCollisionScore * 10.0f
                   + enemyCollisionScore * 30.0f);
        }
 public void Start(FastWorld w, int ownIndex)
 {
     // No op
 }
Beispiel #24
0
        public static Tuple <Direction, float> BestWithHeuristic(Configuration c, FastWorld w, int maxDepth, int currentDepth, float alpha, float betaInitial)
        {
            // Fail fast if terminal or depth exhausted
            bool terminal     = c.Heuristic.IsTerminal(w);
            bool limitReached = currentDepth >= maxDepth;

            if (terminal || limitReached)
            {
                var score = c.Heuristic.Score(w);
                return(Tuple.Create(Direction.North, score));
            }

            Direction bestOwnDirection = Direction.North;

            var desiredMoves = new List <Direction>(w.Snakes.Count);

            for (int i = 0; i < w.Snakes.Count; ++i)
            {
                // Use the given decision functions for all snakes first
                desiredMoves.Add(c.UntargetedDecisionFunction(w, i));
            }

            // Initially have not checked any own moves. We must always check at least one
            // available action to allow the heuristic to evaluate one leaf, even if we know
            // it leads to death. Otherwise would return the theoretical heuristic min value (-Inf)
            // not the practical lower bound as implemented
            bool checkedOwnMove = false;

            for (int i = 0; i < Util.Directions.Length; ++i)
            {
                // If this is the last available action and we have not evaluated any actions,
                // must evaluate.
                bool mustEvaluateOwn = !checkedOwnMove && i == Util.Directions.Length - 1;

                // Skip guaranteed deadly immediately
                if (!mustEvaluateOwn && w.CertainlyDeadly(c.OwnIndex, Util.Directions[i]))
                {
                    continue;
                }

                checkedOwnMove = true;

                // Must reset beta to initial beta value of this node, otherwise using updated beta from different sub tree
                // Beta stores the best move possible for the opponent
                var beta = betaInitial;

                bool checkedEnemyMove = true;

                for (int j = 0; j < Util.Directions.Length; ++j)
                {
                    // Check stop request in inner loop
                    if (c.Stop.StopRequested)
                    {
                        throw new StopSearchException();
                    }

                    bool mustEvaluateEnemy = !checkedEnemyMove && j == Util.Directions.Length - 1;

                    // Skip guaranteed deadly immediately
                    if (!mustEvaluateEnemy && w.CertainlyDeadly(c.EnemyIndex, Util.Directions[j]))
                    {
                        continue;
                    }

                    desiredMoves[c.OwnIndex]   = Util.Directions[i];
                    desiredMoves[c.EnemyIndex] = Util.Directions[j];

                    var worldInstance = w.Clone() as FastWorld;
                    worldInstance.UpdateMovementTick(desiredMoves);

                    var tuple = BestWithHeuristic(c, worldInstance, maxDepth, currentDepth + 1, alpha, beta);

                    if (tuple.Item2 < beta)
                    {
                        beta = tuple.Item2;
                    }

                    // If the best move possible is worse for the first player than the current worst,
                    // stop, no need to find even worse moves
                    if (alpha >= beta)
                    {
                        // Alpha cut-off
                        break;
                    }
                }

                if (beta > alpha)
                {
                    alpha            = beta;
                    bestOwnDirection = Util.Directions[i];
                }

                // If our best move is even better than the current choice of the opponent
                // stop, no need to find even better moves
                // Of course have to compare to initial beta value here
                if (alpha >= betaInitial)
                {
                    // Beta cut-off
                    break;
                }
            }

            return(Tuple.Create(bestOwnDirection, alpha));
        }
 public void End(FastWorld w, int ownIndex)
 {
     // No op
 }