Генератор идеального лабиринта по алгориму Hunt-and-Kill
Зависание программы и переполнение стека при рисовании в PictureBox.
Пробую себя в реализации алгоритма идеального лабиринта hunt-and-kill. Алгоритм выбирает случайную ячейку и идет в случайном направлении и стирает границы между этими ячейками. Если он заходит в тупик, то, начиная с первого ряда, ведется поиск пустой ячейки. Алгоритм продолжается до тех пор, пока не будет заполнена вся таблица.
Возможно это не самый правильный способ его создания, который я написал.
Я решил реализовать его через рекурсию, и дело в том, что на определенном этапе программа зависает, а иногда и выдает ошибку переполнения стека.
Вопрос в том, можно ли как-то оптимизировать код, или обойти переполнение стека?
public Form1()
{
InitializeComponent();
}
Graphics g;
Bitmap buf;
SolidBrush brushCell = new SolidBrush(Color.LightGreen);
SolidBrush fillCell = new SolidBrush(Color.White);
SolidBrush brushBack = new SolidBrush(Color.LightGray);
Pen penBorder = new Pen(Color.Black);
string[,] labirint;
Random rnd = new Random();
int sizeBox = 25;
int countX = 15;
int countY = 15;
int x0, y0;
int randomNum;
private void Form1_Load(object sender, EventArgs e)
{
buf = new Bitmap(pictureBox1.Width, pictureBox1.Height);
g = Graphics.FromImage(buf);
int w = countX * sizeBox;
int h = countY * sizeBox;
x0 = pictureBox1.Width / 2 - w / 2;
y0 = pictureBox1.Height / 2 - h / 2;
g.FillRectangle(brushBack, x0, y0, w, h);
g.DrawRectangle(penBorder, x0, y0, w, h);
for (int i = x0; i < x0 + w; i += sizeBox)
{
g.DrawLine(penBorder, i, y0, i, y0 + h);
}
for (int j = y0; j < y0 + h; j += sizeBox)
{
g.DrawLine(penBorder, x0, j, x0 + w, j);
}
pictureBox1.BackgroundImage = buf;
labirint = new string[countX, countY];
for (int i = 0; i < countX; i++)
{
for (int j = 0; j < countY; j++)
{
labirint[i, j] = "00000";
}
}
}
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
g.Dispose();
}
private void button1_Click(object sender, EventArgs e)
{
int firstCell_X = rnd.Next(countX);
int firstCell_Y = rnd.Next(countY);
g.FillRectangle(brushCell, x0 + firstCell_X * sizeBox + 1, y0 + firstCell_Y * sizeBox + 1, sizeBox - 1, sizeBox - 1);
nextCell(firstCell_X, firstCell_Y);
}
void nextCell(int lastCell_X, int lastCell_Y)
{
pictureBox1.Refresh();
randomNum = randN(lastCell_X, lastCell_Y);
try
{
if (labirint[lastCell_X, lastCell_Y][randomNum] == '0')
{
if (randomNum == 0 && labirint[lastCell_X, lastCell_Y - 1][4] == '0')
{
up(lastCell_X, lastCell_Y, randomNum);
}
else if (randomNum == 1 && labirint[lastCell_X + 1, lastCell_Y][4] == '0')
{
right(lastCell_X, lastCell_Y, randomNum);
}
else if (randomNum == 2 && labirint[lastCell_X, lastCell_Y + 1][4] == '0')
{
down(lastCell_X, lastCell_Y, randomNum);
}
else if (randomNum == 3 && labirint[lastCell_X - 1, lastCell_Y][4] == '0')
{
left(lastCell_X, lastCell_Y, randomNum);
}
else if (lastCell_Y != 0 && labirint[lastCell_X, lastCell_Y - 1][4] == '0')
{
up(lastCell_X, lastCell_Y, 0);
}
else if (lastCell_X != countX - 1 && labirint[lastCell_X + 1, lastCell_Y][4] == '0')
{
right(lastCell_X, lastCell_Y, 1);
}
else if (lastCell_Y != countY - 1 && labirint[lastCell_X, lastCell_Y + 1][4] == '0')
{
down(lastCell_X, lastCell_Y, 2);
}
else if (lastCell_X != 0 && labirint[lastCell_X - 1, lastCell_Y][4] == '0')
{
left(lastCell_X, lastCell_Y, 3);
}
else
{
g.FillRectangle(fillCell, x0 + lastCell_X * sizeBox + 1, y0 + lastCell_Y * sizeBox + 1, sizeBox - 1, sizeBox - 1);
hunt();
}
}
else if (lastCell_Y != 0 && labirint[lastCell_X, lastCell_Y - 1][4] == '0')
{
up(lastCell_X, lastCell_Y, 0);
}
else if (lastCell_X != countX - 1 && labirint[lastCell_X + 1, lastCell_Y][4] == '0')
{
right(lastCell_X, lastCell_Y, 1);
}
else if (lastCell_Y != countY - 1 && labirint[lastCell_X, lastCell_Y + 1][4] == '0')
{
down(lastCell_X, lastCell_Y, 2);
}
else if (lastCell_X != 0 && labirint[lastCell_X - 1, lastCell_Y][4] == '0')
{
left(lastCell_X, lastCell_Y, 3);
}
else
{
g.FillRectangle(fillCell, x0 + lastCell_X * sizeBox + 1, y0 + lastCell_Y * sizeBox + 1, sizeBox - 1, sizeBox - 1);
hunt();
}
}
catch { }
}
int randN(int lastCell_X, int lastCell_Y)
{
if (lastCell_X == 0 && lastCell_Y == 0)
{
return rnd.Next(1, 3);
}
else if (lastCell_X == 0 && lastCell_Y > 0 && lastCell_Y < countY)
{
return rnd.Next(0, 3);
}
else if (lastCell_X == 0 && lastCell_Y == countY - 1)
{
return rnd.Next(0, 2);
}
else if (lastCell_Y == countY - 1 && lastCell_X > 0 && lastCell_X < countX)
{
if (rnd.Next(2) == 0)
{
return rnd.Next(0, 2);
}
else
{
return 3;
}
}
else if (lastCell_Y == countY - 1 && lastCell_X == countX - 1)
{
if (rnd.Next(2) == 0)
{
return 0;
}
else
{
return 3;
}
}
else if (lastCell_X == countX - 1 && lastCell_Y > 0 && lastCell_Y < countY)
{
if (rnd.Next(2) == 0)
{
return rnd.Next(2, 4);
}
else
{
return 0;
}
}
else if (lastCell_X == countX - 1 && lastCell_Y == 0)
{
return rnd.Next(2, 4);
}
else if (lastCell_Y == 0 && lastCell_X > 0 && lastCell_X < countX)
{
return rnd.Next(1, 4);
}
else
{
return rnd.Next(0, 4);
}
}
void up(int lastCell_X, int lastCell_Y, int randomNum)
{
labirint[lastCell_X, lastCell_Y] = labirint[lastCell_X, lastCell_Y].Remove(randomNum, 1).Insert(randomNum, "1");
labirint[lastCell_X, lastCell_Y] = labirint[lastCell_X, lastCell_Y].Remove(4, 1).Insert(4, "1");
labirint[lastCell_X, lastCell_Y - 1] = labirint[lastCell_X, lastCell_Y - 1].Remove(randomNum + 2, 1).Insert(randomNum + 2, "1");
labirint[lastCell_X, lastCell_Y - 1] = labirint[lastCell_X, lastCell_Y - 1].Remove(4, 1).Insert(4, "1");
g.FillRectangle(fillCell, x0 + lastCell_X * sizeBox + 1, y0 + lastCell_Y * sizeBox + 1 - sizeBox, sizeBox - 1, sizeBox * 2 - 1);
g.FillRectangle(brushCell, x0 + lastCell_X * sizeBox + 1, y0 + lastCell_Y * sizeBox + 1 - sizeBox, sizeBox - 1, sizeBox - 1);
nextCell(lastCell_X, lastCell_Y - 1);
}
void right(int lastCell_X, int lastCell_Y, int randomNum)
{
labirint[lastCell_X, lastCell_Y] = labirint[lastCell_X, lastCell_Y].Remove(randomNum, 1).Insert(randomNum, "1");
labirint[lastCell_X, lastCell_Y] = labirint[lastCell_X, lastCell_Y].Remove(4, 1).Insert(4, "1");
labirint[lastCell_X + 1, lastCell_Y] = labirint[lastCell_X + 1, lastCell_Y].Remove(randomNum + 2, 1).Insert(randomNum + 2, "1");
labirint[lastCell_X + 1, lastCell_Y] = labirint[lastCell_X + 1, lastCell_Y].Remove(4, 1).Insert(4, "1");
g.FillRectangle(fillCell, x0 + lastCell_X * sizeBox + 1, y0 + lastCell_Y * sizeBox + 1, sizeBox * 2 - 1, sizeBox - 1);
g.FillRectangle(brushCell, x0 + lastCell_X * sizeBox + 1 + sizeBox, y0 + lastCell_Y * sizeBox + 1, sizeBox - 1, sizeBox - 1);
nextCell(lastCell_X + 1, lastCell_Y);
}
void down(int lastCell_X, int lastCell_Y, int randomNum)
{
labirint[lastCell_X, lastCell_Y] = labirint[lastCell_X, lastCell_Y].Remove(randomNum, 1).Insert(randomNum, "1");
labirint[lastCell_X, lastCell_Y] = labirint[lastCell_X, lastCell_Y].Remove(4, 1).Insert(4, "1");
labirint[lastCell_X, lastCell_Y + 1] = labirint[lastCell_X, lastCell_Y + 1].Remove(randomNum - 2, 1).Insert(randomNum - 2, "1");
labirint[lastCell_X, lastCell_Y + 1] = labirint[lastCell_X, lastCell_Y + 1].Remove(4, 1).Insert(4, "1");
g.FillRectangle(fillCell, x0 + lastCell_X * sizeBox + 1, y0 + lastCell_Y * sizeBox + 1, sizeBox - 1, sizeBox * 2 - 1);
g.FillRectangle(brushCell, x0 + lastCell_X * sizeBox + 1, y0 + lastCell_Y * sizeBox + 1 + sizeBox, sizeBox - 1, sizeBox - 1);
nextCell(lastCell_X, lastCell_Y + 1);
}
void left(int lastCell_X, int lastCell_Y, int randomNum)
{
labirint[lastCell_X, lastCell_Y] = labirint[lastCell_X, lastCell_Y].Remove(randomNum, 1).Insert(randomNum, "1");
labirint[lastCell_X, lastCell_Y] = labirint[lastCell_X, lastCell_Y].Remove(4, 1).Insert(4, "1");
labirint[lastCell_X - 1, lastCell_Y] = labirint[lastCell_X - 1, lastCell_Y].Remove(randomNum - 2, 1).Insert(randomNum - 2, "1");
labirint[lastCell_X - 1, lastCell_Y] = labirint[lastCell_X - 1, lastCell_Y].Remove(4, 1).Insert(4, "1");
g.FillRectangle(fillCell, x0 + lastCell_X * sizeBox + 1 - sizeBox, y0 + lastCell_Y * sizeBox + 1, sizeBox * 2 - 1, sizeBox - 1);
g.FillRectangle(brushCell, x0 + lastCell_X * sizeBox + 1 - sizeBox, y0 + lastCell_Y * sizeBox + 1, sizeBox - 1, sizeBox - 1);
nextCell(lastCell_X - 1, lastCell_Y);
}
void hunt()
{
for (int j = 0; j < countY; j++)
{
for (int i = 0; i < countX; i++)
{
if (labirint[i, j][4] == '0')
{
nextCell(i, j);
return;
}
}
}
}
Ответы (1 шт):
Я не смог вылечить ваш код, но смог полностью переписать. :)
Смысл алгоритма Hunt and Kill в том что он не рекурсивный, а у вас куча рекурсивных вызовов. То есть мысль пошла не в том направлении еще задолго до того как вы попали в исключения переполнения стека.
Давайте по порядку.
Как структура данных подойдет обычная bool[,] матрица, так как задача сводится только к рисованию лабиринта, а не хранению о нем всех данных. Если же надо хранить данные, то у вас они дублируются ровно дважды, и могут быть не косистентными. Например если взять 2 соседние ячейки, то если нет границы с соседом справа, то и у соседа справа не должно быть границы слева, верно? У вас из-за того что границы дублируются и не синхзронизируются при изменении, иногда вылетают исключения выхода за пределы поля.
private bool[,] map;
Для указания направления движения я создал простое перечисление
public enum Direction
{
Right,
Down,
Left,
Top
}
Сначала выбирается рандомная точка и от нее в рандомном направлении надо пройти до тех пор, пока не попадешь в тупик.
Поехали
// идти до попадания в тупик
private void Walk(Point position)
{
while (true)
{
map[position.Y, position.X] = true;
DrawCell(position, true);
Thread.Sleep(50);
var directions = GetRandomDirections();
bool stuck = true;
foreach (var direction in directions)
{
Point next = Next(position, direction);
if (IsValid(next))
{
DrawCell(position);
Expand(position, direction);
position = next;
stuck = false;
break;
}
}
if (stuck)
break;
}
DrawCell(position);
}
// переместиться в указанном направлении
private Point Next(Point positon, Direction direction)
{
if (direction == Direction.Right)
return new Point(positon.X + 1, positon.Y);
if (direction == Direction.Down)
return new Point(positon.X, positon.Y + 1);
if (direction == Direction.Left)
return new Point(positon.X - 1, positon.Y);
return new Point(positon.X, positon.Y - 1);
}
// существует ли текущая ячейка и свободна ли она
private bool IsValid(Point position)
{
if (position.Y < 0 || position.X < 0 || position.Y >= height || position.X >= width)
return false;
return !map[position.Y, position.X];
}
Очень просто, этот метод идет, пока не уткнется в тупик, затем завершается.
Теперь надо просканировать лабиринт в поисках точки, из которой можно пойти в другую сторону.
private Point Hunt()
{
Direction[] directions = Enum.GetValues<Direction>();
for (int row = 0; row < height; row++)
{
for (int col = 0; col < width; col++)
{
if (map[row, col])
{
foreach (var direction in directions)
{
Point pos = new Point(col, row);
Point next = Next(pos, direction);
if (IsValid(next))
return pos;
}
}
}
}
throw new InvalidOperationException("Лабиринт полон");
}
Теперь надо повторять заполнение лабиринта и поиск новой ячейки в цикле, пока он не будет заполнен полностью.
private void Run()
{
button1.Enabled = false;
Point pos = new(rnd.Next(width), rnd.Next(height));
try
{
while (true)
{
Walk(pos);
pos = Hunt();
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
button1.Enabled = true;
}
Вот собственно и весь алгоритм не считая графических методов.
Полный запускаемый код.
public partial class Form1 : Form
{
private const int cellSize = 25;
private const int height = 15;
private const int width = 15;
private readonly Brush markerBrush = Brushes.LightGreen;
private readonly Brush fillBrush = Brushes.White;
private readonly Brush backBrush = Brushes.LightGray;
private readonly Pen borderPen = Pens.Black;
private readonly Pen clearPen = Pens.White;
private readonly Random rnd = Random.Shared;
private Graphics g;
private Bitmap buf;
private bool[,] map;
public Form1()
{
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e)
{
InitMaze();
}
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
g.Dispose();
}
private void button1_Click(object sender, EventArgs e)
{
InitMaze();
Run();
}
private void InitMaze()
{
g?.Dispose();
buf?.Dispose();
buf = CreateMazeBitmap();
g = Graphics.FromImage(buf);
pictureBox1.Image = buf;
map = new bool[height, width];
}
private Bitmap CreateMazeBitmap()
{
int w = height * cellSize + 1;
int h = width * cellSize + 1;
Bitmap bmp = new(w, h);
using Graphics g = Graphics.FromImage(bmp);
g.FillRectangle(backBrush, 0, 0, w, h);
g.DrawRectangle(borderPen, 0, 0, w, h);
for (int x = 0; x < w; x += cellSize)
{
g.DrawLine(borderPen, x, 0, x, h);
}
for (int y = 0; y < h; y += cellSize)
{
g.DrawLine(borderPen, 0, y, w, y);
}
return bmp;
}
private void DrawCell(Point pos, bool isMarker = false)
{
g.FillRectangle(isMarker ? markerBrush : fillBrush, pos.X * cellSize + 1, pos.Y * cellSize + 1, cellSize - 1, cellSize - 1);
pictureBox1.Refresh();
}
private void Expand(Point position, Direction direction)
{
int left = position.X * cellSize;
int top = position.Y * cellSize;
int width = cellSize - 2;
int height = cellSize - 2;
if (direction == Direction.Right)
{
width = 0;
top += 1;
left += cellSize;
}
else if (direction == Direction.Left)
{
width = 0;
top += 1;
}
else if (direction == Direction.Down)
{
height = 0;
left += 1;
top += cellSize;
}
else
{
height = 0;
left += 1;
}
g.DrawLine(clearPen, left, top, left + width, top + height);
}
private void Run()
{
button1.Enabled = false;
Point pos = new(rnd.Next(width), rnd.Next(height));
try
{
while (true)
{
Walk(pos);
pos = Hunt();
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
button1.Enabled = true;
}
private Point Hunt()
{
Direction[] directions = Enum.GetValues<Direction>();
for (int row = 0; row < height; row++)
{
for (int col = 0; col < width; col++)
{
if (map[row, col])
{
foreach (var direction in directions)
{
Point pos = new Point(col, row);
Point next = Next(pos, direction);
if (IsValid(next))
return pos;
}
}
}
}
throw new InvalidOperationException("Лабиринт полон");
}
private void Walk(Point position)
{
while (true)
{
map[position.Y, position.X] = true;
DrawCell(position, true);
Thread.Sleep(50);
var directions = GetRandomDirections();
bool stuck = true;
foreach (var direction in directions)
{
Point next = Next(position, direction);
if (IsValid(next))
{
DrawCell(position);
Expand(position, direction);
position = next;
stuck = false;
break;
}
}
if (stuck)
break;
}
DrawCell(position);
}
private Point Next(Point positon, Direction direction)
{
if (direction == Direction.Right)
return new Point(positon.X + 1, positon.Y);
if (direction == Direction.Down)
return new Point(positon.X, positon.Y + 1);
if (direction == Direction.Left)
return new Point(positon.X - 1, positon.Y);
return new Point(positon.X, positon.Y - 1);
}
private bool IsValid(Point position)
{
if (position.Y < 0 || position.X < 0 || position.Y >= height || position.X >= width)
return false;
return !map[position.Y, position.X];
}
private Direction[] GetRandomDirections()
{
Direction[] values = Enum.GetValues<Direction>();
for (int i = 0; i < values.Length; i++)
{
int j = rnd.Next(values.Length);
(values[i], values[j]) = (values[j], values[i]);
}
return values;
}
}
public enum Direction
{
Right,
Down,
Left,
Top
}
Код дизайнера формы
partial class Form1
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
pictureBox1 = new PictureBox();
button1 = new Button();
tableLayoutPanel1 = new TableLayoutPanel();
((System.ComponentModel.ISupportInitialize)pictureBox1).BeginInit();
tableLayoutPanel1.SuspendLayout();
SuspendLayout();
//
// pictureBox1
//
pictureBox1.Dock = DockStyle.Fill;
pictureBox1.Location = new Point(3, 43);
pictureBox1.Name = "pictureBox1";
pictureBox1.Size = new Size(794, 404);
pictureBox1.SizeMode = PictureBoxSizeMode.CenterImage;
pictureBox1.TabIndex = 0;
pictureBox1.TabStop = false;
//
// button1
//
button1.Dock = DockStyle.Left;
button1.Location = new Point(3, 3);
button1.Name = "button1";
button1.Size = new Size(112, 34);
button1.TabIndex = 1;
button1.Text = "button1";
button1.UseVisualStyleBackColor = true;
button1.Click += button1_Click;
//
// tableLayoutPanel1
//
tableLayoutPanel1.ColumnCount = 1;
tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
tableLayoutPanel1.Controls.Add(button1, 0, 0);
tableLayoutPanel1.Controls.Add(pictureBox1, 0, 1);
tableLayoutPanel1.Dock = DockStyle.Fill;
tableLayoutPanel1.Location = new Point(0, 0);
tableLayoutPanel1.Name = "tableLayoutPanel1";
tableLayoutPanel1.RowCount = 2;
tableLayoutPanel1.RowStyles.Add(new RowStyle());
tableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
tableLayoutPanel1.Size = new Size(800, 450);
tableLayoutPanel1.TabIndex = 2;
//
// Form1
//
AutoScaleDimensions = new SizeF(10F, 25F);
AutoScaleMode = AutoScaleMode.Font;
ClientSize = new Size(800, 450);
Controls.Add(tableLayoutPanel1);
Name = "Form1";
Text = "Form1";
Load += Form1_Load;
((System.ComponentModel.ISupportInitialize)pictureBox1).EndInit();
tableLayoutPanel1.ResumeLayout(false);
ResumeLayout(false);
}
#endregion
private PictureBox pictureBox1;
private Button button1;
private TableLayoutPanel tableLayoutPanel1;
}
Конечно этот код во время выполнения подвешивает форму, и я знаю как исправить, но намеренно оставлю так, чтобы не усложнять ответ.
Предлагаю скачать доработанный проект, не вешает форму и есть прогресс-бар: Яндекс.Диск.

