/*Downloaded from https://www.codeseek.co/iy2/snake-KoOpjx */
const canvasDOMEl = document.getElementById('canvas');
canvasDOMEl.width = 500;
canvasDOMEl.height = 500;
// Default canvas parameters
const canvasParams = {
canvasDOMEl,
shadeOfGray: 150,
gridSize: 25,
gridLineWidth: 1
};
const scoreDOMEl = document.getElementById('score');
const scoreNameDOMEl = document.getElementById('score-name');
const gameEndDOMEl = document.getElementById('game-end-msg');
const playerNameModal = document.querySelector('.player-name-modal');
const playerNameForm = document.forms.nameform;
const playerNameErrorDOMEl = playerNameForm.querySelector('.error-message');
const leaderboardTable = document.querySelector('.leaderboard-table');
const leaderboardTableBody = leaderboardTable.tBodies[0];
/*
* App flow is the following:
* player is prompted to enter his / her username
* if the name is correct, we delete the user prompt from the DOM,
* create a SPACE keydown listener to create a new Game
*/
playerNameForm.addEventListener('submit', function submitPlayerNameForm(event) {
event.preventDefault();
const name = event.target.elements.nameinput.value.trim();
if (!name) {
playerNameErrorDOMEl.innerText = 'Name not entered';
} else if (name.length > 10) {
playerNameErrorDOMEl.innerText = 'Under 10 chars please';
} else {
playerName = name;
playerNameModal.remove();
scoreNameDOMEl.innerHTML = `Hi, ${name}! Your `;
playerNameForm.removeEventListener('submit', submitPlayerNameForm);
document.addEventListener('keydown', event => {
if (event.keyCode === 32) {
event.preventDefault();
game = new Game(canvasParams, playerName, scoreDOMEl, gameEndDOMEl, renderLeaderboard);
game.startGame();
}
});
}
});
class Game {
constructor(canvasParams, playerName, scoreDOMEl, gameEndDOMEl, gameEndCallback) {
this.canvas = new Canvas(canvasParams);
this.snake = new Snake(canvasParams.gridSize, canvasParams.gridSize, canvasParams.gridSize);
this.food = new Food(...this.getRandomFoodPosition(), canvasParams.gridSize);
this.gameOn = true;
this.rafID = null;
this.score = 0;
this.scoreDOMEl = scoreDOMEl;
this.gameEndDOMEl = gameEndDOMEl;
this.playerName = playerName;
this.gameEndCallback = gameEndCallback;
this.prepareGame();
this.keyDirectionMap = new Map([
[38, {x: 0, y: -1}],
[87, {x: 0, y: -1}],
[40, {x: 0, y: 1}],
[83, {x: 0, y: 1}],
[37, {x: -1, y: 0}],
[65, {x: -1, y: 0}],
[39, {x: 1, y: 0}],
[68, {x: 1, y: 0}]
]);
}
prepareGame() {
this.canvas.drawGridBackground();
this.gameEndDOMEl.classList.remove('visible');
}
startGame() {
this.resetGameScore();
this.addKeyDownListener();
requestAnimationFrame(this.gameProcess.bind(this));
}
endGame() {
cancelAnimationFrame(this.rafID);
this.removeKeyDownListener();
this.gameEndDOMEl.classList.add('visible');
if (this.score > 0) GameStorage.setPlayerScore(this.playerName, this.score);
this.gameEndCallback();
}
gameProcess() {
this.canvas.drawGridBackground();
this.canvas.drawElements(this.snake, this.food);
if (this.gameOn) {
if (this.rafID % 5 === 0) {
// Change snake direction if scheduled
this.snake.changeDirection();
// If snake collided with self / canvas edge stop the game
if (this.snake.checkOffCanvas(this.canvas.cw, this.canvas.ch) || this.snake.checkSelfCollision()) {
this.gameOn = false;
} else {
// Update position if no collision detected
this.snake.updatePosition();
}
if (this.snake.isFoodEaten(this.food)) {
this.snake.addTailPiece();
this.snake.paintTail(this.food.color);
this.resetFood();
this.updateGameScore();
}
}
this.rafID = requestAnimationFrame(this.gameProcess.bind(this));
} else {
this.endGame();
}
}
getRandomFoodPosition() {
let maxGridWidth = this.canvas.cw / this.canvas.gridSize;
let maxGridHeight = this.canvas.ch / this.canvas.gridSize;
let xPosition, yPosition;
let forbiddenPosition = this.snake.getSnakePosition();
do {
xPosition = (Math.random() * maxGridWidth|0) * this.canvas.gridSize;
yPosition = (Math.random() * maxGridHeight|0) * this.canvas.gridSize;
} while (forbiddenPosition.filter(snakePart => snakePart.x === xPosition && snakePart.y === yPosition).length > 0);
return [xPosition, yPosition];
}
resetFood() {
this.food = new Food(...this.getRandomFoodPosition(), this.canvas.gridSize);
}
resetGameScore() {
this.score = 0;
this.scoreDOMEl.innerText = 0;
}
updateGameScore () {
this.score++;
this.scoreDOMEl.innerText = this.score;
}
handleEvent(event) {
if (event.type === 'keydown') {
const keyCode = event.keyCode;
if (this.keyDirectionMap.has(keyCode)) {
event.preventDefault();
this.snake.scheduleMovementDirectionChange(this.keyDirectionMap.get(keyCode));
}
}
}
addKeyDownListener() {
document.addEventListener('keydown', this);
}
removeKeyDownListener() {
document.removeEventListener('keydown', this);
}
}
class Canvas {
constructor({gridSize, gridLineWidth, shadeOfGray, canvasDOMEl}) {
this.gray = shadeOfGray;
this.gridSize = gridSize;
this.gridLineWidth = gridLineWidth;
this.canvas = canvasDOMEl;
this.cw = canvasDOMEl.width;
this.ch = canvasDOMEl.height;
this.ctx = canvasDOMEl.getContext('2d');
}
drawGridBackground() {
const ctx = this.ctx;
ctx.fillStyle = 'rgba(255, 255, 255)';
ctx.fillRect(0, 0, this.cw, this.ch);
ctx.lineWidth = this.gridLineWidth;
ctx.strokeStyle = `rgb(${this.gray}, ${this.gray}, ${this.gray})`;
for (let i = 0; i <= this.cw; i += this.gridSize) {
ctx.beginPath();
ctx.moveTo(i, 0);
ctx.lineTo(i, this.ch);
ctx.stroke();
}
for (let i = 0; i <= this.ch; i += this.gridSize) {
ctx.beginPath();
ctx.moveTo(0, i);
ctx.lineTo(this.cw, i);
ctx.stroke();
}
}
drawElements(snake, food) {
snake.draw(this.ctx, this.gridLineWidth, this.gridSize);
food.draw(this.ctx, this.gridLineWidth, this.gridSize);
}
}
class Snake {
constructor (x, y, size) {
this.size = size;
this.movementDirection = {x: 1, y: 0};
this.head = new SnakePart(x, y);
this.headColor = '#000000';
this.tail = [];
this.tailColor = '#000000';
this.directionChangeArray = [];
}
updatePosition() {
// Head
this.head.incrementPosition(this.movementDirection.x * this.size, this.movementDirection.y * this.size);
// Tail
if (this.tail.length > 0) {
for (let i = this.tail.length - 1; i >= 0; i--) {
if (i === 0) {
// Set first tail element to previous position of head
this.tail[i].setPosition(this.head.x - (this.movementDirection.x * this.size),
this.head.y - (this.movementDirection.y * this.size));
} else {
// Update other tail elements
this.tail[i].setPosition(this.tail[i - 1].x, this.tail[i - 1].y);
}
}
}
}
scheduleMovementDirectionChange(direction) {
let directionToCompare = this.movementDirection;
/*
* If we have direction changes pushed we check against the last scheduled
* direction rather than against current direction
*/
if (this.directionChangeArray.length > 0) {
directionToCompare = this.directionChangeArray[this.directionChangeArray.length - 1];
}
if (Math.abs(directionToCompare.x) !== Math.abs(direction.x) ||
Math.abs(directionToCompare.y) !== Math.abs(direction.y)) {
this.directionChangeArray.push(direction);
}
}
changeDirection() {
if (this.directionChangeArray.length > 0) {
this.movementDirection = this.directionChangeArray.shift();
}
}
// Canvas Edge Mode
checkOffCanvas(width, height) {
const dX = this.movementDirection.x * this.size;
const dY = this.movementDirection.y * this.size;
return this.head.x + dX >= width ||
this.head.x + dX < 0 ||
this.head.y + dY >= height ||
this.head.y + dY < 0;
}
checkSelfCollision() {
const dX = this.movementDirection.x * this.size;
const dY = this.movementDirection.y * this.size;
return this.tail.some(piece => piece.x === this.head.x + dX && piece.y === this.head.y + dY);
}
isFoodEaten(food) {
return this.head.x === food.x && this.head.y === food.y;
}
// Add a new piece directly on head's position. It will be changed during updatePosition()
addTailPiece() {
this.tail.unshift(new SnakePart(this.head.x, this.head.y));
}
paintTail(color) {
this.tailColor = color;
}
draw(canvasCtx, canvasGridLineWidth) {
this.head.drawSelf(canvasCtx, canvasGridLineWidth, this.size, this.headColor);
this.tail.forEach(piece => piece.drawSelf(canvasCtx, canvasGridLineWidth, this.size, this.tailColor));
}
getSnakePosition() {
return [this.head, ...this.tail];
}
}
class SnakePart {
constructor(x, y) {
this.x = x;
this.y = y;
}
setPosition(x, y) {
this.x = x;
this.y = y;
}
incrementPosition(dX, dY) {
this.x += dX;
this.y += dY;
}
drawSelf (canvasCtx, canvasGridLineWidth, size, color) {
canvasCtx.fillStyle = color;
canvasCtx.fillRect(
this.x + canvasGridLineWidth / 2,
this.y + canvasGridLineWidth / 2,
size - canvasGridLineWidth,
size - canvasGridLineWidth
);
}
}
/*
* Using a tiny library for generating nice dark colors
* https://github.com/davidmerfield/randomColor
*/
class Food {
constructor(x, y, size) {
this.x = x;
this.y = y;
this.size = size;
this.color = randomColor({luminosity: 'dark'});
}
draw(canvasCtx, canvasGridLineWidth) {
canvasCtx.fillStyle = this.color;
canvasCtx.fillRect(
this.x + canvasGridLineWidth / 2,
this.y + canvasGridLineWidth / 2,
this.size - canvasGridLineWidth,
this.size - canvasGridLineWidth
);
}
}
const KEY = 'snake_leaderboard';
class GameStorage {
/*
* Retrieves data from Local Storage
* If an entry exists, gets it,
* converts to an array and sorts it
* Else returns null
*/
static retrieveStorageData() {
let data = localStorage.getItem(KEY);
if (data) {
data = JSON.parse(data);
data.sort(sortEntriesByScore);
}
console.log(data);
return data;
}
// Store only best 15 entries in Local Storage
static setPlayerScore(player, score) {
let data = GameStorage.retrieveStorageData();
if (data) {
data.push({player, score});
data.sort(sortEntriesByScore);
} else {
data = [{player, score}];
}
localStorage.setItem(KEY, JSON.stringify(data));
}
}
function sortEntriesByScore(a, b) {
if (a.score > b.score) {
return -1;
} else if (a.score < b.score) {
return 1;
} else {
return 0;
}
}
let playerName = '';
// Initialize new Game to prerender canvas
let game = new Game(canvasParams, playerName, scoreDOMEl, gameEndDOMEl, renderLeaderboard);
renderLeaderboard();
/*
* Retrieve storage data, if there is any do the following:
* Clear leadeboard table
* Insert all leaderboard entries
*/
function renderLeaderboard() {
let storageData = GameStorage.retrieveStorageData();
if (storageData) {
// Clear table body
while (leaderboardTableBody.rows.length > 0) {
leaderboardTableBody.deleteRow(0);
}
/*
* Display only 15 latest entries
* Easily customizable because we store all entries
* in the game storage
*/
storageData.splice(15);
storageData.forEach((entry, index) => {
const entryRow = leaderboardTableBody.insertRow();
const scoreIndexCell = entryRow.insertCell(0);
const playerNameCell = entryRow.insertCell(1);
const playerScoreCell = entryRow.insertCell(2);
scoreIndexCell.innerHTML = index + 1;
playerNameCell.innerHTML = entry.player;
playerScoreCell.innerHTML = entry.score;
});
}
}