Related
Working on the google foo-bar challenges and I am stuck on one failing test case (which is a hidden one - so I can't directly see what the problem is)
basically the test is an implementation of a maze solver with a single breakable wall.
I'm doing a modified a* search - with a Boolean flag for paths that contain a broken wall, so that when it reaches the second wall (along a path with a broken wall) it skips it for that path.
I've gone over this code and I can't seem to see the error (if there even is one - at this point I'm almost convinced the test case is somehow wrong)
the parameters:
given a grid of height x,y filled with zero or 1, representing spaces and walls respectively: find the shortest path if you can break exactly 1 wall (if needed)
an example:
[
[0,1,0,0,0,1,0,0,0,0,0]
[0,0,0,1,0,0,1,0,1,1,0]
[1,1,1,1,1,0,1,0,1,0,1]
[0,0,0,0,0,0,0,0,1,1,0]
]
22 is the shortest path breaking 1 wall.
I want a pointer in the right direction: I feel like whatever I'm missing is trivial.
below is the code.
from math import sqrt, ceil
cardinal_moves=[(0,1), (0,-1), (1,0), (-1,0)]
class Node:
def __init__(self, pos ,parent =None):
self.parent = parent
self.pos = pos
self.g = 0
self.h = 0
self.f = 0
self.wall_broken = False
def __eq__(self, other):
return ((self.pos == other.pos) and (self.wall_broken == other.wall_broken))
def update_heuristic(self,parent,end):
self.g = parent.g + 1
self.h = ceil(sqrt((end.pos[0] - self.pos[0])**2 + (end.pos[1] - self.pos[1])**2))
self.f = self.g + self.h
def not_inside_maze(pos,maze):
return pos[0] < 0 or pos[0] >= len(maze) or pos[1] < 0 or pos[1] >= len(maze[0])
def astar_with_1_breakable_wall(maze):
start_pos = (0,0)
end_pos = (len(maze)-1, len(maze[0])-1)
start = Node(start_pos)
end = Node(end_pos)
not_visted = []
visited = []
not_visted.append(start)
while not_visted:
current_node = not_visted[0]
for node in not_visted:
if current_node.f > node.f:
current_node = node
not_visted.remove(current_node)
visited.append(current_node)
if current_node.pos == end.pos:
temp = current_node
path = []
while temp:
path.append(temp.pos)
temp = temp.parent
return path[::-1]
children = []
for position in cardinal_moves:
new_pos = (current_node.pos[0] + position[0], current_node.pos[1] + position[1])
if not_inside_maze(new_pos,maze):
continue
if maze[new_pos[0]][new_pos[1]] == 1:
if current_node.wall_broken:
continue
else:
check = Node(new_pos,current_node)
check.wall_broken = True
already_visited = False
for node in visited:
if check == node:
already_visited = True
break
if not already_visited:
children.append(check)
continue
already_visited = False
check = Node(new_pos,current_node)
check.wall_broken = current_node.wall_broken
for node in visited:
if check == node:
already_visited = True
break
if already_visited:
continue
children.append(check)
for child in children:
child.update_heuristic(current_node,end)
for open_node in not_visted:
if open_node == child:
if open_node.g > child.g:
idx = not_visted.index(open_node)
not_visted[idx] = child
continue
else:
continue
not_visted.append(child)
def shortest_path(maze):
if (astar_with_1_breakable_wall(maze)):
return len(astar_with_1_breakable_wall(maze))
else:
return -1
but every check I make on my machine says this is correct:
#imports added
import sys
#then added this below the maze solver
test = [
[0,1,0,0,0,1,0,0,0,0,0],
[0,0,0,1,0,0,1,0,1,1,0],
[1,1,1,1,1,0,1,0,1,0,1],
[0,0,0,0,0,0,0,0,1,1,0],
]
test2=[
[0,1,0,0,0,0,1,0,0,0,0],
[0,1,0,1,1,0,1,0,1,0,0],
[0,0,0,1,0,0,1,0,1,0,0],
[1,1,1,1,0,1,1,0,1,0,1],
[0,0,0,0,0,0,0,0,1,0,1],
]
print("first test")
print(shortest_path(test))
print("second test")
print(shortest_path(test))
#both of these tests give the correct result
def generate_matrix(h,w,n):
sequence = "{0:b}".format(n).zfill(h*w)
maze =[]
if(sequence[0]=="1" or sequence[len(sequence)-1]=="1"):
return -1
i=0
for y in range(h):
maze.append([])
for x in range(w):
maze[y].append(int(sequence[i]))
i=i+1
return(maze)
for i in range(20):
for j in range(20):
if(i < 6 or j <6):
continue
shortest=j+i
for k in range(2**(i*j)):
if not k%2==0:
continue
if k> 2**(i*j-1):
continue
maze = generate_matrix(i,j,k)
if(not maze == -1):
path = astar_with_1_breakable_wall(maze)
res = shortest_path(maze)
if(res > shortest):
print("\n",res, "shortest was ", shortest)
for index,line in enumerate(maze):
for indx,cell in enumerate(line):
if ((index,indx) in path):
print("\033[94m"+str(cell),end="")
else:
print("\033[92m"+str(cell),end="")
print("\n")
for point in path:
print(point ," ", end="")
print("\n\n")
else:
sys.stdout.write("\r")
the above code makes every matrix possibility, and prints (with highlighting the path) the matrix if the path is longer than the base case.
every result is as I expect - returning the correct shortest path... I have not found the issue with why only the 3rd test case fails...
the answer was something specific to python 2.7.13 apparently - it took a bit of debugging to figure it out, but the not_visited.remove(current_node) was the thing that was failing in certain cases.
My tests of my implementations of Dijkstra and A-Star have revealed that my A-star implementation is approximately 2 times SLOWER. Usually equivalent implementations of Dijkstra and A-star should see A-star beating out Dijkstra. But that isn't the case here and so it has led me to question my implementation of A-star. So I want someone to tell me what I am doing wrong in my implementation of A-star.
Here is my code:
from copy import deepcopy
from math import inf, sqrt
import maze_builderV2 as mb
if __name__ == '__main__':
order = 10
space = ['X']+['_' for x in range(order)]+['X']
maze = [deepcopy(space) for x in range(order)]
maze.append(['X' for x in range(order+2)])
maze.insert(0, ['X' for x in range(order+2)])
finalpos = (order, order)
pos = (1, 1)
maze[pos[0]][pos[1]] = 'S' # Initializing a start position
maze[finalpos[0]][finalpos[1]] = 'O' # Initializing a end position
mb.mazebuilder(maze=maze)
def spit():
for x in maze:
print(x)
spit()
print()
mazemap = {}
def scan(): # Converts raw map/maze into a suitable datastructure.
for x in range(1, order+1):
for y in range(1, order+1):
mazemap[(x, y)] = {}
t = [(x-1, y), (x+1, y), (x, y-1), (x, y+1)]
for z in t:
if maze[z[0]][z[1]] == 'X':
pass
else:
mazemap[(x, y)][z] = [sqrt((pos[0]-z[0])**2+(pos[1]-z[1])**2),
sqrt((finalpos[0]-z[0])**2+(finalpos[1]-z[1])**2)] # Euclidean distance to destination (Heuristic)
scan()
unvisited = deepcopy(mazemap)
distances = {}
paths = {}
# Initialization of distances:
for node in unvisited:
if node == pos:
distances[node] = [0, sqrt((finalpos[0]-node[0])**2+(finalpos[1]-node[1])**2)]
else:
distances[node] = [inf, inf]
while unvisited != {}:
curnode = None
for node in unvisited:
if curnode == None:
curnode = node
elif (distances[node][0]+distances[node][1]) < (distances[curnode][0]+distances[curnode][1]):
curnode = node
else:
pass
for childnode, lengths in mazemap[curnode].items():
# Length to nearby childnode - G length, Euclidean (Heuristic) length from curnode to finalpos - H length
# G length + H length < Euclidean length to reach that childnode directly + Euclidean length to finalpos from that childnode = Better path found, update known distance and paths
if lengths[0] + lengths[1] < distances[childnode][0] + distances[childnode][1]:
distances[childnode] = [lengths[0], lengths[1]]
paths[childnode] = curnode
unvisited.pop(curnode)
def shortestroute(paths, start, end):
shortestpath = []
try:
def rec(start, end):
if end == start:
shortestpath.append(end)
return shortestpath[::-1]
else:
shortestpath.append(end)
return rec(start, paths[end])
return rec(start, end)
except KeyError:
return False
finalpath = shortestroute(paths, pos, finalpos)
if finalpath:
for x in finalpath:
if x == pos or x == finalpos:
pass
else:
maze[x[0]][x[1]] = 'W'
else:
print("This maze not solvable, Blyat!")
print()
spit()
For those who find my code too messy and can't bother to read the comments I added to help with the reading... Here is a gist of my code:
Creates a mazemap (all the coordinates and its connected neighbors along with their euclidean distances from that neighboring point to the start position (G Cost) as well as to the final position (H Cost)... in a dictionary)
start position is selected as the current node. All distances to other nodes is initialised as infinity.
For every node we compare the total path cost i.e is the G cost + H cost. The one with least total cost is selected as then next current node. Each time we select new current node, we add that node to a dictionary that keeps track of through which node it was reached, so that it is easier to backtrack and find our path.
Process continues until current node is the final position.
If anyone can help me out on this, that would be great!
EDIT: On account of people asking for the maze building algorithm, here it is:
# Maze generator - v2: Generates mazes that look like city streets (more or less...)
from copy import deepcopy
from random import randint, choice
if __name__ == "__main__":
order = 10
space = ['X']+['_' for x in range(order)]+['X']
maze = [deepcopy(space) for x in range(order)]
maze.append(['X' for x in range(order+2)])
maze.insert(0, ['X' for x in range(order+2)])
pos = (1, 1)
finalpos = (order, order)
maze[pos[0]][pos[1]] = 'S' # Initializing a start position
maze[finalpos[1]][finalpos[1]] = 'O' # Initializing a end position
def spit():
for x in maze:
print(x)
blocks = []
freespaces = [(x, y) for x in range(1, order+1) for y in range(1, order+1)]
def blockbuilder(kind):
param1 = param2 = 0
double = randint(0, 1)
if kind == 0:
param2 = randint(3, 5)
if double:
param1 = 2
else:
param1 = 1
else:
param1 = randint(3, 5)
if double:
param2 = 2
else:
param2 = 1
for a in range(blockstarter[0], blockstarter[0]+param2):
for b in range(blockstarter[1], blockstarter[1]+param1):
if (a+1, b) in blocks or (a-1, b) in blocks or (a, b+1) in blocks or (a, b-1) in blocks or (a, b) in blocks or (a+1, b+1) in blocks or (a-1, b+1) in blocks or (a+1, b-1) in blocks or (a-1, b-1) in blocks:
pass
else:
if a > order+1 or b > order+1:
pass
else:
if maze[a][b] == 'X':
blocks.append((a, b))
else:
spaces = [(a+1, b), (a-1, b), (a, b+1), (a, b-1)]
for c in spaces:
if maze[c[0]][c[1]] == 'X':
break
else:
maze[a][b] = 'X'
blocks.append((a, b))
for x in range(1, order+1):
for y in range(1, order+1):
if (x, y) in freespaces:
t = [(x+1, y), (x-1, y), (x, y+1), (x, y-1)]
i = 0
while i < len(t):
if maze[t[i][0]][t[i][1]] == 'X' or (t[i][0], t[i][1]) == pos or (t[i][0], t[i][1]) == finalpos:
del t[i]
else:
i += 1
if len(t) > 2:
blockstarter = t[randint(0, len(t)-1)]
kind = randint(0, 1) # 0 - vertical, 1 - horizontal
blockbuilder(kind)
else:
pass
# rch = choice(['d', 'u', 'r', 'l'])
b = 0
while b < len(blocks):
block = blocks[b]
t = {'d': (block[0]+2, block[1]), 'u': (block[0]-2, block[1]),
'r': (block[0], block[1]+2), 'l': (block[0], block[1]-2)}
rch = choice(['d', 'u', 'r', 'l'])
z = t[rch]
# if z[0] > order+1 or z[1] > order+1 or z[0] < 1 or z[1] < 1:
# Decreased chance of having non solvable maze being generated...
if z[0] > order-2 or z[1] > order-2 or z[0] < 2+2 or z[1] < 2+2:
pass
else:
if maze[z[0]][z[1]] == 'X':
if randint(0, 1):
set = None
if rch == 'u':
set = (z[0]+1, z[1])
elif rch == 'd':
set = (z[0]-1, z[1])
elif rch == 'r':
set = (z[0], z[1]-1)
elif rch == 'l':
set = (z[0], z[1]+1)
else:
pass
if maze[set[0]][set[1]] == '_':
# Checks so that no walls that block the entire way are formed
# Makes sure maze is solvable
sets, count = [
(set[0]+1, set[1]), (set[0]-1, set[1]), (set[0], set[1]+1), (set[0], set[1]-1)], 0
for blyat in sets:
while blyat[0] != 0 and blyat[1] != 0 and blyat[0] != order+1 and blyat[1] != order+1:
ch = [(blyat[0]+1, blyat[1]), (blyat[0]-1, blyat[1]),
(blyat[0], blyat[1]+1), (blyat[0], blyat[1]-1)]
suka = []
for i in ch:
if ch not in suka:
if maze[i[0]][i[1]] == 'X':
blyat = i
break
else:
pass
suka.append(ch)
else:
pass
else:
blyat = None
if blyat == None:
break
else:
pass
else:
count += 1
if count < 1:
maze[set[0]][set[1]] = 'X'
blocks.append(set)
else:
pass
else:
pass
else:
pass
b += 1
mazebuilder(maze, order)
spit()
Sorry for leaving this out!
Just at a quick glance, it looks like you don't have a closed set at all?? Your unvisited structure appears to contain every node in the map. This algorithm is not A* at all.
Once you fix that, make sure to change unvisited from a list to a priority queue also.
I am trying to create the A Star algorithm and apply to the game Snake. The problem I'm having is that I never get anything drawn on the window because it never reaches that code. It gets stuck when trying to calculate the path in the method run(). Specifically, open set never goes to zero and it adds already existing nodes.
I've tried printing the various lists to the console, but all i've discovered is that "current" repeats tiles at random points, and "openSet and "closedSet" continue to get larger with these repeated tiles. I've also tested my Tile object, my contains method, and my giveNeighbors method and all of these seem to be working and give the expected results. Also as a reference, I am going of the pseudo code found here: https://en.wikipedia.org/wiki/A*_search_algorithm
import Tile
import pygame
class A_star:
def __init__(self):
self.closedSet = []
self.openSet = []
def run(self, start, goal):
self.closedSet = []
self.openSet = [start]
path = []
#THE LENGTH OF OPEN SET NEVER GOES TO ZERO!
while len(self.openSet) > 0:
#Set current to node in openSet with lowest fscore
current = self.openSet[0]
currindex = 0
for i, node in enumerate(self.openSet):
if node.fScore < current.fScore:
current = node
currindex = i
#romove from Open set and add to closed
self.openSet.pop(currindex)
self.closedSet.append(current)
#Reached the end
if current.tileEquals(goal):
print("Done")
while current != None:
path.append(current)
current = current.cameFrom
return path[::-1]
neighbors = self.giveNeighbors(current)
print("Current: " + str(current.posx) + ", " + str(current.posy))
print("Neighbors")
for i in neighbors:
print(str(i.posx) + ", " + str(i.posy))
for i in neighbors:
#if neighbor is already checked, then ignore it.
if not self.contains(self.closedSet, i):
#Distance between start adn neighbor. tenative gscore
tempGScore = current.gScore + 1
#if neighbor is not in openset. Discovered a new node!
if not self.contains(self.openSet, i):
self.openSet.append(i)
elif tempGScore < i.gScore:
i.gScore = tempGScore
i.cameFrom = current
i.fScore = i.gScore + self.heuristicCost(i, goal) #f = g + h
print("Open:")
for i in self.openSet:
print(str(i.posx) + ", " + str(i.posy))
print("Closed")
for i in self.closedSet:
print(str(i.posx) + ", " + str(i.posy))
#Calculates the estimated distance from a given tile to end
def heuristicCost(self, neighbor, goal):
#The snake never goes diagonal, therefore calculate manhatten distance
distance = abs(neighbor.posx - goal.posx) + abs(neighbor.posy - goal.posy)
return distance
def giveNeighbors(self, current):
neighbors = []
if current.posx > 0:
neighbors.append(Tile(current.posx - 10, current.posy))
if current.posx < 200:
neighbors.append(Tile(current.posx + 10, current.posy))
if current.posy > 0:
neighbors.append(Tile(current.posx, current.posy - 10))
if current.posy < 200:
neighbors.append(Tile(current.posx, current.posy + 10))
return neighbors
def contains(self, s, tile):
for i in s:
if i.tileEquals(tile):
return True
else:
return False
def testAStar():
pygame.init()
window = pygame.display.set_mode((200, 200))
pygame.display.set_caption("AStar Test")
window.fill((255,255,255))
clock = pygame.time.Clock()
start = Tile(0, 0)
end = Tile(190, 190)
astar = A_star()
path = astar.run(start, end)
run = True
while run:
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
pygame.draw.rect(window, (0, 255, 0), (start.posx, start.posy, 10, 10))
pygame.draw.rect(window, (255, 0, 0), (end.posx, end.posy, 10, 10))
for i in path:
pygame.draw.rect(window, (0, 0, 255), (i.posx, i.posy, 10, 10))
print("drew stuff")
pygame.display.update()
window.fill((255,255,255))
clock.tick(10)
pygame.quit()
if __name__== "__main__":
testAStar()
class Tile:
def __init__(self, x, y):
self.posx = x
self.posy = y
self.fScore = 0
self.gScore = 0
self.cameFrom = None
def nextTileRight(self):
self.posx = self.posx + 10
def nextTileDown(self):
self.posy = self.posy + 10
def nextTileUp(self):
self.posy = self.posy - 10
def nextTileLeft(self):
self.posx = self.posx - 10
def tileEquals(self, t):
if self.posx == t.posx and self.posy == t.posy:
return True
else:
return False
The expected result should be a non-diagonal path drawn on the window from the start node to the end node. (Also, sorry if my indentation is off. I am still very new to this website)
I'm not sure why you are checking openSets == 0 , you are looking for currentNode = destination or all the neighbours are closed meaning everything has been checked and no path was found.
I have written a selection of functions to try and solve a N-puzzle / 8-puzzle.
I am quite content with my ability to manipulate the puzzle but am struggling with how to iterate and find the best path. My skills are not in OOP either and so the functions are simple.
The idea is obviously to reduce the heruistic distance and place all pieces in their desired locations.
I have read up a lot of other questions regarding this topic but they're often more advanced and OOP focused.
When I try and iterate through there are no good moves. I'm not sure how to perform the A* algorithm.
from math import sqrt, fabs
import copy as cp
# Trial puzzle
puzzle1 = [
[3,5,4],
[2,1,0],
[6,7,8]]
# This function is used minimise typing later
def starpiece(piece):
'''Checks the input of a *arg and returns either tuple'''
if piece == ():
return 0
elif isinstance(piece[0], (str, int)) == True:
return piece[0]
elif isinstance(piece[0], (tuple, list)) and len(piece[0]) == 2:
return piece[0]
# This function creates the goal puzzle layout
def goal(puzzle):
'''Input a nested list and output an goal list'''
n = len(puzzle) * len(puzzle)
goal = [x for x in range(1,n)]
goal.append(0)
nested_goal = [goal[i:i+len(puzzle)] for i in range(0, len(goal), len(puzzle))]
return nested_goal
# This fuction gives either the coordinates (as a tuple) of a piece in the puzzle
# or the piece in the puzzle at give coordinates
def search(puzzle, *piece):
'''Input a puzzle and piece value and output a tuple of coordinates.
If no piece is selected 0 is chosen by default. If coordinates are
entered the piece value at those coordinates are outputed'''
piece = starpiece(piece)
if isinstance(piece, (tuple, list)) == True:
return puzzle[piece[0]][piece[1]]
for slice1, sublist in enumerate(puzzle):
for slice2, item in enumerate(sublist):
if puzzle[slice1][slice2] == piece:
x, y = slice1, slice2
return (x, y)
# This function gives the neighbours of a piece at a given position as a list of coordinates
def neighbours(puzzle, *piece):
'''Input a position (as a tuple) or piece and output a list
of adjacent neighbours. Default are the neighbours to 0'''
length = len(puzzle) - 1
return_list = []
piece = starpiece(piece)
if isinstance(piece, tuple) != True:
piece = search(puzzle, piece)
if (piece[0] - 1) >= 0:
x_minus = (piece[0] - 1)
return_list.append((x_minus, piece[1]))
if (piece[0] + 1) <= length:
x_plus = (piece[0] + 1)
return_list.append((x_plus, piece[1]))
if (piece[1] - 1) >= 0:
y_minus = (piece[1] - 1)
return_list.append((piece[0], y_minus))
if (piece[1] + 1) <= length:
y_plus = (piece[1] + 1)
return_list.append((piece[0], y_plus))
return return_list
# This function swaps piece values of adjacent cells
def swap(puzzle, cell1, *cell2):
'''Moves two cells, if adjacent a swap occurs. Default value for cell2 is 0.
Input either a cell value or cell cooridinates'''
cell2 = starpiece(cell2)
if isinstance(cell1, (str, int)) == True:
cell1 = search(puzzle, cell1)
if isinstance(cell2, (str, int)) == True:
cell2 = search(puzzle, cell2)
puzzleSwap = cp.deepcopy(puzzle)
if cell1 == cell2:
print('Warning: no swap occured as both cell values were {}'.format(search(puzzle,cell1)))
return puzzleSwap
elif cell1 in neighbours(puzzleSwap, cell2):
puzzleSwap[cell1[0]][cell1[1]], puzzleSwap[cell2[0]][cell2[1]] = puzzleSwap[cell2[0]][cell2[1]], puzzleSwap[cell1[0]][cell1[1]]
return puzzleSwap
else:
print('''Warning: no swap occured as cells aren't adjacent''')
return puzzleSwap
# This function gives true if a piece is in it's correct position
def inplace(puzzle, p):
'''Ouputs bool on whether a piece is in it's correct position'''
if search(puzzle, p) == search(goal(puzzle), p):
return True
else:
return False
# These functions give heruistic measurements
def heruistic(puzzle):
'''All returns heruistic (misplaced, total distance) as a tuple. Other
choices are: heruistic misplaced, heruistic distance or heruistic list'''
heruistic_misplaced = 0
heruistic_distance = 0
heruistic_distance_total = 0
heruistic_list = []
for sublist in puzzle:
for item in sublist:
if inplace(puzzle, item) == False:
heruistic_misplaced += 1
for sublist in puzzle:
for item in sublist:
a = search(puzzle, item)
b = search(goal(puzzle), item)
heruistic_distance = int(fabs(a[0] - b[0]) + fabs(a[1] - b[1]))
heruistic_distance_total += heruistic_distance
heruistic_list.append(heruistic_distance)
return (heruistic_misplaced, heruistic_distance_total, heruistic_list)
def hm(puzzle):
'''Outputs heruistic misplaced'''
return heruistic(puzzle)[0]
def hd(puzzle):
'''Outputs total heruistic distance'''
return heruistic(puzzle)[1]
def hl(puzzle):
'''Outputs heruistic list'''
return heruistic(puzzle)[2]
def hp(puzzle, p):
'''Outputs heruistic distance at a given location'''
x, y = search(puzzle, p)[0], search(puzzle, p)[1]
return heruistic(puzzle)[2][(x * len(puzzle)) + y]
# This is supposted to iterate along a route according to heruistics but doesn't work
def iterMove(puzzle):
state = cp.deepcopy(puzzle)
while state != goal(puzzle):
state_hd = hd(state)
state_hm = hm(state)
moves = neighbours(state)
ok_moves = []
good_moves = []
for move in moves:
maybe_state = swap(state, move)
if hd(maybe_state) < state_hd and hm(maybe_state) < state_hm:
good_moves.append(move)
elif hd(maybe_state) < state_hd:
ok_moves.append(move)
elif hm(maybe_state) < state_hm:
ok_moves.append(move)
if good_moves != []:
print(state)
state = swap(state, good_moves[0])
elif ok_moves != []:
print(state)
state = swap(state, ok_moves[0])
>> iterMove(puzzle1)
'no good moves'
To implement A* in Python you can use https://docs.python.org/3/library/heapq.html for a priority queue. You put possible positions into the queue with a priority of "cost so far + heuristic for remaining cost". When you take them out of the queue you check a set of already seen positions. Skip this one if you've seen the position, else add it to the set and then process.
An untested version of the critical piece of code:
queue = [(heuristic(starting_position), 0, starting_position, None)]
while 0 < len(queue):
(est_moves, cur_moves, position, history) = heapq.heappop(queue)
if position in seen:
continue
elif position = solved:
return history
else:
seen.add(position)
for move in possible_moves(position):
next_position = position_after_move(position, move)
est_moves = cur_moves + 1 + heuristic(next_position)
heapq.heappush(queue,
(est_moves, cur_moves+1,
next_position, (move, history)))
return None
I am currently working on my Python game, in ika, which uses python 2.5
I decided to use A* pathfinding for the AI. However, I find it too slow for my needs (3-4 enemies can lag the game, but I would like to supply up to 4-5 without problems). I know, that such complex search like A* is not mean to be scripted in python, but I am pretty sure, that my pathfinder is also implemented in the wrong way.
My question is: How can I speed up this algorithm?
I wrote my own binary heap, and there are some try: except: lines inside some functions. Those lines can create large overhead? Are there better methods maintaining the open list?
I supplied the algorithm with graphics interface, for testing purposes (when the pathfinder finishes searching, it will write the number of iterations and seconds it takes to find the path, inside the ika.txt file. Also, Pressing A will do a complete search, and S does that step by step.)
Graphical version:
http://data.hu/get/6084681/A_star.rar
Also, here is a pastebin version:
http://pastebin.com/9N8ybX5F
Here is the main code I use for pathfinding:
import ika
import time
class Node:
def __init__(self,x,y,parent=None,g=0,h=0):
self.x = x
self.y = y
self.parent = parent
self.g = g
self.h = h
def cost(self):
return self.g + self.h
def equal(self,node):
if self.x == node.x and self.y == node.y:
return True
else:
return False
class Emerald_Pathfinder:
def __init__(self):
pass
def setup(self,start,goal):
self.start = start
self.goal = goal
self.openlist = [None,start] # Implemented as binary heap
self.closedlist = {} # Implemented as hash
self.onopenlist = {} # Hash, for searching the openlist
self.found = False
self.current = None
self.iterations = 0
def lowest_cost(self):
pass
def add_nodes(self,current):
nodes = []
x = current.x
y = current.y
self.add_node(x+1,y,current,10,nodes)
self.add_node(x-1,y,current,10,nodes)
self.add_node(x,y+1,current,10,nodes)
self.add_node(x,y-1,current,10,nodes)
# Dont cut across corners
up = map.is_obstacle((x,y-1),x,y-1)
down = map.is_obstacle((x,y+1),x,y+1)
left = map.is_obstacle((x-1,y),x-1,y)
right = map.is_obstacle((x+1,y),x+1,y)
if right == False and down == False:
self.add_node(x+1,y+1,current,14,nodes)
if left == False and up == False:
self.add_node(x-1,y-1,current,14,nodes)
if right == False and up == False:
self.add_node(x+1,y-1,current,14,nodes)
if left == False and down == False:
self.add_node(x-1,y+1,current,14,nodes)
return nodes
def heuristic(self,x1,y1,x2,y2):
return (abs(x1-x2)+abs(y1-y2))*10
def add_node(self,x,y,parent,cost,list):
# If not obstructed
if map.is_obstacle((x,y),x,y) == False:
g = parent.g + cost
h = self.heuristic(x,y,self.goal.x,self.goal.y)
node = Node(x,y,parent,g,h)
list.append(node)
def ignore(self,node,current):
# If its on the closed list, or open list, ignore
try:
if self.closedlist[(node.x,node.y)] == True:
return True
except:
pass
# If the node is on the openlist, do the following
try:
# If its on the open list
if self.onopenlist[(node.x,node.y)] != None:
# Get the id number of the item on the real open list
index = self.openlist.index(self.onopenlist[(node.x,node.y)])
# If one of the coordinates equal, its not diagonal.
if node.x == current.x or node.y == current.y:
cost = 10
else:
cost = 14
# Check, is this items G cost is higher, than the current G + cost
if self.openlist[index].g > (current.g + cost):
# If so, then, make the list items parent, the current node.
self.openlist[index].g = current.g + cost
self.openlist[index].parent = current
# Now resort the binary heap, in the right order.
self.resort_binary_heap(index)
# And ignore the node
return True
except:
pass
return False
def resort_binary_heap(self,index):
m = index
while m > 1:
if self.openlist[m/2].cost() > self.openlist[m].cost():
temp = self.openlist[m/2]
self.openlist[m/2] = self.openlist[m]
self.openlist[m] = temp
m = m / 2
else:
break
def heap_add(self,node):
self.openlist.append(node)
# Add item to the onopenlist.
self.onopenlist[(node.x,node.y)] = node
m = len(self.openlist)-1
while m > 1:
if self.openlist[m/2].cost() > self.openlist[m].cost():
temp = self.openlist[m/2]
self.openlist[m/2] = self.openlist[m]
self.openlist[m] = temp
m = m / 2
else:
break
def heap_remove(self):
if len(self.openlist) == 1:
return
first = self.openlist[1]
# Remove the first item from the onopenlist
self.onopenlist[(self.openlist[1].x,self.openlist[1].y)] = None
last = self.openlist.pop(len(self.openlist)-1)
if len(self.openlist) == 1:
return last
else:
self.openlist[1] = last
v = 1
while True:
u = v
# If there is two children
if (2*u)+1 < len(self.openlist):
if self.openlist[2*u].cost() <= self.openlist[u].cost():
v = 2*u
if self.openlist[(2*u)+1].cost() <= self.openlist[v].cost():
v = (2*u)+1
# If there is only one children
elif 2*u < len(self.openlist):
if self.openlist[2*u].cost() <= self.openlist[u].cost():
v = 2*u
# If at least one child is smaller, than parent, swap them
if u != v:
temp = self.openlist[u]
self.openlist[u] = self.openlist[v]
self.openlist[v] = temp
else:
break
return first
def iterate(self):
# If the open list is empty, exit the game
if len(self.openlist) == 1:
ika.Exit("no path found")
# Expand iteration by one
self.iterations += 1
# Make the current node the lowest cost
self.current = self.heap_remove()
# Add it to the closed list
self.closedlist[(self.current.x,self.current.y)] = True
# Are we there yet?
if self.current.equal(self.goal) == True:
# Target reached
self.goal = self.current
self.found = True
print self.iterations
else:
# Add the adjacent nodes, and check them
nodes_around = self.add_nodes(self.current)
for na in nodes_around:
if self.ignore(na,self.current) == False:
self.heap_add(na)
def iterateloop(self):
time1 = time.clock()
while 1:
# If the open list is empty, exit the game
if len(self.openlist) == 1:
ika.Exit("no path found")
# Expand iteration by one
self.iterations += 1
# Make the current node the lowest cost
self.current = self.heap_remove()
# Add it to the closed list
self.closedlist[(self.current.x,self.current.y)] = True
# Are we there yet?
if self.current.equal(self.goal) == True:
# Target reached
self.goal = self.current
self.found = True
print "Number of iterations"
print self.iterations
break
else:
# Add the adjacent nodes, and check them
nodes_around = self.add_nodes(self.current)
for na in nodes_around:
if self.ignore(na,self.current) == False:
self.heap_add(na)
time2 = time.clock()
time3 = time2-time1
print "Seconds to find path:"
print time3
class Map:
def __init__(self):
self.map_size_x = 20
self.map_size_y = 15
self.obstructed = {} # Library, containing x,y couples
self.start = [2*40,3*40]
self.unit = [16*40,8*40]
def is_obstacle(self,couple,x,y):
if (x >= self.map_size_x or x < 0) or (y >= self.map_size_y or y < 0):
return True
try:
if self.obstructed[(couple)] != None:
return True
except:
return False
def render_screen():
# Draw the Character
ika.Video.DrawRect(map.start[0],map.start[1],map.start[0]+40,map.start[1]+40,ika.RGB(40,200,10),1)
# Draw walls
for x in range(0,map.map_size_x):
for y in range(0,map.map_size_y):
if map.is_obstacle((x,y),x,y) == True:
ika.Video.DrawRect(x*40,y*40,(x*40)+40,(y*40)+40,ika.RGB(168,44,0),1)
# Draw openlist items
for node in path.openlist:
if node == None:
continue
x = node.x
y = node.y
ika.Video.DrawRect(x*40,y*40,(x*40)+40,(y*40)+40,ika.RGB(100,100,100,50),1)
# Draw closedlist items
for x in range(0,map.map_size_x):
for y in range(0,map.map_size_y):
try:
if path.closedlist[(x,y)] == True:
ika.Video.DrawRect(x*40,y*40,(x*40)+20,(y*40)+20,ika.RGB(0,0,255))
except:
pass
# Draw the current square
try:
ika.Video.DrawRect(path.current.x*40,path.current.y*40,(path.current.x*40)+40,(path.current.y*40)+40,ika.RGB(128,128,128), 1)
except:
pass
ika.Video.DrawRect(mouse_x.Position(),mouse_y.Position(),mouse_x.Position()+8,mouse_y.Position()+8,ika.RGB(128,128,128), 1)
# Draw the path, if reached
if path.found == True:
node = path.goal
while node.parent:
ika.Video.DrawRect(node.x*40,node.y*40,(node.x*40)+40,(node.y*40)+40,ika.RGB(40,200,200),1)
node = node.parent
# Draw the Target
ika.Video.DrawRect(map.unit[0],map.unit[1],map.unit[0]+40,map.unit[1]+40,ika.RGB(128,40,200),1)
def mainloop():
while 1:
render_screen()
if mouse_middle.Pressed():
# Iterate pathfinder
if path.found == False:
path.iterateloop()
elif mouse_right.Pressed():
# Iterate pathfinder by one
if path.found == False:
path.iterate()
elif ika.Input.keyboard["A"].Pressed():
# Iterate pathfinder
if path.found == False:
path.iterateloop()
elif ika.Input.keyboard["S"].Pressed():
# Iterate pathfinder by one
if path.found == False:
path.iterate()
elif mouse_left.Position():
# Add a square to the map, to be obstructed
if path.iterations == 0:
x = mouse_x.Position()
y = mouse_y.Position()
map.obstructed[(int(x/40),int(y/40))] = True
# Mouse preview
x = mouse_x.Position()
y = mouse_y.Position()
mx = int(x/40)*40
my = int(y/40)*40
ika.Video.DrawRect(mx,my,mx+40,my+40,ika.RGB(150,150,150,70),1)
ika.Video.ShowPage()
ika.Input.Update()
map = Map()
path = Emerald_Pathfinder()
path.setup(Node(map.start[0]/40,map.start[1]/40),Node(map.unit[0]/40,map.unit[1]/40))
mouse_middle = ika.Input.mouse.middle
mouse_right = ika.Input.mouse.right
mouse_left = ika.Input.mouse.left
mouse_x = ika.Input.mouse.x
mouse_y = ika.Input.mouse.y
# Initialize loop
mainloop()
I appreciate any help!
(sorry for any spelling mistakes, English is not my native language)
I think a proper implementation in python will be fast enough for your purposes. But the boost library has an astar implementation and python bindings. https://github.com/erwinvaneijk/bgl-python