Created
October 11, 2023 10:08
-
-
Save codepedia-top/5c536a9db53c4e9dd7ae648cd8f7f589 to your computer and use it in GitHub Desktop.
snake game with javaScript
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <div class="container noselect"> | |
| <div class="wrapper"> | |
| <button id="replay"> | |
| <i class="fas fa-play"></i> | |
| RESTART | |
| </button> | |
| <div id="canvas"> | |
| </div> | |
| <div id="ui"> | |
| <h2>SCORE | |
| </h2> | |
| <span id="score">00</span> | |
| </div> | |
| </div> | |
| <div id="author"> | |
| <h1>SNAKE</h1> | |
| </div> | |
| </div> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * Snake game created with plain JavaScript by Ibrahim fariat. | |
| * Follow me if you like it! | |
| **/ | |
| let dom_replay = document.querySelector("#replay"); | |
| let dom_score = document.querySelector("#score"); | |
| let dom_canvas = document.createElement("canvas"); | |
| document.querySelector("#canvas").appendChild(dom_canvas); | |
| let CTX = dom_canvas.getContext("2d"); | |
| const W = (dom_canvas.width = 400); | |
| const H = (dom_canvas.height = 400); | |
| let snake, | |
| food, | |
| currentHue, | |
| cells = 20, | |
| cellSize, | |
| isGameOver = false, | |
| tails = [], | |
| score = 00, | |
| maxScore = window.localStorage.getItem("maxScore") || undefined, | |
| particles = [], | |
| splashingParticleCount = 20, | |
| cellsCount, | |
| requestID; | |
| let helpers = { | |
| Vec: class { | |
| constructor(x, y) { | |
| this.x = x; | |
| this.y = y; | |
| } | |
| add(v) { | |
| this.x += v.x; | |
| this.y += v.y; | |
| return this; | |
| } | |
| mult(v) { | |
| if (v instanceof helpers.Vec) { | |
| this.x *= v.x; | |
| this.y *= v.y; | |
| return this; | |
| } else { | |
| this.x *= v; | |
| this.y *= v; | |
| return this; | |
| } | |
| } | |
| }, | |
| isCollision(v1, v2) { | |
| return v1.x == v2.x && v1.y == v2.y; | |
| }, | |
| garbageCollector() { | |
| for (let i = 0; i < particles.length; i++) { | |
| if (particles[i].size <= 0) { | |
| particles.splice(i, 1); | |
| } | |
| } | |
| }, | |
| drawGrid() { | |
| CTX.lineWidth = 1.1; | |
| CTX.strokeStyle = "#232332"; | |
| CTX.shadowBlur = 0; | |
| for (let i = 1; i < cells; i++) { | |
| let f = (W / cells) * i; | |
| CTX.beginPath(); | |
| CTX.moveTo(f, 0); | |
| CTX.lineTo(f, H); | |
| CTX.stroke(); | |
| CTX.beginPath(); | |
| CTX.moveTo(0, f); | |
| CTX.lineTo(W, f); | |
| CTX.stroke(); | |
| CTX.closePath(); | |
| } | |
| }, | |
| randHue() { | |
| return ~~(Math.random() * 360); | |
| }, | |
| hsl2rgb(hue, saturation, lightness) { | |
| if (hue == undefined) { | |
| return [0, 0, 0]; | |
| } | |
| var chroma = (1 - Math.abs(2 * lightness - 1)) * saturation; | |
| var huePrime = hue / 60; | |
| var secondComponent = chroma * (1 - Math.abs((huePrime % 2) - 1)); | |
| huePrime = ~~huePrime; | |
| var red; | |
| var green; | |
| var blue; | |
| if (huePrime === 0) { | |
| red = chroma; | |
| green = secondComponent; | |
| blue = 0; | |
| } else if (huePrime === 1) { | |
| red = secondComponent; | |
| green = chroma; | |
| blue = 0; | |
| } else if (huePrime === 2) { | |
| red = 0; | |
| green = chroma; | |
| blue = secondComponent; | |
| } else if (huePrime === 3) { | |
| red = 0; | |
| green = secondComponent; | |
| blue = chroma; | |
| } else if (huePrime === 4) { | |
| red = secondComponent; | |
| green = 0; | |
| blue = chroma; | |
| } else if (huePrime === 5) { | |
| red = chroma; | |
| green = 0; | |
| blue = secondComponent; | |
| } | |
| var lightnessAdjustment = lightness - chroma / 2; | |
| red += lightnessAdjustment; | |
| green += lightnessAdjustment; | |
| blue += lightnessAdjustment; | |
| return [ | |
| Math.round(red * 255), | |
| Math.round(green * 255), | |
| Math.round(blue * 255) | |
| ]; | |
| }, | |
| lerp(start, end, t) { | |
| return start * (1 - t) + end * t; | |
| } | |
| }; | |
| let KEY = { | |
| ArrowUp: false, | |
| ArrowRight: false, | |
| ArrowDown: false, | |
| ArrowLeft: false, | |
| resetState() { | |
| this.ArrowUp = false; | |
| this.ArrowRight = false; | |
| this.ArrowDown = false; | |
| this.ArrowLeft = false; | |
| }, | |
| listen() { | |
| addEventListener( | |
| "keydown", | |
| (e) => { | |
| if (e.key === "ArrowUp" && this.ArrowDown) return; | |
| if (e.key === "ArrowDown" && this.ArrowUp) return; | |
| if (e.key === "ArrowLeft" && this.ArrowRight) return; | |
| if (e.key === "ArrowRight" && this.ArrowLeft) return; | |
| this[e.key] = true; | |
| Object.keys(this) | |
| .filter((f) => f !== e.key && f !== "listen" && f !== "resetState") | |
| .forEach((k) => { | |
| this[k] = false; | |
| }); | |
| }, | |
| false | |
| ); | |
| } | |
| }; | |
| class Snake { | |
| constructor(i, type) { | |
| this.pos = new helpers.Vec(W / 2, H / 2); | |
| this.dir = new helpers.Vec(0, 0); | |
| this.type = type; | |
| this.index = i; | |
| this.delay = 5; | |
| this.size = W / cells; | |
| this.color = "white"; | |
| this.history = []; | |
| this.total = 1; | |
| } | |
| draw() { | |
| let { x, y } = this.pos; | |
| CTX.fillStyle = this.color; | |
| CTX.shadowBlur = 20; | |
| CTX.shadowColor = "rgba(255,255,255,.3 )"; | |
| CTX.fillRect(x, y, this.size, this.size); | |
| CTX.shadowBlur = 0; | |
| if (this.total >= 2) { | |
| for (let i = 0; i < this.history.length - 1; i++) { | |
| let { x, y } = this.history[i]; | |
| CTX.lineWidth = 1; | |
| CTX.fillStyle = "rgba(225,225,225,1)"; | |
| CTX.fillRect(x, y, this.size, this.size); | |
| } | |
| } | |
| } | |
| walls() { | |
| let { x, y } = this.pos; | |
| if (x + cellSize > W) { | |
| this.pos.x = 0; | |
| } | |
| if (y + cellSize > W) { | |
| this.pos.y = 0; | |
| } | |
| if (y < 0) { | |
| this.pos.y = H - cellSize; | |
| } | |
| if (x < 0) { | |
| this.pos.x = W - cellSize; | |
| } | |
| } | |
| controlls() { | |
| let dir = this.size; | |
| if (KEY.ArrowUp) { | |
| this.dir = new helpers.Vec(0, -dir); | |
| } | |
| if (KEY.ArrowDown) { | |
| this.dir = new helpers.Vec(0, dir); | |
| } | |
| if (KEY.ArrowLeft) { | |
| this.dir = new helpers.Vec(-dir, 0); | |
| } | |
| if (KEY.ArrowRight) { | |
| this.dir = new helpers.Vec(dir, 0); | |
| } | |
| } | |
| selfCollision() { | |
| for (let i = 0; i < this.history.length; i++) { | |
| let p = this.history[i]; | |
| if (helpers.isCollision(this.pos, p)) { | |
| isGameOver = true; | |
| } | |
| } | |
| } | |
| update() { | |
| this.walls(); | |
| this.draw(); | |
| this.controlls(); | |
| if (!this.delay--) { | |
| if (helpers.isCollision(this.pos, food.pos)) { | |
| incrementScore(); | |
| particleSplash(); | |
| food.spawn(); | |
| this.total++; | |
| } | |
| this.history[this.total - 1] = new helpers.Vec(this.pos.x, this.pos.y); | |
| for (let i = 0; i < this.total - 1; i++) { | |
| this.history[i] = this.history[i + 1]; | |
| } | |
| this.pos.add(this.dir); | |
| this.delay = 5; | |
| this.total > 3 ? this.selfCollision() : null; | |
| } | |
| } | |
| } | |
| class Food { | |
| constructor() { | |
| this.pos = new helpers.Vec( | |
| ~~(Math.random() * cells) * cellSize, | |
| ~~(Math.random() * cells) * cellSize | |
| ); | |
| this.color = currentHue = `hsl(${~~(Math.random() * 360)},100%,50%)`; | |
| this.size = cellSize; | |
| } | |
| draw() { | |
| let { x, y } = this.pos; | |
| CTX.globalCompositeOperation = "lighter"; | |
| CTX.shadowBlur = 20; | |
| CTX.shadowColor = this.color; | |
| CTX.fillStyle = this.color; | |
| CTX.fillRect(x, y, this.size, this.size); | |
| CTX.globalCompositeOperation = "source-over"; | |
| CTX.shadowBlur = 0; | |
| } | |
| spawn() { | |
| let randX = ~~(Math.random() * cells) * this.size; | |
| let randY = ~~(Math.random() * cells) * this.size; | |
| for (let path of snake.history) { | |
| if (helpers.isCollision(new helpers.Vec(randX, randY), path)) { | |
| return this.spawn(); | |
| } | |
| } | |
| this.color = currentHue = `hsl(${helpers.randHue()}, 100%, 50%)`; | |
| this.pos = new helpers.Vec(randX, randY); | |
| } | |
| } | |
| class Particle { | |
| constructor(pos, color, size, vel) { | |
| this.pos = pos; | |
| this.color = color; | |
| this.size = Math.abs(size / 2); | |
| this.ttl = 0; | |
| this.gravity = -0.2; | |
| this.vel = vel; | |
| } | |
| draw() { | |
| let { x, y } = this.pos; | |
| let hsl = this.color | |
| .split("") | |
| .filter((l) => l.match(/[^hsl()$% ]/g)) | |
| .join("") | |
| .split(",") | |
| .map((n) => +n); | |
| let [r, g, b] = helpers.hsl2rgb(hsl[0], hsl[1] / 100, hsl[2] / 100); | |
| CTX.shadowColor = `rgb(${r},${g},${b},${1})`; | |
| CTX.shadowBlur = 0; | |
| CTX.globalCompositeOperation = "lighter"; | |
| CTX.fillStyle = `rgb(${r},${g},${b},${1})`; | |
| CTX.fillRect(x, y, this.size, this.size); | |
| CTX.globalCompositeOperation = "source-over"; | |
| } | |
| update() { | |
| this.draw(); | |
| this.size -= 0.3; | |
| this.ttl += 1; | |
| this.pos.add(this.vel); | |
| this.vel.y -= this.gravity; | |
| } | |
| } | |
| function incrementScore() { | |
| score++; | |
| dom_score.innerText = score.toString().padStart(2, "0"); | |
| } | |
| function particleSplash() { | |
| for (let i = 0; i < splashingParticleCount; i++) { | |
| let vel = new helpers.Vec(Math.random() * 6 - 3, Math.random() * 6 - 3); | |
| let position = new helpers.Vec(food.pos.x, food.pos.y); | |
| particles.push(new Particle(position, currentHue, food.size, vel)); | |
| } | |
| } | |
| function clear() { | |
| CTX.clearRect(0, 0, W, H); | |
| } | |
| function initialize() { | |
| CTX.imageSmoothingEnabled = false; | |
| KEY.listen(); | |
| cellsCount = cells * cells; | |
| cellSize = W / cells; | |
| snake = new Snake(); | |
| food = new Food(); | |
| dom_replay.addEventListener("click", reset, false); | |
| loop(); | |
| } | |
| function loop() { | |
| clear(); | |
| if (!isGameOver) { | |
| requestID = setTimeout(loop, 1000 / 60); | |
| helpers.drawGrid(); | |
| snake.update(); | |
| food.draw(); | |
| for (let p of particles) { | |
| p.update(); | |
| } | |
| helpers.garbageCollector(); | |
| } else { | |
| clear(); | |
| gameOver(); | |
| } | |
| } | |
| function gameOver() { | |
| maxScore ? null : (maxScore = score); | |
| score > maxScore ? (maxScore = score) : null; | |
| window.localStorage.setItem("maxScore", maxScore); | |
| CTX.fillStyle = "#4cffd7"; | |
| CTX.textAlign = "center"; | |
| CTX.font = "bold 30px Poppins, sans-serif"; | |
| CTX.fillText("GAME OVER", W / 2, H / 2); | |
| CTX.font = "15px Poppins, sans-serif"; | |
| CTX.fillText(`SCORE ${score}`, W / 2, H / 2 + 60); | |
| CTX.fillText(`MAXSCORE ${maxScore}`, W / 2, H / 2 + 80); | |
| } | |
| function reset() { | |
| dom_score.innerText = "00"; | |
| score = "00"; | |
| snake = new Snake(); | |
| food.spawn(); | |
| KEY.resetState(); | |
| isGameOver = false; | |
| clearTimeout(requestID); | |
| loop(); | |
| } | |
| initialize(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| @font-face { | |
| font-family: "game"; | |
| src: url("https://fonts.googleapis.com/css2?family=Poppins:wght@500;800&display=swap"); | |
| } | |
| * { | |
| padding: 0; | |
| margin: 0; | |
| box-sizing: border-box; | |
| } | |
| button:focus { | |
| outline: 0; | |
| } | |
| html, | |
| body { | |
| height: 100%; | |
| font-family: "Poppins", sans-serif; | |
| color: #6e7888; | |
| } | |
| body { | |
| background-color: #1d1d2cff; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| color: #6e7888; | |
| } | |
| canvas { | |
| background-color: #181825; | |
| } | |
| .container { | |
| display: flex; | |
| width: 100%; | |
| height: 100%; | |
| flex-flow: column wrap; | |
| justify-content: center; | |
| align-items: center; | |
| } | |
| #ui { | |
| display: flex; | |
| align-items: center; | |
| font-size: 10px; | |
| flex-flow: column; | |
| margin-left: 10px; | |
| } | |
| h2 { | |
| font-weight: 900; | |
| font-size: 10px; | |
| letter-spacing: 8px; | |
| margin-bottom: 30px; | |
| transform: rotate(270deg); | |
| } | |
| #score { | |
| margin-top: 20px; | |
| font-size: 30px; | |
| font-weight: 800; | |
| transform: rotate(-90deg); | |
| } | |
| .noselect { | |
| user-select: none; | |
| } | |
| #replay { | |
| font-size: 10px; | |
| padding: 10px 20px; | |
| background: #6e7888; | |
| border: none; | |
| color: #222738; | |
| border-radius: 40px; | |
| font-weight: 800; | |
| transform: rotate(270deg); | |
| cursor: pointer; | |
| transition: all 200ms cubic-bezier(0.4, 0, 0.2, 1); | |
| } | |
| #replay:hover { | |
| background: #a6aab5; | |
| background: #4cffd7; | |
| transition: all 200ms cubic-bezier(0.4, 0, 0.2, 1); | |
| } | |
| #replay svg { | |
| margin-right: 8px; | |
| } | |
| @media (max-width: 600px) { | |
| #replay { | |
| margin-bottom: 20px; | |
| } | |
| #replay, | |
| h2 { | |
| transform: rotate(0deg); | |
| } | |
| #ui { | |
| flex-flow: row wrap; | |
| margin-bottom: 20px; | |
| } | |
| #score { | |
| margin-top: 0; | |
| margin-left: 20px; | |
| } | |
| .container { | |
| flex-flow: column wrap; | |
| } | |
| } | |
| #author { | |
| width: 100%; | |
| bottom: 40px; | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-weight: 600; | |
| color: inherit; | |
| text-transform: uppercase; | |
| padding-left: 35px; | |
| } | |
| #author span { | |
| font-size: 10px; | |
| margin-left: 20px; | |
| color: inherit; | |
| letter-spacing: 4px; | |
| } | |
| #author h1 { | |
| font-size: 25px; | |
| } | |
| .wrapper { | |
| display: flex; | |
| flex-flow: row wrap; | |
| justify-content: center; | |
| align-items: center; | |
| margin-bottom: 20px; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment