document.write (`<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>`);
document.write(`<style> @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: black;
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;
}
#replay,
h2 {
transform: rotate(0deg);
}
#ui {
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;
} </style>`);
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();