In the previous part of this series on maze generation, I explained how one can generate perfect mazes using the “recursive backtracking algorithm”. In this part, I will introduce two additional maze generating algorithms. The first is a randomized version of the “Kruskal algorithm” devoloped by Joseph Kuruskal. The second is a randomized version of the “Modified Dijkstra-Jarník-Prim (DJP) algorithm” (or simply “Prim’s algorithm”) discovered by Vojtěch Jarník and later re-discovered by Robert C. Prim and Edsger W. Dijkstra.
Unlike the recursive backtracking algorithm, the Kruskal algorithm doesn’t mark any cells as visited or unvisited. Instead, it assigns each cell a unique ID. The algorithm works as follows:
# create maze (all walls) of size n
n <- 10
maze <- vector("list", 0)
for (i in 1:n) {
for (j in 1:n) {
cell <- list(
i = i,
j = j,
id = paste(i, j, sep = ","),
north = TRUE,
east = TRUE,
south = TRUE,
west = TRUE)
maze[[length(maze)+1]] <- cell
}
}
rm(i,j,cell)
# function to show maze
showMaze <- function(maze) {
# background
par(mar = rep(1,4), bg = "burlywood4", bty = "n")
plot(rep(0,n+1) ~ c(0:n), type = "l", asp = 1,
ylim = c(0,n), xlim = c(0,n),
yaxt = "n", xaxt = "n",
ylab = "", xlab = "")
# walls
for (idx in 1:n^2) {
i <- maze[[idx]]$i
j <- maze[[idx]]$j
if (maze[[idx]]$north) lines(rep(i,2) ~ c(j-1,j), col="white", lwd=4)
if (maze[[idx]]$south) lines(rep(i-1,2) ~ c(j-1,j), col="white", lwd=4)
if (maze[[idx]]$east) lines(c(i-1,i) ~ rep(j,2), col="white", lwd=4)
if (maze[[idx]]$west) lines(c(i-1,i) ~ rep(j-1,2), col="white", lwd=4)
}
}
# function to find a neighbor
findNeighbor <- function(idx) {
# select cell at position "idx"
cell <- maze[[idx]]
# determine rows and columns of all cells in the maze
rows <- sapply(maze, FUN = function(cell) cell$i)
cols <- sapply(maze, FUN = function(cell) cell$j)
# determine neighbors
above <- which(rows == (cell$i + 1) & cols == cell$j)
below <- which(rows == (cell$i - 1) & cols == cell$j)
left <- which(cols == (cell$j - 1) & rows == cell$i)
right <- which(cols == (cell$j + 1) & rows == cell$i)
neighbors <- c(above, below, left, right)
# return
if (length(neighbors) == 0) { # no neighbors
return(NA)
} else if (length(neighbors) == 1) { # exactly one neighbor
return(neighbors)
} else { # multiple neighbors
return(sample(neighbors,1))
}
}
# function to remove walls
removeWalls <- function(maze, a, b) {
# a over b
if (maze[[a]]$i == maze[[b]]$i + 1 & maze[[a]]$j == maze[[b]]$j) {
maze[[a]]$south <- FALSE
maze[[b]]$north <- FALSE
}
# a under b
if (maze[[a]]$i == maze[[b]]$i - 1 & maze[[a]]$j == maze[[b]]$j) {
maze[[a]]$north <- FALSE
maze[[b]]$south <- FALSE
}
# a left of b
if (maze[[a]]$j == maze[[b]]$j - 1 & maze[[a]]$i == maze[[b]]$i) {
maze[[a]]$east <- FALSE
maze[[b]]$west <- FALSE
}
# a right of b
if (maze[[a]]$j == maze[[b]]$j + 1 & maze[[a]]$i == maze[[b]]$i) {
maze[[a]]$west <- FALSE
maze[[b]]$east <- FALSE
}
# returns
return(maze)
}
# function to retrieve IDs of all cells
getID <- function(maze) sapply(maze, FUN = function(cell) cell$id)
# setup
set.seed(1234)
# create maze using Kruskal's algorithm
while (length(unique(getID(maze))) > 1) {
# select a random cell and find its neighbor
current <- sample(1:n^2, 1)
neighbor <- findNeighbor(current)
# check if the IDs of the two cells are different
# if so, remove the wall between them and make the ID of all cells on the
# "second side" of the wall equal to the ID of the cell on the "first side"
id_1 <- maze[[current]]$id
id_2 <- maze[[neighbor]]$id
if (id_1 != id_2) {
maze <- removeWalls(maze, current, neighbor)
reset_id <- which(getID(maze) == id_2)
for (idx in reset_id) maze[[idx]]$id <- id_1
}
# # visualize progress
# showMaze(maze)
# Sys.sleep(0.5)
}
# show final result
showMaze(maze)
The DJP algorithm distinguishes cells depending on their “state”. The algorithm starts by marking one cell as “in”. All other cells of the maze are marked as “out”. A third state called “frontier” describes cells that are next to one of the cells marked “in”. The algorithm works as follows:
# create maze (all walls) of size n
n <- 10
maze <- vector("list", 0)
for (i in 1:n) {
for (j in 1:n) {
cell <- list(
i = i,
j = j,
state = "out",
north = TRUE,
east = TRUE,
south = TRUE,
west = TRUE)
maze[[length(maze)+1]] <- cell
}
}
rm(i,j,cell)
# function to show maze
showMaze <- function(maze) {
# background
par(mar = rep(1,4), bg = "burlywood4", bty = "n")
plot(rep(0,n+1) ~ c(0:n), type = "l", asp = 1,
ylim = c(0,n), xlim = c(0,n),
yaxt = "n", xaxt = "n",
ylab = "", xlab = "")
# shade areas
for (idx in 1:n^2) {
i <- maze[[idx]]$i
j <- maze[[idx]]$j
if (maze[[idx]]$state == "in") rect(j-1, i-1, j, i, col = "deepskyblue3", lty = 0)
if (maze[[idx]]$state == "frontier") rect(j-1, i-1, j, i, col = "orange", lty = 0)
}
# walls
for (idx in 1:n^2) {
i <- maze[[idx]]$i
j <- maze[[idx]]$j
if (maze[[idx]]$north) lines(rep(i,2) ~ c(j-1,j), col="white", lwd=4)
if (maze[[idx]]$south) lines(rep(i-1,2) ~ c(j-1,j), col="white", lwd=4)
if (maze[[idx]]$east) lines(c(i-1,i) ~ rep(j,2), col="white", lwd=4)
if (maze[[idx]]$west) lines(c(i-1,i) ~ rep(j-1,2), col="white", lwd=4)
}
}
# function to find a neighbor
findNeighbors <- function(idx, type) {
cell <- maze[[idx]]
rows <- sapply(maze, FUN = function(cell) cell$i)
cols <- sapply(maze, FUN = function(cell) cell$j)
above <- which(rows == (cell$i + 1) & cols == cell$j)
below <- which(rows == (cell$i - 1) & cols == cell$j)
left <- which(cols == (cell$j - 1) & rows == cell$i)
right <- which(cols == (cell$j + 1) & rows == cell$i)
candidates <- c(above, below, left, right)
states <- sapply(maze[candidates], FUN = function(cell) cell$state)
if (type == "out") neighbors <- candidates[which(states == "out")]
if (type == "in") neighbors <- candidates[which(states == "in")]
if (length(neighbors) == 0) {
return(NA)
} else {
return(neighbors)
}
}
# function to remove walls
removeWalls <- function(maze, a, b) {
# a over b
if (maze[[a]]$i == maze[[b]]$i + 1 & maze[[a]]$j == maze[[b]]$j) {
maze[[a]]$south <- FALSE
maze[[b]]$north <- FALSE
}
# a under b
if (maze[[a]]$i == maze[[b]]$i - 1 & maze[[a]]$j == maze[[b]]$j) {
maze[[a]]$north <- FALSE
maze[[b]]$south <- FALSE
}
# a left of b
if (maze[[a]]$j == maze[[b]]$j - 1 & maze[[a]]$i == maze[[b]]$i) {
maze[[a]]$east <- FALSE
maze[[b]]$west <- FALSE
}
# a right of b
if (maze[[a]]$j == maze[[b]]$j + 1 & maze[[a]]$i == maze[[b]]$i) {
maze[[a]]$west <- FALSE
maze[[b]]$east <- FALSE
}
# returns
return(maze)
}
# setup
set.seed(1234)
current <- 1
maze[[current]]$state <- "in"
neighbors <- findNeighbors(current, "out") # find neighbors that are "out"
for (idx in neighbors) maze[[idx]]$state <- "frontier"
frontier <- which(sapply(maze, FUN = function(cell) cell$state) == "frontier")
# create maze using modified DJP algorithm
while (length(frontier) > 0) {
# select a random frontier cell as new current cell and carve in
current <- ifelse(length(frontier)==1, frontier, sample(frontier, 1))
maze[[current]]$state <- "in"
neighbors <- findNeighbors(current, "in")
neighbor <- ifelse(length(neighbors)==1, neighbors, sample(neighbors, 1))
maze <- removeWalls(maze, neighbor, current)
# mark new frontier
neighbors <- findNeighbors(current, "out") # find neighbors that are out
if (!any(is.na(neighbors))) {
for (idx in neighbors) maze[[idx]]$state <- "frontier"
}
frontier <- which(sapply(maze, FUN = function(cell) cell$state) == "frontier")
# visualize progress
showMaze(maze)
if (length(current) > 0 ) {
i <- maze[[current]]$i
j <- maze[[current]]$j
points(x = j - 0.5, i - 0.5, col = "maroon", pch = 16, cex = 2)
}
Sys.sleep(0.5)
}
# show final result
showMaze(maze)
# show final result
showMaze(maze)