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
268 lines (267 loc) • 11.3 kB
JavaScript
export class Solver {
constructor(cells, start, goal) {
this.cells = cells;
this.mazeHeight = this.cells.length - 1;
this.mazeWidth = this.cells[0].length - 1;
if (start.column < 0 || start.row < 0) {
throw Error("start column/row must be greater than or equal to 0.");
}
if (goal.column < 0 || goal.row < 0) {
throw Error("goal column/row must be greater than or equal to 0.");
}
if (this.mazeHeight < start.row || this.mazeHeight < goal.row) {
throw Error(`start/goal rows must be less than maze height (${this.mazeHeight}).`);
}
if (this.mazeWidth < start.column || this.mazeWidth < goal.column) {
throw Error(`start/goal columns must be less than maze width (${this.mazeWidth}).`);
}
const openSet = [
Object.assign(Object.assign({}, start), { cost: 0 }),
];
const closedSet = [];
const calculateDistanceFromGoal = (currentCell, goalCell) => {
const { row, column } = currentCell;
const distanceToGoalRow = Math.abs(row - goalCell.row);
const distanceToGoalColumn = Math.abs(column - goalCell.column);
return distanceToGoalRow + distanceToGoalColumn;
};
const findAvailableCells = (currentCellInSet) => {
const { row, column, cost } = currentCellInSet;
const { walls } = cells[row][column];
const includedInSet = (set, row, column) => {
let cellInSet = false;
set.forEach((cell) => {
if (cell.row === row && cell.column === column) {
cellInSet = true;
}
});
return cellInSet;
};
if (!walls.left) {
const calulcatedColumn = column - 1;
if (!includedInSet(closedSet, row, calulcatedColumn) &&
!includedInSet(openSet, row, calulcatedColumn)) {
const calculatedCost = cost +
calculateDistanceFromGoal({ row, column: calulcatedColumn }, goal);
openSet.push({
row,
column: calulcatedColumn,
cost: calculatedCost,
previousCell: currentCellInSet,
});
}
}
if (!walls.right) {
const calulcatedColumn = column + 1;
if (!includedInSet(closedSet, row, calulcatedColumn) &&
!includedInSet(openSet, row, calulcatedColumn)) {
const calculatedCost = cost +
calculateDistanceFromGoal({ row, column: calulcatedColumn }, goal);
openSet.push({
row,
column: calulcatedColumn,
cost: calculatedCost,
previousCell: currentCellInSet,
});
}
}
if (!walls.up) {
const calculatedRow = row - 1;
if (!includedInSet(closedSet, calculatedRow, column) &&
!includedInSet(openSet, calculatedRow, column)) {
const calculatedCost = cost +
calculateDistanceFromGoal({ row: calculatedRow, column }, goal);
openSet.push({
row: calculatedRow,
column: column,
cost: calculatedCost,
previousCell: currentCellInSet,
});
}
}
if (!walls.down) {
const calculatedRow = row + 1;
if (!includedInSet(closedSet, calculatedRow, column) &&
!includedInSet(openSet, calculatedRow, column)) {
const calculatedCost = cost +
calculateDistanceFromGoal({ row: calculatedRow, column }, goal);
openSet.push({
row: calculatedRow,
column: column,
cost: calculatedCost,
previousCell: currentCellInSet,
});
}
}
};
let finishedPathfinding = false;
let goalCellCordinates = undefined;
while (!finishedPathfinding) {
if (openSet.length > 0) {
findAvailableCells(openSet[0]);
closedSet.push(openSet[0]);
openSet.shift();
openSet.sort((cellOne, cellTwo) => {
return cellOne.cost - cellTwo.cost;
});
goalCellCordinates = openSet.find((openSetCell) => {
return (openSetCell.column === goal.column && openSetCell.row === goal.row);
});
if (goalCellCordinates) {
finishedPathfinding = true;
}
}
else {
finishedPathfinding = true;
goalCellCordinates = undefined;
this.path = [];
}
}
const path = [];
if (goalCellCordinates) {
const constructPath = (cell) => {
const { row, column } = cell;
path.push({ row, column });
if (cell.previousCell) {
return constructPath(cell.previousCell);
}
path.reverse();
};
constructPath(goalCellCordinates);
}
this.path = path;
}
/**
* Returns an ordered array of cells, with each cell being a cell on the path through the maze.
*/
toJSON() {
return this.path;
}
/**
* Returns the string representation of the path through the maze
* e.g.
* _ _ _ _ _
* |S| | |
* |↓ _| | |_|
* |↳ ↴|_ _| |
* | |↓|_ ↱ ↴|
* |_ ↳ → ⇗|G|
* ¯ ¯ ¯ ¯
*/
toString() {
if (this.path.length === 0) {
return "";
}
const numberedPath = this.path.map((cell, index) => {
return Object.assign(Object.assign({}, cell), { step: index });
});
let stringRepresentation = "";
// Returns the cell that is next in the path
const findNextStepInPath = (currentStep) => {
return numberedPath.find((cell) => cell.step === currentStep + 1);
};
const cellInPath = (row, column) => {
return numberedPath.find((pathCell) => {
return row === pathCell.row && column === pathCell.column;
});
};
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++) {
const pathCell = cellInPath(row, column);
if (column === 0 && this.cells[row][column].walls.left) {
// Adds a wall to the left most cell
stringRepresentation += "|";
}
if (pathCell) {
const nextCellInPath = findNextStepInPath(pathCell.step);
const previousCellInPath = findNextStepInPath(pathCell.step - 2);
if (pathCell.step === 0) {
rowString += "S";
}
else if (nextCellInPath) {
// calculate the direction of travel
const nextRowChange = nextCellInPath.row - pathCell.row;
const nextColumnChange = nextCellInPath.column - pathCell.column;
let previousRowChange, previousColumnChange;
if (previousCellInPath) {
previousRowChange = previousCellInPath.row - pathCell.row;
previousColumnChange =
previousCellInPath.column - pathCell.column;
}
if (nextRowChange === 1) {
if (previousColumnChange === 1) {
rowString += "↶";
}
else if (previousColumnChange === -1) {
rowString += "↴";
}
else {
rowString += "↓";
}
}
else if (nextRowChange === -1) {
if (previousColumnChange === 1) {
rowString += "⇖";
}
else if (previousColumnChange === -1) {
rowString += "⇗";
}
else {
rowString += "↑";
}
}
else if (nextColumnChange === 1) {
if (previousRowChange === 1) {
rowString += "↱";
}
else if (previousRowChange === -1) {
rowString += "↳";
}
else {
rowString += "→";
}
}
else if (nextColumnChange === -1) {
rowString += "←";
}
}
else {
rowString += "G";
}
if (this.cells[row][column].walls.right) {
rowString += "|";
}
else {
rowString += " ";
}
}
else {
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;
}
stringRepresentation += "\n";
for (let bottomRow = 0; bottomRow < this.cells[this.mazeHeight].length; bottomRow++) {
const cellIsInPath = cellInPath(this.mazeHeight, bottomRow);
if (cellIsInPath) {
// Adds a bottom wall to the bottom cells only if cell in path
if (this.cells[this.mazeHeight][bottomRow].walls.down) {
stringRepresentation += " ¯";
}
}
else {
stringRepresentation += " ";
}
}
return stringRepresentation;
}
}