DEV Community

Oinak
Oinak

Posted on

A very _classy_ snake

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''

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

<!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>
Enter fullscreen mode Exit fullscreen mode

And here you have the meat:

The main Game class

// 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);
  }
}
Enter fullscreen mode Exit fullscreen mode

The very slithering Snake class

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();
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

... and the humble tasty Apple class


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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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:
  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
  }
Enter fullscreen mode Exit fullscreen mode

What now?

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

Top comments (0)