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); }
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)); }
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)); }
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); } }
/// <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)); }
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 )); } }
public override Tuple <Direction, float> Search(Configuration c, FastWorld root, int depth) { return(BestWithHeuristic(c, root, depth, 0, float.NegativeInfinity, float.PositiveInfinity)); }
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);
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)); }
public CachedMultiMetricState(FastWorld w) { World = w; adversarialFill = new Lazy <Util.AdversarialFillResult>(() => Util.GenerateFloodFillBoard(World)); }
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 }
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 }