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. ![maze](https://user-images.githubusercontent.com/1436618/106612888-e1d90600-6569-11eb-87cf-2477b2578598.png) ### 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.


The maze can be solved in multiple ways if you remove maze walls. (maximum 300 walls)
The A* search algorithm is used to find the shortest path. This takes more time solving the maze.

Click the colors below to select a color from a color pallete.

download maze
================================================ 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; } }