Snake

In this example below you will see how to do a Snake with some HTML / CSS and Javascript

Thumbnail
This awesome code was written by iy2, you can see more from this user in the personal repository.
You can find the original code on Codepen.io
Copyright iy2 ©

Technologies

  • HTML
  • CSS
  • JavaScript
<!DOCTYPE html>
<html lang="en" >

<head>
  <meta charset="UTF-8">
  <title>Snake</title>
  
    <link href="https://fonts.googleapis.com/css?family=Press+Start+2P" rel="stylesheet">
  
  
      <link rel="stylesheet" href="css/style.css">

  
</head>

<body>

  <div class="player-name-modal">
    <form class="player-name-form" name="nameform">
        <label for="player-name-text">Enter your name:</label>
        <input type="text" id="player-name-text" name="nameinput" placeholder="Destroyer333" required>
        <div class="error-message"></div>
        <button id="player-name-submit-btn">Let's go!</button>
    </form>
</div>

<div class="game-controls">
    <h1 class="game-header">SNAKE</h1>
    <div class="game-info">
        <div>Press SPACE to start</div>
        <div>Control with ▲ ▼ ◀ ▶ or WASD</div>
    </div>
    <div class="score-message"><span id="score-name"></span><span>score:</span><span id="score">0</span></div>
    <div id="game-end-msg">You lost! Start a new game</div>
</div>

<div class="canvas-wrap">
    <canvas id="canvas"></canvas>
</div>


<div class="leaderboard-wrap">
    <table class="leaderboard-table">
        <thead>
            <tr>
                <th>Rank</th>
                <th>Name</th>
                <th>Score</th>
            </tr>
        </thead>
        <tbody>
        <tr>
            <td>-</td>
            <td>-</td>
            <td>-</td>
        </tr></tbody>
    </table>
</div>
  <script src='https://cdnjs.cloudflare.com/ajax/libs/randomcolor/0.5.2/randomColor.min.js'></script>

  

    <script  src="js/index.js"></script>




</body>

</html>

/*Downloaded from https://www.codeseek.co/iy2/snake-KoOpjx */
html,
body {
  margin: 0;
  padding: 0;
  min-height: 100vh;
  min-width: 600px;
}
body {
  font-family: 'Press Start 2P', cursive;
  display: grid;
  grid-template-columns: 1fr minmax(500px, 1fr);
  grid-template-rows: 300px 1fr;
  grid-template-areas: "game-controls game-controls" "canvas leaderboard";
}
.player-name-modal {
  position: fixed;
  z-index: 1;
  top: 0;
  left: 0;
  width: 100vw;
  height: 100vh;
  background-color: rgba(0, 0, 0, 0.7);
  display: flex;
  justify-content: center;
  align-items: center;
}
.player-name-modal .player-name-form {
  background-color: white;
  border: 1px solid black;
  display: flex;
  flex-direction: column;
  padding: 30px;
}
.player-name-modal .player-name-form > * {
  margin-bottom: 10px;
}
.player-name-modal .player-name-form > *:last-child {
  margin-bottom: 0;
}
.player-name-modal .player-name-form .error-message {
  color: red;
  font-size: 0.8em;
}
.player-name-modal .player-name-form #player-name-text {
  border: 3px solid black;
  font: inherit;
  padding: 3px;
  transition: all .3s;
}
.player-name-modal .player-name-form #player-name-text:focus {
  outline: none;
  background-color: black;
  color: white;
}
.player-name-modal .player-name-form #player-name-submit-btn {
  outline: none;
  padding: 7px;
  border: 3px solid black;
  background-color: white;
  font: inherit;
  font-size: 0.8em;
  transition: all .3s;
}
.player-name-modal .player-name-form #player-name-submit-btn:hover {
  background-color: black;
  color: white;
}
.game-controls {
  grid-area: game-controls;
  display: flex;
  justify-content: center;
  align-items: center;
  flex-direction: column;
}
.game-controls div {
  margin: 7px;
}
.game-controls .game-info {
  padding: 7px;
  border: 3px solid black;
  text-align: center;
}
.game-controls #game-end-msg {
  visibility: hidden;
  opacity: 0;
  color: darkred;
}
.game-controls #game-end-msg.visible {
  visibility: visible;
  opacity: 1;
  transition: all .3s;
}
.canvas-wrap {
  grid-area: canvas;
  display: flex;
  justify-content: center;
  align-items: flex-start;
}
.canvas-wrap #canvas {
  border: 1px solid black;
}
.leaderboard-wrap {
  grid-area: leaderboard;
  display: flex;
  justify-content: center;
  align-items: flex-start;
}
.leaderboard-wrap .leaderboard-table {
  margin-top: 10px;
  border-collapse: collapse;
}
.leaderboard-wrap .leaderboard-table th,
.leaderboard-wrap .leaderboard-table td {
  padding: 5px;
  border: 2px solid black;
}
.leaderboard-wrap .leaderboard-table tbody tr:first-child {
  color: darkred;
}
@media (max-width: 1100px) {
  body {
    grid-template-columns: 1fr;
    grid-template-rows: 300px 600px 500px;
    grid-template-areas: "game-controls" "canvas" "leaderboard";
  }
}


/*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;
        });
    }
}

Comments