This tutorial shows how to write Snake Game in
JavaScript. It requires basic knowledge about objects
and methods of arrays. In the first part, we are going to
be able to display map, snake and allow the snake to move
in a chosen direction. This code will be a not playable
version of a game, but I decided to depart this project
to fragments because of the highest educational value of
presenting the process of building code, not only the final
result.

If you are interested in the final result you can
download it from

https://github.com/gustawdaniel/snake_js/releases/tag/v0.1

If you want to write a code line by line together lets
start typing following these chapters.

Map.

In this section, we will see how to generate a map for
snake.

Server setup

Commits
23d7da5a511855efd8e01da219af045d037dba93..26816daa5de0bd3c5203ca61d3b616ac003cfe39

We starting from creating index.html. It can be
empty or filled with simple text. I started from this
one:

<html>
<head>
    <title>Snake - game dedicated for Sylwia Daniecka - my girlfriend!</title>
</head>
<body>
    <h1>I love Sylwia <3</h1>
</body>
</html>

To serve this document I installed http-server using
yarn. I typed:

yarn init

and press Enter such many times as needed. After
this I used the command:

yarn add http-server

These commands are responsible for the shape of the file

package.json

{
  "name": "snake_js",
  "version": "1.0.0",
  "main": "index.js",
  "repository": "git@github.com:gustawdaniel/snake_js.git",
  "author": "Daniel Gustaw <gustaw.daniel@gmail.com>",
  "license": "MIT",
  "dependencies": {
    "http-server": "^0.11.1"
  }
}

Finally, I write docs about starting a project

# snake_js
Snake game written in javascript using objects. 

# Instaltion

To install dependencies

    yarn install

To run

    node node_modules/http-server/bin/http-server 

Map generation

Commits
26816daa5de0bd3c5203ca61d3b616ac003cfe39..87dbf6d8162dde68ab137ddee78dcb975413d104

We will use jquery so we should type in console

yarn add jquery

Now we place div with id map and append script and
style from external files in index.html

index.html

 <html>
 <head>
     <title>Snake - game dedicated for Sylwia Daniecka - my girlfriend!</title>
+    <link rel="stylesheet" href="css/style.css">
 </head>
 <body>
     <h1>I love Sylwia <3</h1>
+    <hr>
+    <h4>TODO:</h4>
+    <ol>
+        <li style="text-decoration: line-through">Add map</li>
+        <li>Add snake</li>
+        <li>Add events</li>
+    </ol>
+    <hr>
+    <main>
+        <div id="map"></div>
+    </main>
+    <script src="node_modules/jquery/dist/jquery.min.js"></script>
+    <script src="js/app.js"></script>
 </body>
 </html>

We are interested in creating the script that iterates over the size of map and place for example 10 rows with 10 rectangles in each. This script will be placed in anonymous function in js/app.js

js/app.js

(function () {

    const config = {
        mapWidth: 10,
        mapHeight: 10
    };

    let map = {
        width: config.mapWidth,
        height: config.mapHeight,
        init: function () {
            let mapDiv = $('#map');
            for(let i=0; i<this.height; i++) {
                console.log(i);
                let rowDiv =$('<div>', {class: "row"});
                console.log(rowDiv);
                for(let j=0; j<this.width; j++) {
                    rowDiv.append($('<div>',{class:"rect", "data-x":i, "data-y":j}));
                }
                mapDiv.append(rowDiv);
            }
        }
    };

    map.init();

    
})();

This script create divs with class rect and place
it into divs with class row that are placed into
existing dive with id map.

These all divs have zero height so we should define
style for him.

css/style.css

.rect {
    width: 30px;
    height: 30px;
    background-color: #dca6d1;
    display: inline-block;
    margin: 2px;
}

The map should look like this:

Snake

Commit
87dbf6d8162dde68ab137ddee78dcb975413d104..d50265a3a5adca92bfe7d5139c0e519e72d853dc

To add snake we should choose his color. In constant config responsible for the global configuration we describing the color of a snake.

js/app.js

@@ -2,7 +2,8 @@
 
     const config = {
         mapWidth: 10,
         mapHeight: 10,
+        snakeColor: "#8165f3"
     };
 
     let map = {

Now we want to create an object of the snake. We have to define his initial shape, iterate over his all parts (coordinates) and change background-color of dives with this coordinates. Fortunately, we added to divs with class rect attributes data-x and data-y that will help in searching proper elements.

js/app.js

@@ -22,7 +23,17 @@
         }
     };
 
+    let snake = {
+        body: [{x:5,y:2},{x:4,y:2},{x:3,y:2}],
+        draw: function() {
+            this.body.forEach(function (part) {
+                $(`div.rect[data-x="${part.x}"][data-y="${part.y}"]`).css('background-color',config.snakeColor);
+            })
+        }
+    };
+
     map.init();
+    snake.draw();
 
     
 })();

Finally, we can proudly check these changes in todo list in index.html adding
to text-decoration property line-through

index.html

@@ -9,7 +9,7 @@
     <h4>TODO:</h4>
     <ol>
         <li style="text-decoration: line-through">Add map</li>
-        <li>Add snake</li>
+        <li style="text-decoration: line-through">Add snake</li>
         <li>Add events</li>
     </ol>
     <hr>

Our map should look like this

Move and events

Commit
d50265a3a5adca92bfe7d5139c0e519e72d853dc..ae194969e7d2a555c9dc7ed2fb57c81b56775b62

It is time to add dynamics to our snake.

We start by adding a counter to show how many turns have our game.

index.html
     </ol>
     <hr>
+    <header>
+        <p class="counter">0</p>
+    </header>
     <main>
         <div id="map"></div>
     </main>

And style for this element

css/style.css
+.counter {
+    text-align: right;
+    border: 1px solid black;
+    padding: 7px;
+ }

Now we come back to logic. Firstly we will need the color of the map and time of one round in our configuration. The second value is measured in milliseconds

js/app.js
@@ -3,7 +3,9 @@
     const config = {
         mapWidth: 10,
         mapHeight: 10,
-        snakeColor: "#8165f3"
+        snakeColor: "#8165f3",
+        mapColor: "#dca6d1",
+        roundTime: 1000
     };
 
     let map = {

Next, we want to add method move to snake object that removes his tail and add his head in a chosen direction in relation to the current head.

js/app.js
@@ -25,15 +25,64 @@
 
     let snake = {
         body: [{x:5,y:2},{x:4,y:2},{x:3,y:2}],
         draw: function() {
             this.body.forEach(function (part) {
                 $(`div.rect[data-x="${part.x}"][data-y="${part.y}"]`).css
('background-color',config.snakeColor);
             })
+        },
+        move: function (direction) {
+            let head = Object.assign({}, this.body[0]);

In function move, we assign new head to variable head cloning current head. Simple equality would not copy variable but only create a reference to the same cell of memory.

js/app.js
+            switch (direction) {
+                case "up":
+                    head.x = head.x -1; break;
+                case "down":
+                    head.x = head.x + 1; break;
+                case "left":
+                    head.y = head.y - 1; break;
+                case "right":
+                    head.y = head.y + 1; break;
+            }

Next, independence of direction we change coordinates. It can be misleading
but x means height and increasing in down direction.

js/app.js
+            this.body.unshift(head);
+            $(`div.rect[data-x="${head.x}"][data-y="${head.y}"]`)
+                .css('background-color',config.snakeColor);
+            let mapCoordinates  = this.body.pop();
+            $(`div.rect[data-x="${mapCoordinates.x}"][data-y="${mapCoordinates.y}"]`)
+                .css('background-color',config.mapColor);
+
+        }
+    };

Finally, we append new head to body by unshift, add snake color to this field, remove the tail by pop function and add map color to this coordinate.

Now we want to call move function in the interval and allow a user to change direction. We need a new object - game.

js/app.js
+    let game = {
+        counter: 0,
+        direction: 'right', // right, left, up, down
+        run: function () {
+            snake.move(this.direction);
+        },

Our game should have a property with the current direction of snake and counter with a number of turns. Function run of the game contains now move of the snake.

When the game will start we will call init method of game.

+        init: function () {
+            map.init();
+            snake.draw();
+            setInterval(() => {
+                this.counter ++;
+                $('.counter').text(this.counter);
+                this.run();
+            },config.roundTime);

Init of the game means initialization of map, drawing a snake and set the interval that increases the counter, update it on view, and call run method that move the snake.

If snake would have the same direction all time, it could end, but we want to allow user change direction of the snake by pressing arrows on the keyboard, for this purpose after defining time interval we defining an event listener
for keypress that will change a direction of the snake.

js/app.js
+            document.addEventListener('keypress',(e) => {
+                console.log(e.key);
+                switch (e.key) {
+                    case "ArrowUp":
+                        this.direction = this.direction === "down" ? this.direction : "up"; break;
+                    case "ArrowDown":
+                        this.direction = this.direction === "up" ? this.direction : "down"; break;
+                    case "ArrowLeft":
+                        this.direction = this.direction === "right" ? this.direction : "left"; break;
+                    case "ArrowRight":
+                        this.direction = this.direction === "left" ? this.direction : "right"; break;
+                }
+            })
         }
     };
 
-    map.init();
-    snake.drow();
+    game.init();

This version works only on Firefox. Chrome was not tested on this stage. To allow for following after code I decided to not change this behavior in this article. It will be fixed later.

Summary

We can now move snake in the map. It is can penetrate the walls and himself. There are no apples for eating, but as it was mentioned. It is not the final product but first pre-release that present rather educational than functional value.

Below I present plans about future releases.

v0.1
  1. Add map
  2. Add snake
  3. Add events
v0.2
  1. Add apples
  2. Add boundaries
  3. Add scores
Future (proposed)
  1. Add bad apples
  2. Add two players
  3. Add network gaming

You can propose your own features if you want and add them as comments or issues in the GitHub repository. I hope you learned something valuable reading this tutorial.