A very _classy_ snake
Tags: javascript, games, learn, canvas
Published: 2019-08-10 15:54:15


I have the habit of start my coding musings by branching away from other people's proposals.

In this case I found myself looking at this video, where the author codes 
[the snake game in 4'30''](https://www.youtube.com/watch?v=xGmXxpIj6vs)

The video is very good and the code is purposefully and relentlessly hacky.
And the game indeed works after that time. While I was looking at it I was thinking, wow,
I cannot beat him at his own game, but is it really so much different between that
wild-brush throwaway code and something I would be proud to show on an interview/job application?

So I got hand on, with a series on constraints, namely:

- Present the exact same UI

| ![A humble but familiar UI](https://thepracticaldev.s3.amazonaws.com/i/99ioboxldbcpq0lsmo17.png) | 
|:--:| 
| *A humble but familiar UI* |

- Do it on the same medium (browser only javascript)
- Don't use libraries/external resources
- Do it on the equivalent on half a working day: ~4 hours
- Use as much os ES6 as you can think off, so it doubles as ES6 exercise.
  - Don't bother with browser compatibility, assume es6 is available
- Don't add additional features


Here is the `index.html` needed to run the game

```html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width">
    <title>repl.it</title>
    <link href="style.css" rel="stylesheet" type="text/css" />
  </head>
  <body>
    <canvas id='field' width='600' height='600'>Loading...</div>
    <script src='script.js'></script> 
    <script>
      window.onload = () => { let game = new Game({canvasId: 'field'}); };  
    </script>
  </body>
</html>
```
And here you have the meat:

### The main `Game` class

```javascript
// Usage: 
//   <canvas id='field' width='600' height='600'>Loading...</div>
//   <script src='script.js'></script> 
//   <script>
//     window.onload = () => { let game = new Game({canvasId: 'field'}); };  
//   </script>
class Game {
  constructor({ canvasId }){
    this.grid = { tiles: 20 , size: 30 };
    this.apple = new Apple(); //{ position: { x: 15 , y: 15 }, calories: 1 };
    this.snake = new Snake();
    this.init(canvasId);
  }

  init(canvasId){
    let canvas = document.getElementById(canvasId);
    this.context = canvas.getContext('2d');                     // GUI
    addEventListener('keydown', (e) => { this.userInput(e) } ); // User input
    setInterval( () => { this.game() }, 1000 / 8);              // Game loop 
  }

  userInput(event){
    const north = { x: +0, y: -1 };
    const east  = { x: +1, y: +0 };
    const south = { x: +0, y: +1 };
    const west  = { x: -1, y: +0 };
    const arrows = { left: 37, up: 38, right: 39, down: 40};
    const bearings = {
      [arrows.left]: west,
      [arrows.up]: north,
      [arrows.right]: east,
      [arrows.down]: south
    }

    let bearing = bearings[event.keyCode];
    this.snake.head(bearing);
  }

  game(){
    this.snake.move();
    let ap = this.apple.position;
    let sp = this.snake.position;
    // console.log(`apple: ${ap.x},${ap.y} snake: ${sp.x},${sp.y} `)
    if (this.snake.position.x === this.apple.position.x &&
        this.snake.position.y === this.apple.position.y) {
      this.levelUp();
    }

    this.draw();
  }

  levelUp(){
    let calories = this.apple.calories; 
    this.snake.eat(this.apple);
    delete this.apple;
    // optional: randomly increase apple nutritive value:
    // calories += ( Math.random(5) > 3 ? 0 : 1 );
    this.apple = Apple.spawn({ max: this.grid.tiles, calories: calories });
  }

  draw() {
    // canvas
    this.context.fillStyle = "black";
    this.context.fillRect(0, 0, this.grid.tiles * this.grid.size, this.grid.tiles * this.grid.size);

    // snake
    for(var i = 0; i < this.snake.trail.length; i++) {
      this.drawSquare(this.snake.trail[i], "lime");
    }
    
    // apple
    this.drawSquare(this.apple.position, "red");
  }

  drawSquare(object, color){
    this.context.fillStyle = color;
    this.context.fillRect(
      object.x * this.grid.size,
      object.y * this.grid.size,
      this.grid.size - 2,
      this.grid.size - 2);
  }
}
```

### The very slithering `Snake` class

```js
class Snake {
  constructor(options = {}) {
    const defaults = {
      x: 10 , y: 10, max: 20,
      tail: 3,
      velocity: {x: 0, y: 0}
    };
    Object.assign(this, defaults, options);
    this.initialLength = options.tail || defaults.tail;
    this.trail = [];
  }

  get position(){
    return { x: this.x, y: this.y };
  }

  head(bearing){
    this.velocity.x = bearing.x;
    this.velocity.y = bearing.y;
  }

  move(){
    this.x += this.velocity.x;
    this.y += this.velocity.y;
    this._wrapWalls();
    this._detectCollision();
    this._updateTrail();
  }
  
  eat (food){
    this.tail += food.calories;
  }
  
  die (){
    this.tail = this.initialLength;
  }

  _wrapWalls(){
    if (this.x < 0) { this.x = this.max - 1; } // wrap left->right
    if (this.x > this.max - 1) { this.x = 0; } // wrap right->left
    if (this.y < 0) { this.y = this.max - 1; } // wrap bottom->top
    if (this.y > this.max - 1) { this.y = 0; } // wrap top->bottom
  }

  _detectCollision(){
    for(var i = 0; i < this.trail.length; i++) {
      if (this.trail[i].x === this.x && this.trail[i].y === this.y) {
        this.die();
      }
    }
  }

  _updateTrail(){
    this.trail.push({ x: this.x, y: this.y });
    while(this.trail.length > this.tail) {
      this.trail.shift();
    }
  }
}
```

### ... and the humble tasty `Apple` class

```js

class Apple {
  constructor(options = {}) {
    const defaults = { x: 15 , y: 15, calories: 1};
    Object.assign(this, defaults, options);
  }
  
  get position(){
    return { x: this.x, y: this.y };
  }

  static spawn({ max, calories }){
    let ax = Math.floor(Math.random() * max);
    let ay = Math.floor(Math.random() * max);
    let sprout = new Apple({ x: ax, y: ay, calories: calories});
    return sprout;
  }
}
```

## Afterthoughts

It is however relatively easy to add other modifications:
- Apples could make you longer by a bigger increment as the game progreses (that's the idea behing the _calories_).
- Game speeed could accelerate every time you level up:
```js
  init(canvasId){
    //...
    this.inpetrval = setInterval( () => { this.game() }, 1000 / this.speed);     // Game loop 
  }
  //...
  levelUp(){
    //...
    this.speed += 1;
    this.clearInterval(this.interval);
    this.inpetrval = setInterval( () => { this.game() }, 1000 / this.speed);     // Game loop updated
  }
  ```

## What now?

What do you think?
What would you have done differently?
Would you mind inheriting code like this?