3
\$\begingroup\$

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>

\$\endgroup\$
  • \$\begingroup\$ hey - try to make it working in the code editor - comment out the use of local storage if you need to \$\endgroup\$ – Deian Jul 19 '16 at 19:42
  • \$\begingroup\$ It works fine in the browser,the problem occurs only here in code snippet. \$\endgroup\$ – ch3m0 Jul 19 '16 at 19:45
  • \$\begingroup\$ I understand but .... look, you're looking for a feedback .. make it easy for us. \$\endgroup\$ – Deian Jul 19 '16 at 19:52
  • \$\begingroup\$ I would like to but I don't even know how to solve this...Do you have any suggestions on how to make this easier for you? \$\endgroup\$ – ch3m0 Jul 19 '16 at 19:55
2
\$\begingroup\$

JS Game Architecture

In general this is how would a JS game architecture look like. You certainly can get it working with another architecture but this one will give good performance and responsiveness.

You have to careful with calls from within the event handlers. You want to release the event handler ASAP - so call only quick core methods.

Everything else (like save to disks, network io) try to do it in the core thread/timer where the process should be triggered by some indicator in the shared data for example.

Comments on specific moments:

1) Global funcitons

let generateApple = function() { .. }

when you put that code in the global scope it is the same like

function generateApple() { .. }

Where the latter is a bit more readable in my opinion.

2) Global functions 2

You are using global functions most of the time - that allows you to call everything from everywhere and makes your program look like a bow of spaghetti. No offense I love it .. however in the long term makes your code hard to read and maintain.

I'd suggest you to keep your code separated in namespaces classes at least.

var Core={}; Core.prototype.generateApple = function() { .. } Core.prototype.checkForCollisions = function() { .. }

var UI = {}; UI.prototype.draw = function() { .. }

This will allow you to have separation of concerns and will help you understand and maintain your own code better.

3) Naming conventions

function loop() { .. } // Is expected to have a loop inside

Try to find name to represent what is the code inside doing. If the code is handling user input, renders the content to screen and updates the high score table .. call it like that.

handleUserInputRenderScreeContentAndUpdateHighScoreTable()

This should hint you to break code in purposeful functions separated by concern.

4) Event handlers

Mind you - these function are called on mouse or button click. if you want responsive UI and happy users keep them short and fast. If it would take time the user would expect at a "please wait" indicator.

I would love to see it again after you apply these principals.

\$\endgroup\$
  • \$\begingroup\$ I removed those calls to loop from the event handlers and I moved setTimeout(50,loop) at the end of the loop function and it worked.I tried to implement 2 timers as you said,but i couldn't figure out how to do that.Could you give me some tips? \$\endgroup\$ – ch3m0 Jul 19 '16 at 21:11
  • \$\begingroup\$ In general we don't do multi-phase reviews like this on the site on a single question - you review the question as written to the best of your ability and interest, and if the OP needs or desires further review after that they would then post an additional question. Furthermore, it is explicitly disallowed for the OP to edit the code in the question after answers have been given so as not to invalidate them. \$\endgroup\$ – Dannnno Jul 20 '16 at 2:18
  • \$\begingroup\$ @Dannnno My bad - I did know that. Okay I will put my input and we'll see how it goes. \$\endgroup\$ – Deian Jul 20 '16 at 17:00

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.