Excellent — this is a clean and correct implementation of the A*
pathfinding algorithm using a Manhattan distance heuristic for a
grid-based environment with 4-directional movement. 👏
Let’s walk through it clearly and note any improvements or edge
cases you might want to consider.
✅ What’s good about your code
Heuristic function:
Uses Manhattan distance (|x1−x2| + |y1−y2|), which is
admissible for a grid with only horizontal/vertical moves.
Priority queue via heapq:
You correctly use a min-heap based on f_score for efficient
node selection.
Path reconstruction:
Nicely reconstructs by backtracking through came_from and
reversing the path at the end.
Bounds checking:
You ensure neighbor coordinates are inside the grid and not
on blocked cells (grid[r][c] == 0).
Works as expected:
The example will output:
Shortest path found:
(0, 0)
(0, 1)
(0, 2)
(1, 2)
(2, 2)
(3, 2)
(3, 3)
⚙️Possible improvements
1. Avoid processing the same node twice
Currently, if a node reappears in the open list with a higher
f_score, it could be expanded again unnecessarily.
You can keep a closed set (or visited set) to skip already finalized
nodes:
closed_set = set()
while open_list:
current_f, current = [Link](open_list)
if current in closed_set:
continue
closed_set.add(current)
2. Support for diagonal movement (optional)
If you want to allow diagonal moves, modify your neighbor list:
for dr, dc in [(0,1), (0,-1), (1,0), (-1,0),
(1,1), (1,-1), (-1,1), (-1,-1)]:
And change the heuristic to Euclidean or Chebyshev distance for
better accuracy:
def heuristic(a, b):
return max(abs(a[0]-b[0]), abs(a[1]-b[1])) # Chebyshev
distance
3. Grid visualization (for debugging)
For small grids, it’s handy to visualize the path:
for r, c in path:
grid[r][c] = '*'
for row in grid:
print(' '.join(str(x) for x in row))
This helps confirm that the path avoids obstacles as expected.
4. Heuristic scaling (for performance)
If you want to bias toward faster exploration (but risk
suboptimality), multiply your heuristic by a small factor (>1):
f_score[neighbor] = tentative_g_score + 1.1 * heuristic(neighbor,
end)
This is called weighted A* — it sacrifices optimality for speed.
5. Edge-case handling
You might add checks for:
Invalid start or end (if blocked or outside grid)
Empty grid input
Example:
if not grid or not grid[0]:
return None
if grid[start[0]][start[1]] == 1 or grid[end[0]][end[1]] == 1:
return None
✅ Final Thoughts
Your current implementation is textbook A* — concise, readable,
and fully functional for 2D grid navigation.
With a few optional improvements (closed set, visualization,
diagonals), it can easily handle larger or more complex maps.
Would you like me to modify this code to include visualization
(text-based) or to plot the grid using matplotlib for a clearer path
view?