<header>SNAKE</header>
 
<!-- Main table for the snake -->
<section class="snakeTable"></section>

<!-- modular window before and after games -->
<section class="modul">
  <div class="start">
    <span>Play Snake</span>
  </div>
</section>

<footer>
  <p>Created by <a href="https://remybeumier.be" target="_blank">Rémy Beumier</a></p>
</footer>

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  align-items: center;
  min-height: 100vh;
  font-family: arial;
  background: #2ecc71;
  background-image: linear-gradient(to top left, #2980b9, #16a085, #2ecc71);
  position: relative;
  text-align: center;
}

/* title */
header {
  text-align: center;
  font-size: 32px;
  letter-spacing: 10px;
  padding: 20px 10px;
  color: white;
}

/* main table */
.snakeTable {
  width: 210px;
  height: 230px;
  margin: auto;
  display: flex;
  flex-direction: row;
  justify-content: flex-start;
  flex-wrap: wrap;
  box-shadow: 0 0 1px 2px rgba(0,0,0,0.3);
}
@media only screen and (min-width: 768px) {
  .snakeTable {
    width: 315px;
    height: 345px;
  }
}

/* all boxes in the table */
.box {
  background: white;
  border: solid 1px #16a085;
  width: 10px;
  height: 10px;
}
@media only screen and (min-width: 768px) {
  .box {
    width: 15px;
    height: 15px;
  }
}

.food {
  background: #8e44ad;
}

.snake {
  background: #16a085;
}

/* bar showing status below table */
.status {
  background: white;
  color: #16a085;
  border: solid 1px #16a085;
  width: 210px;
  height: 20px;
}
@media only screen and (min-width: 768px) {
  .status {
    width: 315px;
    height: 30px;
  }
}

.score {
  float: right;
  padding: 0 5px;
  line-height: 20px;
}
@media only screen and (min-width: 768px) {
  .score {
    padding: 0 10px;
    line-height: 30px;
    font-size: 20px;
  }
}

.hidden {
  display: none !important;
}

/* modular window before and after game */
.modul {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  position: fixed;
  top: 0;
  left: 0;
  height: 100%;
  width: 100%;
  background: rgba(0,0,0,0.5);
  text-align: center;
}
.start {
  background: rgba(256,256,256,1);
  padding: 60px;
  box-shadow: 0 0 2px 3px rgba(0,0,0,0.3);
  border-radius: 2px;
}
.start span {
  color: #16a085;
  border: 1px solid;
  border-radius: 2px;
  padding: 10px 20px;
  font-size: 20px;
  cursor: pointer;
}
.start span:hover {
  background: #16a085;
  color: white;
}

footer {
  padding: 5px;
  color: white;
  font-size: 14px;
}

footer a {
  color: #333;
  text-decoration: none;
}

footer a:hover {
  color: white;
  text-decoration: underline;
}

// ADD time?

var snakeTable = document.querySelector(".snakeTable");
var boxes = document.getElementsByClassName("box");
var modul = document.querySelector(".modul");
var start = document.querySelector(".start");

var table = {
  rowsCols: 21,
  boxes: 21*21
};

var snake = {
  direction: "right",
  position: [[6,10], [7,10], [8,10], [9,10], [10,10]],
  interval: 200,
  food: 0,
  score: 0,
  final: 0,
  time: 0,
  canTurn: 0,
  init: function() {
    snake.direction = "right";
    snake.position = [[6,10], [7,10], [8,10], [9,10], [10,10]];
    snake.interval = 200;
    snake.food = 0;
    snake.score = 0;
    snake.time = 0;
    snake.canTurn = 0;
    snakeTable.innerHTML = "";
    tableCreation();
  }
};

// init game
snake.init();

start.addEventListener("click", startSnake);

document.addEventListener("keydown", function(e) {
  if (e.keyCode === 13 && snake.time === 0) {
    startSnake();
  }
});

// start game
function startSnake() {
  modul.classList.add("hidden");
  // clearInterval(checkPageInterval);
  snake.time = 1;
  renderSnake();
  randomFood();
  // interval, heart of the game
  setInt = setInterval(function() {
    move();
  }, snake.interval);
}

// end of game
function stopp() {
  clearInterval(setInt);
  snake.final = snake.score;
  start.querySelector("span").innerHTML = snake.final + " Points !";
  setTimeout(function() {
    start.querySelector("span").innerHTML = "Play Snake";
  }, 1500);
  snake.init();
  modul.classList.remove("hidden");
}

// move the snake function
function move() {
  // check if move allowed & then eat food
  hitFood();
  hitBorder();
  hitSnake();
  // actually move the snake
  updatePositions();
  renderSnake();
  document.addEventListener("keydown", turn);
  snake.canTurn = 1;
}

function updatePositions() {
  // remove last snake part (first snake pos)
  boxes[snake.position[0][0] + snake.position[0][1] * table.rowsCols].classList.remove("snake");
  snake.position.shift();
  // add new snake part
  var head = snake.position[snake.position.length - 1];
  switch (snake.direction) {
    case "left":
      snake.position.push([head[0] - 1, head[1]]);
      break;
    case "up":
      snake.position.push([head[0], head[1] - 1]);
      break;
    case "right":
      snake.position.push([head[0] + 1, head[1]]);
      break;
    case "down":
      snake.position.push([head[0], head[1] + 1]);
      break;
    default:
      console.log("no direction !");
  }
}

// checks border contact
function hitBorder() {
  var headPos = snake.position.length-1;
  // goes of limits
  if (((snake.position[headPos][0] === table.rowsCols-1) && (snake.direction === "right")) || ((snake.position[headPos][0] === 0) && (snake.direction === "left")) || ((snake.position[headPos][1] === table.rowsCols-1) && (snake.direction === "down")) ||  ((snake.position[headPos][1] === 0) && (snake.direction === "up"))) {
    // console.log("border hit");
    stopp();
  }
}

// checks self contact
function hitSnake() {
  var headPos = snake.position.length-1;
  for (var i=0; i<headPos; i++) {
    if (snake.position[headPos].toString() === snake.position[i].toString()) {
      // console.log("snake hit");
      stopp();
    }
  } 
}

// checks food contact
function hitFood() {
  var head = snake.position[snake.position.length-1];
  var tail = snake.position[0];
  if (head.toString() === foodPos.toString()) {
    boxes[random].classList.remove("food");
    snake.position.unshift(tail);
    randomFood();
    snake.food++;
    snake.score += snake.food;
    scoreElt.innerHTML = snake.score + " pts";
    // increase speed
    clearInterval(setInt);
    snake.interval = snake.interval - snake.interval/40;
    setInt = setInterval(function() {
      move();
    }, snake.interval);
  }
}

// random 'food'
function randomFood() {
  var randomX = Math.floor(Math.random() * table.rowsCols);
  var randomY = Math.floor(Math.random() * table.rowsCols);
  random = randomX + randomY * table.rowsCols;
  // picks another foodPos if food pops on snake
  while (boxes[random].classList.contains("snake")) {
    randomX = Math.floor(Math.random() * table.rowsCols);
    randomY = Math.floor(Math.random() * table.rowsCols);
    random = randomX + randomY * table.rowsCols;
  }  
  boxes[random].classList.add("food");
  foodPos = [randomX, randomY];
}

// read positions and render the snake
function renderSnake() {
  for (var i=0; i<snake.position.length; i++) {
    boxes[snake.position[i][0] + snake.position[i][1] * table.rowsCols].classList.add("snake");
  }
}

// keypress handling to turn
function turn(e) {
  if (snake.canTurn) {
    switch (e.keyCode) {
      case 13:
        // document.removeEventListener()
        break;
      case 37:// left
        if (snake.direction === "right") return;
        snake.direction = "left";
        break;
      case 38:// up
        if (snake.direction === "down") return;
        snake.direction = "up";
        break;
      case 39:// right
        if (snake.direction === "left") return;
        snake.direction = "right";
        break;
      case 40:// down
        if (snake.direction === "up") return;
        snake.direction = "down";
        break;
      default:
        console.log("wrong key");
    }
    snake.canTurn = 0;
  }
}

// table creation
function tableCreation() {
  if (snakeTable.innerHTML === "") {
    // main table
    for (var i = 0; i<table.boxes; i++) {
      var divElt = document.createElement("div");
      divElt.classList.add("box");
      snakeTable.appendChild(divElt);
    }
    // status bar
    var statusElt = document.createElement("div");
    statusElt.classList.add("status");
    snakeTable.appendChild(statusElt);
    scoreElt = document.createElement("span");
    scoreElt.classList.add("score");
    scoreElt.innerHTML = snake.score + " pts";
    statusElt.appendChild(scoreElt);
  }
}

// handle focus of the page
// function checkPageFocus() {
//   if (document.hasFocus()) {
//     modul.classList.remove("hidden");
//   }
//   else {
//     modul.classList.add("hidden");
//   }
// }
// var checkPageInterval = setInterval(checkPageFocus, 300);

// swipe Showcase
$("document").ready(function() {
  $("body")
    .swipeDetector()
    .on("swipeLeft.sd swipeRight.sd swipeUp.sd swipeDown.sd", function(event) {
      if (event.type === "swipeLeft") {
        if (snake.direction === "right") return;
        snake.direction = "left";
      } else if (event.type === "swipeRight") {
        if (snake.direction === "left") return;
        snake.direction = "right";
      } else if (event.type === "swipeUp") {
        if (snake.direction === "down") return;
        snake.direction = "up";
      } else if (event.type === "swipeDown") {
        if (snake.direction === "up") return;
        snake.direction = "down";
      }
      snake.canTurn = 0;
    });
});

// swipe function --> credit: https://codepen.io/AlexEmashev/pen/BKgQdx?editors=0100
(function($) {
  $.fn.swipeDetector = function(options) {
    // States: 0 - no swipe, 1 - swipe started, 2 - swipe released
    var swipeState = 0;
    // Coordinates when swipe started
    var startX = 0;
    var startY = 0;
    // Distance of swipe
    var pixelOffsetX = 0;
    var pixelOffsetY = 0;
    // Target element which should detect swipes.
    var swipeTarget = this;
    var defaultSettings = {
      // Amount of pixels, when swipe don't count.
      swipeThreshold: 30,
      // Flag that indicates that plugin should react only on touch events.
      // Not on mouse events too.
      useOnlyTouch: true
    };

    // Initializer
    (function init() {
      options = $.extend(defaultSettings, options);
      // Support touch and mouse as well.
      swipeTarget.on("mousedown touchstart", swipeStart);
      $("html").on("mouseup touchend", swipeEnd);
      $("html").on("mousemove touchmove", swiping);
    })();

    function swipeStart(event) {
      if (options.useOnlyTouch && !event.originalEvent.touches) return;

      if (event.originalEvent.touches) event = event.originalEvent.touches[0];

      if (swipeState === 0) {
        swipeState = 1;
        startX = event.clientX;
        startY = event.clientY;
      }
    }

    function swipeEnd(event) {
      if (swipeState === 2) {
        swipeState = 0;

        if (
          Math.abs(pixelOffsetX) > Math.abs(pixelOffsetY) &&
          Math.abs(pixelOffsetX) > options.swipeThreshold
        ) {
          // Horizontal Swipe
          if (pixelOffsetX < 0) {
            swipeTarget.trigger($.Event("swipeLeft.sd"));
          } else {
            swipeTarget.trigger($.Event("swipeRight.sd"));
          }
        } else if (Math.abs(pixelOffsetY) > options.swipeThreshold) {
          // Vertical swipe
          if (pixelOffsetY < 0) {
            swipeTarget.trigger($.Event("swipeUp.sd"));
          } else {
            swipeTarget.trigger($.Event("swipeDown.sd"));
          }
        }
      }
    }

    function swiping(event) {
      // If swipe don't occuring, do nothing.
      if (swipeState !== 1) return;

      if (event.originalEvent.touches) {
        event = event.originalEvent.touches[0];
      }

      var swipeOffsetX = event.clientX - startX;
      var swipeOffsetY = event.clientY - startY;

      if (
        Math.abs(swipeOffsetX) > options.swipeThreshold ||
        Math.abs(swipeOffsetY) > options.swipeThreshold
      ) {
        swipeState = 2;
        pixelOffsetX = swipeOffsetX;
        pixelOffsetY = swipeOffsetY;
      }
    }

    return swipeTarget; // Return element available for chaining.
  };
})(jQuery);

// remove scroll for mobile IOS issue
function preventDefault(e){e.preventDefault();}
function disableScroll(){
    document.body.addEventListener('touchmove', preventDefault, { passive: false });
}
function enableScroll(){
    document.body.removeEventListener('touchmove', preventDefault, { passive: false });
}
disableScroll();

// https://www.theodinproject.com/courses/javascript-and-jquery/lessons/jquery-and-the-dom

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js