Skip to content

Instantly share code, notes, and snippets.

@perfectly-panda
Created September 10, 2021 01:51
Show Gist options
  • Save perfectly-panda/9bb1b9be0a59fa808c4bab7a539490fe to your computer and use it in GitHub Desktop.
Save perfectly-panda/9bb1b9be0a59fa808c4bab7a539490fe to your computer and use it in GitHub Desktop.
Maze Game
// 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;
}
}
#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