using System; using System.Collections.Generic; namespace PathFinding { /// /// Enumerasi yang merepresentasikan berbagai status dari PathFinder. /// Digunakan untuk melacak progress dari pencarian jalur (pathfinding). /// public enum PathFinderStatus { NOT_INITIALISED, // PathFinder belum diinisialisasi SUCCESS, // Pencarian jalur berhasil menemukan tujuan FAILURE, // Pencarian jalur gagal (tidak ada jalur ditemukan) RUNNING, // Proses pencarian jalur sedang berjalan } /// /// 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 { /// /// Nilai yang disimpan dalam node /// public T Value { get; private set; } /// /// Konstruktor untuk membuat node baru dengan nilai tertentu /// /// Nilai yang akan disimpan dalam node public Node(T value) { Value = value; } /// /// Mendapatkan daftar tetangga dari node ini. /// Metode ini harus diimplementasikan oleh kelas turunan. /// /// Daftar tetangga dari node ini 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. /// /// Delegate untuk menghitung biaya perjalanan antara dua node /// /// Node asal /// Node tujuan /// Biaya perjalanan dari a ke b public delegate float CostFunction(T a, T b); /// /// Mendapatkan jumlah node dalam daftar tertutup (closedList) /// public int ClosedListCount => closedList.Count; public int OpenListCount => openList.Count; /// /// Fungsi untuk menghitung biaya heuristik antara dua node /// Digunakan dalam algoritma informasi seperti A* dan Greedy Best-First /// public CostFunction HeuristicCost { get; set; } /// /// Fungsi untuk menghitung biaya perjalanan antara dua node yang bertetangga /// 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 { /// /// Node induk dalam jalur pencarian /// public PathFinderNode Parent { get; set; } /// /// Lokasi node dalam struktur graph /// public Node Location { get; private set; } /// /// Referensi ke peta grid /// public GridMap Map { get; set; } /// /// Total biaya (F = G + H) /// public float FCost { get; private set; } /// /// Biaya dari node awal ke node saat ini (cost so far) /// public float GCost { get; private set; } /// /// Biaya heuristik dari node saat ini ke tujuan (estimated cost) /// public float HCost { get; private set; } /// /// Konstruktor untuk PathFinderNode /// /// Lokasi node dalam struktur graph /// Node induk dalam jalur pencarian /// Biaya dari node awal ke node saat ini /// Biaya heuristik dari node saat ini ke tujuan public PathFinderNode(Node location, PathFinderNode parent, float gCost, float hCost) { Location = location; Parent = parent; HCost = hCost; SetGCost(gCost); } /// /// Mengatur biaya G dan menghitung ulang biaya F /// /// Nilai baru untuk biaya G public void SetGCost(float c) { GCost = c; FCost = GCost + HCost; } /// /// Mengatur biaya H dan menghitung ulang biaya F /// /// Nilai baru untuk biaya H public void SetHCost(float h) { HCost = h; FCost = GCost + HCost; } /// /// Membandingkan node berdasarkan biaya F /// Digunakan untuk menyortir prioritas node dalam pencarian /// /// Node lain yang dibandingkan /// Hasil perbandingan nilai FCost public int CompareTo(PathFinderNode other) { if (other == null) return 1; return FCost.CompareTo(other.FCost); } } #endregion #region Properties /// /// Status saat ini dari pathfinder /// Nilai default-nya adalah NOT_INITIALISED /// public PathFinderStatus Status { get; protected set; } = PathFinderStatus.NOT_INITIALISED; /// /// Node awal pencarian jalur /// public Node Start { get; protected set; } /// /// Node tujuan pencarian jalur /// public Node Goal { get; protected set; } /// /// Node yang sedang diproses saat ini oleh pathfinder /// public PathFinderNode CurrentNode { get; protected set; } /// /// Referensi ke peta grid yang digunakan untuk pencarian jalur /// public GridMap Map { get; internal set; } #endregion #region Open and Closed Lists and Associated Functions. /// /// Daftar node yang belum diperiksa (open list) /// Node dalam daftar ini akan diproses di langkah berikutnya /// protected List openList = new List(); /// /// Daftar node yang sudah diperiksa (closed list) /// Node dalam daftar ini sudah dievaluasi /// protected List closedList = new List(); /// /// Mendapatkan node dengan biaya terendah dari suatu daftar /// Digunakan untuk memilih node berikutnya yang akan diperiksa /// /// Daftar node yang akan diperiksa /// Node dengan biaya terendah 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; } /// /// Memeriksa apakah suatu cell (nilai T) ada dalam daftar node /// /// Daftar node yang akan diperiksa /// Cell yang dicari /// Indeks cell dalam daftar jika ditemukan, -1 jika tidak 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 /// /// Delegate untuk menangani event terkait node selama proses pathfinding /// Dapat digunakan untuk visualisasi atau debugging /// /// Node yang terlibat dalam event public delegate void DelegatePathFinderNode(PathFinderNode node); /// /// Event dipanggil ketika current node berubah /// public DelegatePathFinderNode onChangeCurrentNode; /// /// Event dipanggil ketika node ditambahkan ke open list /// public DelegatePathFinderNode onAddToOpenList; /// /// Event dipanggil ketika node ditambahkan ke closed list /// public DelegatePathFinderNode onAddToClosedList; /// /// Event dipanggil ketika node tujuan ditemukan /// public DelegatePathFinderNode onDestinationFound; /// /// Delegate untuk menangani event tanpa parameter /// public delegate void DelegateNoArguments(); /// /// Event dipanggil ketika pencarian jalur dimulai /// public DelegateNoArguments onStarted; /// /// Event dipanggil ketika pencarian jalur sedang berjalan /// public DelegateNoArguments onRunning; /// /// Event dipanggil ketika pencarian jalur gagal /// public DelegateNoArguments onFailure; /// /// Event dipanggil ketika pencarian jalur berhasil /// public DelegateNoArguments onSuccess; #endregion #region Pathfinding Search Related Functions /// /// Mereset variabel internal untuk pencarian baru /// public virtual void Reset() { if (Status == PathFinderStatus.RUNNING) { // Tidak bisa reset karena pathfinding sedang berlangsung return; } CurrentNode = null; openList.Clear(); closedList.Clear(); Status = PathFinderStatus.NOT_INITIALISED; } /// /// Melakukan satu langkah pencarian jalur /// Harus dipanggil berulang kali sampai Status menjadi SUCCESS atau FAILURE /// /// Status pathfinder setelah langkah ini selesai public virtual PathFinderStatus Step() { closedList.Add(CurrentNode); onAddToClosedList?.Invoke(CurrentNode); if (openList.Count == 0) { // Pencarian telah selesai tanpa menemukan jalur Status = PathFinderStatus.FAILURE; onFailure?.Invoke(); return Status; } // Dapatkan node dengan biaya terendah dari openList CurrentNode = GetLeastCostNode(openList); onChangeCurrentNode?.Invoke(CurrentNode); openList.Remove(CurrentNode); // Periksa apakah node ini mengandung cell tujuan if (EqualityComparer.Default.Equals( CurrentNode.Location.Value, Goal.Value)) { Status = PathFinderStatus.SUCCESS; onDestinationFound?.Invoke(CurrentNode); onSuccess?.Invoke(); return Status; } // Dapatkan tetangga dari node saat ini List> neighbours = CurrentNode.Location.GetNeighbours(); // Proses setiap tetangga untuk kemungkinan ekspansi pencarian foreach (Node cell in neighbours) { AlgorithmSpecificImplementation(cell); } Status = PathFinderStatus.RUNNING; onRunning?.Invoke(); return Status; } /// /// Implementasi algoritma spesifik untuk memproses node tetangga /// Metode ini akan diimplementasikan berbeda untuk setiap algoritma pathfinding /// /// Node tetangga yang akan diproses abstract protected void AlgorithmSpecificImplementation(Node cell); /// /// Inisialisasi pathfinder dengan node awal dan tujuan /// /// Node awal pencarian /// Node tujuan pencarian /// True jika inisialisasi berhasil, False jika gagal public virtual bool Initialise(Node start, Node goal) { if (Status == PathFinderStatus.RUNNING) { // Pathfinding sedang berlangsung, tidak bisa diinisialisasi ulang return false; } Reset(); Start = start; Goal = goal; // Deteksi dini jika start dan goal sama if (EqualityComparer.Default.Equals(Start.Value, Goal.Value)) { // Pada kasus start=goal, tidak perlu menjalankan algoritma pencarian // Cukup atur CurrentNode dan Status secara langsung CurrentNode = new PathFinderNode(Start, null, 0.0f, 0.0f); // Panggil callback sesuai urutan normal onChangeCurrentNode?.Invoke(CurrentNode); onStarted?.Invoke(); onDestinationFound?.Invoke(CurrentNode); // Atur status ke SUCCESS 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 } #region Priority Queue /// /// Memprioritaskan item berdasarkan nilai komparatif mereka /// /// Tipe item dalam antrian prioritas public class PriorityQueue where T : IComparable { /// /// Data yang disimpan dalam priority queue /// private List data; /// /// Pembanding untuk menentukan prioritas item /// private IComparer comparer; /// /// Menyimpan indeks setiap elemen dalam data untuk akses cepat /// private Dictionary elementIndexMap; // Cache untuk optimasi private T _lastDequeued; private int _count; /// /// Konstruktor default menggunakan pembanding default untuk tipe T /// public PriorityQueue() : this(Comparer.Default) { } /// /// Konstruktor dengan custom comparer /// /// Pembanding untuk menentukan prioritas item public PriorityQueue(IComparer comparer) { this.data = new List(); this.comparer = comparer; this.elementIndexMap = new Dictionary(); this._count = 0; } /// /// Menambahkan item ke dalam antrian prioritas /// /// Item yang akan ditambahkan public void Enqueue(T item) { data.Add(item); int childIndex = data.Count - 1; elementIndexMap[item] = childIndex; HeapifyUp(childIndex); _count = data.Count; } /// /// Mengambil dan menghapus item dengan prioritas tertinggi /// /// Item dengan prioritas tertinggi 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; } /// /// Menghapus item tertentu dari antrian /// /// Item yang akan dihapus /// True jika berhasil dihapus, False jika tidak public bool Remove(T item) { if (!elementIndexMap.TryGetValue(item, out int index)) return false; int lastIndex = data.Count - 1; if (index == lastIndex) { // Item yang dihapus adalah item terakhir 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; // Tentukan apakah perlu heapify up atau down 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; } /// /// Memperbarui prioritas item dalam antrian /// /// Item yang akan diperbarui /// Nilai prioritas baru public void UpdatePriority(T item, float newPriority) { // Fast check - jika item adalah yang terakhir dihapus, jangan lakukan apa-apa if (_lastDequeued != null && EqualityComparer.Default.Equals(item, _lastDequeued)) return; if (!elementIndexMap.TryGetValue(item, out int index)) return; // Lakukan heapify - lebih efisien untuk A* pada grid kecil int parentIndex = (index - 1) / 2; if (index > 0 && comparer.Compare(data[index], data[parentIndex]) < 0) HeapifyUp(index); else HeapifyDown(index); } /// /// Menjaga sifat heap dengan pergerakan ke atas /// /// Indeks node yang akan diperbaiki posisinya private void HeapifyUp(int index) { // Iteratif lebih cepat daripada rekursif untuk grid kecil int parentIndex = (index - 1) / 2; while (index > 0 && comparer.Compare(data[index], data[parentIndex]) < 0) { Swap(index, parentIndex); index = parentIndex; parentIndex = (index - 1) / 2; } } /// /// Menjaga sifat heap dengan pergerakan ke bawah /// /// Indeks node yang akan diperbaiki posisinya 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; // Fast check untuk rightChildIndex 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; } } /// /// Menukar posisi dua item dalam heap /// /// Indeks item pertama /// Indeks item kedua 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; } /// /// Jumlah item dalam antrian /// public int Count => _count; /// /// Mendapatkan enumerator untuk data /// /// Enumerator untuk data 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 { /// /// HashSet untuk pemeriksaan closed list dengan cepat (O(1) complexity) /// private HashSet closedSet; /// /// Dictionary untuk mengakses node di open list dengan cepat berdasarkan nilai mereka /// private Dictionary openListMap; // Flag untuk mengontrol level optimisasi berdasarkan ukuran grid private bool isGridLarge = false; private int estimatedNodesCount = 0; /// /// Constructor baru dengan estimasi jumlah node /// public DijkstraPathFinder(int estimatedNodeCount = 0) { // Estimasi ukuran grid untuk optimisasi memory this.estimatedNodesCount = estimatedNodeCount; // Tentukan kapasitas awal berdasarkan ukuran grid int initialCapacity = estimatedNodesCount > 0 ? Math.Min(estimatedNodesCount / 4, 256) : 16; // Grid dianggap besar jika memiliki > 2500 node (50x50) isGridLarge = estimatedNodesCount > 2500; // Alokasi dengan kapasitas yang sesuai closedSet = new HashSet(initialCapacity); openListMap = new Dictionary(initialCapacity); } /// /// Implementasi spesifik algoritma Dijkstra untuk memproses node tetangga /// /// Node tetangga yang akan diproses protected override void AlgorithmSpecificImplementation(Node cell) { // Pemeriksaan O(1) dengan HashSet // Melakukan pemeriksaan apakah node sudah ada di closed list if (!closedSet.Contains(cell.Value)) { float G = CurrentNode.GCost + NodeTraversalCost( CurrentNode.Location.Value, cell.Value); // Biaya heuristik untuk Dijkstra adalah 0 float H = 0.0f; // Pemeriksaan O(1) dengan Dictionary // Melakukan pemeriksaan apakah node sudah ada di open list 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); } } } } /// /// Melakukan satu langkah pencarian jalur dengan algoritma Dijkstra /// /// Status pathfinder setelah langkah ini selesai 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); } // Pindahkan pemeriksaan duplikasi ke sini 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; } /// /// Reset state pathfinder /// 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 { /// /// Open list diimplementasikan sebagai priority queue untuk efisiensi /// private new PriorityQueue openList; /// /// Peta untuk mengakses node dalam open list dengan cepat /// private Dictionary openListMap; /// /// HashSet untuk memeriksa closed list dengan cepat (O(1) complexity) /// private HashSet closedSet; // Flag untuk batch processing - hanya diaktifkan untuk grid besar private bool processingBatch = false; private List> neighborBatch; // Flag untuk mengontrol level optimisasi private bool isGridLarge = false; private int estimatedNodesCount = 0; /// /// Constructor baru dengan estimasi jumlah node /// public AStarPathFinder(int estimatedNodeCount = 0) { // Estimasi ukuran grid untuk optimisasi memory this.estimatedNodesCount = estimatedNodeCount; // Tentukan kapasitas awal berdasarkan ukuran grid int initialCapacity = estimatedNodesCount > 0 ? Math.Min(estimatedNodesCount / 4, 256) : 16; // Grid dianggap besar jika memiliki > 2500 node (50x50) isGridLarge = estimatedNodesCount > 2500; // Alokasi dengan kapasitas yang sesuai openList = new PriorityQueue(new FCostComparer()); openListMap = new Dictionary(initialCapacity); closedSet = new HashSet(initialCapacity); // Alokasi neighborBatch hanya jika diperlukan if (isGridLarge) { neighborBatch = new List>(8); } else { neighborBatch = new List>(4); // Lebih kecil untuk grid kecil } } /// /// Implementasi spesifik algoritma A* untuk memproses node tetangga /// /// Node tetangga yang akan diproses protected override void AlgorithmSpecificImplementation(Node cell) { // Fast-reject: node sudah di closed list if (closedSet.Contains(cell.Value)) return; // Hitung biaya G yang sebenarnya (jarak dari start) float G = CurrentNode.GCost + NodeTraversalCost(CurrentNode.Location.Value, cell.Value); // Periksa apakah node sudah ada di open list PathFinderNode existingNode = null; bool nodeExists = openListMap.TryGetValue(cell.Value, out existingNode); if (!nodeExists) { // Hitung heuristik dengan normal float H = HeuristicCost(cell.Value, Goal.Value); // Buat node baru dan tambahkan ke open list PathFinderNode n = new PathFinderNode(cell, CurrentNode, G, H); openList.Enqueue(n); openListMap[cell.Value] = n; // Callback hanya jika tidak dalam batch processing if (!processingBatch || !isGridLarge) onAddToOpenList?.Invoke(n); } else if (G < existingNode.GCost) { // Path lebih baik ditemukan, update nilai G existingNode.Parent = CurrentNode; existingNode.SetGCost(G); // Jika kita menggunakan G sebagai tie-breaker, maka perlu update prioritas // karena G yang lebih rendah sekarang bisa mempengaruhi urutan priority queue openList.UpdatePriority(existingNode, existingNode.HCost); // Callback untuk UI if ((!processingBatch || !isGridLarge) && onAddToOpenList != null) onAddToOpenList.Invoke(existingNode); } } /// /// Melakukan satu langkah pencarian jalur dengan algoritma A* /// /// Status pathfinder setelah langkah ini selesai 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); // Tambahkan ke hashset untuk pemeriksaan O(1) onAddToClosedList?.Invoke(CurrentNode); // Ambil semua tetangga List> neighbors; // Untuk grid besar, gunakan batch processing if (isGridLarge) { neighborBatch.Clear(); neighborBatch.AddRange(CurrentNode.Location.GetNeighbours()); neighbors = neighborBatch; // Gunakan batch hanya jika cukup banyak tetangga dan grid besar processingBatch = neighbors.Count > 5; } else { // Untuk grid kecil, tidak perlu batch processing neighbors = CurrentNode.Location.GetNeighbours(); processingBatch = false; } // Proses semua tetangga foreach (Node cell in neighbors) { AlgorithmSpecificImplementation(cell); } // Jika dalam batch mode, invoke callback sekali untuk semua tetangga if (processingBatch && onAddToOpenList != null && isGridLarge) { // Notify UI untuk refresh (optional) 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; } /// /// Reset state pathfinder /// public override void Reset() { base.Reset(); openListMap.Clear(); closedSet.Clear(); if (isGridLarge && neighborBatch != null) neighborBatch.Clear(); processingBatch = false; } /// /// Pembanding F-Cost untuk priority queue dalam algoritma A* /// private class FCostComparer : IComparer { /// /// Membandingkan dua node berdasarkan FCost mereka /// Jika FCost sama, gunakan HCost sebagai tie-breaker /// /// Node pertama /// Node kedua /// Hasil perbandingan 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 { /// /// Open list diimplementasikan sebagai priority queue untuk performa yang lebih baik /// private new PriorityQueue openList = new PriorityQueue(new HeuristicComparer()); /// /// Dictionary untuk akses node open list yang cepat /// private Dictionary openSet = new Dictionary(256); /// /// HashSet untuk pemeriksaan closed list O(1) /// private HashSet closedSet = new HashSet(256); // Flag untuk batch processing private bool processingBatch = false; private List> neighborBatch = new List>(4); /// /// Implementasi spesifik algoritma Greedy untuk memproses node tetangga /// /// Node tetangga yang akan diproses protected override void AlgorithmSpecificImplementation(Node cell) { // Fast-reject: node sudah diproses if (closedSet.Contains(cell.Value)) return; // Hitung biaya G yang sebenarnya (jarak dari start) float G = CurrentNode.GCost + NodeTraversalCost(CurrentNode.Location.Value, cell.Value); float H; // Periksa apakah node sudah ada di open list PathFinderNode existingNode = null; bool nodeExists = openSet.TryGetValue(cell.Value, out existingNode); if (!nodeExists) { // Hitung heuristik dengan normal if (EqualityComparer.Default.Equals(cell.Value, Goal.Value)) { // Langsung ke tujuan - prioritaskan dengan H = 0 H = 0; } else { H = HeuristicCost(cell.Value, Goal.Value); } // Buat node dan tambahkan ke open list - gunakan G yang benar PathFinderNode n = new PathFinderNode(cell, CurrentNode, G, H); openList.Enqueue(n); onAddToOpenList?.Invoke(n); openSet[cell.Value] = n; //if (!processingBatch && onAddToOpenList != null) // onAddToOpenList.Invoke(n); } else if (G < existingNode.GCost) { // Path lebih baik ditemukan, update nilai G // Meskipun Greedy hanya menggunakan H untuk prioritas, // G cost yang benar tetap penting untuk rekonstruksi jalur existingNode.Parent = CurrentNode; existingNode.SetGCost(G); // Jika kita menggunakan G sebagai tie-breaker, maka perlu update prioritas // karena G yang lebih rendah sekarang bisa mempengaruhi urutan priority queue openList.UpdatePriority(existingNode, existingNode.HCost); onAddToOpenList.Invoke(existingNode); // Callback untuk UI //if (!processingBatch && onAddToOpenList != null) } } /// /// Melakukan satu langkah pencarian jalur dengan algoritma Greedy /// /// Status pathfinder setelah langkah ini selesai 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; } // Dapatkan semua tetangga sekaligus untuk mengurangi panggilan fungsi neighborBatch.Clear(); neighborBatch.AddRange(CurrentNode.Location.GetNeighbours()); // Proses tetangga dalam batch untuk mengurangi callback overhead // processingBatch = neighborBatch.Count > 5; // Gunakan batch hanya jika cukup banyak tetangga foreach (Node cell in neighborBatch) { AlgorithmSpecificImplementation(cell); } // Jika dalam batch mode, invoke callback sekali untuk semua tetangga if (processingBatch && onAddToOpenList != null) { // Notify UI untuk refresh (optional) 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; } /// /// Reset state pathfinder /// public override void Reset() { base.Reset(); openSet.Clear(); closedSet.Clear(); neighborBatch.Clear(); processingBatch = false; } /// /// Pembanding Heuristik untuk priority queue dalam algoritma Greedy /// private class HeuristicComparer : IComparer { /// /// Membandingkan dua node berdasarkan nilai heuristik mereka. /// PENTING: Ini adalah kunci perbedaan dari algoritma Greedy - prioritas /// hanya didasarkan pada nilai H cost (jarak ke tujuan), tidak seperti /// A* yang menggunakan FCost (G+H). G cost tetap dilacak untuk /// rekonstruksi jalur yang benar, tetapi tidak memengaruhi prioritas. /// /// Node pertama /// Node kedua /// Hasil perbandingan public int Compare(PathFinderNode x, PathFinderNode y) { int result = x.HCost.CompareTo(y.HCost); if (result == 0) { // Jika H cost sama, gunakan G cost sebagai tie-breaker // Pilih jalur dengan G cost lebih rendah (lebih dekat dari start) 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 { /// /// Stack untuk menyimpan node yang akan dikunjungi (open list) /// private Stack openStack = new Stack(); /// /// HashSet untuk pemeriksaan cepat apakah node tertentu sudah dikunjungi /// private HashSet closedSet = new HashSet(); /// /// HashSet untuk pemeriksaan cepat apakah node tertentu sudah ada di open list /// private HashSet openSet = new HashSet(); /// /// Implementasi spesifik algoritma Backtracking untuk memproses node tetangga /// /// Node tetangga yang akan diproses protected override void AlgorithmSpecificImplementation(Node cell) { if (!closedSet.Contains(cell.Value) && !openSet.Contains(cell.Value)) { // Backtracking tidak memperhitungkan cost, tetapi tetap menghitungnya untuk visualisasi 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); // Tambahkan ke list untuk tujuan kompatibilitas openStack.Push(n); // Tambahkan ke stack untuk DFS openSet.Add(cell.Value); onAddToOpenList?.Invoke(n); } } /// /// Melakukan satu langkah pencarian jalur dengan algoritma Backtracking /// /// Status pathfinder setelah langkah ini selesai public override PathFinderStatus Step() { // Jika currentNode belum diinisialisasi if (CurrentNode == null) { // Jika tidak ada node dalam open stack, berarti pencarian gagal if (openStack.Count == 0) { Status = PathFinderStatus.FAILURE; onFailure?.Invoke(); return Status; } // Ambil node berikutnya dari stack (LIFO - kedalaman pertama) CurrentNode = openStack.Pop(); openList.Remove(CurrentNode); // Hapus dari list untuk kompatibilitas openSet.Remove(CurrentNode.Location.Value); onChangeCurrentNode?.Invoke(CurrentNode); } // Tambahkan node saat ini ke closed list closedList.Add(CurrentNode); closedSet.Add(CurrentNode.Location.Value); onAddToClosedList?.Invoke(CurrentNode); // Cek apakah kita telah mencapai tujuan if (EqualityComparer.Default.Equals(CurrentNode.Location.Value, Goal.Value)) { Status = PathFinderStatus.SUCCESS; onDestinationFound?.Invoke(CurrentNode); onSuccess?.Invoke(); return Status; } // Proses semua tetangga dari node saat ini List> neighbours = CurrentNode.Location.GetNeighbours(); foreach (Node cell in neighbours) { AlgorithmSpecificImplementation(cell); } // Jika tidak ada lagi node yang dapat dikunjungi, berarti pencarian gagal if (openStack.Count == 0) { Status = PathFinderStatus.FAILURE; onFailure?.Invoke(); return Status; } // Ambil node berikutnya dari stack CurrentNode = openStack.Pop(); openList.Remove(CurrentNode); // Hapus dari list untuk kompatibilitas openSet.Remove(CurrentNode.Location.Value); onChangeCurrentNode?.Invoke(CurrentNode); Status = PathFinderStatus.RUNNING; onRunning?.Invoke(); return Status; } /// /// Reset state pathfinder /// 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 { /// /// Queue sebagai open list untuk BFS /// private Queue openQueue = new Queue(); /// /// HashSet untuk pemeriksaan nilai node dalam closed list (O(1) complexity) /// private HashSet closedSet = new HashSet(); /// /// HashSet untuk pemeriksaan nilai node dalam open list (O(1) complexity) /// private HashSet openSet = new HashSet(); /// /// Implementasi spesifik algoritma BFS untuk memproses node tetangga /// /// Node tetangga yang akan diproses protected override void AlgorithmSpecificImplementation(Node cell) { if (!closedSet.Contains(cell.Value) && !openSet.Contains(cell.Value)) { // BFS tidak memperhitungkan cost, tapi kita tetap menghitungnya untuk visualisasi 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); // Tambahkan ke list untuk tujuan kompatibilitas openQueue.Enqueue(n); // Tambahkan ke queue untuk BFS openSet.Add(cell.Value); onAddToOpenList?.Invoke(n); } } /// /// Melakukan satu langkah pencarian jalur dengan algoritma BFS /// /// Status pathfinder setelah langkah ini selesai public override PathFinderStatus Step() { // Jika currentNode belum diinisialisasi if (CurrentNode == null) { // Jika tidak ada node dalam open queue, berarti pencarian gagal if (openQueue.Count == 0) { Status = PathFinderStatus.FAILURE; onFailure?.Invoke(); return Status; } // Ambil node berikutnya dari queue (FIFO) CurrentNode = openQueue.Dequeue(); openList.Remove(CurrentNode); // Hapus dari list untuk kompatibilitas openSet.Remove(CurrentNode.Location.Value); onChangeCurrentNode?.Invoke(CurrentNode); } // Tambahkan node saat ini ke closed list closedList.Add(CurrentNode); closedSet.Add(CurrentNode.Location.Value); onAddToClosedList?.Invoke(CurrentNode); // Cek apakah kita telah mencapai tujuan if (EqualityComparer.Default.Equals(CurrentNode.Location.Value, Goal.Value)) { Status = PathFinderStatus.SUCCESS; onDestinationFound?.Invoke(CurrentNode); onSuccess?.Invoke(); return Status; } // Proses semua tetangga dari node saat ini List> neighbours = CurrentNode.Location.GetNeighbours(); foreach (Node cell in neighbours) { AlgorithmSpecificImplementation(cell); } // Jika tidak ada lagi node yang dapat dikunjungi, berarti pencarian gagal if (openQueue.Count == 0) { Status = PathFinderStatus.FAILURE; onFailure?.Invoke(); return Status; } // Ambil node berikutnya dari queue CurrentNode = openQueue.Dequeue(); openList.Remove(CurrentNode); // Hapus dari list untuk kompatibilitas openSet.Remove(CurrentNode.Location.Value); onChangeCurrentNode?.Invoke(CurrentNode); Status = PathFinderStatus.RUNNING; onRunning?.Invoke(); return Status; } /// /// Reset state pathfinder /// public override void Reset() { base.Reset(); openQueue.Clear(); closedSet.Clear(); openSet.Clear(); } } #endregion }