diff --git a/static/dndlabyrinth/README.md b/static/dndlabyrinth/README.md
new file mode 100644
index 00000000..61c21f99
--- /dev/null
+++ b/static/dndlabyrinth/README.md
@@ -0,0 +1,32 @@
+# Dungeons & Dragons™ Computer Labyrinth Game Simulator
+## Version 1.1
+
+This game is a simulation of the Mattel Electronics™ Dungeons and Dragons™ Computer Labyrinth Game
+
+
+
+Images of the game, the manual, game pieces, artwork and any audio are copyright of [Mattel](https://www.mattel.com/en-us)™. Dungeons and Dragons™ is copyright of [Wizards of the Coast](https://company.wizards.com/)™.
+
+## Background
+While browsing on Facebook I came across a post on a friend's feed about a 1980 Mattel Electronics™ Dungeons and Dragons™ Computer Labyrinth Game. At the time the game looked vaguely familiar but as I read through the post all the memories came back to me. I remembered owning this game when I was about 11 and being fascinated by it. Being a software developer I couldn't help but to try and recreate it.
+
+## Technical Info
+This repo consists of an HTML5 page with a simulation of the game built on Javascript and CSS. It is entirely built in native Javascript so no other packages or libraries are required. The styles are also all written in native CSS so no SCSS/SASS compilers are needed. The HTML, styles and code are all unobfuscated and unminified as I use Cloudflare to perform the caching and minification.
+
+### Browser Support
+All modern browsers running ECMAScript 2015 ES6 should be able to run the game. If the game detects that the Javascript `Promise` or `URL` objects do not exist it assumes the browser is not capable of supporting the game and alerts the user.
+
+## Game Play
+The original game required the user to place and move the game pieces but to make this simulation easier to play, I have the game pieces being placed and moved automatically. Time permitting I may add a "classic" mode later that would rely on the user to manually place and move all the game pieces to make it more like the original game. A [PDF User Manual](./assets/gameManual.pdf) describing the game is also included.
+
+## Licensing
+
+### MIT License
+
+Copyright© 2021 Bob Whitley
+
+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.
diff --git a/static/dndlabyrinth/assets/gameManual.pdf b/static/dndlabyrinth/assets/gameManual.pdf
new file mode 100644
index 00000000..95de741a
Binary files /dev/null and b/static/dndlabyrinth/assets/gameManual.pdf differ
diff --git a/static/dndlabyrinth/audio/defeat.mp3 b/static/dndlabyrinth/audio/defeat.mp3
new file mode 100644
index 00000000..5ba1f7db
Binary files /dev/null and b/static/dndlabyrinth/audio/defeat.mp3 differ
diff --git a/static/dndlabyrinth/audio/door.mp3 b/static/dndlabyrinth/audio/door.mp3
new file mode 100644
index 00000000..4873bc88
Binary files /dev/null and b/static/dndlabyrinth/audio/door.mp3 differ
diff --git a/static/dndlabyrinth/audio/dragonAttacks.mp3 b/static/dndlabyrinth/audio/dragonAttacks.mp3
new file mode 100644
index 00000000..9231f0b5
Binary files /dev/null and b/static/dndlabyrinth/audio/dragonAttacks.mp3 differ
diff --git a/static/dndlabyrinth/audio/dragonFlying.mp3 b/static/dndlabyrinth/audio/dragonFlying.mp3
new file mode 100644
index 00000000..9b5df39f
Binary files /dev/null and b/static/dndlabyrinth/audio/dragonFlying.mp3 differ
diff --git a/static/dndlabyrinth/audio/dragonWakes.mp3 b/static/dndlabyrinth/audio/dragonWakes.mp3
new file mode 100644
index 00000000..5ffa1f09
Binary files /dev/null and b/static/dndlabyrinth/audio/dragonWakes.mp3 differ
diff --git a/static/dndlabyrinth/audio/illegalMove.mp3 b/static/dndlabyrinth/audio/illegalMove.mp3
new file mode 100644
index 00000000..a424ae26
Binary files /dev/null and b/static/dndlabyrinth/audio/illegalMove.mp3 differ
diff --git a/static/dndlabyrinth/audio/levelOne.mp3 b/static/dndlabyrinth/audio/levelOne.mp3
new file mode 100644
index 00000000..a424ae26
Binary files /dev/null and b/static/dndlabyrinth/audio/levelOne.mp3 differ
diff --git a/static/dndlabyrinth/audio/levelTwo.mp3 b/static/dndlabyrinth/audio/levelTwo.mp3
new file mode 100644
index 00000000..ffd5aa53
Binary files /dev/null and b/static/dndlabyrinth/audio/levelTwo.mp3 differ
diff --git a/static/dndlabyrinth/audio/on.mp3 b/static/dndlabyrinth/audio/on.mp3
new file mode 100644
index 00000000..0ed4eca3
Binary files /dev/null and b/static/dndlabyrinth/audio/on.mp3 differ
diff --git a/static/dndlabyrinth/audio/treasure.mp3 b/static/dndlabyrinth/audio/treasure.mp3
new file mode 100644
index 00000000..c662de34
Binary files /dev/null and b/static/dndlabyrinth/audio/treasure.mp3 differ
diff --git a/static/dndlabyrinth/audio/wall.mp3 b/static/dndlabyrinth/audio/wall.mp3
new file mode 100644
index 00000000..6729e39b
Binary files /dev/null and b/static/dndlabyrinth/audio/wall.mp3 differ
diff --git a/static/dndlabyrinth/audio/warriorMoves.mp3 b/static/dndlabyrinth/audio/warriorMoves.mp3
new file mode 100644
index 00000000..ffd5aa53
Binary files /dev/null and b/static/dndlabyrinth/audio/warriorMoves.mp3 differ
diff --git a/static/dndlabyrinth/audio/warriorOne.mp3 b/static/dndlabyrinth/audio/warriorOne.mp3
new file mode 100644
index 00000000..0ed4eca3
Binary files /dev/null and b/static/dndlabyrinth/audio/warriorOne.mp3 differ
diff --git a/static/dndlabyrinth/audio/warriorTwo.mp3 b/static/dndlabyrinth/audio/warriorTwo.mp3
new file mode 100644
index 00000000..b8514dd5
Binary files /dev/null and b/static/dndlabyrinth/audio/warriorTwo.mp3 differ
diff --git a/static/dndlabyrinth/audio/winner.mp3 b/static/dndlabyrinth/audio/winner.mp3
new file mode 100644
index 00000000..423e422a
Binary files /dev/null and b/static/dndlabyrinth/audio/winner.mp3 differ
diff --git a/static/dndlabyrinth/css/main.css b/static/dndlabyrinth/css/main.css
new file mode 100644
index 00000000..27366a90
--- /dev/null
+++ b/static/dndlabyrinth/css/main.css
@@ -0,0 +1,420 @@
+@import url('https://fonts.googleapis.com/css2?family=MedievalSharp&display=swap');
+html {
+ font-family: Arial, Helvetica, sans-serif;
+ margin: 0;
+ padding: 0;
+ background-color: black;
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ min-height: 100%;
+ touch-action: manipulation;
+}
+body {
+ font-family: Arial, Helvetica, sans-serif;
+ margin: 0;
+ padding: 0;
+ background-color: black;
+ color: rgb(203 203 203);
+ display: flex;
+ flex: 1;
+ justify-content: center;
+ font-size: 14px;
+}
+input {
+ font-size: 14px;
+}
+button {
+ font-size: 14px;
+}
+body.play {
+ overflow: hidden;
+}
+main {
+ margin: 0;
+ padding: 0;
+ position: relative;
+}
+body.play main {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+}
+a, a:visited, a:active {
+ color: #fff;
+ text-decoration: underline;;
+}
+a:hover, a:active {
+ color: rgb(185 54 61);
+}
+#intro {
+ display: flex;
+ flex-direction: column;
+ margin: 0 auto;
+ overflow: hidden;
+ max-width: 640px;
+ max-height: 1000px;
+ text-align: center;
+ opacity: 1;
+ padding: 20px 20px;
+ transition: max-height 250ms, opacity 250ms, padding 250ms;
+}
+body.play #intro {
+ border: solid 0 transparent;
+ opacity: 0;
+ max-height: 0;
+ padding: 0;
+}
+#intro #header {
+ border: solid 3px rgb(185 54 61);
+ border-radius: 10px;
+ padding: 5px 50px;
+ background: #242424;
+ text-shadow: 3px 3px 1px rgb(0 0 0);
+ margin: 0 auto 30px auto;
+ display: inline-block;
+ background-image: url('../images/dragonBackground.jpg');
+ background-size: cover;
+ background-position: center;
+}
+#intro #header h1 {
+ font-family: MedievalSharp;
+ color: #fff;
+ font-size: 1.4em;
+ font-weight: 700;
+ line-height: 1.3em;
+}
+#intro .subtext {
+ color: #999;
+ font-size: .75em;
+}
+#intro .subtext a {
+ color: #999
+}
+#intro .subtext a:hover, a:active {
+ color: rgb(185 54 61);
+}
+#intro .subtext a:hover, a:active {
+ color: rgb(185 54 61);
+}
+#container {
+ position: relative;
+ display: block;
+ width: 90vmin;
+ height: 30%;
+ margin: 0 auto;
+ transition: width 250ms, height 250ms, margin 250ms;
+}
+body.play #container {
+ height: 90vmin;
+ margin: auto;
+}
+#start {
+ display: flex;
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ z-index: 10000;
+ background-color: rgba(0,0,0,.7);
+ text-align: center;
+ cursor: pointer;
+ align-content: center;
+ justify-content: center;
+}
+#start .text {
+ background-color: 242424;
+ padding: 10px 15px;
+ border-radius: 5px;
+ font-weight: 700;
+ border: solid 3px rgb(185 54 61);
+ z-index: 10001;
+ box-shadow: 0px 0px 20px 5px black;
+ display: inline-block;
+ margin: auto;
+ font-family: 'MedievalSharp';
+ text-shadow: 3px 3px 1px rgb(0 0 0);
+ vertical-align: middle;
+ letter-spacing: normal;
+ color: #fff;
+}
+body.play #start {
+ display: none;
+}
+#board {
+ width: 100%;
+ height: 100%;
+ position: relative;
+ display: flex;
+ margin: 0;
+ background-image: url('../images/board.jpg');
+ background-repeat: no-repeat;
+ background-size: contain;
+ background-position: center;
+ transition: width 250ms ease-in-out, height 250ms ease-in-out, margin 250ms ease-in-out;
+}
+body.play #home {
+ display: flex;
+}
+#menu {
+ display: none;
+ flex-direction: row;
+ position: fixed;
+ top: 5px;
+ right: 5px;
+ left: 5px;
+ padding: 5px;
+ align-items: center;
+ color: #bebebe;
+}
+#menuLeft {
+ display: flex;
+ width: 30%;
+ justify-content: flex-start;
+}
+#menuCenter {
+ display: flex;
+ flex: 1;
+ justify-content: center;
+}
+#menuRight {
+ display: flex;
+ width: 30%;
+ justify-content: flex-end;
+}
+#menu a, #menu a:visited {
+ text-decoration: none;
+ cursor: pointer;
+ color: #bebebe;
+}
+#menu a:hover, #menu a:active {
+ color: rgb(185 54 61);
+ text-decoration: underline;
+}
+#menu button {
+ color: #bebebe;
+ border: solid 2px rgb(185 54 61);
+ border-radius: 5px;
+ background-color: rgba(0,0,0,.7);
+ padding: 3px 15px;
+ outline: none;
+ cursor: pointer;
+ transition: background-color 250ms;
+}
+#menu button:hover {
+ background-color: rgba(185, 54, 61, .6);
+ color: #fff;
+}
+#menu input[type=checkbox] {
+ margin-right: 5px;
+ cursor: pointer;
+}
+#menu label {
+ cursor: pointer;
+}
+body.play #menu {
+ display: flex;
+}
+#helpMessage {
+ opacity: 0;
+ position: fixed;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ text-align: center;
+ margin: 0 0 10px 0;
+ transition: opacity 250ms;
+}
+body.play #helpMessage.show {
+ opacity: 1;
+}
+#grid {
+ display: none;
+ flex-direction: column;
+ flex: 1;
+ height: 90vmin;
+ width: 90vmin;
+ margin: auto;
+}
+body.play #grid {
+ display: block;
+}
+#grid .row {
+ display: flex;
+ flex-direction: row;
+}
+#grid .col {
+ display: flex;
+ width: 7.7vmin;
+ height: 7.4vmin;
+ margin: 0;
+ border-radius: 1px;
+ transition: box-shadow 100ms ease-in-out;
+}
+#grid .cell {
+ display: flex;
+ flex: 1;
+ align-items: center;
+ justify-content: center;
+ color: white;
+ z-index: 1000;
+}
+#grid .row:first-child {
+ margin-top: 21.2vmin;
+}
+#grid .button {
+ margin-left: 11vmin;
+ margin-right: 1.7vmin;
+}
+#grid .button.switch .cell {
+ margin: 1.7vmin 1.3vmin .9vmin 1.3vmin;
+ border-radius: .25vmin;
+ transition: box-shadow 250ms, background-color 250ms;
+}
+#grid .button.selected .cell {
+ box-shadow: 0 0 1vmin 0.5vmin rgb(255 80 80);
+ background-color: rgba(255,180,180,.5);
+}
+#grid .button, #grid .chamber {
+ cursor: pointer;
+ position: relative;
+}
+#grid .chamber .southWall {
+ visibility: hidden;
+ width: 100%;
+ position: absolute;
+ bottom: -1vmin;
+ left: 0;
+ height: 1.9vmin;
+ display: block;
+ z-index: 10;
+ background-image: url('../images/southWall.png');
+ background-size: contain;
+ background-position: center;
+ background-repeat: no-repeat;
+}
+#grid .chamber .eastWall {
+ visibility: hidden;
+ height: 100%;
+ position: absolute;
+ top: 0;
+ right: -1.1vmin;
+ width: 2vmin;
+ display: block;
+ z-index: 10;
+ background-image: url('../images/eastWall.png');
+ background-size: contain;
+ background-position: center;
+ background-repeat: no-repeat;
+}
+#grid .chamber .secretRoomOne, #grid .chamber .secretRoomTwo, #grid .chamber .treasureRoom {
+ position: absolute;
+ left: 1vmin;
+ top: 1.3vmin;
+ right: 1.1vmin;
+ bottom: 1.2vmin;
+ background-size: contain;
+ background-position: center;
+ background-repeat: no-repeat;
+}
+#grid .chamber .secretRoomOne {
+ background-image: url('../images/warriorOneMarker.png');
+}
+#grid .chamber .secretRoomTwo {
+ background-image: url('../images/warriorTwoMarker.png');
+}
+#grid .chamber .treasureRoom {
+ background-image: url('../images/treasureMarker.png');
+}
+#grid .chamber .warriorOne, #grid .chamber .warriorTwo, #grid .chamber .dragon, #grid .chamber .treasure {
+ background-position: bottom center;
+ background-repeat: no-repeat;
+ position: absolute;
+ left: 0;
+ right: 0;
+ bottom: 1vmin;
+}
+#grid .chamber .warriorOne, #grid .chamber .warriorTwo {
+ display: none;
+ height: 11vmin;
+}
+.gameStarted #grid .chamber .warriorOne, .gameStarted #grid .chamber .warriorTwo {
+ display: block;
+}
+#grid .chamber .warriorOne {
+ background-size: auto 90%;
+ background-image: url("../images/warriorOne.png");
+}
+#grid .chamber .warriorTwo {
+ background-size: auto 100%;
+ background-image: url("../images/warriorTwo.png");
+}
+#grid .chamber .dragon {
+ background-size: auto 100%;
+ height: 14vmin;
+ background-image: url("../images/dragon.png");
+}
+#grid .chamber .treasure {
+ background-size: auto 100%;
+ height: 10vmin;
+ background-image: url("../images/treasure.png");
+}
+#grid .chamber .treasure.withWarrior {
+ transform: translate(1.7vmin, 2.9vmin) scale(.5);
+}
+
+body.debug #container.layoutDebug #grid .col.button, body.debug #container.layoutDebug #grid .col.chamber {
+ background-color: rgba(0,0,0,.4);
+}
+body.debug #grid .cell.northWallDebug {
+ border-top: solid .5vmin #eee;
+}
+body.debug #grid .cell.eastWallDebug {
+ border-right: solid .5vmin #eee;
+}
+body.debug #grid .cell.southWallDebug {
+ border-bottom: solid .5vmin #eee;
+}
+body.debug #grid .cell.westWallDebug {
+ border-left: solid .5vmin #eee;
+}
+body.debug #grid .cell.northDoorDebug {
+ border-top: dotted .5vmin #eee;
+}
+body.debug #grid .cell.eastDoorDebug {
+ border-right: dotted .5vmin #eee;
+}
+body.debug #grid .cell.southDoorDebug {
+ border-bottom: dotted .5vmin #eee;
+}
+body.debug #grid .cell.westDoorDebug {
+ border-left: dotted .5vmin #eee;
+}
+body.debug #grid .cell.northOpenDebug {
+ border-top: dotted .5vmin rgba(255,255,255,.25);
+}
+body.debug #grid .cell.eastOpenDebug {
+ border-right: dotted .5vmin rgba(255,255,255,.25);
+}
+body.debug #grid .cell.southOpenDebug {
+ border-bottom: dotted .5vmin rgba(255,255,255,.25);
+}
+body.debug #grid .cell.westOpenDebug {
+ border-left: dotted .5vmin rgba(255,255,255,.25);
+}
+body.debug #grid .chamber.traveledDebug .cell {
+ background-color: rgba(0,0,255,.4);
+}
+body.debug #grid .chamber.travelingDebug .cell {
+ background-color: rgba(255,0,0,.4);
+}
+body.debug #grid .chamber.treasureOptionDebug .cell {
+ background-color: rgba(255,215,0,.4);
+}
+body.debug #grid .chamber.treasureDebug .cell {
+ background-color: rgba(255,215,0, .6);
+}
+body.debug #grid .chamber .dragon {
+ display: block;
+}
diff --git a/static/dndlabyrinth/icons/icon128x128.jpg b/static/dndlabyrinth/icons/icon128x128.jpg
new file mode 100644
index 00000000..f9985f2e
Binary files /dev/null and b/static/dndlabyrinth/icons/icon128x128.jpg differ
diff --git a/static/dndlabyrinth/icons/icon152x152.jpg b/static/dndlabyrinth/icons/icon152x152.jpg
new file mode 100644
index 00000000..fc78be63
Binary files /dev/null and b/static/dndlabyrinth/icons/icon152x152.jpg differ
diff --git a/static/dndlabyrinth/icons/icon167x167.jpg b/static/dndlabyrinth/icons/icon167x167.jpg
new file mode 100644
index 00000000..cdccf608
Binary files /dev/null and b/static/dndlabyrinth/icons/icon167x167.jpg differ
diff --git a/static/dndlabyrinth/icons/icon180x180.jpg b/static/dndlabyrinth/icons/icon180x180.jpg
new file mode 100644
index 00000000..40c27394
Binary files /dev/null and b/static/dndlabyrinth/icons/icon180x180.jpg differ
diff --git a/static/dndlabyrinth/icons/icon192x192.jpg b/static/dndlabyrinth/icons/icon192x192.jpg
new file mode 100644
index 00000000..064a7f7c
Binary files /dev/null and b/static/dndlabyrinth/icons/icon192x192.jpg differ
diff --git a/static/dndlabyrinth/icons/icon196x196.jpg b/static/dndlabyrinth/icons/icon196x196.jpg
new file mode 100644
index 00000000..8ebba0fd
Binary files /dev/null and b/static/dndlabyrinth/icons/icon196x196.jpg differ
diff --git a/static/dndlabyrinth/images/board.jpg b/static/dndlabyrinth/images/board.jpg
new file mode 100644
index 00000000..4fb140de
Binary files /dev/null and b/static/dndlabyrinth/images/board.jpg differ
diff --git a/static/dndlabyrinth/images/dragon.png b/static/dndlabyrinth/images/dragon.png
new file mode 100644
index 00000000..1e40389d
Binary files /dev/null and b/static/dndlabyrinth/images/dragon.png differ
diff --git a/static/dndlabyrinth/images/dragonBackground.jpg b/static/dndlabyrinth/images/dragonBackground.jpg
new file mode 100644
index 00000000..d4435b5a
Binary files /dev/null and b/static/dndlabyrinth/images/dragonBackground.jpg differ
diff --git a/static/dndlabyrinth/images/eastWall.png b/static/dndlabyrinth/images/eastWall.png
new file mode 100644
index 00000000..5ac5f191
Binary files /dev/null and b/static/dndlabyrinth/images/eastWall.png differ
diff --git a/static/dndlabyrinth/images/featuredImage.jpg b/static/dndlabyrinth/images/featuredImage.jpg
new file mode 100644
index 00000000..d7762b8b
Binary files /dev/null and b/static/dndlabyrinth/images/featuredImage.jpg differ
diff --git a/static/dndlabyrinth/images/southWall.png b/static/dndlabyrinth/images/southWall.png
new file mode 100644
index 00000000..014da8d2
Binary files /dev/null and b/static/dndlabyrinth/images/southWall.png differ
diff --git a/static/dndlabyrinth/images/treasure.png b/static/dndlabyrinth/images/treasure.png
new file mode 100644
index 00000000..932f3cd0
Binary files /dev/null and b/static/dndlabyrinth/images/treasure.png differ
diff --git a/static/dndlabyrinth/images/treasureMarker.png b/static/dndlabyrinth/images/treasureMarker.png
new file mode 100644
index 00000000..e9531ce7
Binary files /dev/null and b/static/dndlabyrinth/images/treasureMarker.png differ
diff --git a/static/dndlabyrinth/images/warriorOne.png b/static/dndlabyrinth/images/warriorOne.png
new file mode 100644
index 00000000..374519bc
Binary files /dev/null and b/static/dndlabyrinth/images/warriorOne.png differ
diff --git a/static/dndlabyrinth/images/warriorOneMarker.png b/static/dndlabyrinth/images/warriorOneMarker.png
new file mode 100644
index 00000000..ac79203d
Binary files /dev/null and b/static/dndlabyrinth/images/warriorOneMarker.png differ
diff --git a/static/dndlabyrinth/images/warriorTwo.png b/static/dndlabyrinth/images/warriorTwo.png
new file mode 100644
index 00000000..2a465001
Binary files /dev/null and b/static/dndlabyrinth/images/warriorTwo.png differ
diff --git a/static/dndlabyrinth/images/warriorTwoMarker.png b/static/dndlabyrinth/images/warriorTwoMarker.png
new file mode 100644
index 00000000..6b99f2bd
Binary files /dev/null and b/static/dndlabyrinth/images/warriorTwoMarker.png differ
diff --git a/static/dndlabyrinth/index.html b/static/dndlabyrinth/index.html
new file mode 100644
index 00000000..445f3e13
--- /dev/null
+++ b/static/dndlabyrinth/index.html
@@ -0,0 +1,101 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Dungeons & Dragons™ Computer Labyrinth Game Simulator
+
+
+
+
+
+
+
+
+ This game is a simulation of the Dungeons and Dragons™ Computer Labyrinth Game™
+ made by Mattel Electronics ™ released in 1980.
+
+
+ I owned this game back in 1981 when I was 11 years old and only recently rediscovered it so I
+ thought it would be a fun project to recreate it on the internet for others to enjoy.
+
+
+ To play the game simply press the play button below. If you are new to the game I recommend reading the
+ original game manual to get familiar with the game play.
+ The game relies on sound queues for play so be sure to have your speakers turned on or enable the in-game
+ help messages.
+
+
+ Enjoy!
+
+
+ Images of the game, the manual, game pieces, artwork and any audio are copyright of Mattel ™
+
+ Dungeons and Dragons™ is copyright of Wizards of the Coast ™
+
+
+
+
+
+
\ No newline at end of file
diff --git a/static/dndlabyrinth/js/main.js b/static/dndlabyrinth/js/main.js
new file mode 100644
index 00000000..a34198e5
--- /dev/null
+++ b/static/dndlabyrinth/js/main.js
@@ -0,0 +1,1207 @@
+var labyrinth = {
+ VERSION: 1.1,
+ DIR_TYPE: {
+ North: 0,
+ East: 1,
+ South: 2,
+ West: 3
+ },
+ PATH_TYPE: {
+ Undefined: 0,
+ Open: 1,
+ Wall: 2,
+ Door: 3
+ },
+ GAME_STATE: {
+ Wait: 0,
+ WarriorOneSelectRoom: 1,
+ WarriorTwoSelectRoom: 2,
+ WarriorOneTurn: 3,
+ WarriorTwoTurn: 4,
+ GameOver: 5
+ },
+ DRAGON_STATE: {
+ Asleep: 0,
+ Awake: 1
+ },
+ settings: {
+ debug: false,
+ travelSpeed: 200,
+ doorProb: .1,
+ wallProb: .8,
+ removeWallThreshold: 2,
+ removeWallProb: 0,
+ edgeWallBias: true,
+ treasureRoomDistance: 3,
+ maxLives: 3,
+ baseMoves: 2,
+ movesPerLife: 2,
+ movesWithTreasure: 4,
+ doorClosedProb: .2,
+ dragonWakeDistance: 3,
+ dragonFollows: 1
+ },
+ elements: {
+ board: null,
+ grid: null,
+ buttons: [],
+ chambers: [],
+ help: null
+ },
+ maze: {
+ chamberPaths: [],
+ stack: null,
+ visited: {},
+ lastChamber: null,
+ cancelTravel: false
+ },
+ game: {
+ init: false,
+ state: 0,
+ delayTimeout: null,
+ numberOfWarriors: 1,
+ warrior: [
+ {
+ lives: null,
+ secretRoom: null,
+ position: null,
+ moves: null
+ },
+ {
+ lives: null,
+ secretRoom: null,
+ position: null,
+ moves: null
+ }
+ ],
+ dragon: {
+ position: null,
+ state: null,
+ visible: false
+ },
+ treasure: {
+ room: null,
+ warrior: -1,
+ visible: false
+ },
+ switchButtonOn: false,
+ level: 1,
+ help: null,
+ helpTimeout: null,
+ helpMessage: ""
+ },
+ audio: {
+ on: new Audio("audio/on.mp3"),
+ levelOne: new Audio("audio/levelOne.mp3"),
+ levelTwo: new Audio("audio/levelTwo.mp3"),
+ dragonFlying: new Audio("audio/dragonFlying.mp3"),
+ defeat: new Audio("audio/defeat.mp3"),
+ dragonAttacks: new Audio("audio/dragonAttacks.mp3"),
+ dragonWakes: new Audio("audio/dragonWakes.mp3"),
+ wall: new Audio("audio/wall.mp3"),
+ door: new Audio("audio/door.mp3"),
+ illegalMove: new Audio("audio/illegalMove.mp3"),
+ warriorMoves: new Audio("audio/warriorMoves.mp3"),
+ warriorOne: new Audio("audio/warriorOne.mp3"),
+ winner: new Audio("audio/winner.mp3"),
+ warriorTwo: new Audio("audio/warriorTwo.mp3"),
+ treasure: new Audio("audio/treasure.mp3")
+ },
+ log(msg) {
+ if (this.settings.debug) console.log(msg);
+ },
+ trackEvent(action, category, label, value) {
+ if (window.gaEnabled) {
+ try {
+ gtag('event', action, {
+ 'event_category': category,
+ 'event_label': label,
+ 'value': value
+ });
+ }
+ catch (ex) { }
+ }
+ },
+ startMessageResolve: null,
+ serialAudio: new Audio(),
+ audioPlayResolve: null,
+ audioPlayTimeout: null,
+ audioMetadataListener() {
+ var duration = this.serialAudio.duration;
+ var self = this;
+ this.audioPlayTimeout = setTimeout(function() {
+ self.audioPlayTimeout = null;
+ var resolve = self.audioPlayResolve;
+ self.audioPlayResolve = null;
+ resolve();
+ }, duration * 1000);
+ },
+ playAudio(audio) {
+ return new Promise((resolve, reject) => {
+ if (this.audioPlayTimeout && this.audioPlayResolve) {
+ var timeout = this.audioPlayTimeout;
+ this.audioPlayTimeout = null;
+ clearTimeout(timeout);
+ var previousResolve = this.audioPlayResolve;
+ this.audioPlayResolve = null;
+ previousResolve();
+ }
+ this.audioPlayResolve = resolve;
+ this.serialAudio.src = audio.src;
+ this.serialAudio.play();
+ });
+ },
+ init() {
+ if (!window.URL || !window.Promise) {
+ alert("Sorry! Your browser is not supported.");
+ }
+ else {
+ let location = new URL(document.location);
+ if (location.searchParams.get("debug") == "1") this.settings.debug = true;
+ let travelSpeed = location.searchParams.get("travelSpeed");
+ if (!isNaN(parseInt(travelSpeed))) this.settings.travelSpeed = travelSpeed;
+ if (this.settings.debug) {
+ document.body.className = document.body.className + " debug";
+ var container = document.getElementById("container");
+ container.className = " layoutDebug";
+ document.addEventListener("onclick", event => {
+ if (event.keyCode === 88) this.maze.cancelTravel = true;
+ });
+ }
+ else {
+ document.addEventListener("contextmenu", function(event){
+ event.preventDefault();
+ }, false);
+ }
+ this.loadSettings();
+ this.initMenus();
+ this.initAudio();
+ this.initStartMessage();
+ }
+ },
+ loadSettings() {
+ try {
+ let helpEnabled = localStorage.getItem("helpEnabled");
+ this.toggleHelp(helpEnabled == null || helpEnabled == "true");
+ }
+ catch(ex) {}
+ },
+ saveSetting(key, value) {
+ try {
+ localStorage.setItem(key, value);
+ }
+ catch(ex) {}
+ },
+ toggleHelp(checked) {
+ let helpCheckbox = document.getElementById("help");
+ helpCheckbox.checked = checked;
+ let helpMessage = document.getElementById("helpMessage");
+ helpMessage.className = checked ? "show" : "";
+ this.saveSetting("helpEnabled", checked);
+ },
+ initMenus() {
+ var self = this;
+ document.getElementById("exit").onclick = function() {
+ self.trackEvent("Game Exit");
+ document.body.className = document.body.className.replace(" play", "");
+ };
+ document.getElementById("reset").onclick = function() {
+ self.trackEvent("Game Reset");
+ self.reset.call(self);
+ };
+ document.getElementById("help").onclick = function() {
+ self.trackEvent("Help " + (this.checked ? "Enabled" : "Disabled"));
+ self.toggleHelp.call(self, this.checked);
+ };
+ },
+ initStartMessage() {
+ var self = this;
+ document.getElementById("start").addEventListener("click", function() {
+ self.trackEvent("Game Started");
+ self.start.call(self);
+ });
+ },
+ initAudio() {
+ this.audioIndex = [
+ [
+ this.audio.dragonFlying,
+ this.audio.dragonAttacks,
+ this.audio.wall,
+ this.audio.illegalMove,
+ this.audio.warriorOne,
+ this.audio.warriorTwo
+ ],
+ [
+ this.audio.defeat,
+ this.audio.dragonWakes,
+ this.audio.door,
+ this.audio.warriorMoves,
+ this.audio.winner,
+ this.audio.treasure
+ ]
+ ];
+ this.audioNameIndex = [
+ [
+ "Dragon Flying",
+ "Dragon Attacks",
+ "Wall",
+ "Illegal Move",
+ "Warrior One",
+ "Warrior Two"
+ ],
+ [
+ "Defeat",
+ "Dragon Wakes",
+ "Door",
+ "Warrior Moves",
+ "Winner",
+ "Treasure"
+ ]
+ ];
+ var self = this;
+ this.serialAudio.addEventListener("loadedmetadata", function () {
+ self.audioMetadataListener.call(self)
+ }, false);
+ },
+ reset() {
+ this.initBoard();
+ this.initMaze().then(() => {
+ this.startGame();
+ });
+ },
+ initBoard() {
+ this.elements.help = document.getElementById("helpMessage");
+ this.elements.board = document.getElementById("board");
+ let board = this.elements.board;
+ board.innerHTML = "";
+ this.elements.grid = document.createElement("div");
+ let grid = this.elements.grid
+ grid.id = "grid";
+ this.elements.buttons = [];
+ this.elements.chambers = [];
+ var chamberPaths = this.maze.chamberPaths;
+ chamberPaths.splice(0);
+ for(let r=0; r<8; r++) {
+ chamberPaths.push([]);
+ let row = document.createElement("div");
+ row.className = "row";
+ for(let c=0; c<9; c++) {
+ let col = document.createElement("div");
+ col.className = "col";
+ let cell = document.createElement("div");
+ cell.className = "cell";
+ if (c == 0) {
+ var self = this;
+ col.id = "button-" + (r).toString();
+ col.className += " button";
+ if (r == 0) {
+ col.className += " switch";
+ var self = this;
+ col.onmousedown = function() {
+ self.toggleSwitch.call(self);
+ }
+ }
+ else if (r == 1) {
+ col.onmousedown = function(event) {
+ if (event.which == 1) {
+ self.nextTurn.call(self);
+ } else if (event.which == 3) {
+ self.toggleLevel.call(self);
+ }
+ }
+ }
+ else {
+ col.onmousedown = function(event) {
+ if (event.which == 1) {
+ self.playAudioClick.call(self, r-2, false);
+ } else if (event.which == 3) {
+ self.playAudioClick.call(self, r-2, true);
+ }
+ }
+ }
+ }
+ else {
+ chamberPaths[r].push([
+ r==0 ? this.PATH_TYPE.Wall : this.PATH_TYPE.Undefined,
+ c==8 ? this.PATH_TYPE.Wall : this.PATH_TYPE.Undefined,
+ r==7 ? this.PATH_TYPE.Wall : this.PATH_TYPE.Undefined,
+ c==1 ? this.PATH_TYPE.Wall : this.PATH_TYPE.Undefined
+ ]);
+ col.id = "chamber-" + (r).toString() + "-" + (c-1).toString();
+ col.className += " chamber";
+ cell.onmousedown = function(event) {
+ self.chamberSelect([r, c-1], event);
+ }
+ }
+ if (c == 0) {
+ this.elements.buttons.push(col);
+ }
+ else {
+ if (c == 1) this.elements.chambers.push([]);
+ this.elements.chambers[this.elements.chambers.length - 1].push(col);
+ if (r < 7) {
+ let southWall = document.createElement("div");
+ southWall.className = "southWall";
+ col.appendChild(southWall);
+ }
+ if (c < 8) {
+ let eastWall = document.createElement("div");
+ eastWall.className = "eastWall";
+ col.appendChild(eastWall);
+ }
+ }
+ col.appendChild(cell);
+ row.appendChild(col);
+ }
+ grid.appendChild(row);
+ }
+ board.appendChild(grid);
+ },
+ showSep(row, col, dir, type) {
+ if (this.settings.debug) {
+ var classSuffix = type == this.PATH_TYPE.Wall ? "WallDebug" : (type == this.PATH_TYPE.Door ? "DoorDebug" : "OpenDebug");
+ var chamberCell = this.elements.chambers[row][col].querySelector(".cell");
+ var classPrefix = "";
+ if (dir == this.DIR_TYPE.North) classPrefix = "north";
+ if (dir == this.DIR_TYPE.East) classPrefix = "east";
+ if (dir == this.DIR_TYPE.South) classPrefix = "south";
+ if (dir == this.DIR_TYPE.West) classPrefix = "west";
+ chamberCell.className = chamberCell.className.replace(new RegExp("\\s" + classPrefix + "[^\\s]+"), "") + " " + classPrefix + classSuffix;
+ }
+ },
+ setPath(row, col, dir, type) {
+ var chamberPaths = this.maze.chamberPaths;
+ chamberPaths[row][col][dir] = type;
+ if (dir == this.DIR_TYPE.East || dir == this.DIR_TYPE.South) this.showSep(row, col, dir, type);
+ if (dir == this.DIR_TYPE.North && row != 0) {
+ chamberPaths[row - 1][col][this.DIR_TYPE.South] = type;
+ this.showSep(row -1 , col, this.DIR_TYPE.South, type);
+ } else if (dir == this.DIR_TYPE.East && col != 7) {
+ chamberPaths[row][col + 1][this.DIR_TYPE.West] = type;
+ } else if (dir == this.DIR_TYPE.South && row != 7) {
+ chamberPaths[row + 1][col][this.DIR_TYPE.North] = type;
+ } else if (dir == this.DIR_TYPE.West && col != 0) {
+ chamberPaths[row][col - 1][this.DIR_TYPE.East] = type;
+ this.showSep(row, col - 1, this.DIR_TYPE.East, type);
+ }
+ },
+ getPaths(row, col) {
+ var chamberPaths = this.maze.chamberPaths;
+ return {
+ north: chamberPaths[row][col][this.DIR_TYPE.North],
+ east: chamberPaths[row][col][this.DIR_TYPE.East],
+ south: chamberPaths[row][col][this.DIR_TYPE.South],
+ west: chamberPaths[row][col][this.DIR_TYPE.West]
+ }
+ },
+ wallOrNot() {
+ return Math.random() <= this.settings.wallProb ? this.PATH_TYPE.Wall : this.openOrDoor();
+ },
+ openOrDoor() {
+ return Math.random() <= this.settings.doorProb ? this.PATH_TYPE.Door : this.PATH_TYPE.Open;
+ },
+ travelMaze() {
+ return new Promise((resolve, reject) => {
+ var stack = this.maze.stack;
+ var pos = stack[stack.length - 1];
+ var wallProb = this.settings.wallProb;
+ var visited = this.maze.visited;
+ var chamberPaths = this.maze.chamberPaths;
+ var row = pos[0];
+ var col = pos[1];
+ visited[row.toString() + "_" + col.toString()] = true;
+ if (this.settings.debug) {
+ var lastChamber = this.maze.lastChamber;
+ var chamber = this.elements.chambers[row][col];
+ if (chamber.className.indexOf("traveledDebug") == -1) chamber.className += " traveledDebug";
+ chamber.className += " travelingDebug";
+ if (lastChamber) lastChamber.className = lastChamber.className.replace(" travelingDebug", "");
+ this.maze.lastChamber = chamber;
+ var chamberCell = chamber.querySelector(".cell");
+ if (chamberCell.innerHTML == "") chamberCell.innerHTML = stack.length;
+ }
+ if (this.settings.edgeWallBias && row > 0 && chamberPaths[row][col][this.DIR_TYPE.North] == this.PATH_TYPE.Undefined && visited[(row - 1).toString() + "_" + col.toString()]) {
+ this.setPath(row, col, this.DIR_TYPE.North, this.wallOrNot(col == 0 || col == 11 ? 1 : wallProb)); // Bias for keeping walls near the left/right edges
+ }
+ if (this.settings.edgeWallBias && col < 7 && chamberPaths[row][col][this.DIR_TYPE.East] == this.PATH_TYPE.Undefined && visited[row.toString() + "_" + ((col + 1).toString()).toString()]) {
+ this.setPath(row, col, this.DIR_TYPE.East, this.wallOrNot(row == 0 || row == 11 ? 1 : wallProb)); // Bias for keeping walls near the top/bottom edges
+ }
+ if (this.settings.edgeWallBias && row < 7 && chamberPaths[row][col][this.DIR_TYPE.South] == this.PATH_TYPE.Undefined && visited[(row + 1).toString() + "_" + col.toString()]) {
+ this.setPath(row, col, this.DIR_TYPE.South, this.wallOrNot(col == 0 || col == 11 ? 1 : wallProb)); // Bias for keeping walls near the left/right edges
+ }
+ if (this.settings.edgeWallBias && col > 0 && chamberPaths[row][col][this.DIR_TYPE.West] == this.PATH_TYPE.Undefined && visited[row.toString() + "_" + ((col - 1).toString()).toString()]) {
+ this.setPath(row, col, this.DIR_TYPE.West, this.wallOrNot(row == 0 || row == 11 ? 1 : wallProb)); // Bias for keeping walls near the top/bottom edges
+ }
+ var opts = [];
+ var paths = this.getPaths(row, col);
+ if (paths.north == this.PATH_TYPE.Undefined) {
+ opts.push(this.DIR_TYPE.North)
+ } else if (this.settings.edgeWallBias && row == 0 && paths.south == this.PATH_TYPE.Undefined) {
+ opts.push(this.DIR_TYPE.South); opts.push(this.DIR_TYPE.South); // Extra bias to turn south if at top
+ }
+ if (paths.east == this.PATH_TYPE.Undefined) {
+ opts.push(this.DIR_TYPE.East);
+ } else if (this.settings.edgeWallBias && col == 7 && paths.west == this.PATH_TYPE.Undefined) {
+ opts.push(this.DIR_TYPE.West); opts.push(this.DIR_TYPE.West); // Extra bias to turn west if at right
+ }
+ if (paths.south == this.PATH_TYPE.Undefined) {
+ opts.push(this.DIR_TYPE.South);
+ } else if (this.settings.edgeWallBias && row == 7 && paths.north == this.PATH_TYPE.Undefined) {
+ opts.push(this.DIR_TYPE.North); opts.push(this.DIR_TYPE.North); // Extra bias to turn north if at bottom
+ }
+ if (paths.west == this.PATH_TYPE.Undefined) {
+ opts.push(this.DIR_TYPE.West);
+ } else if (this.settings.edgeWallBias && col == 0 && paths.east == this.PATH_TYPE.Undefined) {
+ opts.push(this.DIR_TYPE.East); opts.push(this.DIR_TYPE.East); // Extra bias to turn east if at left
+ }
+ if (opts.length == 0) {
+ if (this.settings.debug) {
+ var popRow = stack[stack.length - 1][0];
+ var popCol = stack[stack.length - 1][1];
+ var cell = this.elements.chambers[popRow][popCol].querySelector(".cell");
+ cell.innerHTML = "";
+ }
+ stack.pop();
+ }
+ else {
+ var dir = opts[Math.floor(Math.random() * opts.length)];
+ this.setPath(row, col, dir, this.openOrDoor());
+ stack.push([
+ row + (dir == this.DIR_TYPE.North ? -1 : (dir == this.DIR_TYPE.South ? 1 : 0)),
+ col + (dir == this.DIR_TYPE.West ? -1 : (dir == this.DIR_TYPE.East ? 1 : 0))
+ ]);
+ }
+ if (!this.maze.cancelTravel) {
+ var self = this;
+ if (stack.length > 0) {
+ if (this.settings.debug) {
+ setTimeout(function() {
+ self.travelMaze.call(self).then(() => resolve())
+ }, this.settings.travelSpeed);
+ }
+ else {
+ this.travelMaze().then(() => resolve());
+ }
+ }
+ else {
+ resolve();
+ }
+ }
+ });
+ },
+ removeWalls() {
+ return new Promise((resolve, reject) => {
+ if (this.settings.removeWallProb > 0 && this.settings.removeWallThreshold < 4) {
+ var chamberPaths = this.maze.chamberPaths;
+ for(let r=1; r<7; r++) {
+ for(let c=1; c<7; c++) {
+ var opts = [];
+ for(dir=0; dir<4; dir++) {
+ if (chamberPaths[r][c][dir] == this.PATH_TYPE.Wall) opts.push(dir);
+ }
+ if (opts.length >= this.settings.removeWallThreshold && Math.random() <= this.settings.removeWallProb) {
+ var removeDir = Math.floor(Math.random() * opts.length);
+ var type = this.openOrDoor();
+ this.setPath(r, c, opts[removeDir], type);
+ }
+ }
+ }
+ }
+ resolve();
+ });
+ },
+ initMaze() {
+ return new Promise((resolve, reject) => {
+ this.maze.stack = [[Math.floor(Math.random() * 8), Math.floor(Math.random() * 8)]];
+ this.maze.visited = {};
+ this.maze.lastChamber = null;
+ this.maze.cancelTravel = false;
+ let self = this;
+ this.travelMaze().then(() => setTimeout(function() {
+ self.removeWalls.call(self).then(() => resolve());
+ }, this.settings.travelSpeed));
+ });
+ },
+ startGame() {
+ this.game.level = 1
+ this.game.numberOfWarriors = 1;
+ this.game.warrior[0] = {
+ lives: this.settings.maxLives,
+ secretRoom: null,
+ position: null,
+ moves: null
+ };
+ this.game.warrior[1] = {
+ lives: this.settings.maxLives,
+ secretRoom: null,
+ position: null,
+ moves: null
+ };
+ this.game.treasure = {
+ room: null,
+ warrior: -1,
+ visible: false
+ }
+ this.game.dragon = {
+ position: null,
+ state: this.DRAGON_STATE.Asleep,
+ visible: false
+ };
+ this.selectWarriorRoom(0);
+ },
+ start() {
+ document.body.className += " play";
+ if (!this.game.init) {
+ this.reset();
+ this.game.init = true;
+ }
+ },
+ toggleSwitch() {
+ this.game.switchButtonOn = !this.game.switchButtonOn;
+ this.elements.buttons[0].className = this.elements.buttons[0].className.replace(" selected", "");
+ if (this.game.switchButtonOn) this.elements.buttons[0].className += " selected";
+ },
+ toggleSwitchOff() {
+ if (this.game.switchButtonOn) {
+ this.elements.buttons[0].className = this.elements.buttons[0].className.replace(" selected", "");
+ this.game.switchButtonOn = false;
+ }
+ },
+ toggleLevel() {
+ this.game.level = this.game.level == 1 ? 2 : 1;
+ let sound = this.game.level == 1 ? this.audio.levelOne : this.audio.levelTwo;
+ this.help("Switched to game level " + (this.game.level == 1 ? "one (no doors)" : "two (with doors)"));
+ this.playAudio(sound);
+ },
+ playAudioClick(index, secondary) {
+ if (!secondary) secondary = this.game.switchButtonOn ? true : false;
+ this.toggleSwitchOff();
+ let message = "Playing sound \"" + this.audioNameIndex[secondary ? 1 : 0][index] + "\"";
+ this.help(message, true, true);
+ var timestamp = (new Date()).getTime();
+ this.playAudio(this.audioIndex[secondary ? 1 : 0][index]).then(() => {
+ let duration = (new Date()).getTime() - timestamp;
+ if (duration < 1000) {
+ var self = this;
+ setTimeout(function() { self.resetHelpMessage.call(self); }, 1000 - duration);
+ }
+ else {
+ this.resetHelpMessage();
+ }
+ });
+ },
+ numToText(warriorNumber, oneText, twoText) {
+ return warriorNumber == 0 ? (oneText ? oneText : "one") : (twoText ? twoText : "two");
+ },
+ selectWarriorRoom(warriorNumber) {
+ this.game.state = warriorNumber == 0 ? this.GAME_STATE.WarriorOneSelectRoom : this.GAME_STATE.WarriorTwoSelectRoom;
+ this.playAudio(warriorNumber == 0 ? this.audio.warriorOne : this.audio.warriorTwo);
+ this.help("Pick a secret room for warrior " + this.numToText(warriorNumber, "one", "two or tap \"Next Turn\" for a one player game"))
+ this.log("Select secret room for warrior " + this.numToText(warriorNumber));
+
+ },
+ illegalWarriorRoom(warriorNumber) {
+ this.log("No room selected for warrior " + this.numToText(warriorNumber));
+ this.help("Invalid secret room", true);
+ this.playAudio(this.audio.illegalMove);
+ },
+ nextTurn() {
+ if (this.game.switchButtonOn) {
+ this.toggleSwitchOff();
+ this.toggleLevel();
+ }
+ else {
+ if (this.game.state == this.GAME_STATE.WarriorOneSelectRoom) {
+ if (!this.game.warrior[0].secretRoom) {
+ this.illegalWarriorRoom(0);
+ }
+ else {
+ this.selectWarriorRoom(1);
+ }
+ } else if (this.game.state == this.GAME_STATE.WarriorTwoSelectRoom) {
+ this.startTurns();
+ } else if (this.game.state == this.GAME_STATE.WarriorOneTurn) {
+ this.finishWarriorTurn(0);
+ } else if (this.game.state == this.GAME_STATE.WarriorTwoTurn) {
+ this.finishWarriorTurn(1);
+ }
+ }
+ },
+ startTurns() {
+ document.body.className += " gameStarted";
+ this.game.numberOfWarriors = this.game.warrior[1].secretRoom ? 2 : 1;
+ this.log("Number of players: " + this.game.numberOfWarriors);
+ this.setTreasureRoom();
+ this.log("Treasure room at: " + this.game.treasure.room);
+ this.warriorTurn(0);
+ },
+ warriorTurn(warriorNumber) {
+ this.game.state = warriorNumber == 0 ? this.GAME_STATE.WarriorOneTurn : this.GAME_STATE.WarriorTwoTurn;
+ let moves = this.resetWarriorMoves(warriorNumber)
+ let message = "Warrior " + this.numToText(warriorNumber) + "'s turn with " + moves + " move" + (moves == 1 ? "" : "s");
+ this.help(message);
+ this.log(message);
+ this.playAudio(warriorNumber == 0 ? this.audio.warriorOne : this.audio.warriorTwo);
+ },
+ illegalMove(warriorNumber, chamber) {
+ this.help("Illegal move for warrior " + this.numToText(warriorNumber), true);
+ this.log("Illegal move to " + chamber + " for warrior " + this.numToText(warriorNumber));
+ this.playAudio(this.audio.illegalMove);
+ },
+ chamberSelect(chamber, event) {
+ if (!this.game.delayTimeout) {
+ var self = this;
+ this.game.delayTimeout = setTimeout(function() { self.game.delayTimeout = null; }, 250);
+ if (!this.audioPlayTimeout) {
+ if (this.game.state == this.GAME_STATE.WarriorOneSelectRoom) {
+ this.setWarriorRoom(0, chamber);
+ } else if (this.game.state == this.GAME_STATE.WarriorTwoSelectRoom) {
+ if (chamber[0] == this.game.warrior[0].secretRoom[0] && chamber[1] == this.game.warrior[0].secretRoom[1]) {
+ this.playAudio(this.audio.illegalMove);
+ }
+ else {
+ this.setWarriorRoom(1, chamber);
+ }
+ } else if (this.game.state == this.GAME_STATE.WarriorOneTurn || this.game.state == this.GAME_STATE.WarriorTwoTurn) {
+ let warriorNumber = this.game.state == this.GAME_STATE.WarriorOneTurn ? 0 : 1;
+ this.warriorMoving(warriorNumber, chamber);
+ } else if (this.game.state == this.GAME_STATE.GameOver) {
+ this.log("Ignoring selection since game is over");
+ }
+ }
+ }
+ },
+ warriorMoving(warriorNumber, chamber) {
+ if (this.settings.debug) {
+ document.querySelectorAll(".treasureOptionDebug").forEach(function(optionChamber){
+ optionChamber.className = optionChamber.className.replace(" treasureOptionDebug", "");
+ });
+ document.querySelectorAll(".treasureDebug").forEach(function(optionChamber){
+ optionChamber.className = optionChamber.className.replace(" treasureDebug", "");
+ });
+ }
+ let position = this.game.warrior[warriorNumber].position;
+ let dir = this.getMoveDirection(this.game.warrior[warriorNumber].position, chamber);
+ if (dir == null) {
+ this.illegalMove(warriorNumber, chamber);
+ }
+ else {
+ this.game.warrior[warriorNumber].moves--;
+ let type = this.getPathType(position, dir);
+ let warriorName = this.numToText(warriorNumber);
+ if (type == this.PATH_TYPE.Wall) {
+ let message = "Warrior " + warriorName + " moves into wall";
+ this.help(message, true);
+ this.log(message);
+ this.renderWall(position, dir);
+ this.playAudio(this.audio.wall).then(() => {
+ this.finishWarriorTurn(warriorNumber);
+ });
+ } else if (this.game.level == 2 && type == this.PATH_TYPE.Door) {
+ if (Math.random() <= this.settings.doorClosedProb) {
+ let message = "Warrior " + warriorName + " moves into closed door";
+ this.help(message);
+ this.log(message);
+ this.playAudio(this.audio.door).then(() => {
+ this.finishWarriorTurn(warriorNumber);
+ });
+ }
+ else {
+ this.log("Warrior " + warriorName + " moves successfully thru door");
+ this.warriorMoved(warriorNumber, chamber);
+ }
+ } else {
+ this.log("Warrior " + warriorName + " moves successfully");
+ this.warriorMoved(warriorNumber, chamber);
+ }
+ }
+ },
+ warriorMoved(warriorNumber, chamber) {
+ this.playAudio(this.audio.warriorMoves).then(() => {
+ this.game.warrior[warriorNumber].position = chamber;
+ this.renderWarriorPosition(warriorNumber);
+ this.checkTreasureFound(warriorNumber).then(() => {
+ if (this.game.treasure.warrior == warriorNumber && this.getDistance(chamber, this.game.warrior[warriorNumber].secretRoom) == 0) {
+ this.help("Warrior " + this.numToText(warriorNumber) + " wins!!");
+ this.log("Warrior " + this.numToText(warriorNumber) + " wins");
+ this.trackEvent("Game Won");
+ this.game.warrior[warriorNumber].moves = 0;
+ this.playAudio(this.audio.winner).then(() => {
+ this.game.state = this.GAME_STATE.GameOver;
+ });
+ }
+ else {
+ this.checkWarriorBattle().then(() => {
+ this.checkDragonWakes(warriorNumber).then(() => {
+ this.checkDragonAttacks().then((attacked) => {
+ if (attacked) {
+ this.game.warrior[warriorNumber].moves = 0;
+ }
+ let remainingMoves = this.game.warrior[warriorNumber].moves;
+ if (remainingMoves == 0) {
+ this.finishWarriorTurn(warriorNumber)
+ }
+ else {
+ let message = "Warrior " + this.numToText(warriorNumber) + " has " + remainingMoves + " move" + (remainingMoves == 1 ? "" : "s") + " remaining";
+ this.help(message);
+ this.log(message);
+ };
+ });
+ });
+ });
+ }
+ });
+ });
+ },
+ checkWarriorBattle() {
+ return new Promise((resolve, reject) => {
+ let bothWarriorsLive = this.game.numberOfWarriors == 2 && this.game.warrior[0].lives > 0 && this.game.warrior[1].lives > 0;
+ if (bothWarriorsLive && this.game.treasure.warrior >=0 && this.getDistance(this.game.warrior[0].position, this.game.warrior[1].position) == 0) {
+ this.log("Warriors battle for the treasure");
+ let winner = Math.floor(Math.random() * 2);
+ let message = "Warrior " + (winner == 0 ? "one" : "two") + " " + (winner == this.game.treasure.warrior ? "keeps" : "steals") + " the treasure";
+ this.game.treasure.warrior = winner;
+ this.help(message);
+ this.log(message);
+ this.playAudio(winner == 0 ? this.audio.warriorOne : this.audio.warriorTwo).then(() =>{
+ this.playAudio(this.audio.treasure).then(() =>{
+ resolve();
+ })
+ })
+ }
+ else {
+ resolve();
+ }
+ });
+ },
+ renderWall(position, dir) {
+ if (this.getPathType(position, dir) == this.PATH_TYPE.Wall) {
+ let wall = null;
+ if (dir == this.DIR_TYPE.North && position[0] != 0) {
+ wall = this.elements.chambers[position[0] - 1][position[1]].querySelector(".southWall");
+ } else if (dir == this.DIR_TYPE.West && position[1] != 0) {
+ wall = this.elements.chambers[position[0]][position[1] - 1].querySelector(".eastWall");
+ } else if (dir == this.DIR_TYPE.South && position[0] != 7) {
+ wall = this.elements.chambers[position[0]][position[1]].querySelector(".southWall");
+ } else if (dir == this.DIR_TYPE.East && position[1] != 7) {
+ wall = this.elements.chambers[position[0]][position[1]].querySelector(".eastWall");
+ }
+ if (wall) {
+ wall.style.visibility = "visible";
+ }
+ }
+ },
+ finishWarriorTurn(currentWarrior) {
+ if (this.game.state != this.GAME_STATE.GameOver) {
+ if (currentWarrior == 0 && this.game.numberOfWarriors == 2 && this.game.warrior[1].lives > 0) {
+ this.warriorTurn(1);
+ }
+ else {
+ this.moveDragon().then(() => {
+ this.warriorTurn(this.game.warrior[0].lives > 0 ? 0 : 1);
+ });
+ }
+ }
+ },
+ checkDragonWakes(warriorNumber) {
+ return new Promise((resolve, reject) => {
+ if (this.game.dragon.state == this.DRAGON_STATE.Asleep) {
+ if (this.getDistance(this.game.warrior[warriorNumber].position, this.game.warrior[warriorNumber].secretRoom) != 0) {
+ let dragonDistance = this.getDistance(this.game.warrior[warriorNumber].position, this.game.dragon.position);
+ if (dragonDistance <= this.settings.dragonWakeDistance) {
+ this.help("Dragon awakes!");
+ this.log("Dragon awakes");
+ this.trackEvent("Dragon Awakes");
+ this.playAudio(this.audio.dragonWakes).then(() => {
+ this.help("");
+ this.game.dragon.state = this.DRAGON_STATE.Awake;
+ resolve();
+ });
+ }
+ else {
+ resolve();
+ }
+ }
+ else {
+ resolve();
+ }
+ }
+ else {
+ resolve();
+ }
+ });
+ },
+ unsafeWarriors: function() {
+ let warriors = [];
+ if (this.game.warrior[0].lives > 0 && !this.warriorSafe(0)) warriors.push(0);
+ if (this.game.numberOfWarriors == 2 && this.game.warrior[1].lives > 0 && !this.warriorSafe(1)) warriors.push(1);
+ return warriors;
+ },
+ moveDragon() {
+ return new Promise((resolve, reject) => {
+ if (this.game.dragon.state == this.DRAGON_STATE.Awake) {
+ let dragonFlies = true;
+ let moveTowards = null;
+ let followWarrior = -1;
+ if (this.game.treasure.warrior < 0) {
+ let warriors = this.unsafeWarriors();
+ if (warriors.length == 2) {
+ let warriorOneDistance = this.getDistance(this.game.dragon.position, this.game.warrior[0].position);
+ let warriorTwoDistance = this.getDistance(this.game.dragon.position, this.game.warrior[1].position);
+ followWarrior = warriorOneDistance < warriorTwoDistance ? 0 : (warriorOneDistance > warriorTwoDistance ? 1 : Math.floor(Math.random() * 2));
+ }
+ else {
+ followWarrior = warriors[0];
+ }
+ }
+ if (this.game.treasure.warrior >= 0) {
+ followWarrior = this.game.treasure.warrior;
+ let message = "Dragon follows treasure with warrior " + (followWarrior == 0 ? "one" : "two");
+ this.help(message, true);
+ this.log(message);
+ moveTowards = this.game.warrior[followWarrior].position;
+ } else if (followWarrior >= 0) {
+ let message = "Dragon follows warrior " + (followWarrior == 0 ? "one" : "two");
+ if (this.game.dragon.visible) {
+ this.help(message, true);
+ }
+ else {
+ let warriorText = "closest warrior";
+ if (this.game.numberOfWarriors == 1 || this.game.warrior[1].lives == 0) {
+ warriorText = "warrior one";
+ }
+ else if (this.game.numberOfWarriors == 2 && this.game.warrior[0].lives == 0) {
+ warriorText = "warrior two";
+ }
+ this.help("Dragon follows " + warriorText, true);
+ }
+ this.log(message);
+ moveTowards = this.game.warrior[followWarrior].position;
+ }
+ else if (this.getDistance(this.game.dragon.position, this.game.treasure.room) > 0) {
+ this.log("Dragon returns towards treasure");
+ if (!this.game.treasure.visible) {
+ this.help("Dragon moves to an unknown location towards the treasure room", true);
+ this.log("Hiding dragon since the treasure room location is unknown");
+ this.game.dragon.visible = false;
+ }
+ else {
+ this.help("Dragon moves back towards the treasure room", true);
+ }
+ moveTowards = this.game.treasure.room;
+ } else {
+ this.log("Dragon stays in treasure room");
+ if (!this.game.treasure.visible) {
+ this.game.dragon.visible = false;
+ }
+ dragonFlies = false;
+ }
+ if (dragonFlies) {
+ if (moveTowards) {
+ let lastPosition = this.game.dragon.position;
+ let moveY = Math.sign(moveTowards[0] - lastPosition[0]);
+ let moveX = Math.sign(moveTowards[1] - lastPosition[1]);
+ let newPosition = [
+ lastPosition[0] + moveY,
+ lastPosition[1] + moveX
+ ];
+ this.game.dragon.position = newPosition;
+ this.renderDragonPosition();
+ }
+ this.playAudio(this.audio.dragonFlying).then(() => {
+ this.checkDragonAttacks().then(() => {
+ resolve();
+ });
+ });
+ }
+ else {
+ resolve();
+ }
+ }
+ else {
+ resolve();
+ }
+ });
+ },
+ checkDragonAttacks() {
+ return new Promise((resolve, reject) => {
+ let warriors = this.unsafeWarriors();
+ let self = this;
+ let threatenedWarriors = [];
+ warriors.forEach(function(warrior) {
+ if (self.getDistance(self.game.dragon.position, self.game.warrior[warrior].position) == 0) threatenedWarriors.push(warrior);
+ });
+ let attackWarrior = threatenedWarriors.length == 1 ? threatenedWarriors[0] : -1;
+ let multipleWarriors = threatenedWarriors.length == 2;
+ if (multipleWarriors) {
+ if (this.game.treasure.warrior >= 0) {
+ attackWarrior = this.game.treasure.warrior;
+ }
+ else {
+ attackWarrior = Math.floor(Math.random() * 2);
+ }
+ }
+ if (attackWarrior >= 0 ) {
+ this.dragonAttack(attackWarrior).then((attacked) => {
+ resolve(true);
+ });
+ }
+ else {
+ resolve(false);
+ }
+ });
+ },
+ dragonAttack(warriorNumber) {
+ return new Promise((resolve, reject) => {
+ let message = "Dragon attacks warrior " + this.numToText(warriorNumber);
+ this.help(message);
+ this.log(message);
+ if (!this.game.dragon.visible) {
+ this.game.dragon.visible = true;
+ this.renderDragonPosition();
+ }
+ this.playAudio(warriorNumber == 0 ? this.audio.warriorOne : this.audio.warriorTwo).then(() => {
+ this.completeAttack(warriorNumber).then(() => {
+ resolve();
+ });
+ });
+ });
+ },
+ completeAttack(warriorNumber) {
+ return new Promise((resolve, reject) => {
+ if (this.game.treasure.warrior == warriorNumber) this.game.treasure.warrior = -1;
+ this.playAudio(this.audio.dragonAttacks).then(() => {
+ this.help("");
+ this.renderTreasurePosition();
+ this.removeLife(warriorNumber).then(() => {
+ resolve();
+ })
+ });
+ });
+ },
+ removeLife(warriorNumber) {
+ return new Promise((resolve, reject) => {
+ this.game.warrior[warriorNumber].lives--;
+ let lives = this.game.warrior[warriorNumber].lives;
+ this.log("Warrior " + this.numToText(warriorNumber) + " has " + lives + " lives");
+ if (lives < 1) {
+ this.killWarrior(warriorNumber).then(() => {
+ resolve();
+ });
+ }
+ else {
+ this.help("Warrior " + this.numToText(warriorNumber) + " has " + lives + " lives left", true);
+ var secretRoom = this.game.warrior[warriorNumber].secretRoom;
+ this.game.warrior[warriorNumber].position = secretRoom;
+ this.renderWarriorPosition(warriorNumber);
+ resolve();
+ }
+ });
+ },
+ killWarrior(warriorNumber) {
+ return new Promise((resolve, reject) => {
+ let message = "Warrior " + this.numToText(warriorNumber) + " has been killed!";
+ if (this.game.warrior[0].lives < 1 && (this.game.numberOfWarriors == 1 || this.game.warrior[1].lives < 1)) {
+ this.game.state = this.GAME_STATE.GameOver;
+ this.trackEvent("Game Lost");
+ }
+ this.trackEvent("Warrior Killed");
+ this.help(message, true);
+ this.log(message);
+ this.playAudio(this.audio.defeat).then(() => {
+ this.removeWarrior(warriorNumber);
+ this.removeSecretRoom(warriorNumber);
+ resolve();
+ });
+ });
+ },
+ checkTreasureFound(warriorNumber) {
+ return new Promise((resolve, reject) => {
+ let warriorPosition = this.game.warrior[warriorNumber].position;
+ let treasureRoom = this.game.treasure.room;
+ if (this.game.treasure.warrior < 0 && this.getDistance(warriorPosition, treasureRoom) == 0) {
+ let message = "Warrior " + this.numToText(warriorNumber) + " found the treasure!";
+ this.trackEvent("Treasure Found");
+ this.help(message, true);
+ this.log(message);
+ this.game.warrior[warriorNumber].moves = 0;
+ this.game.treasure.warrior = warriorNumber;
+ this.game.treasure.visible = true;
+ this.renderTreasureMarker();
+ this.renderTreasurePosition();
+ this.playAudio(this.audio.treasure).then(() => {
+ resolve();
+ })
+ }
+ else {
+ resolve();
+ }
+ });
+ },
+ renderTreasureMarker() {
+ let treasureRoom = this.game.treasure.room;
+ let treasureMarker = document.createElement("div");
+ treasureMarker.className = "treasureRoom";
+ let chamberElement = this.elements.chambers[treasureRoom[0]][treasureRoom[1]];
+ chamberElement.appendChild(treasureMarker);
+ },
+ warriorSafe(warriorNumber) {
+ return this.getDistance(this.game.warrior[warriorNumber].position, this.game.warrior[warriorNumber].secretRoom) == 0;
+ },
+ resetWarriorMoves(warriorNumber) {
+ let moves = this.settings.movesPerLife + (this.game.warrior[warriorNumber].lives * this.settings.movesPerLife);
+ if (this.game.treasure.warrior == warriorNumber) moves = this.settings.movesWithTreasure;
+ this.game.warrior[warriorNumber].moves = moves;
+ return moves;
+ },
+ getMoveDirection(startPos, nextPos) {
+ let dir = null;
+ let distance = this.getDistance(startPos, nextPos);
+ if (distance == 1) {
+ let y = startPos[0] - nextPos[0];
+ let x = startPos[1] - nextPos[1];
+ if (y == 1) {
+ dir = this.DIR_TYPE.North;
+ } else if (y == -1) {
+ dir = this.DIR_TYPE.South;
+ } else if (x == 1) {
+ dir = this.DIR_TYPE.West;
+ } else if (x == -1) {
+ dir = this.DIR_TYPE.East;
+ }
+ }
+ return dir;
+ },
+ getPathType(position, dir) {
+ return this.maze.chamberPaths[position[0]][position[1]][dir];
+ },
+ setWarriorRoom(warriorNumber, chamber) {
+ this.help("Pick a different room or tap \"Next Turn\"");
+ this.log("Secret room for warrior " + this.numToText(warriorNumber) + " is " + chamber.toString());
+ if (this.settings.debug) {
+ document.querySelectorAll(".traveledDebug,.travelingDebug").forEach(function(chamber) {
+ chamber.className = chamber.className.replace(" traveledDebug", "").replace(" travelingDebug", "");
+ });
+ var container = document.querySelector("#container:not(.layoutDebug)");
+ if (container) container.className += " layoutDebug";
+ }
+ this.playAudio(this.audio.warriorMoves);
+ this.removeSecretRoom(warriorNumber);
+ let markerClass = "secretRoom" + this.numToText(warriorNumber, "One", "Two");
+ let warriorMarker = document.createElement("div");
+ warriorMarker.className = markerClass;
+ let chamberElement = this.elements.chambers[chamber[0]][chamber[1]];
+ chamberElement.appendChild(warriorMarker);
+ this.game.warrior[warriorNumber].secretRoom = chamber;
+ this.game.warrior[warriorNumber].position = chamber;
+ this.renderWarriorPosition(warriorNumber);
+ },
+ removeSecretRoom(warriorNumber) {
+ let secretRoom = this.game.warrior[warriorNumber].secretRoom;
+ let markerClass = "secretRoom" + this.numToText(warriorNumber, "One", "Two");
+ if (secretRoom) {
+ let chamberElement = this.elements.chambers[secretRoom[0]][secretRoom[1]];
+ let warriorMarker = chamberElement.querySelector("." + markerClass);
+ if (warriorMarker) {
+ chamberElement.removeChild(warriorMarker);
+ }
+ }
+ },
+ renderWarriorPosition(warriorNumber) {
+ this.removeWarrior(warriorNumber);
+ let name = "warrior" + this.numToText(warriorNumber, "One", "Two");
+ let position = this.game.warrior[warriorNumber].position;
+ let warriorMarker = document.createElement("div");
+ warriorMarker.id = name;
+ warriorMarker.className = name;
+ warriorMarker.style.zIndex = (position[0] * 100) + warriorNumber + 1;
+ let chamberElement = this.elements.chambers[position[0]][position[1]];
+ chamberElement.appendChild(warriorMarker);
+ if (this.game.treasure.warrior == warriorNumber) this.renderTreasurePosition();
+ },
+ removeWarrior(warriorNumber) {
+ let name = "warrior" + this.numToText(warriorNumber, "One", "Two");
+ let warriorElement = document.getElementById(name);
+ if (warriorElement) {
+ warriorElement.parentNode.removeChild(warriorElement);
+ }
+ },
+ renderDragonPosition() {
+ let dragonElement = document.getElementById("dragon");
+ if (dragonElement) {
+ dragonElement.parentNode.removeChild(dragonElement);
+ }
+ if (this.game.dragon.visible || this.settings.debug) {
+ let position = this.game.dragon.position;
+ let dragonMarker = document.createElement("div");
+ dragonMarker.id = "dragon";
+ dragonMarker.className = "dragon";
+ dragonMarker.style.zIndex = (position[0] * 100) + 4;
+ let chamberElement = this.elements.chambers[position[0]][position[1]];
+ chamberElement.appendChild(dragonMarker);
+ }
+ },
+ renderTreasurePosition() {
+ if (this.game.treasure.visible || this.settings.debug) {
+ let treasureElement = document.getElementById("treasure");
+ if (treasureElement) {
+ treasureElement.parentNode.removeChild(treasureElement);
+ }
+ let withWarrior = this.game.treasure.warrior >= 0;
+ let position = this.game.treasure.room;
+ if (withWarrior) {
+ position = this.game.warrior[this.game.treasure.warrior].position;
+ }
+ let treasureMarker = document.createElement("div");
+ treasureMarker.id = "treasure";
+ treasureMarker.className = "treasure" + (withWarrior ? " withWarrior" : "");
+ treasureMarker.style.zIndex = (position[0] * 100) + 3;
+ let chamberElement = this.elements.chambers[position[0]][position[1]];
+ chamberElement.appendChild(treasureMarker);
+ }
+ },
+ setTreasureRoom() {
+ let opts = [];
+ for(let row=0; row<8; row++) {
+ for(let col=0; col<8; col++) {
+ if (this.getDistance([row,col], this.game.warrior[0].secretRoom) > this.settings.treasureRoomDistance) {
+ if (this.game.numberOfWarriors == 1 || this.getDistance([row,col], this.game.warrior[1].secretRoom) > this.settings.treasureRoomDistance) {
+ opts.push([row,col]);
+ if (this.settings.debug) this.elements.chambers[row][col].className += " treasureOptionDebug";
+ }
+ }
+ }
+ }
+ let treasurePos = this.game.treasure.room = opts[Math.floor(Math.random() * opts.length)];
+ if (this.settings.debug) this.elements.chambers[treasurePos[0]][treasurePos[1]].className += " treasureDebug";
+ this.game.treasure.room = treasurePos;
+ this.game.dragon.position = treasurePos;
+ if (this.settings.debug) {
+ this.renderDragonPosition();
+ this.renderTreasurePosition();
+ }
+ },
+ getDistance(posA, posB) {
+ return Math.abs(posA[0] - posB[0]) + Math.abs(posA[1] - posB[1]);
+ },
+ help(message, temporary, skipTimeout) {
+ if (!temporary) {
+ this.game.helpMessage = message;
+ }
+ if (temporary || !this.game.helpTimeout) {
+ this.elements.help.innerText = message;
+ if (temporary && !skipTimeout) {
+ if (this.game.helpTimeout) clearTimeout(this.game.helpTimeout);
+ var self = this;
+ this.game.helpTimeout = setTimeout(function() {
+ self.game.helpTimeout = null;
+ self.resetHelpMessage.call(self);
+ }, 2500);
+ }
+ }
+ },
+ resetHelpMessage() {
+ this.elements.help.innerText = this.game.helpMessage;
+ }
+}
+window.addEventListener('DOMContentLoaded', (event) => { labyrinth.init() });