Генерация лабиринтов: алгоритм Эллера
Вступление
Как оказалось, что тема генерации лабиринтов не сильно раскрывается в русско- и англоязычном сообществе. На хабре существует одна статья Алгоритм Эллера для генерации лабиринтов. Статья, является переводом англоязычной статьи с описанием алгоритма по шагам. В своей реализации, я опирался на алгоритм из статьи. В процессе я столкнулся с трудностями и недопонимаем алгоритма. Поэтому я решил подробно разобрать алгоритм Эллера и разъяснить некоторые моменты с особенными случаями.
Базовые понятия
Идеальный лабиринт — это лабиринт в котором нет циклов (между двумя ячейками есть только один путь) и изолированных частей (ячейки или групп ячеек, которые не связаны с другими частями лабиринта).
Множество — это не более чем неупорядоченная коллекция уникальных элементов.
Описание алгоритма
Создайте первую строку. Ни одна ячейка не будет являться частью ни одного множества.
Присвойте ячейкам, не входящим в множество, свое уникальное множество.
Создайте правые границы, двигаясь слева направо:
Случайно решите добавлять границу или нет
Если текущая ячейка и ячейка справа принадлежат одному множеству, то создайте границу между ними (для предотвращения зацикливаний)
Если вы решили не добавлять границу, то объедините два множества в которых находится текущая ячейка и ячейка справа.
Создайте границы снизу, двигаясь слева направо:
Решите, будете ли вы дальше добавлять строки или хотите закончить лабиринт
Если вы хотите добавить еще одну строку, то:
Выведите текущую строку
Удалите все правые границы
Удалите ячейки с нижней границей из их множества
Удалите все нижние границы
Продолжайте с шага 2
Если вы решите закончить лабиринт, то:
Добавьте нижнюю границу к каждой ячейке
Двигаясь слева направо:
Если текущая ячейка и ячейка справа члены разных множеств, то:
Удалите правую границу
Объедините множества текущей ячейки и ячейки справа
Выведите завершающую строку
Реализация алгоритма по шагам
Описание лабиринта:
Лабиринт может храниться в файле в виде количества строк и столбцов, а также двух матриц, содержащих положение вертикальных и горизонтальных стен соответственно. В первой матрице отображается наличие стены справа от каждой ячейки, а во второй — снизу.
4 4
0 0 0 1
1 0 1 1
0 1 0 1
0 0 0 1
1 0 1 0
0 0 1 0
1 1 0 1
1 1 1 1
Пример лабиринта из файлаМетод генерации лабиринта
/* Метод генерации лабиринта */
void Maze::generateMaze() {
/* Шаг 1 */
fillEmptyValue();
for (int j = 0; j < rows_ - 1; j++) {
/* Шаг 2 */
assignUniqueSet();
/* Шаг 3 */
addingVerticalWalls(j);
/* Шаг 4 */
addingHorizontalWalls(j);
checkedHorizontalWalls(j);
/* Шаг 5.1*/
preparatingNewLine(j);
}
/* Шаг 5.2 */
addingEndLine();
}
Шаг 1:
Создайте первую строку. Ни одна ячейка не будет являться частью ни одного множества.
Создаем первую строкуРеализация шага 1
/* Заполним вектор пустым значением */
void Maze::fillEmptyValue() {
for (int i = 0; i < cols_; i++) {
sideLine_.push_back(kEmpty);
}
}
Что такое kEmpty?
constexpr int kEmpty = 0;
Шаг 2
Присвойте ячейкам, не входящим в множество, свое уникальное множество.
Присваиваем ячейкам уникальное множествоРеализация шага 2
/* Присваиваем ячейкам свое уникальное множество */
void Maze::assignUniqueSet() {
for (int i = 0; i < cols_; i++) {
/* Проверяем на пустую ячейку */
if (sideLine_[i] == kEmpty) {
/* Присваиваем ячейки уникальное множество */
sideLine_[i] = counter_;
counter_++;
}
}
}
Что такое counter_?
counter_ — это счетчик генерации уникальных множеств.
Шаг 3
Создайте правые границы, двигаясь слева направо:
— Случайно решите добавлять границу или нет:
1. Если текущая ячейка и ячейка справа принадлежат одному множеству, то создайте границу между ними (для предотвращения зацикливаний)
2. Если вы решили не добавлять границу, то объедините два множества в которых находится текущая ячейка и ячейка справа.
Что такое номер?
Под номером я понимаю расположение ячейки в лабиринте. Не стоит путать это с множеством
Ставим правую стенку у ячейки под номером 2Объединяем два множества в котором находится текущая ячейка и ячейка справаСтавим правую стенку у ячейки под номером 3Объединяем два множества в котором находится текущая ячейка и ячейка справаРеализация шага 3
/* Добавление правой вертикальной стенки */
void Maze::addingVerticalWalls(int row) {
for (int i = 0; i < cols_ - 1; i++) {
/* Ставим стенку или нет */
bool choise = randomBool();
/* Проверка условия для предотовращения зацикливания */
if (choise == true || sideLine_[i] == sideLine_[i + 1]) {
v_walls_(row, i) = true;
} else {
/* Объединение ячеек в одно множество */
mergeSet(i, sideLine_[i]);
}
}
/* Добавление правой стенки в последней ячейки */
v_walls_(row, cols_ - 1) = true;
}
/* Объединение ячеек в одно множество */
void Maze::mergeSet(int index, int element) {
int mutableSet = sideLine_[index + 1];
for (int j = 0; j < cols_; j++) {
/* Проверка ячеек на одно множество */
if (sideLine_[j] == mutableSet) {
/* Объединение ячейки в множество */
sideLine_[j] = element;
}
}
}
Шаг 4
Создайте границы снизу, двигаясь слева направо:
— Случайно решите добавлять границу или нет. Убедитесь что каждое множество имеет хотя бы одну ячейку без нижней границы (для предотвращения изолирования областей):
1. Если ячейка в своем множестве одна, то не создавайте границу снизу
2. Если ячейка одна в своем множестве без нижней границы, то не создавайте нижнюю границу
Создадим нижнюю границу в ячейке под номером 2, так-как 1 множество имеет хотя бы одну ячейку без нижней границы
В ячейке под номером 3 не можем добавить нижнюю стенку, так-как ячейка в множестве 3 одна
Реализация шага 4
/* Добавление нижней горизонтальной стенки */
void Maze::addingHorizontalWalls(int row) {
for (int i = 0; i < cols_; i++) {
/* Ставим стенку или нет */
bool choise = randomBool();
/* Проверка, что множество имеет более одной ячейки (это предовратит замкнутые области */
if (calculateUniqueSet(sideLine_[i]) != 1 && choise == true) {
/* Ставим горизонтальную стенку */
h_walls_(row, i) = true;
}
}
}
/* Подсчет ячеек, которые содержаться в уникальном множестве */
int Maze::calculateUniqueSet(int element) {
int countUniqSet = 0;
for (int i = 0; i < cols_; i++) {
if (sideLine_[i] == element) {
countUniqSet++;
}
}
return countUniqSet;
}
/* Проверка условие 4.1 и 4.2 */
void Maze::checkedHorizontalWalls(int row) {
for (int i = 0; i < cols_; i++) {
if (calculateHorizontalWalls(sideLine_[i], row) == 0) {
h_walls_(row, i) = false;
}
}
}
/* Подсчет горизонтальных стен у ячеек уникального множества */
int Maze::calculateHorizontalWalls(int element, int row) {
int countHorizontalWalls = 0;
for (int i = 0; i < cols_; i++) {
if (sideLine_[i] == element && h_walls_(row, i) == false) {
countHorizontalWalls++;
}
}
return countHorizontalWalls;
}
Шаг 5.1
Если вы хотите добавить еще одну строку, то:
1. Выведите текущую строку
2. Удалите все правые границы
3. Удалите ячейки с нижней границей из их множества
4. Удалите все нижние границы
5. Продолжайте с шага 2
Копируем текущую строкуУдаляем правые стенкиУдаляем ячейку с нижней границейУдаляем все нижние границыРеализация шага 5.1
void Maze::preparatingNewLine(int row) {
for (int i = 0; i < cols_; i++) {
if (h_walls_(row, i) == true) {
/* Присваиваем ячейки пустое множество */
sideLine_[i] = kEmpty;
}
}
}
Продолжаем алгоритм до последней строки:
Шаг 2. Присваиваем пустым ячейкам уникальное множество
Ячейке под номером 2 присваиваем множества 5
Шаг 3. Создание правых границ
Добавим правую стенку, так-как ячейка под номером 4 и 5 из одного уникального множества
Шаги 4 и 5. Создание нижней границы и копирование строки
Добавили горизонтальную стенку у ячейки под номером 5
Генерируем новую строку:
Генерация новой строки
Генерация последней строки лабиринта
Шаг 2. Присваиваем пустым ячейкам уникальное множество
Ячейкам под номерами 2 и 5 присваиваем уникальное множество
Шаг 3. Создание правых границ
Добавляем правую границу в ячейке под номером 1Если мы решаем не добавлять правую границу в ячейке под номером 2, то к какому уникальному множеству будут принадлежать ячейка под номером 2 и 3?
1 или 7 (все ячейки принадлежащие к множеству 1 изменить на 7)
Результат добавления правых стенок
Пропускаем шаг 4, так-как это наша последняя строка
Шаг 5.2
Если вы хотите закончить лабиринт, то:
Добавьте нижнюю границу к каждой ячейке:
Двигаясь слева направо:
1. Если текущая ячейка и ячейка справа члены разных множеств, то:
1.1 Удалите правую границу
1.2 Объедините множества текущей ячейки и ячейки справа
1.3 Выведите завершающую строку
Удаляем правую стенку у ячейки под номером 4, так-как они разного множества и объединяем множествоРеализация шага 5.2
/* Добавление последней строки */
void Maze::addingEndLine() {
assignUniqueSet();
addingVerticalWalls(rows_ - 1);
checkedEndLine();
}
/* Проверка условий на добавление последней строки */
void Maze::checkedEndLine() {
for (int i = 0; i < cols_ - 1; i++) {
/* Проверка условия пункта 5.2.1 */
if (sideLine_[i] != sideLine_[i + 1]) {
/* Убираем вертикальную стенку */
v_walls_(rows_ - 1, i) = false;
/* Объединяем множества */
mergeSet(i, sideLine_[i]);
}
/* Добавляем горизонтальные стенки */
h_walls_(rows_ - 1, i) = true;
}
/* Добавляем горизонтальную стенку в последней ячейке */
h_walls_(rows_ - 1, cols_ - 1) = true;
}
Вывод
Выше я продемонстрировал как выглядят реализация алгоритма Эллера, вы можете попробовать реализовать его по моей статье.
Код проекта с алгоритмом из статьи можно найти на Github.
Благодарю за внимание! Надеюсь, я поделился чем-то новым и интересным для вас.
Если вам понравилась статья — то, пожалуйста, напишите об этом в комментарии. Буду очень рад любой критике.