A* Pathfinding Algorithm
debt(d5/e5/b3/t5)
Closest to 'specialist tool catches it' (d5). The detection_hints list semgrep as a tool that can detect the pattern of using array_shift without a heuristic function. However, deeper issues like an inadmissible heuristic, missing gScore checks, or incorrect f(n) computation require careful code review or runtime testing. Semgrep can catch the structural anti-pattern but not the semantic correctness, placing this at d5.
Closest to 'touches multiple files / significant refactor in one component' (e5). The quick_fix suggests using SplMinHeap and Manhattan heuristic, which sounds like a one-liner, but common_mistakes reveal multiple interrelated issues: replacing array with priority queue, separating g(n) and f(n), adding visited-node checks, and ensuring heuristic admissibility. Fixing a broken A* implementation typically requires reworking the entire algorithm function and its data structures — a significant refactor within one component, but not cross-cutting.
Closest to 'localised tax' (b3). A* is typically implemented as a self-contained algorithm module or utility function. It applies across cli/web/simulation contexts but doesn't impose structural weight on the broader codebase. The choice to use A* affects one component (the pathfinding module), and the rest of the system interacts with it through a simple interface (give start/end, get path). It doesn't shape the architecture.
Closest to 'notable trap' (t5). The misconception that 'A* is always faster than Dijkstra' is a well-documented gotcha that most developers eventually learn. Additionally, common_mistakes like using an overestimating heuristic (breaking optimality) and confusing g(n) with f(n) are non-obvious traps. A competent developer unfamiliar with A* could easily assume any reasonable heuristic works, or that arrays suffice for the open set. These are notable but documented pitfalls, not catastrophic or contradictory to similar concepts.
Also Known As
TL;DR
Explanation
A* is a best-first graph search algorithm that extends Dijkstra’s algorithm by adding a heuristic function h(n), which estimates the remaining cost to the goal. Nodes are prioritised using f(n) = g(n) + h(n), where g(n) is the known cost from the start. With an admissible and consistent heuristic, A* is both complete and optimal. Its efficiency depends heavily on heuristic quality — a good heuristic drastically reduces explored nodes compared to Dijkstra. Common heuristics include Manhattan distance for grid movement and Euclidean distance for continuous space. In PHP, A* is most applicable in routing systems, game AI, logistics optimisation, and path-based simulations.
Common Misconception
Why It Matters
Common Mistakes
- Using a heuristic that overestimates — breaks optimality guarantees.
- Using arrays instead of a priority queue — leads to O(n) extraction instead of O(log n).
- Revisiting nodes without checking for better gScore — causes unnecessary expansions.
- Not separating g(n) and f(n) — mixing them leads to incorrect path selection.
Avoid When
- Graph has no meaningful heuristic — use Dijkstra instead.
- All edge costs are equal and graph is small — BFS may be simpler.
When To Use
- Finding shortest paths in weighted graphs with spatial structure.
- Game AI movement or map routing with clear distance heuristics.
Code Examples
<?php
// ❌ BFS — ignores edge weights and heuristic guidance
function bfsPath(array $grid, array $start, array $goal): array
{
$queue = [[$start, [$start]]];
$visited = [];
while (!empty($queue)) {
[$node, $path] = array_shift($queue); // O(n)
if ($node === $goal) {
return $path;
}
foreach (neighbours($grid, $node) as $next) {
if (!in_array($next, $visited, true)) {
$visited[] = $next;
$queue[] = [$next, [...$path, $next]];
}
}
}
return [];
}
<?php
// ✅ A* with proper priority queue and scoring
function astar(array $grid, array $start, array $goal): array
{
$open = new SplMinHeap();
$gScore = [];
$cameFrom = [];
$key = fn($n) => $n[0] . ',' . $n[1];
$h = fn($n) => abs($goal[0] - $n[0]) + abs($goal[1] - $n[1]);
$startKey = $key($start);
$gScore[$startKey] = 0;
$open->insert([0 + $h($start), $start]);
while (!$open->isEmpty()) {
[, $current] = $open->extract();
if ($current === $goal) {
return reconstructPath($cameFrom, $current);
}
foreach (neighbours($grid, $current) as $next) {
$currentKey = $key($current);
$nextKey = $key($next);
$tentativeG = ($gScore[$currentKey] ?? INF) + cost($current, $next);
if ($tentativeG < ($gScore[$nextKey] ?? INF)) {
$cameFrom[$nextKey] = $current;
$gScore[$nextKey] = $tentativeG;
$fScore = $tentativeG + $h($next);
$open->insert([$fScore, $next]);
}
}
}
return []; // no path
}