11#!/usr/bin/env python3
2-
32"""
43Pure Python implementation of the A* (A-star) pathfinding algorithm.
54
109 python3 astar.py
1110"""
1211
12+ from collections .abc import Callable , Iterable
1313import heapq
14- from typing import Callable , Iterable , Tuple , List , Dict , Optional , Set
1514
16- # Type aliases for readability
17- Node = Tuple [int , int ]
18- NeighborsFn = Callable [[Node ], Iterable [Tuple [Node , float ]]]
15+ # Type aliases for readability (PEP 585 built-in generics, PEP 604 unions)
16+ Node = tuple [int , int ]
17+ NeighborsFn = Callable [[Node ], Iterable [tuple [Node , float ]]]
1918HeuristicFn = Callable [[Node , Node ], float ]
2019
2120
2221def astar (
23- start : Node , goal : Node , neighbors : NeighborsFn , h : HeuristicFn
24- ) -> Optional [List [Node ]]:
22+ start : Node ,
23+ goal : Node ,
24+ neighbors : NeighborsFn ,
25+ h : HeuristicFn ,
26+ ) -> list [Node ] | None :
2527 """
2628 A* algorithm for pathfinding on a graph defined by a neighbor function.
2729
2830 A* maintains:
29- -> g[n]: cost from start to node n (best known so far)
30- -> f[n] = g[n] + h(n, goal): estimated total cost of a path through n to goal
31- -> open_list: min-heap of candidate nodes prioritized by smallest f-score
32- -> closed_list: set of nodes already expanded (best path to them fixed)
31+ - g[n]: cost from start to node n (best known so far)
32+ - f[n] = g[n] + h(n, goal): estimated total cost through n to goal
33+ - open_list: min-heap of candidate nodes prioritized by smallest f-score
34+ - closed_list: set of nodes already expanded (best path to them fixed)
3335
3436 :param start: starting node
3537 :param goal: target node
@@ -38,27 +40,27 @@ def astar(
3840 :return: list of nodes from start to goal (inclusive), or None if no path
3941
4042 Examples:
41- >>> def _h(a, b) : # Manhattan distance
43+ >>> def _h(a: Node , b: Node) -> float : # Manhattan distance
4244 ... (x1, y1), (x2, y2) = a, b
4345 ... return abs(x1 - x2) + abs(y1 - y2)
44- >>> def _nbrs(p): # 4-connected grid, unit costs, unbounded
46+ >>> def _nbrs(p: Node):
4547 ... x, y = p
4648 ... return [((x + 1, y), 1), ((x - 1, y), 1), ((x, y + 1), 1), ((x, y - 1), 1)]
47- >>> astar((0, 0), (2, 2), _nbrs, _h)[-1]
48- (2, 2)
49+ >>> path = astar((0, 0), (2, 2), _nbrs, _h)
50+ >>> path is not None and path[0] == (0, 0) and path[-1] == (2, 2)
51+ True
4952 """
50- # Min-heap of (f_score, node). We only store (priority, node) to keep it simple;
51- # if your nodes aren't directly comparable, add a tiebreaker counter to the tuple.
52- open_list : List [Tuple [float , Node ]] = []
53+ # Min-heap of (f_score, node)
54+ open_list : list [tuple [float , Node ]] = []
5355
54- # Nodes we've fully explored (their best path is finalized).
55- closed_list : Set [Node ] = set ()
56+ # Nodes we've fully explored (their best path is finalized)
57+ closed_list : set [Node ] = set ()
5658
5759 # g-scores: best known cost to reach each node from start
58- g : Dict [Node , float ] = {start : 0.0 }
60+ g : dict [Node , float ] = {start : 0.0 }
5961
6062 # Parent map to reconstruct the path once we reach the goal
61- parent : Dict [Node , Optional [ Node ] ] = {start : None }
63+ parent : dict [Node , Node | None ] = {start : None }
6264
6365 # Initialize the frontier with the start node (f = h(start, goal))
6466 heapq .heappush (open_list , (h (start , goal ), start ))
@@ -74,7 +76,7 @@ def astar(
7476
7577 # Goal check: reconstruct the path by following parents backward
7678 if current == goal :
77- path : List [Node ] = []
79+ path : list [Node ] = []
7880 while current is not None :
7981 path .append (current )
8082 current = parent [current ]
@@ -89,7 +91,7 @@ def astar(
8991 # Tentative g-score via current
9092 tentative_g = g [current ] + cost
9193
92- # If this is the first time we see neighbor, or we found a cheaper path to it
94+ # If first time seeing neighbor, or we found a cheaper path to it
9395 if neighbor not in g or tentative_g < g [neighbor ]:
9496 g [neighbor ] = tentative_g
9597 parent [neighbor ] = current
@@ -101,30 +103,17 @@ def astar(
101103
102104
103105def heuristic (n : Node , goal : Node ) -> float :
104- """
105- Manhattan (L1) distance heuristic for 4-connected grid movement with unit costs.
106- Admissible and consistent for axis-aligned moves.
107-
108- :param n: current node
109- :param goal: target node
110- :return: |x1 - x2| + |y1 - y2|
111- """
106+ """Manhattan (L1) distance heuristic for 4-connected grid movement with unit costs."""
112107 x1 , y1 = n
113108 x2 , y2 = goal
114109 return abs (x1 - x2 ) + abs (y1 - y2 )
115110
116111
117- def neighbors (node : Node ) -> Iterable [Tuple [Node , float ]]:
112+ def neighbors (node : Node ) -> Iterable [tuple [Node , float ]]:
118113 """
119114 4-neighborhood on an unbounded grid with unit edge costs.
120115
121- Replace/extend this for:
122- -> bounded grids (check bounds before yielding)
123- -> obstacles (skip blocked cells)
124- -> diagonal moves (add the 4 diagonals with cost sqrt(2) and switch heuristic)
125-
126- :param node: (x, y) coordinate
127- :return: iterable of ((nx, ny), step_cost)
116+ Replace/extend this for bounds, obstacles, or diagonal moves.
128117 """
129118 x , y = node
130119 return [
0 commit comments