Created
September 10, 2021 01:51
-
-
Save perfectly-panda/9bb1b9be0a59fa808c4bab7a539490fe to your computer and use it in GitHub Desktop.
Maze Game
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Original JavaScript code by Chirp Internet: chirpinternet.eu | |
// Please acknowledge use of this code by including this header. | |
class MazeBuilder { | |
constructor(width, height) { | |
this.width = width; | |
this.height = height; | |
this.cols = 2 * this.width + 1; | |
this.rows = 2 * this.height + 1; | |
this.maze = this.initArray([]); | |
// place initial walls | |
this.maze.forEach((row, r) => { | |
row.forEach((cell, c) => { | |
switch(r) | |
{ | |
case 0: | |
case this.rows - 1: | |
this.maze[r][c] = ["wall"]; | |
break; | |
default: | |
if((r % 2) == 1) { | |
if((c == 0) || (c == this.cols - 1)) { | |
this.maze[r][c] = ["wall"]; | |
} | |
} else if(c % 2 == 0) { | |
this.maze[r][c] = ["wall"]; | |
} | |
} | |
}); | |
if(r == 0) { | |
// place exit in top row | |
let doorPos = this.posToSpace(this.rand(1, this.width)); | |
this.maze[r][doorPos] = ["door", "exit"]; | |
} | |
if(r == this.rows - 1) { | |
// place entrance in bottom row | |
let doorPos = this.posToSpace(this.rand(1, this.width)); | |
this.maze[r][doorPos] = ["door", "entrance"]; | |
} | |
}); | |
// start partitioning | |
this.partition(1, this.height - 1, 1, this.width - 1); | |
} | |
initArray(value) { | |
return new Array(this.rows).fill().map(() => new Array(this.cols).fill(value)); | |
} | |
rand(min, max) { | |
return min + Math.floor(Math.random() * (1 + max - min)); | |
} | |
posToSpace(x) { | |
return 2 * (x-1) + 1; | |
} | |
posToWall(x) { | |
return 2 * x; | |
} | |
inBounds(r, c) { | |
if((typeof this.maze[r] == "undefined") || (typeof this.maze[r][c] == "undefined")) { | |
return false; // out of bounds | |
} | |
return true; | |
} | |
shuffle(array) { | |
// sauce: https://stackoverflow.com/a/12646864 | |
for(let i = array.length - 1; i > 0; i--) { | |
const j = Math.floor(Math.random() * (i + 1)); | |
[array[i], array[j]] = [array[j], array[i]]; | |
} | |
return array; | |
} | |
partition(r1, r2, c1, c2) { | |
// create partition walls | |
// ref: https://en.wikipedia.org/wiki/Maze_generation_algorithm#Recursive_division_method | |
let horiz, vert, x, y, start, end; | |
if((r2 < r1) || (c2 < c1)) { | |
return false; | |
} | |
if(r1 == r2) { | |
horiz = r1; | |
} else { | |
x = r1+1; | |
y = r2-1; | |
start = Math.round(x + (y-x) / 4); | |
end = Math.round(x + 3*(y-x) / 4); | |
horiz = this.rand(start, end); | |
} | |
if(c1 == c2) { | |
vert = c1; | |
} else { | |
x = c1 + 1; | |
y = c2 - 1; | |
start = Math.round(x + (y - x) / 3); | |
end = Math.round(x + 2 * (y - x) / 3); | |
vert = this.rand(start, end); | |
} | |
for(let i = this.posToWall(r1)-1; i <= this.posToWall(r2)+1; i++) { | |
for(let j = this.posToWall(c1)-1; j <= this.posToWall(c2)+1; j++) { | |
if((i == this.posToWall(horiz)) || (j == this.posToWall(vert))) { | |
this.maze[i][j] = ["wall"]; | |
} | |
} | |
} | |
let gaps = this.shuffle([true, true, true, false]); | |
// create gaps in partition walls | |
if(gaps[0]) { | |
let gapPosition = this.rand(c1, vert); | |
this.maze[this.posToWall(horiz)][this.posToSpace(gapPosition)] = []; | |
} | |
if(gaps[1]) { | |
let gapPosition = this.rand(vert+1, c2+1); | |
this.maze[this.posToWall(horiz)][this.posToSpace(gapPosition)] = []; | |
} | |
if(gaps[2]) { | |
let gapPosition = this.rand(r1, horiz); | |
this.maze[this.posToSpace(gapPosition)][this.posToWall(vert)] = []; | |
} | |
if(gaps[3]) { | |
let gapPosition = this.rand(horiz+1, r2+1); | |
this.maze[this.posToSpace(gapPosition)][this.posToWall(vert)] = []; | |
} | |
// recursively partition newly created chambers | |
this.partition(r1, horiz-1, c1, vert-1); | |
this.partition(horiz+1, r2, c1, vert-1); | |
this.partition(r1, horiz-1, vert+1, c2); | |
this.partition(horiz+1, r2, vert+1, c2); | |
} | |
isGap(...cells) { | |
return cells.every((array) => { | |
let row, col; | |
[row, col] = array; | |
if(this.maze[row][col].length > 0) { | |
if(!this.maze[row][col].includes("door")) { | |
return false; | |
} | |
} | |
return true; | |
}); | |
} | |
countSteps(array, r, c, val, stop) { | |
if(!this.inBounds(r, c)) { | |
return false; // out of bounds | |
} | |
if(array[r][c] <= val) { | |
return false; // shorter route already mapped | |
} | |
if(!this.isGap([r, c])) { | |
return false; // not traversable | |
} | |
array[r][c] = val; | |
if(this.maze[r][c].includes(stop)) { | |
return true; // reached destination | |
} | |
this.countSteps(array, r-1, c, val+1, stop); | |
this.countSteps(array, r, c+1, val+1, stop); | |
this.countSteps(array, r+1, c, val+1, stop); | |
this.countSteps(array, r, c-1, val+1, stop); | |
} | |
getKeyLocation() { | |
let fromEntrance = this.initArray(); | |
let fromExit = this.initArray(); | |
this.totalSteps = -1; | |
for(let j = 1; j < this.cols-1; j++) { | |
if(this.maze[this.rows-1][j].includes("entrance")) { | |
this.countSteps(fromEntrance, this.rows-1, j, 0, "exit"); | |
} | |
if(this.maze[0][j].includes("exit")) { | |
this.countSteps(fromExit, 0, j, 0, "entrance"); | |
} | |
} | |
let fc = -1, fr = -1; | |
this.maze.forEach((row, r) => { | |
row.forEach((cell, c) => { | |
if(typeof fromEntrance[r][c] == "undefined") { | |
return; | |
} | |
let stepCount = fromEntrance[r][c] + fromExit[r][c]; | |
if(stepCount > this.totalSteps) { | |
fr = r; | |
fc = c; | |
this.totalSteps = stepCount; | |
} | |
}); | |
}); | |
return [fr, fc]; | |
} | |
placeKey() { | |
let fr, fc; | |
[fr, fc] = this.getKeyLocation(); | |
this.maze[fr][fc] = ["key"]; | |
} | |
display(id) { | |
this.parentDiv = document.getElementById(id); | |
if(!this.parentDiv) { | |
alert("Cannot initialise maze - no element found with id \"" + id + "\""); | |
return false; | |
} | |
while(this.parentDiv.firstChild) { | |
this.parentDiv.removeChild(this.parentDiv.firstChild); | |
} | |
const container = document.createElement("div"); | |
container.id = "maze"; | |
container.dataset.steps = this.totalSteps; | |
this.maze.forEach((row) => { | |
let rowDiv = document.createElement("div"); | |
row.forEach((cell) => { | |
let cellDiv = document.createElement("div"); | |
if(cell) { | |
cellDiv.className = cell.join(" "); | |
} | |
rowDiv.appendChild(cellDiv); | |
}); | |
container.appendChild(rowDiv); | |
}); | |
this.parentDiv.appendChild(container); | |
return true; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#maze_container { | |
position: relative; | |
display: inline-block; | |
} | |
#maze { | |
position: relative; | |
background-color: #a7c53f; | |
background-image: radial-gradient(circle at 0% 0%, transparent 50%, rgba(0,0,0,0.1) 50%), radial-gradient(circle at center, rgba(0,0,0,0.1) 50%, transparent 50%), radial-gradient(circle at 100% 100%, transparent 50%, rgba(0,0,0,0.1) 50%), radial-gradient(circle at 0% 100%, transparent 50%, rgba(0,0,0,0.1) 50%), radial-gradient(circle at 100% 0%, transparent 50%, rgba(0,0,0,0.1) 50%); | |
background-size: 8em 8em; | |
} | |
#maze div { | |
display: flex; | |
} | |
#maze div div { | |
position: relative; | |
width: 1em; | |
height: 1em; | |
} | |
#maze div div::after { | |
position: absolute; | |
left: -3px; | |
top: -4px; | |
text-align: center; | |
text-shadow: 0 0 1px black; | |
font-size: 1.2em; | |
z-index: 10; | |
} | |
#maze div div.door.exit::after { | |
content: "\1F6AA"; | |
} | |
#maze div div.nubbin::after { | |
content: "\1F33C"; | |
} | |
#maze div.nubbin:nth-of-type(3n)::after { | |
content: "\1F344"; | |
} | |
#maze div.nubbin:nth-of-type(5n)::after { | |
content: "\1F33B"; | |
} | |
#maze div.nubbin:nth-of-type(7n)::after { | |
content: "\1F48E"; | |
} | |
#maze div.nubbin:nth-of-type(13n)::after { | |
content: "\1F381"; | |
} | |
#maze div.hero::after { | |
content: "\1F6B6" !important; | |
} | |
#maze.face-right div.hero::after { | |
transform: scale(-1, 1); | |
} | |
#maze div div.wall, #maze div div.nubbin.wall, #maze div div.door.exit { | |
background-color: #454545; | |
background-image: linear-gradient(45deg, rgba(0,0,0,0.2) 45%, transparent 55%), linear-gradient(to bottom, rgba(0,0,0,0.2) 45%, transparent 55%); | |
background-size: 0.5em 0.5em; | |
} | |
#maze div div.nubbin.wall::after { | |
content: ""; | |
} | |
#maze div div.sentinel.wall { | |
background: transparent; | |
} | |
#maze div div.sentinel.wall::after { | |
content: "\1F40A"; | |
} | |
#maze div.sentinel.wall:nth-of-type(3n)::after { | |
content: "\1F40D"; | |
} | |
#maze div div.key::after { | |
content: "\1F511"; | |
} | |
#maze div div:nth-child(odd) { | |
width: 1em; | |
} | |
#maze div:nth-child(odd) div { | |
height: 1em; | |
} | |
#maze.finished::after { | |
position: absolute; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
display: flex; | |
justify-content: space-around; | |
align-items: center; | |
content: "Game Over"; | |
background: rgba(0,0,0,0.4); | |
text-transform: uppercase; | |
font-family: monospace; | |
font-size: 5em; | |
text-shadow: 2px 2px 2px rgba(0,0,0,0.8); | |
color: #fff; | |
z-index: 10; | |
} | |
#maze_output { | |
display: flex; | |
margin: 0.5em auto; | |
} | |
#maze_score, #maze_message { | |
font-family: fantasy; | |
font-weight: bold; | |
font-size: 1em; | |
} | |
#maze_score { | |
flex: 1; | |
white-space: nowrap; | |
text-align: left; | |
} | |
#maze_score::before { | |
content: "Score: "; | |
} | |
#maze_score.has-key::after { | |
content: "\00a0\1F511"; | |
} | |
#maze_message { | |
flex: 3; | |
text-align: right; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment