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 + + + + +
+
+ +
+

Hello, world!

+
+ +
+
+ + + + + + + + + + + + + + + + + + + + \ 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 += ` +
+

+ Sounds from dklon +
+ Sprites from PNGKit +
+ Some code from Dr. Mathias +
+ Developed by Logan Hunt +

+
+ ` + } 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 + + + + + + + + + + +
+

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