Mazes (1)
by Mithrandir
As mentioned previously, I had an assignment which involved constructing a maze from code and letting a robot escape from it. This series won’t follow the second part, it will concentrate on maze specific topics. I will write about maze generation, maze drawing, maze counting and several other aspects. Maybe I’ll touch several properties of the mazes, possibly I’ll write about something like the moments in statistics. Will see where this series will end and what areas will be covered by it.
This post will speak about maze construction. After a few searches for good and valid algorithms, I arrived at Jamis‘ blog. From there, after reading the exhaustive listing of generation procedures, I selected one algorithm to implement in Haskell and to present here. I choose the Sidewinder both because it has a nice name and because it seemed easy to implement in Haskell (requiring little state information and only a few random numbers). It runs very quickly and can be generated on the fly if needed.
It has two disadvantages though: the top row is free of walls and it can be easily solved. If starting on the lower row simple walk until you find an opening to the north and go in that direction. Repeat until you finish. If starting on the top row, you’ll have to do a little backtracking but usually not a time-consuming one. Also, it doesn’t produce all of the possible mazes. Solutions to all these problems will come in following articles, hopefully.
Now, for the generation part, I tweaked the algorithm a little. Instead of deciding at each cell whether to carve east or not, I started by deciding how many cells to carve east before doing any work. This will be clearer soon, after seeing the code.
First, some type definitions used to make the code look cleaner
type Length = Int type Size = (Length, Length) type Point = Size {- The cardinal directions. -} data Cardinal = N | E | S | W deriving (Eq, Show, Read, Ord, Enum) {- A cell. The list contains the openings. -} newtype Cell = C [Cardinal] deriving (Show, Read) {- Simple type for maze. -} type Maze = Array Size Cell
We have to generate a maze, we will use a function which receives the size of the maze as input and outputs the maze. Since we need randomness, we will be working in the State monad working with a random generator as the state (that’s the State StdGen thing in the signature).
genMaze :: Size -> State StdGen Maze
Basically, the generation is split into two phases: one in which we generate the important points in the maze (ends for working sets and upwards openings) and one in which the actual building is done.
genMaze s@(sx, sy) = do (ews, ups) <- gMP s return $ build sx sy ews ups
The building process is very simple, it seems that it can be generalized to any other maze generation algorithm with only a few changes. However, this is not true, to use this process you’ll have to obtain a list of important points and build the maze from it. It won’t even work for on-the-fly generation. But this wasn’t the point at that time and is not really relevant now.
build :: Length -> Length -> [Point] -> [Point] -> Maze
We will construct an array of cells, each of them opened to the East and West first.
build sx sy ews ups = runSTArray $ do m <- newListArray ((1, 1), (sy, sx)) $ repeat $ C [E, W]
Then, using the first set of important points, the eastern openings of some cells will be closed. Using the same set, we will also close some of the western openings such that if a cell claims to be closed eastward the next cell will be closed westward.
mapM_ (blockCell m E) $ (sx, 1) : ews mapM_ (blockCell m W . first (+1)) $ filter (fst . first (/= sx)) ews
After blocking the western edges of each row we will have a set of rows, like in the following picture
mapM_ (blockCell m W . (\y -> (1, y))) [1 .. sy]
We’ll have to open some cells northwards and some southwards, just like we did with the East-West relationship. Then, we will have our maze.
mapM_ (openCell m N) ups mapM_ (openCell m S . second (subtract 1)) ups return m
Opening and closing cells is in fact modifying the contents of the array.
{- Block one cell from the maze, represented as an array. -} -- blockCell :: Data.Array.MArray Size Cell -> Cardinal -> Size -> m () blockCell m d (x, y) = do e <- readArray m (y, x) writeArray m (y, x) $ block e d {- Open one cell from the maze, represented as an array. -} -- openCell :: (MArray a Cell m) => a Size Cell -> Cardinal -> Size -> m () openCell m d (x, y) = do e <- readArray m (y, x) writeArray m (y, x) $ open e d {- Block a cell from one direction. -} block :: Cell -> Cardinal -> Cell block (C l) x = C $ filter (/= x) l {- Open a cell to one direction. -} open :: Cell -> Cardinal -> Cell open (C l) x = C $ if x `elem` l then l else x : l
The only remaining part is the generation of the important points, as given by the Sidewinder’s algorithm. Since the first row is special (no walls blocking eastward) we will skip it when generating the points. Only the other rows will be used to generate the important Row Points. After this, do a simple fold to construct the Maze Points used in the above build procedure.
gMP :: Size -> State StdGen ([Point], [Point]) gMP (sx, sy) = do points <- mapM (gRP 0 sx) [2..sy] return $ foldl (\(x, y) (a, b) -> (x ++ a, y ++ b)) ([], []) points
Generating the RowPoints is simple: generate two random numbers and return the points obtained from them. More exactly, construct a list of them. The numbers will represent the length of the current corridor and the offset at which the opening to the previous row will be carved.
gRP :: Coord -> Length -> Coord -> State StdGen ([Point], [Point]) gRP c sx y | sx <= 0 = return ([], []) | otherwise = do len <- state $ randomR (1, sx) up <- state $ randomR (1, len) (rx, ry) <- gRP (c + len) (sx - len) y return ((len + c, y):rx, (up + c, y):ry)
Basically, the above function is the only one which needs random numbers. However, since we want to be able to change the generation if another, more efficient, algorithm is found, the only exported function should be genMaze.
The entire code can be seen on Github.
Next time we will look at how to do the actual rendering of this maze in Haskell (the above pictures were generated using a modified version of what will be presented then). Afterwards, we’ll touch some more interesting topics related to mazes but I don’t want to spoil anything right now.




If you’re interested, the game I wrote and put on Hackage (http://hackage.haskell.org/package/MazesOfMonad) features random maze generation too. I use Prim’s algorithm if I remember well.
Nice, I’ll look into it. Thanks :)
If you like mazes, you can find many information about it on this site : http://www.astrolog.org/labyrnth/algrithm.htm
But maybe you already know this site…
Yup, I know this and I have used it as a documentation before dwelling into this series.
[...] today, I read this article by Mihai Maruseac about generating mazes in Haskell. It’s very interesting, but it turns [...]