Implementing a randomly generated maze using Prim's Algorithm

时间秒杀一切 提交于 2019-12-31 22:39:12

问题


I am trying to implement a randomly generated maze using Prim's algorithm.

I want my maze to look like this:

however the mazes that I am generating from my program look like this:

I'm currently stuck on correctly implementing the steps highlighted in bold:

  1. Start with a grid full of walls.
  2. Pick a cell, mark it as part of the maze. Add the walls of the cell to the wall list.
  3. While there are walls in the list:
    • **1. Pick a random wall from the list. If the cell on the opposite side isn't in the maze yet:
        1. Make the wall a passage and mark the cell on the opposite side as part of the maze.**
        1. Add the neighboring walls of the cell to the wall list.
      1. Remove the wall from the list.

from this article on maze generation.

How do I determine whether or not a cell is a valid candidate for the wall list? I would like to change my algorithm so that it produces a correct maze. Any ideas that would help me solve my problem would be appreciated.


回答1:


The description in the Wikipedia article truly deserves improvement.

The first confusing part of the article is, that the description of the randomized Prim's algorithm does not elaborate on the assumed data structure used by the algorithm. Thus, phrases like "opposite cell" become confusing.

Basically there are 2 main approaches "maze generator programmers" can opt for:

  1. Cells have walls or passages to their 4 neighbors. The information about walls/passages is stored and manipulated.
  2. Cells can either be Blocked (walls) or Passages, without storing any extra connectivity information.

Depending on which model (1) or (2) the reader has in mind when reading the description of the algorithm, they either understand or do not understand.

Me, personally I prefer to use cells as either walls or passages, rather than fiddling with dedicated passage/wall information.

Then, the "frontier" patches have a distance of 2 (rather than 1) from a passage. A random frontier patch from the list of frontier patches is selected and connected to a random neighboring passage (at distance 2) by means of also making the cell between frontier patch and neighboring passage a passage.

Here my F# implementation of how it looks like:

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

A resulting maze then can look like this:

Here an attempt to describe my solution in wikipedia "algorithm" style:

  1. A Grid consists of a 2 dimensional array of cells.
  2. A Cell has 2 states: Blocked or Passage.
  3. Start with a Grid full of Cells in state Blocked.
  4. Pick a random Cell, set it to state Passage and Compute its frontier cells. A frontier cell of a Cell is a cell with distance 2 in state Blocked and within the grid.
  5. While the list of frontier cells is not empty:
    1. Pick a random frontier cell from the list of frontier cells.
    2. Let neighbors(frontierCell) = All cells in distance 2 in state Passage. Pick a random neighbor and connect the frontier cell with the neighbor by setting the cell in-between to state Passage. Compute the frontier cells of the chosen frontier cell and add them to the frontier list. Remove the chosen frontier cell from the list of frontier cells.



回答2:


A simple Java implementation of Prim's algorithm:

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();
    }
}

A sample output of new Maze(20,20).toString() is:

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



回答3:


Try weighting the walls with uniquely random weights at the very beginning of the procedure. That list of weights will never change. When you choose your next wall from the list of available walls, choose the wall with minimal weight.




回答4:


Your solution doesn't look very wrong. In particular, it is a maze, and (if you cannot walk diagonally) there is a unique path from each (open) location to each other (open) location. The only problem with it seems to be the style.

If you consider the "correct" maze you posted, without the outer border, and take the topleft cell to be (0,0), you can observe that passages and walls, in a sense, alternate. Every cell where both coordinates are even must be a passage, and each cell where both coordinates are odd must be a wall. So the only cells where you have a choice are the ones where one coordinate is even and the other is odd.

Let a cell (x,y) in the middle of the field, where both coordinate are even, be given. This cell must be a passage. The cells (x-1,y), (x+1,y), (x,y-1) and (x,y+1) are the potential walls surrounding it, and the the cells (x-2,y), (x+2,y), (x,y-2) and (x,y+2) the squares on the opposites of those walls, respectively.

With this information you can just implement you algorithm, with the additional requirement that in step 2 you have to pick a cell where both coordinates are even.




回答5:


The simple answer to your question is that when adding an edge you need to check if the edge implies removing a wall that is the last neighbour of any of its neighbouring wall pieces.

This will prevent any walls from being connected only by the corner.




回答6:


On my own I came up with something quite different before I researched the issue at all. See if you think it is a helpful approach.

Long ago when I saw IBM PC Character Graphics (glyphs that are part of the Code Page) I thought about creating mazes that way. My approach has two phases:

  1. Generating a maze in an array of integers, using bit-coded values 1-15 to indicate directions open at each cell of the maze
  2. Rendering it to a visible form. So the walls are not a consideration until I am displaying the maze.

Every cell begins as 0 (unselected) and then any of the 4 bits (1 = to right, 2 = down, 4 = left, 8 = up) can be switched on. Naively, you can just choose a random number from 1-15 at every cell, except for five things:

  1. Begin by drawing a "wall" of corridors and corners around the entire array, and leave a pass-through at two points. This is the easiest way to handle the boundary conditions.
  2. Weight the choices so that dead-ends are rare and straight or corner corridors are common, full intersections infrequent.
  3. Match up each cell to the already set ones around it: if an adjacent cell has the appropriate bit On (1 bit in the cell to the left, and so on) force that bit on in this cell, and if it is Off, force it off in this cell.
  4. Find a way to ensure that the start and end hook up (further research required here).
  5. Manage to fill all cells and not create voids (more research required).

Here is a display of the "raw" array in terms of character graphics:

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

When rendering the result, I display each cell using a 3x4 grid of character graphics. Here is an example:

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

See what you can do with this method. (A different choice of font makes it look better than here, the lines all join seamlessly - must be monospace, of course).



来源:https://stackoverflow.com/questions/29739751/implementing-a-randomly-generated-maze-using-prims-algorithm

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!