diff --git a/centipede/.DS_Store b/centipede/.DS_Store
new file mode 100644
index 0000000..0342a22
Binary files /dev/null and b/centipede/.DS_Store differ
diff --git a/centipede/assets/.DS_Store b/centipede/assets/.DS_Store
new file mode 100644
index 0000000..12856bc
Binary files /dev/null and b/centipede/assets/.DS_Store differ
diff --git a/centipede/assets/images/.DS_Store b/centipede/assets/images/.DS_Store
new file mode 100644
index 0000000..b7bf291
Binary files /dev/null and b/centipede/assets/images/.DS_Store differ
diff --git a/centipede/assets/images/centipede-assets.png b/centipede/assets/images/centipede-assets.png
new file mode 100644
index 0000000..20b9eac
Binary files /dev/null and b/centipede/assets/images/centipede-assets.png differ
diff --git a/centipede/assets/sounds/.DS_Store b/centipede/assets/sounds/.DS_Store
new file mode 100644
index 0000000..34bc351
Binary files /dev/null and b/centipede/assets/sounds/.DS_Store differ
diff --git a/centipede/assets/sounds/enemy_hit.wav b/centipede/assets/sounds/enemy_hit.wav
new file mode 100644
index 0000000..1b9c53f
Binary files /dev/null and b/centipede/assets/sounds/enemy_hit.wav differ
diff --git a/centipede/assets/sounds/laser.wav b/centipede/assets/sounds/laser.wav
new file mode 100644
index 0000000..16159a4
Binary files /dev/null and b/centipede/assets/sounds/laser.wav differ
diff --git a/centipede/assets/sounds/mushroom_hit.wav b/centipede/assets/sounds/mushroom_hit.wav
new file mode 100644
index 0000000..ef26432
Binary files /dev/null and b/centipede/assets/sounds/mushroom_hit.wav differ
diff --git a/centipede/css/style.css b/centipede/css/style.css
new file mode 100644
index 0000000..7142e28
--- /dev/null
+++ b/centipede/css/style.css
@@ -0,0 +1,63 @@
+body {
+ margin: 0;
+ font-family: Lucida Sans Typewriter,Lucida Console,monaco,Bitstream Vera Sans Mono,monospace;
+}
+
+.canvas-holder canvas {
+ padding: 0;
+ margin: auto;
+ display: block;
+ height: 100vh;
+ width: auto;
+ max-width: 100%;
+ background-color: black;
+}
+
+.game-hud {
+ position: absolute;
+ top: 0;
+ left: 50%;
+ transform: translate(-50%, -5px);
+ z-index: 1;
+
+ background-color: rgba(255,255,255,0.5);
+ padding-left: 10px;
+ padding-right: 10px;
+ padding-top: 3px;
+ border: 1px solid white;
+ border-radius: 5px;
+}
+
+.game-hud p {
+ margin: 0;
+ padding: 0;
+ font-size: 3vh;
+ text-align: center;
+}
+
+.menu {
+ text-align:center;
+ left: 50%;
+ top: 50%;
+ -webkit-transform: translate(-50%, -50%);
+ transform: translate(-50%, -50%);
+ position:absolute;
+
+ background-color: rgba(255,255,255,0.75);
+ border: 1px solid #fff;
+ border-radius: 5px;
+ padding: 12px;
+ min-width: 400px;
+}
+
+.menu-button {
+ background-color: #fff;
+ border-radius: 5px;
+ padding: 12px;
+ margin-bottom: 8px;
+ cursor: pointer;
+}
+
+.menu-button:hover {
+ background-color: #d0d0d0;
+}
\ No newline at end of file
diff --git a/centipede/index.html b/centipede/index.html
new file mode 100644
index 0000000..3d73470
--- /dev/null
+++ b/centipede/index.html
@@ -0,0 +1,38 @@
+
+
+
+ Centipede
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/centipede/js/game/game.js b/centipede/js/game/game.js
new file mode 100644
index 0000000..30454f9
--- /dev/null
+++ b/centipede/js/game/game.js
@@ -0,0 +1,39 @@
+const game = {
+ stopped: false,
+ width: document.getElementById('game-canvas').width,
+ height: document.getElementById('game-canvas').height,
+ level: 1,
+};
+
+game.resume = () => {
+ game.stopped = false;
+ game.lastTimeStamp = performance.now();
+ menu.reRegisterKeys();
+ requestAnimationFrame(gameLoop);
+}
+
+game.resetObjects = () => {
+ game.player.x = game.width/2;
+ game.player.y = game.height/2;
+ game.bullets = [];
+ game.explosions = [];
+ game.mushroomDims = {width: 40, height: 40};
+ game.mushrooms = game.Mushroom.generateMushrooms(game.mushroomDims);
+ game.centipede = game.Centipede({segments: Math.min(game.level*5 + 5, 15), startX: game.width/2, startY: 0, rot: 180, width: 40, height: 40, dx: 0.2, dy: 0});
+ game.spiders = [];
+ game.fleas = [];
+ game.scorpions = [];
+}
+
+game.gameOver = () => {
+ menu.showMenu();
+ menu.setState('game-over');
+ menu.addScore(game.score);
+
+ menu.onHide = initialize;
+}
+
+game.getObjects = () => [game.player, ...game.bullets, ...game.mushrooms, ...game.spiders, ...game.fleas, ...game.scorpions, game.centipede, ...game.explosions];
+game.getBulletCollidableObjects = () => [...game.mushrooms, ...game.spiders, ...game.fleas, ...game.scorpions, game.centipede];
+game.getMushroomCollidableObjects = () => [game.player, ...game.scorpions, game.centipede];
+game.getPlayerCollidableObjects = () => [...game.spiders, ...game.fleas, ...game.scorpions, game.centipede]
\ No newline at end of file
diff --git a/centipede/js/game/graphics.js b/centipede/js/game/graphics.js
new file mode 100644
index 0000000..3a0d7f9
--- /dev/null
+++ b/centipede/js/game/graphics.js
@@ -0,0 +1,55 @@
+game.graphics = (
+ (context) => {
+ context.imageSmoothingEnabled = false;
+ const clear = () => {
+ context.clearRect(0, 0, game.width, game.height);
+ };
+
+ const Sprite = ({sheetSrc, spriteX, spriteY, spriteWidth, spriteHeight, timePerFrame, cols, rows, numFrames, drawFunction}) => {
+ timePerFrame = timePerFrame ?? 100;
+ numFrames = numFrames ?? 1;
+ cols = cols ?? numFrames;
+ rows = rows ?? 1;
+
+ let ready = false;
+
+ let image;
+ if (sheetSrc) {
+ image = new Image();
+ image.src = sheetSrc;
+ image.onload = () => { ready = true; };
+ }
+
+ let currentFrame = 0;
+ let lastTime = performance.now();
+
+ let draw;
+ if (!drawFunction) {
+ draw = (_elapsedTime, {x, y, rot, width, height}) => {
+
+ if (ready) {
+ if (numFrames > 1) {
+ if (performance.now()-lastTime > timePerFrame) {
+ lastTime = performance.now();
+ currentFrame = (currentFrame + 1) % numFrames;
+ }
+ }
+ context.save();
+ context.translate(x+width/2, y+height/2);
+ context.rotate(rot * Math.PI / 180);
+ context.translate(-x-width/2, -y-height/2);
+ const row = currentFrame % rows;
+ const col = Math.floor(currentFrame / rows);
+ context.drawImage(image, spriteX+col*spriteWidth, spriteY+row*spriteHeight, spriteWidth, spriteHeight, x, y, width, height);
+ context.restore();
+ }
+ };
+ } else {
+ draw = (elapsedTime, drawSpec) => drawFunction(elapsedTime, drawSpec, context);
+ }
+ return { draw, timePerFrame, numFrames };
+ }
+
+ return { clear, Sprite };
+ }
+)(document.getElementById("game-canvas").getContext("2d"));
\ No newline at end of file
diff --git a/centipede/js/game/input.js b/centipede/js/game/input.js
new file mode 100644
index 0000000..6ceff13
--- /dev/null
+++ b/centipede/js/game/input.js
@@ -0,0 +1,32 @@
+game.input = (() => {
+ "use strict";
+ const Keyboard = () => {
+ const keys = {};
+ const handlers = {};
+ const keyPress = (event) => {
+ keys[event.key] = event.timeStamp;
+ };
+ const keyRelease = (event) => {
+ delete keys[event.key];
+ };
+ const registerCommand = (key, handler) => {
+ handlers[key] = handler;
+ };
+ const unregisterCommand = (key) => {
+ delete handlers[key];
+ }
+ const update = (elapsedTime) => {
+ for (let key in keys) {
+ if (keys.hasOwnProperty(key)) {
+ if (handlers[key]) {
+ handlers[key](elapsedTime);
+ }
+ }
+ }
+ };
+ window.addEventListener("keydown", keyPress);
+ window.addEventListener("keyup", keyRelease);
+ return {keys, handlers, registerCommand, unregisterCommand, update};
+ }
+ return { Keyboard };
+})();
\ No newline at end of file
diff --git a/centipede/js/game/menu.js b/centipede/js/game/menu.js
new file mode 100644
index 0000000..d4a7fe9
--- /dev/null
+++ b/centipede/js/game/menu.js
@@ -0,0 +1,143 @@
+const menu = {};
+menu.initialize = () => {
+ menu.scores = localStorage.getItem("scores") ? JSON.parse(localStorage.getItem("scores")) : [];
+ menu.state = "main";
+
+ menu.controls = localStorage.getItem("controls") ? JSON.parse(localStorage.getItem("controls")) : {
+ "moveUp": "ArrowUp",
+ "moveDown": "ArrowDown",
+ "moveLeft": "ArrowLeft",
+ "moveRight": "ArrowRight",
+ "shoot": " ",
+ };
+}
+
+menu.setState = (state) => {
+ menu.state = state;
+ menu.draw();
+}
+
+menu.escapeEventListener = (e) => {
+ if (e.key == "Escape") {
+ menu.setState('main');
+ menu.draw();
+ }
+}
+
+menu.showMenu = () => {
+ menu.draw();
+ game.stopped = true;
+ window.addEventListener("keydown", menu.escapeEventListener);
+}
+
+menu.reRegisterKeys = () => {
+ Object.keys(game.keyboard.handlers).map(key => game.keyboard.unregisterCommand(key));
+ game.keyboard.registerCommand(menu.controls.moveUp, game.player.moveUp);
+ game.keyboard.registerCommand(menu.controls.moveDown, game.player.moveDown);
+ game.keyboard.registerCommand(menu.controls.moveLeft, game.player.moveLeft);
+ game.keyboard.registerCommand(menu.controls.moveRight, game.player.moveRight);
+ game.keyboard.registerCommand(menu.controls.shoot, game.player.shoot);
+ game.keyboard.registerCommand("Escape", menu.showMenu);
+ localStorage.setItem("controls", JSON.stringify(menu.controls));
+}
+
+menu.addScore = (score) => {
+ menu.scores.push(score);
+ menu.scores.sort((a, b) => b - a);
+ localStorage.setItem("scores", JSON.stringify(menu.scores));
+}
+
+menu.hide = () => {
+ const menuElement = document.getElementById("menu");
+ menuElement.style.display = "none";
+ menu.reRegisterKeys();
+ window.removeEventListener("keydown", menu.escapeEventListener);
+ if (menu.onHide) {
+ menu.onHide();
+ menu.onHide = null;
+ }
+ game.resume();
+}
+
+menu.listenFor = (action, elementId) => {
+ const element = document.getElementById(elementId);
+ element.innerHTML = "Listening...";
+ const handleKey = (event) => {
+ window.removeEventListener("keydown", handleKey);
+ if (event.key == "Escape") {
+ element.innerHTML = menu.controls[action];
+ return;
+ }
+ menu.controls[action] = event.key;
+ element.innerHTML = event.key;
+ }
+ window.addEventListener("keydown", handleKey);
+}
+
+menu.draw = () => {
+ const menuElement = document.getElementById("menu");
+ menuElement.style.display = "block";
+ menuElement.innerHTML = `Centipede
`;
+ if (menu.state == "main") {
+ menuElement.innerHTML += `
+
+
+
+ `;
+ }
+ else if (menu.state == "controls") {
+ menuElement.innerHTML += `
+
+
+ Move left:
+
+ Move right:
+
+ Move up:
+
+ Move down:
+
+ Shoot:
+
+
+ `
+ } else if (menu.state == "credits") {
+ menuElement.innerHTML += `
+
+ `
+ } else if (menu.state == "scores") {
+ menuElement.innerHTML += `
+
+
+ ${menu.scores.map((score, index) => `${index + 1}: ${score}
`).join("")}
+
+
+ `
+ } else if (menu.state == "game-over") {
+ menuElement.innerHTML += `
+
+
+ Game Over
+
+ Your final score was: ${game.score}
+
+ `
+ }
+
+ menuElement.innerHTML += ""
+ if (menu.state !== "main") {
+ menuElement.innerHTML += ""
+ }
+}
+
+menu.initialize();
\ No newline at end of file
diff --git a/centipede/js/game/object.js b/centipede/js/game/object.js
new file mode 100644
index 0000000..1e22ec6
--- /dev/null
+++ b/centipede/js/game/object.js
@@ -0,0 +1,51 @@
+game.Object = (object) => {
+ object.dx = object.dx ?? 0;
+ object.dy = object.dy ?? 0;
+ object.rot = object.rot ?? 0;
+ object.drot = object.drot ?? 0;
+ object.alive = object.alive ?? true;
+
+ object.poisonedTimer = object.poisonedTimer ?? 4000;
+ object.poisoned = false;
+ object.elapsedPoisonedTimer = 0;
+ object.poison = () => {
+ object.poisoned = true;
+ object.elapsedPoisonedTimer = 0;
+ }
+
+ object.intersects = (other) => {
+ if (object.x + object.width <= other.x) {
+ return false;
+ }
+ if (object.x >= other.x + other.width) {
+ return false;
+ }
+ if (object.y + object.height <= other.y) {
+ return false;
+ }
+ if (object.y >= other.y + other.height) {
+ return false;
+ }
+ return true;
+ }
+
+ object.update = (elapsedTime) => {
+ if (object.poisoned && object.y >= game.height - object.height) {
+ object.elapsedPoisonedTimer += elapsedTime;
+ if (object.elapsedPoisonedTimer > object.poisonedTimer) {
+ object.poisoned = false;
+ object.elapsedPoisonedTimer = 0;
+ }
+ }
+
+ object.x += (object.poisoned ? 0 : object.dx)*elapsedTime;
+ object.y += (object.poisoned ? 0.2 : object.dy)*elapsedTime;
+ object.rot += object.drot*elapsedTime;
+ };
+
+ object.draw = (elapsedTime) => {
+ object.sprite.draw(elapsedTime, object);
+ };
+
+ return object;
+};
diff --git a/centipede/js/game/objects/bullet.js b/centipede/js/game/objects/bullet.js
new file mode 100644
index 0000000..5791aca
--- /dev/null
+++ b/centipede/js/game/objects/bullet.js
@@ -0,0 +1,11 @@
+game.Bullet = (spec) => {
+ const object = game.Object(spec);
+ const parentUpdate = object.update;
+ object.update = (elapsedTime) => {
+ parentUpdate(elapsedTime);
+ if (object.y < 0) {
+ object.alive = false;
+ }
+ };
+ return object;
+}
\ No newline at end of file
diff --git a/centipede/js/game/objects/centipede.js b/centipede/js/game/objects/centipede.js
new file mode 100644
index 0000000..7e4c8f6
--- /dev/null
+++ b/centipede/js/game/objects/centipede.js
@@ -0,0 +1,105 @@
+game.CentipedePiece = (spec) => {
+ const object = game.Object(spec);
+ object.poisonedTimer = 1000;
+ const parentUpdate = object.update;
+ object.turningState = {
+ turning: false,
+ turnDirectionY: 1,
+ objectStateBeforeTurn: null,
+ };
+ object.turn = () => {
+ object.turningState.objectStateBeforeTurn = {dx: object.dx, dy: object.dy, x: object.x, y: object.y};
+ object.turningState.turning = true;
+ if (object.y >= game.height - object.height) {
+ object.turningState.turnDirectionY = -1;
+ }
+ if (object.y <= 0) {
+ object.turningState.turnDirectionY = 1;
+ }
+ };
+ object.update = (elapsedTime) => {
+ if (object.poisoned) {
+ object.turningState.turning = false;
+ }
+ else if ((object.x+object.width > game.width || object.x < 0) && !object.turningState.turning) {
+ object.x = Math.min(Math.max(object.x, 0), game.width - object.width);
+ object.turn();
+ }
+ if (object.turningState.turning) {
+ object.dx = 0;
+ object.dy = Math.abs(object.turningState.objectStateBeforeTurn.dx) * object.turningState.turnDirectionY;
+ object.rot = object.dy > 0 ? -90 : 90;
+ if (Math.abs(object.turningState.objectStateBeforeTurn.y - object.y) >= object.height) {
+ object.y = object.turningState.objectStateBeforeTurn.y + object.height * object.turningState.turnDirectionY;
+ object.dx = -object.turningState.objectStateBeforeTurn.dx;
+ object.rot = object.dx > 0 ? 180 : 0;
+ object.dy = 0;
+ object.turningState.turning = false;
+ }
+ }
+ parentUpdate(elapsedTime);
+ object.y = Math.min(Math.max(object.y, 0), game.height - object.height);
+ };
+ object.onMushroomCollision = (mushroom) => {
+ if (mushroom.poisoned && object.dy === 0) {
+ object.poison();
+ return;
+ }
+ if (!object.turningState.turning) {
+ if (mushroom.x < object.x && object.dx > 0) {
+ return;
+ }
+ object.turn();
+ }
+ }
+ return object;
+}
+
+game.Centipede = (spec) => {
+ const segments = [
+ ...Array(spec.segments).fill(0).map((_, i) => game.CentipedePiece({...spec, x: spec.startX - spec.width*(i+1), y: spec.startY, sprite: game.sprites.centipedeBody})),
+ game.CentipedePiece({...spec, x: spec.startX, y: spec.startY, sprite: game.sprites.centipedeHead}),
+ ];
+
+ const update = (elapsedTime) => {
+ segments.map((segment) => segment.update(elapsedTime));
+ }
+
+ const draw = (elapsedTime) => {
+ segments.map((segment) => segment.draw(elapsedTime));
+ }
+
+ const intersects = (object) => {
+ return segments.filter((segment) => segment.intersects(object)).length;
+ }
+
+ const onBulletCollision = (bullet) => {
+ if (bullet.alive) {
+ const segment = segments.find((segment) => segment.intersects(bullet));
+ const segmentIndex = segments.indexOf(segment);
+
+ const {mushX, mushY} = game.Mushroom.toMushCoords(segment);
+ game.explosions.push(game.Explosion({...game.Mushroom.toGameCoords({mushX, mushY}), width: segment.width, height: segment.height, sprite: game.sprites.explosionSmall}));
+ if (!game.mushrooms.find((mushroom) => mushroom.mushX === mushX && mushroom.mushY === mushY)) {
+ game.mushrooms.push(game.Mushroom({mushX, mushY, ...game.mushroomDims}));
+ }
+ game.score += segment.sprite === game.sprites.centipedeHead ? 20 : 5;
+ game.sounds.enemy_hit.load();
+ game.sounds.enemy_hit.play();
+ segments.splice(segmentIndex, 1);
+ }
+ bullet.alive = false;
+ }
+
+ const onMushroomCollision = (mushroom) => {
+ segments.find((segment) => segment.intersects(mushroom)).onMushroomCollision(mushroom);
+ }
+
+ const onPlayerCollision = (player) => {
+ player.kill();
+ }
+
+ const alive = () => segments.length ? true : false;
+
+ return {update, draw, segments, intersects, onBulletCollision, onMushroomCollision, onPlayerCollision, alive};
+}
\ No newline at end of file
diff --git a/centipede/js/game/objects/explosion.js b/centipede/js/game/objects/explosion.js
new file mode 100644
index 0000000..f38d820
--- /dev/null
+++ b/centipede/js/game/objects/explosion.js
@@ -0,0 +1,14 @@
+game.Explosion = (spec) => {
+ const object = game.Object(spec);
+ let explosionTime = 0;
+ const parentUpdate = object.update;
+ object.update = (elapsedTime) => {
+ parentUpdate(elapsedTime);
+ explosionTime += elapsedTime;
+
+ if (explosionTime > (object.sprite.numFrames * object.sprite.timePerFrame)) {
+ object.alive = false;
+ }
+ }
+ return object;
+}
\ No newline at end of file
diff --git a/centipede/js/game/objects/flea.js b/centipede/js/game/objects/flea.js
new file mode 100644
index 0000000..23479cc
--- /dev/null
+++ b/centipede/js/game/objects/flea.js
@@ -0,0 +1,49 @@
+game.Flea = (spec) => {
+ const object = game.Object(spec);
+ const parentUpdate = object.update;
+ object.mushroomCoords = game.Mushroom.toMushCoords(object);
+ object.update = (elapsedTime) => {
+ const newMushroomCoords = game.Mushroom.toMushCoords(object);
+ if (newMushroomCoords.mushY !== object.mushroomCoords.mushY || newMushroomCoords.mushX !== object.mushroomCoords.mushX) {
+ if (Math.random() < Math.min(0.15 + 0.05*game.level, 0.7)) {
+ if (!game.mushrooms.find((mushroom) => mushroom.mushX === newMushroomCoords.mushX && mushroom.mushY === newMushroomCoords.mushY)) {
+ game.mushrooms.push(game.Mushroom({...newMushroomCoords, ...game.mushroomDims}));
+ }
+ }
+ object.mushroomCoords = newMushroomCoords;
+ }
+ parentUpdate(elapsedTime);
+ };
+
+ object.onMushroomCollision = (mushroom) => {
+ if (mushroom.poisoned) {
+ mushroom.state = 0;
+ object.poison();
+ }
+ }
+
+ object.explode = () => {
+ game.explosions.push(game.Explosion({x: object.x, y: object.y, width: object.width, height: object.height, sprite: game.sprites.explosionSmall}));
+ game.sounds.enemy_hit.load();
+ game.sounds.enemy_hit.play();
+ }
+
+ object.onBulletCollision = (bullet) => {
+ game.score += 20;
+ object.alive = false;
+ object.explode();
+ }
+
+ object.onPlayerCollision = (player) => {
+ object.alive = false;
+ player.kill();
+ object.explode();
+ }
+
+ object.onBulletCollision = (bullet) => {
+ object.explode();
+ object.alive = false;
+ }
+
+ return object;
+}
\ No newline at end of file
diff --git a/centipede/js/game/objects/mushroom.js b/centipede/js/game/objects/mushroom.js
new file mode 100644
index 0000000..b1f4787
--- /dev/null
+++ b/centipede/js/game/objects/mushroom.js
@@ -0,0 +1,43 @@
+game.Mushroom = (spec) => {
+ spec.state = spec.state ?? 4;
+ const {mushX, mushY} = spec;
+ const objectSpec = {...spec};
+ objectSpec.x = mushX * objectSpec.width;
+ objectSpec.y = mushY * objectSpec.height;
+ const object = {...spec, ...game.Object(objectSpec)};
+ object.onBulletCollision = (bullet) => {
+ if (bullet.alive) {
+ object.state--;
+ game.score += 5;
+ game.sounds.mushroom_hit.load();
+ game.sounds.mushroom_hit.play();
+ }
+ bullet.alive = false;
+ };
+ object.draw = (elapsedTime) => {
+ if (object.state) {
+ object.sprite = object.poisoned ? game.sprites.poisonMushrooms[object.state-1] : game.sprites.regularMushrooms[object.state-1];
+ object.sprite.draw(elapsedTime, object);
+ }
+ }
+ return object;
+};
+
+game.Mushroom.toMushCoords = (coords) => {
+ return {mushX: Math.ceil(coords.x / game.mushroomDims.width), mushY: Math.ceil(coords.y / game.mushroomDims.height)};
+}
+
+game.Mushroom.toGameCoords = (mushCoords) => {
+ return {x: mushCoords.mushX * game.mushroomDims.width, y: mushCoords.mushY * game.mushroomDims.height};
+}
+
+game.Mushroom.generateMushrooms = (mushroomSpec) => {
+ const mushPositions = new Set();
+ for (let i = 0; i < Math.max(Math.random(), 0.05) * game.height / mushroomSpec.height * game.width / mushroomSpec.width * game.level * 0.5; i++) {
+ mushPositions.add(JSON.stringify([Math.floor(Math.random() * game.width / mushroomSpec.width), Math.max(1, Math.floor(Math.random() * (game.height / mushroomSpec.height - 3)))]));
+ }
+ return Array.from(mushPositions).map((pos) => {
+ const [mushX, mushY] = JSON.parse(pos);
+ return game.Mushroom({...mushroomSpec, mushX, mushY});
+ });
+}
diff --git a/centipede/js/game/objects/player.js b/centipede/js/game/objects/player.js
new file mode 100644
index 0000000..a1f6ea0
--- /dev/null
+++ b/centipede/js/game/objects/player.js
@@ -0,0 +1,63 @@
+game.Player = (spec) => {
+ const object = game.Object(spec);
+ object.poisonedTimer = 4000;
+ object.elapsedPoisonedTimer = 0;
+ object.poisoned = false;
+ object.bulletTimer = spec.bulletTimer ?? 150;
+ object.maxPlayerHeight = spec.maxPlayerHeight ?? game.height - object.height*6;
+ object.elapsedBulletTimer = 0;
+ object.lives = spec.lives ?? 3;
+
+ const parentUpdate = object.update;
+ object.update = (elapsedTime) => {
+ parentUpdate(elapsedTime);
+ object.x = Math.max(0, Math.min(object.x, game.width - object.width));
+ object.y = Math.max(object.maxPlayerHeight, Math.min(object.y, game.height - object.height));
+ object.dx = object.dy = 0;
+ object.elapsedBulletTimer += elapsedTime;
+ };
+ object.moveUp = () => {
+ object.dy = -0.75;
+ }
+ object.moveDown = () => {
+ object.dy = 0.75;
+ }
+ object.moveLeft = () => {
+ object.dx = -0.5;
+ }
+ object.moveRight = () => {
+ object.dx = 0.5;
+ }
+
+ object.shoot = () => {
+ if (object.elapsedBulletTimer > object.bulletTimer) {
+ object.elapsedBulletTimer = 0;
+ game.bullets.push(game.Bullet({x: object.x + object.width/2 - 5, y: object.y-object.height/2, dx: 0, dy: -1.5, width: 5, height: 50, sprite: game.sprites.bullet}));
+ game.sounds.laser.load();
+ game.sounds.laser.play();
+ }
+ }
+
+ object.onMushroomCollision = (mushroom) => {
+ if (mushroom.poisoned) {
+ mushroom.state = 0;
+ object.poison();
+ }
+ if (mushroom.x > object.x) {
+ object.x = mushroom.x - mushroom.width;
+ }
+ if (mushroom.x < object.x) {
+ object.x = mushroom.x + mushroom.width;
+ }
+ }
+
+ object.kill = () => {
+ object.lives--;
+ game.resetObjects();
+ if (object.lives == 0) {
+ game.gameOver();
+ }
+ }
+
+ return object;
+}
\ No newline at end of file
diff --git a/centipede/js/game/objects/scorpion.js b/centipede/js/game/objects/scorpion.js
new file mode 100644
index 0000000..036db14
--- /dev/null
+++ b/centipede/js/game/objects/scorpion.js
@@ -0,0 +1,32 @@
+game.Scorpion = (spec) => {
+ const object = game.Object(spec);
+
+ const parentUpdate = object.update;
+ object.update = (elapsedTime) => {
+ parentUpdate(elapsedTime);
+ };
+
+ object.explode = () => {
+ game.explosions.push(game.Explosion({x: object.x, y: object.y, width: object.width, height: object.height, sprite: game.sprites.explosionBig}));
+ game.sounds.enemy_hit.load();
+ game.sounds.enemy_hit.play();
+ }
+
+ object.onBulletCollision = (bullet) => {
+ game.score += 100;
+ object.alive = false;
+ object.explode();
+ }
+
+ object.onPlayerCollision = (player) => {
+ object.alive = false;
+ player.kill();
+ object.explode();
+ }
+
+ object.onMushroomCollision = (mushroom) => {
+ mushroom.poisoned = true;
+ }
+
+ return object;
+}
\ No newline at end of file
diff --git a/centipede/js/game/objects/spider.js b/centipede/js/game/objects/spider.js
new file mode 100644
index 0000000..732c0a3
--- /dev/null
+++ b/centipede/js/game/objects/spider.js
@@ -0,0 +1,45 @@
+game.Spider = (spec) => {
+ const object = game.Object(spec);
+
+ const parentUpdate = object.update;
+
+ object.randomizeVel = () => {
+ object.dx = Math.min(Math.random(), 0.25 + 0.05*game.level) * (Math.random() > 0.5 ? 1 : -1);
+ object.dy = Math.min(Math.random(), 0.25 + 0.05*game.level) * (Math.random() > 0.5 ? 1 : -1);
+ }
+
+ object.update = (elapsedTime) => {
+ if (Math.random() < 0.01*game.level) {
+ object.randomizeVel();
+ }
+ if (object.x < 0 || object.x > game.width - object.width) {
+ object.dx = -object.dx;
+ }
+ if (object.y < 0 || object.y > game.height - object.height) {
+ object.dy = -object.dy;
+ }
+ object.x = Math.max(0, Math.min(game.width - object.width, object.x));
+ object.y = Math.max(0, Math.min(game.height - object.height, object.y));
+ parentUpdate(elapsedTime);
+ };
+
+ object.explode = () => {
+ game.explosions.push(game.Explosion({x: object.x, y: object.y, width: object.width, height: object.height, sprite: game.sprites.explosionBig}));
+ game.sounds.enemy_hit.load();
+ game.sounds.enemy_hit.play();
+ }
+
+ object.onBulletCollision = (bullet) => {
+ game.score += 150;
+ object.alive = false;
+ object.explode();
+ }
+
+ object.onPlayerCollision = (player) => {
+ object.alive = false;
+ player.kill();
+ object.explode();
+ }
+
+ return object;
+}
\ No newline at end of file
diff --git a/centipede/js/game/sounds.js b/centipede/js/game/sounds.js
new file mode 100644
index 0000000..584dbd3
--- /dev/null
+++ b/centipede/js/game/sounds.js
@@ -0,0 +1,5 @@
+game.sounds = {
+ mushroom_hit: new Audio("assets/sounds/mushroom_hit.wav"),
+ enemy_hit: new Audio("assets/sounds/enemy_hit.wav"),
+ laser: new Audio("assets/sounds/laser.wav"),
+}
\ No newline at end of file
diff --git a/centipede/js/game/sprites.js b/centipede/js/game/sprites.js
new file mode 100644
index 0000000..3cdd77f
--- /dev/null
+++ b/centipede/js/game/sprites.js
@@ -0,0 +1,120 @@
+game.sprites = {
+ centipedeHead: game.graphics.Sprite({
+ sheetSrc: "assets/images/centipede-assets.png",
+ spriteX: 0,
+ spriteY: 0,
+ spriteWidth: 40,
+ spriteHeight: 40,
+ numFrames: 4,
+ timePerFrame: 100,
+ }),
+ centipedeBody: game.graphics.Sprite({
+ sheetSrc: "assets/images/centipede-assets.png",
+ spriteX: 0,
+ spriteY: 80,
+ spriteWidth: 40,
+ spriteHeight: 40,
+ numFrames: 4,
+ timePerFrame: 100,
+ }),
+ spider: game.graphics.Sprite({
+ sheetSrc: "assets/images/centipede-assets.png",
+ spriteX: 0,
+ spriteY: 160,
+ spriteWidth: 80,
+ spriteHeight: 40,
+ numFrames: 8,
+ timePerFrame: 100,
+ cols: 4,
+ rows: 2,
+ }),
+ flea: game.graphics.Sprite({
+ sheetSrc: "assets/images/centipede-assets.png",
+ spriteX: 320,
+ spriteY: 160,
+ spriteWidth: 45,
+ spriteHeight: 40,
+ numFrames: 4,
+ timePerFrame: 500,
+ cols: 2,
+ rows: 2,
+ }),
+ scorpion: game.graphics.Sprite({
+ sheetSrc: "assets/images/centipede-assets.png",
+ spriteX: 0,
+ spriteY: 280,
+ spriteWidth: 80,
+ spriteHeight: 40,
+ numFrames: 4,
+ timePerFrame: 500,
+ cols: 4,
+ }),
+ ship: game.graphics.Sprite({
+ sheetSrc: "assets/images/centipede-assets.png",
+ spriteX: 0,
+ spriteY: 400,
+ spriteWidth: 40,
+ spriteHeight: 40,
+ numFrames: 1,
+ timePerFrame: 0,
+ cols: 1,
+ rows: 1
+ }),
+ bullet: game.graphics.Sprite({
+ drawFunction: (_elapsedTime, {x, y, rot, width, height}, context) => {
+ context.save();
+ context.translate(x+width/2, y+height/2);
+ context.rotate(rot * Math.PI / 180);
+ context.translate(-x-width/2, -y-height/2);
+ const fillStyle = context.fillStyle;
+ context.fillStyle = "#FF0000";
+ context.fillRect(x, y, width, height);
+ context.fillStyle = fillStyle;
+ context.restore();
+ }
+ }),
+ explosionBig: game.graphics.Sprite({
+ sheetSrc: "assets/images/centipede-assets.png",
+ spriteX: 0,
+ spriteY: 320,
+ numFrames: 8,
+ spriteWidth: 80,
+ spriteHeight: 40,
+ cols: 4,
+ rows: 2,
+ timePerFrame: 30,
+ }),
+ explosionSmall: game.graphics.Sprite({
+ sheetSrc: "assets/images/centipede-assets.png",
+ spriteX: 320,
+ spriteY: 320,
+ numFrames: 6,
+ spriteWidth: 40,
+ spriteHeight: 40,
+ cols: 3,
+ rows: 2,
+ timePerFrame: 30,
+ }),
+ regularMushrooms: [3,2,1,0].map(i =>
+ game.graphics.Sprite({
+ sheetSrc: "assets/images/centipede-assets.png",
+ spriteX: 320 + i*40,
+ spriteY: 0,
+ numFrames: 1,
+ spriteWidth: 40,
+ spriteHeight: 40,
+ timePerFrame: 0,
+ })
+ ),
+ poisonMushrooms: [3,2,1,0].map(i =>
+ game.graphics.Sprite({
+ sheetSrc: "assets/images/centipede-assets.png",
+ spriteX: 320 + i*40,
+ spriteY: 40,
+ numFrames: 1,
+ spriteWidth: 40,
+ spriteHeight: 40,
+ timePerFrame: 0,
+ })
+ )
+};
\ No newline at end of file
diff --git a/centipede/js/main.js b/centipede/js/main.js
new file mode 100644
index 0000000..f4c954d
--- /dev/null
+++ b/centipede/js/main.js
@@ -0,0 +1,75 @@
+let handleInput;
+const initialize = () => {
+ game.level = 1;
+ game.score = 0;
+ game.totalTime = 0;
+
+ game.keyboard = game.input.Keyboard();
+ handleInput = game.keyboard.update;
+
+ game.lastTimeStamp = performance.now();
+
+ game.player = game.Player({x: game.width/2 - 20, y: game.height-40, width: 40, height: 40, sprite: game.sprites.ship});
+
+ game.resetObjects();
+};
+
+const update = (elapsedTime) => {
+ game.totalTime += elapsedTime;
+
+ game.mushrooms.map((mushroom) => game.getMushroomCollidableObjects().filter((object) => object.intersects(mushroom))).map((objects, i) => {
+ objects.map((object => object.onMushroomCollision ? object.onMushroomCollision(game.mushrooms[i]) : null));
+ });
+ game.bullets.map((bullet) => game.getBulletCollidableObjects().filter((object) => object.intersects(bullet))).map((objects, i) => {
+ objects.map((object) => object.onBulletCollision ? object.onBulletCollision(game.bullets[i]) : null)
+ })
+ game.getPlayerCollidableObjects().map((object) => object.intersects(game.player) ? object.onPlayerCollision(game.player) : null);
+
+ game.bullets = game.bullets.filter((bullet) => bullet.alive);
+ game.spiders = game.spiders.filter((spider) => spider.alive);
+ game.explosions = game.explosions.filter((explosion) => explosion.alive);
+ game.mushrooms = game.mushrooms.filter((mushroom) => mushroom.state > 0);
+ game.scorpions = game.scorpions.filter((scorpion) => scorpion.x >= 0 && scorpion.x <= game.width - scorpion.width && scorpion.alive);
+ game.fleas = game.fleas.filter((flea) => flea.y < game.height - flea.height && flea.alive);
+ game.getObjects().map((object) => object.update(elapsedTime));
+
+ if (Math.random() < 0.002 * game.level && game.spiders.length < game.level) {
+ game.spiders.push(game.Spider({x: game.width/2, y: 0, width: 80, height: 40, rot: 0, dx: -0.2, dy: 0, sprite: game.sprites.spider}));
+ }
+ if (Math.random() < 0.001 * game.level && game.fleas.length < game.level) {
+ game.fleas.push(game.Flea({x: Math.floor(Math.random() * game.width / game.mushroomDims.width) * game.mushroomDims.width, y: 0, width: 40, height: 40, rot: 0, dx: 0, dy: 0.2, sprite: game.sprites.flea}));
+ }
+ if (Math.random() < 0.001 * game.level && game.scorpions.length < game.level) {
+ game.scorpions.push(game.Scorpion({y: Math.floor(Math.random() * game.height / game.mushroomDims.height) * game.mushroomDims.height, x: 0, width: 80, height: 40, rot: 0, dx: 0.2, dy: 0, sprite: game.sprites.scorpion}));
+ }
+
+ if (!game.centipede.alive()) {
+ game.resetObjects();
+ game.level++;
+ game.score += game.level*400;
+ }
+};
+
+const render = (elapsedTime) => {
+ game.graphics.clear();
+ game.getObjects().map((object) => object.draw(elapsedTime));
+
+ document.getElementById("hud").innerHTML = `Level: ${game.level} Lives: ${game.player.lives} Score: ${game.score} ${game.player.poisoned ? "POISONED" : ""}`;
+};
+
+const gameLoop = (time) => {
+ const elapsedTime = time - game.lastTimeStamp;
+ game.lastTimeStamp = time;
+
+ handleInput(elapsedTime);
+ update(elapsedTime);
+ render(elapsedTime);
+
+ if (!game.stopped) {
+ requestAnimationFrame(gameLoop);
+ }
+};
+
+initialize();
+menu.reRegisterKeys();
+requestAnimationFrame(gameLoop);
\ No newline at end of file
diff --git a/maize-maze/.gitignore b/maize-maze/.gitignore
new file mode 100644
index 0000000..e43b0f9
--- /dev/null
+++ b/maize-maze/.gitignore
@@ -0,0 +1 @@
+.DS_Store
diff --git a/maize-maze/LICENSE.md b/maize-maze/LICENSE.md
new file mode 100644
index 0000000..47a36cd
--- /dev/null
+++ b/maize-maze/LICENSE.md
@@ -0,0 +1,21 @@
+Copyright (c) 2012-2022 Scott Chacon and others
+
+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/maize-maze/README.md b/maize-maze/README.md
new file mode 100644
index 0000000..d48c881
--- /dev/null
+++ b/maize-maze/README.md
@@ -0,0 +1,9 @@
+# A Simple Maze Game
+
+Controls:
++ Arrow-keys / WASD / IJKL
++ h to toggle hint
++ p to toggle path
++ b to toggle breadcrumbs
+
+![Screenshot](./screenshot.png)
diff --git a/maize-maze/css/styles.css b/maize-maze/css/styles.css
new file mode 100644
index 0000000..4c8aac0
--- /dev/null
+++ b/maize-maze/css/styles.css
@@ -0,0 +1,24 @@
+body {
+ margin: 0;
+ padding: 0;
+ overflow: scroll;
+ /* Font from https://www.cssfontstack.com/Lucida-Console */
+ font-family: Lucida Console,Lucida Sans Typewriter,monaco,Bitstream Vera Sans Mono,monospace;
+ background: radial-gradient(circle, transparent 20%, black 80vw), white;
+}
+
+.canvas-holder canvas {
+ padding: 0;
+ margin: auto;
+ display: block;
+ height: 70vh;
+ width: auto;
+ max-width: 100%;
+}
+
+.button {
+ border: 1px solid black;
+ border-radius: 5px;
+ padding: 5px;
+ cursor: pointer;
+}
\ No newline at end of file
diff --git a/maize-maze/images/corn.png b/maize-maze/images/corn.png
new file mode 100644
index 0000000..f392216
Binary files /dev/null and b/maize-maze/images/corn.png differ
diff --git a/maize-maze/images/fire.png b/maize-maze/images/fire.png
new file mode 100644
index 0000000..5e7b2ec
Binary files /dev/null and b/maize-maze/images/fire.png differ
diff --git a/maize-maze/images/popcorn.jpg b/maize-maze/images/popcorn.jpg
new file mode 100644
index 0000000..ad73e91
Binary files /dev/null and b/maize-maze/images/popcorn.jpg differ
diff --git a/maize-maze/index.html b/maize-maze/index.html
new file mode 100644
index 0000000..36bae40
--- /dev/null
+++ b/maize-maze/index.html
@@ -0,0 +1,75 @@
+
+
+
+ Amazeing Maize Maze
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+ Move with arrow keys, WASD, or IJKL
+
+ -
+ h to toggle a hint
+
+ -
+ p to toggle the shortest path
+
+ -
+ b to toggle breadcrumbs
+
+
+
+
+
+
+
+
+
+
The A-maze-ing Maize Maze!
+
Get to the fire to make popcorn! Current Time: seconds
+
+
+
+
+ 5x5
+ 10x10
+ 15x15
+ 20x20
+
+
+
+
+
+
+
+ "Low" scores (player path length - shortest path length):
+
+
+
+
+
+
+
+
+
+
diff --git a/maize-maze/js/assets.js b/maize-maze/js/assets.js
new file mode 100644
index 0000000..54ea6bb
--- /dev/null
+++ b/maize-maze/js/assets.js
@@ -0,0 +1,8 @@
+const GOAL = new Image();
+GOAL.src = "./images/fire.png";
+
+const PLAYER = new Image();
+PLAYER.src = "./images/corn.png";
+
+const BACKGROUND = new Image();
+BACKGROUND.src = "./images/popcorn.jpg";
diff --git a/maize-maze/js/game.js b/maize-maze/js/game.js
new file mode 100644
index 0000000..1249a20
--- /dev/null
+++ b/maize-maze/js/game.js
@@ -0,0 +1,249 @@
+let canvas,context;
+
+const HEIGHT = 1000;
+const WIDTH = 1000;
+
+// Next assignment: don't use so much effing global data
+let n = 10;
+let maze;
+
+let total_time;
+
+let player_pos = [];
+let goal_pos = [];
+
+let myInput = input.Keyboard();
+
+let show_player_path = false;
+let player_path = [];
+let show_next_move = false;
+let show_shortest_path = false;
+let shortest_path = [];
+
+let do_score_update = false;
+let scores = [];
+let initial_shortest_path_length = 0;
+let current_score;
+
+let do_time_update = false;
+let dom_time;
+let elapsed_time;
+let last_time;
+
+function render_maze(maze, n, dx, dy) {
+ for (let i = 0; i < n; i++) {
+ for (let j = 0; j < n; j++) {
+ context.beginPath();
+ if (maze[i][j].left) {
+ context.moveTo(j * dx, i * dy);
+ context.lineTo(j * dx, (i + 1) * dy);
+ }
+ if (maze[i][j].right) {
+ context.moveTo((j + 1) * dx, i * dy);
+ context.lineTo((j + 1) * dx, (i + 1) * dy);
+ }
+ if (maze[i][j].top) {
+ context.moveTo(j * dx, i * dy);
+ context.lineTo((j + 1) * dx, i * dy);
+ }
+ if (maze[i][j].bottom) {
+ context.moveTo(j * dx, (i + 1) * dy);
+ context.lineTo((j + 1) * dx, (i + 1) * dy);
+ }
+ context.stroke();
+ }
+ }
+}
+
+function render_player(x, y, dx, dy) {
+ context.drawImage(PLAYER, x*dx, y*dy, dx, dy);
+}
+
+function render_scores(scores) {
+ let html = scores.sort((a,b) => a-b).map((x,i) => `${i+1}: ${x}`).join('
');
+ document.getElementById('scores').innerHTML = html;
+}
+
+function render_time(total_time) {
+ document.getElementById('elapsed-time').innerHTML = total_time;
+}
+
+function render_goal(x, y, dx, dy) {
+ context.drawImage(GOAL, x*dx, y*dy, dx, dy);
+}
+
+function render_background(n, dx, dy) {
+ for (let x = 0; x < WIDTH; x += WIDTH/n) {
+ for (let y = 0; y < HEIGHT; y += HEIGHT/n) {
+ context.drawImage(BACKGROUND, x, y, dx, dy);
+ }
+ }
+}
+
+function render_circle(x,y,r,color) {
+ context.beginPath();
+ context.fillStyle = color;
+ context.arc(x, y, r, 0, 2*Math.PI);
+ context.fill();
+ context.stroke();
+}
+
+function render_path(path, dx, dy, color) {
+ path.map((coord) => {
+ render_circle(coord[0]*dx + dx/2, coord[1]*dy + dy/2, Math.min(dx/4, dy/4), color);
+ });
+}
+
+function render(elapsed) {
+ const dx = WIDTH / n;
+ const dy = HEIGHT / n;
+
+ context.fillStyle = 'rgba(255,255,255,255)';
+ context.fillRect(0, 0, canvas.width, canvas.height);
+ context.rect(0, 0, canvas.width, canvas.height);
+ context.stroke();
+
+ render_background(n, dx, dy);
+ render_maze(maze, n, dx, dy);
+
+ if (show_player_path) {
+ render_path(player_path, dx, dy, 'rgba(255, 0, 0, 255)');
+ }
+
+ if (show_shortest_path) {
+ render_path(shortest_path, dx, dy, 'rgba(255, 255, 0, 255)');
+ }
+
+ if (show_next_move && shortest_path.length>1) {
+ render_path([shortest_path[1]], dx, dy, 'rgba(255, 255, 0, 255)');
+ }
+
+ if (do_score_update) {
+ render_scores(scores);
+ }
+ if (do_time_update) {
+ render_time(dom_time);
+ }
+
+ render_player(Math.floor(player_pos[0]), Math.floor(player_pos[1]), dx, dy);
+ render_goal(goal_pos[0], goal_pos[1], dx, dy);
+}
+
+let key_actions_down = {};
+let key_actions = {
+ "move_up": ['ArrowUp', 'i', 'w'],
+ "move_right": ['ArrowRight', 'l', 'd'],
+ "move_down": ['ArrowDown', 'k', 's'],
+ "move_left": ['ArrowLeft', 'j', 'a'],
+ "breadcrumbs": ['b'],
+ "shortest_path": ['p'],
+ "hint": ['h']
+};
+function handle_input(input) {
+ if (input) {
+ if (any(key_actions['move_up'].map((x) => input.keys[x])) && !key_actions_down['move_up'] && player_pos[1] > 0) {
+ key_actions_down['move_up'] = true;
+ if (!maze[player_pos[1]][player_pos[0]].top) {
+ player_pos[1] -= 1;
+ }
+ }
+ if (any(key_actions['move_right'].map((x) => input.keys[x])) && !key_actions_down['move_right'] && player_pos[0] < n-1) {
+ key_actions_down['move_right'] = true;
+ if (!maze[player_pos[1]][player_pos[0]].right) {
+ player_pos[0] += 1;
+ }
+ }
+ if (any(key_actions['move_down'].map((x) => input.keys[x])) && !key_actions_down['move_down'] && player_pos[1] < n-1) {
+ key_actions_down['move_down'] = true;
+ if (!maze[player_pos[1]][player_pos[0]].bottom) {
+ player_pos[1] += 1;
+ }
+ }
+ if (any(key_actions['move_left'].map((x) => input.keys[x])) && !key_actions_down['move_left'] && player_pos[0] > 0) {
+ key_actions_down['move_left'] = true;
+ if (!maze[player_pos[1]][player_pos[0]].left) {
+ player_pos[0] -= 1;
+ }
+ }
+ if (input.keys['b'] && !key_actions_down['breadcrumbs']) {
+ key_actions_down['breadcrumbs'] = true;
+ show_player_path = !show_player_path;
+ }
+ if (input.keys['p'] && !key_actions_down['shortest_path']) {
+ key_actions_down['shortest_path'] = true;
+ show_shortest_path = !show_shortest_path;
+ }
+ if (input.keys['h'] && !key_actions_down['hint']) {
+ key_actions_down['hint'] = true;
+ show_next_move = !show_next_move;
+ }
+ Object.keys(key_actions).map((x) => {
+ if (key_actions_down[x] && !any(key_actions[x].map((y) => input.keys[y]))) {
+ key_actions_down[x] = false;
+ }
+ });
+ }
+}
+
+function update(elapsed) {
+ total_time += elapsed;
+
+ if (do_score_update) {
+ do_score_update = false;
+ }
+ if (do_time_update) {
+ do_time_update = false;
+ }
+
+ if (JSON.stringify(player_pos) !== JSON.stringify(player_path[player_path.length-1])) {
+ player_path.push([player_pos[0], player_pos[1]]);
+ shortest_path = solve_maze(maze, player_pos[0], player_pos[1], goal_pos[0], goal_pos[1]);
+ }
+
+ if (total_time / 1000 > dom_time) {
+ dom_time = Math.floor(total_time/1000);
+ }
+ do_time_update = true;
+
+ if (player_pos[0] == goal_pos[0] && player_pos[1] == goal_pos[1]) {
+ current_score = player_path.length - initial_shortest_path_length;
+ scores.push(current_score);
+ initialize();
+ do_score_update = true;
+ }
+}
+
+function initialize(new_n) {
+ if (new_n) {
+ n = new_n;
+ }
+ maze = generate_maze(n);
+ player_pos = [random_in_range(0, 2), random_in_range(0, 2)];
+ goal_pos = [random_in_range(n-2, n), random_in_range(n-2, n)];
+ player_path = [];
+ shortest_path = solve_maze(maze, player_pos[0], player_pos[1], goal_pos[0], goal_pos[1]);
+ initial_shortest_path_length = shortest_path.length;
+ total_time = 0;
+ current_score = 0;
+ dom_time = 0;
+ last_time = performance.now();
+}
+
+function game_loop(time_stamp) {
+ elapsed_time = time_stamp - last_time;
+ last_time = time_stamp;
+ handle_input(myInput);
+ update(elapsed_time);
+ render(elapsed_time);
+
+ // Wow! Tail call recursion! /sarcasm
+ requestAnimationFrame(game_loop);
+}
+
+window.onload = function() {
+ initialize();
+ canvas = document.getElementById('canvas');
+ context = canvas.getContext('2d');
+
+ game_loop(performance.now());
+}
diff --git a/maize-maze/js/helpers.js b/maize-maze/js/helpers.js
new file mode 100644
index 0000000..1bc6579
--- /dev/null
+++ b/maize-maze/js/helpers.js
@@ -0,0 +1,18 @@
+function random_in_range(a,z) {
+ return Math.floor(Math.random() * (z - a) + a);
+}
+
+function shuffle_array(a) {
+ for (let i = 0; i < a.length; i++) {
+ let j = random_in_range(0, i+1);
+ temp = a[i]; a[i] = a[j]; a[j] = temp;
+ }
+}
+
+function sub(p1, p2) {
+ return [p1[0] - p2[0], p1[1] - p2[1]];
+}
+
+function any(l) {
+ return l.filter((x) => x).length > 0;
+}
\ No newline at end of file
diff --git a/maize-maze/js/json-ds.js b/maize-maze/js/json-ds.js
new file mode 100644
index 0000000..feb0622
--- /dev/null
+++ b/maize-maze/js/json-ds.js
@@ -0,0 +1,30 @@
+// If I were to rewrite this, I would use IEFE's - Dean was right about OO in JS
+class JSONSet {
+ items = new Set();
+ constructor(initial){
+ if (initial) {
+ this.apply_set_function('add', initial);
+ }
+ }
+ apply_set_function(f_name, x) {
+ return this.items[f_name](JSON.stringify(x));
+ }
+}
+
+class JSONHash {
+ items = {};
+ constructor(initial_key, initial_value){
+ if (initial_key && initial_value) {
+ this.items[JSON.stringify(initial)] = initial_value;
+ }
+ }
+ set_value(key, value) {
+ this.items[JSON.stringify(key)] = value;
+ }
+ get_value(key) {
+ return this.items[JSON.stringify(key)];
+ }
+ delete_value(key) {
+ delete this.items[JSON.stringify(key)];
+ }
+}
\ No newline at end of file
diff --git a/maize-maze/js/keyboard.js b/maize-maze/js/keyboard.js
new file mode 100644
index 0000000..2196834
--- /dev/null
+++ b/maize-maze/js/keyboard.js
@@ -0,0 +1,22 @@
+// Shameless stolen code from "Process the Input" presentation
+let input = (function() {
+ function Keyboard() {
+ let that = {
+ keys : {}
+ };
+ function keyPress(e) {
+ that.keys[e.key] = e.timeStamp;
+ }
+ function keyRelease(e) {
+ delete that.keys[e.key];
+ }
+ window.addEventListener('keydown', keyPress);
+ window.addEventListener('keyup', keyRelease);
+
+ return that;
+ }
+
+ return {
+ Keyboard : Keyboard
+ };
+}());
\ No newline at end of file
diff --git a/maize-maze/js/maze.js b/maize-maze/js/maze.js
new file mode 100644
index 0000000..8bede6b
--- /dev/null
+++ b/maize-maze/js/maze.js
@@ -0,0 +1,104 @@
+function get_neighbors(maze, x, y) {
+ let neighbor_indices = [];
+ if (!maze[y][x].left && x > 0) {
+ neighbor_indices.push([x-1,y]);
+ }
+ if (!maze[y][x].right && x < maze[0].length-1) {
+ neighbor_indices.push([x+1,y]);
+ }
+ if (!maze[y][x].top && y > 0) {
+ neighbor_indices.push([x,y-1]);
+ }
+ if (!maze[y][x].bottom && y < maze.length-1) {
+ neighbor_indices.push([x,y+1]);
+ }
+ return neighbor_indices;
+}
+
+function new_cell() {
+ return {
+ left: false,
+ right: false,
+ top: false,
+ bottom: false
+ }
+}
+
+function generate_maze(n) {
+ let grid = new Array(n).fill().map((x) => (new Array(n).fill().map(new_cell)));
+
+ let point_sets = [];
+ for (let i = 0; i < n; i++) {
+ for (let j = 0; j < n; j++) {
+ point_sets.push(new JSONSet([j,i]))
+ }
+ }
+
+ let edges = [];
+ for (let i = 0; i < n; i++) {
+ for (let j = 0; j < n; j++) {
+ if (i !== n-1) {
+ edges.push([[i,j],[i+1,j]])
+ }
+ if (j !== n-1) {
+ edges.push([[i,j],[i,j+1]])
+ }
+ }
+ }
+ shuffle_array(edges);
+
+ let maze_edges = edges.map((x) => x);
+
+ while (edges.length) {
+ let edge = edges.pop();
+
+ let set_inds = edge.map((i) => point_sets.findIndex((x) => x.apply_set_function('has', i)));
+ if (set_inds[0] == -1 || set_inds[1] == -1) {
+ throw new Error("Could not find correct index");
+ }
+ if (set_inds[0] == set_inds[1]) {
+ continue;
+ }
+
+ // Perform the union of the sets
+ for (let i of point_sets[set_inds[1]].items) {
+ point_sets[set_inds[0]].items.add(i);
+ }
+
+ point_sets.splice(set_inds[1], 1);
+ maze_edges = maze_edges.filter((x) => x !== edge);
+ }
+ maze_edges.forEach((edge) => {
+ let direction = sub(edge[0], edge[1]);
+ if (direction[0] == -1) {
+ grid[edge[0][1]][edge[0][0]].right = grid[edge[1][1]][edge[1][0]].left = true;
+ }
+ else if (direction[1] == -1) {
+ grid[edge[0][1]][edge[0][0]].bottom = grid[edge[1][1]][edge[1][0]].top = true;
+ }
+ })
+ return grid;
+}
+
+function solve_maze(maze, x, y, end_x, end_y) {
+ let path = new JSONHash();
+ let visited = new JSONSet();
+ let queue = [[x,y]];
+ while (queue.length) {
+ let cell = queue.shift();
+ visited.apply_set_function('add', cell);
+ if (cell[0] == end_x && cell[1] == end_y) {
+ let sol_path = [[end_x, end_y]];
+ while (sol_path[0][0] != x || sol_path[0][1] != y) {
+ sol_path.unshift(path.get_value(sol_path[0]));
+ }
+ return sol_path;
+ }
+ for (let i of get_neighbors(maze, cell[0], cell[1])) {
+ if (!visited.apply_set_function('has', i)) {
+ queue.push(i);
+ path.set_value(i, cell);
+ }
+ }
+ }
+}
diff --git a/maize-maze/screenshot.png b/maize-maze/screenshot.png
new file mode 100644
index 0000000..965eb61
Binary files /dev/null and b/maize-maze/screenshot.png differ