Repository: keesiemeijer/maze-generator
Branch: master
Commit: 29da89fd5b43
Files: 11
Total size: 49.0 KB
Directory structure:
gitextract_6ojt6g7z/
├── LICENSE
├── README.md
├── index.html
└── src/
├── app.js
├── color-picker.js
├── entries.js
├── globals.js
├── human-colours-en-gb.js
├── maze.js
├── solver.js
└── utils.js
================================================
FILE CONTENTS
================================================
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2017 Kees Meijer
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# maze-generator
Create mazes using the [recursive backtracking algorithm](https://en.wikipedia.org/wiki/Maze_generation_algorithm#Recursive_backtracker).
Check out the [maze generator demo](https://keesiemeijer.github.io/maze-generator/).
**Note**: There are restrictions for the maze dimensions you can use. You can remove the restrictions in the [globals.js](https://github.com/keesiemeijer/maze-generator/blob/master/src/globals.js) file. Be aware that the larger the maze dimensions, the more memory is consumed. With recursive backtracking the whole maze is stored in memory.

### LICENSE
MIT
================================================
FILE: index.html
================================================
Maze generator
Maze Generator
Create, solve and download random maze puzzles in any size or color with this online tool. Enter the values for your maze design below and click the "Generate Maze" button.
The recursive backtracking algorithm is used to create the mazes.
For more information check out the Github Repository .
Sorry... this site requires JavaScript to generate a maze. Please enable it in your browser
================================================
FILE: src/app.js
================================================
// Global variables
let mazeNodes = {};
// Check if globals are defined
if (typeof maxMaze === 'undefined') {
maxMaze = 0;
}
if (typeof maxSolve === 'undefined') {
maxSolve = 0;
}
if (typeof maxCanvas === 'undefined') {
maxCanvas = 0;
}
if (typeof maxCanvasDimension === 'undefined') {
maxCanvasDimension = 0;
}
if (typeof maxWallsRemove === 'undefined') {
maxWallsRemove = 300;
}
// Update remove max walls html
const removeMaxWallsText = document.querySelector('.desc span');
if (removeMaxWallsText) {
removeMaxWallsText.innerHTML = maxWallsRemove;
}
const removeWallsInput = document.getElementById('remove_walls');
if (removeWallsInput) {
removeWallsInput.max = maxWallsRemove;
}
const download = document.getElementById("download");
download.addEventListener("click", downloadImage, false);
download.setAttribute('download', 'maze.png');
function initMaze() {
download.setAttribute('download', 'maze.png');
download.innerHTML = 'download maze';
const settings = {
width: getInputIntVal('width', 20),
height: getInputIntVal('height', 20),
wallSize: getInputIntVal('wall-size', 10),
removeWalls: getInputIntVal('remove_walls', 0),
entryType: '',
bias: '',
color: '#000000',
backgroundColor: '#FFFFFF',
solveColor: '#cc3737',
// restrictions
maxMaze: maxMaze,
maxCanvas: maxCanvas,
maxCanvasDimension: maxCanvasDimension,
maxSolve: maxSolve,
maxWallsRemove: maxWallsRemove,
}
const colors = ['color', 'backgroundColor', 'solveColor'];
for (let i = 0; i < colors.length; i++) {
const colorInput = document.getElementById(colors[i]);
settings[colors[i]] = colorInput.value
if (!isValidHex(settings[colors[i]])) {
let defaultColor = colorInput.parentNode.dataset.default;
colorInput.value = defaultColor;
settings[colors[i]] = defaultColor;
}
const colorSample = colorInput.parentNode.querySelector('.color-sample');
colorSample.style = 'background-color: ' + settings[colors[i]] + ';';
}
if (settings['removeWalls'] > maxWallsRemove) {
settings['removeWalls'] = maxWallsRemove;
if (removeWallsInput) {
removeWallsInput.value = maxWallsRemove;
}
}
const entry = document.getElementById('entry');
if (entry) {
settings['entryType'] = entry.options[entry.selectedIndex].value;
}
const bias = document.getElementById('bias');
if (bias) {
settings['bias'] = bias.options[bias.selectedIndex].value;
}
const maze = new Maze(settings);
maze.generate();
maze.draw();
if (download && download.classList.contains('hide')) {
download.classList.toggle("hide");
}
const solveButton = document.getElementById("solve");
if (solveButton && solveButton.classList.contains('hide')) {
solveButton.classList.toggle("hide");
}
mazeNodes = {}
if (maze.matrix.length) {
mazeNodes = maze;
}
location.href = "#";
location.href = "#generate";
}
function downloadImage(e) {
const image = document.getElementById('maze').toDataURL("image/png");
image.replace("image/png", "image/octet-stream");
download.setAttribute("href", image);
}
function initSolve() {
const solveButton = document.getElementById("solve");
if (solveButton) {
solveButton.classList.toggle("hide");
}
download.setAttribute('download', 'maze-solved.png');
download.innerHTML = 'download solved maze';
if ((typeof mazeNodes.matrix === 'undefined') || !mazeNodes.matrix.length) {
return;
}
const solver = new Solver(mazeNodes);
solver.solve();
if (mazeNodes.wallsRemoved) {
solver.drawAstarSolve();
} else {
solver.draw();
}
mazeNodes = {}
}
================================================
FILE: src/color-picker.js
================================================
(function() {
// Amount of colors in a row
const row = 11;
// Classes
const colorPickerClass = 'color-picker';
const colorSampleClass = 'color-sample';
const paletteClass = 'palette';
const screenReaderClass = 'screen-reader-text';
// Todo: Use object with color description for accessibility
var hexColors = [
'#000000',
'#191919',
'#323232',
'#4b4b4b',
'#646464',
'#7d7d7d',
'#969696',
'#afafaf',
'#c8c8c8',
'#e1e1e1',
'#ffffff',
'#820000',
'#9b0000',
'#b40000',
'#cd0000',
'#e60000',
'#ff0000',
'#ff1919',
'#ff3232',
'#ff4b4b',
'#ff6464',
'#ff7d7d',
'#823400',
'#9b3e00',
'#b44800',
'#cd5200',
'#e65c00',
'#ff6600',
'#ff7519',
'#ff8532',
'#ff944b',
'#ffa364',
'#ffb27d',
'#828200',
'#9b9b00',
'#b4b400',
'#cdcd00',
'#e6e600',
'#ffff00',
'#ffff19',
'#ffff32',
'#ffff4b',
'#ffff64',
'#ffff7d',
'#003300',
'#004d00',
'#008000',
'#00b300',
'#00cc00',
'#00e600',
'#1aff1a',
'#4dff4d',
'#66ff66',
'#80ff80',
'#b3ffb3',
'#001a4d',
'#002b80',
'#003cb3',
'#004de6',
'#0000ff',
'#0055ff',
'#3377ff',
'#4d88ff',
'#6699ff',
'#80b3ff',
'#b3d1ff',
'#003333',
'#004d4d',
'#006666',
'#009999',
'#00cccc',
'#00ffff',
'#1affff',
'#33ffff',
'#4dffff',
'#80ffff',
'#b3ffff',
'#4d004d',
'#602060',
'#660066',
'#993399',
'#ac39ac',
'#bf40bf',
'#c653c6',
'#cc66cc',
'#d279d2',
'#d98cd9',
'#df9fdf',
'#660029',
'#800033',
'#b30047',
'#cc0052',
'#e6005c',
'#ff0066',
'#ff1a75',
'#ff3385',
'#ff4d94',
'#ff66a3',
'#ff99c2',
];
const colorPickers = document.querySelectorAll('.' + colorPickerClass);
const palette = '
';
let paletteHasFocus = false;
let desc = "Use a hex color code or use the tab key to select a color.";
desc += ' Use the arrow keys to scroll through all colors. Use the space or return key to select the color.';
desc += ' Use the escape key to close the palette.'
for (let i = 0; i < colorPickers.length; i++) {
// Create aria describedby element for the color input
var describedby = document.createElement("p");
describedby.style.display = 'none';
describedby.id = 'desc-' + i;
describedby.innerHTML = desc;
// Insert describedby description
colorPickers[i].insertAdjacentElement('afterbegin', describedby)
const colorInput = colorPickers[i].querySelector('input');
// Show color palette on input focus
colorInput.addEventListener("focus", showColorPalette, false);
// Check if tab key is used to focus a color in the palette
colorInput.addEventListener("keydown", inputTabPressed, false);
// Update color sample after key up
colorInput.addEventListener("keyup", updateColorSample, false);
// Add describedby attribute
colorInput.setAttribute("aria-describedby", 'desc-' + i);
// Insert color palette
colorPickers[i].insertAdjacentHTML('beforeend', palette);
// Get inserted palette
const colorPalette = colorPickers[i].querySelector('.' + paletteClass);
for (let j = 0; j < hexColors.length; j++) {
var colorDiv = document.createElement("div");
var colorDivText = document.createElement("span");
colorDivText.className = screenReaderClass;
colorDivText.innerHTML = hexColors[j];
colorDiv.appendChild(colorDivText);
// Make color divs tabbable.
colorDiv.tabIndex = 0;
colorDiv.style = 'background-color: ' + hexColors[j] + ';';
colorDiv.setAttribute("role", "button");
colorDiv.setAttribute('data-index', j + 1);
colorPalette.appendChild(colorDiv);
// Get RGB color from background
let rgbColor = colorDiv.style.backgroundColor;
// Get human readable colorname
let colorLabel = getHumanReadableColor(rgbColor, hexColors[j]);
if (colorLabel.length) {
colorDiv.setAttribute("aria-label", colorLabel);
}
// Check if a color is the new focused element
colorDiv.addEventListener("blur", colorBlur);
// Navigate colors in palette
colorDiv.addEventListener("keyup", colorNavigation, false);
}
colorPalette.onmouseenter = function() {
paletteHasFocus = true;
}
colorPalette.onmouseleave = function() {
paletteHasFocus = false;
}
// Close palette if palette or color is clicked
colorPalette.addEventListener("click", paletteClick, false);
// Hide colorpalette if paletteHasFocus is false
colorInput.addEventListener("focusout", hideColorPalette);
}
let colorSample = document.querySelectorAll('.' + colorSampleClass)
for (let i = 0; i < colorSample.length; i++) {
// Set initial color same as default (should already be set in HTML)
let defaultColor = colorSample[i].parentNode.dataset.default;
colorSample[i].style = 'background-color: ' + defaultColor + ';';
// Get RGB color from background
let rgbColor = colorSample[i].style.backgroundColor;
// Add span for human readable text
let colorSampleText = document.createElement("span");
colorSampleText.className = screenReaderClass;
colorSample[i].appendChild(colorSampleText);
// Update human readable text
updateColorSampleText(colorSample[i], defaultColor);
// Display palette if sample is clicked
colorSample[i].addEventListener("click", showColorPalette, false);
}
function colorBlur(e) {
// Check what element has focus
if (e.relatedTarget === null) {
// No element has focus
this.parentNode.style.display = 'none';
paletteHasFocus = false;
} else {
if (paletteClass !== e.relatedTarget.parentNode.className) {
// No element in the palette has focus
this.parentNode.style.display = 'none';
paletteHasFocus = false;
}
}
}
function showColorPalette(e) {
this.parentNode.querySelector('input').focus();
let palette = this.parentNode.querySelector('.' + paletteClass);
palette.style.display = 'block';
}
function hideColorPalette(e) {
let colorPalette = this.parentNode.querySelector('.' + paletteClass);
if (paletteHasFocus === false) {
colorPalette.style.display = 'none';
}
}
function paletteClick(e) {
if (paletteClass !== e.target.className) {
// Get the clicked color
let hexColor = rgbToHex(e.target.style.backgroundColor);
this.parentNode.querySelector('input').value = hexColor;
let colorSample = this.parentNode.querySelector('.' + colorSampleClass);
colorSample.style = 'background-color: ' + hexColor + ';';
updateColorSampleText(colorSample, hexColor)
}
// Hide palette
this.style.display = 'none';
paletteHasFocus = false;
}
function colorNavigation(e) {
// Select color with space or enter
if (13 === e.which || 32 === e.which) {
let hexColor = rgbToHex(this.style.backgroundColor);
this.parentNode.parentNode.querySelector('input').value = hexColor;
let colorSample = this.parentNode.parentNode.querySelector('.' + colorSampleClass);
colorSample.style = 'background-color: ' + hexColor + ';';
updateColorSampleText(colorSample, hexColor)
this.parentNode.style.display = 'none';
paletteHasFocus = false;
return;
}
if(27 === e.which) {
this.parentNode.style.display = 'none';
paletteHasFocus = false;
return;
}
let index = 0;
// Palette navigation with arrow keys
if (40 === e.which) {
// down arrow
index = parseInt(this.dataset.index, 10) + row;
} else if (38 === e.which) {
// up arrow
index = parseInt(this.dataset.index, 10) - row;
} else if (37 === e.which) {
// left arrow
index = parseInt(this.dataset.index, 10) - 1;
} else if (39 === e.which) {
// right arrow
index = parseInt(this.dataset.index, 10) + 1;
} else {
// Not a navigation key
return;
}
if (0 >= index || hexColors.length < index) {
return;
}
let next = this.parentNode.querySelector('[data-index="' + index + '"]');
if (next) {
next.focus();
}
}
function inputTabPressed(e) {
// Tab key to go to the first color in the palette
if (9 === e.which) {
// Palette has focus if a color has focus
paletteHasFocus = true;
}
}
function updateColorSample(e) {
// Update colorsample if it's a valid color
if (isValidHex(this.value)) {
let colorSample = this.parentNode.querySelector('.' + colorSampleClass);
colorSample.style = 'background-color: ' + this.value + ';';
updateColorSampleText(colorSample, this.value);
}
}
function updateColorSampleText(el, hexColor) {
let span = el.querySelector('span');
let rgbColor = el.style.backgroundColor;
let readableColor = getHumanReadableColor(rgbColor, hexColor);
if (readableColor.length) {
span.innerHTML = readableColor;
}
}
function getHumanReadableColor(rgbColor, hexColor) {
if (typeof HumanColours === "undefined") {
return hexColor;
}
let arr = rgbColor.replace('rgb', '').replace('(', '').replace(')', '').split(',');
let hslColor = rgbToHsl(arr[0], arr[1], arr[2]);
hslColor = 'hsl(' + hslColor.join(',') + ')';
let readable = new HumanColours(hslColor);
return 'Color ' + readable.hueName() + ', ' + readable.saturationName() + ', ' + readable.lightnessName();
}
function isValidHex(hex) {
return /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/i.test(hex.trim());
}
function componentToHex(c) {
var hex = c.toString(16);
return hex.length == 1 ? "0" + hex : hex;
}
function rgbToHex(color) {
arr = color.replace('rgb', '').replace('(', '').replace(')', '').split(',');
return "#" + componentToHex(Number(arr[0])) + componentToHex(Number(arr[1])) + componentToHex(Number(arr[2]));
}
function rgbToHsl(r, g, b) {
r /= 255, g /= 255, b /= 255;
var max = Math.max(r, g, b),
min = Math.min(r, g, b);
var h, s, l = (max + min) / 2;
if (max == min) {
h = s = 0; // achromatic
} else {
var d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r:
h = (g - b) / d + (g < b ? 6 : 0);
break;
case g:
h = (b - r) / d + 2;
break;
case b:
h = (r - g) / d + 4;
break;
}
h /= 6;
}
return [Math.floor(h * 360), Math.floor(s * 100), Math.floor(l * 100)];
}
})();
================================================
FILE: src/entries.js
================================================
function getEntryNode( entries, type, gate = false ) {
if ( !hasEntries( entries ) ) {
return false;
}
if( 'start' === type ) {
return gate ? entries.start.gate : {'x': entries.start.x, 'y': entries.start.y};
}
if( 'end' === type ) {
return gate ? entries.end.gate : {'x': entries.end.x, 'y': entries.end.y};
}
return false;
}
function hasEntries( entries ) {
if ( entries.hasOwnProperty( 'start' ) && entries.hasOwnProperty( 'end' ) ) {
return true;
}
return false;
}
================================================
FILE: src/globals.js
================================================
// Maze Restrictions.
//
// The higher the values the less restricted.
// Set values to 0 for no restriction.
//
// Be aware that the larger the maze, the more memory is consumed.
// With recursive backtracking the whole maze is stored in memory.
const maxMaze = 75000;
const maxSolve = 30000;
const maxCanvas = 16080100;
const maxCanvasDimension = 32760;
// Maximum walls you can remove
const maxWallsRemove = 300;
================================================
FILE: src/human-colours-en-gb.js
================================================
(function(global) {
var regex = /hsl\((.*)\)/, //Match hsl values
h, //Hue
s, //Saturation
l, //Lightness
hue,
sat,
light;
function HumanColours(hsl){
this.HSL = hsl;
this.values = this.HSL.replace(regex, '$1').split(',');
}
HumanColours.prototype = {
getHSL: function() {
return this.HSL;
},
getHue: function() {
return this.values[0];
},
getSaturation: function() {
return this.values[1].replace('%', '');
},
getLightness: function() {
return this.values[2].replace('%', '');
},
hueName: function() {
h = this.getHue();
if ( h < 15 ) { hue = 'red'; }
if ( h === 15 ) { hue = 'reddish'; }
if ( h > 15 ) { hue = 'orange'; }
if ( h > 45 ) { hue = 'yellow'; }
if ( h > 70 ) { hue = 'lime'; }
if ( h > 79 ) { hue = 'green'; }
if ( h > 163 ) { hue = 'cyan'; }
if ( h > 193 ) { hue = 'blue'; }
if ( h > 240 ) { hue = 'indigo'; }
if ( h > 260 ) { hue = 'violet'; }
if ( h > 270 ) { hue = 'purple'; }
if ( h > 291 ) { hue = 'magenta'; }
if ( h > 327 ) { hue = 'rose'; }
if ( h > 344 ) { hue = 'red'; }
return hue;
},
saturationName: function() {
s = this.getSaturation();
if( s < 4) { sat = 'grey'; }
if( s > 3) { sat = 'almost grey'; }
if( s > 10) { sat = 'very unsaturated'; }
if( s > 30) { sat = 'unsaturated'; }
if( s > 46) { sat = 'rather unsaturated'; }
if( s > 60) { sat = 'saturated'; }
if( s > 80) { sat = 'rather saturated'; }
if( s > 90) { sat = 'very saturated'; }
return sat;
},
lightnessName: function() {
l = this.getLightness();
if( l < 10 ) { light = 'almost black'; }
if( l > 9 ) { light = 'very dark'; }
if( l > 22 ) { light = 'dark'; }
if( l > 30 ) { light = 'normal?'; }
if( l > 60 ) { light = 'light'; }
if( l > 80 ) { light = 'very light'; }
if( l > 94 ) { light = 'almost white'; }
return light;
}
};
global.HumanColours = HumanColours;
}(this));
================================================
FILE: src/maze.js
================================================
function Maze(args) {
const defaults = {
width: 20,
height: 20,
wallSize: 10,
entryType: '',
bias: '',
color: '#000000',
backgroundColor: '#FFFFFF',
solveColor: '#cc3737',
removeWalls: 0,
// Maximum 300 walls can be removed
maxWallsRemove: 300,
// No restrictions
maxMaze: 0,
maxCanvas: 0,
maxCanvasDimension: 0,
maxSolve: 0,
}
const settings = Object.assign({}, defaults, args);
this.matrix = [];
this.wallsRemoved = 0;
this.width = parseInt(settings['width'], 10);
this.height = parseInt(settings['height'], 10);
this.wallSize = parseInt(settings['wallSize'], 10);
this.removeWalls = parseInt(settings['removeWalls'], 10);
this.entryNodes = this.getEntryNodes(settings['entryType']);
this.bias = settings['bias'];
this.color = settings['color'];
this.backgroundColor = settings['backgroundColor'];
this.solveColor = settings['solveColor'];
this.maxMaze = parseInt(settings['maxMaze'], 10);
this.maxCanvas = parseInt(settings['maxCanvas'], 10);
this.maxCanvasDimension = parseInt(settings['maxCanvasDimension'], 10);
this.maxSolve = parseInt(settings['maxSolve'], 10);
this.maxWallsRemove = parseInt(settings['maxWallsRemove'], 10);
}
Maze.prototype.generate = function() {
if (!this.isValidSize()) {
this.matrix = [];
alert('Please use smaller maze dimensions');
return;
}
let nodes = this.generateNodes();
nodes = this.parseMaze(nodes);
this.getMatrix(nodes);
this.removeMazeWalls();
}
Maze.prototype.isValidSize = function() {
const max = this.maxCanvasDimension;
const canvas_width = ((this.width * 2) + 1) * this.wallSize;
const canvas_height = ((this.height * 2) + 1) * this.wallSize;
// Max dimension Firefox and Chrome
if (max && ((max <= canvas_width) || (max <= canvas_height))) {
return false;
}
// Max area (200 columns) * (200 rows) with wall size 10px
if (this.maxCanvas && (this.maxCanvas <= (canvas_width * canvas_height))) {
return false;
}
return true;
}
Maze.prototype.generateNodes = function() {
const count = this.width * this.height;
let nodes = [];
for (let i = 0; i < count; i++) {
// visited, nswe
nodes[i] = "01111";
}
return nodes;
}
Maze.prototype.parseMaze = function(nodes) {
const mazeSize = nodes.length;
const positionIndex = { 'n': 1, 's': 2, 'w': 3, 'e': 4, };
const oppositeIndex = { 'n': 2, 's': 1, 'w': 4, 'e': 3 };
if (!mazeSize) {
return;
}
let max = 0;
let moveNodes = [];
let visited = 0;
let position = parseInt(Math.floor(Math.random() * nodes.length), 10);
let biasCount = 0;
let biasFactor = 3;
if (this.bias) {
if (('horizontal' === this.bias)) {
biasFactor = (1 <= (this.width / 100)) ? Math.floor(this.width / 100) + 2 : 3;
} else if ('vertical' === this.bias) {
biasFactor = (1 <= (this.height / 100)) ? Math.floor(this.height / 100) + 2 : 3;
}
}
// Set start node visited.
nodes[position] = replaceAt(nodes[position], 0, 1);
while (visited < (mazeSize - 1)) {
biasCount++;
max++;
if (this.maxMaze && (this.maxMaze < max)) {
alert('Please use smaller maze dimensions');
move_nodes = [];
this.matrix = [];
return [];
}
let next = this.getNeighbours(position);
let directions = Object.keys(next).filter(function(key) {
return (-1 !== next[key]) && !stringVal(this[next[key]], 0);
}, nodes);
if (this.bias && (biasCount !== biasFactor)) {
directions = this.biasDirections(directions);
} else {
biasCount = 0;
}
if (directions.length) {
++visited;
if (1 < directions.length) {
moveNodes.push(position);
}
let direction = directions[Math.floor(Math.random() * directions.length)];
// Update current position
nodes[position] = replaceAt(nodes[position], positionIndex[direction], 0);
// Set new position
position = next[direction];
// Update next position
nodes[position] = replaceAt(nodes[position], oppositeIndex[direction], 0);
nodes[position] = replaceAt(nodes[position], 0, 1);
} else {
if (!moveNodes.length) {
break;
}
position = moveNodes.pop();
}
}
return nodes;
}
Maze.prototype.getMatrix = function(nodes) {
const mazeSize = this.width * this.height;
// Add the complete maze in a matrix
// where 1 is a wall and 0 is a corridor.
let row1 = '';
let row2 = '';
if (nodes.length !== mazeSize) {
return;
}
for (let i = 0; i < mazeSize; i++) {
row1 += !row1.length ? '1' : '';
row2 += !row2.length ? '1' : '';
if (stringVal(nodes[i], 1)) {
row1 += '11';
if (stringVal(nodes[i], 4)) {
row2 += '01';
} else {
row2 += '00';
}
} else {
let hasAbove = nodes.hasOwnProperty(i - this.width);
let above = hasAbove && stringVal(nodes[i - this.width], 4);
let hasNext = nodes.hasOwnProperty(i + 1);
let next = hasNext && stringVal(nodes[i + 1], 1);
if (stringVal(nodes[i], 4)) {
row1 += '01';
row2 += '01';
} else if (next || above) {
row1 += '01';
row2 += '00';
} else {
row1 += '00';
row2 += '00';
}
}
if (0 === ((i + 1) % this.width)) {
this.matrix.push(row1);
this.matrix.push(row2);
row1 = '';
row2 = '';
}
}
// Add closing row
this.matrix.push('1'.repeat((this.width * 2) + 1));
}
Maze.prototype.getEntryNodes = function(access) {
const y = ((this.height * 2) + 1) - 2;
const x = ((this.width * 2) + 1) - 2;
let entryNodes = {};
if ('diagonal' === access) {
entryNodes.start = { 'x': 1, 'y': 1, 'gate': { 'x': 0, 'y': 1 } };
entryNodes.end = { 'x': x, 'y': y, 'gate': { 'x': x + 1, 'y': y } };
}
if ('horizontal' === access || 'vertical' === access) {
let xy = ('horizontal' === access) ? y : x;
xy = ((xy - 1) / 2);
let even = (xy % 2 === 0);
xy = even ? xy + 1 : xy;
let start_x = ('horizontal' === access) ? 1 : xy;
let start_y = ('horizontal' === access) ? xy : 1;
let end_x = ('horizontal' === access) ? x : (even ? start_x : start_x + 2);
let end_y = ('horizontal' === access) ? (even ? start_y : start_y + 2) : y;
let startgate = ('horizontal' === access) ? { 'x': 0, 'y': start_y } : { 'x': start_x, 'y': 0 };
let endgate = ('horizontal' === access) ? { 'x': x + 1, 'y': end_y } : { 'x': end_x, 'y': y + 1 };
entryNodes.start = { 'x': start_x, 'y': start_y, 'gate': startgate };
entryNodes.end = { 'x': end_x, 'y': end_y, 'gate': endgate };
}
return entryNodes;
}
Maze.prototype.biasDirections = function(directions) {
const horizontal = (-1 !== directions.indexOf('w')) || (-1 !== directions.indexOf('e'));
const vertical = (-1 !== directions.indexOf('n')) || (-1 !== directions.indexOf('s'));
if (('horizontal' === this.bias) && horizontal) {
directions = directions.filter(function(key) {
return (('w' === key) || ('e' === key))
});
} else if (('vertical' === this.bias) && vertical) {
directions = directions.filter(function(key) {
return (('n' === key) || ('s' === key))
});
}
return directions;
}
Maze.prototype.getNeighbours = function(pos) {
return {
'n': (0 <= (pos - this.width)) ? pos - this.width : -1,
's': ((this.width * this.height) > (pos + this.width)) ? pos + this.width : -1,
'w': ((0 < pos) && (0 !== (pos % this.width))) ? pos - 1 : -1,
'e': (0 !== ((pos + 1) % this.width)) ? pos + 1 : -1,
};
}
Maze.prototype.removeWall = function(row, index) {
// Remove wall if possible.
const evenRow = (row % 2 === 0);
const evenIndex = (index % 2 === 0);
const wall = stringVal(this.matrix[row], index);
if (!wall) {
return false;
}
if (!evenRow && evenIndex) {
// Uneven row and even column
const hasTop = (row - 2 > 0) && (1 === stringVal(this.matrix[row - 2], index));
const hasBottom = (row + 2 < this.matrix.length) && (1 === stringVal(this.matrix[row + 2], index));
if (hasTop && hasBottom) {
this.matrix[row] = replaceAt(this.matrix[row], index, '0');
return true;
} else if (!hasTop && hasBottom) {
const left = 1 === stringVal(this.matrix[row - 1], index - 1);
const right = 1 === stringVal(this.matrix[row - 1], index + 1);
if (left || right) {
this.matrix[row] = replaceAt(this.matrix[row], index, '0');
return true;
}
} else if (!hasBottom && hasTop) {
const left = 1 === stringVal(this.matrix[row + 1], index - 1);
const right = 1 === stringVal(this.matrix[row + 1], index + 1);
if (left || right) {
this.matrix[row] = replaceAt(this.matrix[row], index, '0');
return true;
}
}
} else if (evenRow && !evenIndex) {
// Even row and uneven column
const hasLeft = 1 === stringVal(this.matrix[row], index - 2);
const hasRight = 1 === stringVal(this.matrix[row], index + 2);
if (hasLeft && hasRight) {
this.matrix[row] = replaceAt(this.matrix[row], index, '0');
return true;
} else if (!hasLeft && hasRight) {
const top = 1 === stringVal(this.matrix[row - 1], index - 1);
const bottom = 1 === stringVal(this.matrix[row + 1], index - 1);
if (top || bottom) {
this.matrix[row] = replaceAt(this.matrix[row], index, '0');
return true;
}
} else if (!hasRight && hasLeft) {
const top = 1 === stringVal(this.matrix[row - 1], index + 1);
const bottom = 1 === stringVal(this.matrix[row + 1], index + 1);
if (top || bottom) {
this.matrix[row] = replaceAt(this.matrix[row], index, '0');
return true;
}
}
}
return false;
}
Maze.prototype.removeMazeWalls = function() {
if (!this.removeWalls || !this.matrix.length) {
return;
}
const min = 1;
const max = this.matrix.length - 1;
const maxTries = this.maxWallsRemove;
let tries = 0;
while (tries < maxTries) {
tries++;
// Did we reached the goal
if (this.wallsRemoved >= this.removeWalls) {
break;
}
// Get random row from matrix
let y = Math.floor(Math.random() * (max - min + 1)) + min;
y = (y === max) ? y - 1 : y;
let walls = [];
let row = this.matrix[y];
// Get walls from random row
for (let i = 0; i < row.length; i++) {
if (i === 0 || i === row.length - 1) {
continue;
}
const wall = stringVal(row, i);
if (wall) {
walls.push(i);
}
}
// Shuffle walls randomly
shuffleArray(walls);
// Try breaking a wall for this row.
for (let i = 0; i < walls.length; i++) {
if (this.removeWall(y, walls[i])) {
// Wall can be broken
this.wallsRemoved++;
break;
}
}
}
}
Maze.prototype.draw = function() {
const canvas = document.getElementById('maze');
if (!canvas || !this.matrix.length) {
return;
}
if (!this.isValidSize()) {
this.matrix = [];
alert('Please use smaller maze dimensions');
return;
}
canvas.width = ((this.width * 2) + 1) * this.wallSize;;
canvas.height = ((this.height * 2) + 1) * this.wallSize;
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Add background
ctx.fillStyle = this.backgroundColor;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Set maze collor
ctx.fillStyle = this.color;
const row_count = this.matrix.length;
const gateEntry = getEntryNode(this.entryNodes, 'start', true);
const gateExit = getEntryNode(this.entryNodes, 'end', true);
for (let i = 0; i < row_count; i++) {
let row_length = this.matrix[i].length;
for (let j = 0; j < row_length; j++) {
if (gateEntry && gateExit) {
if ((j === gateEntry.x) && (i === gateEntry.y)) {
continue;
}
if ((j === gateExit.x) && (i === gateExit.y)) {
continue;
}
}
let pixel = parseInt(this.matrix[i].charAt(j), 10);
if (pixel) {
ctx.fillRect((j * this.wallSize), (i * this.wallSize), this.wallSize, this.wallSize);
}
}
}
}
================================================
FILE: src/solver.js
================================================
function Solver(maze) {
this.maze = maze;
this.maxSolve = maze.maxSolve;
this.start = false;
this.finish = false;
this.solved = false;
this.path = false;
}
Solver.prototype.solve = function() {
const startPosition = getEntryNode(this.maze.entryNodes, 'start');
const endPosition = getEntryNode(this.maze.entryNodes, 'end');
// Get nodes (from the maze matrix) that have connections to other nodes.
const nodes = this.getMazeSolveNodes(startPosition, endPosition);
// Get the connections for every solve node.
const connected = this.connectMazeSolveNodes(nodes);
if (this.maze.wallsRemoved) {
this.path = this.walkMazeAstar(connected);
} else {
this.path = this.walkMaze(connected);
}
}
Solver.prototype.getMazeSolveNodes = function(start, end) {
const matrix = this.maze.matrix;
const nodes = [];
// Property used (by both solvers) to find and draw the path to the exit
const previous = undefined;
const rowCount = matrix.length;
for (let y = 0; y < rowCount; y++) {
if (y === 0 || y === (rowCount - 1) || (0 === (y % 2))) {
// First and last rows are walls only.
// Even rows don't have any connections
continue;
}
let rowLength = matrix[y].length;
for (let x = 0; x < rowLength; x++) {
if (stringVal(matrix[y], x)) {
// Walls don't have connections.
continue;
}
const nswe = {
'n': (0 < y) && stringVal(matrix[y - 1], x),
's': (rowCount > y) && stringVal(matrix[y + 1], x),
'w': (0 < x) && stringVal(matrix[y], (x - 1)),
'e': (rowLength > x) && stringVal(matrix[y], (x + 1))
}
if (start && end) {
if ((x === start.x) && (y === start.y)) {
this.start = nodes.length;
nodes.push({ x, y, nswe, previous });
continue;
}
if ((x === end.x) && (y === end.y)) {
this.finish = nodes.length;
nodes.push({ x, y, nswe, previous });
continue;
}
}
// Walls left or right
if (nswe['w'] || nswe['e']) {
// left or right direction possible
if (!nswe['w'] || !nswe['e']) {
nodes.push({ x, y, nswe, previous });
continue;
} else {
// Up or down direction possible.
if ((!nswe['n'] && nswe['s']) || (nswe['n'] && !nswe['s'])) {
nodes.push({ x, y, nswe, previous });
continue;
}
}
} else {
// All directions possible
if (!nswe['n'] && !nswe['s'] && !nswe['w'] && !nswe['e']) {
nodes.push({ x, y, nswe, previous });
continue;
} else {
// Up or down direction possible.
if ((!nswe['n'] && nswe['s']) || (nswe['n'] && !nswe['s'])) {
nodes.push({ x, y, nswe, previous });
continue;
}
}
}
} // x loop
} // y loop
return nodes;
}
Solver.prototype.connectMazeSolveNodes = function(nodes) {
// Connect nodes to their neighbours.
const y_nodes = {};
const nodes_length = nodes.length;
for (let i = 0; i < nodes_length; i++) {
nodes[i]['connected'] = {};
let x = nodes[i]['x'];
let y = nodes[i]['y'];
if (!nodes[i]['nswe']['w']) {
nodes[i]['connected']['w'] = i - 1;
}
if (!nodes[i]['nswe']['e']) {
nodes[i]['connected']['e'] = i + 1;
}
if (!nodes[i]['nswe']['n'] && y_nodes.hasOwnProperty(x)) {
nodes[i]['connected']['n'] = y_nodes[x];
if (nodes.hasOwnProperty(y_nodes[x])) {
nodes[y_nodes[x]]['connected']['s'] = i;
delete y_nodes[x];
}
}
if (!nodes[i]['nswe']['s']) {
y_nodes[x] = i;
}
if (this.maze.wallsRemoved) {
// Not needed for A star solve
delete nodes[i]['nswe'];
}
}
return nodes;
}
Solver.prototype.heuristic = function(a, b) {
return Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
}
Solver.prototype.walkMazeAstar = function(nodes) {
this.solved = false;
if (!nodes.length) {
return;
}
let openSet = [];
let closedSet = [];
let startNode = 0;
let endNode = nodes.length - 1;
if ((false !== this.start) && (false !== this.finish)) {
startNode = this.start;
endNode = this.finish;
}
// Add defaults to all nodes before we walk the maze.
nodes.forEach( e => {
e['f'] = 0;
e['g'] = 0;
e['h'] = 0;
});
openSet.push(startNode);
let max = 0;
while (openSet.length > 0) {
max++
if (this.maxSolve && (this.maxSolve < max)) {
alert('Solving maze took too long. Please try again or use smaller maze dimensions');
break
}
// Best next option
let winner = 0;
for (let i = 0; i < openSet.length; i++) {
if (nodes[openSet[i]].f < nodes[openSet[winner]].f) {
winner = i;
}
}
var current = nodes[openSet[winner]];
let currentKey = openSet[winner]
// Did I finish?
if (current === nodes[endNode]) {
this.solved = true;
break;
}
removeFromArray(openSet, openSet[winner]);
closedSet.push(currentKey);
let neighbors = [];
for (key in current.connected) {
if (current.connected.hasOwnProperty(key)) {
neighbors.push(current.connected[key]);
}
}
for (let i = 0; i < neighbors.length; i++) {
let neighbor = nodes[neighbors[i]];
// Valid next spot?
if (!closedSet.includes(neighbors[i])) {
let tempG = current.g + this.heuristic(neighbor, current);
// Is this a better path than before?
let newPath = false;
if (openSet.includes(neighbors[i])) {
if (tempG < neighbor.g) {
neighbor.g = tempG;
newPath = true;
}
} else {
neighbor.g = tempG;
newPath = true;
openSet.push(neighbors[i]);
}
// Yes, it's a better path
if (newPath) {
neighbor.h = this.heuristic(neighbor, nodes[endNode]);
neighbor.f = neighbor.g + neighbor.h;
neighbor.previous = currentKey;
}
}
}
}
path = [];
let temp = current;
path.push(temp);
while (temp.previous) {
path.push(nodes[temp.previous]);
temp = nodes[temp.previous];
}
// Add the startNode for drawing the solved path.
path.push(nodes[startNode]);
return path;
}
Solver.prototype.walkMaze = function(nodes) {
this.solved = false;
if (!nodes.length) {
return;
}
let startNode = 0;
let endNode = nodes.length - 1;
if ((false !== this.start) && (false !== this.finish)) {
startNode = this.start;
endNode = this.finish;
}
let max = 0;
let i = 0;
let node = false;
let from = false;
const multi_nodes = [];
const opposite = { 'n': 's', 's': 'n', 'w': 'e', 'e': 'w' };
while (this.solved === false) {
max++
if (this.maxSolve && (this.maxSolve < max)) {
alert('Solving maze took too long. Please try again or use smaller maze dimensions');
break
}
if (!node) {
i = startNode;
node = nodes[i];
}
if (i === endNode) {
// Found the end node.
this.solved = true;
break
}
node['count'] = 4 - (Object.keys(node['nswe'])
.map(key => !node['nswe'][key] ? 0 : 1)
.reduce((a, b) => a + b, 0));
if (node.count > 2) {
if (-1 === multi_nodes.indexOf(i)) {
multi_nodes.push(i);
}
}
if (false !== from) {
node['nswe'][from] = 1;
node.count--;
nodes[i] = node;
}
if (0 === node.count) {
from = false;
if (!multi_nodes.length) {
// Jump back to start.
i = startNode;
node = nodes[startNode];
continue;
}
// Jump back to multiple directions node
i = multi_nodes.pop();
node = nodes[i];
if (node.count > 1) {
// Add multi node back if more than one option left
multi_nodes.push(i);
}
continue;
}
let directions = Object.keys(node['nswe']).filter(key => !node['nswe'][key] ? true : false);
let direction = directions[Math.floor(Math.random() * directions.length)];
if (node.count >= 1) {
node.count--;
from = opposite[direction];
node['nswe'][direction] = 1;
node['previous'] = direction;
nodes[i] = node;
}
if (node['connected'].hasOwnProperty(direction)) {
i = node['connected'][direction];
node = nodes[i];
} else {
// Error: Node is not connected to direction
break;
}
}
return nodes;
}
Solver.prototype.drawAstarSolve = function() {
const nodes = this.path;
const wallSize = this.maze.wallSize;
const canvas = document.getElementById('maze');
if (!canvas || !nodes.length || !this.solved) {
return;
}
const canvas_width = ((this.maze.width * 2) + 1) * wallSize;
const canvas_height = ((this.maze.height * 2) + 1) * wallSize;
if (!((canvas.width === canvas_width) && (canvas.height === canvas_height))) {
// Error: Not the expected canvas size.
return;
}
const ctx = canvas.getContext('2d');
ctx.fillStyle = this.maze.solveColor;
let startNode = 0;
let endNode = nodes.length - 1;
let finished = false
let node = false;
const hasGates = (false !== this.start) && (false !== this.finish);
if (hasGates) {
startNode = this.start;
endNode = this.finish;
const gateEntry = getEntryNode(this.maze.entryNodes, 'start', true);
ctx.fillRect((gateEntry.x * wallSize), (gateEntry.y * wallSize), wallSize, wallSize);
}
for (let i = nodes.length - 1; i >= 0; i--) {
if (!(0 <= (i - 1))) {
continue;
}
let previousX = nodes[i - 1].x;
let previousY = nodes[i - 1].y;
let start;
let to_x;
if (nodes[i].y === previousY) {
let start = nodes[i].x
let to_x = ((previousX - start) * wallSize) + wallSize;
if (nodes[i].x > previousX) {
start = previousX
to_x = ((nodes[i].x - previousX) * wallSize) + wallSize;
}
ctx.fillRect((start * wallSize), (nodes[i].y * wallSize), to_x, wallSize);
}
if (nodes[i].x === previousX) {
let start = nodes[i].y;
let to_y = ((previousY - start) * wallSize) + wallSize;
if (nodes[i].y > previousY) {
start = previousY;
to_y = ((nodes[i].y - previousY) * wallSize) + wallSize;
}
ctx.fillRect((nodes[i].x * wallSize), (start * wallSize), wallSize, to_y);
}
}
if (hasGates) {
const gateExit = getEntryNode(this.maze.entryNodes, 'end', true);
ctx.fillRect((gateExit.x * wallSize), (gateExit.y * wallSize), wallSize, wallSize);
}
}
Solver.prototype.draw = function() {
const nodes = this.path;
const wallSize = this.maze.wallSize;
const canvas = document.getElementById('maze');
if (!canvas || !nodes.length || !this.solved) {
return;
}
const canvas_width = ((this.maze.width * 2) + 1) * wallSize;
const canvas_height = ((this.maze.height * 2) + 1) * wallSize;
if (!((canvas.width === canvas_width) && (canvas.height === canvas_height))) {
// Error: Not the expected canvas size.
return;
}
const ctx = canvas.getContext('2d');
ctx.fillStyle = this.maze.solveColor;
let max = 0;
let i;
let startNode = 0;
let endNode = nodes.length - 1;
let finished = false
let node = false;
const hasGates = (false !== this.start) && (false !== this.finish);
if (hasGates) {
startNode = this.start;
endNode = this.finish;
const gateEntry = getEntryNode(this.maze.entryNodes, 'start', true);
ctx.fillRect((gateEntry.x * wallSize), (gateEntry.y * wallSize), wallSize, wallSize);
}
while (finished === false) {
max++
if (this.maxSolve && (this.maxSolve < max)) {
alert('Solving maze took too long. Please try again or use smaller maze dimensions');
break
}
if (!node) {
node = nodes[startNode];
}
if (i === endNode) {
finished = true;
break
}
if (node.previous === "undefined" || node.connected === "undefined") {
// Error: Last step or connected nodes doesn't exist.
break;
}
if (!node.connected.hasOwnProperty(node.previous)) {
// Error: Connected direction doesnt exist.
break;
}
i = node.connected[node.previous];
let connected_node = nodes[i];
if (-1 !== ['w', 'e'].indexOf(node.previous)) {
let start = node.x
let to_x = ((connected_node.x - start) * wallSize) + wallSize;
if ('w' === node.previous) {
start = connected_node.x
to_x = ((node.x - connected_node.x) * wallSize) + wallSize;
}
ctx.fillRect((start * wallSize), (node.y * wallSize), to_x, wallSize);
}
if (-1 !== ['n', 's'].indexOf(node.previous)) {
let start = node.y;
let to_y = ((connected_node.y - start) * wallSize) + wallSize;
if ('n' === node.previous) {
start = connected_node.y
to_y = ((node.y - connected_node.y) * wallSize) + wallSize;
}
ctx.fillRect((node.x * wallSize), (start * wallSize), wallSize, to_y);
}
node = nodes[i];
}
if (hasGates) {
const gateExit = getEntryNode(this.maze.entryNodes, 'end', true);
ctx.fillRect((gateExit.x * wallSize), (gateExit.y * wallSize), wallSize, wallSize);
}
}
================================================
FILE: src/utils.js
================================================
function isValidHex(hex) {
return /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/i.test(hex.trim());
}
function replaceAt(str, index, replacement) {
// Replace a character at index in a string
if (index > str.length - 1) {
return str;
}
return str.substr(0, index) + replacement + str.substr(index + 1);
}
function stringVal(str, index) {
// Get the number value at a specific index in a string (0 or 1)
return parseInt(str.charAt(index), 10);
}
function getInputIntVal(id, defaultValue) {
const el = document.getElementById(id);
if (el) {
let el_value = parseInt(el.value, 10);
el_value = (0 < el_value) ? el_value : defaultValue;
el.value = el_value;
return el_value;
}
el.value = defaultValue;
return defaultValue;
}
function removeFromArray(arr, element) {
const index = arr.indexOf(element);
if (-1 !== index) {
arr.splice(index, 1);
}
}
/**
* Randomize array element order in-place.
* Using Durstenfeld shuffle algorithm.
*/
function shuffleArray(array) {
for (let i = array.length - 1; i > 0; i--) {
let j = Math.floor(Math.random() * (i + 1));
let temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}