Как стать автором
Поиск
Написать публикацию
Обновить

Неприлично простая реализация неприлично простого алгоритма генерации лабиринта

Время на прочтение6 мин
Количество просмотров41K
Продолжение этой статьи об очень простом алгоритме генерации прямоугольных лабиринтов. В этой статье я приведу мою реализацию алгоритма на С++, а также покажу несколько дополнительных функций, которые породил мой скучающий мозг. Если осмелитесь продолжить читать, убедитесь, что ознакомились с моей предыдущей статьей. Глянули? Молодцы, продолжаем.

Где же код?


Начнем с начала — подтягиваем стандартные библиотеки, объявляем функции.

#include<iostream>
#include<cstdlib>
#include<ctime>
using namespace std;
bool deadend(int, int, int**, int, int); // Вспомогательная функция, определяет тупики
void visual(int**, int, int); // Изображение результата с помощью консольной графики
void mazemake(int**, int, int); // Собственно алгоритм
const int wall = 0, pass = 1;

Все еще очень просто, как я и люблю. В main мы сможем определить габариты лабиринта в переменных height и width (напоминаю, что размеры лабиринта исключительно нечетные, издержки алгоритма). Эти параметры можно вынести за пределы main и сделать их константами или просто глобальными переменными, программа от этого не пострадает.

int main(){
	
	srand((unsigned)time(NULL));
	
	int height = 21, width = 21;
	
	int** maze = new int*[height]; 
	for(int i = 0; i < height; i++)
		maze[i] = new int[width];
		
	mazemake(maze, height, width);
	
	visual(maze,height,width);
	
	for(int i = 0; i < height; i++) 
    delete[] maze[i];
    delete[] maze;
    
    return 0;
}

Собственно, вот и весь main. Весь лабиринт без проблем сохраняется в двухмерный динамический массив, с которым мы и работаем. После выполнения функции mazemake, в массив maze сохраняется готовый лабиринт, в котором 0 — это стена, а 1 — это проход.

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

bool deadend(int x, int y, int** maze, int height, int width){
	int a = 0;
	
	if(x != 1){
		if(maze[y][x-2] == pass)
			a+=1;
		}
	else a+=1;
	
	if(y != 1){
		if(maze[y-2][x] == pass)
			a+=1;
		}
	else a+=1;
	
	if(x != width-2){
		if(maze[y][x+2] == pass)
			a+=1;
		}
	else a+=1;
	
	if(y != height-2){
		if(maze[y+2][x] == pass)
			a+=1;
		}
	else a+=1;
	
	if(a == 4)
		return 1;
	else
		return 0;
}

Немного по deadend. Эта функция возвращает значение true если крот в тупике, иначе — false. Почему дважды if, а не логическое И? Очень просто — первая проверка — на присутствие рядом внешней непроницаемой стены. Непроницаема она по нескольким причинам — мы так сделали, выбрав именно эти габариты, соответственно выделили память для массива (управление памятью, аняня ^>^), да и не очень-то проверишь (-1)-ый элемент массива. Также, обратите внимание на фигурные скобки после первичного ifelse относится именно к нему, а другой способ записать это мне неизвестен.

void visual(int** maze, int height, int width){
	for(int i = 0; i < height; i++){
		for(int j = 0; j < width; j++)
			switch(maze[i][j]){
				case wall: cout<<"0 "; break;
				case pass: cout<<"  "; break;
			}
		cout<<endl;
		}
	cout<<endl;	
}

Что еще нужно для счастья? Следующая остановка — mazemake.

void mazemake(int** maze, int height, int width){
	int x, y, c, a; 
	bool b;
	
	for(int i = 0; i < height; i++) // Массив заполняется землей-ноликами
		for(int j = 0; j < width; j++)
			maze[i][j] = wall;
	
	x = 3; y = 3; a = 0; // Точка приземления крота и счетчик
	while(a < 10000){ // Да, простите, костыль, иначе есть как, но лень
		maze[y][x] = pass; a++;
		while(1){ // Бесконечный цикл, который прерывается только тупиком
			c = rand()%4; // Напоминаю, что крот прорывает
			switch(c){  // по две клетки в одном направлении за прыжок
				case 0: if(y != 1)
					if(maze[y-2][x] == wall){ // Вверх
                                        maze[y-1][x] = pass;
					maze[y-2][x] = pass;
					y-=2;
				}
				case 1: if(y != height-2)      
					if(maze[y+2][x] == wall){ // Вниз
					maze[y+1][x] = pass;
					maze[y+2][x] = pass;
					y+=2;
				}
				case 2: if(x != 1)
					if(maze[y][x-2] == wall){ // Налево
					maze[y][x-1] = pass;
					maze[y][x-2] = pass;
					x-=2;
				}
				case 3: if(x != width-2)
					if(maze[y][x+2] == wall){ // Направо
					maze[y][x+1] = pass;
					maze[y][x+2] = pass;
					x+=2;
				}
			}
			if(deadend(x,y,maze,height,width))
			   break;
		}
		
		if(deadend(x,y,maze,height,width)) // Вытаскиваем крота из тупика
			do{
				x = 2*(rand()%((width-1)/2))+1;
				y = 2*(rand()%((height-1)/2))+1;
			}
			while(maze[y][x] != pass);
	} // На этом и все.
}

Слишком просто? В общем-то да, так и есть. До неприличия просто. Здесь есть все те же двойные if, по той же причине. Даже посетовать не на что. Разве что костыль в виде счетчика. Если он ранит ваши чувства — добро пожаловать во вторую главу нашего повествования.

Фичи и штуки-дрюки


Комнаты


Мы научили млекопитающее делать нам лабиринт. Почему бы это же создание не заставить сделать нам пару комнат? Мы ведь коварные садисты-ученые и не знаем куда деть бедных зверюшек.

Если мы хотим иметь возможность делать комнаты и определять их параметры, то код немного меняется то тут, то там. Комнаты тоже имеют нечетные размеры.


void mazemake(int**, int, int, int, int, int); // Более расширенное объявление функции
const int wall = 0, pass = 1, room = 4; // Новая константа
...
	int height = 59, width = 111, k = 30; // Мы включили параметр количества комнат
	int rheight = 7, rwidth = 5; // Размеры комнаты
...
void mazemake(int** maze, int height, int width, int k, int rheight, int rwidth){
	int x, y, c, a;
	bool b, swap = 1;
for(int i = 0; i < height; i++)
	for(int j = 0; j < width; j++)
		maze[i][j] = wall;
	
	rheight--; rwidth--; // Исключительно для удобства
				
	for(int l = 0; l < k; l++){  // Генерация комнат
		b = 1;					
		while(b){
			do{ // Точка-центр комнаты
				if(rwidth%4 == 0) // Комнаты, с разной делимостью на 4 ведут себя 
					x = 2*(rand()%(width/2))+1; // по разному, унифицируем
				else
					x = 2*(rand()%(width/2))+2;
				if(rheight%4 == 0)	
					y = 2*(rand()%(height/2))+1;
				else
					y = 2*(rand()%(height/2))+2;
			}
			while(x < (rwidth+2) || x > (width-rwidth-2) || 
                                y < (rheight+2) || y > (height-rheight-2));
			
			b = 0; // Комнаты не должны прикасаться
			for(int i = (y-rheight-2); i < (y+rheight+2); i++)
				for(int j = (x-rwidth-2); j < (x+rwidth+2); j++)
				if(maze[i][j] == room)
					b = 1;
					
			if(b)
				continue;
			
			for(int i = (y-rheight/2); i < (y+rheight/2+1); i++) // Раскопки комнаты
				for(int j = (x-rwidth/2); j < (x+rwidth/2+1); j++)
				maze[i][j] = room;
			
			c = rand()%4; // Дверь в комнату, определяем в какую стену
			// Нижняя, верхняя, правая и левая соответственно
                        // Нагромождение в виде rand()... нужно для того, чтобы дверь стояла в разных
                        // местах стены
			if(c == 0) maze[y+rheight/2+1][x-rwidth/2+2*(rand()%(rwidth/2+1))] = room;
			if(c == 1) maze[y-rheight/2-1][x-rwidth/2+2*(rand()%(rwidth/2+1))] = room;
			if(c == 2) maze[y-rheight/2+2*(rand()%(rheight/2+1))][x+rwidth/2+1] = room;
			if(c == 3) maze[y-rheight/2+2*(rand()%(rheight/2+1))][x-rwidth/2-1] = room;
			// swap отвечает за возможность поворачивать комнату на 90°
			if(swap){
				rheight += rwidth;
				rwidth = rheight - rwidth;
				rheight -= rwidth;
			} // Вот так настоящие мужики меняют переменные значениями
		}
	}
...
int deadend(int x, int y, int** maze, int height, int width){
	int a = 0; // В deadend теперь нужно учитывать то, что в комнату мы прыгнуть не можем
	
	if(x != 1){
		if(maze[y][x-2] == pass || 
		   maze[y][x-2] == room)
			a+=1;
		}
	else a+=1;
...

Гриб


Хм… Нет, не солидно. Грибовидный алгоритм поиска пути в идеальных лабиринтах. Вот так лучше. Ладно, меньше слов, больше кода.

...
void shroom(int**, int, int); 
const int wall = 0, pass = 1, liveshroom = 2, deadshroom = 3, room = 4, start = 5, finish = 6;
int main(){
	
	srand((unsigned)time(NULL));
	
	int height = 59, width = 111, k = 30; // Грибной алгоритм безопасно работает с комнатами -
	int rheight = 7, rwidth = 5; // он их игнорирует
	
	int** maze = new int*[height]; 
	
	for(int i = 0; i < height; i++)
		maze[i] = new int[width];
			
	mazemake(maze, height, width, k, rheight, rwidth); 
	
	visual(maze,height,width);
	
	maze[1][1] = start; 
	maze[height-2][width-2] = finish;
	shroom(maze,height,width); // Гриб не изменяет архитектуру лабиринта, но оставляет след
	visual(maze,height,width); // между стартом и финишем
	
	for(int i = 0; i < height; i++) 
    delete[] maze[i];
    delete[] maze;
    
    return 0;
... 
void shroom(int** maze, int height, int width){
	int x, y;
	
	for(int i = 0; i < height; i++) // Поиск старта
		for(int j = 0; j < width; j++)
			if(maze[i][j] == start){
				x = j; y = i;
			}

	while(maze[y][x] != finish){ // Условие выхода - гриб нашел финиш
		maze[y][x] = liveshroom; // Заражаем проход
		// Гриб ищет проход направо, налево, вниз и вверх последовательно,
                // он может передвигаться только по пустым коридорам
		if(maze[y][x+1] == pass){ 
			maze[y][x+1] = liveshroom;
			x+=2;
			continue;
		}
		
		if(maze[y][x-1] == pass){ 
			maze[y][x-1] = liveshroom;
			x-=2;
			continue;
		}
		
		if(maze[y+1][x] == pass){
			maze[y+1][x] = liveshroom;
			y+=2;
			continue;
		}
		
		if(maze[y-1][x] == pass){ 
			maze[y-1][x] = liveshroom;
			y-=2;
			continue;
		}
		
		if(maze[y][x+1] != pass && // Если гриб не может двигаться - он умирает, 
	       maze[y][x-1] != pass && // ведущая головка гриба (x, y) возвращается на ближайший 
		   maze[y+1][x] != pass && // живой гриб
		   maze[y-1][x] != pass){
			maze[y][x] = deadshroom;
			
			if(maze[y][x+1] == liveshroom){ 
				maze[y][x+1] = deadshroom;
				x+=2;
				continue;
			}
			
			if(maze[y][x-1] == liveshroom){ 
				maze[y][x-1] = deadshroom;
				x-=2;
				continue;
			}
			
			if(maze[y+1][x] == liveshroom){ 
				maze[y+1][x] = deadshroom;
				y+=2;
				continue;
			}
			
			if(maze[y-1][x] == liveshroom){ 
				maze[y-1][x] = deadshroom;
				y-=2;
				continue;
			}
		}
	}
	
	for(int i = 0; i < height; i++) // Мертвый гриб разлагается, не оставляя следа в лабиринте
		for(int j = 0; j < width; j++)
			if(maze[i][j] == deadshroom)
				maze[i][j] = pass;
}
}

Честно, даже нечего сказать. Просто находим единственный путь между точками и показываем. Разве что в visual добавить case liveshroom: cout<<"* "; break;

Не костыль


Если вам очень не понравился счетчик в основном алгоритме, то вот вам прекрасная альтернатива — функция проверки, которая запускается раз в сто циклов:

...
x = 3; y = 3; a = 0;
	while(1){
		a++;
		if(a%100 == 0)
			if(ended(maze, height, width))
				break;
		maze[y][x] = pass;
...
bool ended(int** maze, int height, int width){
	bool b = 1;
	
	for(int i = 1; i < (height - 1); i += 2)
		for(int j = 1; j < (width - 1); j += 2)
			if(maze[i][j] == wall)
				b = 0;
	return b;
}

С опережающим объявлением справитесь. Успехов, пользуйтесь на здоровье.

Ах да, картиночки.

Посмотреть
Лабиринты 59х111, 30-40 комнат.

Простой лабиринт без комнат.

image

Комнаты 5х5, демонстрируют старый баг — двери были только в двух позициях на стенах.

image

Комнаты 3х5, без свапа, двери адекватно распределены.

image

Комнаты 5х7, свап включен

image

Демонстрация гриба, без комнат…

image

… и с 70-ю свапнутыми комнатами 3х5.

image
Теги:
Хабы:
Всего голосов 51: ↑39 и ↓12+27
Комментарии54

Публикации

Ближайшие события