This is the first version of my first HTML5 game. Beyond the standard features, I've added a high scores board which uses local storage to keep track of the scores.
You can move the snake with the arrow keys. Unlike the normal game, running into the wall doesn't kill the snake, only when it runs into itself. This will display the "Game Over" screen, at which point you can either enter your name or be anonymous as 'Noname'. You can view the high scores at any time by pressing the 'Highscores' button. From there you are also able to go back to the main menu and start a new game.
I'd like feedback to focus on the JavaScript, especially with regards to cleanliness and readability, as well as overall best practices.
As a side note, I'm not sure if the stack snippets will run properly because of the use of local storage, but it works fine if run normally.
"use strict";
(function (){
let canvas = document.getElementById('gameboard');
let scoreBoard = document.querySelector('.scoreboard');
let highscoreLabel = document.querySelector('.highscore');
let restartBtn = document.getElementById('restart');
let gameOverScreen = document.querySelector('.gameover-screen');
let finalScoreLabel = document.querySelector('.final-score');
let playerNameInput = document.querySelector('.player-name');
let highscoreBoardBtn = document.getElementById('highscores-btn');
let highscoresBoard = document.querySelector('.highscores-board');
let highscoresList = document.querySelector('.highscores-list');
let highscoresReturnBtn = document.getElementById('return-from-highscores');
canvas.width = 800;
canvas.height = 600;
let ctx = canvas.getContext('2d');
let isPlayerSaved,
state,
points,
playerName,
snakeBody,
direction,
blockSize,
snakeHead,
apple;
let init = function() {
canvas.style.display = "block";
gameOverScreen.style.display = "none";
isPlayerSaved = false;//this flag helps eliminate duplicates from highscore board;if it's value's true,same player can't be saved again
state = 'game';
points = 0;
playerName = '';
snakeBody = [{
x: 60,
y: 60,
color:'red',
lastPosX: null,
lastPosY: null
}];
for(let i = 0; i < 5; i++) {
snakeBody.push({color:'blue'});
}
direction = 'right';
blockSize = 20;
snakeHead = snakeBody[0];
apple = {
x: Math.floor(Math.random() * canvas.width),
y: Math.floor(Math.random() * canvas.height),
color: 'red',
};
}
init();
let draw = function(o) {
if(o.x != undefined || o.x != null) {
ctx.beginPath();
ctx.fillStyle = o.color;
ctx.fillRect(o.x, o.y, blockSize, blockSize);
}
}
let inputHandler = function(e) {
if(direction === 'right' && e.keyCode === 37) {
return;
}
else if(direction === 'left' && e.keyCode === 39) {
return;
}
else if(direction === 'up' && e.keyCode === 40) {
return;
}
else if(direction === 'down' && e.keyCode === 38) {
return;
}
switch (e.keyCode) {
case 38: /* Up arrow was pressed */
direction = 'up';
break;
case 40: /* Down arrow was pressed */
direction = 'down';
break;
case 37: /* Left arrow was pressed */
direction = 'left';
break;
case 39: /* Right arrow was pressed */
direction = 'right';
break;
}
}
let move = function() {
switch (direction) {
case 'left':
snakeHead.x -= blockSize;
break;
case 'right':
snakeHead.x += blockSize;
break;
case 'down':
snakeHead.y += blockSize;
break;
case 'up':
snakeHead.y -= blockSize;
break;
}
}
let update = function() {
scoreBoard.innerHTML = "Points: " + points;
//Calculate the highscore and display it
if(localStorage.getItem('highscores')) {
let storageHighscoresItemsArr = [];
let storageHighscoresItems = JSON.parse(localStorage.getItem('highscores'));
for(let i = 0; i < storageHighscoresItems.length; i++) {
storageHighscoresItemsArr.push(storageHighscoresItems[i].score);
}
let highscore = Math.max(...storageHighscoresItemsArr);
highscoreLabel.innerHTML = "Highscore: " + highscore;
}
//Snake's body movement
for(let i = 1; i < snakeBody.length; i++) {
snakeBody[i].x = snakeBody[i - 1].lastPosX;
snakeBody[i].y = snakeBody[i - 1].lastPosY;
snakeBody[i - 1].lastPosX = snakeBody[i - 1].x;
snakeBody[i - 1].lastPosY = snakeBody[i - 1].y;
}
}
let generateApple = function() {
apple = {
x: Math.floor(Math.random() * canvas.width),
y: Math.floor(Math.random() * canvas.height),
color: 'red',
};
let isAppleReachable = false;
let isAppleOutsideBody = function() {
for(let i = 0; i < snakeBody.length; i++ ) {
if((snakeBody[i].x === apple.x) && (snakeBody[i].y === apple.y)) {
return false;
}
}
return true;
}
while(isAppleReachable === false){
if(((apple.x % blockSize === 0) && (apple.y % blockSize === 0)) && isAppleOutsideBody()) {
isAppleReachable = true;
}
else {
apple.x = Math.floor(Math.random() * canvas.width);
apple.y = Math.floor(Math.random() * canvas.height);
}
}
}
let checkForColliions = function() {
if(snakeHead.x >= canvas.width) {
snakeHead.x = 0;
}
else if(snakeHead.x <= -10) {
snakeHead.x = canvas.width;
}
else if(snakeHead.y <= -10) {
snakeHead.y = canvas.height;
}
else if(snakeHead.y >= canvas.height) {
snakeHead.y = 0;
}
if((snakeHead.x === apple.x) && (snakeHead.y === apple.y)) {
points += 10;
generateApple();
snakeBody.push({color:'blue'});
}
for(let i = 1; i < snakeBody.length; i++) {
if((snakeHead.x === snakeBody[i].x) && (snakeHead.y === snakeBody[i].y)) {
state = 'gameOver';
}
}
}
let savePlayerToLocalStorage = function() {
if(isPlayerSaved === false) {
isPlayerSaved = true;
playerName = playerNameInput.value;
let highscores = [];
if(localStorage.getItem('highscores')) {
highscores = highscores.concat(JSON.parse(localStorage.getItem('highscores')));
}
highscores.push({name: playerName, score: points});
localStorage.setItem('highscores', JSON.stringify(highscores));
}
}
let loop = function() {
if(state === 'game') {
update();
ctx.clearRect(0, 0, canvas.width, canvas.height);
snakeBody.forEach( (s) => {
draw(s);
})
draw(apple);
for(let y = blockSize; y < 600; y += blockSize) {
ctx.beginPath();
ctx.moveTo(0,y);
ctx.lineTo(800, y);
ctx.stroke();
}
for(let x = blockSize; x < 800; x += blockSize) {
ctx.beginPath();
ctx.moveTo(x,0);
ctx.lineTo(x, 600);
ctx.stroke();
}
move();
checkForColliions();
setTimeout(loop,50);
}
else if(state === 'gameOver') {
canvas.style.display = "none";
gameOverScreen.style.display = "block";
highscoresBoard.style.display = "none";
finalScoreLabel.innerHTML = "Your score: " + points;
}
else if(state === 'highscores') {
gameOverScreen.style.display = 'none';
highscoresBoard.style.display = 'flex';
highscoresList.innerHTML = '';
let storageHighscoresItems = JSON.parse(localStorage.getItem('highscores'));
let storageHighscoresItemsArr = [];
for(let i = 0; i < storageHighscoresItems.length; i++) {
storageHighscoresItemsArr.push(storageHighscoresItems[i]);
}
storageHighscoresItemsArr.sort(function (a, b) {
if (a.score > b.score) {
return 1;
}
if (a.score < b.score) {
return -1;
}
return 0;
});
storageHighscoresItemsArr.reverse();
for(let i = 0; i < 10; i++) {
if(storageHighscoresItemsArr[i]) {
let li = document.createElement('li');
highscoresList.appendChild(li);
li.innerHTML = (storageHighscoresItemsArr[i].name || 'Noname') + " - " + storageHighscoresItemsArr[i].score;
}
}
}
}
loop();
generateApple()
addEventListener('keydown', e => inputHandler(e), true);
restartBtn.addEventListener('click', () => {
savePlayerToLocalStorage();
init();
generateApple();
loop();
})
highscoreBoardBtn.addEventListener('click', () => {
savePlayerToLocalStorage();
state = 'highscores';
loop();
})
highscoresReturnBtn.addEventListener('click', () => {
state = 'gameOver';
loop();
})
} ())
html,
body {
margin: 0;
padding: 0;
overflow: hidden;
height: 100%;
}
.game-wrap {
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.board {
background: white;
border: 2px black solid;
}
.label {
width: 800px;
display: flex;
flex-direction: row;
justify-content: space-between;
}
.gameover-screen {
height: 600px;
width: 800px;
background: red;
/*display: flex;
flex-direction: column;
justify-content: center;
align-items: center;*/
display: none;
}
.gameover-screen ul {
list-style: none;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.gameover-screen ul li {
margin-bottom: 50px;
}
.final-score {
font-size: 32px;
}
.highscores-board {
display: flex;
flex-direction: column;
align-items: center;
height: 600px;
width: 800px;
color: white;
background: black;
display: none;
}
.highscores-board ol {
margin-top: 35px;
}
.highscores-board ol li {
font-size: 36px;
margin-bottom: 10px;
}
#return-from-highscores {
width: 200px;
height: 100px;
}
<!DOCTYPE html>
<html>
<head lang="en-Us">
<meta charset="utf-8">
<title>Snake</title>
<link rel="stylesheet" href="css/main.css">
</head>
<body>
</div>
<div class="game-wrap">
<div class="label">
<p class="scoreboard">Points: 0</p>
<p class="highscore">Highscore: 0</p>
</div>
<div class="gameover-screen">
<ul>
<li><p class="final-score">Your score: </p></li>
<li><label>Your name: </label>
<input class="player-name" type="text" maxlength="6"></li>
<li><button id="restart">Restart</button></li>
<li><button id="highscores-btn">Highscores</button></li>
</ul>
</div>
<div class="highscores-board">
<ol class="highscores-list"></ol>
<button id="return-from-highscores">Back</button>
</div>
<canvas id="gameboard" class="board"></canvas>
</div>
<script src="js/game.js"></script>
</body>
</html>