a*star pathfinding visualization is incredibly slow / python - python

I tried to implement shortest pathfinding algorithm using python and pygame. The algorithm and visualization seems to work. However, the time it takes to find the path is increasing exponentially as I put the start and end nodes far away from each other. It takes hours to find a path with a length of 5 or 6 nodes. I reckon the problem is with one of the loops in the astar function but I don't know which one and how to solve it. I searched the internet regarding the problem but couldn't find any solution. Here is the code:
import pygame
class cube():
rows = 30
w = 840
def __init__(self, pos=None, color=None):
self.pos = pos
self.color = color
self.neighbours = []
def draw(self, surface):
dis = self.w // self.rows
i = self.pos[0]
j = self.pos[1]
pygame.draw.rect(surface, self.color, (i * dis + 1, j * dis + 1, dis - 2, dis - 2))
class node(cube):
def __init__(self, pos=None, color=None, parent=None):
super().__init__(pos, color)
self.parent = parent
self.g = 0
self.h = 0
self.f = self.g + self.h
def __eq__(self, other):
return self.pos == other.pos
//here is the astar function
def astar(start, end):
global rows, path, obs_list, start_node, end_node, window, closed_list, blue, children, red, green, grey
counter = 0
green = (0,255,0)
red = (255,0,0)
blue = (0,0,255)
grey = (170,170,170)
start_node = node(start, red)
end_node = node(end, red)
start_node.g = start_node.h = start_node.f = 0
end_node.g = end_node.h = end_node.f = 0
open_list = []
closed_list = []
open_list.append(start_node)
while len(open_list) > 0:
# Get the current node
current_node = open_list[0]
current_index = 0
for index, item in enumerate(open_list):
if item.f < current_node.f:
current_node = item
current_index = index
# Pop current off open list, add to closed list
open_list.pop(current_index)
closed_list.append(current_node)
visualize(window, open_list)
if current_node.pos == end_node.pos:
path = []
current = current_node
while current is not None:
path.append(current.pos)
current = current.parent
return path[::-1] # Return reversed path
children = []
for new_position in [(0, -1), (0, 1), (-1, 0), (1, 0), (-1, -1), (-1, 1), (1, -1), (1, 1)]:
node_position = (current_node.pos[0] + new_position[0], current_node.pos[1] + new_position[1])
#check if neighbour isn't a obstacle
if node_position in obs_list:
# print("not possible")
continue
#check if neighbour isn't in closed list
#create a new node
new_node = node(node_position, green, current_node)
# new node is the child of the previous node. Add child node to the list
children.append(new_node)
# print(children)
#loop through children
for child in children:
for closed_child in closed_list:
if child == closed_child:
continue
child.g = current_node.g + 10
child.h = ((child.pos[0] - end_node.pos[0]) **2) + ((child.pos[1] - end_node.pos[1]) **2)
child.f = child.g + child.h
for open_node in open_list:
if child == open_node and child.g > open_node.g:
continue
# visualize(window, children)
# Add the child to the open list
open_list.append(child)
def draw_grid(w, rows, window):
size_between = w // rows
x = 0
y = 0
for l in range(rows):
x = x + size_between
y = y + size_between
pygame.draw.line(window, (255,255,255), (x, 0), (x, w))
pygame.draw.line(window, (255,255,255), (0,y), (w,y))
def visualize(surface, list):
global red, blue, green, grey, counter
for i,c in enumerate(list):
c.draw(surface)
pygame.display.update()
def show_path(surface):
global path
for i in path:
node_path = cube(i, color=(0,0,255))
node_path.draw(surface)
pygame.display.update()
for c in (path):
node_p = cube(c, color=(35, 180, 89))
node_p.draw(surface)
def draw_inital(surface):
surface.fill((0, 0, 0))
draw_grid(840, 30, surface)
start_node.draw(surface)
end_node.draw(surface)
pygame.display.update()
def mouse_press(x):
global rows, window, obs, start_node, end_node, obs_list
t = x[0]
w = x[1]
g1 = t // (840 // rows)
g2 = w // (840 // rows)
obs = cube((g1,g2), (125, 125, 125))
if obs.pos == start_node.pos:
obs.color = (255,0,0)
obs.draw(window)
pygame.display.update()
elif obs.pos == end_node.pos:
obs.color = (255, 0, 0)
obs.draw(window)
pygame.display.update()
else:
if obs.pos not in obs_list:
obs_list.append(obs.pos)
obs.draw(window)
pygame.display.update()
def main():
global start_node, end_node, rows, window, counter, obs_list, start, end
width = 840
rows = 30
window = pygame.display.set_mode((width,width))
start = (12, 24)
end = (12, 26)
obs_list = []
start_node = node(start, color=(255,0,0))
end_node = node(end, color=(255,0,0))
start_node.draw(window)
end_node.draw(window)
draw_inital(window)
loop = True
while loop:
ev = pygame.event.get()
for event in ev:
if event.type == pygame.QUIT:
pygame.quit()
if pygame.mouse.get_pressed()[0]:
try:
pos = pygame.mouse.get_pos()
mouse_press(pos)
except AttributeError:
pass
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_SPACE:
loop = False
break
path = astar(start, end)
print(path)
show_path(window)
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
main()

Your code is very hard to read. Here's some observations.
Minor Bug
child.g = current_node.g + 10
Needs to become
child.g = current_node.g + 1
This fixes the problem, but only by luck. It is not actually the solution!
Real Bug
You are adding too many elements to the open_list.
for new_position in ...:
# create a new node
new_node = node(node_position, green, current_node)
...
children.append(new_node)
# print(children)
# loop through children
for child in children:
...
open_list.append(child)
Notice that for each neighbor (for new_position in ...) (8x) you are appending a
new child to the children list, then iterating over all children so far. That's 1/2 * 8 * 8 = 32 children added.
A simple fix is to have only one loop per neighbor/child, not the two that you currently have.
Priority Queue
You are not using a Priority Queue for the open list. This means for each element that you pop from the open list, you are scanning through the whole list.
Use this: https://docs.python.org/3/library/heapq.html
Set Membership
for closed_child in closed_list:
if child == closed_child:
continue
Should become, but would require making the 'node' type hashable:
closed_list = set()
...
if child in closed_list:
continue
Heuristic Function
Bug: you forgot the square root when computing the Euclidean distance.

Related

How to turn set items into indexes for an array

I am currently trying to solve an exercise in CS50 AI, where I am supposed to create a tictactoe using a minimax algorithm. While doing this, I have to also create a function that generates possible options as well a function that generates a new state of the board as soon as one of the possible actions is chosen. However, I am having an error in my code.
This is the error:
File "c:\Users\Melisa\OneDrive\Desktop\tictactoe\tictactoe.py", line 40, in result
kopja[i][j] = player(board)
TypeError: list indices must be integers or slices, not tuple`
This is my code for the whole problem:
import copy
import math
X = "X"
O = "O"
EMPTY = None
def initial_state():
return [[EMPTY, EMPTY, EMPTY],
[EMPTY, EMPTY, EMPTY],
[EMPTY, EMPTY, EMPTY]]
def player(board):
Xc= 0
Oc= 0
for i in board:
for j in i:
if j == X:
Xc+=1
if j == O:
Oc+=1
# since we start with x
if Xc>Oc:
return O
else:
return X
def actions(board):
possible= set()
for i in range(0,len(board)):
for j in range(len(board[0])):
if board[i][j]== EMPTY:
possible.add((i,j))
return possible
def result(board, action):
kopja = copy.deepcopy(board)
i,j = action[0],action[1]
kopja[i][j] = player(board)
return kopja
def winner(board):
# present all of the winning outlays in a manual way:
# horisontally; only i changes ,j is contantly 0,1,2
for e in range(3):
if board[e][0]==board[e][1]==board[e][2] and board[e][0] != EMPTY:
winneri = board[0][e]
# diagonals are purely fixed
if (board[0][0]==board[1][1]==board[2][2] or board[0][2]==board[1][1]==board[2][0])and board[1][1]!=EMPTY:
winneri= board[1][1]
else:
winneri= None
return winneri
def terminal(board):
if winner(board) == X or winner(board) == O:
return True
for i in range(3):
for j in range(3):
if board[i][j] == EMPTY:
return False
return True
def utility(board):
if winner(board)== X:
return 1
if winner(board)== O:
return -1
else:
return 0
def maxval(board):
if terminal(board):
return utility(board)
else:
v = float('-inf')
for action in actions(board):
v = max(v,minval(result(board,action)))
return v
def minval(board):
if terminal(board):
return utility(board)
else:
v = float('inf')
for action in actions(board):
v = min(v,maxval(result(board,action)))
return v
def minimax(board):
if terminal(board):
return None
else:
listx=[]
if player(board) == X:
for action in actions(board):
listx.append((minval(result(board,action)),action))
listx.reverse()
listi=listx
return listi[0]
if player(board) == O:
listo=[]
for action in actions(board):
listo.append((maxval(result(board,action)),action))
listo.reverse()
lista=listo
return lista[0]
Here is the code that CS50 uses to run the program:
import pygame
import sys
import time
import tictactoe as ttt
pygame.init()
size = width, height = 600, 400
black = (0, 0, 0)
white = (255, 255, 255)
screen = pygame.display.set_mode(size)
mediumFont = pygame.font.Font("OpenSans-Regular.ttf", 28)
largeFont = pygame.font.Font("OpenSans-Regular.ttf", 40)
moveFont = pygame.font.Font("OpenSans-Regular.ttf", 60)
user = None
board = ttt.initial_state()
ai_turn = False
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
screen.fill(black)
# Let user choose a player.
if user is None:
# Draw title
title = largeFont.render("Play Tic-Tac-Toe", True, white)
titleRect = title.get_rect()
titleRect.center = ((width / 2), 50)
screen.blit(title, titleRect)
# Draw buttons
playXButton = pygame.Rect((width / 8), (height / 2), width / 4, 50)
playX = mediumFont.render("Play as X", True, black)
playXRect = playX.get_rect()
playXRect.center = playXButton.center
pygame.draw.rect(screen, white, playXButton)
screen.blit(playX, playXRect)
playOButton = pygame.Rect(5 * (width / 8), (height / 2), width / 4, 50)
playO = mediumFont.render("Play as O", True, black)
playORect = playO.get_rect()
playORect.center = playOButton.center
pygame.draw.rect(screen, white, playOButton)
screen.blit(playO, playORect)
# Check if button is clicked
click, _, _ = pygame.mouse.get_pressed()
if click == 1:
mouse = pygame.mouse.get_pos()
if playXButton.collidepoint(mouse):
time.sleep(0.2)
user = ttt.X
elif playOButton.collidepoint(mouse):
time.sleep(0.2)
user = ttt.O
else:
# Draw game board
tile_size = 80
tile_origin = (width / 2 - (1.5 * tile_size),
height / 2 - (1.5 * tile_size))
tiles = []
for i in range(3):
row = []
for j in range(3):
rect = pygame.Rect(
tile_origin[0] + j * tile_size,
tile_origin[1] + i * tile_size,
tile_size, tile_size
)
pygame.draw.rect(screen, white, rect, 3)
if board[i][j] != ttt.EMPTY:
move = moveFont.render(board[i][j], True, white)
moveRect = move.get_rect()
moveRect.center = rect.center
screen.blit(move, moveRect)
row.append(rect)
tiles.append(row)
game_over = ttt.terminal(board)
player = ttt.player(board)
# Show title
if game_over:
winner = ttt.winner(board)
if winner is None:
title = f"Game Over: Tie."
else:
title = f"Game Over: {winner} wins."
elif user == player:
title = f"Play as {user}"
else:
title = f"Computer thinking..."
title = largeFont.render(title, True, white)
titleRect = title.get_rect()
titleRect.center = ((width / 2), 30)
screen.blit(title, titleRect)
# Check for AI move
if user != player and not game_over:
if ai_turn:
time.sleep(0.5)
move = ttt.minimax(board)
board = ttt.result(board, move)
ai_turn = False
else:
ai_turn = True
# Check for a user move
click, _, _ = pygame.mouse.get_pressed()
if click == 1 and user == player and not game_over:
mouse = pygame.mouse.get_pos()
for i in range(3):
for j in range(3):
if (board[i][j] == ttt.EMPTY and tiles[i][j].collidepoint(mouse)):
board = ttt.result(board, (i, j))
if game_over:
againButton = pygame.Rect(width / 3, height - 65, width / 3, 50)
again = mediumFont.render("Play Again", True, black)
againRect = again.get_rect()
againRect.center = againButton.center
pygame.draw.rect(screen, white, againButton)
screen.blit(again, againRect)
click, _, _ = pygame.mouse.get_pressed()
if click == 1:
mouse = pygame.mouse.get_pos()
if againButton.collidepoint(mouse):
time.sleep(0.2)
user = None
board = ttt.initial_state()
ai_turn = False
pygame.display.flip()
I tried to use the tuples inside the set as indexes for the array(board) by assigning them :
i,j = action[0],action[1]
and expected this solution to work, but instead it generated an error.
After some digging thru your code, I think I figured it out. Did you check the value of action before the call to result()? If so, I think you will find it is not a what you think it is. (In other words, it does not look like (1,1)).
The cause is complicated...it begins near the end of minimax() then propagates thru your code. The code segment of interest is repeated below for easy reference:
if player(board) == X:
for action in actions(board):
listx.append((minval(result(board,action)),action))
listx.reverse()
listi=listx
return listi[0]
If I understand your code, inside this for loop you are creating listx as a list of tuples with (value, action) pairs. Then, when you exit the loop, you return listi[0] which is the 1st (value, action) tuple in the list. You want to return the action from the first tuple, which is listi[0][1].
Once you get that fixed, review the logic in the loop. You are creating a list and reversing it each time thru the loop. Its not clear to me why are you reversing it. Do you want to sort the tuples based on the value? If so, you should do that after you exit the loop, AND use the value in the tuple as the sort key. Also, you really don't need 2 lists.

How to generate my maze instantly so I don't have to watch it Generate?

So I'm creating a game and I'm using Recursive backtracking algorithm to create the maze, however, I don't want it to show the maze generation and just to instantly generate the maze. I'm unsure of how to actually do this though so any help would be appreciated, I've already tried not drawing the generated white part but that then doesn't create the maze.
import pygame
import random
import time
class Cell(object):
def __init__(self, x, y, cell_size, screen, black, white, red, blue):
# position in matrix
self.x = x
self.y = y
# keeps track of which walls are still visible
self.walls = [True, True, True, True]
# checks if cell has been visited during generation
self.generated = False
# checks if cell is on path during solving
self.on_path = False
# checks if cell has been visited during solving
self.visited = False
self.cell_size = cell_size
self.screen = screen
self.black = black
self.white = white
self.red = red
self.blue = blue
def draw_cell(self):
# coordinates on screen
x = self.x * self.cell_size
y = self.y * self.cell_size
# draws a wall if it still exists
if self.walls[0]:
pygame.draw.line(self.screen, self.black, (x, y), (x + self.cell_size, y), 5)
if self.walls[1]:
pygame.draw.line(self.screen, self.black,
(x, y + self.cell_size), (x + self.cell_size, y + self.cell_size), 5)
if self.walls[2]:
pygame.draw.line(self.screen, self.black,
(x + self.cell_size, y), (x + self.cell_size, y + self.cell_size), 5)
if self.walls[3]:
pygame.draw.line(self.screen, self.black, (x, y), (x, y + self.cell_size), 5)
# marks out white if generated during generation
if self.generated:
pygame.draw.rect(self.screen, self.white, (x, y, self.cell_size, self.cell_size))
class Maze:
def __init__(self, screen, cell_size, rows, cols, white, black, red, blue):
self.screen = screen
self.cell_size = cell_size
self.rows = rows
self.cols = cols
self.state = None
self.maze = []
self.stack = []
self.current_x = 0
self.current_y = 0
self.row = []
self.neighbours = []
self.black = black
self.white = white
self.red = red
self.blue = blue
self.cell = None
def on_start(self):
# maintains the current state
# maze matrix of cell instances
self.maze = []
# stack of current cells on path
self.stack = []
self.current_x, self.current_y = 0, 0
self.maze.clear()
self.stack.clear()
for x in range(self.cols):
self.row = []
for y in range(self.rows):
self.cell = Cell(x, y, self.cell_size, self.screen, self.black, self.white, self.red, self.blue)
self.row.append(self.cell)
self.maze.append(self.row)
def in_bounds(self, x, y):
return 0 <= x < self.cols and 0 <= y < self.rows
def find_next_cell(self, x, y):
# keeps track of valid neighbors
self.neighbours = []
# loop through these two arrays to find all 4 neighbor cells
dx, dy = [1, -1, 0, 0], [0, 0, 1, -1]
for d in range(4):
# add cell to neighbor list if it is in bounds and not generated
if self.in_bounds(x + dx[d], y + dy[d]):
if not self.maze[x + dx[d]][y + dy[d]].generated:
self.neighbours.append((x + dx[d], y + dy[d]))
# returns a random cell in the neighbors list, or -1 -1 otherwise
if len(self.neighbours) > 0:
return self.neighbours[random.randint(0, len(self.neighbours) - 1)]
else:
return -1, -1
def remove_wall(self, x1, y1, x2, y2):
# x distance between original cell and neighbor cell
xd = self.maze[x1][y1].x - self.maze[x2][y2].x
# to the bottom
if xd == 1:
self.maze[x1][y1].walls[3] = False
self.maze[x2][y2].walls[1] = False
# to the top
elif xd == -1:
self.maze[x1][y1].walls[1] = False
self.maze[x2][y2].walls[3] = False
# y distance between original cell and neighbor cell
xy = self.maze[x1][y1].y - self.maze[x2][y2].y
# to the right
if xy == 1:
self.maze[x1][y1].walls[0] = False
self.maze[x2][y2].walls[2] = False
# to the left
elif xy == -1:
self.maze[x1][y1].walls[2] = False
self.maze[x2][y2].walls[0] = False
def create_maze(self):
self.maze[self.current_x][self.current_y].generated = True
# self.maze[self.current_x][self.current_y].draw_current()
next_cell = self.find_next_cell(self.current_x, self.current_y)
# checks if a neighbor was returned
if next_cell[0] >= 0 and next_cell[1] >= 0:
self.stack.append((self.current_x, self.current_y))
self.remove_wall(self.current_x, self.current_y, next_cell[0], next_cell[1])
self.current_x = next_cell[0]
self.current_y = next_cell[1]
# no neighbor, so go to the previous cell in the stack
elif len(self.stack) > 0:
previous = self.stack.pop()
self.current_x = previous[0]
self.current_y = previous[1]
def main():
WIDTH, HEIGHT = 800, 800
CELL_SIZE = 40
ROWS, COLUMNS = int(HEIGHT / CELL_SIZE), int(WIDTH / CELL_SIZE)
# color variables
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
RED = (255, 0, 0)
BLUE = (0, 0, 255)
# initialize pygame
pygame.init()
SCREEN = pygame.display.set_mode((WIDTH, HEIGHT))
SCREEN.fill(WHITE)
pygame.display.set_caption("Maze Gen")
CLOCK = pygame.time.Clock()
FPS = 60
m = Maze(SCREEN, CELL_SIZE, ROWS, COLUMNS, WHITE, BLACK, RED, BLUE)
m.on_start()
running = True
while running:
CLOCK.tick(FPS)
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
for i in range(m.cols):
for j in range(m.rows):
m.maze[i][j].draw_cell()
m.create_maze()
pygame.display.flip()
if __name__ == "__main__":
main()
pygame.quit()
Call m.create_maze() in a loop before the application loop. Terminate the loop when len(m.stack) == 0:
def main():
# [...]
m = Maze(SCREEN, CELL_SIZE, ROWS, COLUMNS, WHITE, BLACK, RED, BLUE)
m.on_start()
while True:
m.create_maze()
if len(m.stack) == 0:
break
running = True
while running:
CLOCK.tick(FPS)
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
for i in range(m.cols):
for j in range(m.rows):
m.maze[i][j].draw_cell()
pygame.display.flip()

Pathfinding Visualizer in pygame [duplicate]

This question already has answers here:
tips on Adding/creating a drop down selection box in pygame
(1 answer)
trying creating dropdown menu pygame, but got stuck
(1 answer)
Closed 1 year ago.
I am still learning how to code and wanted to create a pathfinding visualizer by following this video:
https://www.youtube.com/watch?v=JtiK0DOeI4A&ab_channel=TechWithTim
I think i understand the algorithm and the whole code more or less, but i want to expand the program. I want to implement more algorithms, some buttons and a file dialog to import and export labyrinths and stop the time the algorithm takes and some stuff like that. I know how to implement another algorithm, but i am new to Pygame, so i have no idea how to display that. I tried to combined it with PyQt5 and searched for different solutions with Pygame itself, but nothing really worked.
How can i add some bar or something like that above where i can choose the algorithm i want to use with a dropdown menu or similar and import the labyrinth and all that stuff?
This is the corresponding code:
import pygame
import math
from queue import PriorityQueue
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)
YELLOW = (255, 255, 0)
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
PURPLE = (128, 0, 128)
ORANGE = (255, 165, 0)
GREY = (128, 128, 128)
TURQUOISE = (64, 224, 208)
class Spot:
def __init__(self, row, col, width, total_rows):
self.row = row
self.col = col
self.x = row * width
self.y = col * width
self.color = WHITE
self.neighbors = []
self.width = width
self.total_rows = total_rows
def get_pos(self):
return (self.row, self.col)
def is_closed(self):
return (self.color == RED)
def is_open(self):
return (self.color == GREEN)
def is_barrier(self):
return (self.color == BLACK)
def is_start(self):
return (self.color == ORANGE)
def is_end(self):
return (self.color == TURQUOISE)
def reset(self):
self.color = WHITE
def make_closed(self):
self.color = RED
def make_open(self):
self.color = GREEN
def make_barrier(self):
self.color = BLACK
def make_start(self):
self.color = ORANGE
def make_end(self):
self.color = TURQUOISE
def make_path(self):
self.color = PURPLE
def draw(self, win):
pygame.draw.rect(win, self.color, (self.x, self.y, self.width, self.width))
def update_neighbors(self, grid):
self.neightbors = []
if self.row < self.total_rows -1 and not grid[self.row + 1][self.col].is_barrier(): # DOWN
self.neightbors.append(grid[self.row + 1][self.col])
if self.row > 0 and not grid[self.row - 1][self.col].is_barrier(): # UP
self.neightbors.append(grid[self.row - 1][self.col])
if self.col < self.total_rows -1 and not grid[self.row][self.col + 1].is_barrier(): # RIGHT
self.neightbors.append(grid[self.row][self.col + 1])
if self.col > 0 and not grid[self.row][self.col - 1].is_barrier(): # LEFT
self.neightbors.append(grid[self.row][self.col - 1])
def __lt__(self, other):
return (False)
def h(p1, p2):
x1, y1 = p1
x2, y2 = p2
return (abs(x1 - x2) + abs(y1 - y2))
def reconstruct_path(came_from, current, draw):
while current in came_from:
current = came_from[current]
current.make_path()
draw()
def algorithm(draw, grid, start, end):
count = 0
open_set = PriorityQueue()
open_set.put((0, count, start))
came_from = {}
g_score = {spot: float ("inf") for row in grid for spot in row}
g_score[start] = 0
f_score = {spot: float ("inf") for row in grid for spot in row}
f_score[start] = h(start.get_pos(), end.get_pos())
open_set_hash = {start}
while not open_set.empty():
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
current = open_set.get()[2]
open_set_hash.remove(current)
if current == end: # draws the path
reconstruct_path(came_from, end, draw)
end.make_end()
return True
for neighbor in current.neightbors:
temp_g_score = g_score[current] + 1
if temp_g_score < g_score[neighbor]:
came_from[neighbor] = current
g_score[neighbor] = temp_g_score
f_score[neighbor] = temp_g_score + h(neighbor.get_pos(), end.get_pos())
if neighbor not in open_set_hash:
count += 1
open_set.put((f_score[neighbor], count, neighbor))
open_set_hash.add(neighbor)
neighbor.make_open()
draw()
if current != start:
current.make_closed()
return False
def make_grid(rows, width):
grid = []
gap = width // rows
for i in range(rows):
grid.append([])
for j in range(rows):
spot = Spot(i, j, gap, rows)
grid[i].append(spot)
return grid
def draw_grid(win, rows, width):
gap = width // rows
for i in range(rows):
pygame.draw.line(win, GREY, (0, i * gap), (width, i * gap))
for j in range(rows):
pygame.draw.line(win, GREY, (j * gap, 0), (j * gap, width))
def draw(win, grid, rows, width):
win.fill(WHITE)
for row in grid:
for spot in row:
spot.draw(win)
draw_grid(win, rows, width)
pygame.display.update()
def get_clicked_pos(pos, rows, width):
gap = width // rows
y, x = pos
row = y // gap
col = x // gap
return (row, col)
def main():
width = 800
win = pygame.display.set_mode((width, width))
pygame.display.set_caption("A* Path Finding Algorithm")
ROWS = 50
grid = make_grid(ROWS, width)
start = None
end = None
run = True
while run:
draw(win, grid, ROWS, width)
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
if pygame.mouse.get_pressed()[0]:
pos = pygame.mouse.get_pos()
row, col = get_clicked_pos(pos, ROWS, width)
spot = grid [row][col]
if not start and spot != end:
start = spot
start.make_start()
elif not end and spot != start:
end = spot
end.make_end()
elif spot != end and spot != start:
spot.make_barrier()
elif pygame.mouse.get_pressed()[2]:
pos = pygame.mouse.get_pos()
row, col = get_clicked_pos(pos, ROWS, width)
spot = grid [row][col]
spot.reset()
if spot == start:
start = None
elif spot == end:
end = None
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_SPACE and start and end:
for row in grid:
for spot in row:
spot.update_neighbors(grid)
algorithm(lambda: draw(win, grid, ROWS, width), grid, start, end)
if event.key == pygame.K_c:
start = None
end = None
grid = make_grid(ROWS, width)
pygame.quit()
if __name__ == "__main__":
main()

Algorithm Visualizer DFS Implementation Error

The algorithm searches straight upwards and when it reaches the barrier, it just stops, instead of going to the right
Here.
I am new to graph-algorithms and have just learned about them, so I am clueless when it comes to implementing them.
My code is not optimized, however, I will optimize it later. Here is my code:
import pygame
import queue
WIDTH = 720
pygame.init()
WIN = pygame.display.set_mode((WIDTH, WIDTH))
pygame.display.set_caption("Pathfinding Visualizer")
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 255, 0)
YELLOW = (255, 255, 0)
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
PURPLE = (128, 0, 128)
ORANGE = (255, 165 ,0)
GREY = (128, 128, 128)
TURQUOISE = (64, 224, 208)
class Node:
def __init__(self, row, col, width, total_rows):
self.row = row
self.col = col
self.width = width
self.x = row * width
self.y = col * width
self.color = WHITE
self.neighbours = []
self.total_rows = total_rows
def get_pos(self):
return self.row, self.col
def is_closed(self):
return self.color == RED
def is_open(self):
return self.color == GREEN
def is_barrier(self):
return self.color == BLACK
def is_start(self):
return self.color == ORANGE
def is_end(self):
return self.color == PURPLE
def reset(self):
self.color = WHITE
def make_start(self):
self.color = ORANGE
def make_closed(self):
self.color = RED
def make_open(self):
self.color = GREEN
def make_barrier(self):
self.color = BLACK
def make_end(self):
self.color = TURQUOISE
def make_path(self):
self.color = PURPLE
def draw(self, win):
pygame.draw.rect(win, self.color, (self.x, self.y, self.width, self.width))
def update_neighbours(self, grid):
self.neighbours = []
if self.row < self.total_rows - 1 and not grid[self.row + 1][self.col].is_barrier(): # DOWN
self.neighbours.append(grid[self.row + 1][self.col])
if self.row > 0 and not grid[self.row - 1][self.col].is_barrier(): # UP
self.neighbours.append(grid[self.row - 1][self.col])
if self.col < self.total_rows - 1 and not grid[self.row][self.col + 1].is_barrier(): # RIGHT
self.neighbours.append(grid[self.row][self.col + 1])
if self.col > 0 and not grid[self.row][self.col - 1].is_barrier(): # LEFT
self.neighbours.append(grid[self.row][self.col - 1])
def __lt__(self, other):
return False
def h(p1, p2):
x1, y1 = p1
x2, y2 = p2
return abs(x1 - x2) + abs(y1 - y2)
def reconstruct_path(came_from, current, draw):
print(came_from)
while current in came_from:
current = came_from[current]
current.make_path()
draw()
def dfs(draw, grid, start, end):
open_set = queue.LifoQueue()
open_set.put(start)
came_from = {}
g = {node: float("inf") for row in grid for node in row}
g[start] = 0
open_set_hash = {start}
while not open_set.empty():
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_c:
return False
current = open_set.get()
open_set_hash.remove(current)
if current == end:
reconstruct_path(came_from, end, draw)
end.make_end()
start.make_start()
return True, g
for n in current.neighbours:
g[n] = g[current] + 1
came_from[n] = current
if n not in open_set_hash:
open_set.put(n)
open_set_hash.add(n)
draw()
if current != start:
current.make_closed()
return False
def astar(draw, grid, start, end):
count = 0
open_set = queue.PriorityQueue()
open_set.put(item=(0, count, start))
came_from = {}
g = {node: float("inf") for row in grid for node in row}
g[start] = 0
f = {node: float("inf") for row in grid for node in row}
f[start] = h(start.get_pos(), end.get_pos())
open_set_hash = {start}
while not open_set.empty():
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_c:
return False
current = open_set.get()[2]
open_set_hash.remove(current)
if current == end:
reconstruct_path(came_from, end, draw)
end.make_end()
start.make_start()
return True, g
for neighbour in current.neighbours:
temp_g = g[current] + 1
if temp_g < g[neighbour]:
came_from[neighbour] = current
g[neighbour] = temp_g
f[neighbour] = temp_g + h(neighbour.get_pos(), end.get_pos())
if neighbour not in open_set_hash:
count += 1
open_set.put((f[neighbour], count, neighbour))
open_set_hash.add(neighbour)
neighbour.make_open()
draw()
if current != start:
current.make_closed()
return False
def make_grid(rows, width):
grid = []
gap = width // rows
for i in range(rows):
# If rows = 5, then grid would be [[], [], [], [], []]
grid.append([])
for j in range(rows):
node = Node(i, j, gap, rows)
if i == 0 or j == 0 or i == 59 or j == 59:
node.make_barrier()
grid[i].append(node)
return grid
def draw_grid(win, rows, width):
gap = width // rows
for i in range(rows):
pygame.draw.line(win, GREY, (0, i*gap), (width, i*gap))
for j in range(rows):
pygame.draw.line(win, GREY, (j*gap, 0), (j*gap, width))
def draw_text(win):
font = pygame.font.Font(None, 20)
text1 = font.render("DepthFirstSearch - D", 1, (255, 255, 255))
text2 = font.render("BreadthFirstSearch - B", 1, (255, 255, 255))
text3 = font.render("AStar - A", 1, (255, 255, 255))
textpos1 = text1.get_rect()
textpos2 = text2.get_rect()
textpos3 = text3.get_rect()
textpos1.centerx = win.get_rect().centerx-290
textpos2.centerx = win.get_rect().centerx-125
textpos3.centerx = win.get_rect().centerx+10
win.blit(text1, textpos1)
win.blit(text2, textpos2)
win.blit(text3, textpos3)
def draw(win, grid, rows, width):
win.fill(WHITE)
for row in grid:
for node in row:
node.draw(win)
draw_grid(win, rows, width)
draw_text(win)
pygame.display.update()
def get_clicked_pos(pos, rows, width):
gap = width // rows
y, x = pos
row = y // gap
col = x // gap
return row, col
def main(win, width):
ROWS = 60
grid = make_grid(ROWS, width)
start = None
end = None
run = True
started = False
while run:
draw(win, grid, ROWS, width)
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
if started:
continue
if pygame.mouse.get_pressed()[0]: # LEFT
pos = pygame.mouse.get_pos()
row, col = get_clicked_pos(pos, ROWS, width)
node = grid[row][col]
if not start and node != end and node.is_barrier() == False:
start = node
start.make_start()
elif not end and node != start and node.is_barrier() == False:
end = node
end.make_end()
elif node != end and node != start:
node.make_barrier()
elif pygame.mouse.get_pressed()[2]: # RIGHT
pos = pygame.mouse.get_pos()
row, col = get_clicked_pos(pos, ROWS, width)
node = grid[row][col]
node.reset()
if node == start:
start = None
elif node == end:
end = None
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_a and start and end:
for row in grid:
for node in row:
node.update_neighbours(grid)
astar(lambda: draw(win, grid, ROWS, width), grid, start, end)
elif event.key == pygame.K_d and start and end:
for row in grid:
for node in row:
node.update_neighbours(grid)
dfs(lambda: draw(win, grid, ROWS, width), grid, start, end)
elif event.key == pygame.K_b and start and end:
for row in grid:
for node in row:
node.update_neighbours(grid)
# bfs(lambda: draw(win, grid, ROWS, width), grid, start, end)
if event.key == pygame.K_c:
start = None
end = None
grid = make_grid(ROWS, width)
pygame.quit()
main(WIN, WIDTH)
I commented out your open_set_hash.remove(current). I don't see why you remove the current from the visited list? Also moved the came_from[n] = current inside the if condition. This was to get the reconstruct_path function to work properly.
if n not in open_set_hash:
came_from[n] = current
Looks like it works just fine after this. BTW. nice visualization you made!

My A* pathfinding algorithm does not always get the shortest path

thanks in advance for any help.
I am making a pathfinder visualiser using python in pygame.
I have tried to make the A* algorithm, but sometimes it does not find the shortest path. I have been looking through several previous questions with the same issue, which has led me to believe it may be a problem with the heuristic. If I set the hueristic value to 0, then the algorithm becomes dijkstra's and always gets the shortest path.
A grid is used for the algorithm, with x being the number row and y being the number column (I believe might be the other way around but it doesnt matter)
Each square on the grid is an object, with x and y values, as well as a gScore, hScore and fScore. On initialisation these are all set to None.
I also have some functions at the bottom to do calculations, such as find the lowest fScore node from an array, find the gScore, find the hScore, fScore and get the distance between two grid squares.
I think the problem is in the hueristic function and have tried several different methods of fixing to no avail. From looking at the code below would anyone be able to see the problem, or point me in the right direction? Any help is much appreciated .
For simplicity I have only included the A* function without any of the pygame stuff, but I can add the entire program if need be, including the gridsquare object.
def a_star():
for row in grid:
for square in row:
if square.state == "start_pos":
start_pos = square
elif square.state == "end_pos":
end_pos = square
start_pos.gScore = find_g(start_pos, start_pos)
start_pos.hScore = find_h(start_pos, end_pos)
start_pos.fScore = find_f(start_pos.gScore, start_pos.hScore)
openList = [start_pos]
closedList = []
while len(openList) > 0:
current_node = get_lowest_f_node(openList)
if current_node.state == "end_pos":
print("found")
path = [end_pos]
node = current_node
while node.parent != None:
time.sleep(SHORTEST_PATH_DELAY)
node = node.parent
path.append(node)
return
openList.remove(current_node)
closedList.append(current_node)
x = current_node.x
y = current_node.y
# get nodes around current node
node1 = grid[x][y - 1]
node2 = grid[x][y + 1]
node3 = grid[x - 1][y]
node4 = grid[x + 1][y]
successor_nodes = [node1, node2, node3, node4]
for node in successor_nodes:
# check if walkable
if (node.state == "wall") or (node in closedList):
continue
if node.gScore == None:
node.gScore = current_node.gScore
tentative_g_score = current_node.gScore + get_distance(node, current_node)
if (node in closedList) and (tentative_g_score >= node.gScore):
continue
if (node not in openList) or (tentative_g_score < node.gScore):
node.parent = current_node
node.gScore = tentative_g_score
node.fScore = node.gScore + find_h(node, end_pos)
if node not in openList:
openList.append(node)
def get_lowest_f_node(array):
min_f = min(array, key = attrgetter("fScore"))
return min_f
# distance from current node and start node
def find_g(current, start_pos):
g = get_distance(current, start_pos)
return g
# distance from current node and target / destination / finish node
def find_h(current, end_pos):
h = get_distance(current, end_pos)
return h
# hscore and gscore added together
def find_f(score1, score2):
return score1 + score2
# distance from 2 points
def get_distance(start, end):
x1 = start.x
y1 = start.y
x2 = end.x
y2 = end.y
distancex = sqr(x2 - x1)
distancey = sqr(y2 - y1)
#distance = sqrt(distancex + distancey)
distance = distancex + distancey
return distance
def sqr(number):
return number * number
Below are some images of the result of the path finding, with different patters. The starting node is always the bottom red square.
^^^ This is where the A* algorithm finds the correct, shortest path. All good.
^^^This is where A* finds a path, but it is not the shortest path. This is what I am trying to fix, any help is much appreciated.
^^^ this is dijkstra's finding the correct path when presented with the same arrangement of walls.
I am very grateful for any help.
<==== Download This image
I'm no expert on A* but awhile I wrote a script for a YouTube video I was going to produce.
If you want it then it's here: https://pastebin.com/WycrpAfZ
You can also view it here:
import math, random, sys
import pygame
from pygame.locals import *
# exit the program
def events():
for event in pygame.event.get():
if event.type == QUIT or (event.type == KEYDOWN and event.key == K_ESCAPE):
pygame.quit()
sys.exit()
# define display surface
W, H = 1920, 1080
HW, HH = W / 2, H / 2
AREA = W * H
# initialise display
pygame.init()
pygame.font.init()
CLOCK = pygame.time.Clock()
FONT_SMALL = pygame.font.Font(None, 26)
FONT_LARGE = pygame.font.Font(None, 50)
DS = pygame.display.set_mode((W, H))
pygame.display.set_caption("code.Pylet - Template")
FPS = 1
# define some colors
BLACK = (0, 0, 0, 255)
WHITE = (255, 255, 255, 255)
RED = (255, 0, 0, 255)
GREEN = (0, 128, 0, 255)
BLUE = (0, 0, 255, 255)
PURPLE = (255, 255, 0, 255)
# define node class
class node:
def __init__(self, x, y, obstacle):
self.x = x
self.y = y
self.pos = (x, y)
self.h = 0
self.g = 0
self.f = 0
self.obstacle = obstacle
self.other = None
self.parent = None
def neighbourPos(self, offset):
return (self.x + offset[0], self.y + offset[1])
def draw(self, size, color = None, id = None, surface = None):
global text, FONT_SMALL, FONT_LARGE
if not surface: surface = pygame.display.get_surface()
pos = (self.x * size[0], self.y * size[1])
if not color:
if not self.obstacle:
if not self.other: pygame.draw.rect(surface, BLACK, pos + size, 0)
else: pygame.draw.rect(surface, BLUE, pos + size, 0)
else:
pygame.draw.rect(surface, WHITE, pos + size, 0)
else:
pygame.draw.rect(surface, color, pos + size, 0)
pygame.draw.rect(surface, WHITE, pos + size, 1)
if self.f:
text(FONT_SMALL, "G:{0}".format(self.g), pos[0] + 5, pos[1] + 5, 0, 0, surface)
text(FONT_SMALL, "H:{0}".format(self.h), pos[0] + size[0] - 5, pos[1] + 5, 1, 0, surface)
text(FONT_LARGE, "F:{0}".format(self.f), pos[0] + size[0] / 2, pos[1] + size[1] / 2 , 2, 2, surface)
if not id == None:
text(FONT_SMALL, "{0}".format(id), pos[0] + 5, pos[1] + size[1] - 5, 0, 1, surface)
def drawNodes(n, ms, cs):
for x in range(ms[0]):
for y in range(ms[1]):
n[x][y].draw(cs)
def drawNodeList(node_list, cs, color):
id = 0
for n in node_list:
n.draw(cs, color, id)
id += 1
def heuristics(pos1, pos2):
return int(math.hypot(pos1[0] - pos2[0], pos1[1] - pos2[1]) * 10)
def text(font, string, x, y, xJustify = None, yJustify = None, surface = None):
global WHITE
if not surface: surface = pygame.display.get_surface()
textSurface = font.render(string, 1, WHITE)
textRect = textSurface.get_rect()
if xJustify == 1:
x -= textRect.width
elif xJustify == 2:
x -= textRect.center[0]
if yJustify == 1:
y -= textRect.height
elif yJustify == 2:
y -= textRect.center[1]
surface.blit(textSurface, (x, y))
map = pygame.image.load("test.png").convert()
map_size = map_width, map_height = map.get_rect().size
cell_size = (W / map_width, H / map_height)
#create list of nodes
nodes = list([])
for x in range(map_width):
nodes.append(list([]))
for y in range(map_height):
color = map.get_at((x, y))
if color != WHITE:
nodes[x].append(node(x, y, False))
if color == BLUE:
start = nodes[x][y]
start.other = True
elif color == RED:
end = nodes[x][y]
end.other = True
else:
nodes[x].append(node(x, y, True))
# This list contains relative x & y positions to reference a node's neighbour
NEIGHBOURS = list([(-1, -1), (0, -1), (1, -1), (1, 0), (1, 1), (0, 1), (-1, 1), (-1, 0)])
# the closed list contains all the nodes that have been considered economical viable.
# By that I mean a node that has been closer to the end node than any other in the open list at one time
closed = list([])
# The open list contains all the closed list's neighbours that haven't been identified as being economically sound node yet
open = list([])
open.append(start) # add the start node so that we can then add it's neighbours
# if the algorithm finds the end node then pathFound will be true otherwise it's false.
# Once it becomes true there's no more calculations to do so the path finding script will be skipped over
pathFound = False
completedPath = list([]) #
# main loop
while True:
DS.fill(BLACK)
drawNodes(nodes, map_size, cell_size)
drawNodeList(open, cell_size, GREEN)
drawNodeList(closed, cell_size, RED)
if pathFound: drawNodeList(completedPath, cell_size, PURPLE)
pygame.display.update()
# wait for user to press mouse button
while not pygame.mouse.get_pressed()[0]:
events()
while pygame.mouse.get_pressed()[0]:
events()
# if we've found the quickest path from start node to end node then just draw, no need continue path finding
if pathFound: continue
if not open: continue
# get lowest f from the open list, the node with the lowest f is the most economical in terms of the path towards the end node
openNodeWithlowestF = open[0]
for o in open:
if o.f < openNodeWithlowestF.f: openNodeWithlowestF = o
mostEconomicalNodeSoFar = openNodeWithlowestF # let's make this more readable! Economical means the best path to the end given the choices but not definitive.
# remove the mostEconomicalNodeSoFar from the open list
open.remove(mostEconomicalNodeSoFar)
# add mostEconomicalNodeSoFar to the closed list
closed.append(mostEconomicalNodeSoFar)
# if the mostEconomicalNodeSoFar is equal to the end node then we've reach our target
if mostEconomicalNodeSoFar == end:
temp = end
while temp.parent:
completedPath.append(temp)
temp = temp.parent
completedPath.append(start)
pathFound = True
# get the path etc
# iterate through the list of neighbours belonging to the mostEconomicalNodeSoFar. Why?
for neighbourOffset in NEIGHBOURS:
nx, ny = mostEconomicalNodeSoFar.neighbourPos(neighbourOffset)
if nx < 0 or nx >= map_width or ny < 0 or ny >= map_height: continue
neighbour = nodes[nx][ny] # create a variable to represent the mostEconomicalNodeSoFar's neighbour
if neighbour.obstacle: continue # if the mostEconomicalNodeSoFar's neighbouring node is an obstacle then we can't ...?
if neighbour in closed: continue # if the mostEconomicalNodeSoFar's neighbouring node is in the closed list then we can't ...?
# now we need to see if the mostEconomicalNodeSoFar's neighbour is more economical ...?
hypotheticalFScore = mostEconomicalNodeSoFar.g + heuristics(neighbour.pos, mostEconomicalNodeSoFar.pos)
NeighbourIsBetterThanMostEconomicalNodeSoFar = False # Yes it's a long variable name but it describes what it is so all is good!
# is this neighbour already in open list? if it is then we don't want to be adding it again. to chec
if not neighbour in open:
NeighbourIsBetterThanMostEconomicalNodeSoFar = True
neighbour.h = heuristics(neighbour.pos, end.pos)
open.append(neighbour)
elif hypotheticalFScore < neighbour.g:
NeighbourIsBetterThanMostEconomicalNodeSoFar = True
if NeighbourIsBetterThanMostEconomicalNodeSoFar:
neighbour.parent = mostEconomicalNodeSoFar
neighbour.g = hypotheticalFScore
neighbour.f = neighbour.g + neighbour.h
#sys.exit()

Categories