Jump to content

Maze generation algorithm

From Wikipedia, the free encyclopedia

This is an old revision of this page, as edited by 2a01:5c0:e082:2161:8d78:64c:2ea4:61c9 (talk) at 10:54, 27 April 2019 (Remove sentence which has no further explanation or sample code). The present address (URL) is a permanent link to this revision, which may differ significantly from the current revision.

Maze generation algorithms are automated methods for the creation of mazes.

This maze generated by modified version of Prim's algorithm, below.

Graph theory based methods

Animation of Graph theory based method

A maze can be generated by starting with a predetermined arrangement of cells (most commonly a rectangular grid but other arrangements are possible) with wall sites between them. This predetermined arrangement can be considered as a connected graph with the edges representing possible wall sites and the nodes representing cells. The purpose of the maze generation algorithm can then be considered to be making a subgraph in which it is challenging to find a route between two particular nodes.

If the subgraph is not connected, then there are regions of the graph that are wasted because they do not contribute to the search space. If the graph contains loops, then there may be multiple paths between the chosen nodes. Because of this, maze generation is often approached as generating a random spanning tree. Loops, which can confound naive maze solvers, may be introduced by adding random edges to the result during the course of the algorithm.

The animation shows the maze generation steps for a graph that is not on a rectangular grid. First, the computer creates a random planar graph G shown in blue, and its dual F shown in yellow. Second, computer traverses F using a chosen algorithm, such as a depth-first search, coloring the path red. During the traversal, whenever a red edge crosses over a blue edge, the blue edge is removed. Finally, when all vertices of F have been visited, F is erased and two edges from G, one for the entrance and one for the exit, are removed.

Randomized Depth-First Search

Animation of generator's thinking process using Depth-First Search

This algorithm is a randomized version of the depth-first search algorithm. Frequently implemented with a stack, this approach is one of the simplest ways to generate a maze using a computer. Consider the space for a maze being a large grid of cells (like a large chess board), each cell starting with four walls. Starting from a random cell, the computer then selects a random neighbouring cell that has not yet been visited. The computer removes the wall between the two cells and marks the new cell as visited, and adds it to the stack to facilitate backtracking. The computer continues this process, with a cell that has no unvisited neighbours being considered a dead-end. When at a dead-end it backtracks through the path until it reaches a cell with an unvisited neighbour, continuing the path generation by visiting this new, unvisited cell (creating a new junction). This process continues until every cell has been visited, causing the computer to backtrack all the way back to the beginning cell. We can be sure every cell is visited.

When this algorithm is implemented using a recursive procedure, (call) stack overflow will occur for larger grids. This can be avoided by using an explicit stack instead. The Java code below shows both implementation variants.

Horizontal Passage Bias

Mazes generated with a depth-first search have a low branching factor and contain many long corridors, because the algorithm explores as far as possible along each branch before backtracking.

Recursive backtracker on a hexagonal grid

The randomized depth-first search algorithm of maze generation can be implemented using a stack:

  1. Make the initial cell the current cell
  2. Push the current cell to the stack and mark it as visited
  3. While the stack is not empty
    1. If the current cell has unvisited neighbours
      1. Choose one of these neighbours randomly
      2. Remove the wall (add an edge) between the current cell and the chosen neighbor
      3. Push the neighbor to the stack and mark is as visited
      4. Make the neighbor the current cell
    2. Else
      1. Pop a cell from the stack and make it the current cell

The backtracking occurs when there are no more unvisited neighbors of the current cell. In that case the algorithm tracks back and continues with the cell at the top of the stack. This is a cell somewhere on the path from the current cell back to the start cell.

Randomized Kruskal's algorithm

An animation of generating a 30 by 20 maze using Kruskal's algorithm.

This algorithm is a randomized version of Kruskal's algorithm.

  1. Create a list of all walls, and create a set for each cell, each containing just that one cell.
  2. For each wall, in some random order:
    1. If the cells divided by this wall belong to distinct sets:
      1. Remove the current wall.
      2. Join the sets of the formerly divided cells.

There are several data structures that can be used to model the sets of cells. An efficient implementation using a disjoint-set data structure can perform each union and find operation on two sets in nearly constant amortized time (specifically, time; for any plausible value of ), so the running time of this algorithm is essentially proportional to the number of walls available to the maze.

It matters little whether the list of walls is initially randomized or if a wall is randomly chosen from a nonrandom list, either way is just as easy to code.

Because the effect of this algorithm is to produce a minimal spanning tree from a graph with equally weighted edges, it tends to produce regular patterns which are fairly easy to solve.

Randomized Prim's algorithm

An animation of generating a 30 by 20 maze using Prim's algorithm.

This algorithm is a randomized version of Prim's algorithm.

  1. Start with a grid full of walls.
  2. Pick a cell, mark it as part of the maze. Add the walls of the cell to the wall list.
  3. While there are walls in the list:
    1. Pick a random wall from the list. If only one of the two cells that the wall divides is visited, then:
      1. Make the wall a passage and mark the unvisited cell as part of the maze.
      2. Add the neighboring walls of the cell to the wall list.
    2. Remove the wall from the list.

It will usually be relatively easy to find the way to the starting cell, but hard to find the way anywhere else.

Note that simply running classical Prim's on a graph with random edge weights would create mazes stylistically identical to Kruskal's, because they are both minimal spanning tree algorithms. Instead, this algorithm introduces stylistic variation because the edges closer to the starting point have a lower effective weight.

Modified version

Although the classical Prim's algorithm keeps a list of edges, for maze generation we could instead maintain a list of adjacent cells. If the randomly chosen cell has multiple edges that connect it to the existing maze, select one of these edges at random. This will tend to branch slightly more than the edge-based version above.

Wilson's algorithm

All the above algorithms have biases of various sorts: depth-first search is biased toward long corridors, while Kruskal's/Prim's algorithms are biased toward many short dead ends. Wilson's algorithm,[1] on the other hand, generates an unbiased sample from the uniform distribution over all mazes, using loop-erased random walks.

We begin the algorithm by initializing the maze with one cell chosen arbitrarily. Then we start at a new cell chosen arbitrarily, and perform a random walk until we reach a cell already in the maze—however, if at any point the random walk reaches its own path, forming a loop, we erase the loop from the path before proceeding. When the path reaches the maze, we add it to the maze. Then we perform another loop-erased random walk from another arbitrary starting cell, repeating until all cells have been filled.

This procedure remains unbiased no matter which method we use to arbitrarily choose starting cells. So we could always choose the first unfilled cell in (say) left-to-right, top-to-bottom order for simplicity.

Recursive division method

Illustration of Recursive Division
original chamber division by two walls holes in walls continue subdividing... completed
step 1
step 2
step 3
step 4
step 5

Mazes can be created with recursive division, an algorithm which works as follows: Begin with the maze's space with no walls. Call this a chamber. Divide the chamber with a randomly positioned wall (or multiple walls) where each wall contains a randomly positioned passage opening within it. Then recursively repeat the process on the subchambers until all chambers are minimum sized. This method results in mazes with long straight walls crossing their space, making it easier to see which areas to avoid.

Recursive Maze generation

For example, in a rectangular maze, build at random points two walls that are perpendicular to each other. These two walls divide the large chamber into four smaller chambers separated by four walls. Choose three of the four walls at random, and open a one cell-wide hole at a random point in each of the three. Continue in this manner recursively, until every chamber has a width of one cell in either of the two directions.

Simple algorithms

3D version of Prim's algorithm. Vertical layers are labeled 1 through 4 from bottom to top. Stairs up are indicated with "/"; stairs down with "\", and stairs up-and-down with "x". Source code is included with the image description.

Other algorithms exist that require only enough memory to store one line of a 2D maze or one plane of a 3D maze. Eller's algorithm prevents loops by storing which cells in the current line are connected through cells in the previous lines, and never removes walls between any two cells already connected.[2] The Sidewinder algorithm starts with an open passage along the entire the top row, and subsequent rows consist of shorter horizontal passages with one connection to the passage above. The Sidewinder algorithm is trivial to solve from the bottom up because it has no upward dead ends.[3] Given a starting width, both algorithm create perfect mazes of unlimited height.

Most maze generation algorithms require maintaining relationships between cells within it, to ensure the end result will be solvable. Valid simply connected mazes can however be generated by focusing on each cell independently. A binary tree maze is a standard orthogonal maze where each cell always has a passage leading up or leading left, but never both. To create a binary tree maze, for each cell flip a coin to decide whether to add a passage leading up or left. Always pick the same direction for cells on the boundary, and the end result will be a valid simply connected maze that looks like a binary tree, with the upper left corner its root. As with Sidewinder, the binary tree maze has no dead ends in the directions of bias.

A related form of flipping a coin for each cell is to create an image using a random mix of forward slash and backslash characters. This doesn't generate a valid simply connected maze, but rather a selection of closed loops and unicursal passages. (The manual for the Commodore 64 presents a BASIC program using this algorithm, but using PETSCII diagonal line graphic characters instead for a smoother graphic appearance.)

Cellular automaton algorithms

Certain types of cellular automata can be used to generate mazes.[4] Two well-known such cellular automata, Maze and Mazectric, have rulestrings B3/S12345 and B3/S1234.[4] In the former, this means that cells survive from one generation to the next if they have at least one and at most five neighbours. In the latter, this means that cells survive if they have one to four neighbours. If a cell has exactly three neighbours, it is born. It is similar to Conway's Game of Life in that patterns that do not have a living cell adjacent to 1, 4, or 5 other living cells in any generation will behave identically to it.[4] However, for large patterns, it behaves very differently from Life.[4]

For a random starting pattern, these maze-generating cellular automata will evolve into complex mazes with well-defined walls outlining corridors. Mazecetric, which has the rule B3/S1234 has a tendency to generate longer and straighter corridors compared with Maze, with the rule B3/S12345.[4] Since these cellular automaton rules are deterministic, each maze generated is uniquely determined by its random starting pattern. This is a significant drawback since the mazes tend to be relatively predictable.

Like some of the graph-theory based methods described above, these cellular automata typically generate mazes from a single starting pattern; hence it will usually be relatively easy to find the way to the starting cell, but harder to find the way anywhere else.

Python code example

Example implementation of a variant of Prim's algorithm in Python/NumPy. Prim's algorithm above starts with a grid full of walls and grows a single component of pathable tiles. In this example, we start with an open grid and grow multiple components of walls.

This algorithm works by creating n (density) islands of length p (complexity). An island is created by choosing a random starting point with odd coordinates, then a random direction is chosen. If the cell two steps in the direction is free, then a wall is added at both one step and two steps in this direction. The process is iterated for n steps for this island. p islands are created. n and p are expressed as float to adapt them to the size of the maze. With a low complexity, islands are very small and the maze is easy to solve. With low density, the maze has more "big empty rooms".

import numpy
from numpy.random import randint as rand
import matplotlib.pyplot as pyplot

def maze(width=81, height=51, complexity=.75, density=.75):
    # Only odd shapes
    shape = ((height // 2) * 2 + 1, (width // 2) * 2 + 1)
    # Adjust complexity and density relative to maze size
    complexity = int(complexity * (5 * (shape[0] + shape[1]))) # number of components
    density    = int(density * ((shape[0] // 2) * (shape[1] // 2))) # size of components
    # Build actual maze
    Z = numpy.zeros(shape, dtype=bool)
    # Fill borders
    Z[0, :] = Z[-1, :] = 1
    Z[:, 0] = Z[:, -1] = 1
    # Make aisles
    for i in range(density):
        x, y = rand(0, shape[1] // 2) * 2, rand(0, shape[0] // 2) * 2 # pick a random position
        Z[y, x] = 1
        for j in range(complexity):
            neighbours = []
            if x > 1:             neighbours.append((y, x - 2))
            if x < shape[1] - 2:  neighbours.append((y, x + 2))
            if y > 1:             neighbours.append((y - 2, x))
            if y < shape[0] - 2:  neighbours.append((y + 2, x))
            if len(neighbours):
                y_,x_ = neighbours[rand(0, len(neighbours) - 1)]
                if Z[y_, x_] == 0:
                    Z[y_, x_] = 1
                    Z[y_ + (y - y_) // 2, x_ + (x - x_) // 2] = 1
                    x, y = x_, y_
    return Z

pyplot.figure(figsize=(10, 5))
pyplot.imshow(maze(80, 40), cmap=pyplot.cm.binary, interpolation='nearest')
pyplot.xticks([]), pyplot.yticks([])
pyplot.show()

Java code example

Sample implementation of the Randomized Depth-First Search algorithm.

RandomizedDFS.java:

package de.amr.maze.simple;

import java.util.ArrayDeque;
import java.util.BitSet;
import java.util.Deque;
import java.util.Random;

/**
 * Creates a maze from a grid graph using <em>randomized depth-first search</em>.
 * 
 * @author Armin Reichert
 */
public class RandomizedDFS{

	public static void main(String[] args) {
		// Using recursive procedure:
		System.out.println(maze(10, 10, 0, 0, true));
		// Using non-recursive procedure:
		System.out.println(maze(10, 10, 0, 0, false));
	}

	/** Returns a maze of the given size starting generation at the given grid position. */
	public static Grid maze(int numCols, int numRows, int startCol, int startRow, boolean recursive) {
		Grid grid = new Grid(numCols, numRows);
		BitSet visited = new BitSet(numCols * numRows);
		if (recursive) {
			traverseRecursive(grid, grid.vertex(startCol, startRow), visited);
		}
		else {
			traverseUsingStack(grid, grid.vertex(startCol, startRow), visited);
		}
		return grid;
	}

	/** Recursive procedure traversing the grid and creating the maze (spanning tree). */
	private static void traverseRecursive(Grid grid, int v, BitSet visited) {
		visited.set(v);
		for (int dir = unvisitedDir(grid, v, visited); dir != -1; dir = unvisitedDir(grid, v, visited)) {
			grid.addEdge(v, dir);
			traverseRecursive(grid, grid.neighbor(v, dir), visited);
		}
	}

	/** Non-recursive procedure traversing the grid and creating the maze (spanning tree). */
	private static void traverseUsingStack(Grid grid, int v, BitSet visited) {
		Deque<Integer> stack = new ArrayDeque<>();
		visited.set(v);
		stack.push(v);
		while (!stack.isEmpty()) {
			int dir = unvisitedDir(grid, v, visited);
			if (dir != -1) {
				int neighbor = grid.neighbor(v, dir);
				grid.addEdge(v, dir);
				visited.set(neighbor);
				stack.push(neighbor);
				v = neighbor;
			}
			else {
				v = stack.pop();
			}
		}
	}

	/** Returns a random direction to an unvisited neighbor or {@code -1}. */
	private static int unvisitedDir(Grid grid, int v, BitSet visited) {
		int[] candidates = new int[4];
		int numCandidates = 0;
		for (int dir : new int[] { Grid.NORTH, Grid.EAST, Grid.SOUTH, Grid.WEST }) {
			int neighbor = grid.neighbor(v, dir);
			if (neighbor != Grid.NO_VERTEX && !visited.get(neighbor)) {
				candidates[numCandidates++] = dir;
			}
		}
		return numCandidates == 0 ? -1 : candidates[new Random().nextInt(numCandidates)];
	}
}

Grid.java:

package de.amr.maze.simple;

import java.util.BitSet;

/**
 * Grid graph implementation. Edge set is represented by a single bit-set.
 */
public class Grid {

	/** Directions. */
	public static final int NORTH = 0, EAST = 1, SOUTH = 2, WEST = 3;

	/** Opposite directions. */
	public static final int[] OPPOSITE = { SOUTH, WEST, NORTH, EAST };

	/* Direction vectors. */
	public static final int[] X = { 0, 1, 0, -1 };
	public static final int[] Y = { -1, 0, 1, 0 };

	/** Index representing no vertex. */
	public static final int NO_VERTEX = -1;

	/** Number of grid columns. */
	public final int numCols;

	/** Number of grid rows. */
	public final int numRows;

	private BitSet edges;

	/** The bit-set index of the edge leaving vertex {@code v} towards direction {@code dir}. */
	private int bit(int v, int dir) {
		return 4 * v + dir;
	}

	/** Creates an empty grid of the given size. */
	public Grid(int numCols, int numRows) {
		this.numCols = numCols;
		this.numRows = numRows;
		edges = new BitSet(numCols * numRows * 4);
	}

	/** Vertex at column {@code col} and row {@code row}. */
	public int vertex(int col, int row) {
		return numCols * row + col;
	}

	/** Column of vertex {@code v}. */
	public int col(int v) {
		return v % numCols;
	}

	/** Row of vertex {@code v}. */
	public int row(int v) {
		return v / numCols;
	}

	/** Returns the number of (undirected) edges. */
	public int numEdges() {
		return edges.cardinality() / 2;
	}

	/** Adds the edge from vertex {@code v} towards direction {@code dir}. */
	public void addEdge(int v, int dir) {
		edges.set(bit(v, dir));
		edges.set(bit(neighbor(v, dir), OPPOSITE[dir]));
	}

	/** Tells if the edge from vertex {@code v} towards direction {@code dir} exists. */
	public boolean hasEdge(int v, int dir) {
		return edges.get(bit(v, dir));
	}

	/**
	 * Returns the neighbor of vertex {@code v} towards direction {@code dir} or {@link #NO_VERTEX}.
	 */
	public int neighbor(int v, int dir) {
		int col = col(v) + X[dir], row = row(v) + Y[dir];
		return col >= 0 && col < numCols && row >= 0 && row < numRows ? vertex(col, row) : NO_VERTEX;
	}

	/** Returns a textual representation of this grid. */
	@Override
	public String toString() {
		StringBuilder sb = new StringBuilder();
		for (int row = 0; row < numRows; ++row) {
			for (int col = 0; col < numCols; ++col) {
				sb.append(String.format("[%2d,%2d]", row, col));
				sb.append(col < numCols - 1 && hasEdge(vertex(col, row), EAST) ? "--" : "  ");
			}
			if (row < numRows - 1) {
				sb.append("\n");
				for (int col = 0; col < numCols; ++col) {
					sb.append(hasEdge(vertex(col, row), SOUTH) ? "   |     " : "         ");
				}
				sb.append("\n");
			}
		}
		sb.append(String.format("\n\n[%d cols, %d rows, %d edges]\n", numCols, numRows, numEdges()));
		return sb.toString();
	}
}

C code example

The code below is an example of depth-first search maze generator in C.

//Code by Jacek Wieczorek

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>

typedef struct
{
	int x, y; //Node position - little waste of memory, but it allows faster generation
	void *parent; //Pointer to parent node
	char c; //Character to be displayed
	char dirs; //Directions that still haven't been explored
} Node;

Node *nodes; //Nodes array
int width, height; //Maze dimensions


int init( )
{
	int i, j;
	Node *n;
	
	//Allocate memory for maze
	nodes = calloc( width * height, sizeof( Node ) );
	if ( nodes == NULL ) return 1;
		
	//Setup crucial nodes
	for ( i = 0; i < width; i++ )
	{
		for ( j = 0; j < height; j++ )
		{
			n = nodes + i + j * width;
			if ( i * j % 2 ) 
			{
				n->x = i;
				n->y = j;
				n->dirs = 15; //Assume that all directions can be explored (4 youngest bits set)
				n->c = ' '; 
			}
			else n->c = '#'; //Add walls between nodes
		}
	}
	return 0;
}

Node *link( Node *n )
{
	//Connects node to random neighbor (if possible) and returns
	//address of next node that should be visited

	int x, y;
	char dir;
	Node *dest;
	
	//Nothing can be done if null pointer is given - return
	if ( n == NULL ) return NULL;
	
	//While there are directions still unexplored
	while ( n->dirs )
	{
		//Randomly pick one direction
		dir = ( 1 << ( rand( ) % 4 ) );
		
		//If it has already been explored - try again
		if ( ~n->dirs & dir ) continue;
		
		//Mark direction as explored
		n->dirs &= ~dir;
		
		//Depending on chosen direction
		switch ( dir )
		{
			//Check if it's possible to go right
			case 1:
				if ( n->x + 2 < width )
				{
					x = n->x + 2;
					y = n->y;
				}
				else continue;
				break;
			
			//Check if it's possible to go down
			case 2:
				if ( n->y + 2 < height )
				{
					x = n->x;
					y = n->y + 2;
				}
				else continue;
				break;
			
			//Check if it's possible to go left	
			case 4:
				if ( n->x - 2 >= 0 )
				{
					x = n->x - 2;
					y = n->y;
				}
				else continue;
				break;
			
			//Check if it's possible to go up
			case 8:
				if ( n->y - 2 >= 0 )
				{
					x = n->x;
					y = n->y - 2;
				}
				else continue;
				break;
		}
		
		//Get destination node into pointer (makes things a tiny bit faster)
		dest = nodes + x + y * width;
		
		//Make sure that destination node is not a wall
		if ( dest->c == ' ' )
		{
			//If destination is a linked node already - abort
			if ( dest->parent != NULL ) continue;
			
			//Otherwise, adopt node
			dest->parent = n;
			
			//Remove wall between nodes
			nodes[n->x + ( x - n->x ) / 2 + ( n->y + ( y - n->y ) / 2 ) * width].c = ' ';
			
			//Return address of the child node
			return dest;
		}
	}
	
	//If nothing more can be done here - return parent's address
	return n->parent;
}

void draw( )
{
	int i, j;

	//Outputs maze to terminal - nothing special
	for ( i = 0; i < height; i++ )
	{
		for ( j = 0; j < width; j++ )
		{
			printf( "%c", nodes[j + i * width].c );
		}
		printf( "\n" );
	}
}

int main( int argc, char **argv )
{
	Node *start, *last;

	//Check argument count
	if ( argc < 3 )
	{
		fprintf( stderr, "%s: please specify maze dimensions!\n", argv[0] );
		exit( 1 );
	}
	
	//Read maze dimensions from command line arguments
	if ( sscanf( argv[1], "%d", &width ) + sscanf( argv[2], "%d", &height ) < 2 )
	{
		fprintf( stderr, "%s: invalid maze size value!\n", argv[0] );
		exit( 1 );
	}

	//Allow only odd dimensions
	if ( !( width % 2 ) || !( height % 2 ) )
	{
		fprintf( stderr, "%s: dimensions must be odd!\n", argv[0] );
		exit( 1 );
	}
	
	//Do not allow negative dimensions
	if ( width <= 0 || height <= 0 )
	{
		fprintf( stderr, "%s: dimensions must be greater than 0!\n", argv[0] );
		exit( 1 );
	}

	//Seed random generator
	srand( time( NULL ) );
	
	//Initialize maze
	if ( init( ) )
	{
		fprintf( stderr, "%s: out of memory!\n", argv[0] );
		exit( 1 );
	}
	
	//Setup start node
	start = nodes + 1 + width;
	start->parent = start;
	last = start;
	
	//Connect nodes until start node is reached and can't be left
	while ( ( last = link( last ) ) != start );
	draw( );
}

See also

References

  1. ^ Wilson, David Bruce (May 22–24, 1996). "Generating random spanning trees more quickly than the cover time" (PDF). Proceedings of the Twenty-eighth Annual ACM Symposium on Theory of Computing. Symposium on Theory of Computing. Philadelphia: ACM. pp. 296–303. doi:10.1145/237814.237880. ISBN 0-89791-785-5.
  2. ^ Jamis Buck (29 December 2010). "Maze Generation: Eller's Algorithm".
  3. ^ Jamis Buck (3 February 2011). "Maze Generation: Sidewinder Algorithm".
  4. ^ a b c d e Nathaniel Johnston; et al. (21 August 2010). "Maze - LifeWiki". LifeWiki. Retrieved 1 March 2011. {{cite web}}: |author= has generic name (help); External link in |author= (help)

External links