using UnityEngine; using System.Collections.Generic; using System.Linq; /// /// Handles pathfinding through the maze using A* algorithm /// Takes into account terrain movement costs /// public class MazePathfinder { private MazeData mazeData; public class PathNode { public Vector2Int Position { get; set; } public PathNode Parent { get; set; } public float GCost { get; set; } // Cost from start public float HCost { get; set; } // Heuristic cost to goal public float FCost => GCost + HCost; public PathNode(Vector2Int position, PathNode parent, float gCost, float hCost) { Position = position; Parent = parent; GCost = gCost; HCost = hCost; } } public MazePathfinder(MazeData mazeData) { this.mazeData = mazeData; } /// /// Finds a path from start to goal using A* with a min-heap open set (O(n log n)). /// public List FindPath(Vector2Int start, Vector2Int goal) { // Min-heap keyed by FCost var openHeap = new MinHeap(); var closedSet = new HashSet(); // Fast lookup: position → best node in open set var openLookup = new Dictionary(); var startNode = new PathNode(start, null, 0, Heuristic(start, goal)); openHeap.Enqueue(startNode, startNode.FCost); openLookup[start] = startNode; while (openHeap.Count > 0) { var currentNode = openHeap.Dequeue(); openLookup.Remove(currentNode.Position); if (currentNode.Position == goal) return ReconstructPath(currentNode); closedSet.Add(currentNode.Position); foreach (var neighbor in GetNeighbors(currentNode.Position)) { if (closedSet.Contains(neighbor)) continue; float movementCost = GetMovementCost(neighbor); float tentativeG = currentNode.GCost + movementCost; if (openLookup.TryGetValue(neighbor, out var existingNode)) { if (tentativeG < existingNode.GCost) { existingNode.Parent = currentNode; existingNode.GCost = tentativeG; openHeap.UpdatePriority(existingNode, existingNode.FCost); } } else { float hCost = Heuristic(neighbor, goal); var newNode = new PathNode(neighbor, currentNode, tentativeG, hCost); openHeap.Enqueue(newNode, newNode.FCost); openLookup[neighbor] = newNode; } } } return new List(); } /// /// Gets the movable neighbors of a position /// private List GetNeighbors(Vector2Int position) { var neighbors = new List(); Vector2Int[] directions = new[] { new Vector2Int(position.x + 1, position.y), new Vector2Int(position.x - 1, position.y), new Vector2Int(position.x, position.y + 1), new Vector2Int(position.x, position.y - 1), // Optionally add diagonals: // new Vector2Int(position.x + 1, position.y + 1), // new Vector2Int(position.x - 1, position.y + 1), // new Vector2Int(position.x + 1, position.y - 1), // new Vector2Int(position.x - 1, position.y - 1), }; foreach (var dir in directions) { if (mazeData.IsWalkable(dir.x, dir.y)) { neighbors.Add(dir); } } return neighbors; } /// /// Gets the movement cost for a tile based on terrain /// private float GetMovementCost(Vector2Int position) { var tile = mazeData.GetTile(position.x, position.y); if (tile == null) return float.MaxValue; return tile.GetMovementCost(); } /// /// Heuristic function (Manhattan distance) /// private float Heuristic(Vector2Int from, Vector2Int to) { return Mathf.Abs(from.x - to.x) + Mathf.Abs(from.y - to.y); } /// /// Reconstructs the path from goal to start /// private List ReconstructPath(PathNode node) { var path = new List(); while (node != null) { path.Add(node.Position); node = node.Parent; } path.Reverse(); return path; } /// /// Finds all reachable tiles from a starting position within a certain distance /// public HashSet FindReachableTiles(Vector2Int start, float maxDistance) { var reachable = new HashSet(); var queue = new Queue<(Vector2Int pos, float distance)>(); queue.Enqueue((start, 0)); reachable.Add(start); while (queue.Count > 0) { var (current, distance) = queue.Dequeue(); var neighbors = GetNeighbors(current); foreach (var neighbor in neighbors) { if (!reachable.Contains(neighbor)) { float cost = GetMovementCost(neighbor); float newDistance = distance + cost; if (newDistance <= maxDistance) { reachable.Add(neighbor); queue.Enqueue((neighbor, newDistance)); } } } } return reachable; } }