Coding Snake Game
Dive into the world of game development with our comprehensive tutorial on coding a classic Snake Game from the ground up. In this tutorial, we provide a detailed walkthrough of each coding step, ensuring you grasp the concepts while crafting your very own Snake Game. Join us in this coding adventure and level up your programming skills today!
Html Code:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Snake Game</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<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>Infinity Snake Game</h1>
</div>
</body>
<script type="module" src="script.js"></script>
</html>
Css Code:
@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: #222738;
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: 200;
transform: rotate(270deg);
}
#score {
margin-top: 20px;
font-size: 30px;
font-weight: 800;
}
.noselect {
user-select: none;
}
#replay {
font-size: 10px;
padding: 10px 20px;
background: #6e7888;
border: none;
color: #222738;
border-radius: 20px;
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;
padding-left: 35px;
}
#author h1 {
font-size: 25px;
}
.wrapper {
display: flex;
flex-flow: row wrap;
justify-content: center;
align-items: center;
margin-bottom: 20px;
}
JavaScript Code:
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 = 0,
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 = requestAnimationFrame(loop);
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;
cancelAnimationFrame(requestID);
loop();
}
initialize();