A* on a grid
Implement A* pathfinding on a 2D tile grid using a heap-backed priority queue — returning an ordered list of tile coordinates.
- Represent a tile grid as a 2D list where 0 = passable and 1 = wall
- Implement A* using heapq with f = g + h where h is Manhattan distance
- Return an ordered path of (row, col) tuples from start to goal
- Test the algorithm on a grid with a wall and verify the returned path
A* is short and self-contained. It does not require pygame — just Python's standard library. This makes it straightforward to test in isolation before plugging it into a game.
The algorithm
import heapq
def astar(grid, start, goal):
"""
Find the shortest path from start to goal on a 2D tile grid.
Parameters
----------
grid : list[list[int]] — 0 = passable, 1 = wall
start : (row, col) — starting tile
goal : (row, col) — destination tile
Returns
-------
list[(row, col)] from start to goal (inclusive), or [] if no path exists.
"""
rows = len(grid)
cols = len(grid[0])
def h(node):
"""Manhattan distance heuristic."""
return abs(node[0] - goal[0]) + abs(node[1] - goal[1])
def neighbours(r, c):
for dr, dc in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
nr, nc = r + dr, c + dc
if 0 <= nr < rows and 0 <= nc < cols and grid[nr][nc] == 0:
yield (nr, nc)
# Priority queue entries: (f_score, g_score, node)
# g_score is included to break f_score ties consistently
open_heap = [(h(start), 0, start)]
came_from = {} # node -> parent node
g_score = {start: 0} # best known cost to reach each node
while open_heap:
f, g, current = heapq.heappop(open_heap)
if current == goal:
# Reconstruct path
path = []
while current in came_from:
path.append(current)
current = came_from[current]
path.append(start)
path.reverse()
return path
# Skip if we already found a better path to this node
if g > g_score.get(current, float('inf')):
continue
for neighbour in neighbours(*current):
tentative_g = g + 1 # uniform cost (all moves cost 1)
if tentative_g < g_score.get(neighbour, float('inf')):
g_score[neighbour] = tentative_g
came_from[neighbour] = current
f_new = tentative_g + h(neighbour)
heapq.heappush(open_heap, (f_new, tentative_g, neighbour))
return [] # no path foundWalking through an example
# 0 = open, 1 = wall
# col: 0 1 2 3 4
GRID = [
[0, 0, 0, 0, 0], # row 0
[0, 1, 1, 1, 0], # row 1 — wall in cols 1-3
[0, 0, 0, 0, 0], # row 2
]
start = (0, 0)
goal = (0, 4)
path = astar(GRID, start, goal)
print(path)
# Expected: [(0,0), (1,0), (2,0), (2,1), (2,2), (2,3), (2,4), (1,4), (0,4)]
# The algorithm detours around the wall via the bottom row.The wall in row 1 (columns 1–3) blocks the direct top route. A* detours via row 2, adding 6 extra steps versus the 4 steps of a straight path — the minimum possible detour.
Why the duplicate-check matters
if g > g_score.get(current, float('inf')):
continueWhen the same node is added to the heap multiple times with different g scores
(because a better path was found later), stale entries remain in the heap. This
guard discards them cheaply rather than re-expanding obsolete paths.
Connecting A* to your enemy
# Inside Enemy.update, when transitioning to CHASE:
def enter_chase(self, player_tile, tile_grid):
"""Recalculate path when entering CHASE or player moves significantly."""
start = world_to_tile(self.pos)
goal = player_tile
self.path = astar(tile_grid, start, goal)
self.path_idx = 0 # index of the next waypoint to move toward
def move_along_path(self, dt):
if not self.path or self.path_idx >= len(self.path):
return
target_tile = self.path[self.path_idx]
target_world = tile_to_world(target_tile)
to_target = target_world - self.pos
if to_target.length() < 4: # close enough — advance waypoint
self.path_idx += 1
elif to_target.length() > 0:
self.pos += to_target.normalize() * CHASE_SPEED * dtworld_to_tile and tile_to_world are simple scale / offset conversions based
on your tile size.
Do not call astar every frame for every enemy. Cache the path and invalidate
it only when the player moves more than one tile, or at most once every 15–20
frames. On maps with hundreds of tiles the repeated search will stall your
frame rate.
Where to go next
Next: lab — enemy AI — integrating A* and the FSM into the tile platformer so the enemy patrols, detects the player, paths toward them, and flees when low on health.
Pathfinding concepts
Understand why straight-line movement fails in complex maps, how BFS explores a tile grid level by level, and how A* uses a heuristic to explore far fewer nodes.
Lab: Enemy AI
Integrate A* pathfinding and the FSM enemy into the tile platformer — patrol, detect, path toward the player, and flee on low health.