Генерация лабиринтов: алгоритм Эллера

6f4d63b8b050b8b7008e73acb61095ce.gif

Вступление

Как оказалось, что тема генерации лабиринтов не сильно раскрывается в русско- и англоязычном сообществе. На хабре существует одна статья Алгоритм Эллера для генерации лабиринтов. Статья, является переводом англоязычной статьи с описанием алгоритма по шагам. В своей реализации, я опирался на алгоритм из статьи. В процессе я столкнулся с трудностями и недопонимаем алгоритма. Поэтому я решил подробно разобрать алгоритм Эллера и разъяснить некоторые моменты с особенными случаями.

Базовые понятия

Идеальный лабиринт — это лабиринт в котором нет циклов (между двумя ячейками есть только один путь) и изолированных частей (ячейки или групп ячеек, которые не связаны с другими частями лабиринта).

Множество — это не более чем неупорядоченная коллекция уникальных элементов.

Описание алгоритма

  1. Создайте первую строку. Ни одна ячейка не будет являться частью ни одного множества.

  2. Присвойте ячейкам, не входящим в множество, свое уникальное множество.

  3. Создайте правые границы, двигаясь слева направо:

    1. Случайно решите добавлять границу или нет 

      1. Если текущая ячейка и ячейка справа принадлежат одному множеству, то создайте границу между ними (для предотвращения зацикливаний)

      2. Если вы решили не добавлять границу, то объедините два множества в которых находится текущая ячейка и ячейка справа.

  4. Создайте границы снизу, двигаясь слева направо:

  5. Решите, будете ли вы дальше добавлять строки или хотите закончить лабиринт

    1. Если вы хотите добавить еще одну строку, то:

      1. Выведите текущую строку

      2. Удалите все правые границы

      3. Удалите ячейки с нижней границей из их множества

      4. Удалите все нижние границы

      5. Продолжайте с шага 2

    2. Если вы решите закончить лабиринт, то:

      1. Добавьте нижнюю границу к каждой ячейке

      2. Двигаясь слева направо:

        1. Если текущая ячейка и ячейка справа члены разных множеств, то:

          1. Удалите правую границу

          2. Объедините множества текущей ячейки и ячейки справа

          3. Выведите завершающую строку

Реализация алгоритма по шагам

Описание лабиринта:

Лабиринт может храниться в файле в виде количества строк и столбцов, а также двух матриц, содержащих положение вертикальных и горизонтальных стен соответственно. В первой матрице отображается наличие стены справа от каждой ячейки, а во второй — снизу.

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Ставим правую стенку у ячейки под номером 2Объединяем два множества в котором находится текущая ячейка и ячейка справаОбъединяем два множества в котором находится текущая ячейка и ячейка справаСтавим правую стенку у ячейки под номером 3Ставим правую стенку у ячейки под номером 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 множество имеет хотя бы одну ячейку без нижней границыСоздадим нижнюю границу в ячейке под номером 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Ячейке под номером 2 присваиваем множества 5

Шаг 3. Создание правых границ

Добавим правую стенку, так-как ячейка под номером 4 и 5 из одного уникального множестваДобавим правую стенку, так-как ячейка под номером 4 и 5 из одного уникального множества

Шаги 4 и 5. Создание нижней границы и копирование строки

Добавили горизонтальную стенку у ячейки под номером 5Добавили горизонтальную стенку у ячейки под номером 5

Генерируем новую строку:

Генерация новой строкиГенерация новой строки

Генерация последней строки лабиринта

Шаг 2. Присваиваем пустым ячейкам уникальное множество

Ячейкам под номерами 2 и 5 присваиваем уникальное множествоЯчейкам под номерами 2 и 5 присваиваем уникальное множество

Шаг 3. Создание правых границ

Добавляем правую границу в ячейке под номером 1Добавляем правую границу в ячейке под номером 1Если мы решаем не добавлять правую границу в ячейке под номером 2, то к какому уникальному множеству будут принадлежать ячейка под номером 2 и 3?

1 или 7 (все ячейки принадлежащие к множеству 1 изменить на 7)

Результат добавления правых стенокРезультат добавления правых стенок

Пропускаем шаг 4, так-как это наша последняя строка

Шаг 5.2

Если вы хотите закончить лабиринт, то:

Добавьте нижнюю границу к каждой ячейке:

Двигаясь слева направо:

1. Если текущая ячейка и ячейка справа члены разных множеств, то:

1.1 Удалите правую границу

1.2 Объедините множества текущей ячейки и ячейки справа

1.3 Выведите завершающую строку

Удаляем правую стенку у ячейки под номером 4, так-как они разного множества и объединяем множествоУдаляем правую стенку у ячейки под номером 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.

Благодарю за внимание! Надеюсь, я поделился чем-то новым и интересным для вас.

Если вам понравилась статья — то, пожалуйста, напишите об этом в комментарии. Буду очень рад любой критике.

© Habrahabr.ru