mirror of
https://github.com/BobbyRafael31/Unity-MazeRunner-Pathfinding-Visualizer.git
synced 2025-08-13 08:52:21 +00:00
1481 lines
52 KiB
C#
1481 lines
52 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
|
|
namespace PathFinding
|
|
{
|
|
/// <summary>
|
|
/// Enumerasi yang merepresentasikan berbagai status dari PathFinder.
|
|
/// Digunakan untuk melacak progress dari pencarian jalur (pathfinding).
|
|
/// </summary>
|
|
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
|
|
}
|
|
|
|
/// <summary>
|
|
/// Kelas abstrak Node yang menjadi dasar untuk semua jenis vertex
|
|
/// yang digunakan dalam algoritma pathfinding.
|
|
/// </summary>
|
|
/// <typeparam name="T">Tipe data nilai yang disimpan dalam node</typeparam>
|
|
abstract public class Node<T>
|
|
{
|
|
/// <summary>
|
|
/// Nilai yang disimpan dalam node
|
|
/// </summary>
|
|
public T Value { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Konstruktor untuk membuat node baru dengan nilai tertentu
|
|
/// </summary>
|
|
/// <param name="value">Nilai yang akan disimpan dalam node</param>
|
|
public Node(T value)
|
|
{
|
|
Value = value;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Mendapatkan daftar tetangga dari node ini.
|
|
/// Metode ini harus diimplementasikan oleh kelas turunan.
|
|
/// </summary>
|
|
/// <returns>Daftar tetangga dari node ini</returns>
|
|
abstract public List<Node<T>> GetNeighbours();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Kelas abstrak PathFinder yang menjadi dasar untuk semua algoritma pencarian jalur.
|
|
/// </summary>
|
|
/// <typeparam name="T">Tipe data nilai yang disimpan dalam node</typeparam>
|
|
public abstract class PathFinder<T>
|
|
{
|
|
#region Delegates for Cost Calculation.
|
|
/// <summary>
|
|
/// Delegate untuk menghitung biaya perjalanan antara dua node
|
|
/// </summary>
|
|
/// <param name="a">Node asal</param>
|
|
/// <param name="b">Node tujuan</param>
|
|
/// <returns>Biaya perjalanan dari a ke b</returns>
|
|
public delegate float CostFunction(T a, T b);
|
|
|
|
/// <summary>
|
|
/// Mendapatkan jumlah node dalam daftar tertutup (closedList)
|
|
/// </summary>
|
|
public int ClosedListCount => closedList.Count;
|
|
|
|
public int OpenListCount => openList.Count;
|
|
|
|
/// <summary>
|
|
/// Fungsi untuk menghitung biaya heuristik antara dua node
|
|
/// Digunakan dalam algoritma informasi seperti A* dan Greedy Best-First
|
|
/// </summary>
|
|
public CostFunction HeuristicCost { get; set; }
|
|
|
|
/// <summary>
|
|
/// Fungsi untuk menghitung biaya perjalanan antara dua node yang bertetangga
|
|
/// </summary>
|
|
public CostFunction NodeTraversalCost { get; set; }
|
|
#endregion
|
|
|
|
#region PathFinderNode
|
|
/// <summary>
|
|
/// Kelas PathFinderNode.
|
|
/// Merepresentasikan node dalam proses pencarian jalur.
|
|
/// Node ini mengenkapsulasi Node<T> dan informasi tambahan untuk algoritma pencarian jalur.
|
|
/// </summary>
|
|
public class PathFinderNode : System.IComparable<PathFinderNode>
|
|
{
|
|
/// <summary>
|
|
/// Node induk dalam jalur pencarian
|
|
/// </summary>
|
|
public PathFinderNode Parent { get; set; }
|
|
|
|
/// <summary>
|
|
/// Lokasi node dalam struktur graph
|
|
/// </summary>
|
|
public Node<T> Location { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Referensi ke peta grid
|
|
/// </summary>
|
|
public GridMap Map { get; set; }
|
|
|
|
/// <summary>
|
|
/// Total biaya (F = G + H)
|
|
/// </summary>
|
|
public float FCost { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Biaya dari node awal ke node saat ini (cost so far)
|
|
/// </summary>
|
|
public float GCost { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Biaya heuristik dari node saat ini ke tujuan (estimated cost)
|
|
/// </summary>
|
|
public float HCost { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Konstruktor untuk PathFinderNode
|
|
/// </summary>
|
|
/// <param name="location">Lokasi node dalam struktur graph</param>
|
|
/// <param name="parent">Node induk dalam jalur pencarian</param>
|
|
/// <param name="gCost">Biaya dari node awal ke node saat ini</param>
|
|
/// <param name="hCost">Biaya heuristik dari node saat ini ke tujuan</param>
|
|
public PathFinderNode(Node<T> location,
|
|
PathFinderNode parent,
|
|
float gCost,
|
|
float hCost)
|
|
{
|
|
Location = location;
|
|
Parent = parent;
|
|
HCost = hCost;
|
|
SetGCost(gCost);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Mengatur biaya G dan menghitung ulang biaya F
|
|
/// </summary>
|
|
/// <param name="c">Nilai baru untuk biaya G</param>
|
|
public void SetGCost(float c)
|
|
{
|
|
GCost = c;
|
|
FCost = GCost + HCost;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Mengatur biaya H dan menghitung ulang biaya F
|
|
/// </summary>
|
|
/// <param name="h">Nilai baru untuk biaya H</param>
|
|
public void SetHCost(float h)
|
|
{
|
|
HCost = h;
|
|
FCost = GCost + HCost;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Membandingkan node berdasarkan biaya F
|
|
/// Digunakan untuk menyortir prioritas node dalam pencarian
|
|
/// </summary>
|
|
/// <param name="other">Node lain yang dibandingkan</param>
|
|
/// <returns>Hasil perbandingan nilai FCost</returns>
|
|
public int CompareTo(PathFinderNode other)
|
|
{
|
|
if (other == null) return 1;
|
|
return FCost.CompareTo(other.FCost);
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region Properties
|
|
/// <summary>
|
|
/// Status saat ini dari pathfinder
|
|
/// Nilai default-nya adalah NOT_INITIALISED
|
|
/// </summary>
|
|
public PathFinderStatus Status
|
|
{
|
|
get;
|
|
protected set;
|
|
} = PathFinderStatus.NOT_INITIALISED;
|
|
|
|
/// <summary>
|
|
/// Node awal pencarian jalur
|
|
/// </summary>
|
|
public Node<T> Start { get; protected set; }
|
|
|
|
/// <summary>
|
|
/// Node tujuan pencarian jalur
|
|
/// </summary>
|
|
public Node<T> Goal { get; protected set; }
|
|
|
|
/// <summary>
|
|
/// Node yang sedang diproses saat ini oleh pathfinder
|
|
/// </summary>
|
|
public PathFinderNode CurrentNode { get; protected set; }
|
|
|
|
/// <summary>
|
|
/// Referensi ke peta grid yang digunakan untuk pencarian jalur
|
|
/// </summary>
|
|
public GridMap Map { get; internal set; }
|
|
#endregion
|
|
|
|
#region Open and Closed Lists and Associated Functions.
|
|
/// <summary>
|
|
/// Daftar node yang belum diperiksa (open list)
|
|
/// Node dalam daftar ini akan diproses di langkah berikutnya
|
|
/// </summary>
|
|
protected List<PathFinderNode> openList =
|
|
new List<PathFinderNode>();
|
|
|
|
/// <summary>
|
|
/// Daftar node yang sudah diperiksa (closed list)
|
|
/// Node dalam daftar ini sudah dievaluasi
|
|
/// </summary>
|
|
protected List<PathFinderNode> closedList =
|
|
new List<PathFinderNode>();
|
|
|
|
/// <summary>
|
|
/// Mendapatkan node dengan biaya terendah dari suatu daftar
|
|
/// Digunakan untuk memilih node berikutnya yang akan diperiksa
|
|
/// </summary>
|
|
/// <param name="myList">Daftar node yang akan diperiksa</param>
|
|
/// <returns>Node dengan biaya terendah</returns>
|
|
protected PathFinderNode GetLeastCostNode(
|
|
List<PathFinderNode> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Memeriksa apakah suatu cell (nilai T) ada dalam daftar node
|
|
/// </summary>
|
|
/// <param name="myList">Daftar node yang akan diperiksa</param>
|
|
/// <param name="cell">Cell yang dicari</param>
|
|
/// <returns>Indeks cell dalam daftar jika ditemukan, -1 jika tidak</returns>
|
|
protected int IsInList(List<PathFinderNode> myList, T cell)
|
|
{
|
|
for (int i = 0; i < myList.Count; i++)
|
|
{
|
|
if (EqualityComparer<T>.Default.Equals(myList[i].Location.Value, cell))
|
|
return i;
|
|
}
|
|
return -1;
|
|
}
|
|
#endregion
|
|
|
|
#region Delegates for Action Callbacks
|
|
/// <summary>
|
|
/// Delegate untuk menangani event terkait node selama proses pathfinding
|
|
/// Dapat digunakan untuk visualisasi atau debugging
|
|
/// </summary>
|
|
/// <param name="node">Node yang terlibat dalam event</param>
|
|
public delegate void DelegatePathFinderNode(PathFinderNode node);
|
|
|
|
/// <summary>
|
|
/// Event dipanggil ketika current node berubah
|
|
/// </summary>
|
|
public DelegatePathFinderNode onChangeCurrentNode;
|
|
|
|
/// <summary>
|
|
/// Event dipanggil ketika node ditambahkan ke open list
|
|
/// </summary>
|
|
public DelegatePathFinderNode onAddToOpenList;
|
|
|
|
/// <summary>
|
|
/// Event dipanggil ketika node ditambahkan ke closed list
|
|
/// </summary>
|
|
public DelegatePathFinderNode onAddToClosedList;
|
|
|
|
/// <summary>
|
|
/// Event dipanggil ketika node tujuan ditemukan
|
|
/// </summary>
|
|
public DelegatePathFinderNode onDestinationFound;
|
|
|
|
/// <summary>
|
|
/// Delegate untuk menangani event tanpa parameter
|
|
/// </summary>
|
|
public delegate void DelegateNoArguments();
|
|
|
|
/// <summary>
|
|
/// Event dipanggil ketika pencarian jalur dimulai
|
|
/// </summary>
|
|
public DelegateNoArguments onStarted;
|
|
|
|
/// <summary>
|
|
/// Event dipanggil ketika pencarian jalur sedang berjalan
|
|
/// </summary>
|
|
public DelegateNoArguments onRunning;
|
|
|
|
/// <summary>
|
|
/// Event dipanggil ketika pencarian jalur gagal
|
|
/// </summary>
|
|
public DelegateNoArguments onFailure;
|
|
|
|
/// <summary>
|
|
/// Event dipanggil ketika pencarian jalur berhasil
|
|
/// </summary>
|
|
public DelegateNoArguments onSuccess;
|
|
#endregion
|
|
|
|
#region Pathfinding Search Related Functions
|
|
/// <summary>
|
|
/// Mereset variabel internal untuk pencarian baru
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Melakukan satu langkah pencarian jalur
|
|
/// Harus dipanggil berulang kali sampai Status menjadi SUCCESS atau FAILURE
|
|
/// </summary>
|
|
/// <returns>Status pathfinder setelah langkah ini selesai</returns>
|
|
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<T>.Default.Equals(
|
|
CurrentNode.Location.Value, Goal.Value))
|
|
{
|
|
Status = PathFinderStatus.SUCCESS;
|
|
onDestinationFound?.Invoke(CurrentNode);
|
|
onSuccess?.Invoke();
|
|
return Status;
|
|
}
|
|
|
|
// Dapatkan tetangga dari node saat ini
|
|
List<Node<T>> neighbours = CurrentNode.Location.GetNeighbours();
|
|
|
|
// Proses setiap tetangga untuk kemungkinan ekspansi pencarian
|
|
foreach (Node<T> cell in neighbours)
|
|
{
|
|
AlgorithmSpecificImplementation(cell);
|
|
}
|
|
|
|
Status = PathFinderStatus.RUNNING;
|
|
onRunning?.Invoke();
|
|
return Status;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Implementasi algoritma spesifik untuk memproses node tetangga
|
|
/// Metode ini akan diimplementasikan berbeda untuk setiap algoritma pathfinding
|
|
/// </summary>
|
|
/// <param name="cell">Node tetangga yang akan diproses</param>
|
|
abstract protected void AlgorithmSpecificImplementation(Node<T> cell);
|
|
|
|
/// <summary>
|
|
/// Inisialisasi pathfinder dengan node awal dan tujuan
|
|
/// </summary>
|
|
/// <param name="start">Node awal pencarian</param>
|
|
/// <param name="goal">Node tujuan pencarian</param>
|
|
/// <returns>True jika inisialisasi berhasil, False jika gagal</returns>
|
|
public virtual bool Initialise(Node<T> start, Node<T> 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<T>.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
|
|
/// <summary>
|
|
/// Memprioritaskan item berdasarkan nilai komparatif mereka
|
|
/// </summary>
|
|
/// <typeparam name="T">Tipe item dalam antrian prioritas</typeparam>
|
|
public class PriorityQueue<T> where T : IComparable<T>
|
|
{
|
|
/// <summary>
|
|
/// Data yang disimpan dalam priority queue
|
|
/// </summary>
|
|
private List<T> data;
|
|
|
|
/// <summary>
|
|
/// Pembanding untuk menentukan prioritas item
|
|
/// </summary>
|
|
private IComparer<T> comparer;
|
|
|
|
/// <summary>
|
|
/// Menyimpan indeks setiap elemen dalam data untuk akses cepat
|
|
/// </summary>
|
|
private Dictionary<T, int> elementIndexMap;
|
|
|
|
// Cache untuk optimasi
|
|
private T _lastDequeued;
|
|
private int _count;
|
|
|
|
/// <summary>
|
|
/// Konstruktor default menggunakan pembanding default untuk tipe T
|
|
/// </summary>
|
|
public PriorityQueue() : this(Comparer<T>.Default) { }
|
|
|
|
/// <summary>
|
|
/// Konstruktor dengan custom comparer
|
|
/// </summary>
|
|
/// <param name="comparer">Pembanding untuk menentukan prioritas item</param>
|
|
public PriorityQueue(IComparer<T> comparer)
|
|
{
|
|
this.data = new List<T>();
|
|
this.comparer = comparer;
|
|
this.elementIndexMap = new Dictionary<T, int>();
|
|
this._count = 0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Menambahkan item ke dalam antrian prioritas
|
|
/// </summary>
|
|
/// <param name="item">Item yang akan ditambahkan</param>
|
|
public void Enqueue(T item)
|
|
{
|
|
data.Add(item);
|
|
int childIndex = data.Count - 1;
|
|
elementIndexMap[item] = childIndex;
|
|
HeapifyUp(childIndex);
|
|
_count = data.Count;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Mengambil dan menghapus item dengan prioritas tertinggi
|
|
/// </summary>
|
|
/// <returns>Item dengan prioritas tertinggi</returns>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Menghapus item tertentu dari antrian
|
|
/// </summary>
|
|
/// <param name="item">Item yang akan dihapus</param>
|
|
/// <returns>True jika berhasil dihapus, False jika tidak</returns>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Memperbarui prioritas item dalam antrian
|
|
/// </summary>
|
|
/// <param name="item">Item yang akan diperbarui</param>
|
|
/// <param name="newPriority">Nilai prioritas baru</param>
|
|
public void UpdatePriority(T item, float newPriority)
|
|
{
|
|
// Fast check - jika item adalah yang terakhir dihapus, jangan lakukan apa-apa
|
|
if (_lastDequeued != null && EqualityComparer<T>.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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Menjaga sifat heap dengan pergerakan ke atas
|
|
/// </summary>
|
|
/// <param name="index">Indeks node yang akan diperbaiki posisinya</param>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Menjaga sifat heap dengan pergerakan ke bawah
|
|
/// </summary>
|
|
/// <param name="index">Indeks node yang akan diperbaiki posisinya</param>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Menukar posisi dua item dalam heap
|
|
/// </summary>
|
|
/// <param name="index1">Indeks item pertama</param>
|
|
/// <param name="index2">Indeks item kedua</param>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Jumlah item dalam antrian
|
|
/// </summary>
|
|
public int Count => _count;
|
|
|
|
/// <summary>
|
|
/// Mendapatkan enumerator untuk data
|
|
/// </summary>
|
|
/// <returns>Enumerator untuk data</returns>
|
|
public IEnumerator<T> GetEnumerator()
|
|
{
|
|
return data.GetEnumerator();
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region Dijkstra Implementation
|
|
/// <summary>
|
|
/// Implementasi algoritma Dijkstra yang melakukan pencarian secara merata
|
|
/// ke semua arah untuk menemukan jalur terpendek
|
|
/// </summary>
|
|
/// <typeparam name="T">Tipe data nilai yang disimpan dalam node</typeparam>
|
|
public class DijkstraPathFinder<T> : PathFinder<T>
|
|
{
|
|
/// <summary>
|
|
/// HashSet untuk pemeriksaan closed list dengan cepat (O(1) complexity)
|
|
/// </summary>
|
|
private HashSet<T> closedSet;
|
|
|
|
/// <summary>
|
|
/// Dictionary untuk mengakses node di open list dengan cepat berdasarkan nilai mereka
|
|
/// </summary>
|
|
private Dictionary<T, PathFinderNode> openListMap;
|
|
|
|
// Flag untuk mengontrol level optimisasi berdasarkan ukuran grid
|
|
private bool isGridLarge = false;
|
|
private int estimatedNodesCount = 0;
|
|
|
|
/// <summary>
|
|
/// Constructor baru dengan estimasi jumlah node
|
|
/// </summary>
|
|
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<T>(initialCapacity);
|
|
openListMap = new Dictionary<T, PathFinderNode>(initialCapacity);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Implementasi spesifik algoritma Dijkstra untuk memproses node tetangga
|
|
/// </summary>
|
|
/// <param name="cell">Node tetangga yang akan diproses</param>
|
|
protected override void AlgorithmSpecificImplementation(Node<T> 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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Melakukan satu langkah pencarian jalur dengan algoritma Dijkstra
|
|
/// </summary>
|
|
/// <returns>Status pathfinder setelah langkah ini selesai</returns>
|
|
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<T>.Default.Equals(CurrentNode.Location.Value, Goal.Value))
|
|
{
|
|
Status = PathFinderStatus.SUCCESS;
|
|
onDestinationFound?.Invoke(CurrentNode);
|
|
onSuccess?.Invoke();
|
|
return Status;
|
|
}
|
|
|
|
List<Node<T>> neighbours = CurrentNode.Location.GetNeighbours();
|
|
foreach (Node<T> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reset state pathfinder
|
|
/// </summary>
|
|
public override void Reset()
|
|
{
|
|
base.Reset();
|
|
closedSet.Clear();
|
|
openListMap.Clear();
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region A* Implementation
|
|
/// <summary>
|
|
/// Implementasi algoritma A* (A-Star) yang menggunakan informasi heuristik
|
|
/// untuk menemukan jalur terpendek dari titik awal ke titik akhir
|
|
/// </summary>
|
|
/// <typeparam name="T">Tipe data nilai yang disimpan dalam node</typeparam>
|
|
public class AStarPathFinder<T> : PathFinder<T>
|
|
{
|
|
/// <summary>
|
|
/// Open list diimplementasikan sebagai priority queue untuk efisiensi
|
|
/// </summary>
|
|
private new PriorityQueue<PathFinderNode> openList;
|
|
|
|
/// <summary>
|
|
/// Peta untuk mengakses node dalam open list dengan cepat
|
|
/// </summary>
|
|
private Dictionary<T, PathFinderNode> openListMap;
|
|
|
|
/// <summary>
|
|
/// HashSet untuk memeriksa closed list dengan cepat (O(1) complexity)
|
|
/// </summary>
|
|
private HashSet<T> closedSet;
|
|
|
|
// Flag untuk batch processing - hanya diaktifkan untuk grid besar
|
|
private bool processingBatch = false;
|
|
private List<Node<T>> neighborBatch;
|
|
|
|
// Flag untuk mengontrol level optimisasi
|
|
private bool isGridLarge = false;
|
|
private int estimatedNodesCount = 0;
|
|
|
|
/// <summary>
|
|
/// Constructor baru dengan estimasi jumlah node
|
|
/// </summary>
|
|
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<PathFinderNode>(new FCostComparer());
|
|
openListMap = new Dictionary<T, PathFinderNode>(initialCapacity);
|
|
closedSet = new HashSet<T>(initialCapacity);
|
|
|
|
// Alokasi neighborBatch hanya jika diperlukan
|
|
if (isGridLarge)
|
|
{
|
|
neighborBatch = new List<Node<T>>(8);
|
|
}
|
|
else
|
|
{
|
|
neighborBatch = new List<Node<T>>(4); // Lebih kecil untuk grid kecil
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Implementasi spesifik algoritma A* untuk memproses node tetangga
|
|
/// </summary>
|
|
/// <param name="cell">Node tetangga yang akan diproses</param>
|
|
protected override void AlgorithmSpecificImplementation(Node<T> 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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Melakukan satu langkah pencarian jalur dengan algoritma A*
|
|
/// </summary>
|
|
/// <returns>Status pathfinder setelah langkah ini selesai</returns>
|
|
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<T>.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<Node<T>> 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<T> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reset state pathfinder
|
|
/// </summary>
|
|
public override void Reset()
|
|
{
|
|
base.Reset();
|
|
openListMap.Clear();
|
|
closedSet.Clear();
|
|
if (isGridLarge && neighborBatch != null)
|
|
neighborBatch.Clear();
|
|
processingBatch = false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Pembanding F-Cost untuk priority queue dalam algoritma A*
|
|
/// </summary>
|
|
private class FCostComparer : IComparer<PathFinderNode>
|
|
{
|
|
/// <summary>
|
|
/// Membandingkan dua node berdasarkan FCost mereka
|
|
/// Jika FCost sama, gunakan HCost sebagai tie-breaker
|
|
/// </summary>
|
|
/// <param name="x">Node pertama</param>
|
|
/// <param name="y">Node kedua</param>
|
|
/// <returns>Hasil perbandingan</returns>
|
|
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
|
|
/// <summary>
|
|
/// Implementasi algoritma Greedy Best-First Search
|
|
/// Algoritma ini hanya mempertimbangkan biaya heuristik (H) ke tujuan
|
|
/// </summary>
|
|
/// <typeparam name="T">Tipe data nilai yang disimpan dalam node</typeparam>
|
|
public class GreedyPathFinder<T> : PathFinder<T>
|
|
{
|
|
/// <summary>
|
|
/// Open list diimplementasikan sebagai priority queue untuk performa yang lebih baik
|
|
/// </summary>
|
|
private new PriorityQueue<PathFinderNode> openList = new PriorityQueue<PathFinderNode>(new HeuristicComparer());
|
|
|
|
/// <summary>
|
|
/// Dictionary untuk akses node open list yang cepat
|
|
/// </summary>
|
|
private Dictionary<T, PathFinderNode> openSet = new Dictionary<T, PathFinderNode>(256);
|
|
|
|
/// <summary>
|
|
/// HashSet untuk pemeriksaan closed list O(1)
|
|
/// </summary>
|
|
private HashSet<T> closedSet = new HashSet<T>(256);
|
|
|
|
// Flag untuk batch processing
|
|
private bool processingBatch = false;
|
|
private List<Node<T>> neighborBatch = new List<Node<T>>(4);
|
|
|
|
/// <summary>
|
|
/// Implementasi spesifik algoritma Greedy untuk memproses node tetangga
|
|
/// </summary>
|
|
/// <param name="cell">Node tetangga yang akan diproses</param>
|
|
protected override void AlgorithmSpecificImplementation(Node<T> 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<T>.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)
|
|
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Melakukan satu langkah pencarian jalur dengan algoritma Greedy
|
|
/// </summary>
|
|
/// <returns>Status pathfinder setelah langkah ini selesai</returns>
|
|
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<T>.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<T> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reset state pathfinder
|
|
/// </summary>
|
|
public override void Reset()
|
|
{
|
|
base.Reset();
|
|
openSet.Clear();
|
|
closedSet.Clear();
|
|
neighborBatch.Clear();
|
|
processingBatch = false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Pembanding Heuristik untuk priority queue dalam algoritma Greedy
|
|
/// </summary>
|
|
private class HeuristicComparer : IComparer<PathFinderNode>
|
|
{
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <param name="x">Node pertama</param>
|
|
/// <param name="y">Node kedua</param>
|
|
/// <returns>Hasil perbandingan</returns>
|
|
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
|
|
/// <summary>
|
|
/// Implementasi algoritma Backtracking untuk pencarian jalur
|
|
/// Menggunakan pendekatan depth-first dengan backtracking
|
|
/// </summary>
|
|
/// <typeparam name="T">Tipe data nilai yang disimpan dalam node</typeparam>
|
|
public class BacktrackingPathFinder<T> : PathFinder<T>
|
|
{
|
|
/// <summary>
|
|
/// Stack untuk menyimpan node yang akan dikunjungi (open list)
|
|
/// </summary>
|
|
private Stack<PathFinderNode> openStack = new Stack<PathFinderNode>();
|
|
|
|
/// <summary>
|
|
/// HashSet untuk pemeriksaan cepat apakah node tertentu sudah dikunjungi
|
|
/// </summary>
|
|
private HashSet<T> closedSet = new HashSet<T>();
|
|
|
|
/// <summary>
|
|
/// HashSet untuk pemeriksaan cepat apakah node tertentu sudah ada di open list
|
|
/// </summary>
|
|
private HashSet<T> openSet = new HashSet<T>();
|
|
|
|
/// <summary>
|
|
/// Implementasi spesifik algoritma Backtracking untuk memproses node tetangga
|
|
/// </summary>
|
|
/// <param name="cell">Node tetangga yang akan diproses</param>
|
|
protected override void AlgorithmSpecificImplementation(Node<T> 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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Melakukan satu langkah pencarian jalur dengan algoritma Backtracking
|
|
/// </summary>
|
|
/// <returns>Status pathfinder setelah langkah ini selesai</returns>
|
|
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<T>.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<Node<T>> neighbours = CurrentNode.Location.GetNeighbours();
|
|
foreach (Node<T> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reset state pathfinder
|
|
/// </summary>
|
|
public override void Reset()
|
|
{
|
|
base.Reset();
|
|
openStack.Clear();
|
|
closedSet.Clear();
|
|
openSet.Clear();
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region Breath-First Search Algorithm
|
|
/// <summary>
|
|
/// 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
|
|
/// </summary>
|
|
/// <typeparam name="T">Tipe data nilai yang disimpan dalam node</typeparam>
|
|
public class BFSPathFinder<T> : PathFinder<T>
|
|
{
|
|
/// <summary>
|
|
/// Queue sebagai open list untuk BFS
|
|
/// </summary>
|
|
private Queue<PathFinderNode> openQueue = new Queue<PathFinderNode>();
|
|
|
|
/// <summary>
|
|
/// HashSet untuk pemeriksaan nilai node dalam closed list (O(1) complexity)
|
|
/// </summary>
|
|
private HashSet<T> closedSet = new HashSet<T>();
|
|
|
|
/// <summary>
|
|
/// HashSet untuk pemeriksaan nilai node dalam open list (O(1) complexity)
|
|
/// </summary>
|
|
private HashSet<T> openSet = new HashSet<T>();
|
|
|
|
/// <summary>
|
|
/// Implementasi spesifik algoritma BFS untuk memproses node tetangga
|
|
/// </summary>
|
|
/// <param name="cell">Node tetangga yang akan diproses</param>
|
|
protected override void AlgorithmSpecificImplementation(Node<T> 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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Melakukan satu langkah pencarian jalur dengan algoritma BFS
|
|
/// </summary>
|
|
/// <returns>Status pathfinder setelah langkah ini selesai</returns>
|
|
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<T>.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<Node<T>> neighbours = CurrentNode.Location.GetNeighbours();
|
|
foreach (Node<T> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reset state pathfinder
|
|
/// </summary>
|
|
public override void Reset()
|
|
{
|
|
base.Reset();
|
|
openQueue.Clear();
|
|
closedSet.Clear();
|
|
openSet.Clear();
|
|
}
|
|
}
|
|
#endregion
|
|
}
|