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
292 lines (291 loc) • 11.4 kB
JavaScript
import { Cell } from "./Cell.js";
import { Solver } from "./Solver.js";
// TODO fix circular reference
// solver is stored in maze, maze stores solver
/**
* A class to represent the generated maze. This is made of cells
* @see Cell
*/
export class Maze {
/**
* Constructs a 2D array of cells
* @param {*} width The width of the maze, i.e. how many cells each row contains
* @param {*} height The height of the maze, i.e. how many rows the maze contains
*/
constructor(width, height) {
this.cells = [];
// Create an [i, j] 2D array of cells
for (let i = 0; i < height; i++) {
const row = [];
for (let j = 0; j < width; j++) {
row.push(new Cell());
}
this.cells.push(row);
}
}
/**
* Returns if the cell has been visited or not
* @param {*} row The row index of the cell
* @param {*} column The column index ofsolution the cell
* @returns true if the cell has been visited; false if the cell hasn't been visited.
*/
getCellVisited(row, column) {
return this.cells[row][column].getCellVisited();
}
/**
* Marks a cell as visited
* @param {*} row The row index of the cell
* @param {*} column The column index of the cell
*/
visitCell(row, column) {
this.cells[row][column].setCellVisited(true);
}
/**
* Gets the first unvisited cell in the maze with visited neighbours and returns the cell and the neighbours information
* @returns If true: the first unvisited cell indicies and the indicies of its neighbours; false if no cell with visitedNeighbours exists
*/
getFirstUnvisitedCellWithVisitedNeighbour() {
const unvisitedCells = this.getUnvisitedCells();
for (let i = 0; i < unvisitedCells.length; i++) {
const visitedNeighbours = this.getVisitedNeigbourIndices(unvisitedCells[i].y, unvisitedCells[i].x);
if (visitedNeighbours.length > 0) {
return {
firstCell: unvisitedCells[i],
neighbours: visitedNeighbours,
};
}
}
return false;
}
getUnvisitedCells() {
const unvisitedCells = [];
for (let i = 0; i < this.cells.length; i++) {
for (let j = 0; j < this.cells[i].length; j++) {
if (!this.getCellVisited(i, j)) {
unvisitedCells.push({ x: j, y: i });
}
}
}
return unvisitedCells;
}
getTotalUnvisitedCells() {
return this.getUnvisitedCells().length;
}
/**
* Removes the wall of the selected cell
* @param {*} row The row index of the cell
* @param {*} column The column index of the cell
* @param {string} direction left;right;up;down. The wall that should be removed.
*/
removeWall(row, column, direction) {
this.cells[row][column].removeWall(direction);
if (direction === "right" && column + 1 < this.cells[row].length) {
this.cells[row][column + 1].removeWall("left");
}
else if (direction === "left" && column - 1 >= 0) {
this.cells[row][column - 1].removeWall("right");
}
else if (direction === "up" && row - 1 >= 0) {
this.cells[row - 1][column].removeWall("down");
}
else if (direction === "down" && row + 1 < this.cells.length) {
this.cells[row + 1][column].removeWall("up");
}
}
/**
* Returns if a wall exists in the specified direction
* @param {*} row The row index of the cell
* @param {*} column The column index of the cell
* @param {string} direction left;right;up;down. The wall that should be removed.
* @returns {boolean} true if the wall exists; false if the wall does not exist.
*/
getWallStatus(row, column, direction) {
return this.cells[row][column].getWallStatus(direction);
}
/**
* Gets the indicies of neighbouring cells
* @param {*} row The row index of the cell
* @param {*} column The column index of the cell
* @returns {{[]}} An object containing the indicies of neighbouring cells
*/
getCellNeighbourIndices(row, column) {
const neighbourIndices = {};
const mazeHeight = this.cells.length;
const mazeWidth = this.cells[0].length;
// Get up neighbour
if (row > 0) {
neighbourIndices.up = { y: row - 1, x: column };
}
// Get down neighbour
if (row < mazeHeight - 1) {
neighbourIndices.down = { y: row + 1, x: column };
}
// Get left neighbour
if (column > 0) {
neighbourIndices.left = { y: row, x: column - 1 };
}
// Get right neighbour
if (column < mazeWidth - 1) {
neighbourIndices.right = { y: row, x: column + 1 };
}
return neighbourIndices;
}
/**
* Calls getCellNeighbourIndices, checks if each neighbour is unvisited and adds the unvisited cell's coordinates to an array
* @param {*} row The row index of the cell
* @param {*} column The column index of the cell
* @returns {[]} The indicies of unvisited neighours of the chosen cell
*/
getUnvisitedNeigbourIndices(row, column) {
const neighbourIndices = this.getCellNeighbourIndices(row, column);
const unvisitedNeighbours = [];
if (typeof neighbourIndices.up !== "undefined" &&
this.getCellVisited(neighbourIndices.up.y, neighbourIndices.up.x) ===
false) {
const cell = {
direction: "up",
x: neighbourIndices.up.x,
y: neighbourIndices.up.y,
};
unvisitedNeighbours.push(cell);
}
if (typeof neighbourIndices.down !== "undefined" &&
this.getCellVisited(neighbourIndices.down.y, neighbourIndices.down.x) ===
false) {
const cell = {
direction: "down",
x: neighbourIndices.down.x,
y: neighbourIndices.down.y,
};
unvisitedNeighbours.push(cell);
}
if (typeof neighbourIndices.left !== "undefined" &&
this.getCellVisited(neighbourIndices.left.y, neighbourIndices.left.x) ===
false) {
const cell = {
direction: "left",
x: neighbourIndices.left.x,
y: neighbourIndices.left.y,
};
unvisitedNeighbours.push(cell);
}
if (typeof neighbourIndices.right !== "undefined" &&
this.getCellVisited(neighbourIndices.right.y, neighbourIndices.right.x) === false) {
const cell = {
direction: "right",
x: neighbourIndices.right.x,
y: neighbourIndices.right.y,
};
unvisitedNeighbours.push(cell);
}
return unvisitedNeighbours;
}
/**
* Calls getCellNeighbourIndices, checks if each neighbour is visited and adds the visited cell's coordinates to an array
* @param {*} row The row index of the cell
* @param {*} column The column index of the cell
* @returns {[]} The indicies of visited neighours of the chosen cell
*/
getVisitedNeigbourIndices(row, column) {
const neighbourIndices = this.getCellNeighbourIndices(row, column);
const unvisitedNeighbours = [];
if (typeof neighbourIndices.up !== "undefined" &&
this.getCellVisited(neighbourIndices.up.y, neighbourIndices.up.x) === true) {
const cell = {
direction: "up",
x: neighbourIndices.up.x,
y: neighbourIndices.up.y,
};
unvisitedNeighbours.push(cell);
}
if (typeof neighbourIndices.down !== "undefined" &&
this.getCellVisited(neighbourIndices.down.y, neighbourIndices.down.x) ===
true) {
const cell = {
direction: "down",
x: neighbourIndices.down.x,
y: neighbourIndices.down.y,
};
unvisitedNeighbours.push(cell);
}
if (typeof neighbourIndices.left !== "undefined" &&
this.getCellVisited(neighbourIndices.left.y, neighbourIndices.left.x) ===
true) {
const cell = {
direction: "left",
x: neighbourIndices.left.x,
y: neighbourIndices.left.y,
};
unvisitedNeighbours.push(cell);
}
if (typeof neighbourIndices.right !== "undefined" &&
this.getCellVisited(neighbourIndices.right.y, neighbourIndices.right.x) === true) {
const cell = {
direction: "right",
x: neighbourIndices.right.x,
y: neighbourIndices.right.y,
};
unvisitedNeighbours.push(cell);
}
return unvisitedNeighbours;
}
/**
* Generates a solution for the maze
* @param {{row: number, column: number}} start the {row, column} coordinates of the starting cell
* @param {{row: number, column: number}} goal the {row, column} coordinates of the goal cell
*/
generateSolution(start, goal) {
const solvedMaze = new Solver(this.cells, start, goal);
this.solution = solvedMaze;
return this.solution;
}
/**
* @returns {string} The string represention of all cells within the maze.
* e.g.
* _ _ _
* | _|
* |_| | |
* | | | |
* |_ _ _|
**/
toString() {
let stringRepresentation = "";
for (let topRow = 0; topRow < this.cells[0].length; topRow++) {
// Adds a top wall to the top cells
stringRepresentation += this.cells[0][topRow].walls.up ? " _" : " ";
}
stringRepresentation += "\n";
for (let row = 0; row < this.cells.length; row++) {
let rowString = "";
for (let column = 0; column < this.cells[row].length; column++) {
if (column === 0 && this.cells[row][column].walls.left) {
// Adds a wall to the left most cell
stringRepresentation += "|";
}
rowString += this.cells[row][column].toString();
}
// Add a new line if the last cell of the row
stringRepresentation +=
row + 1 < this.cells.length ? rowString + "\n" : rowString;
}
return stringRepresentation;
}
/**
* Returns a JSON representation of the maze.
* The JSON object contains a rows array, which contains an array for each row.
* Each row array contains the JSON representations of each cell within the Maze for that row.
*/
toJSON() {
const JSONRepresentation = {
rows: [],
};
for (let row = 0; row < this.cells.length; row++) {
const rowArray = [];
for (let cell = 0; cell < this.cells[row].length; cell++) {
rowArray.push(this.cells[row][cell].toJSON());
}
JSONRepresentation.rows.push(rowArray);
}
return JSONRepresentation;
}
}