Python A-Star

  • Category: Algorithms
  • Project date: 02 February, 2025
  • Time: 4 weeks
  • Made with: Python

Pathfinding!

In this project I made an A-Star pathfinding algorithm in Python. I made this because I wanted to learn more about pathfinding and I chose the A-star algorithm because it is one of the most optimized pathfinding algorithms. This was my first real Python project.

Week 1-2

In the first week I made the a grid with a 2D array. Then I made a breadth first algorithm to find the shortest path from a start point to the end point. First I displayed the path found with directions in the console, and at the end of the week I made the grid visual in the console with the path found.


# Make a node class for each position in the grid
class node():
  def __init__(self, x, y, parent=None, action=None):
    self.parent = parent
    self.action = action
    self.x = x
    self.y = y
    self.visited = False

  def __eq__(self, other):
  return self.x == other.x and self.y == other.y
                

Here you can see how the main logic works. I check around the current node for any walkable nodes, and put them in a 'to-do' list. I made a node class for ever location on the grid so I can easily get information about the node like if its already walked or if its walkable.


while len(to_do) > 0:
  current = to_do.pop(0)
  if current.x == goal_node.x and current.y == goal_node.y:
      to_do.clear()
      #print("found shortest path")
      path = []
      while current.parent is not None:
          # print(current.action)
          path.append(current)
          current = current.parent
      # print(path[::-1])
      return path[::-1]
  # Add down
  if current.y + 1 < max_length and grid[current.y + 1][current.x] == 0 and current not in done:
      to_do.append(node(current.x, current.y + 1, parent=current, action="down"))

  # Add right
  if current.x + 1 < max_width and grid[current.y][current.x + 1] == 0 and current not in done:
      to_do.append(node(current.x + 1, current.y, parent=current, action="right"))
      

  # Add left
  if current.x - 1 >= 0 and grid[current.y][current.x - 1] == 0 and current not in done:
      to_do.append(node(current.x - 1, current.y, parent=current, action="left"))
  
  # Add up
  if current.y - 1 >= 0 and grid[current.y - 1][current.x] == 0 and current not in done:
      to_do.append(node(current.x, current.y - 1, parent=current, action="up"))
  
  done.append(current)
  current.visited = True
              

Week 3

In the third week I used Pygame to visualize the grid better. This was quite a challenge since I never worked with Pygame before. But eventually I got it working. Also I added color to the start and end. This is how it looked in the end.

Week 4

In the fourth week I evolved the breadth first algorithm to the A-star algorithm. This was easier than expected since I already had a good base with the breadth first algorithm. The only thing I had to do was add a heuristic to the algorithm to make it A-star.
I also made maps for the algorithm to run on. So you can see how it performs on different maps.


def calcute_cost(self, goal_node):
# If has parent then g is parent's g + 1, else g is 0
self.g = self.parent.g + 1 if self.parent else 0
# Calculates the distance between the current node and the goal node
# self.h = abs(goal_node.x - self.x) + abs(goal_node.y - self.y)
self.h = ((goal_node.x - self.x) ** 2 + (goal_node.y - self.y) ** 2) ** 0.5
self.f = self.g + self.h
          

Full script:


import os
import time
import pygame

# Make a node class for each position in the grid
class node():
    def __init__(self, x, y, parent=None, action=None):
        self.parent = parent
        self.action = action
        self.x = x
        self.y = y
        self.visited = False
        self.g = 0
        self.h = 0
        self.f = 0

    def calcute_cost(self, goal_node):
        # If has parent then g is parent's g + 1, else g is 0
        self.g = self.parent.g + 1 if self.parent else 0
        # Calculates the distance between the current node and the goal node
        # self.h = abs(goal_node.x - self.x) + abs(goal_node.y - self.y)
        self.h = ((goal_node.x - self.x) ** 2 + (goal_node.y - self.y) ** 2) ** 0.5
        self.f = self.g + self.h
        

    def __eq__(self, other):
        return self.x == other.x and self.y == other.y


#region maps
# --- MAP 1: Original ---
grid1 = [
    [0,1,0,1,0,0,1,0,1,0,1,0],
    [0,1,0,1,0,1,1,0,1,0,1,0],
    [0,0,0,0,0,1,0,0,0,0,1,0],
    [1,1,1,1,0,1,0,1,1,0,1,0],
    [0,1,0,0,0,1,0,0,1,0,0,0],
    [0,1,1,1,0,1,1,0,1,1,1,0],
    [0,0,0,1,0,0,0,0,0,0,0,0],
    [1,1,0,1,1,1,1,1,1,1,1,0],
    [0,0,0,0,0,0,0,0,1,0,0,0],
    [0,1,1,1,1,1,1,0,0,0,1,0]
]
visual_grid1 = [
    [0,'∎',0,'∎',0,0,'∎',0,'∎',0,'∎',0],
    [0,'∎',0,'∎',0,'∎','∎',0,'∎',0,'∎',0],
    [0,0,0,0,0,'∎',0,0,0,0,'∎',0],
    ['∎','∎','∎','∎',0,'∎',0,'∎','∎',0,'∎',0],
    [0,'∎',0,0,0,'∎',0,0,'∎',0,0,0],
    [0,'∎','∎','∎',0,'∎','∎',0,'∎','∎','∎',0],
    [0,0,0,'∎',0,0,0,0,0,0,0,0],
    ['∎','∎',0,'∎','∎','∎','∎','∎','∎','∎','∎',0],
    [0,0,0,0,0,0,0,0,'∎',0,0,0],
    [0,'∎','∎','∎','∎','∎','∎',0,0,0,'∎',0]
]

# --- MAP 2: Open field with a wall ---
grid2 = [
    [0,0,0,0,0,0],
    [0,0,0,0,0,0],
    [0,0,0,0,1,0],
    [1,1,1,1,1,0],
    [0,0,0,0,0,0]
]
visual_grid2 = [
    [0,0,0,0,0,0],
    [0,0,0,0,0,0],
    [0,0,0,0,'∎',0],
    ['∎','∎','∎','∎','∎',0],
    [0,0,0,0,0,0]
]

# --- MAP 3: Maze-like ---
grid3 = [
    [0,1,0,0,0,0,0,0,0,0],
    [0,1,0,0,0,0,0,1,0,1],
    [0,0,0,1,0,0,0,1,0,1],
    [1,1,1,1,1,1,1,1,0,0],
    [0,0,0,0,0,0,0,0,0,0]
]
visual_grid3 = [
    [0,'∎',0,0,0,0,0,0,0,0],
    [0,'∎',0,0,0,0,0,'∎',0,'∎'],
    [0,0,0,'∎',0,0,0,'∎',0,'∎'],
    ['∎','∎','∎','∎','∎','∎','∎','∎',0,0],
    [0,0,0,0,0,0,0,0,0,0]
]

# --- MAP 4: Large open with scattered walls ---
grid4 = [
    [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
    [1,1,1,1,0,1,1,1,1,1,1,1,1,1,1,0],
    [0,0,0,1,0,0,0,0,0,0,0,0,0,0,1,0],
    [0,1,0,1,1,1,1,1,1,1,1,1,1,0,1,0],
    [0,1,0,0,0,0,0,0,0,0,0,0,1,0,1,0],
    [0,1,1,1,1,1,1,1,1,1,1,0,1,0,1,0],
    [0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0],
    [1,1,1,1,1,1,1,1,1,0,1,0,1,1,1,0],
    [0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0],
    [0,1,1,1,1,1,1,0,1,1,1,1,1,1,1,0],
    [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
]
visual_grid4 = [
    [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
    ['∎','∎','∎','∎',0,'∎','∎','∎','∎','∎','∎','∎','∎','∎','∎',0],
    [0,0,0,'∎',0,0,0,0,0,0,0,0,0,0,'∎',0],
    [0,'∎',0,'∎','∎','∎','∎','∎','∎','∎','∎','∎','∎',0,'∎',0],
    [0,'∎',0,0,0,0,0,0,0,0,0,0,'∎',0,'∎',0],
    [0,'∎','∎','∎','∎','∎','∎','∎','∎','∎','∎',0,'∎',0,'∎',0],
    [0,0,0,0,0,0,0,0,0,0,'∎',0,'∎',0,0,0],
    ['∎','∎','∎','∎','∎','∎','∎','∎','∎',0,'∎',0,'∎','∎','∎',0],
    [0,0,0,0,0,0,0,0,'∎',0,0,0,0,0,0,0],
    [0,'∎','∎','∎','∎','∎','∎',0,'∎','∎','∎','∎','∎','∎','∎',0],
    [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
]

# --- MAP 5: Large maze ---
grid5 = [
    [0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,0],
    [0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1],
    [0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,1],
    [1,1,0,1,1,1,0,1,1,1,0,1,1,1,0,1,1,1,0,1],
    [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
    [0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0],
    [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
]
visual_grid5 = [
    [0,'∎',0,0,0,'∎',0,0,0,'∎',0,0,0,'∎',0,0,0,'∎',0,0],
    [0,'∎',0,'∎',0,'∎',0,'∎',0,'∎',0,'∎',0,'∎',0,'∎',0,'∎',0,'∎'],
    [0,0,0,'∎',0,0,0,'∎',0,0,0,'∎',0,0,0,'∎',0,0,0,'∎'],
    ['∎','∎',0,'∎','∎','∎',0,'∎','∎','∎',0,'∎','∎','∎',0,'∎','∎','∎',0,'∎'],
    [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
    [0,'∎','∎','∎','∎','∎','∎','∎','∎','∎','∎','∎','∎','∎','∎','∎','∎','∎','∎',0],
    [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
]

# --- MAP 6: Large spiral ---
grid6 = [
    [0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
    [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
    [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1],
    [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1],
    [1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,0,1],
    [1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,1],
    [1,0,1,0,1,1,1,1,1,1,1,1,1,1,0,1,0,1,0,1],
    [1,0,1,0,1,0,0,0,0,0,0,0,0,1,0,1,0,1,0,1],
    [1,0,1,0,1,0,1,1,1,1,1,1,0,1,0,1,0,1,0,1],
    [1,0,1,0,1,0,1,0,0,0,0,1,0,1,0,1,0,1,0,1],
    [1,0,1,0,1,0,1,0,1,1,0,1,0,1,0,1,0,1,0,1],
    [1,0,1,0,1,0,1,0,1,0,0,1,0,1,0,1,0,1,0,1],
    [1,0,1,0,1,0,1,0,1,0,1,1,0,1,0,1,0,1,0,1],
    [1,0,1,0,1,0,1,0,1,0,0,0,0,1,0,1,0,1,0,1],
    [1,0,1,0,1,0,1,0,1,1,1,1,1,1,0,1,0,1,0,1],
    [1,0,1,0,1,0,1,0,0,0,0,0,0,0,0,1,0,1,0,1],
    [1,0,1,0,1,0,1,0,1,1,1,1,1,1,1,1,0,1,0,1],
    [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1],
    [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1],
    [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
]
visual_grid6 = [
    [0,'∎','∎','∎','∎','∎','∎','∎','∎','∎','∎','∎','∎','∎','∎','∎','∎','∎','∎','∎'],
    [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,'∎'],
    ['∎','∎','∎','∎','∎','∎','∎','∎','∎','∎','∎','∎','∎','∎','∎','∎','∎','∎',0,'∎'],
    ['∎',0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,'∎',0,'∎'],
    ['∎',0,'∎','∎','∎','∎','∎','∎','∎','∎','∎','∎','∎','∎','∎','∎',0,'∎',0,'∎'],
    ['∎',0,'∎',0,0,0,0,0,0,0,0,0,0,0,0,'∎',0,'∎',0,'∎'],
    ['∎',0,'∎',0,'∎','∎','∎','∎','∎','∎','∎','∎','∎','∎',0,'∎',0,'∎',0,'∎'],
    ['∎',0,'∎',0,'∎',0,0,0,0,0,0,0,0,'∎',0,'∎',0,'∎',0,'∎'],
    ['∎',0,'∎',0,'∎',0,'∎','∎','∎','∎','∎','∎',0,'∎',0,'∎',0,'∎',0,'∎'],
    ['∎',0,'∎',0,'∎',0,'∎',0,0,0,0,'∎',0,'∎',0,'∎',0,'∎',0,'∎'],
    ['∎',0,'∎',0,'∎',0,'∎',0,'∎','∎',0,'∎',0,'∎',0,'∎',0,'∎',0,'∎'],
    ['∎',0,'∎',0,'∎',0,'∎',0,'∎',0,0,'∎',0,'∎',0,'∎',0,'∎',0,'∎'],
    ['∎',0,'∎',0,'∎',0,'∎',0,'∎',0,'∎','∎',0,'∎',0,'∎',0,'∎',0,'∎'],
    ['∎',0,'∎',0,'∎',0,'∎',0,'∎',0,0,0,0,'∎',0,'∎',0,'∎',0,'∎'],
    ['∎',0,'∎',0,'∎',0,'∎',0,'∎','∎','∎','∎','∎','∎',0,'∎',0,'∎',0,'∎'],
    ['∎',0,'∎',0,'∎',0,'∎',0,0,0,0,0,0,0,0,'∎',0,'∎',0,'∎'],
    ['∎',0,'∎',0,'∎',0,'∎','∎','∎','∎','∎','∎','∎','∎','∎','∎',0,'∎',0,'∎'],
    ['∎',0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,'∎',0,'∎'],
    ['∎','∎','∎','∎','∎','∎','∎','∎','∎','∎','∎','∎','∎','∎','∎','∎','∎','∎',0,'∎'],
    [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
]

# --- MAP 7: Open with a horizontal wall and a gap (12x16) ---
grid7 = [
    [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
    [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
    [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
    [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
    [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
    [1,1,1,1,1,1,1,1,0,1,1,1,1,1,1,1],
    [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
    [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
    [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
    [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
    [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
    [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
]
visual_grid7 = [
    [0]*16,
    [0]*16,
    [0]*16,
    [0]*16,
    [0]*16,
    ['∎','∎','∎','∎','∎','∎','∎','∎',0,'∎','∎','∎','∎','∎','∎','∎'],
    [0]*16,
    [0]*16,
    [0]*16,
    [0]*16,
    [0]*16,
    [0]*16
]

# region A*

#g = 0  Cost from start to current node
#h = 0  How far away cost from goal
#f = 0  Total cost (g + h)

def draw_text(text, font, color, x, y):
    img = font.render(text, True, color)
    screen.blit(img, (x, y))

def reset_visual_grid(visual_grid, original_visual_grid):
    for y in range(len(visual_grid)):
        for x in range(len(visual_grid[0])):
            visual_grid[y][x] = original_visual_grid[y][x]

def display_grid(start_x, start_y, goal_x, goal_y, grid, visual_grid):
    x, y = 10, 0
    screen.fill((255, 255, 255))  # Clear the screen
    for item_index, item in enumerate(visual_grid):
        for col_index, col in enumerate(item):
            if item_index == start_y and col_index == start_x:
                draw_text(f"{visual_grid[start_y][start_x]}", text_font, (0, 255, 0), x, y)
            elif item_index == goal_y and col_index == goal_x:
                draw_text(f"{visual_grid[goal_y][goal_x]}", text_font, (255, 0, 0), x, y)
            elif col == '×':
                draw_text(f"{col}", text_font, (0, 0, 255), x, y)
            elif col == '✓':
                draw_text(f"{col}", text_font, (0, 200, 0), x, y)
            elif col == '■':
                draw_text(f"{col}", text_font, (200, 0, 200), x, y)
            elif col == '∎':
                draw_text(f"{col}", text_font, (100, 100, 100), x, y)
            else:
                draw_text(f"{col}", text_font, (0, 0, 0), x, y)
            x += 20
        x = 10
        y += 30
    pygame.display.flip()

def find_path(start_x, start_y, goal_x, goal_y, grid, visual_grid, visualize=True):
    start_node = node(start_x, start_y)
    goal_node = node(goal_x, goal_y)
    to_do = [start_node]
    done = []
    max_width = len(grid[0])
    max_length = len(grid)
    while len(to_do) > 0:
        current = to_do[0]
        for item in to_do:
            item.calcute_cost(goal_node)
            if item.f < current.f:
                current = item
            elif item.f == current.f and item.h < current.h:
                current = item
        to_do.remove(current)
        if current.x == goal_node.x and current.y == goal_node.y:
            to_do.clear()
            path = []
            while current.parent is not None:
                path.append(current)
                current = current.parent
            return path[::-1]
        # Add neighbors
        for dx, dy, action in [(0,1,"down"), (1,0,"right"), (-1,0,"left"), (0,-1,"up")]:
            nx, ny = current.x + dx, current.y + dy
            if 0 <= nx < max_width and 0 <= ny < max_length and grid[ny][nx] == 0:
                neighbor = node(nx, ny, parent=current, action=action)
                if neighbor not in done and neighbor not in to_do:
                    to_do.append(neighbor)
        done.append(current)
        current.visited = True
        if visual_grid[current.y][current.x] not in ('✓', '■'):
            visual_grid[current.y][current.x] = '×'
        if visualize:
            display_grid(start_x, start_y, goal_x, goal_y, grid, visual_grid)
            pygame.time.delay(100)
    return []

# List of all maps and their visual grids, start and goal positions
maps = [
    (grid1, visual_grid1, [row[:] for row in visual_grid1], 0, 0, 0, len(grid1)-1),
    (grid2, visual_grid2, [row[:] for row in visual_grid2], 0, 0, 0, len(grid2)-1),
    (grid3, visual_grid3, [row[:] for row in visual_grid3], 0, 0, 0, len(grid3)-1),
    (grid4, visual_grid4, [row[:] for row in visual_grid4], 0, 0, 0, len(grid4)-1),
    (grid5, visual_grid5, [row[:] for row in visual_grid5], 0, 0, 0, len(grid5)-1),
    (grid6, visual_grid6, [row[:] for row in visual_grid6], 0, 0, 0, len(grid6)-1),
    (grid7, visual_grid7, [row[:] for row in visual_grid7], 0, 0, 0, len(grid7)-1),
]

#region Pygame
pygame.init()
screen = pygame.display.set_mode((1024, 768))
text_font = pygame.font.SysFont("Arial", 30)
running = True

map_index = 0

while running and map_index < len(maps):
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False

    grid, visual_grid, original_visual_grid, start_x, start_y, goal_x, goal_y = maps[map_index]
    reset_visual_grid(visual_grid, original_visual_grid)
    display_grid(start_x, start_y, goal_x, goal_y, grid, visual_grid)
    pygame.time.delay(500)

    # Find path and visualize search
    path = find_path(start_x, start_y, goal_x, goal_y, grid, visual_grid, visualize=True)
    # Animate the path with checkmarks
    for item in path:
        visual_grid[item.y][item.x] = '✓'
        display_grid(start_x, start_y, goal_x, goal_y, grid, visual_grid)
        pygame.time.delay(80)
    pygame.time.delay(800)
    map_index += 1

pygame.quit()
#end region pygame