Maze Solver in Python
Intermediate · Logic and puzzles · about 60 minutes to build · 148 lines
Solve the example maze or a new random one and see the shortest path from S to E drawn in dots.
The program
Press Run and answer its questions in the box that appears under the code.
Open it in the full editor to save it, share it and keep building.
Sample run
What the program printed, and what was typed (in yellow).
Maze solver 1) Solve the example maze 2) Solve a random maze 3) Quit Choose 1-3: 1 Maze Solved ############### ############### #S # # #S # # # ### # ##### # #.### # ##### # # # # # # #. # # # # # # ##### # # # #.# ##### # # # # # # # # #.# # # # # ### ####### # #.### ####### # # E# #............E# ############### ############### Shortest path: 18 steps. Cells explored: 50. 1) Solve the example maze 2) Solve a random maze 3) Quit Choose 1-3: 2 Rooms across (3-10): big Please type a whole number from 3 to 10. Rooms across (3-10): 6 Maze Solved ############# ############# #S # # #S..# ... # ### # # # # # ###.# #.#.# # # # # # # # #...# #.#.# # # ### # # # # #.### #.#.# # # # # # # # #.# #.#.# # # # ### # # # #.# ###.#.# # # # # # # #.# #...#. # # ### ### ### #.###.###.### # # # # #.....# #...# ####### ### # ####### ###.# # E# # E# ############# ############# Shortest path: 40 steps. Cells explored: 60. 1) Solve the example maze 2) Solve a random maze 3) Quit Choose 1-3: 3 Goodbye.
How it is built
1. The maze as text
A maze is a list of strings: # is a wall, a space is open, S is the start and E the end. A cell is a (row, col) pair, and maze[row][col] tells you what is there.
WALL, OPEN, START, END, PATH = "#", " ", "S", "E", "." STEPS = [(-1, 0), (1, 0), (0, -1), (0, 1)] # up, down, left, right MIN_ROOMS, MAX_ROOMS = 3, 10 EXAMPLE = [ "###############", "#S # #", "# ### # ##### #", "# # # # #", "# # ##### # # #", "# # # # #", "# ### ####### #", "# E#", "###############", ]
2. Carving a random maze
Rooms sit at odd rows and columns with walls between them. The loop walks from room to room in random order, knocking down the wall it passes through, and backs up along stack at dead ends. This is depth-first search, and it makes a maze with exactly one way between any two rooms.
def carve(rooms): """Carve a square maze with exactly one way between any two rooms.""" size = 2 * rooms + 1 # rooms sit at odd rows and columns, walls between grid = [[WALL] * size for _ in range(size)] grid[1][1] = OPEN stack = [(1, 1)] while stack: # walk randomly, knocking down walls; back up at dead ends row, col = stack[-1] nexts = [(row + 2 * dr, col + 2 * dc) for dr, dc in STEPS if 0 < row + 2 * dr < size and 0 < col + 2 * dc < size and grid[row + 2 * dr][col + 2 * dc] == WALL] if not nexts: stack.pop() continue r, c = random.choice(nexts) grid[(row + r) // 2][(col + c) // 2] = OPEN # the wall in between grid[r][c] = OPEN stack.append((r, c)) return grid
3. Making shortcuts
With only one route, the shortest path would be the only path, so a few walls between rooms are knocked out at random to make loops. A wall between two rooms always has an odd row plus column.
def random_maze(rooms): """Make a random maze with a few extra gaps, so some routes are shorter.""" grid = carve(rooms) size = len(grid) for _ in range(rooms): r, c = random.randrange(1, size - 1), random.randrange(1, size - 1) if (r + c) % 2 == 1: # a wall between two rooms grid[r][c] = OPEN grid[1][1], grid[size - 2][size - 2] = START, END return ["".join(row) for row in grid]
4. Open neighbours
yield hands back the open cells next to a cell one at a time; cells off the edge or inside a wall are skipped.
def neighbours(maze, cell): """Yield the open cells next to this one.""" row, col = cell for dr, dc in STEPS: r, c = row + dr, col + dc if 0 <= r < len(maze) and 0 <= c < len(maze[r]) and maze[r][c] != WALL: yield r, c
5. Breadth-first search
The queue starts with S. Each step takes the oldest cell from the front with popleft() and adds its unvisited neighbours to the back, so cells are explored in rings by distance from S, and the first route that reaches E is the shortest. came_from remembers how each cell was reached, and following it back from E gives the path.
def solve(maze): """Return the shortest path from S to E (or None) and cells explored.""" start, end = find(maze, START), find(maze, END) came_from = {start: None} # each cell remembers where we reached it from queue = deque([start]) while queue: cell = queue.popleft() # oldest first, so we explore in rings if cell == end: break for nxt in neighbours(maze, cell): if nxt not in came_from: came_from[nxt] = cell queue.append(nxt) if end not in came_from: return None, len(came_from) path, cell = [], end while cell is not None: # walk back from E to S path.append(cell) cell = came_from[cell] return path[::-1], len(came_from)
6. Drawing the path
Strings cannot be changed, so each line is copied into a list. Every cell on the path except S and E becomes a dot, and the lists are joined back into strings.
def draw_path(maze, path): """Return a copy of the maze with the path drawn in dots.""" grid = [list(line) for line in maze] for row, col in path[1:-1]: # keep S and E as they are grid[row][col] = PATH return ["".join(row) for row in grid]
Make it bigger
- Let the player type their own maze line by line, then solve it.
- Use a list as a stack instead of the deque and compare: depth-first search finds a path, but not always the shortest.
- Print each open cell's distance from S to see the rings breadth-first search explores.
- Add mud cells that cost 3 steps and solve with Dijkstra's algorithm and heapq.
What it practises
- breadth-first search
- collections.deque
- dictionaries
- generators with yield
- depth-first search