#!/usr/bin/env python
# coding: utf-8
#
Peter Norvig
13 March 2018
#
# # Maze Generation
#
# Let's make some mazes! I'm thinking of mazes like this one, which is a rectangular grid of squares, with walls on some of the sides of squares, and openings on other sides:
#
# ![Wikipedia](https://upload.wikimedia.org/wikipedia/commons/thumb/8/88/Maze_simple.svg/475px-Maze_simple.svg.png)
#
# The main constraint is that there should be a path from entrance to exit, and it should be ***fun*** to solve the maze with pencil, paper, and brain power—not too easy, but also not impossible.
#
# As I think about how to model a maze on the computer, it seems like a **graph** is the right model: the nodes of
# the graph are the squares of the grid, and the edges of the graph are the openings between adjacent squares. So what properties of a graph make a good maze?
# - There must be a path from entrance to exit.
# - There must not be too such many paths; maybe it is best if there is only one.
# - Probably the graph should be *singly connected*—there shouldn't be islands of squares that are unreachable from the start. And maybe we want exactly one path between any two squares.
# - The path should have many twists; it would be too easy if it was mostly straight.
#
# I know that a **tree** has all these properties except the last one. So my goal has become: *Superimpose a tree over the grid, covering every square, and make sure the paths are twisty.* Here's how I'll do it:
#
# - Start with a grid with no edges (every square is surrounded by walls on all sides).
# - Add edges (that is, knock down walls) for the entrance at upper left and exit at lower right.
# - Place the root of the tree in some square.
# - Then repeat until the tree covers the whole grid:
# * Select some node already in the tree.
# * Randomly select a neighbor that hasn't been added to the tree yet.
# * Add an edge (knock down the wall) from the node to the neighbor.
#
# In the example below, the root, `A`, has been placed in the upper-left corner, and two branches,
# `A-B-C-D` and `A-b-c-d`, have been randomly chosen (well, not actually random; they are starting to create the same maze as in the diagram above):
#
# o o--o--o--o--o--o--o--o--o--o
# | A b c| | | | | | | |
# o o--o o--o--o--o--o--o--o--o
# | B| | d| | | | | | | |
# o o--o--o--o--o--o--o--o--o--o
# | C D| | | | | | | | |
# o--o--o--o--o--o--o--o--o--o--o
# | | | | | | | | | | |
# o--o--o--o--o--o--o--o--o--o--o
# | | | | | | | | | | |
# o--o--o--o--o--o--o--o--o--o o
#
# Next I select node `d` and extend it to `e` (at which point there are no available neighbors, so `e` will not be selected in the future), and then I select `D` and extend from there all the way to `N`, at each step selecting the node I just added:
#
# o o--o--o--o--o--o--o--o--o--o
# | A b c| | | | | | | |
# o o--o o--o--o--o--o--o--o--o
# | B| e d| | N| | | | | |
# o o--o--o--o o--o--o--o--o--o
# | C D| | | M| | | | | |
# o--o o--o--o o--o--o--o--o--o
# | F E| | K L| | | | | |
# o o--o--o o--o--o--o--o--o--o
# | G H I J| | | | | | |
# o--o--o--o--o--o--o--o--o--o o
#
# Continue like this until every square in the grid has been added to the tree.
#
#
# ## Implementing Random Trees
#
# I'll make the following implementation choices:
#
# - A tree will be represented as a list of edges.
# - An `Edge` is a tuple of two nodes. I'll keep them sorted so that `Edge(A, B)` is the same as `Edge(B, A)`.
# - A node in a tree can be anything: a number, a letter, a square, ...
# - The algorithm for `random_tree(nodes, neighbors, pop)` is as follows:
# * We will keep track of three collections:
# - `tree`: a list of edges that constitutes the tree.
# - `nodes`: the set of nodes that have not yet been added to the tree, but will be.
# - `frontier`: a queue of nodes in the tree that are eligible to have an edge added.
# * On each iteration:
# - Use `pop` to pick a `node` from the frontier, and find the neighbors that are not already in the tree.
# - If there are any neighbors, randomly pick one (`nbr`), add `Edge(node, nbr)` to `tree`, remove the
# neighbor from `nodes`, and keep both the node and the neighbor on the frontier. If there are no neighbors,
# drop the node from the frontier.
# * When no `nodes` remain, return `tree`.
# In[1]:
import random
from collections import deque, namedtuple
def Edge(node1, node2): return tuple(sorted([node1, node2]))
def random_tree(nodes: set, neighbors: callable, pop: callable) -> [Edge]:
"Repeat: pop a node and add Edge(node, nbr) until all nodes have been added to tree."
tree = []
root = nodes.pop()
frontier = deque([root])
while nodes:
node = pop(frontier)
nbrs = neighbors(node) & nodes
if nbrs:
nbr = random.choice(list(nbrs))
tree.append(Edge(node, nbr))
nodes.remove(nbr)
frontier.extend([node, nbr])
return tree
# ## Implementing Random Mazes
#
# Now let's use `random_tree` to implement `random_maze`. Some more choices:
#
# * A `Maze` is a named tuple with three fields: the `width` and `height` of the grid, and a list of `edges` between squares.
# * A square is denoted by an `(x, y)` tuple of integer coordinates.
# * The function `neighbors4` gives the four surrounding squares.
# In[2]:
Maze = namedtuple('Maze', 'width, height, edges')
def neighbors4(square):
"The 4 neighbors of an (x, y) square."
(x, y) = square
return {(x + 1, y), (x - 1, y), (x, y + 1), (x, y - 1)}
def squares(width, height):
"All squares in a grid of these dimensions."
return {(x, y) for x in range(width) for y in range(height)}
def random_maze(width, height, pop=deque.pop):
"Use random_tree to generate a random maze."
nodes = squares(width, height)
tree = random_tree(nodes, neighbors4, pop)
return Maze(width, height, tree)
# In[3]:
random_maze(10,5)
# That's not very pretty to look at. I'm going to need a way to visualize a maze.
#
# # Printing a maze
# In[4]:
def print_maze(maze, dot='o', lin='--', bar='|', sp=' '):
"Print maze in ASCII."
exit = Edge((maze.width-1, maze.height-1), (maze.width-1, maze.height))
edges = set(maze.edges) | {exit}
print(dot + sp + lin.join(dot * maze.width)) # Top line, including entrance
def vert_wall(x, y): return (' ' if Edge((x, y), (x+1, y)) in edges else bar)
def horz_wall(x, y): return (sp if Edge((x, y), (x, y+1)) in edges else lin)
for y in range(maze.height):
print(bar + cat(sp + vert_wall(x, y) for x in range(maze.width)))
print(dot + cat(horz_wall(x, y) + dot for x in range(maze.width)))
cat = ''.join
print_maze(random_maze(10, 5))
# *Much better!* But can I do better still?
#
# # Plotting a maze
#
# I'll use `matplotlib` to plot lines where the edges aren't:
# In[5]:
get_ipython().run_line_magic('matplotlib', 'inline')
import matplotlib.pyplot as plt
def plot_maze(maze):
"Plot a maze by drawing lines between adjacent squares, except for pairs in maze.edges"
plt.figure(figsize=(8, 4))
plt.axis('off')
plt.gca().invert_yaxis()
w, h = maze.width, maze.height
exits = {Edge((0, 0), (0, -1)), Edge((w-1, h-1), (w-1, h))}
edges = set(maze.edges) | exits
for sq in squares(w, h):
for nbr in neighbors4(sq):
if Edge(sq, nbr) not in edges:
plot_wall(sq, nbr)
plt.show()
def plot_wall(s1, s2):
"Plot a thick black line between squares s1 and s2."
(x1, y1), (x2, y2) = s1, s2
if x1 == x2: # horizontal wall
y = max(y1, y2)
X, Y = [x1, x1+1], [y, y]
else: # vertical wall
x = max(x1, x2)
X, Y = [x, x], [y1, y1+1]
plt.plot(X, Y, 'k-', linewidth=4.0)
# Let's compare the two visualization functions:
# In[6]:
M = random_maze(10, 5)
plot_maze(M)
print_maze(M)
# # `pop` strategies
#
# Now I want to compare how the maze varies based on theree different choices for the `pop` parameter.
#
# (1) The default is `deque.pop`
# which means that the tree is created **depth-first**; we always select the `node` at the end of the `frontier`, so the tree follows a single branch along a randomly-twisted path until the path doubles back on itself and there are no more neighbors; at that point we select the most recent square for which there are neighbors:
# In[7]:
plot_maze(random_maze(40, 20, deque.pop))
# The maze with `deque.pop` looks pretty good. Reminds me of those [cyber brain](https://www.vectorstock.com/royalty-free-vector/cyber-brain-vector-3071965) images.
#
# (2) An alternative is `queue.popleft`, which creates the maze roughly **breadth-first**—we start at some root square , add an edge to it, and from then on we always select first a parent edge before we select a child edge. The net result is a design that appears to radiate out in concentric layers from the root (which is chosen by `random_tree` and is not necessarily the top-left square; below it looks like the root is in the upper-left quadrant).
# In[8]:
plot_maze(random_maze(40, 20, deque.popleft))
# The `deque.popleft` maze is interesting as a design, but to me it doesn't work well as a maze.
#
# (3) We can select a cell at random by shuffling the frontier before popping an element off of it:
# In[9]:
def poprandom(seq):
"Shuffle a mutable sequence (deque or list) and then pop an element."
random.shuffle(seq)
return seq.pop()
plot_maze(random_maze(40, 20, poprandom))
# This is an interesting compromise: it has some structure, but still works nicely as a maze, in my opinion.
#
# What other variations can you come up with to generate interesting mazes?