4
\$\begingroup\$

const GAME_STATE = (function() {
  let canvas = document.getElementById('gameboard');
  let scoreBoard = document.querySelector('.scoreboard');
  let highscoreLabel = document.querySelector('.highscore');

  canvas.width = 800;
  canvas.height = 600;

  let ctx = canvas.getContext('2d'),
      gameBoardHeight = canvas.height,
      gameBoardWidth = canvas.width,
      player,
      snake,
      snakeHead,
      apple;

  function init() {
    definePlayerProperties();
    defineSnakeProperties();
    defineAppleProperties();
    update();
    displayGameboard();
    bindEvents();
  }

  function dispose() {
    unbindEvents();
    canvas.style.display = 'none';
    GAME.init();
  }

  function displayGameboard() {
    canvas.style.display = 'block';
  }

  function definePlayerProperties() {
    player = {
      points: 0,
      name: '',
      isSaved: false
    };
  }

  function defineSnakeProperties() {
   snake = {
      body: [{
        x: 60,
        y: 60,
        color:'red',
        lastPosX: null,
        lastPosY: null
      }],
      direction: 'right',
      blockSize: 20
    }
    snakeHead = snake.body[0];

    for( let i = 0; i < 100; i++ ) {
      snake.body.push({ color:'blue' });
    }
  }

  function defineAppleProperties() {
    apple = {
      x: Math.floor( Math.random() * gameBoardWidth ),
      y: Math.floor( Math.random() * gameBoardHeight ),
      color: 'red',
      isReachable: false,
      isOutsideSnakeBody: function() {
        snake.body.forEach( (bodySegment) => {
          if( ( bodySegment.x === this.x ) && ( bodySegment.y === this.y ) ) {
            return false;
          }
        });
        return true;
      },
      setValidPosition: function() {
        while( this.isReachable === false ){
          if( (( this.x % snake.blockSize === 0 ) && ( this.y % snake.blockSize === 0 )) && this.isOutsideSnakeBody() ) {
            this.isReachable = true;
          }
          else {
            this.x =  Math.floor( Math.random() * gameBoardWidth );
            this.y = Math.floor( Math.random() * gameBoardHeight );
          }
        }
      }
    };
    apple.setValidPosition();
  }

  function handleInput( event ) {
    if(snake.direction === 'right' && event.keyCode === 37) {
      return;
    }
    else if(snake.direction === 'left' && event.keyCode === 39) {
      return;
    }
    else if(snake.direction === 'up' && event.keyCode === 40) {
      return;
    }
    else if(snake.direction === 'down' && event.keyCode === 38) {
      return;
    }

    switch ( event.keyCode ) {
      case 38:  /* Up arrow was pressed */
        snake.direction = 'up';
      break;
      case 40:  /* Down arrow was pressed */
        snake.direction = 'down';
      break;
      case 37:  /* Left arrow was pressed */
        snake.direction = 'left';
      break;
      case 39:  /* Right arrow was pressed */
        snake.direction = 'right';
      break;
      }
  }

  function update() {
    calculateSnakePosition();
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    snake.body.forEach( (s) => {
      draw(s);
    });
    draw(apple);
    drawGrid();
    displayScore();
    move();
    checkForCollisions();
  }

  function draw( o ) {
    if( o.x != undefined || o.x != null ) {
      ctx.beginPath();
      ctx.fillStyle = o.color;
      ctx.fillRect( o.x, o.y, snake.blockSize, snake.blockSize );
      ctx.closePath();
    }
  }

  function drawGrid() {
  for( let y = snake.blockSize; y < 600; y += snake.blockSize ) {
      ctx.beginPath();
      ctx.moveTo( 0, y );
      ctx.lineTo( 800, y );
      ctx.stroke();
    }
    for( let x = snake.blockSize; x < 800; x += snake.blockSize ) {
      ctx.beginPath();
      ctx.moveTo( x, 0 );
      ctx.lineTo( x, 600 );
      ctx.stroke();
    }
  }

  function move() {
    switch ( snake.direction ) {
      case 'left':
        snakeHead.x -= snake.blockSize;
        break;
      case 'right':
        snakeHead.x += snake.blockSize;
        break;
      case 'down':
        snakeHead.y += snake.blockSize;
        break;
      case 'up':
        snakeHead.y -= snake.blockSize;
        break;
    }
  }


  function displayScore() {
    scoreBoard.innerHTML = 'Points: ' + player.points;

    if( localStorage.getItem('highscores') ) {
      let storageHighscoresItemsArr = [ ];
      let storageHighscoresItems = JSON.parse(localStorage.getItem('highscores'));
      storageHighscoresItems.forEach( ( item ) => {
        storageHighscoresItemsArr.push( item.score );
      })

      let highscore = Math.max( ...storageHighscoresItemsArr );
      highscoreLabel.innerHTML = 'Highscore: ' + highscore;
    }
  }

  function calculateSnakePosition() {
    for(let i = 1; i < snake.body.length; i++) {
      snake.body[i].x = snake.body[i - 1].lastPosX;
      snake.body[i].y = snake.body[i - 1].lastPosY;

      snake.body[i - 1].lastPosX = snake.body[i - 1].x;
      snake.body[i - 1].lastPosY = snake.body[i - 1].y;
    }
  }

  function checkForCollisions() {
    if( snakeHead.x >= gameBoardWidth ) {
      snakeHead.x = 0;
    }
    else if( snakeHead.x <= -10 ) {
      snakeHead.x = gameBoardWidth;
    }
    else if( snakeHead.y <= -10 ) {
      snakeHead.y = gameBoardHeight;
    }
    else if( snakeHead.y >= gameBoardHeight ) {
      snakeHead.y = 0;
    }

    if(( snakeHead.x === apple.x ) && ( snakeHead.y === apple.y )) {
        player.points += 10;
        defineAppleProperties();
        snake.body.push( {color:'blue' });
    }

    for(let i = 1; i < snake.body.length; i++) {
    if((snakeHead.x === snake.body[i].x) && (snakeHead.y === snake.body[i].y)) {
      GAME.setState('GAMEOVER');
      dispose();
    }
  }
  }

  function bindEvents() {
    addEventListener( 'keydown', e => handleInput(e), true );
  }

  function unbindEvents() {
    removeEventListener( 'keydown', handleInput, true );
  }

  function getPlayer() {
    return player;
  }

  return {
    init: init,
    update: update,
    getPlayer: getPlayer
  };
})();

const GAMEOVER_STATE = (function() {
  let restartBtn = document.getElementById('restart'),
    gameOverScreen = document.querySelector('.gameover-screen'),
    finalScoreLabel = document.querySelector('.final-score'),
    playerNameInput = document.querySelector('.player-name'),
    highscoreBoardBtn = document.getElementById('highscores-btn');

  function init() {
    bindEvents();
    gameOverScreen.style.display = 'block';
    finalScoreLabel.innerHTML = 'Your score: ' + GAME_STATE.getPlayer().points;
  }

  function dispose() {
    gameOverScreen.style.display = 'none';
    savePlayerToStorage()
    unbindEvents();
    GAME.init();
  }

  let savePlayerToStorage = function() {
    if(GAME_STATE.getPlayer().isSaved === false) {
      GAME_STATE.getPlayer().isSaved = true;
      GAME_STATE.getPlayer().name = playerNameInput.value;
      let highscores = [];
      if(localStorage.getItem('highscores')) {
        highscores = highscores.concat(JSON.parse(localStorage.getItem('highscores')));
      }
      highscores.push({name: GAME_STATE.getPlayer().name, score: GAME_STATE.getPlayer().points});
      localStorage.setItem('highscores', JSON.stringify(highscores));
    }
  };

  function bindEvents() {
    restartBtn.addEventListener( 'click', restartGame, false );
    highscoreBoardBtn.addEventListener('click', showHighscores, false );
  }

  function unbindEvents() {
    restartBtn.removeEventListener( 'click', restartGame, false );
    highscoreBoardBtn.removeEventListener('click', showHighscores, false );
  }

  function restartGame() {
    GAME.setState('GAME');
    dispose();
  }

  function showHighscores() {
    GAME.setState('HIGHSCORES');
    dispose();
  }

  return {
    init: init
  }
})()

let HIGHSCORES_STATE = (function() {
  let highscoresBoard = document.querySelector('.highscores-board'),
    highscoresList = document.querySelector('.highscores-list'),
    highscoresReturnBtn = document.getElementById('return-from-highscores');

  function init() {
    highscoresBoard.style.display = 'flex';
    highscoresList.innerHTML = '';
    bindEvents();
    showHighscoresList();
  }

  function dispose() {
    highscoresBoard.style.display = 'none';
    unbindEvents();
    GAME.init();
  }

  function showHighscoresList() {
    let storageHighscoresItems = JSON.parse(localStorage.getItem('highscores'));
    let storageHighscoresItemsArr = [];
    storageHighscoresItems.forEach(( item ) => {
      storageHighscoresItemsArr.push( item );
    })

    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;
        }
      }
  }

  function bindEvents() {
    highscoresReturnBtn.addEventListener( 'click', goBack, false );
  }

  function unbindEvents() {
    highscoresReturnBtn.removeEventListener( 'click', goBack, false );
  }

  function goBack() {
    GAME.setState('GAMEOVER');
    dispose();
  }

  return {
    init: init
  }

})();

let GAME = (function() {
  let state = 'GAME';

  function init() {
    if( state === 'GAME' ) {
      GAME_STATE.init();
    }

    else if( state === 'GAMEOVER') {
      GAMEOVER_STATE.init();
    }
    else if( state === 'HIGHSCORES' ) {
      HIGHSCORES_STATE.init();
    }
  }

  function setState( st ) {
    state = st;
  }

  function getState() {
    return state;
  }

  let loop = function() {
    if( state === 'GAME' ) {
      GAME_STATE.update();
    }
     setTimeout( loop, 50 );
   }

  return {
    getState: getState,
    setState: setState,
    loop: loop,
    init: init
  };

})();

GAME.init();
GAME.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: #FFFACD;

    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: #1A1406;

  display: none;
}

.highscores-board ol  {
  margin-top: 35px;
}

.highscores-board ol li {
  font-size: 36px;
  margin-bottom: 10px;
}

.btn {
  display: inline-block;
 margin: 0 auto;
 text-align: center;
 width: 100%;
 height: 100%;
 border: 0;
 -moz-border-radius:0px;
 -webkit-border-radius:0px;
 border-radius:2px;
 display:inline-block;
 cursor:pointer;
 color:#ffffff;
 font-size:17px;
 text-decoration:none;
}

.reset-btn {
   background-color:#53b56d;
}

.highscores-btn {
  background-color:#2A6BD4;
}

.return-btn {
  width: 400px;
  height: 100px;
  background-color: #D9C750;
}
<!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 class="btn reset-btn" id="restart">Restart</button></li>
        <li><button class="btn highscores-btn" id="highscores-btn">Highscores</button></li>
      </ul>
    </div>
    <div class="highscores-board">
      <ol class="highscores-list"></ol>
      <button class="btn return-btn" id="return-from-highscores">Back</button>
    </div>
    <canvas id="gameboard" class="board"></canvas>
  </div>
  <script src="js/game.js"></script>
</body>
</html>

Previous question

I've decided to take your advice and improve the style of my code.I would like to know what do you think of it now,and maybe what's more needs to be changed.Though I'm not sure if I refactored it correctly because now my JS script has 100 more lines of code.

I've divided my code into 4 modules:

1.GAME_STATE - responible for everything while player is not dead.Updates data,draws on the canvas

2.GAMEOVER_STATE - updates localStorage and handles user input on the Game Over screen

3.HIGHSCORES_STATE - updates the Highscores List

4.GAME - initialazes and updates a modules,depending on it's 'state' varaible

\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

The function handleInput(e) on line 92 does not need 4 if statements to determine if you are going the opposite direction. You can concatonate that in your switch and make a comment about it:

    // handle snake direction and make sure you can't go to the opposite direction
    switch ( event.keyCode ) {
        case 38 && snake.direction != 'down':  /* Up arrow was pressed */
            snake.direction = 'up';
            break;
        case 40 && snake.direction != 'up':  /* Down arrow was pressed */
            snake.direction = 'down';
            break;
        case 37 && snake.direction != 'right':  /* Left arrow was pressed */
            snake.direction = 'left';
            break;
        case 39 && snake.direction != 'left':  /* Right arrow was pressed */
            snake.direction = 'right';
            break;
    }

You have an update function on line 123, with a forEach method. That method takes a function as parameter, and provides one argument. You have this on line 126:

    snake.body.forEach( (s) => {
        draw(s);
    });

You pass that variable in a function and do nothing else then calling another function with the same parameter. You could replace that like this:

    snake.body.forEach( draw );

This does not work for line 327 (functor push will throw error)

Example:

let two = ['test', 'me'];

// first argument = array element
// second argument = element index
// third argument = array
two.forEach(console.log);


I like what you do with defineAppleProperties, using the while loop to determine the apple location, but it could cause issues;

If the player has a perfect game, the snake will fill the entire map if he ate the last apple. If that happens, you have an endless while loop, causing a tab/browser crash. I would check the snake length vs amount of positions on the game board to fix that.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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