/// <summary> /// Finds something for the AI to seek towards (we'll go with closest) /// </summary> /// <param name="graph">The scene graph</param> /// <param name="state">The state of the level</param> /// <returns></returns> private LogicalCell findAIGoal(LogicalCellGraph graph, ILevelState state) { int closestItemDistance = int.MaxValue; LogicalCell closestItemCell = null; var stats = state.GameStats; foreach (var i in state.ActiveItems) { if ((!stats.ExitUnlocked && i.EndsLevel) || (stats.ExitUnlocked && !i.EndsLevel)) { continue; } var logicalPosition = i.LogicalLocation; var cell = graph.LookupCell(logicalPosition.x, logicalPosition.y); var(accessible, distance) = enemyWeightGraph.LookupDistance(cell); if (!accessible) { continue; } if (distance < closestItemDistance) { closestItemDistance = distance; closestItemCell = cell; } } return(closestItemCell); }
/// <summary> /// Sets the neighbor linked cells for this cell /// </summary> /// <param name="top">The first neighboring cell to the top</param> /// <param name="left">The first neighboring cell to the left</param> /// <param name="right">The first neighboring cell to the right</param> /// <param name="bottom">The first neighboring cell to the bottom</param> public void SetNeighbors(LogicalCell top, LogicalCell left, LogicalCell right, LogicalCell bottom) { neighborReferences[0] = top; neighborReferences[1] = left; neighborReferences[2] = right; neighborReferences[3] = bottom; }
/// <summary> /// Finds the shortest distance from precalculated starting location to the given target. /// Adjacent cells and teleports (per special game rules) count as 1 distance point. /// </summary> /// <param name="target">The cell to find the distance to</param> /// <returns>1. True if cell is accessible, False if blocked 2. The shortest distance</returns> public (bool accessible, int distance) LookupDistance(LogicalCell target) { bool accessible = false; int distance = 0; if (distancesByCell.ContainsKey(target)) { int d = distancesByCell[target]; if (d != int.MaxValue) { accessible = true; distance = d; } } return(accessible : accessible, distance : distance); }
/// <summary> /// Finds the shortest path from precalculated starting location to the given target /// Adjacent cells and teleports (per special game rules) count as 1 distance point. /// </summary> /// <param name="target">The cell to find the path to</param> /// <returns>1. True if cell is accessible, False if blocked 2. The shortest path</returns> public (bool accessible, LogicalPath path) LookupShortestPath(LogicalCell target) { LogicalPath path = new LogicalPath(); var p = new List <LogicalCell>(); bool accessible = false; if (distancesByCell[target] != int.MaxValue || target == startingLocation) { LogicalCell currentCell = target; while (currentCell != null) { accessible = true; path.PrependPath(currentCell.Loc); currentCell = paths[currentCell]; } } return(accessible : accessible, path : path); }
/// <summary> /// Extracts the logical space from a cell, likely taken from the cell graph /// </summary> /// <param name="cell">The cell to extract</param> /// <returns>The logical space of the cell</returns> public static Vector3Int GetLogicalSpaceFromCell(LogicalCell cell) { return(new Vector3Int(cell.X, cell.Y, 0)); }
/// <summary> /// Builds a new precalculated Dijkstra graph based on the starting location. /// </summary> /// <param name="graph">The cell graph we are working from</param> /// <param name="startingLocation">The location common to all futher distance calculations</param> /// <param name="maxDistance">The maximum distance away from the starting location the graph will store. If left as 0, it counts as infinite.</param> /// <param name="allowNeighborTeleportation">This game has special rules to allow traversal on non-adjacent cells. Passing false will disable this rule making it more like the assignment likely intended (but that would be less fun!)</param> /// <returns>The baked/calculated Dijkstra graph</returns> public static DijkstraWeightGraph BuildDijkstraWeightGraph(LogicalCellGraph graph, LogicalCell startingLocation, int maxDistance = 0, bool allowNeighborTeleportation = false) { DijkstraWeightGraph g = new DijkstraWeightGraph(); g.startingLocation = startingLocation; var distances = new Dictionary <LogicalCell, int>(); var path = new Dictionary <LogicalCell, LogicalCell>(); var remainingCells = new HashSet <LogicalCell>(); foreach (LogicalCell cell in graph.Cells) { distances[cell] = int.MaxValue; path[cell] = null; remainingCells.Add(cell); } distances[startingLocation] = 0; while (remainingCells.Any()) { LogicalCell currentCell = remainingCells.First(); int currentCellDistance = int.MaxValue; foreach (LogicalCell cell in remainingCells) { var d = distances[cell]; if (d < currentCellDistance) { currentCellDistance = d; currentCell = cell; } } remainingCells.Remove(currentCell); if (currentCellDistance != int.MaxValue && (maxDistance == 0 || currentCellDistance + 1 <= maxDistance)) { foreach (var neighbor in currentCell.Neighbors) { if (neighbor == null) { continue; } // The special rule for this game allows non-adjacent traversal on matching colors. if (!allowNeighborTeleportation) { bool adjacent = (Math.Abs(neighbor.X - currentCell.X) <= 1 && neighbor.Y == currentCell.Y) || (Math.Abs(neighbor.Y - currentCell.Y) <= 1 && neighbor.X == currentCell.X); if (!adjacent) { continue; // Skip } } if (!remainingCells.Contains(neighbor)) { continue; // Skip } int newDistance = currentCellDistance + 1; if (newDistance < distances[neighbor]) { distances[neighbor] = newDistance; path[neighbor] = currentCell; } } } } g.distancesByCell = distances; g.paths = path; return(g); }
/// <summary> /// Builds a logical cell grid from the game data /// </summary> /// <param name="tilemap">The game map</param> /// <param name="gateLocations">The locations of all the locked doors/gates</param> /// <returns>A logical cell graph which contains useful neighboring data</returns> public static LogicalCellGraph BuildCellGraph(IMap tilemap, IEnumerable <Vector3Int> gateLocations) { LogicalCellGraph graph = new LogicalCellGraph(); var logicalSize = tilemap.CellBounds.size / 2; var sizeX = logicalSize.x; var sizeY = logicalSize.y; int[,,] colors = new int[sizeX, logicalSize.y, 4]; LogicalCell[,] cells = new LogicalCell[sizeX, logicalSize.y]; var gateLocationsSet = new HashSet <Vector3Int>(); foreach (var g in gateLocations) { gateLocationsSet.Add(g); } for (int y = 0; y < sizeY; ++y) { for (int x = 0; x < sizeX; ++x) { var cell = new LogicalCell { X = x, Y = y }; var logicalSpace = GridSpaceConversion.GetLogicalSpaceFromCell(cell); if (gateLocationsSet.Contains(logicalSpace)) { colors[x, y, 0] = 0; // Reserved for gates colors[x, y, 1] = 0; colors[x, y, 2] = 0; colors[x, y, 3] = 0; } else { var gridSpace = GridSpaceConversion.GetGridSpaceFromLogical(logicalSpace, tilemap); // I wouldn't typically depend on render state for logical stuff, // But this game will be all about color so I think it's OK. colors[x, y, 0] = tilemap.GetColor(new Vector3Int(gridSpace.x, gridSpace.y, 0)).GetHashCode(); colors[x, y, 1] = tilemap.GetColor(new Vector3Int(gridSpace.x + 1, gridSpace.y, 0)).GetHashCode(); colors[x, y, 2] = tilemap.GetColor(new Vector3Int(gridSpace.x, gridSpace.y + 1, 0)).GetHashCode(); colors[x, y, 3] = tilemap.GetColor(new Vector3Int(gridSpace.x + 1, gridSpace.y + 1, 0)).GetHashCode(); } cells[x, y] = cell; } } for (int y = 0; y < sizeY; ++y) { for (int x = 0; x < sizeX; ++x) { HashSet <int> colorsInThisCell = new HashSet <int>(new int[] { colors[x, y, 0], colors[x, y, 1], colors[x, y, 2], colors[x, y, 3], }); LogicalCell thisCell = cells[x, y]; LogicalCell upNeighbor = null; for (int up = y + 1; up < sizeY; ++up) { if (colorsInThisCell.Contains(colors[x, up, 0]) || colorsInThisCell.Contains(colors[x, up, 1]) || colorsInThisCell.Contains(colors[x, up, 2]) || colorsInThisCell.Contains(colors[x, up, 3])) { upNeighbor = cells[x, up]; break; } } LogicalCell downNeighbor = null; for (int down = y - 1; down >= 0; --down) { if (colorsInThisCell.Contains(colors[x, down, 0]) || colorsInThisCell.Contains(colors[x, down, 1]) || colorsInThisCell.Contains(colors[x, down, 2]) || colorsInThisCell.Contains(colors[x, down, 3])) { downNeighbor = cells[x, down]; break; } } LogicalCell rightNeighbor = null; for (int right = x + 1; right < sizeX; ++right) { if (colorsInThisCell.Contains(colors[right, y, 0]) || colorsInThisCell.Contains(colors[right, y, 1]) || colorsInThisCell.Contains(colors[right, y, 2]) || colorsInThisCell.Contains(colors[right, y, 3])) { rightNeighbor = cells[right, y]; break; } } LogicalCell leftNeighbor = null; for (int left = x - 1; left >= 0; --left) { if (colorsInThisCell.Contains(colors[left, y, 0]) || colorsInThisCell.Contains(colors[left, y, 1]) || colorsInThisCell.Contains(colors[left, y, 2]) || colorsInThisCell.Contains(colors[left, y, 3])) { leftNeighbor = cells[left, y]; break; } } thisCell.SetNeighbors(upNeighbor, leftNeighbor, rightNeighbor, downNeighbor); } } graph.indexedCells = cells; return(graph); }