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
}