Depth-First Search

Introduction

The maze generation algorithm that I’ll cover in this article is the depth-first search algorithm. This algorithm is described as one of the simplest ways to generate a maze with a computer on the wikipedia page for maze generation algorithms. It’s implemented with a stack, which facilitates ‘backtracking’, it’s much the same as the recursive backtracker algorithm, so I won’t go into details on that particular algorithm because I’ll cover a lot of it with this discussion.

The algorithm itself is very simple:

Generate a grid of cells, each cell has four walls Pick a random cell Select a random neighbouring cell that has not been visited yet Remove the wall separating the two cells Push the neighbouring cell to the stack Make the neighbouring cell the current cell and mark it as visited If there are no unvisited neighbouring cells Pop a cell off the stack and mark it as the current cell If there are neighbouring unvisited cells Go back to step 3 Go back to step 7 Else go back to step 3

In the list of steps above, I specified that each cell will have four walls, assuming that all four walls are the same length, this implies that I’ll be using a square grid. It’s important to note that there are other types of grids, and I’ll be covering two other types of grids in this series of articles about the depth-first search algorithm, I’m going to be discussing hexagonal grids and 3D grids. Although you could construct mazes out of just about any type of geometry, for example, you could generate a Voronoi diagram and use the same algorithm to generate a maze.

Here’s an example gif of the maze being generated on a grid composed of squares using my implementation. The green dots are an indication of a cell being marked as visited, and you can see the walls disappear as the algorithm moves through the maze. The orange dots that you see are indications of the maze reaching a dead end and backtracking until it finds another branch to take, as in the subroutine that is executed in step 7 of the algorithm. The red dot indicates the starting cell:

This shows that the algorithm works well, and, as you can see, I’ve put it onto a square grid. I essentially followed the exact steps listed above, first, generating a grid made up of cells with four walls each. Here’s how I specified it to start in JavaScript.

function DepthFirstSquareCell() { this.walls = ['up', 'down', 'left', 'right']; }

Now I needed to write a draw function. My logic was that I could use the walls array to check if it included a wall, and if it did, I’d draw the wall. So, using this logic, I began to write the basics of the function.

function DepthFirstSquareCell() { this.walls = ['up', 'down', 'left', 'right']; this.draw = function() { if(this.walls.includes('up')) { //draw top wall } if(this.walls.includes('down')) { //draw bottom wall } if(this.walls.includes('left')) { //draw left wall } if(this.walls.includes('right')) { //draw right wall } } }

The next thing that I needed to know was how I was going to draw the wall, and more importantly where I was going to draw the wall. For this I needed to add a couple more variables to my cell, an x and a y coordinate. I figured that I could pass these in as parameters when it came to generating the grid.

function DepthFirstSquareCell(x, y) { this.x = x; this.y = y; ... }

And now, for each conditional, I could use the p5 library to draw each wall.

I decided that each cell should be 20px in width and height, meaning that they were quite small, but not too small to see. I used the ‘line’ function provided by p5, which made it a very easy task to draw the lines, I just needed to do a bit of math around the coordinates.

If each cell was 20px by 20px, that would mean that the x and y coordinates would need to be multiplied by 20 to be put into the right place, then I’d need to stretch each edge 20 pixels in the right direction to make it look correct. Let’s look at this on a smaller scale.

The origin of the cell starts in the top-left corner at 0, 0. To draw the top line, we start at the origin and stretch 20 pixels along the x axis. So, assuming that x = 0 and y = 0, we can say that the top line can be draw from (x, y) to (x + 20, y) and the y coordinate stays the same. If we repeat this for each other line:

top: (x, y) -> (x + 20, y) left: (x, y) -> (x, y + 20) right: (x + 20, y) -> (x + 20, y + 20) bottom: (x, y + 20) -> (x + 20, y + 20)

Drawing four lines using these values will give us a square with each edge measuring 20 pixels. I also mentioned earlier that the x and y coordinates should be multiplied by 20 to place the cells correctly, if we were to place all of the cells without multiplying the coordinates they would overlap. So, now that we’ve figured out our spacing, we can finish the implementation of our draw function.

if(this.walls.includes('up')) { //draw top wall line(x * 20, y * 20, (x * 20) + 20, y); } if(this.walls.includes('down')) { //draw bottom wall line(x * 20, (y * 20) + 20, (x * 20) + 20, (y * 20) + 20); } if(this.walls.includes('left')) { //draw left wall line(x * 20, y * 20, x * 20, (y * 20) + 20); } if(this.walls.includes('right')) { //draw right wall line((x * 20) + 20, y * 20, (x * 20) + 20, (y * 20) + 20); }

The final thing that we need to know to implement the algorithm is whether the cell has been visited or not. I simply added a boolean value into the cell’s properties so that we could set it as needed and query it when searching for unvisited neighbours.

this.visited = false;

Now that we’ve got everything to do with our cell implemented, we need to look at what to do next. We’ll need a grid to operate on, so let’s look at that. I decided to implement a pretty basic data structure that held a two dimensional array, and a function to find unvisited neighbours for any given cell in that array.

function DepthFirstGrid(width, height) { this.data = []; for(var x = 0; x < width; x++) { data[x] = []; for(var y = 0; y < height; y++) { data[x][y] = new DepthFirstSquareCell(x, y); } } this.findUnvisitedNeighbours = function(cell) { return [ cell.x > 0 ? this.data[cell.x - 1][cell.y] : continue, cell.y > 0 ? this.data[cell.x][cell.y - 1] : null, cell.x < this.data.length - 1 ? this.data[cell.x + 1][cell.y] : null, cell.y < this.data[0].length - 1 ? this.data[cell.x][cell.y + 1] : null ].filter( function(item) { return item != null && !item.visited; } ); } }

The first part of the code in there is very straightforward, we simply specify a two dimensional array and then populate it with our cells. The findUnvisitedNeighbours function may look scary, but let’s just break it down, because I’ve used a lot of syntactic sugar in there to make the code a bit more concise.

Essentially, we need to look at what we want to put into the function and what we want to get out. The input is an arbitrary cell, and the output that we want is an array of the cells that are adjacent to it with the condition that they are not visited. We need to take into account a few things here.

First, we need to check if a cell is on an edge, if it is, we can’t return neighbours for that side of the cell. Let’s say, for example, we’re looking for neighbours of the top left cell. The x value would be 0, and the y value would be 0. If we tried to get the neighbour to the left we would have to access index -1 of the grid, however, no such index exists, and the code would throw up an error telling us that we can’t get any data from there because firstly, no such index exists, and secondly, even if it did, there wouldn’t be any data to return. The same would happen on the y coordinate. So, we need to put conditionals in to make sure that we don’t attempt to access any indices of the array that don’t exist. Now that we know what our checks will be, we can plug that into the array that we return with the neighbours, and we simply say, if the cell is not on an edge, give us the neighbour, but if it is on an edge, just put a null value into it’s place so we know that there is no neighbour there.

Now we have an array with neighbours in it, and we simply want to filter the array down so that we can get rid of the null values left in the array, and in the same line, also specify a conditional that removes any neighbours that have been visited from the array. The final result will be an array that has a list of neighbours for a given cell that have not been visited.

I’ve written some more simplified and self-explanatory code below that does the same thing.

this.findUnvisitedNeighbours(cell) { var neighbours = []; //is the cell on the left-hand edge of the grid? if(cell.x > 0) { neighbours.push(this.data[cell.x - 1][cell.y]); } if(cell.y > 0) { neighbours.push(this.data[cell.x][cell.y - 1]); } if(cell.x < this.data.length - 1) { neighbours.push(this.data[cell.x + 1][cell.y]); } if(cell.y < this.data[0].length - 1) { neighbours.push(this.data[cell.x][cell.y + 1]); } for(var x = 0; x < neighbours.length; x++) { var unvisitedNeighbours = []; if(!neighbours[x].visited) { unvisitedNeighbours.push(neighbours[x]); } return unvisitedNeighbours; } }

Both sets of code do virtually the same thing (although the second one doesn’t put null values into the array, which means that you might see a slight optimisation on the second method in terms of speed, although it’d be negligible in small examples like the one I’m showing you, although with scale it might shave a bit of time off).

Now that I’ve implemented the code to find unvisited neighbours, I’ve only got one thing left to do that isn’t absolutely necessary before diving into the meat of the algorithm, but I’d like to do just to make things a bit neater, and that’s implementing a function to remove a wall from a cell. It’s a one-liner but it saves a lot of typing later.

this.removeWall = function(wall) { this.walls.splice(this.walls.indexOf(wall), 1); }

Now we can really focus on using all of these data structures and functions that we’ve built out to implement the algorithm.

Here’s the final commented version of the code:

var grid; var currentCell; //this is used for backtracking var stack; //helper to get a random value between limits function randRange(min, max) { return Math.floor(Math.random() * (max - min)) + min; } //p5 setup function, get things ready function setup() { createCanvas(400, 400); grid = new DepthFirstGrid(20, 20); //get random starting cell var startX = randRange(0, grid.data.length - 1); var startY = randRange(0, grid.data.length - 1); currentCell = grid.data[startX][startY]; currentCell.visited = true; } function draw() { background(85); //Draw the grid for(var x = 0; x < grid.data.length; x++) { for(var y = 0; y < grid.data[x].length; y++) { grid.data[x][y].draw(); } } //check that we've got a cell //if we haven't the maze is complete if(currentCell) { var unvisitedNeighbours = grid.findUnvisitedNeighbours(currentCell); //if there are any unvisited neighbours if(unvisitedNeighbours.length > 0) { var neighbourCell = unvisitedNeighbours[randRange(0, unvisitedNeighbours.length)]; //if the neighbour cell is to the left of the current cell if(currentCell.x > neighbourCell.x) { currentCell.removeWall('left'); neighbourCell.removeWall('right'); } //if the neighbour cell is to the right of the current cell if(currentCell.x < neighbourCell.x) { currentCell.removeWall('right'); neighbourCell.removeWall('left'); } //if the neighbour cell is above the current cell if(currentCell.y > neighbourCell.y) { currentCell.removeWall('up'); neighbourCell.removeWall('down'); } //if the neighbour cell is below the current cell if(currentCell.y < neighbourCell.y) { currentCell.removeWall('down'); neighbourCell.removeWall('up') } //push the neighbourCell to the stack stack.push(neighbourCell) neighbourCell = currentCell; currentCell.visited = true; //otherwise, backtrack } else { /* grab the top cell on the stack ready for the next loop if the top cell on the stack doesn't have any other neighbours it'll just call this line again on the next loop */ currentCell = stack.pop(); } } else { //erase all the data and start again //if you want to keep your maze you can omit this part of the conditional setup(); } }

That’s pretty much it for square grids, in the next part of the article I’ll be discussing the implementation on a hexagonal grid and the things that I had to do differently, and in part 3 where I’ll be discussing my 3D implementation. Here’s another gif of a much bigger example of the maze being generated.

I have also got a page where you can check out the working examples (and full code) for each algorithm as and when I complete their implementations.