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

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()

Related

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()

Python pygame noob question about animation [closed]

Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 2 years ago.
Improve this question
could you help me with a problem I have? I am a new person with regard to programming and to guide me I am using the book: How to think like a computer scientist 3rd edition. And it could not solve exercise 2 of chapter 17. This says that an error occurs when clicking on any frame that is on the right side of the sprite, which causes the animation to start, in theory it should only do the animation if it is you click directly on the sprite, I tried to solve it in many ways but I couldn't, could you help me ?, I think the error occurs in this part
return ( x >= my_x and x + my_width and y >= my_y and y < my_y + my_height)
but I'm not sure, for anything I leave you all the code I have
import pygame
gravity = 0.025
my_clock = pygame.time.Clock()
class QueenSprite:
def __init__(self, img, target_posn):
self.image = img
self.target_posn = target_posn
(x, y) = target_posn
self.posn = (x, 0) # Start ball at top of its column
self.y_velocity = 0 # with zero initial velocity
def update(self):
self.y_velocity += gravity
(x, y) = self.posn
new_y_pos = y + self.y_velocity
(target_x, target_y) = self.target_posn # Unpack the position
dist_to_go = target_y - new_y_pos # How far to our floor?
if dist_to_go < 0: # Are we under floor?
self.y_velocity = -0.65 * self.y_velocity # Bounce
new_y_pos = target_y + dist_to_go # Move back above floor
self.posn = (x, new_y_pos) # Set our new position.
def draw(self, target_surface): # Same as before.
target_surface.blit(self.image, self.posn)
def contains_point(self, pt):
""" Return True if my sprite rectangle contains point pt """
(my_x, my_y) = self.posn
my_width = self.image.get_width()
my_height = self.image.get_height()
(x, y) = pt
return ( x >= my_x and x < my_x + my_width and
y >= my_y and y < my_y + my_height)
def handle_click(self):
self.y_velocity += -2 # Kick it up
class DukeSprite:
def __init__(self, img, target_posn):
self.image = img
self.posn = target_posn
self.anim_frame_count = 0
self.curr_patch_num = 0
def update(self):
if self.anim_frame_count > 0:
self.anim_frame_count = (self.anim_frame_count + 1 ) % 60
self.curr_patch_num = self.anim_frame_count // 6
def draw(self, target_surface):
patch_rect = (self.curr_patch_num * 50, 0,
50, self.image.get_width())
target_surface.blit(self.image, self.posn, patch_rect)
def contains_point(self, pt):
""" Return True if my sprite rectangle contains pt """
(my_x, my_y) = self.posn
my_width = self.image.get_width()
my_height = self.image.get_height()
(x, y) = pt
return ( x >= my_x and x + my_width and y >= my_y and y < my_y + my_height)
def handle_click(self):
if self.anim_frame_count == 0:
self.anim_frame_count = 5
def draw_board(the_board):
""" Draw a chess board with queens, as determined by the the_board. """
pygame.init()
colors = [(255,0,0), (0,0,0)] # Set up colors [red, black]
n = len(the_board) # This is an NxN chess board.
surface_sz = 480 # Proposed physical surface size.
sq_sz = surface_sz // n # sq_sz is length of a square.
surface_sz = n * sq_sz # Adjust to exactly fit n squares.
# Create the surface of (width, height), and its window.
surface = pygame.display.set_mode((surface_sz, surface_sz))
ball = pygame.image.load("ball.png")
# Use an extra offset to centre the ball in its square.
# If the square is too small, offset becomes negative,
# but it will still be centered :-)
ball_offset = (sq_sz-ball.get_width()) // 2
all_sprites = [] # Keep a list of all sprites in the game
# Create a sprite object for each queen, and populate our list.
for (col, row) in enumerate(the_board):
a_queen = QueenSprite(ball,
(col*sq_sz+ball_offset, row*sq_sz+ball_offset))
all_sprites.append(a_queen)
# Load the sprite sheet
duke_sprite_sheet = pygame.image.load("duke_spritesheet.png")
# Instantiate two duke instances, put them on the chessboard
duke1 = DukeSprite(duke_sprite_sheet,(sq_sz*2, 0))
duke2 = DukeSprite(duke_sprite_sheet,(sq_sz*5, sq_sz))
# Add them to the list of sprites which our game loop manages
all_sprites.append(duke1)
all_sprites.append(duke2)
while True:
# Look for an event from keyboard, mouse, etc.
ev = pygame.event.poll()
if ev.type == pygame.QUIT:
break;
if ev.type == pygame.KEYDOWN:
key = ev.dict["key"]
if key == 27: # On Escape key ...
break # leave the game loop.
if key == ord("r"):
colors[0] = (255, 0, 0) # Change to red + black.
elif key == ord("g"):
colors[0] = (0, 255, 0) # Change to green + black.
elif key == ord("b"):
colors[0] = (0, 0, 255) # Change to blue + black.
if ev.type == pygame.MOUSEBUTTONDOWN: # Mouse gone down?
posn_of_click = ev.dict["pos"] # Get the coordinates.
for sprite in all_sprites:
if sprite.contains_point(posn_of_click):
sprite.handle_click()
break
for sprite in all_sprites:
sprite.update()
# Draw a fresh background (a blank chess board)
for row in range(n): # Draw each row of the board.
c_indx = row % 2 # Alternate starting color
for col in range(n): # Run through cols drawing squares
the_square = (col*sq_sz, row*sq_sz, sq_sz, sq_sz)
surface.fill(colors[c_indx], the_square)
# Now flip the color index for the next square
c_indx = (c_indx + 1) % 2
# Ask every sprite to draw itself.
for sprite in all_sprites:
sprite.draw(surface)
my_clock.tick(60) # Waste time so that frame rate becomes 60 fps
pygame.display.flip()
pygame.quit()
if __name__ == "__main__":
draw_board([0, 5, 3, 1, 6, 4, 2]) # 7 x 7 to test window size
There is < my_x missing in the comparisons expression in the method contains_point of the class DukeSprite:
return ( x >= my_x and x + my_width and y >= my_y and y < my_y + my_height)
return ( x >= my_x and x < my_x + my_width and y >= my_y and y < my_y + my_height)
Anyway in python you should use chained comparisons:
return my_x <= x < my_x + my_width and my_y <= y < my_y + my_height
In pygame you should use pygame.Rect and collidepoint(). The rectangle of the object you can get from the pygame.Surface with the method get_rect and the position can be set by an keyword argument:
def contains_point(self, pt):
""" Return True if my sprite rectangle contains pt """
my_rect = self.image.get_rect(topleft = self.posn)
return my_rect.collidepoint(pt)

Issue finding side of collision for Circle-Rectangle collision

I have found a function that works quite well for detecting collisions between a circle and a rectangle, and also finding the point of contact. I then use that point to determine which side of the rectangle the circle hit so I can reflect the circle. However, when the center of the circle is inside the rectangle, the function gives the closest point as the center of circle, and it handles it as hitting a vertex rather than a side. Here is my code:
def collide_rect(box, ball_):
#convenience
left = box.rect.left
right = left + box.rect.width
top = box.rect.top
bottom = top + box.rect.height
#find the closest point
closest = (max(left, min(ball_.center[0], right)), max(top, min(ball_.center[1], bottom)))
dx = ball_.center[0] - closest[0]
dy = ball_.center[1] - closest[1]
#handle the collsion
if math.hypot(dx, dy) <= ball.radius:
#Hit on the top or bottom
if left <= closest[0] <= right and (closest[1] == top or closest[1] == bottom):
ball_.vector = (ball_.vector[0], -1*ball_.vector[1])
#Hit on the side
elif top <= closest[1] <= bottom and (closest[0] == left or closest[0] == right):
ball_.vector = (-1*ball_.vector[0], ball_.vector[1])
#Hit a vertex
else:
ball_.vector = (-1*ball_.vector[0], -1*ball_.vector[1])
return True
else:
return False
Note that ball_.vector is the circle's direction vector and ball.radius is a class variable.
Any help with a better way to find the side of collision would be greatly appreciated!
You can find the side of the rectangle by finding the point on the rectangle that lies on the straight line given by the center of the circle and the center of the rectangle.
The point on the rectangle and the circle can be computed by the minimum relation of the offset between the center points and the size of the rectangle.
In the following algorithm, the rectangle is defined by the center point (r_cpt) and the size (r_size) and the circle is defined by the center point (c_cpt) and the radius (c_rad):
def intersectRectangleCircle(r_cpt, r_size, c_cpt, c_rad):
v2_c_cpt = pygame.math.Vector2(c_cpt)
v2_r_cpt = pygame.math.Vector2(r_cpt)
offset = v2_c_cpt - v2_r_cpt
if offset.x == 0 and offset.y == 0:
return [v2_c_cpt, v2_r_cpt]
if offset.x == 0:
ratio = r_size[1] / abs(offset.y)
elif offset.y == 0:
ratio = r_size[0] / abs(offset.x)
else:
ratio = min(r_size[0] / abs(offset.x), r_size[1] / abs(offset.y))
ratio *= 0.5
p1 = v2_r_cpt + (offset * ratio)
offset.scale_to_length(c_rad)
p2 = v2_c_cpt - offset
return [p1, p2]
The direction to the circle is the given by the vector from the center point of the rectangle to the point on the rectangle contour:
isect_pts = intersectRectangleCircle(rect_center, rect_size, circle_center, circle_diameter/2)
dx, dy = isect_pts[0].x - rect_center[0], isect_pts[1].y - rect_center[1]
See the example, (dx, dy) is represented by the magenta colored line:
repl.it/#Rabbid76/PyGame-NearestPointOnRectangle
import pygame
import math
pygame.init()
screen = pygame.display.set_mode((500, 500))
def intersectRectangleCircle(r_cpt, r_size, c_cpt, c_rad):
v2_c_cpt = pygame.math.Vector2(c_cpt)
v2_r_cpt = pygame.math.Vector2(r_cpt)
offset = v2_c_cpt - v2_r_cpt
if offset.x == 0 and offset.y == 0:
return [v2_c_cpt, v2_r_cpt]
if offset.x == 0:
ratio = r_size[1] / abs(offset.y)
elif offset.y == 0:
ratio = r_size[0] / abs(offset.x)
else:
ratio = min(r_size[0] / abs(offset.x), r_size[1] / abs(offset.y))
ratio *= 0.5
p1 = v2_r_cpt + (offset * ratio)
offset.scale_to_length(c_rad)
p2 = v2_c_cpt - offset
return [p1, p2]
def inBetween(p1, p2, px):
v = pygame.math.Vector2(p2) - pygame.math.Vector2(p1)
d = v.length()
if d == 0:
return False
v.normalize_ip()
vx = pygame.math.Vector2(px) - pygame.math.Vector2(p1)
dx = v.dot(vx)
return dx >= 0 and dx <= d
done = False
while not done:
for event in pygame.event.get():
if event.type == pygame.QUIT:
done = True
rect_center = screen.get_rect().center
rect_size = screen.get_width() // 5, screen.get_height() // 10
rect = pygame.Rect(rect_center[0] - rect_size[0] // 2, rect_center[1] - rect_size[1] // 2, *rect_size)
circle_center = pygame.mouse.get_pos()
circle_diameter = min(*screen.get_size()) // 5
isect_pts = intersectRectangleCircle(rect_center, rect_size, circle_center, circle_diameter/2)
dx, dy = isect_pts[0].x - rect_center[0], isect_pts[1].y - rect_center[1]
screen.fill((255,255,255))
pygame.draw.rect(screen, (0, 0, 0), rect, 3)
pygame.draw.circle(screen, (0, 0, 0), circle_center, circle_diameter // 2, 3)
pygame.draw.line(screen, (0, 0, 255), rect_center, circle_center, 1)
pygame.draw.line(screen, (255, 0, 255), rect_center, (round(isect_pts[0].x), round(isect_pts[0].y)), 3)
for i in range(2):
px, py = round(isect_pts[i].x), round(isect_pts[i].y)
col = (255, 0, 0) if inBetween(rect_center, circle_center, (px, py)) else (0, 255, 0)
pygame.draw.line(screen, col, (px-5, py), (px+5, py), 3)
pygame.draw.line(screen, col, (px, py-5), (px, py+5), 3)
pygame.display.flip()
pygame.quit()
quit()

a*star pathfinding visualization is incredibly slow / 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.

Display a certain element in a list

Right now our code creates a grid starting at the top left and filling in rows and columns from left to right, row by row. Currently, there are a bunch of images it can pick from. It is set using a handful of IF statements that picks between shapes and rareshapes. What I am trying to figure out how to do is change the code so instead of it picking a random rareshape, I can decide what rareshape spawns. Still new to Python and finding a lot of little things that make sense to me from other languages don't work in Python so its throwing me off a little.
EDIT:
Here is the full code. Credit for the base code written by cactusbin and revised by Gareth Rees.
import pygame, random, time, sys
from pygame.locals import *
import itertools
import os
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
SHAPE_WIDTH = 64 # Width of each shape (pixels).
SHAPE_HEIGHT = 64 # Height of each shape (pixels).
PUZZLE_COLUMNS = 10 # Number of columns on the board.
PUZZLE_ROWS = 11 # Number of rows on the board.
MARGIN = 118 # Margin around the board (pixels).
WINDOW_WIDTH = PUZZLE_COLUMNS * SHAPE_WIDTH + 2 * MARGIN + 485
WINDOW_HEIGHT = PUZZLE_ROWS * SHAPE_HEIGHT + 2 * MARGIN - 150
FONT_SIZE = 60
TEXT_OFFSET = MARGIN + 950
# Map from number of matches to points scored.
MINIMUM_MATCH = 10
EXTRA_LENGTH_POINTS = .1
RANDOM_POINTS = .3
DELAY_PENALTY_SECONDS = 1
DELAY_PENALTY_POINTS = 0
FPS = 30
EXPLOSION_SPEED = 15 # In frames per second.
SPIN_SPEED = 15
REFILL_SPEED = 10 # In cells per second.
VERTICAL = False
class Cell(object):
"""
A cell on the board, with properties:
`image` -- a `Surface` object containing the sprite to draw here.
`offset` -- vertical offset in pixels for drawing this cell.
"""
def __init__(self, image):
self.offset = 0.0
self.image = image
def tick(self, dt):
self.offset = max(0.0, self.offset - dt * REFILL_SPEED)
class Board(object):
"""
A rectangular board of cells, with properties:
`w` -- width in cells.
`h` -- height in cells.
`size` -- total number of cells.
`board` -- list of cells.
`matches` -- list of matches, each being a list of exploding cells.
`refill` -- list of cells that are moving up to refill the board.
`score` -- score due to chain reactions.
"""
def __init__(self, width, height):
self.explosion = [pygame.image.load('images/explosion{}.png'.format(i))
for i in range(1, 7)]
self.spin = [pygame.image.load('images/powerframe{}.png'.format(i))
for i in range (1, 12)]
self.image_color = {}
self.shapes = []
self.rareshapes = []
colors = 'red blue yellow'
letters = 'acgtu'
for c in colors.split():
im = pygame.image.load('images/{}.png'.format(c))
self.shapes.append(im)
self.image_color[im] = c
for l in letters:
im = pygame.image.load('rareimages/{}{}.png'.format(c, l))
self.rareshapes.append(im)
self.image_color[im] = l
self.background = pygame.image.load("images/bg.png")
self.blank = pygame.image.load("images/blank.png")
self.x = pygame.image.load("images/x.png")
self.w = width
self.h = height
self.size = width * height
self.board = [Cell(self.blank) for _ in range(self.size)]
self.matches = []
self.refill = []
self.score = 0.0
self.spin_time = 15
def randomize(self):
"""
Replace the entire board with fresh shapes.
"""
rare_shapes = [1, 9, 23, 27, 40, 42, 50, 56, 70, 81, 90]
for i in range(self.size):
if i in rare_shapes:
self.board[i] = Cell(random.choice(self.rareshapes))
else:
self.board[i] = Cell(random.choice(self.shapes))
def pos(self, i, j):
"""
Return the index of the cell at position (i, j).
"""
assert(0 <= i < self.w)
assert(0 <= j < self.h)
return j * self.w + i
def busy(self):
"""
Return `True` if the board is busy animating an explosion or a
refill and so no further swaps should be permitted.
"""
return self.refill or self.matches
def tick(self, dt):
"""
Advance the board by `dt` seconds: move rising blocks (if
any); otherwise animate explosions for the matches (if any);
otherwise check for matches.
"""
if self.refill:
for c in self.refill:
c.tick(dt)
self.refill = [c for c in self.refill if c.offset > 0]
if self.refill:
return
elif self.matches:
self.explosion_time += dt
f = int(self.explosion_time * EXPLOSION_SPEED)
if f < len(self.explosion):
self.update_matches(self.explosion[f])
return
self.update_matches(self.blank)
self.refill = list(self.refill_columns())
self.explosion_time = 0
self.matches = self.find_matches()
def draw(self, display):
"""
Draw the board on the pygame surface `display`.
"""
display.blit(self.background, (0, 0))
for i, c in enumerate(self.board):
display.blit(c.image,
(MARGIN + SHAPE_WIDTH * (i % self.w),
MARGIN + SHAPE_HEIGHT * (i // self.w - c.offset) - 68))
display.blit(self.x, (995, 735))
display.blit(self.x, (1112, 735))
display.blit(self.x, (1228, 735))
def swap(self, cursor):
"""
Swap the two board cells covered by `cursor` and update the
matches.
"""
i = self.pos(*cursor)
b = self.board
b[i], b[i+1] = b[i+1], b[i]
self.matches = self.find_matches()
def find_matches(self):
"""
Search for matches (lines of cells with identical images) and
return a list of them, each match being represented as a list
of board positions.
"""
def lines():
for j in range(self.h):
yield range(j * self.w, (j + 1) * self.w)
for i in range(self.w):
yield range(i, self.size, self.w)
def key(i):
return self.image_color.get(self.board[i].image)
def matches():
for line in lines():
for _, group in itertools.groupby(line, key):
match = list(group)
if len(match) >= MINIMUM_MATCH:
yield match
self.score = self.score + 1
return list(matches())
def update_matches(self, image):
"""
Replace all the cells in any of the matches with `image`.
"""
for match in self.matches:
for position in match:
self.board[position].image = image
def refill_columns(self):
"""
Move cells downwards in columns to fill blank cells, and
create new cells as necessary so that each column is full. Set
appropriate offsets for the cells to animate into place.
"""
for i in range(self.w):
target = self.size - i - 1
for pos in range(target, -1, -self.w):
if self.board[pos].image != self.blank:
c = self.board[target]
c.image = self.board[pos].image
c.offset = (target - pos) // self.w
target -= self.w
yield c
offset = 1 + (target - pos) // self.w
for pos in range(target, -1, -self.w):
c = self.board[pos]
c.image = random.choice(self.shapes)
c.offset = offset
yield c
class Game(object):
"""
The state of the game, with properties:
`clock` -- the pygame clock.
`display` -- the window to draw into.
`font` -- a font for drawing the score.
`board` -- the board of cells.
`cursor` -- the current position of the (left half of) the cursor.
`score` -- the player's score.
`last_swap_ticks` --
`swap_time` -- time since last swap (in seconds).
"""
def __init__(self):
pygame.init()
pygame.display.set_caption("Nucleotide")
self.clock = pygame.time.Clock()
self.display = pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT),
DOUBLEBUF)
self.board = Board(PUZZLE_COLUMNS, PUZZLE_ROWS)
self.font = pygame.font.Font(None, FONT_SIZE)
def start(self):
"""
Start a new game with a random board.
"""
self.board.randomize()
self.cursor = [0, 0]
self.score = 0.0
self.swap_time = 125
def quit(self):
"""
Quit the game and exit the program.
"""
pygame.quit()
sys.exit()
def play(self):
"""
Play a game: repeatedly tick, draw and respond to input until
the QUIT event is received.
"""
self.start()
while True:
self.draw()
dt = min(self.clock.tick(FPS) / 1000, 1 / FPS)
self.swap_time -= dt
for event in pygame.event.get():
if event.type == KEYUP:
self.input(event.key)
elif event.type == QUIT:
self.quit()
elif self.swap_time == 0:
self.quit()
self.board.tick(dt)
def input(self, key):
"""
Respond to the player pressing `key`.
"""
if key == K_q:
self.quit()
elif key == K_RIGHT and self.cursor[0] < self.board.w - 2:
self.cursor[0] += 1
elif key == K_LEFT and self.cursor[0] > 0:
self.cursor[0] -= 1
elif key == K_DOWN and self.cursor[1] < self.board.h - 1:
self.cursor[1] += 1
elif key == K_UP and self.cursor[1] > 0:
self.cursor[1] -= 1
elif key == K_SPACE and not self.board.busy():
self.swap()
def swap(self):
"""
Swap the two cells under the cursor and update the player's score.
"""
self.board.swap(self.cursor)
def draw(self):
self.board.draw(self.display)
self.draw_score()
self.draw_time()
if VERTICAL == False:
self.draw_cursor()
elif VERTICAL == True:
self.draw_cursor2()
pygame.display.update()
def draw_time(self):
s = int(self.swap_time)
text = self.font.render(str(int(s/60)) + ":" + str(s%60).zfill(2),
True, BLACK)
self.display.blit(text, (TEXT_OFFSET, WINDOW_HEIGHT - 170))
def draw_score(self):
total_score = self.score
def draw_cursor(self):
topLeft = (MARGIN + self.cursor[0] * SHAPE_WIDTH,
MARGIN + self.cursor[1] * SHAPE_HEIGHT - 68)
topRight = (topLeft[0] + SHAPE_WIDTH * 2, topLeft[1])
bottomLeft = (topLeft[0], topLeft[1] + SHAPE_HEIGHT)
bottomRight = (topRight[0], topRight[1] + SHAPE_HEIGHT)
pygame.draw.lines(self.display, WHITE, True,
[topLeft, topRight, bottomRight, bottomLeft], 3)
if __name__ == '__main__':
Game().play()
If what you are asking for is a way to more easily specify at which rareshapecount intervals you should place a rare shape instead of a normal shape, the following approach is more readable:
def randomize(self):
"""
Replace the entire board with fresh shapes.
"""
# locations we want to place a rare shape
rare_shapes = [9, 23, 27]
for i in range(self.size):
if i in rare_shapes:
self.board[i] = Cell(random.choice(self.rareshapes))
else:
self.board[i] = Cell (random.choice(self.shapes))
Optionally, you could randomly populate rare_shapes if you don't feel like hardcoding the intervals each time, making for a more varied experience (i.e., if you're designing a game or something similar).
What you mean by "I can decide what rareshape spawns instead of it picking a random rareshape" is unclear to me. Would you care to give more explanations ? Like how you would tell the program which rareshape to use ?
In the meantime, here's a somewhat more pythonic version of your code:
def randomize(self):
"""
Replace the entire board with fresh shapes.
"""
specials = dict((x, self.rareshapes) for x in (9, 23, 27))
get_shape_source = lambda x: specials.get(x, self.shapes)
for i in xrange(min(self.size, 41)):
self.board[i] = Cell(random.choice(get_shape_source(i)))
Note that this would break if len(self.board) < min(self.size, 41) but well, that's still basically what your current code do.
edit: given your comment, the obvious way to explicitly choose which rareshape goes where is to explicitly associate images with spots. Now what's the best way to do so / the best place ton configure this really depends on your whole code or at least on more than what you posted. As a very simple and minimal exemple, you could just have this:
from collections import ordereddict
def load_images(self)
self.image_color = {}
self.shapes = []
self.rareshapes = ordereddict()
colors = 'red', 'blue', 'yellow'
letters = 'acgtu'
for c in colors:
im = pygame.image.load('images/{}.png'.format(c))
self.shapes.append(im)
self.image_color[im] = c
for l in letters:
im = pygame.image.load('rareimages/{}{}.png'.format(c, l))
self.rareshapes.[(c, l)] = im
self.image_color[im] = l
def randomize(self):
"""
Replace the entire board with fresh shapes.
"""
raremap = {
# spot index : rareshape
9: ('red', 'a')],
23: ('blue', 'u'),
27: ('yellow', 'g')
}
for i in xrange(self.size):
if i in raremap:
im = self.rareshapes[raremap[i]]
else:
im = random.choice(self.shapes)
self.board[i] = Cell(im)
But it will be just unmaintainable in the long run - too much hardcoded stuff, and too much knowledge leaking from one method to another. I don't know what 'self' is an instance of, but you should considered splitting the responsabilities so you have the invariant parts in one class and the "configration" (images to load, spots / rareshapes mapping etc) in another. Some design patterns that come to mind are TemplateMethod (where you have an abstract base class with the invariant parts and concrete subclasses implementing the "configuration" part), Builder, and of course Strategy (in your case the "Strategy" class would take care of the configuration).

Categories