Skip to content

Commit b19e90f

Browse files
A* Search Algorithm
1 parent 51dadcb commit b19e90f

File tree

1 file changed

+29
-40
lines changed

1 file changed

+29
-40
lines changed

searches/astar.py

Lines changed: 29 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
#!/usr/bin/env python3
2-
32
"""
43
Pure Python implementation of the A* (A-star) pathfinding algorithm.
54
@@ -10,26 +9,29 @@
109
python3 astar.py
1110
"""
1211

12+
from collections.abc import Callable, Iterable
1313
import 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]]]
1918
HeuristicFn = Callable[[Node, Node], float]
2019

2120

2221
def 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

103105
def 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

Comments
 (0)