Реализация случайно сгенерированного лабиринта с использованием алгоритма Prim

Я пытаюсь реализовать случайно сгенерированный лабиринт, используя алгоритм Prim.

Я хочу, чтобы мой лабиринт выглядел так: enter image description here

однако лабиринты, которые я генерирую из своей программы, выглядят так:

enter image description here

в настоящее время я застрял на правильной реализации шагов, выделенных жирным шрифтом:

  1. начните с сетки, полной стен.
  2. выберите ячейку, отметьте ее как часть лабиринта. Добавить стены камеры в список стен.
  3. пока есть стены в списке:
    • **1. Выберите случайную стену из списка. Если камера на противоположной стороне еще не в лабиринте:
        1. сделайте стену проходом и отметьте ячейку на противоположной стороне как часть лабиринта.**
        1. добавьте соседние стены ячейки в список стен.
      1. удалите стену из списка.

от эта статья о генерации лабиринта.

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

6 ответов


описание в статье Википедии действительно заслуживает повышения.

первая запутанная часть статьи заключается в том, что описание алгоритма рандомизированного Prim не уточняет предполагаемую структуру данных, используемую алгоритмом. Таким образом, такие фразы, как "противоположная ячейка", становятся запутанными.

в основном есть 2 основных подхода "программисты генератора лабиринта" могут выбрать:

  1. клетки имеют стены или проходы к своим 4 соседям. Этот информация о стенах/проходах хранится и обрабатывается.
  2. клетки можно или преградить (стены) или проходы, без хранить любые дополнительные данные по взаимодействия.

в зависимости от того, какую модель (1) или (2) имеет в виду читатель при чтении описания алгоритма, они либо понимают, либо не понимают.

Я лично предпочитаю использовать ячейки в качестве стен или проходов, а не возиться с выделенным проходом / стеной информация.

тогда" пограничные " участки имеют расстояние 2 (а не 1) от прохода. Случайный пограничный патч из списка пограничных патчей выбирается и соединяется со случайным соседним проходом (на расстоянии 2), также делая ячейку между пограничным патчем и соседним проходом проходом.

вот моя реализация F# того, как это выглядит:

let rng = new System.Random()
type Cell = | Blocked | Passage
type Maze = 
    { 
        Grid : Cell[,]
        Width : int
        Height : int
    }

let initMaze dx dy = 
    let six,siy = (1,1)
    let eix,eiy = (dx-2,dy-2)
    { 
        Grid = Array2D.init dx dy 
            (fun _ _ -> Blocked
            ) 
        Width = dx
        Height = dy
    }

let generate (maze : Maze) : Maze =
    let isLegal (x,y) =
        x>0 && x < maze.Width-1 && y>0 && y<maze.Height-1
    let frontier (x,y) =
        [x-2,y;x+2,y; x,y-2; x, y+2]
        |> List.filter (fun (x,y) -> isLegal (x,y) && maze.Grid.[x,y] = Blocked)
    let neighbor (x,y) =
        [x-2,y;x+2,y; x,y-2; x, y+2]
        |> List.filter (fun (x,y) -> isLegal (x,y) && maze.Grid.[x,y] = Passage)
    let randomCell () = rng.Next(maze.Width),rng.Next(maze.Height)
    let removeAt index (lst : (int * int) list) : (int * int) list =
        let x,y = lst.[index]
        lst |> List.filter (fun (a,b) -> not (a = x && b = y) )
    let between p1 p2 =
        let x = 
            match (fst p2 - fst p1) with
            | 0 -> fst p1
            | 2 -> 1 + fst p1
            | -2 -> -1 + fst p1
            | _ -> failwith "Invalid arguments for between()"
        let y = 
            match (snd p2 - snd p1) with
            | 0 -> snd p1
            | 2 -> 1 + snd p1
            | -2 -> -1 + snd p1
            | _ -> failwith "Invalid arguments for between()"
        (x,y)
    let connectRandomNeighbor (x,y) =
        let neighbors = neighbor (x,y)
        let pickedIndex = rng.Next(neighbors.Length)
        let xn,yn = neighbors.[pickedIndex]
        let xb,yb = between (x,y) (xn,yn)
        maze.Grid.[xb,yb] <- Passage
        ()
    let rec extend front =
        match front with
        | [] -> ()
        | _ ->
            let pickedIndex = rng.Next(front.Length)
            let xf,yf = front.[pickedIndex]
            maze.Grid.[xf,yf] <- Passage
            connectRandomNeighbor (xf,yf)
            extend ((front |> removeAt pickedIndex) @ frontier (xf,yf))

    let x,y = randomCell()
    maze.Grid.[x,y] <- Passage
    extend (frontier (x,y))

    maze


let show maze =
    printfn "%A" maze
    maze.Grid |> Array2D.iteri 
        (fun y x cell ->
            if x = 0 && y > 0 then 
                printfn "|"
            let c = 
                match cell with
                | Blocked -> "X"
                | Passage -> " "
            printf "%s" c
        )
    maze

let render maze =
    let cellWidth = 10;
    let cellHeight = 10;
    let pw = maze.Width * cellWidth
    let ph = maze.Height * cellHeight
    let passageBrush = System.Drawing.Brushes.White
    let wallBrush = System.Drawing.Brushes.Black
    let bmp = new System.Drawing.Bitmap(pw,ph)
    let g = System.Drawing.Graphics.FromImage(bmp);
    maze.Grid
    |> Array2D.iteri 
        (fun y x cell ->
            let brush = 
                match cell with
                | Passage -> passageBrush
                | Blocked -> wallBrush
            g.FillRectangle(brush,x*cellWidth,y*cellHeight,cellWidth,cellHeight)
        )
    g.Flush()
    bmp.Save("""E:\temp\maze.bmp""")

initMaze 50 50 |> generate |> show |> render

результирующий лабиринт тогда может выглядеть так это:

enter image description here

здесь попытка описать мое решение в Википедии в стиле "алгоритм":

  1. сетка состоит из 2-х мерного массива ячеек.
  2. ячейка имеет 2 состояния: заблокирован или проход.
  3. начните с сетки, полной ячеек в заблокированном состоянии.
  4. выберите случайную ячейку, установите ее в состояние прохода и вычислите ее пограничные ячейки. Пограничная ячейка ячейки-это ячейка с расстоянием 2 в состоянии Заблокирован и внутри сетки.
  5. пока список пограничных ячеек не пуст:
    1. выберите случайную пограничную ячейку из списка пограничных ячеек.
    2. пусть соседи (frontierCell) = все ячейки на расстоянии 2 в государственном проходе. Выберите случайного соседа и соедините пограничную ячейку с соседом, установив ячейку между ними для прохождения состояния. Вычислите ячейки границы выбранной ячейки границы и добавьте их в список границ. Снять выбранная ячейка frontier из списка ячеек frontier.

простая реализация Java алгоритма Prim:

import java.util.LinkedList;
import java.util.Random;

public class Maze {
    public static final char PASSAGE_CHAR = ' ';
    public static final char WALL_CHAR = '▓';
    public static final boolean WALL    = false;
    public static final boolean PASSAGE = !WALL;

    private final boolean map[][];
    private final int width;
    private final int height;

    public Maze( final int width, final int height ){
        this.width = width;
        this.height = height;
        this.map = new boolean[width][height];

        final LinkedList<int[]> frontiers = new LinkedList<>();
        final Random random = new Random();
        int x = random.nextInt(width);
        int y = random.nextInt(height);
        frontiers.add(new int[]{x,y,x,y});

        while ( !frontiers.isEmpty() ){
            final int[] f = frontiers.remove( random.nextInt( frontiers.size() ) );
            x = f[2];
            y = f[3];
            if ( map[x][y] == WALL )
            {
                map[f[0]][f[1]] = map[x][y] = PASSAGE;
                if ( x >= 2 && map[x-2][y] == WALL )
                    frontiers.add( new int[]{x-1,y,x-2,y} );
                if ( y >= 2 && map[x][y-2] == WALL )
                    frontiers.add( new int[]{x,y-1,x,y-2} );
                if ( x < width-2 && map[x+2][y] == WALL )
                    frontiers.add( new int[]{x+1,y,x+2,y} );
                if ( y < height-2 && map[x][y+2] == WALL )
                    frontiers.add( new int[]{x,y+1,x,y+2} );
            }
        }
    }

    @Override
    public String toString(){
        final StringBuffer b = new StringBuffer();
        for ( int x = 0; x < width + 2; x++ )
            b.append( WALL_CHAR );
        b.append( '\n' );
        for ( int y = 0; y < height; y++ ){
            b.append( WALL_CHAR );
            for ( int x = 0; x < width; x++ )
                b.append( map[x][y] == WALL ? WALL_CHAR : PASSAGE_CHAR );
            b.append( WALL_CHAR );
            b.append( '\n' );
        }
        for ( int x = 0; x < width + 2; x++ )
            b.append( WALL_CHAR );
        b.append( '\n' );
        return b.toString();
    }
}

пример вывода new Maze(20,20).toString() - это:

▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
▓   ▓     ▓       ▓ ▓▓
▓ ▓▓▓ ▓▓▓▓▓▓▓▓▓ ▓▓▓ ▓▓
▓     ▓ ▓ ▓ ▓   ▓ ▓ ▓▓
▓ ▓▓▓▓▓ ▓ ▓ ▓▓▓ ▓ ▓ ▓▓
▓   ▓ ▓ ▓   ▓       ▓▓
▓ ▓ ▓ ▓ ▓ ▓▓▓▓▓▓▓ ▓ ▓▓
▓ ▓ ▓ ▓ ▓   ▓ ▓   ▓ ▓▓
▓ ▓▓▓ ▓ ▓▓▓ ▓ ▓ ▓▓▓▓▓▓
▓   ▓     ▓ ▓ ▓   ▓ ▓▓
▓ ▓▓▓▓▓ ▓▓▓ ▓ ▓ ▓▓▓ ▓▓
▓   ▓   ▓           ▓▓
▓ ▓ ▓ ▓ ▓▓▓ ▓▓▓▓▓▓▓▓▓▓
▓ ▓   ▓   ▓       ▓ ▓▓
▓ ▓▓▓▓▓▓▓ ▓ ▓▓▓▓▓ ▓ ▓▓
▓ ▓     ▓   ▓   ▓ ▓ ▓▓
▓▓▓ ▓▓▓ ▓▓▓ ▓ ▓▓▓▓▓ ▓▓
▓   ▓               ▓▓
▓▓▓ ▓ ▓▓▓ ▓▓▓ ▓▓▓ ▓ ▓▓
▓   ▓ ▓   ▓     ▓ ▓ ▓▓
▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓

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


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

если вы считаете, что" правильный " лабиринт вы разместили, без внешней границы, и взять ячейку topleft, чтобы быть (0,0), вы можете наблюдать, что проходы и стены, в некотором смысле, чередуются. Каждая ячейка, где обе координаты четны, должна быть проход, и каждая ячейка, где обе координаты нечетные, должна быть стеной. Таким образом, единственные ячейки, где у вас есть выбор, - это те, где одна координата четная, а другая нечетная.

пусть ячейка (x,y) в середине поля, где обе координаты даже, быть даны. Эта камера должна быть коридором. Клетки (x-1,y), (x+1,y), (x,y-1) и (x,y+1) потенциальные стены, окружающие его, и клетки (x-2,y), (x+2,y), (x,y-2) и (x,y+2) квадраты на противоположностях эти стены, соответственно.

С этой информацией вы можете просто реализовать алгоритм, с дополнительным требованием, что на Шаге 2 вы должны выбрать ячейку, где обе координаты четны.


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

Это предотвратит любые стены от быть соединенным только углом.


самостоятельно я придумал что-то совсем другое, прежде чем я исследовал проблему вообще. Посмотрите, считаете ли вы, что это полезный подход.

давно, когда я видел IBM PC Character Graphics (глифы, которые являются частью кодовой страницы) я думал о создании лабиринтов таким образом. Мой подход состоит из двух этапов:--3-->

  1. создание лабиринта в массиве целых чисел с использованием битовых значений 1-15 для указания направлений, открытых в каждой ячейке лабиринта
  2. отображение его в видимую форму. Поэтому стены не имеют значения, пока я не покажу лабиринт.

каждая ячейка начинается как 0 (Не выбран), а затем любой из 4 битов (1 = вправо, 2 = вниз, 4 = влево, 8 = вверх) может быть включен. Наивно, вы можете просто выбрать случайное число от 1-15 в каждой ячейке, за исключением пяти вещей:

  1. начните с рисования "стены" коридоров и углов вокруг всего массива и оставьте проход на две точки. Это самый простой способ справиться с граничными условиями.
  2. весьте выборы так, что тупики будут редки и прямые или угловые коридоры общие, полные пересечения нечастые.
  3. сопоставьте каждую ячейку с уже установленными вокруг нее: если соседняя ячейка имеет соответствующий бит (1 бит в ячейке слева и т. д.), принудительно включите этот бит в этой ячейке, и если он выключен, принудительно выключите его в этой ячейке.
  4. найдите способ гарантировать, что начало и конец подключения (дополнительные исследования, необходимые здесь).
  5. управление, чтобы заполнить все ячейки и не создавать пустоты (требуется больше исследований).

вот отображение" сырого " массива с точки зрения символьной графики:

    ┌│───────┐  
    │└─┐┌┐│  │  
    ││┌┴┤│├─┐│  
    │├┴─┘│└┐││  
    │└─┐──┐│││  
    │┌┬┴─┌┘│││  
    ││├┐│└┬┘││  
    │└┤│└┬┴─┤│  
    │─┴┴─┘──┤│  
    └───────│┘  

при рендеринге результата я отображаю каждую ячейку, используя сетку 3x4 символьной графики. Вот пример:

╔═══╡  ╞═══════════════════════════════╗
║░░░│  │░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░║
║░░╔╡  ╞════════════════════════════╗░░║
║░░║│  └───────┐┌──────┐┌──┐        ║░░║
║░░║│          ││      ││  │        ║░░║
║░░║└───────┐  ││  ┌┐  ││  │        ║░░║
║░░║┌──┐┌───┘  └┘  ││  ││  └───────┐║░░║
║░░║│  ││          ││  ││          │║░░║
║░░║│  ││  ┌────┐  ││  ││  ┌────┐  │║░░║
║░░║│  └┘  └────┘  ││  ││  └───┐│  │║░░║
║░░║│              ││  ││      ││  │║░░║
║░░║│  ┌───────────┘└──┘└───┐  ││  │║░░║
║░░║│  └───────┐┌──────────┐│  ││  │║░░║
║░░║│          ││          ││  ││  │║░░║
║░░║└───────┐  │└───────┐  │└──┘│  │║░░║
║░░║┌───────┘  └───┐┌───┘  │┌──┐│  │║░░║
║░░║│              ││      ││  ││  │║░░║
║░░║│  ┌┐  ┌───────┘│  ┌───┘│  ││  │║░░║
║░░║│  ││  └───┐┌──┐│  └────┘  ││  │║░░║
║░░║│  ││      ││  ││          ││  │║░░║
║░░║│  ││  ┌┐  ││  │└───┐  ┌───┘│  │║░░║
║░░║│  └┘  ││  ││  └────┘  └────┘  │║░░║
║░░║│      ││  ││                  │║░░║
║░░║└───┐  ││  │└───┐  ┌────────┐  │║░░║
║░░║┌───┘  └┘  └────┘  │┌───────┘  │║░░║
║░░║│                  ││          │║░░║
║░░║└──────────────────┘└───────┐  │║░░║
║░░╚════════════════════════════╡  ╞╝░░║
║░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░│  │░░░║
╚═══════════════════════════════╡  ╞═══╝

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