Foreword
Writing an article led me to the almost complete lack of materials in Russian about labyrinth generation algorithms. On Habré, from what is generally on the topic, two articles can be noted:
one and
two . The value and benefits of which is only the second. In the first, just a translation of the formal algorithm and a small explanation of it. Which, of course, not bad, but very poorly and does not cause the desire to study the topic further.
If you like my article, I will continue to write about various algorithms. We will look at two of the most primitive and simple cases - the generation of a binary tree and Sidewinder, which, in its essence, is just a slightly modified version of a binary tree with one noticeable plus.
CAUTION TRAFFIC .
')
I will give one piece of advice - do not peek into the code until you write your implementation. You will get much more pleasure and benefit from fixing bugs and finding errors than if you simply translate from one language to another.
Seriously. Listen to the advice. You, truly, will spend more time, but it costs. For example, because of a couple of errors, I had a very funny generator of “alien” texts, which can be used in various Sci-Fi games to create text. I hope you study the topic for yourself and do not hurry anywhere.
PS:
I will use the term "
bias ", assuming English bias. Those. addiction of the algorithm to directivity in any direction. For example, the right offset - the algorithm generates mazes with long right passes.
The coloring of the labyrinths occurs relative to the distance from the extreme left corner of the field to a certain cell. The farther from the initial coordinate - the darker the color will be.
An ideal maze is a maze in which one cell is connected to another by one single path. In other words, spanning tree.
Pro luaWhen I first began to dig in the topic of labyrinths, I didn’t assume that I’ll end up writing an article and chose a language in which I have the opportunity not to spend too much time on rendering and architecture and only do logic. As a result, between C ++ and Lua, the choice fell on Lua and Love2D.
Do not worry if you are unfamiliar with Lua. Ignorance of the language does not hurt you to understand the implementation, thanks to the simplicity of the syntax. If you are able to program at least a little, 80% of the code will not cause you problems with understanding. The remaining 20% are language specific, about which I will always write articles at the beginning, explaining their work.
The first thing I should say is:
Lua has only one data structure — tables — an associative array, with which we create the structures we need. For example, classes, sets, ordinary arrays, stacks, queues, and the like. We can access them either with the help of small-keys, or with the help of indexes.
Also, the tables do not limit us in storing only one type of data in one object and work like structures in C / C ++. This code is absolutely correct:
ourStructure = {} ourStructure[“BeautyKey”] = 42 ourStructure[42] = “UltimateAnswer” ourStructure[1] = false
Assigning nil will remove the field:
ourStructure[42] = nil
Second:
Lua does not have a hidden table copying mechanism. The code below will not copy and create a new SomeNewArray object, it only copies the link to the SomeArray object into it, and, therefore, will change it just as if you pass the value through a non-constant link or pointer in C / C ++:
someArray = {} someArray[1] = 42 someArray[2] = “ReferencesTest” someNewArray = someArray someNewArray[1] = “42 Is Gone, Now Only the Garbage-String here”
And third, for those who are well acquainted with Lua:
Yes, I know that in some places the code is redundant. And the fact that in some places everything could be simplified by metamethods, too. It should be borne in mind that the code was written primarily to deal with the algorithms, and not for use in a real project. In addition, the absence of an excess of language-specific functions allows you to lay out the code in the form in which it is, without a pile of comments.
Binary tree algorithm
Description :
The very first and easiest algorithm in the understanding, which I will consider. Its essence lies in paving the way in a random direction from each cell of the field: in my implementation, either upward or to the right (depending on the offset chosen by you). We process only 1 cell per unit of time, therefore, we can generate mazes of infinite size, keeping only the final result (maze) without the need to store any side information.
This generation method has two side effects:
1. Labyrinths have a strong diagonal displacement and no dead ends in its direction. Look at the screenshots above and you will see that each of the corridors tends to the right upper cell, and, as a result, it has exactly one path to it, and there is no dead end anywhere on the path:
2. Two empty corridors on the sides of the maze. When the algorithm “digs through” to the end of the row / column, it has no choice but to continue the path in one single direction, creating empty “borders”.
By the way, the name does not just coincide with the data structure. The result of his work is a random binary tree, in which from each cell (vertex) there is exactly 1 path towards the root (parent vertex), and, accordingly, exactly 1 path to any other cell. As a result, any cell has no more than 3 connections with its neighbors.
Formal algorithm (for northeast offset):
- Select the starting cell;
- Choose a random direction for paving the way. If a neighboring cell in this direction extends beyond the field, dig a cage in the only possible direction;
- Go to the next cell;
- Repeat 2-3 until all cells are processed;
Work exampleGreen - the current cell under consideration, red - the front, the cells into which you can move.
We start with the coordinate (0; 0). We cannot go up in this row, because otherwise we will go beyond the boundaries of the maze. We go to the right until it stops, on the way tearing down all the walls.
Everything is a dead end. Nowhere to go. Move to the next row and see that now there is an opportunity to go up and to the right.
We throw a coin and choose ... Top. We remove the wall and proceed to the next cell.
Fine. Case prompts us to go to the right. Remove the wall and move to the next cell.
We have no choice, we cannot go to the left, which means we remove the wall from above and go to the next row.
The coin convinces us to go to the right. Well, obey. We remove the wall and proceed to the next cell.
A meter rolled, our unfortunate piece of metal falls and says it's time to go upstairs. We demolish the wall, step to the next cell, and, since it is extreme in this row, remove the wall from above. Labyrinth is over.
Pros :
- Simple implementation;
- High speed;
- The ability to generate endless labyrinths;
Cons :
- Low complexity of the pattern;
- Strong offset diagonally;
- No deadlocks on the offset;
- The monotony of the generated mazes;
Implementation local mod = {} local aux = {} aux.width = false aux.height = false aux.sx = false aux.sy = false aux.grid = false function aux.createGrid (rows, columns) local MazeGrid = {} for y = 1, rows do MazeGrid[y] = {} for x = 1, columns do MazeGrid[y][x] = {bottom_wall = true, right_wall = true}
Algorithm "Sidewinder"
Description :
The algorithm with the untranslatable name Sidewinder is very similar in its work to the binary tree algorithm, in that there is no characteristic bias along the diagonal, one empty corridor and a cell, we consider not individually, but sets. Labyrinths are obtained with a predominantly vertical or horizontal displacement (depending on the implementation), with no dead ends in their direction. Compared with its more primitive fellow, the displacement is not so noticeable and more like a “spiral” that smoothly replaces vertical and horizontal corridors.
As for side effects, Sidewinder creates only one empty corridor on one side, instead of two. Starting the creation of sets from the first row of the field, we do not have the opportunity to dig up the path, since we are in the most extreme vertical position and trying to go higher will result in going beyond the field boundaries. But even if we organize sets without going vertically, we will create several regions isolated from each other.
For example: 9 cells of the first row can be divided into three sets, between which walls are located. Each set of the second row will dig through the path to one of the three “blocks” above. The third row will pave the way for the "blocks" of the second. And so on until the end of the field. As a result, we will have 3 branched, isolated from each other vertical areas, which is not suitable for a perfect maze, in which from each point of the field there is exactly 1 way to any other.
Formal algorithm (for standard offset):
- Select the starting row;
- Select the starting cell of the row and make it current;
- Initialize empty set;
- Add the current cell to the set;
- Decide whether to pave the right;
- If laid, then go to the new cell and make it current. Repeat steps 3-6;
- If not paved, choose a random cell of the set and pave the way up from there. Go to the next row and repeat 2-7;
- Continue until each row has been processed;
Work exampleRed cells are members of the set.
We start from the first row and see that above us is going beyond the field. We demolish all the walls and go immediately to the second row, create an empty set.
So, and here it is more interesting. Let's add to the set the first two cells of the row.
Select one of these cells and remove the top wall belonging to it in the first row.
Reset the set, add the next cell of the row to it.
This time we don’t unite with anyone, we just make our way up from this single cell.
We repeat our actions. Reset the set, go to the next cell, add it ... And since it remains the last in a row, we also remove the wall from the top and go to the row below.
And now we immediately combine the first three cells into one set.
Randomly select a cell, in our case, the second one and remove the wall from the top to the previous row.
Well, here we again have no choice, remove the wall above and go to the row below.
This time, we will make the first cell the only one. We remove the wall to the previous row and go further into the next cell.
Suppose you want at the end to combine the three cells.
And again we liked the middle cell from the set, from which we remove the wall to the top. Everything, our labyrinth is ready.

Pros :
- The ability to generate endless labyrinths;
- Only 1 empty corridor;
- A more complex picture, unlike the binary tree algorithm;
Cons :
- More confusing implementation;
- No deadlocks on the offset;
- Strong vertical displacement;
Implementation local mod = {} local aux = {} aux.width = false aux.height = false aux.sx = false aux.sy = false aux.grid = false function aux.createGrid (rows, columns) local MazeGrid = {} for y = 1, rows do MazeGrid[y] = {} for x = 1, columns do MazeGrid[y][x] = {bottom_wall = true, right_wall = true} end end return MazeGrid end function mod.createMaze(x1, y1, x2, y2, grid) aux.height, aux.width, aux.sx, aux.sy = y2, x2, x1, y1 aux.grid = grid or aux.createGrid(y2, x2) aux.sidewinder() return aux.grid end function aux.sidewinder() local cx = aux.sx for y = aux.sy, aux.height do for x = aux.sx, aux.width do if y ~= aux.sy then if math.random(0, 1) == 0 and x ~= aux.width then aux.grid[y][x].right_wall = false else aux.grid[y-1][math.random(cx, x)].bottom_wall = false if x ~= aux.width then cx = x+1 else cx = aux.sx end end else if x ~= aux.width then aux.grid[y][x].right_wall = false end end end end end return mod
Epilogue
I hope you liked the article and you learned new knowledge about the primitive procedural generation of labyrinths. I chose the two most simple ones in the implementation and operation of the algorithm, so that it would be easier for newbies to “touch” the topic and see if they want to study it further. It is important for me to know whether people like Habrahabr are interested in such articles and whether to continue writing them. For readers, I still have at least 9 classic algorithms worth considering. Some of them are random wanderings around the field, such as, for example, the Prima or Wilson algorithm, some require more resources for work, as they work with graphs, for example, Eller and Kruskal, and some maintain a middle ground. But this is not the end - I have such things in my sleeve as: polar (round) labyrinths, generation of labyrinths on different grids (hexes, triangles, etc.), masking labyrinths in inscriptions and forms, 2.5D labyrinths and 3D, theory labyrinths, statistical comparison of algorithms, combined generation algorithms. In the end, we still have a huge variety of variations of the types of labyrinths. For example, now we are considering ideal algorithms, in which from each point there is exactly one path to any other. But there are also those that allow one cell to have several paths for any other! For example, Quake, Doom, and other shooters in the emerging genre used such algorithms to generate levels, according to some rumors.
Therefore, if you liked the article, the topic, and you want to see them further - then please write about it in the comments. Also, I will be very happy to any criticism, both technically and in linguistic and stylistic.