Pygame Maze Game not creating levels correctly - python

So I'm trying to create a maze game with levels for a project at school. The code is a bit repetitive sorry I've only just started coding using pygame. When run the program should output a maze that once the user completes moves onto the next level - each level is randomly generated. However, only the first level is showing properly the rest of the levels appear to be a grid- which is making me think that the game is creating a new maze over the old.
I've pasted the code below - feel free to leave any advice on how to improve what I have :)
class Maze:
def __init__(self, rows=30, cols=40):
self.rows = rows
self.cols = cols
self.keep_going = 1
self.maze = {}
for y in range(rows):
for x in range(cols):
cell = {'south' : 1, 'east' : 1, 'visited': 0}
self.maze[(x,y)] = cell
def generate(self, start_cell=None, stack=[])
if start_cell is None:
start_cell = self.maze[(self.cols-1, self.rows-1)]
if not self.keep_going:
return
self.check_finished()
neighbors = []
# if the stack is empty, add the start cell
if len(stack) == 0:
stack.append(start_cell)
# set current cell to last cell
curr_cell = stack[-1]
# get neighbors and shuffle 'em up a bit
neighbors = self.get_neighbors(curr_cell)
shuffle(neighbors)
for neighbor in neighbors:
if neighbor['visited'] == 0:
neighbor['visited'] = 1
stack.append(neighbor)
self.knock_wall(curr_cell, neighbor)
self.generate(start_cell, stack)
def get_coords(self, cell):
# grabs coords of a given cell
coords = (-1, -1)
for k in self.maze:
if self.maze[k] is cell:
coords = (k[0], k[1])
break
return coords
def get_neighbors(self, cell):
# obvious
neighbors = []
(x, y) = self.get_coords(cell)
if (x, y) == (-1, -1):
return neighbors
north = (x, y-1)
south = (x, y+1)
east = (x+1, y)
west = (x-1, y)
if north in self.maze:
neighbors.append(self.maze[north])
if south in self.maze:
neighbors.append(self.maze[south])
if east in self.maze:
neighbors.append(self.maze[east])
if west in self.maze:
neighbors.append(self.maze[west])
return neighbors
def knock_wall(self, cell, neighbor):
# knocks down wall between cell and neighbor.
xc, yc = self.get_coords(cell)
xn, yn = self.get_coords(neighbor)
# Which neighbor?
if xc == xn and yc == yn + 1:
# neighbor's above, knock out south wall of neighbor
neighbor['south'] = 0
elif xc == xn and yc == yn - 1:
# neighbor's below, knock out south wall of cell
cell['south'] = 0
elif xc == xn + 1 and yc == yn:
# neighbor's left, knock out east wall of neighbor
neighbor['east'] = 0
elif xc == xn - 1 and yc == yn:
# neighbor's right, knock down east wall of cell
cell['east'] = 0
def check_finished(self):
# Checks if we're done generating
done = 1
for k in self.maze:
if self.maze[k]['visited'] == 0:
done = 0
break
if done:
self.keep_going = 0

[...] the rest of the levels appear to be a grid- which is making me think that the game is creating a new maze over the old.
The issue is caused by a common mistake in Python.
See Default Argument Values
Important warning: The default value is evaluated only once. This makes a difference when the default is a mutable object such as a list, dictionary, or instances of most classes
In your case the arguments to the method generate of class Maze has default arguments:
class Maze:
# [...]
def generate(self, start_cell=None, stack=[]):
# [...]
In the method generate elements are append to stack. The maze is generated trusting in the default argument:
self.maze_obj.generate(self.maze_obj.maze[(0,0)])
That causes that the 1st generation of the mace succeeds, but the following generation fails, because stack contains all the elements of the former generation process.
Pass an empty list to generate to solve the issue:
self.maze_obj.generate(self.maze_obj.maze[(0,0)])
self.maze_obj.generate(self.maze_obj.maze[(0,0)], [])
Or change the default argument to None:
class Maze:
# [...]
def generate(self, start_cell=None, stack=None):
if stack == None:
stack = []

Related

Find the path think as coordinate plane

def path(given_map, x, y):
x = given_map[0][0]
y = given_map[0][0]
cnt = 0
if x == len(given_map) and y == len(given_map):
cnt += 1
return cnt
else:
if x < len(given_map) and y < len(given_map):
return path(given_map, x, y)
elif x < len(given_map) and y == len(given_map):
return path(given_map, x, y)
elif x == len(given_map) and y < len(given_map):
return path(given_map, x, y)
else:
cnt = 0
return cnt
if __name__ == '__main__':
input_map = [[1,2,9,4,9],
[1,5,8,7,9],
[9,3,9,9,2],
[2,3,7,5,9],
[1,9,9,1,0]]
print(path(input_map, 0, 0))
input_map = [[1,1,2],
[1,2,2],
[1,2,0]]
print(path(input_map, 0, 0))
The n*n list must be input to create a function that returns the number of paths that can reach from the starting point [0][0] to the ending point [n-1][n-1].
Movement can only be done downward and right, and can only be moved in one direction by the number shown in the corresponding x and y coordinates.
It does not include cases outside the map after movement.
The above code is implemented as much as possible within my ability. How can I modify it to function normally?
Unless you are required to implement this using recursion, I would suggest a BFS (Breadth First Search) approach using a dictionary to track the positions/count that you have reached at each step of the progression:
def path(M):
result = 0 # final result
bounds = range(len(M)) # inside map
paths = {(0,0):1} # one path at start
while paths: # spread counts
nextPos = dict() # track next positions
for (x,y),count in paths.items(): # extend positon/count
if x == y == len(M)-1: result += count # capture final count
dist = M[x][y] # travel distance
if not dist: continue # no movement ...
for x2,y2 in [(x,y+dist),(x+dist,y)]: # travel down & right
if x2 in bounds and y2 in bounds: # propagate count
nextPos[x2,y2] = nextPos.get((x2,y2),0)+count
paths = nextPos # next step on paths
return result
input_map = [[1,2,9,4,9],
[1,5,8,7,9],
[9,3,9,9,2],
[2,3,7,5,9],
[1,9,9,1,0]]
print(path(input_map)) # 2
input_map = [[1,1,2],
[1,2,2],
[1,2,0]]
print(path(input_map)) # 1
If you are required to implement a recursive approach, the function can be adapted like this (returning the sum of recursive path counts) but then it won't be a BFS anymore:
def path(M,x=0,y=0):
if x == y == len(M)-1: return 1 # goal reached
dist = M[x][y] # travel distance
if not dist: return 0 # no movement
bounds = range(len(M)) # inside map
return sum(path(M,x2,y2) for x2,y2 in [(x,y+dist),(x+dist,y)]
if x2 in bounds and y2 in bounds) # sum of paths

Add sprite to the group that it doesn't collide with any sprites in the group [duplicate]

A video by Sebastion Lague explained the Bridson's algorithm really well.
To oversimplify,
Create cell grid that has sides of radius/sqrt(2).
Place initial point and list as spawnpoint.
Place point into cell in grid.
For any spawnpoint, spawn a point between radius and 2*radius.
Look at the cells 2 units away from cell of new point.
If contains other points, compare distance.
If any point is closer to new point than the radius, new point is invalid.
If new point is valid, new point is listed as spawnpoint and placed into cell in grid.
If spawnpoint spawns too many invalid points, spawnpoint is removed and turns into point.
Repeat until no more spawnpoints exists.
Return points.
I basically written the same thing down in Python 3.7.2 and pygame 1.7~, but as said in the title, I'm stuck in recursive purgatory.
I used one Point() class for this algorithm, which might seem redundant given that pygame.Vector2() exists, but I needed some elements for a separate algorithm (Delaunay's with infinite vertices) that required this class to work.
For the sake of simplicity I'm going to cut away all the Delaunay-specific elements and show the bare-bones of this class that is needed for this algorithm:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def DistanceToSquared(self,other):
return (self.x-other.x)**2 + (self.y-other.y)**2
The code that is related to the Bridson's algorithm is:
def PoissonDiskSampling(width, height, radius, startPos = None, spawnAttempts = 10):
if startPos == None:
startPos = [width//2,height//2]
cellSize = radius / math.sqrt(2)
cellNumberX = int(width // cellSize + 1) # Initialise a cells grid for optimisation
cellNumberY = int(height // cellSize + 1)
cellGrid = [[None for x in range(cellNumberX)] for y in range(cellNumberY)]
startingPoint = Point(startPos[0],startPos[1]) # Add an iniial point for spawning purposes
cellGrid[startingPoint.x//radius][startingPoint.y//radius] = startingPoint
points = [startingPoint] # Initialise 2 lists tracking all points and active points
spawnpoints = [startingPoint]
while len(spawnpoints) > 0:
spawnIndex = random.randint(0,len(spawnpoints)-1)
spawnpoint = spawnpoints[spawnIndex]
spawned = False
for i in range(spawnAttempts):
r = random.uniform(radius,2*radius)
radian = random.uniform(0,2*math.pi)
newPoint = Point(spawnpoint.x + r*math.cos(radian),
spawnpoint.y + r*math.sin(radian))
if 0 <= newPoint.x <= width and 0 <= newPoint.y <= height:
isValid = True
else:
continue
newPointIndex = [int(newPoint.x//cellSize), int(newPoint.y//cellSize)]
neighbours = FindNeighbours(cellNumberX,cellNumberY,newPointIndex,cellGrid)
for neighbour in neighbours:
if newPoint.DistanceToSquared(neighbour) < radius**2:
isValid = False
break
if isValid:
points.append(newPoint)
spawnpoints.append(newPoint)
spawned = True
break
else:
continue
if spawned == False:
spawnpoints.remove(spawnpoint)
return points
def FindNeighbours(cellNumberX, cellNumberY, index, cellGrid):
neighbours = []
for cellX in range(max(0,(index[0]-2)), min(cellNumberX,(index[1]+2))):
for cellY in range(max(0,(index[0]-2)), min(cellNumberY,(index[1]+2))):
if cellGrid[cellX][cellY] != None:
neighbours.append(cellGrid[cellX][cellY])
return neighbours
Please help.
The probably most important step is missing in your code:
If new point is valid, new point is listed as spawnpoint and placed into cell in grid.
I suggest to add the point to the cellGrid if it is valid:
if isValid:
cellGrid[newPointIndex[0]][newPointIndex[1]] = newPoint
points.append(newPoint)
spawnpoints.append(newPoint)
spawned = True
break
Further, you have to verify if the cell with the index newPointIndex is not already occupied before a point can be add:
newPointIndex = [int(newPoint.x/cellSize), int(newPoint.y/cellSize)]
if cellGrid[newPointIndex[0]][newPointIndex[1]] != None:
continue
neighbours = FindNeighbours(cellNumberX,cellNumberY,newPointIndex,cellGrid)
Finally there is an issue in the function FindNeighbours. range(start, stop) creates a range for x in start <= x < stop.
So the stop has to be index[0]+3 rather than index[0]+2.
Further the ranges which control the 2 nested for loops, run both from x-2 to y+2 rather than from x-2 to x+2 respectively from y-2 to y+2:
for cellX in range(max(0,(index[0]-2)), min(cellNumberX,(index[1]+2))):
for cellY in range(max(0,(index[0]-2)), min(cellNumberY,(index[1]+2)))
The fixed function has to be:
def FindNeighbours(cellNumberX, cellNumberY, index, cellGrid):
neighbours = []
for cellX in range(max(0, index[0]-2), min(cellNumberX, index[0]+3)):
for cellY in range(max(0, index[1]-2), min(cellNumberY, index[1]+3)):
if cellGrid[cellX][cellY] != None:
neighbours.append(cellGrid[cellX][cellY])
return neighbours
See the result, for a size of 300 x 300 and a radius of 15:
An even better result can be achieve, if always the 1st point of spawnpoints is used to rather than a random point:
# spawnIndex = random.randint(0,len(spawnpoints)-1)
spawnIndex = 0 # 0 rather than random
spawnpoint = spawnpoints[spawnIndex]

Minimax returning future moves, not the actual moves leading to a win

I'm making a Dots & Boxes AI with python, my problem is that when i increase the depth of minimax (for example: depth = 2) the best move that is returned to me is a future good move.
. _ . _ .
| |
. . .
It would return something like this:
. _ . _ .
| |
. _ . .
There isn't a good move here for the AI, since it will always give the other player a box to complete. What my AI returns is for example [1, 0, -25, true] which means 1, 0 coordinates, a score -25 (Which means it completed a box but in this case it did not) and True meaning that the Horizontal line has been played (False would mean a vertical).
My guess is that it's just returning the future good move. Here's the code:
Evaluation
def evaluate(boxes, linesX, linesY, m, n, is_maximizing):
# linesX is the matrix of horizontal lines
# linesY is the matrix of vertical lines
i = 0
j = 0
is_x = True
if winner(boxes) is True:
score = 5000
elif winner(boxes) is False:
score = -5000
else:
# This returns coordinates to complete a box and score points
can_complete = can_complete_box(boxes, linesX, linesY, m, n)
if can_complete and is_maximizing is True:
i = can_complete[0][0]
j = can_complete[0][1]
is_x = can_complete[0][2]
score = +25
elif can_complete and is_maximizing is False:
i = can_complete[0][0]
j = can_complete[0][1]
is_x = can_complete[0][2]
score = -25
else:
move = choice(possible_moves(linesX, linesY))
i = move[0]
j = move[1]
is_x = move[2]
score = 0
# i and j are coordinates,
# score is the evaluation,
# is_x says if it's linesX or linesY
return i, j, score, is_x
Minimax
def minimax(boxes, linesX, linesY, m, n, depth, is_maximizing):
# Possible moves returns all the possible moves available
if depth == 0 or len(possible_moves(linesX, linesY)) == 1:
i, j, score, is_x = evaluate(boxes, linesX, linesY, m, n, is_maximizing)
return [i, j, score, is_x]
# Maximizing players turn
if is_maximizing:
max_eval = -infinity
best_move = None
for move in possible_moves(linesX, linesY):
i, j, is_x = move[0], move[1], move[2]
# is_x tells me if it's a horizontal move or vertical
if is_x:
linesX[i][j] = True
else:
linesY[i][j] = True
evaluation = minimax(boxes, linesX, linesY, m, n, depth - 1, False)
max_eval = max(max_eval, evaluation[2])
# Reverses the moves that were played
if is_x:
linesX[i][j] = None
else:
linesY[i][j] = None
if max_eval == evaluation[2]:
best_move = [evaluation[0], evaluation[1], max_eval, evaluation[3]]
return best_move
else:
min_eval = infinity
best_move = None
for move in possible_moves(linesX, linesY):
i, j, is_x = move[0], move[1], move[2]
if is_x:
linesX[i][j] = False
else:
linesY[i][j] = False
# evaluation returns [i, j, score, is_x]
# i and j are coordinates of the move
# score is the evaluation
# is_x tells me if it's a horizontal or vertical move
evaluation = minimax(boxes, linesX, linesY, m, n, depth - 1, True)
min_eval = min(min_eval, evaluation[2])
if is_x:
linesX[i][j] = None
else:
linesY[i][j] = None
if min_eval == evaluation[2]:
best_move = [evaluation[0], evaluation[1], min_eval, evaluation[3]]
return best_move
I've been stuck on this problem for a long time so help would be very welcome.
Thanks!

2D Pathfinding with variable movement speed?

Hi I'm pretty new to the path-finding field and I have searched all around the interwebs for an answer to my question so I figured its time to ask the experts:
My environment consists of a 2D rectangular map with walls and units to traverse the environment
(no collision between units but they can't go through walls)
Units in the environment move in a turn-based fashion in a straight line but they can move at a speed of
0 to unit.movespeed (e.g. 5)(you have control over the movespeed for every movement). (map size is range is from 20,20 to 200,200 cells but you can move to floating-point locations add walls can be at a location like eg wall[(x=10.75,y=9.34),(x=33.56,y=62.43])
In summery every tick, you tell a unit to move to a destination (x,y) of type float
I have tried A* and goal-based vector pathfinding algorithms but the problem I keep running into is figuring out how fast they should move because obviously, the optimal movespeed should be the maximum speed yet if they always move at the maximum speed they are prone to hit a wall because these algorithms don't take into account variable movement speed.
Any Ideas?
image of movespeed issue with a* star and goal-based vector pathfinding problem
Code:
class Cell:
def __init__(self,coords,vector=None,distance=None,obstacle=False):
self.coords = coords
self.vector = vector
self.distance = distance
self.obstacle = obstacle
class VectorMap:
def __init__(self,unit, dest, map=HardCoded._make_map()):
self.size = Coords(len(map), len(map[0]))
self.map = map
VectorMap.map_converter()
self.unit = unit
self.dest = dest
def _create_vector_map(self):
return self._cell_def(1,self.map[self.dest])
def _cell_def(self,dist,current_cell):
neighbors = [Coords(0, 1), Coords(0, -1), Coords(1, 0), Coords(-1, 0), Coords(1, 1), Coords(1, -1),
Coords(-1, 1), Coords(-1, -1)]
for neighbor in neighbors:
#check if out of range of arr then return map
if current_cell.coords.y + neighbor.y < self.size[1] and current_cell.coords.x + neighbor.x < self.size[0]:
neighbor_cell = self.map[current_cell.coords.x + neighbor.x][current_cell.coords.y + neighbor.y]
if neighbor_cell.obstacle:
continue
neighbor_cell.distance = dist
#neighbor_cell.vector = current_cell
return self._cell_def(self.map,dist+1,neighbor_cell)
def map_converter(self):
nmap = []
for x,element in enumerate(self.map):
tmap = []
for y,value in enumerate(element):
tmap.append(Cell(Coords(x,y),vector=None,distance=None,obstacle=False if value == 0 else True))
nmap.append(tmap)
self.map = nmap
self._create_vector_map()
for x,c in enumerate(self.map):
for y,cell in enumerate(c):
cell.vector = Coords(0,0)
right_tile_distance = (self.map[x+1][y].distance if x+1 < self.size.x else self.map[x][y].distance)
up_tile_distance = (self.map[x][y+1].distance if y+1 < self.size.y else self.map[x][y].distance)
cell.vector.x = (self.map[x-1][y].distance if x is not 0 else self.map[x][y].distance) - right_tile_distance
cell.vector.y = up_tile_distance - (self.map[x][y-1].distance if y is not 0 else self.map[x][y].distance)
if cell.vector.x == 0 and right_tile_distance < self.map[x][y].distance:
cell.vector.x = 1
if cell.vector.y == 0 and up_tile_distance < self.map[x][y].distance:
cell.vector.y = 1
cell.vector = Util.normalize(cell.vector)
def vetor_move(self):
vector = self.map[self.unit.coords.x][self.unit.coords.y].vector
movespeed = self.desired_movespeed()
dest = Coords(vector.x * movespeed,vector.y * movespeed)
return Action(mechanic='move', params=[dest], report='Moving With Maths')
def desired_movespeed(self):
pass
class Util:
#staticmethod
def normalize(vector):
norm=np.linalg.norm(vector,ord=1)
if norm == 0:
norm = np.finfo(vector.dtype).eps
return Coords(*(vector/norm))

My implementation of Bridson's algorithm Poisson-Disk Sampling seems to be stuck in an infinite loop

A video by Sebastion Lague explained the Bridson's algorithm really well.
To oversimplify,
Create cell grid that has sides of radius/sqrt(2).
Place initial point and list as spawnpoint.
Place point into cell in grid.
For any spawnpoint, spawn a point between radius and 2*radius.
Look at the cells 2 units away from cell of new point.
If contains other points, compare distance.
If any point is closer to new point than the radius, new point is invalid.
If new point is valid, new point is listed as spawnpoint and placed into cell in grid.
If spawnpoint spawns too many invalid points, spawnpoint is removed and turns into point.
Repeat until no more spawnpoints exists.
Return points.
I basically written the same thing down in Python 3.7.2 and pygame 1.7~, but as said in the title, I'm stuck in recursive purgatory.
I used one Point() class for this algorithm, which might seem redundant given that pygame.Vector2() exists, but I needed some elements for a separate algorithm (Delaunay's with infinite vertices) that required this class to work.
For the sake of simplicity I'm going to cut away all the Delaunay-specific elements and show the bare-bones of this class that is needed for this algorithm:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def DistanceToSquared(self,other):
return (self.x-other.x)**2 + (self.y-other.y)**2
The code that is related to the Bridson's algorithm is:
def PoissonDiskSampling(width, height, radius, startPos = None, spawnAttempts = 10):
if startPos == None:
startPos = [width//2,height//2]
cellSize = radius / math.sqrt(2)
cellNumberX = int(width // cellSize + 1) # Initialise a cells grid for optimisation
cellNumberY = int(height // cellSize + 1)
cellGrid = [[None for x in range(cellNumberX)] for y in range(cellNumberY)]
startingPoint = Point(startPos[0],startPos[1]) # Add an iniial point for spawning purposes
cellGrid[startingPoint.x//radius][startingPoint.y//radius] = startingPoint
points = [startingPoint] # Initialise 2 lists tracking all points and active points
spawnpoints = [startingPoint]
while len(spawnpoints) > 0:
spawnIndex = random.randint(0,len(spawnpoints)-1)
spawnpoint = spawnpoints[spawnIndex]
spawned = False
for i in range(spawnAttempts):
r = random.uniform(radius,2*radius)
radian = random.uniform(0,2*math.pi)
newPoint = Point(spawnpoint.x + r*math.cos(radian),
spawnpoint.y + r*math.sin(radian))
if 0 <= newPoint.x <= width and 0 <= newPoint.y <= height:
isValid = True
else:
continue
newPointIndex = [int(newPoint.x//cellSize), int(newPoint.y//cellSize)]
neighbours = FindNeighbours(cellNumberX,cellNumberY,newPointIndex,cellGrid)
for neighbour in neighbours:
if newPoint.DistanceToSquared(neighbour) < radius**2:
isValid = False
break
if isValid:
points.append(newPoint)
spawnpoints.append(newPoint)
spawned = True
break
else:
continue
if spawned == False:
spawnpoints.remove(spawnpoint)
return points
def FindNeighbours(cellNumberX, cellNumberY, index, cellGrid):
neighbours = []
for cellX in range(max(0,(index[0]-2)), min(cellNumberX,(index[1]+2))):
for cellY in range(max(0,(index[0]-2)), min(cellNumberY,(index[1]+2))):
if cellGrid[cellX][cellY] != None:
neighbours.append(cellGrid[cellX][cellY])
return neighbours
Please help.
The probably most important step is missing in your code:
If new point is valid, new point is listed as spawnpoint and placed into cell in grid.
I suggest to add the point to the cellGrid if it is valid:
if isValid:
cellGrid[newPointIndex[0]][newPointIndex[1]] = newPoint
points.append(newPoint)
spawnpoints.append(newPoint)
spawned = True
break
Further, you have to verify if the cell with the index newPointIndex is not already occupied before a point can be add:
newPointIndex = [int(newPoint.x/cellSize), int(newPoint.y/cellSize)]
if cellGrid[newPointIndex[0]][newPointIndex[1]] != None:
continue
neighbours = FindNeighbours(cellNumberX,cellNumberY,newPointIndex,cellGrid)
Finally there is an issue in the function FindNeighbours. range(start, stop) creates a range for x in start <= x < stop.
So the stop has to be index[0]+3 rather than index[0]+2.
Further the ranges which control the 2 nested for loops, run both from x-2 to y+2 rather than from x-2 to x+2 respectively from y-2 to y+2:
for cellX in range(max(0,(index[0]-2)), min(cellNumberX,(index[1]+2))):
for cellY in range(max(0,(index[0]-2)), min(cellNumberY,(index[1]+2)))
The fixed function has to be:
def FindNeighbours(cellNumberX, cellNumberY, index, cellGrid):
neighbours = []
for cellX in range(max(0, index[0]-2), min(cellNumberX, index[0]+3)):
for cellY in range(max(0, index[1]-2), min(cellNumberY, index[1]+3)):
if cellGrid[cellX][cellY] != None:
neighbours.append(cellGrid[cellX][cellY])
return neighbours
See the result, for a size of 300 x 300 and a radius of 15:
An even better result can be achieve, if always the 1st point of spawnpoints is used to rather than a random point:
# spawnIndex = random.randint(0,len(spawnpoints)-1)
spawnIndex = 0 # 0 rather than random
spawnpoint = spawnpoints[spawnIndex]

Categories