UNPKG

maze-generation

Version:

A package to generate mazes using the depth first or hunt and kill algorithm. Mazes can be generated with seed values for reproducibility

150 lines (149 loc) 6.93 kB
import Prando from "prando"; import { Maze } from "./Maze.js"; export class Generator { constructor(width, height) { this.width = width; this.height = height; } /** * Returns the generated maze from the generation algorithm pass as a parameter. * @param {*} algorithm the algorithm to use to generate the maze * @param {*} prando A prando object constructed with the seed to generate the maze */ generateMaze(algorithm = "DEPTHFIRST", prando = new Prando()) { const capitalisedAlgorithm = algorithm.toUpperCase(); if (capitalisedAlgorithm === "DEPTHFIRST") { return this.depthFirst(prando); } else if (capitalisedAlgorithm === "HUNTANDKILL") { return this.huntAndKill(prando); } if (capitalisedAlgorithm == "") { return this.depthFirst(prando); } else { throw new Error(`${algorithm} is an Invalid Maze Generation Algorithm`); } } /** * Generates a maze using the Depth First algorithm * @param {*} prando A prando object constructed with the seed to generate the maze. Used as arandom number generator. */ depthFirst(prando) { const rng = prando; const generatedMaze = new Maze(this.width, this.height); const cellStack = []; // Set currentCell = random cell const randomCell = { randomHeight: rng.nextInt(0, this.height - 1), randomWidth: rng.nextInt(0, this.width - 1), }; // Select random cell and mark as visited let currentCell = { x: randomCell.randomWidth, y: randomCell.randomHeight }; // Generate the maze do { generatedMaze.visitCell(currentCell.y, currentCell.x); // Generate a list of unvisited neighbours const unvisitedNeighbours = generatedMaze.getUnvisitedNeigbourIndices(currentCell.y, currentCell.x); // Find which of the unvisited neighbours can be visited const validDirections = []; for (let i = 0; i < unvisitedNeighbours.length; i++) { validDirections.push(unvisitedNeighbours[i].direction); } if (validDirections.length > 0) { // Push current cell to stack to allow for backtracking cellStack.push(currentCell); // Randomly select a valid direction and remove the wall const nextDirection = validDirections[rng.nextInt(0, validDirections.length - 1)]; generatedMaze.removeWall(currentCell.y, currentCell.x, nextDirection); // Move to the cell in the direction of the removed wall for (let i = 0; i < unvisitedNeighbours.length; i++) { if (unvisitedNeighbours[i].direction === nextDirection) { currentCell = { x: unvisitedNeighbours[i].x, y: unvisitedNeighbours[i].y, }; } } } else { const nextCell = cellStack.pop(); if (nextCell) { currentCell = nextCell; } } } while (cellStack.length > 0); return generatedMaze; } /** * Generates a maze using the Hunt And Kill algorithm * @param {*} prando A prando object constructed with the seed to generate the maze. Used as arandom number generator. */ huntAndKill(prando) { const rng = prando; let generatedMaze = new Maze(this.width, this.height); // Set currentCell = random cell const randomCell = { randomHeight: rng.nextInt(0, this.height - 1), randomWidth: rng.nextInt(0, this.width - 1), }; // Select random cell and mark as visited let currentCell = { x: randomCell.randomWidth, y: randomCell.randomHeight }; generatedMaze = this.randomisedWalk(currentCell, rng, generatedMaze); while (generatedMaze.getTotalUnvisitedCells() > 0) { const firstUnvisitedCellNeighbours = generatedMaze.getFirstUnvisitedCellWithVisitedNeighbour(); if (!firstUnvisitedCellNeighbours) { throw Error("No univisted cells fetched, whilst expecting getTotalUnvisitedCells() > 0"); } currentCell = firstUnvisitedCellNeighbours.firstCell; const neighbours = firstUnvisitedCellNeighbours.neighbours; generatedMaze.removeWall(currentCell.y, currentCell.x, neighbours[rng.nextInt(0, neighbours.length - 1)].direction); generatedMaze.visitCell(currentCell.y, currentCell.x); generatedMaze = this.randomisedWalk(currentCell, rng, generatedMaze); } return generatedMaze; } /** * Get the unvisited neighbours of the current cell * @param {[]} unvisitedNeighbours Generated using maze.getUnvisitedNeigbourIndices */ getValidDirections(unvisitedNeighbours) { const validDirections = []; for (let i = 0; i < unvisitedNeighbours.length; i++) { validDirections.push(unvisitedNeighbours[i].direction); } return validDirections; } /** * Performs a randomised walk from the specified current cell * @param {{x: int, y: int}} currentCell * @param {*} prando Prando random number generator * @param {*} maze A Maze object * @returns A modified maze object */ randomisedWalk(currentCell, prando, maze) { const modifiedMaze = maze; let unvisitedNeighbours = modifiedMaze.getUnvisitedNeigbourIndices(currentCell.y, currentCell.x); let validDirections = this.getValidDirections(unvisitedNeighbours); // If there's an unvisited neighbour while (validDirections.length > 0) { // Randomly select a valid direction and remove the wall const nextDirection = validDirections[prando.nextInt(0, validDirections.length - 1)]; modifiedMaze.removeWall(currentCell.y, currentCell.x, nextDirection); // Move to the cell in the direction of the removed wall for (let i = 0; i < unvisitedNeighbours.length; i++) { if (unvisitedNeighbours[i].direction === nextDirection) { currentCell = { x: unvisitedNeighbours[i].x, y: unvisitedNeighbours[i].y, }; modifiedMaze.visitCell(currentCell.y, currentCell.x); } } // Generate a list of unvisited neighbours unvisitedNeighbours = modifiedMaze.getUnvisitedNeigbourIndices(currentCell.y, currentCell.x); validDirections = this.getValidDirections(unvisitedNeighbours); } return modifiedMaze; } }