using System; using System.Collections.Generic; namespace PathFinding { #region PathFinderStatus Enumeration /// /// Enumerasi yang merepresentasikan berbagai status dari PathFinder. /// Digunakan untuk melacak progress dari pencarian jalur (pathfinding). /// public enum PathFinderStatus { NOT_INITIALISED, SUCCESS, FAILURE, RUNNING, } /// /// Kelas abstrak Node yang menjadi dasar untuk semua jenis vertex /// yang digunakan dalam algoritma pathfinding. /// /// Tipe data nilai yang disimpan dalam node abstract public class Node { public T Value { get; private set; } public Node(T value) { Value = value; } abstract public List> GetNeighbours(); } /// /// Kelas abstrak PathFinder yang menjadi dasar untuk semua algoritma pencarian jalur. /// /// Tipe data nilai yang disimpan dalam node public abstract class PathFinder { #region Delegates for Cost Calculation. public delegate float CostFunction(T a, T b); public int ClosedListCount => closedList.Count; public int OpenListCount => openList.Count; public CostFunction HeuristicCost { get; set; } public CostFunction NodeTraversalCost { get; set; } #endregion #region PathFinderNode /// /// Kelas PathFinderNode. /// Merepresentasikan node dalam proses pencarian jalur. /// Node ini mengenkapsulasi Node dan informasi tambahan untuk algoritma pencarian jalur. /// public class PathFinderNode : System.IComparable { public PathFinderNode Parent { get; set; } public Node Location { get; private set; } public GridMap Map { get; set; } public float FCost { get; private set; } public float GCost { get; private set; } public float HCost { get; private set; } public PathFinderNode(Node location, PathFinderNode parent, float gCost, float hCost) { Location = location; Parent = parent; HCost = hCost; SetGCost(gCost); } public void SetGCost(float c) { GCost = c; FCost = GCost + HCost; } public void SetHCost(float h) { HCost = h; FCost = GCost + HCost; } public int CompareTo(PathFinderNode other) { if (other == null) return 1; return FCost.CompareTo(other.FCost); } } #endregion #region Properties public PathFinderStatus Status { get; protected set; } = PathFinderStatus.NOT_INITIALISED; public Node Start { get; protected set; } public Node Goal { get; protected set; } public PathFinderNode CurrentNode { get; protected set; } public GridMap Map { get; internal set; } #endregion #region Open and Closed Lists and Associated Functions. protected List openList = new List(); protected List closedList = new List(); protected PathFinderNode GetLeastCostNode( List myList) { int best_index = 0; float best_priority = myList[0].FCost; for (int i = 1; i < myList.Count; i++) { if (best_priority > myList[i].FCost) { best_priority = myList[i].FCost; best_index = i; } } PathFinderNode n = myList[best_index]; return n; } protected int IsInList(List myList, T cell) { for (int i = 0; i < myList.Count; i++) { if (EqualityComparer.Default.Equals(myList[i].Location.Value, cell)) return i; } return -1; } #endregion #region Delegates for Action Callbacks public delegate void DelegatePathFinderNode(PathFinderNode node); public DelegatePathFinderNode onChangeCurrentNode; public DelegatePathFinderNode onAddToOpenList; public DelegatePathFinderNode onAddToClosedList; public DelegatePathFinderNode onDestinationFound; public delegate void DelegateNoArguments(); public DelegateNoArguments onStarted; public DelegateNoArguments onRunning; public DelegateNoArguments onFailure; public DelegateNoArguments onSuccess; #endregion #region Pathfinding Search Related Functions public virtual void Reset() { if (Status == PathFinderStatus.RUNNING) { return; } CurrentNode = null; openList.Clear(); closedList.Clear(); Status = PathFinderStatus.NOT_INITIALISED; } public virtual PathFinderStatus Step() { closedList.Add(CurrentNode); onAddToClosedList?.Invoke(CurrentNode); if (openList.Count == 0) { Status = PathFinderStatus.FAILURE; onFailure?.Invoke(); return Status; } CurrentNode = GetLeastCostNode(openList); onChangeCurrentNode?.Invoke(CurrentNode); openList.Remove(CurrentNode); if (EqualityComparer.Default.Equals( CurrentNode.Location.Value, Goal.Value)) { Status = PathFinderStatus.SUCCESS; onDestinationFound?.Invoke(CurrentNode); onSuccess?.Invoke(); return Status; } List> neighbours = CurrentNode.Location.GetNeighbours(); foreach (Node cell in neighbours) { AlgorithmSpecificImplementation(cell); } Status = PathFinderStatus.RUNNING; onRunning?.Invoke(); return Status; } abstract protected void AlgorithmSpecificImplementation(Node cell); public virtual bool Initialise(Node start, Node goal) { if (Status == PathFinderStatus.RUNNING) { return false; } Reset(); Start = start; Goal = goal; if (EqualityComparer.Default.Equals(Start.Value, Goal.Value)) { // Cost set to 0 CurrentNode = new PathFinderNode(Start, null, 0.0f, 0.0f); onChangeCurrentNode?.Invoke(CurrentNode); onStarted?.Invoke(); onDestinationFound?.Invoke(CurrentNode); Status = PathFinderStatus.SUCCESS; onSuccess?.Invoke(); return true; } float H = HeuristicCost(Start.Value, Goal.Value); PathFinderNode root = new PathFinderNode(Start, null, 0.0f, H); openList.Add(root); onAddToOpenList?.Invoke(root); CurrentNode = root; onChangeCurrentNode?.Invoke(CurrentNode); onStarted?.Invoke(); Status = PathFinderStatus.RUNNING; return true; } #endregion } #endregion #region Priority Queue /// /// Memprioritaskan item berdasarkan nilai komparatif mereka /// /// Tipe item dalam antrian prioritas public class PriorityQueue where T : IComparable { private List data; private IComparer comparer; private Dictionary elementIndexMap; // Cache untuk optimasi private T _lastDequeued; private int _count; public PriorityQueue() : this(Comparer.Default) { } public PriorityQueue(IComparer comparer) { this.data = new List(); this.comparer = comparer; this.elementIndexMap = new Dictionary(); this._count = 0; } public void Enqueue(T item) { data.Add(item); int childIndex = data.Count - 1; elementIndexMap[item] = childIndex; HeapifyUp(childIndex); _count = data.Count; } public T Dequeue() { if (data.Count == 0) throw new InvalidOperationException("The priority queue is empty."); int lastIndex = data.Count - 1; T frontItem = data[0]; _lastDequeued = frontItem; data[0] = data[lastIndex]; data.RemoveAt(lastIndex); elementIndexMap.Remove(frontItem); if (data.Count > 0) { elementIndexMap[data[0]] = 0; HeapifyDown(0); } _count = data.Count; return frontItem; } public bool Remove(T item) { if (!elementIndexMap.TryGetValue(item, out int index)) return false; int lastIndex = data.Count - 1; if (index == lastIndex) { data.RemoveAt(lastIndex); elementIndexMap.Remove(item); _count = data.Count; return true; } data[index] = data[lastIndex]; data.RemoveAt(lastIndex); elementIndexMap.Remove(item); if (index < data.Count) { elementIndexMap[data[index]] = index; int parentIndex = (index - 1) / 2; if (index > 0 && comparer.Compare(data[index], data[parentIndex]) < 0) HeapifyUp(index); else HeapifyDown(index); } _count = data.Count; return true; } public void UpdatePriority(T item, float newPriority) { if (_lastDequeued != null && EqualityComparer.Default.Equals(item, _lastDequeued)) return; if (!elementIndexMap.TryGetValue(item, out int index)) return; int parentIndex = (index - 1) / 2; if (index > 0 && comparer.Compare(data[index], data[parentIndex]) < 0) HeapifyUp(index); else HeapifyDown(index); } private void HeapifyUp(int index) { int parentIndex = (index - 1) / 2; while (index > 0 && comparer.Compare(data[index], data[parentIndex]) < 0) { Swap(index, parentIndex); index = parentIndex; parentIndex = (index - 1) / 2; } } private void HeapifyDown(int index) { int lastIndex = data.Count - 1; while (true) { int leftChildIndex = 2 * index + 1; if (leftChildIndex > lastIndex) break; int rightChildIndex = leftChildIndex + 1; int smallestChildIndex = leftChildIndex; if (rightChildIndex <= lastIndex && comparer.Compare(data[rightChildIndex], data[leftChildIndex]) < 0) smallestChildIndex = rightChildIndex; if (comparer.Compare(data[index], data[smallestChildIndex]) <= 0) break; Swap(index, smallestChildIndex); index = smallestChildIndex; } } private void Swap(int index1, int index2) { T tmp = data[index1]; data[index1] = data[index2]; data[index2] = tmp; elementIndexMap[data[index1]] = index1; elementIndexMap[data[index2]] = index2; } public int Count => _count; public IEnumerator GetEnumerator() { return data.GetEnumerator(); } } #endregion #region Dijkstra Implementation /// /// Implementasi algoritma Dijkstra yang melakukan pencarian secara merata /// ke semua arah untuk menemukan jalur terpendek /// /// Tipe data nilai yang disimpan dalam node public class DijkstraPathFinder : PathFinder { private HashSet closedSet; private Dictionary openListMap; private bool isGridLarge = false; private int estimatedNodesCount = 0; public DijkstraPathFinder(int estimatedNodeCount = 0) { this.estimatedNodesCount = estimatedNodeCount; int initialCapacity = estimatedNodesCount > 0 ? Math.Min(estimatedNodesCount / 4, 256) : 16; isGridLarge = estimatedNodesCount > 2500; closedSet = new HashSet(initialCapacity); openListMap = new Dictionary(initialCapacity); } protected override void AlgorithmSpecificImplementation(Node cell) { if (!closedSet.Contains(cell.Value)) { float G = CurrentNode.GCost + NodeTraversalCost( CurrentNode.Location.Value, cell.Value); float H = 0.0f; if (!openListMap.TryGetValue(cell.Value, out PathFinderNode existingNode)) { PathFinderNode n = new PathFinderNode(cell, CurrentNode, G, H); openList.Add(n); openListMap[cell.Value] = n; onAddToOpenList?.Invoke(n); } else { float oldG = existingNode.GCost; if (G < oldG) { existingNode.Parent = CurrentNode; existingNode.SetGCost(G); onAddToOpenList?.Invoke(existingNode); } } } } public override PathFinderStatus Step() { if (CurrentNode == null) { if (openList.Count == 0) { Status = PathFinderStatus.FAILURE; onFailure?.Invoke(); return Status; } CurrentNode = GetLeastCostNode(openList); openList.Remove(CurrentNode); openListMap.Remove(CurrentNode.Location.Value); onChangeCurrentNode?.Invoke(CurrentNode); } if (!closedSet.Contains(CurrentNode.Location.Value)) { closedList.Add(CurrentNode); closedSet.Add(CurrentNode.Location.Value); onAddToClosedList?.Invoke(CurrentNode); } if (EqualityComparer.Default.Equals(CurrentNode.Location.Value, Goal.Value)) { Status = PathFinderStatus.SUCCESS; onDestinationFound?.Invoke(CurrentNode); onSuccess?.Invoke(); return Status; } List> neighbours = CurrentNode.Location.GetNeighbours(); foreach (Node cell in neighbours) { AlgorithmSpecificImplementation(cell); } if (openList.Count == 0) { Status = PathFinderStatus.FAILURE; onFailure?.Invoke(); return Status; } CurrentNode = GetLeastCostNode(openList); openList.Remove(CurrentNode); openListMap.Remove(CurrentNode.Location.Value); onChangeCurrentNode?.Invoke(CurrentNode); Status = PathFinderStatus.RUNNING; onRunning?.Invoke(); return Status; } public override void Reset() { base.Reset(); closedSet.Clear(); openListMap.Clear(); } } #endregion #region A* Implementation /// /// Implementasi algoritma A* (A-Star) yang menggunakan informasi heuristik /// untuk menemukan jalur terpendek dari titik awal ke titik akhir /// /// Tipe data nilai yang disimpan dalam node public class AStarPathFinder : PathFinder { private new PriorityQueue openList; private Dictionary openListMap; private HashSet closedSet; private bool processingBatch = false; private List> neighborBatch; private bool isGridLarge = false; private int estimatedNodesCount = 0; public AStarPathFinder(int estimatedNodeCount = 0) { this.estimatedNodesCount = estimatedNodeCount; int initialCapacity = estimatedNodesCount > 0 ? Math.Min(estimatedNodesCount / 4, 256) : 16; isGridLarge = estimatedNodesCount > 2500; openList = new PriorityQueue(new FCostComparer()); openListMap = new Dictionary(initialCapacity); closedSet = new HashSet(initialCapacity); if (isGridLarge) { neighborBatch = new List>(8); } else { neighborBatch = new List>(4); // Lebih kecil untuk grid kecil } } protected override void AlgorithmSpecificImplementation(Node cell) { if (closedSet.Contains(cell.Value)) return; float G = CurrentNode.GCost + NodeTraversalCost(CurrentNode.Location.Value, cell.Value); PathFinderNode existingNode = null; bool nodeExists = openListMap.TryGetValue(cell.Value, out existingNode); if (!nodeExists) { float H = HeuristicCost(cell.Value, Goal.Value); PathFinderNode n = new PathFinderNode(cell, CurrentNode, G, H); openList.Enqueue(n); openListMap[cell.Value] = n; if (!processingBatch || !isGridLarge) onAddToOpenList?.Invoke(n); } else if (G < existingNode.GCost) { existingNode.Parent = CurrentNode; existingNode.SetGCost(G); openList.UpdatePriority(existingNode, existingNode.HCost); if ((!processingBatch || !isGridLarge) && onAddToOpenList != null) onAddToOpenList.Invoke(existingNode); } } public override PathFinderStatus Step() { if (CurrentNode == null) { if (openList.Count == 0) { Status = PathFinderStatus.FAILURE; onFailure?.Invoke(); return Status; } CurrentNode = openList.Dequeue(); openListMap.Remove(CurrentNode.Location.Value); onChangeCurrentNode?.Invoke(CurrentNode); } if (EqualityComparer.Default.Equals(CurrentNode.Location.Value, Goal.Value)) { Status = PathFinderStatus.SUCCESS; onDestinationFound?.Invoke(CurrentNode); onSuccess?.Invoke(); return Status; } closedList.Add(CurrentNode); closedSet.Add(CurrentNode.Location.Value); onAddToClosedList?.Invoke(CurrentNode); List> neighbors; if (isGridLarge) { neighborBatch.Clear(); neighborBatch.AddRange(CurrentNode.Location.GetNeighbours()); neighbors = neighborBatch; processingBatch = neighbors.Count > 5; } else { neighbors = CurrentNode.Location.GetNeighbours(); processingBatch = false; } foreach (Node cell in neighbors) { AlgorithmSpecificImplementation(cell); } if (processingBatch && onAddToOpenList != null && isGridLarge) { onAddToOpenList.Invoke(CurrentNode); processingBatch = false; } if (openList.Count == 0) { Status = PathFinderStatus.FAILURE; onFailure?.Invoke(); return Status; } CurrentNode = openList.Dequeue(); openListMap.Remove(CurrentNode.Location.Value); onChangeCurrentNode?.Invoke(CurrentNode); Status = PathFinderStatus.RUNNING; onRunning?.Invoke(); return Status; } public override void Reset() { base.Reset(); openListMap.Clear(); closedSet.Clear(); if (isGridLarge && neighborBatch != null) neighborBatch.Clear(); processingBatch = false; } private class FCostComparer : IComparer { public int Compare(PathFinderNode x, PathFinderNode y) { int result = x.FCost.CompareTo(y.FCost); if (result == 0) { result = x.HCost.CompareTo(y.HCost); // Tie-breaking dengan H cost } return result; } } } #endregion #region Greedy Best-First Search /// /// Implementasi algoritma Greedy Best-First Search /// Algoritma ini hanya mempertimbangkan biaya heuristik (H) ke tujuan /// /// Tipe data nilai yang disimpan dalam node public class GreedyPathFinder : PathFinder { private new PriorityQueue openList = new PriorityQueue(new HeuristicComparer()); private Dictionary openSet = new Dictionary(256); private HashSet closedSet = new HashSet(256); private bool processingBatch = false; private List> neighborBatch = new List>(4); protected override void AlgorithmSpecificImplementation(Node cell) { if (closedSet.Contains(cell.Value)) return; float G = CurrentNode.GCost + NodeTraversalCost(CurrentNode.Location.Value, cell.Value); float H; PathFinderNode existingNode = null; bool nodeExists = openSet.TryGetValue(cell.Value, out existingNode); if (!nodeExists) { if (EqualityComparer.Default.Equals(cell.Value, Goal.Value)) { H = 0; } else { H = HeuristicCost(cell.Value, Goal.Value); } PathFinderNode n = new PathFinderNode(cell, CurrentNode, G, H); openList.Enqueue(n); onAddToOpenList?.Invoke(n); openSet[cell.Value] = n; } else if (G < existingNode.GCost) { existingNode.Parent = CurrentNode; existingNode.SetGCost(G); openList.UpdatePriority(existingNode, existingNode.HCost); onAddToOpenList.Invoke(existingNode); } } public override PathFinderStatus Step() { if (CurrentNode == null) { if (openList.Count == 0) { Status = PathFinderStatus.FAILURE; onFailure?.Invoke(); return Status; } CurrentNode = openList.Dequeue(); openSet.Remove(CurrentNode.Location.Value); onChangeCurrentNode?.Invoke(CurrentNode); } closedList.Add(CurrentNode); closedSet.Add(CurrentNode.Location.Value); onAddToClosedList?.Invoke(CurrentNode); if (EqualityComparer.Default.Equals(CurrentNode.Location.Value, Goal.Value)) { Status = PathFinderStatus.SUCCESS; onDestinationFound?.Invoke(CurrentNode); onSuccess?.Invoke(); return Status; } neighborBatch.Clear(); neighborBatch.AddRange(CurrentNode.Location.GetNeighbours()); foreach (Node cell in neighborBatch) { AlgorithmSpecificImplementation(cell); } if (processingBatch && onAddToOpenList != null) { onAddToOpenList.Invoke(CurrentNode); processingBatch = false; } if (openList.Count == 0) { Status = PathFinderStatus.FAILURE; onFailure?.Invoke(); return Status; } CurrentNode = openList.Dequeue(); openSet.Remove(CurrentNode.Location.Value); onChangeCurrentNode?.Invoke(CurrentNode); Status = PathFinderStatus.RUNNING; onRunning?.Invoke(); return Status; } public override void Reset() { base.Reset(); openSet.Clear(); closedSet.Clear(); neighborBatch.Clear(); processingBatch = false; } private class HeuristicComparer : IComparer { public int Compare(PathFinderNode x, PathFinderNode y) { int result = x.HCost.CompareTo(y.HCost); if (result == 0) { result = x.GCost.CompareTo(y.GCost); } return result; } } } #endregion #region Backtracking Algorithm /// /// Implementasi algoritma Backtracking untuk pencarian jalur /// Menggunakan pendekatan depth-first dengan backtracking /// /// Tipe data nilai yang disimpan dalam node public class BacktrackingPathFinder : PathFinder { private Stack openStack = new Stack(); private HashSet closedSet = new HashSet(); private HashSet openSet = new HashSet(); protected override void AlgorithmSpecificImplementation(Node cell) { if (!closedSet.Contains(cell.Value) && !openSet.Contains(cell.Value)) { float G = CurrentNode.GCost + NodeTraversalCost(CurrentNode.Location.Value, cell.Value); float H = 0.0f; PathFinderNode n = new PathFinderNode(cell, CurrentNode, G, H); openList.Add(n); openStack.Push(n); openSet.Add(cell.Value); onAddToOpenList?.Invoke(n); } } public override PathFinderStatus Step() { if (CurrentNode == null) { if (openStack.Count == 0) { Status = PathFinderStatus.FAILURE; onFailure?.Invoke(); return Status; } CurrentNode = openStack.Pop(); openList.Remove(CurrentNode); openSet.Remove(CurrentNode.Location.Value); onChangeCurrentNode?.Invoke(CurrentNode); } closedList.Add(CurrentNode); closedSet.Add(CurrentNode.Location.Value); onAddToClosedList?.Invoke(CurrentNode); if (EqualityComparer.Default.Equals(CurrentNode.Location.Value, Goal.Value)) { Status = PathFinderStatus.SUCCESS; onDestinationFound?.Invoke(CurrentNode); onSuccess?.Invoke(); return Status; } List> neighbours = CurrentNode.Location.GetNeighbours(); foreach (Node cell in neighbours) { AlgorithmSpecificImplementation(cell); } if (openStack.Count == 0) { Status = PathFinderStatus.FAILURE; onFailure?.Invoke(); return Status; } CurrentNode = openStack.Pop(); openList.Remove(CurrentNode); openSet.Remove(CurrentNode.Location.Value); onChangeCurrentNode?.Invoke(CurrentNode); Status = PathFinderStatus.RUNNING; onRunning?.Invoke(); return Status; } public override void Reset() { base.Reset(); openStack.Clear(); closedSet.Clear(); openSet.Clear(); } } #endregion #region Breath-First Search Algorithm /// /// Implementasi algoritma Breadth-First Search (BFS) /// Algoritma ini menjelajahi semua node pada jarak yang sama dari /// titik awal sebelum bergerak ke node yang lebih jauh /// /// Tipe data nilai yang disimpan dalam node public class BFSPathFinder : PathFinder { private Queue openQueue = new Queue(); private HashSet closedSet = new HashSet(); private HashSet openSet = new HashSet(); protected override void AlgorithmSpecificImplementation(Node cell) { if (!closedSet.Contains(cell.Value) && !openSet.Contains(cell.Value)) { float G = CurrentNode.GCost + NodeTraversalCost( CurrentNode.Location.Value, cell.Value); float H = 0.0f; PathFinderNode n = new PathFinderNode(cell, CurrentNode, G, H); openList.Add(n); openQueue.Enqueue(n); openSet.Add(cell.Value); onAddToOpenList?.Invoke(n); } } public override PathFinderStatus Step() { if (CurrentNode == null) { if (openQueue.Count == 0) { Status = PathFinderStatus.FAILURE; onFailure?.Invoke(); return Status; } CurrentNode = openQueue.Dequeue(); openList.Remove(CurrentNode); openSet.Remove(CurrentNode.Location.Value); onChangeCurrentNode?.Invoke(CurrentNode); } closedList.Add(CurrentNode); closedSet.Add(CurrentNode.Location.Value); onAddToClosedList?.Invoke(CurrentNode); if (EqualityComparer.Default.Equals(CurrentNode.Location.Value, Goal.Value)) { Status = PathFinderStatus.SUCCESS; onDestinationFound?.Invoke(CurrentNode); onSuccess?.Invoke(); return Status; } List> neighbours = CurrentNode.Location.GetNeighbours(); foreach (Node cell in neighbours) { AlgorithmSpecificImplementation(cell); } if (openQueue.Count == 0) { Status = PathFinderStatus.FAILURE; onFailure?.Invoke(); return Status; } CurrentNode = openQueue.Dequeue(); openList.Remove(CurrentNode); openSet.Remove(CurrentNode.Location.Value); onChangeCurrentNode?.Invoke(CurrentNode); Status = PathFinderStatus.RUNNING; onRunning?.Invoke(); return Status; } public override void Reset() { base.Reset(); openQueue.Clear(); closedSet.Clear(); openSet.Clear(); } } #endregion }