I was bored on an airplane a while ago, and I just wanted to do something mindless. I decided that playing Snake was a little too mindless, but writing Snake (and then playing it) was just the ticket.

Here’s the game up front. Press g to start, use w/a/s/d to move, and press the j and k keys to adjust the game speed. I found that bumping up the difficulty a bit (by pressing k a few times) makes the game a lot more fun.

The code was written in about 45 minutes on a plane, and it has some hallmarks of that: constants scattered throughout the code, both Snake and CanvasView tracking the canvas size, etc. Nonetheless, it passes the main test I have for it: that I find it fun.

class CanvasView {
  constructor (el, width, height, pxSize=5) {
    this._canvas = el;
    this._canvas.width = width;
    this._canvas.height = height;
    this._ctx = this._canvas.getContext('2d');
    this._ctx.fillStyle = 'black';
    this._pxSize = pxSize;
  }

  pxSize() {
    return this._pxSize;
  }

  drawPixel(x, y) {
    this._ctx.fillRect(x * this._pxSize, y * this._pxSize, this._pxSize, this._pxSize);
  }

  clearPixel(x, y) {
    this._ctx.clearRect(x * this._pxSize, y * this._pxSize, this._pxSize, this._pxSize);
  }

  clear() {
    this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
  }
}

Direction = {
  "UP" : 0,
  "RIGHT": 1,
  "DOWN": 2,
  "LEFT": 3
}

class Snake {
  constructor () {
    this._height = 101;
    this._width = 101;
    this._canvas = new CanvasView(document.querySelector('canvas'), 505, 505);
    this.running = false;
    this.reset();
  }

  reset() {
    this._x = 50;
    this._y = 50;
    this._direction = Direction.RIGHT
    this._points = []; /* from tail to head */
    this._length = 10;
    this._interval = 150;
    this.setTreat();
  }

  static toPoint(x, y) {
    return {"x": x, "y": y}
  }

  isAtPoint(x, y) {
    return this._points.filter(function(pt) { return pt.x == x && pt.y == y }).length > 0
  }

  step() {
    if (this._direction == Direction.UP) this._y -= 1
    if (this._direction == Direction.RIGHT) this._x += 1
    if (this._direction == Direction.DOWN) this._y += 1
    if (this._direction == Direction.LEFT) this._x -= 1
    if (this._x < 0 || this._x >= this._width || this._y < 0 || this._y >= this._height) { this.die(); }
    if (this.isAtPoint(this._x, this._y)) { this.die(); }
    if (this.isAtPoint(this._treat.x, this._treat.y)) { this.eatTreat(); }
    this._canvas.drawPixel(this._x, this._y)
    this._points.push(Snake.toPoint(this._x, this._y))
    if (this._points.length > this._length) {
      let pt = this._points.shift()
      this._canvas.clearPixel(pt.x, pt.y)
    }
  }

  setTreat() {
    this._treat = Snake.toPoint(Math.floor(Math.random() * 101), Math.floor(Math.random() * 101));
    this._canvas.drawPixel(this._treat.x, this._treat.y);
  }

  eatTreat() {
    this._length = Math.floor(this._length * 1.5);
    this.setTreat();
  }

  handleKeyPress(e) {
    if (this.running) {
      if (e.key == "w") this.turn(Direction.UP)
      if (e.key == "d") this.turn(Direction.RIGHT)
      if (e.key == "s") this.turn(Direction.DOWN)
      if (e.key == "a") this.turn(Direction.LEFT)
      if (e.key == "j") this.changeSpeed(1.75)
      if (e.key == "k") this.changeSpeed(0.75)
    }
    else {
      if (e.key == "g") this.start()
    }
  }

  turn(d) {
    if (d == Direction.UP && this._direction == Direction.DOWN) this.die()
    if (d == Direction.RIGHT && this._direction == Direction.LEFT) this.die()
    if (d == Direction.DOWN && this._direction == Direction.UP) this.die()
    if (d == Direction.LEFT && this._direction == Direction.RIGHT) this.die()
    this._direction = d;
  }
  
  changeSpeed(multFactor) {
    snake._interval *= multFactor;
    clearInterval(this._step_timer);
    snake._step_timer = setInterval(function() {snake.step()}, this._interval)
  }

  die() {
    clearInterval(this._step_timer);
    this.running = false;
    this._canvas.clear();
    this.reset();
  }

  start() {
    snake._step_timer = setInterval(function() {snake.step()}, this._interval)
    this.running = true
  }
}

snake = new Snake();
window.onkeypress = function(e) {snake.handleKeyPress(e)}