Генератор идеального лабиринта по алгориму Hunt-and-Kill

Рейтинг: 1Ответов: 1Опубликовано: 12.04.2023

Зависание программы и переполнение стека при рисовании в 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;
            }
        }
    }
}

Ответы

▲ 3Принят

Я не смог вылечить ваш код, но смог полностью переписать. :)

Смысл алгоритма 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;
}

Конечно этот код во время выполнения подвешивает форму, и я знаю как исправить, но намеренно оставлю так, чтобы не усложнять ответ.

введите сюда описание изображения


Предлагаю скачать доработанный проект, не вешает форму и есть прогресс-бар: Яндекс.Диск.