Skip to main content

Project 7 - Maze Generation

Maze generation is a fascinating topic. There are many ways to generate mazes and each one of them generates mazes with its own unique properties.

In this blog post, I look some of the ways in which we can generate mazes. Before we can get into any of these algorithms, we must first figure out how our maze is modeled and stored inside our program. 

Modelling the maze

To get started, we can think of our maze a n by n grid of cells where a cell contains information about its index in the grid.



Note: The cell can also contain references to its neighbors. This is helpful when you want to solve the maze using state space search algorithms like Depth First Search, Breadth First Search, etc.

Next, we need to be able to store which cells are connected, i.e., which cells do not have a wall separating them. For this we use what is known as an adjacency matrix. The adjacency matrix is a m by m grid where m is the number of cells in our maze = n2. In the matrix the value at row i and column j represents whether or not the cell i and cell j are connected or not.




Here for example since A and C are connected, [A, C] and [C, A] are given the value 1.

This is essentially a graph data structure.

Now that we have a way to store our maze, we can start looking at some of the algorithms we can use to actually generate our mazes.

Generating the maze

We are going to look at the following algorithms:

  1. Randomized Depth First Search algorithm

  2. Randomized Kruskal's algorithm

  3. Randomized Prim's algorithm

1. Randomized Depth First Search

Stack<Cell> backtrackingPath; // A stack to facilitate backtracking

// Pick random starting cell from grid of cells
startingCell = getRandomCell(grid);

// Add starting cell to backtracking path
backtrackingPath.Push(startingCell);

while(backtrackingPath.Count != 0) {
    // Get the cell to explore from the stack
    currentCell = backtrackingPath.Peek();

    currentCell.visited = true;

    // Get the possible neighbors of the current cell
    neighbors = getNeighbors(currentCell);

    // If all the neighbors have been viewed then backtrack
    if (getVisitedNeighborsCount(neighbors) == 0)
        backtrackingPath.Pop();
    else {
        // Select a random neighbor from all non-null and un-visited cells
        neighbor = neighbors[random.Next(n)];

        // Add selected neighbor to stack for backtracking
        backtrackingPath.Push(neighbor);

        // Remove walls between neighbor
        setNeighbor(neighbor, currentCell);
    }
}

2. Randomized Kruskal's Algorithm

// A wall is essentially a pair of cells which are seperated
struct Wall { Cell c1; Cell c2; }

// To help us check for loops in maze
DisjointSet connectedCells;

List<Wall> walls; // List of all the walls

// Shuffle the order of the walls
shuffleWalls(walls);

foreach(wall in walls) {
    // Connect the cells divided by this wall 
    // if are not indirectly connected
    if (connectedCells.find(getIndex(wall.c1)) 
        != connectedCells.find(getIndex(wall.c2))) {
        setNeighbor(wall.c1, wall.c2);
        connectedCells.union(wall.c1, wall.c2);
    }
}

3. Randomized Prim's Algorithm

// A wall is essentially a pair of cells which are seperated
struct Wall { Cell c1; Cell c2; }

List<Wall> walls; // Empty list to store walls

// Pick random starting cell from grid of cells
startingCell = getRandomCell(grid);
startingCell.inMaze = true;

// Add the surrounding walls of the cell to the walls list
walls.Add(getSurroundingWalls(startingCell));

while(walls.Count != 0) {
    randomWall = removeRandomWall(walls);
    
    // Set as neighbors if only one of the cells seperated 
    // by the wall is in the maze
    if (randomWall.c1.inMaze ^ randomWall.c2.inMaze) {
        setNeighbor(randomWall.c1, randomWall.c2);
        
        // assign cell not in the maze to the variable toAdd
        if (randomWall.c1.inMaze) toAdd = randomWall.c2;
        else toAdd = randomWall.c1;
        
        // Add surrounding walls of the cell not in the maze to the list
        walls.Add(getSurroundingWalls(toAdd));
        
        toAdd.inMaze = true;
    }
}

As we can see, the mazes generated by Randomized Depth First Search have long corridors whereas the mazes generated by Randomized Prim's and Kruskal's Algorithms have short corridors.

Another properly of all these algorithms is that any two cells in the maze have only one connected path, i.e., to get from one point to another you only have a single path available. This is also known as a minimum spanning tree.

If we want multiple paths to be available, we can easily achieve by removing random walls until we are satisfied.

You can find the source code for this project in this GitHub repository.

If you have any questions or suggestion for future projects, feel free to leave a comment down below.

Comments

Popular posts from this blog

Project 6 - State Space Search - 8-Puzzle

State space search is a process which is used to create simple artificial intelligence. It can be used when the problem can be represented as a set of simple states and the player / agent is the only one who can affect the environment. It allows us to generate a path from the initial state to the goal state (of which there can be many depending on the problem). 8-Puzzle / Sliding Puzzle ,  N-queens  and  Route Finding  are some of the various problems which can be solved using state space search. Let us explore the process of solving the 8-Puzzle problem using various path finding algorithms and comparing how effective each of them is. To get started, we need to start by creating the 8-Puzzle game itself. First, we need a simple way to represent different states of the puzzle. A good way to do this is to store the state as a string. For example: Next up, we need a way to generate the actions that are possible from a particular state. We can  think of it as moving the empty space itself

Project 1 - Browser linked list implementation

All of us use a browser to surf through the internet. In fact you are using one right now. Have you ever wondered how the forward and backward buttons of the browser work ? Or how the undo and redo functions of your text editor works ? The answer to this is a doubly linked list. A linked list consists of various individual nodes which store some data as well as a pointer to the next node. A doubly linked list has nodes which have pointers to the previous node as well. In a circular linked list, the last node points to the first node. Here is how the application is going to work: Whenever you go to a new website, a new node is added in front of the current node. And the forward and backward button traverse through the linked list.  If you wish to tinker with the code, here is the Github Repository . You can easily expand this program to act like undo and redo buttons of a text editor. Linked list are used in various other practical applications such as photo viewers and music players. A

Project 3 - Analysis of sorting algorithms

Sorting algorithms are one of the most basic as well as one of the most used algorithms. They form the basis for many other data structures and algorithms and are also a great way to learn to analyse algorithms.  In this post, I would like to perform my own analysis of these sorting algorithms to understand where and why various sorting algorithms should be used. My main focus is going to be practical analysis of these sorting algorithms and I am also going to be considering the simplicity of these algorithms. So first, let us think about what basis we are going to use for these sorting algorithms. To analyse any sorting algorithm, let us measure the time it takes to sort an array of integers. The array of integers that we are going to give to the sorting algorithms should be of the following types: Random arrays. Ex: [5, 2, 9, 7, 0, 4]. Sorted arrays. Ex: [3, 5, 7, 8, 11]. Sorted arrays in reverse order. Ex: [14, 11, 7, 3, 1]. Sorted arrays with a few random elements added to the end.