Implementing a randomly generated maze using Prim's Algorithm
Asked Answered
V

9

19

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

I want my maze to look like this: enter image description here

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

enter image description here

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.

Variation answered 20/4, 2015 at 5:13 Comment(3)
Depends a bit how you wrote your program. If you have an m,n array and use that to mark used cells, it is not hard to do. From your maze picture, yet I think the mistake is that you remove more than 1 wall in your implementation. The "good maze" always only has 1 open path (removed wall). Your maze, e.g. at the starting point has 2 open paths. Well kind of. The good maze never "connects" paths already existing...Gigi
I do have a 2D array to store values for each cell. At each iteration of the while loop, I only remove one wall. I think the problem lies in my function that adds frontier cells to the wall list.Variation
Does anyone else notice something funny 22 seconds into the animation on the article you posted?Incipit
G
22

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:

enter image description here

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.
Gigi answered 20/4, 2015 at 21:41 Comment(7)
I am still trying to understand your way of doing this. Can you elaborate more on why frontier cells have a distance of 2 from a passage?Variation
Because a cell can be a wall or a passage in my data model. Others have a data model where a cell has a set of 4 "walls" they can turn into "passages". Those with that model type Cell = { North : bool; East : bool; West : bool; South : bool } need to inspect the adjoining cells at distance 1 as the cells of them contain the wall information. In my case, a cell can be wall or passage and so, as in the other model, the next cell is "past a wall".Gigi
Thank you so much. I just got my program to produce a proper maze.Variation
@BitTickler, does that mean the minimum size of the maze must be 5x5 since your algorithm attempts to find frontier cells that are 2 away from the current cell?Artiste
@KurtMueller Maybe :) 3x3 could also work trivially (a maze with 1 passage cell surrounded by walls.)Gigi
When we select a frontier cell randomly in step 5, do we set it to passage as well or do we only set the cell in between to passage?Hellhole
What I find confusing is why this method is guaranteed to visit all parts of the maze, and not leave any 2x2 blocks of wall anywhere.Hydroplane
S
7

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:

▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
▓   ▓     ▓       ▓ ▓▓
▓ ▓▓▓ ▓▓▓▓▓▓▓▓▓ ▓▓▓ ▓▓
▓     ▓ ▓ ▓ ▓   ▓ ▓ ▓▓
▓ ▓▓▓▓▓ ▓ ▓ ▓▓▓ ▓ ▓ ▓▓
▓   ▓ ▓ ▓   ▓       ▓▓
▓ ▓ ▓ ▓ ▓ ▓▓▓▓▓▓▓ ▓ ▓▓
▓ ▓ ▓ ▓ ▓   ▓ ▓   ▓ ▓▓
▓ ▓▓▓ ▓ ▓▓▓ ▓ ▓ ▓▓▓▓▓▓
▓   ▓     ▓ ▓ ▓   ▓ ▓▓
▓ ▓▓▓▓▓ ▓▓▓ ▓ ▓ ▓▓▓ ▓▓
▓   ▓   ▓           ▓▓
▓ ▓ ▓ ▓ ▓▓▓ ▓▓▓▓▓▓▓▓▓▓
▓ ▓   ▓   ▓       ▓ ▓▓
▓ ▓▓▓▓▓▓▓ ▓ ▓▓▓▓▓ ▓ ▓▓
▓ ▓     ▓   ▓   ▓ ▓ ▓▓
▓▓▓ ▓▓▓ ▓▓▓ ▓ ▓▓▓▓▓ ▓▓
▓   ▓               ▓▓
▓▓▓ ▓ ▓▓▓ ▓▓▓ ▓▓▓ ▓ ▓▓
▓   ▓ ▓   ▓     ▓ ▓ ▓▓
▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
Sappy answered 22/4, 2015 at 22:45 Comment(0)
B
4

The following is a commented Java implementation base on the accepted answer:

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;

/**
 * Generate a maze using Prime's algorithm
 * Based on: https://mcmap.net/q/632447/-implementing-a-randomly-generated-maze-using-prim-39-s-algorithm
 *
 * @author c0der
 * 25 Jun 2020
 *
 */
public class PrimeMazeGenerator implements Runnable {

    private static final int[][] DIRECTIONS = { //distance of 2 to each side
            { 0 ,-2}, // north
            { 0 , 2}, // south
            { 2 , 0}, // east
            {-2 , 0}, // west
    };

    private long delay = 0;
    private final CellModel[][] cells;
    private final Random random;

    public PrimeMazeGenerator(CellModel[][] cells) {
        this.cells = cells;
        random = new Random();
    }

    @Override
    public void run() {
        primMazeGeneration();
    }

    public void execute() {
        new Thread(this).start();
    }

    void primMazeGeneration() {

        //Start with a grid full of cellModelViews in state wall (not a path).
        for(int i = 0; i < cells.length; i++){
            for(int j = 0; j < cells[0].length ; j++){
                cells[i][j].setWall(true);
            }
        }

        //Pick a random cell
        int x = random.nextInt(cells.length);
        int y = random.nextInt(cells[0].length);

        cells[x][y].setWall(false); //set cell to path
        //Compute cell frontier and add it to a frontier collection
        Set<CellModel> frontierCells = new HashSet<>(frontierCellsOf(cells[x][y]));

        while (!frontierCells.isEmpty()){

            //Pick a random cell from the frontier collection
            CellModel frontierCell = frontierCells.stream().skip(random.nextInt(frontierCells.size())).findFirst().orElse(null);

            //Get its neighbors: cells in distance 2 in state path (no wall)
            List<CellModel> frontierNeighbors =  passageCellsOf(frontierCell);

            if(!frontierNeighbors.isEmpty()) {
                //Pick a random neighbor
                CellModel neighbor = frontierNeighbors.get(random.nextInt(frontierNeighbors.size()));
                //Connect the frontier cell with the neighbor
                connect(frontierCell, neighbor);
            }

            //Compute the frontier cells of the chosen frontier cell and add them to the frontier collection
            frontierCells.addAll(frontierCellsOf(frontierCell));
            //Remove frontier cell from the frontier collection
            frontierCells.remove( frontierCell);
            try {
                Thread.sleep(delay);
            } catch (InterruptedException ex) { ex.printStackTrace();}
        }
    }

    //Frontier cells: wall cells in a distance of 2
    private List<CellModel> frontierCellsOf(CellModel cell) {

        return cellsAround(cell, true);
    }

    //Frontier cells: passage (no wall) cells in a distance of 2
    private List<CellModel> passageCellsOf(CellModel cell) {

        return cellsAround(cell, false);
    }

    private List<CellModel> cellsAround(CellModel cell, boolean isWall) {

        List<CellModel> frontier = new ArrayList<>();
        for(int[] direction : DIRECTIONS){
            int newRow = cell.getRow() + direction[0];
            int newCol = cell.getColumn() + direction[1];
            if(isValidPosition(newRow, newCol) && cells[newRow][newCol].isWall() == isWall){
                frontier.add(cells[newRow][newCol]);
            }
        }

        return frontier;
    }

    //connects cells which are distance 2 apart
    private void connect( CellModel frontierCellModelView, CellModel neighbour) {

        int inBetweenRow = (neighbour.getRow() + frontierCellModelView.getRow())/2;
        int inBetweenCol = (neighbour.getColumn() + frontierCellModelView.getColumn())/2;
        frontierCellModelView.setWall(false);
        cells[inBetweenRow][inBetweenCol].setWall(false);
        neighbour.setWall(false);
    }

    private boolean isValidPosition(int row, int col) {
        return row >= 0 && row < cells.length
                    && col >= 0 && col < cells[0].length;
    }

    public PrimeMazeGenerator setDelay(long delay) {
        this.delay = delay;
        return this;
    }
}

CellModel.java:

/**
 * Maze cell representation
 *
 * @author c0der
 * 25 Jun 2020
 *
 */
public class CellModel{

    private final int row, column;
    private boolean isWall;
    //support to fire property change events
    private PropertyChangeSupport pcs;

    public CellModel(int row, int column)  {
       this(row, column, false);
    }

    public CellModel(int row, int column, boolean isWall) {
        this.row = row;
        this.column = column;
        this.isWall = isWall;
    }

    @Override
    public boolean equals(Object obj) {
        if(!(obj instanceof CellModel)) return false;
        CellModel other = (CellModel)obj;
        return row == other.getRow() && column == other.getColumn();
    }

    public void setPropertChangeSupport(PropertyChangeSupport pcs) {
        this.pcs = pcs;
    }

    private void firePropertyChange(String name, Object oldValue, Object newValue) {
        if(pcs != null) {
            pcs.firePropertyChange(name, oldValue, newValue);
        }
    }

    /**
    * Get {@link #isWall}
    */
    public boolean isWall() {
        return isWall;
    }

    /**
    * Set {@link #isWall}
    */
    public void setWall(boolean isWall) {
        Object old = this.isWall;
        this.isWall = isWall;
        firePropertyChange("Wall", old, isWall);
    }

    /**
    * Get {@link #row}
    */
    public int getRow() {
        return row;
    }

    /**
    * Get {@link #column}
    */
    public int getColumn() {
        return column;
    }

    @Override
    public String toString() {
        return  "["+ (isWall ? "Wall " : "Path " ) +  row + "-" + column + "]";
    }

    /* (non-Javadoc)
     * @see java.lang.Object#hashCode()
     */
    @Override
    public int hashCode() {
        return 17*row + 31*column;
    }
}

CellModel[][] cells can be obtained from MazeModel:

/**
 * Maze representation
 *
 * @author c0der
 * 25 Jun 2020
 *
 */
public class MazeModel {

    /**
     * Collection to represent an entire maze
     */
    private final CellModel[][] cellModels;

    public MazeModel(int rows, int columns) {

        cellModels = new CellModel[rows][columns];
        for(int row=0; row <cellModels.length; row++) {
            for(int col=0; col<cellModels[row].length; col++) {
                CellModel cellModel = new CellModel(row, col);
                cellModels[row][col] = cellModel;
            }
        }
    }

    /**
    * Get {@link #cellModels}
    */
    public CellModel[][] getCellModels() {
        return cellModels;
    }
}

A complete runnable code including Swing as well as JavaFx gui is available on this repository.


enter image description here

Bricabrac answered 25/6, 2020 at 13:53 Comment(1)
I tried to implement this in C#, but it doesn't generate the maze correctly. I don't see how your algorithm avoids generating "isolated" walls, in other words, a wall cell surrounded by 8 'passage' cells.Hydroplane
V
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.

Vernon answered 20/4, 2015 at 5:35 Comment(3)
I am not sure how that would produce a different result, but I can give your suggestion a try.Variation
@Variation It works because it changes Prim's algorithm into the much better Kruskal's algorithm. :-)Vernon
I start to understand the problem. The wikipedia article is badly written. Depending on how people see the problem, they either maintain a wall list located between cells or they try to use cells themselves as wall or passage. The article the question refers to does not specify. If one thinks of cells as possible walls, the "what is the opposite" question comes up.Gigi
S
3

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.

Seibold answered 20/4, 2015 at 21:31 Comment(0)
I
2

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.

Incipit answered 22/4, 2015 at 0:17 Comment(0)
F
2

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).

Fraenum answered 27/8, 2015 at 14:45 Comment(0)
D
1

Port to javascript of mto simple java answer for randomized Prim's Algorithm (with optional portable bitmap file export):

const Grid = (width, height) => {
    const points = new Uint8Array(width * height)
    return {
        width, height, points,
        set: (x, y, v) => points[x + y * width] = v,
        get: (x, y) => points[x + y * width],
    }
}
const random_int = (m, M) => m + Math.floor(Math.random() * (M - m))

const WALL = 0
const PASSAGE = 1
const grid = Grid(21, 21)
const frontiers = []

let x = random_int(0, grid.width)
let y = random_int(0, grid.height)
frontiers.push([x, y, x, y])

while (frontiers.length > 0) {
    const random_frontier_index = random_int(0, frontiers.length)
    const f = frontiers[random_frontier_index]
    frontiers.splice(random_frontier_index, 1)
    x = f[2]
    y = f[3]
    if (grid.get(x, y) === WALL) {
        grid.set(f[0], f[1], PASSAGE)
        grid.set(x, y, PASSAGE)
        if ((x >= 2) && (grid.get(x - 2, y) === WALL)) {
            frontiers.push([x - 1, y, x - 2, y])
        }
        if ((y >= 2) && (grid.get(x, y - 2) === WALL)) {
            frontiers.push([x, y - 1, x, y - 2])
        }
        if ((x < grid.width - 2) && (grid.get(x + 2, y) === WALL)) {
            frontiers.push([x + 1, y, x + 2, y])
        }
        if ((y < grid.height - 2) && (grid.get(x, y + 2) === WALL)) {
            frontiers.push([x, y + 1, x, y + 2])
        }
    }
}

// (optional) export to portable bitmap file format (PBM) 
const grid_to_pbm = ({ width, height, points }) => [
    'P1', [width, height].join(' '), ...points.map(x => x ? 0 : 1), ''
].join("\n")

import fs from 'node:fs'
fs.writeFileSync('maze.bpm', grid_to_pbm(grid))

final result

A maze produced by this algorithm, walls are black, passages are white

An animation of the algorithm

An animation of the algorithm, walls are black, passages are white, current frontier in green, frontiers in red

Dashboard answered 8/5 at 11:49 Comment(0)
S
0

I think your code is not working as expected because you forgot to delete 2 identical frontier cells in your list of frontier cells. Note that two passage cells can share the same frontier cell

Schwarz answered 18/9, 2022 at 4:57 Comment(1)
that feels a bit more like a comment, than answer :)Risner

© 2022 - 2024 — McMap. All rights reserved.