Initial issues
Okay, so to begin with you usually wouldn't setup your recursive function like this. Since you need to call the same function over and over again, you don't want to re-create your maze object each call. You want to perform all of the setup and calculation outside of the recursive function call, and only do one very specific job recursively.
Setup
I'm assuming your setup for the maze class is a little like this:
import random
class Maze:
def __init__(self, width, height):
self.width = width // 2 * 2 + 1
self.height = height // 2 * 2 + 1
# this creates a 2d-array for your maze data (False: path, True: wall)
self.cells = [
[True for x in range(self.width)]
for y in range(self.height)
]
def set_path(self, x, y):
self.cells[y][x] = False
def set_wall(self, x, y):
self.cells[y][x] = True
Ways to think about our maze
Okay, so now I can begin on the recursive generation itself. Now rather than taking the approach of adding walls, I instead fill the entire maze with walls and "dig" myself the paths.
In our maze, even though we're treating it like a simple on/off 2d grid with walls and paths, its helpful to picture it more like a series of nodes (junctions) and links between those. I can see you begun to implement this with the way you made sure your maze width and height were odd numbers with (Width // 2) * 2 + 1
for example, and that you only chose even cells (albeit it I couldn't figure out what for).
Here's a quick diagram to visualise what I mean:

Each red circle is a node, and as you can see every single odd tile contains a node and is always a path tile. We will automatically assume that every odd tile will contain a path (and therefore a node). This means that when generating our maze, when we "dig" our way through, we will be moving two cells at a time, so that we create a link between nodes (the cells in grey).
The algorithm
The essence of the algorithm I will implement will be as follows:
- Move in random directions, "digging" my way forward, keeping track of the places I've been
- Repeat until I reach a dead end
- 'Backtrack' through the places I've been until I find a path with at least one viable path. Go to step 1
Here's those same steps, in more detail:
Move in random directions, keeping track of where I've been
1.1. We look around each direction, and see where our move options are
1.2. Pick a random direction where there is a valid path
1.3. Move to the new node (remember, we're moving by two cells)
Repeat until I reach a dead end
2.1. Keep on repeating the process in step one
2.2. If during step one, you found no path options, you've reached a dead end
Backtrack through the places I've been
3.1. Follow your footsteps (backtrack) through the nodes previously visited
3.2. Repeat until you've found a node with at least one unvisited path to try
3.3. Go to step one (as in, start walking in random directions through the new path)
Now to implement these steps with a recursive function. Every step we take to a new node (by moving two cells) will be a new function call, with new x-y coordinates. Here's the same steps, but recursively:
Move in random directions keeping track of where I've been
1.1. Randomly pick a direction and check it hasn't already been visited (eg if we had already walked down it, we would have already "dug" through so it would be a path). So pick any direction which is a wall (eg unvisited)
1.2. Now move two cells in that direction (but remember to set both the node cell and the link cell between the two nodes to paths, otherwise you've just jumped over a wall). Remember, when 'moving' to a new cell, we are going to call the function again with the new node's x-y coordinates
Repeat until you reach a dead end
2.1. If during step one, you found that all of your directions contained paths (eg you had already visited every single direction at that node), you need to backtrack
2.2. Now to backtrack, we are going to exit the current function call. This means we are moving backwards into the previous function which had initially moved us into this current node
Backtrack until you find a path
3.1. After exiting out of the function call you have now moved back into the previous node, with the previous x-y coordinates. You now go to step one, where you look for potential directions, and if none you go to step two and backtrack even more
The code
Okay, so with all of the theory and planning finished, we can now implement our design into code.
I'm going to create the recursive function as a method of our Maze class like so:
class Maze:
# ...
# constructor and other methods go here
# ...
def create_maze(self, x, y):
# our recursive function goes here
This means to fully create our maze, we call maze.create_maze(1, 1)
(replacing 1, 1
with whatever your starting coordinates are).
So lets walk through each of the steps we designed earlier, and turn them into code.
1.1 Pick a random, viable direction
def create_maze(self, x, y):
# set the current cell to a path, so that we don't return here later
self.set_path(self, x, y)
# we create a list of directions (in a random order) we can try
all_directions = [[1, 0], [-1, 0], [0, 1], [0, -1]]
random.shuffle(all_directions)
# we keep trying the next direction in the list, until we have no directions left
while len(all_directions) > 0:
# we remove and return the last item in our directions list
direction_to_try = all_directions.pop()
# calculate the new node's coordinates using our random direction.
# we *2 as we are moving two cells in each direction to the next node
node_x = x + (direction_to_try[0] * 2)
node_y = y + (direction_to_try[1] * 2)
# check if the test node is a wall (eg it hasn't been visited)
if self.is_wall(node_x, node_y):
# success code: we have found a path
# failure code: we find no paths
# a function to return if the current cell is a wall, and if the cell is within the maze bounds
def is_wall(self, x, y):
# checks if the coordinates are within the maze grid
if 0 <= x < self.width and 0 <= y < self.height:
# if they are, then we can check if the cell is a wall
return self.cells[y][x]
# if the coordinates are not within the maze bounds, we don't want to go there
else:
return False
1.2. Move two cells in that direction, and create the link path cell
So now the question is, what do we do once we've found a viable path option? The answer: we turn the link cell into a path (so we're not jumping over a wall to our new node), and move two cells in our new direction.
This becomes:
# success code: we have found a path
# set our linking cell (between the two nodes we're moving from/to) to a path
link_cell_x = x + direction_to_try[0]
link_cell_y = y + direction_to_try[1]
self.set_path(link_cell_x, link_cell_y)
# "move" to our new node. remember we are calling the function every
# time we move, so we call it again but with the updated x and y coordinates
self.create_maze(node_x, node_y)
and then to integrate our success code into our create_maze
function, it becomes:
def create_maze(self, x, y):
self.set_path(x, y)
all_directions = [[1, 0], [-1, 0], [0, 1], [0, -1]]
random.shuffle(all_directions)
while len(all_directions) > 0:
direction_to_try = all_directions.pop()
node_x = x + (direction_to_try[0] * 2)
node_y = y + (direction_to_try[1] * 2)
if self.is_wall(node_x, node_y):
# success code: we have found a path
# set our linking cell (between the two nodes we're moving from/to) to a path
link_cell_x = x + direction_to_try[0]
link_cell_y = y + direction_to_try[1]
self.set_path(link_cell_x, link_cell_y)
# "move" to our new node. remember we are calling the function every
# time we move, so we call it again but with the updated x and y coordinates
self.create_maze(node_x, node_y)
# failure code: we find no paths
2.1. If all of our directions contain paths (they have been visited), then we need to backtrack
2.2. To backtrack, we exit the function call, which brings us to the previous node
A simple way to end a function, is to call return
. This stops any further code from being run in the function, and returns to the previous function which had called it.
# failure code: we find no paths
return
And to add this into our recursive function, it now becomes:
def create_maze(self, x, y):
self.set_path(x, y)
all_directions = [[1, 0], [-1, 0], [0, 1], [0, -1]]
random.shuffle(all_directions)
while len(all_directions) > 0:
direction_to_try = all_directions.pop()
node_x = x + (direction_to_try[0] * 2)
node_y = y + (direction_to_try[1] * 2)
if self.is_wall(node_x, node_y):
link_cell_x = x + direction_to_try[0]
link_cell_y = y + direction_to_try[1]
self.set_path(link_cell_x, link_cell_y)
self.create_maze(node_x, node_y)
# failure code: we find no paths
return
3.1. Try step one again, if not go to step two
Essentially we have now programmed a fully functional maze generation program, using (to the best of my ability) a fairly good recursive method. Once a function reaches a dead end, it will return, backtrack into the previous function, and continue the process until that function reaches a dead end etc..
The final code
import random
class Maze:
def __init__(self, width, height):
self.width = width // 2 * 2 + 1
self.height = height // 2 * 2 + 1
# this creates a 2d-array for your maze data (False: path, True: wall)
self.cells = [
[True for x in range(self.width)]
for y in range(self.height)
]
def set_path(self, x, y):
self.cells[y][x] = False
def set_wall(self, x, y):
self.cells[y][x] = True
# a function to return if the current cell is a wall,
# and if the cell is within the maze bounds
def is_wall(self, x, y):
# checks if the coordinates are within the maze grid
if 0 <= x < self.width and 0 <= y < self.height:
# if they are, then we can check if the cell is a wall
return self.cells[y][x]
# if the coordinates are not within the maze bounds, we don't want to go there
else:
return False
def create_maze(self, x, y):
# set the current cell to a path, so that we don't return here later
self.set_path(x, y)
# we create a list of directions (in a random order) we can try
all_directions = [[1, 0], [-1, 0], [0, 1], [0, -1]]
random.shuffle(all_directions)
# we keep trying the next direction in the list, until we have no directions left
while len(all_directions) > 0:
# we remove and return the last item in our directions list
direction_to_try = all_directions.pop()
# calculate the new node's coordinates using our random direction.
# we *2 as we are moving two cells in each direction to the next node
node_x = x + (direction_to_try[0] * 2)
node_y = y + (direction_to_try[1] * 2)
# check if the test node is a wall (eg it hasn't been visited)
if self.is_wall(node_x, node_y):
# success code: we have found a path
# set our linking cell (between the two nodes we're moving from/to) to a path
link_cell_x = x + direction_to_try[0]
link_cell_y = y + direction_to_try[1]
self.set_path(link_cell_x, link_cell_y)
# "move" to our new node. remember we are calling the function every
# time we move, so we call it again but with the updated x and y coordinates
self.create_maze(node_x, node_y)
return
And there we go! A recursive algorithm to generate a maze. Sorry that I didn't use more of your code, but since I wasn't quite sure where you were going with it I thought I could at least help by showing you some working code.
One last thing we can quickly add is a print function, so that we can print out our maze to the screen. By using the "double underscore method" (dundermethod) __repr__
(update: use the method name __str__
instead, see comments) we can overwrite the print
functionality. The following code generates a string representing our newly generated maze:
def __repr__(self):
string = ""
conv = {
True: "██",
False: " "
}
for y in range(self.height):
for x in range(self.width):
string += conv[self.cells[y][x]]
string += "\n"
return string
By placing this inside the maze class, we can now perform the following, and it even works starting with even cells!
>>> maze.create_maze(1, 1)
>>> print(maze)
██████████████████
██ ██ ██
██████ ██ ██ ██
██ ██ ██ ██ ██
██ ██ ██████ ██
██ ██ ██
██ ██████████ ██
██ ██
██████████████████
>>> maze.create_maze(4, 4)
██
██████ ██████
██
██████████████
██ ██
██ ██████████
██ ██
██████████ ██
██
Hope it helped!