Skip to content

Instantly share code, notes, and snippets.

@oinak
Last active August 10, 2019 16:13
Show Gist options
  • Save oinak/2231c4986fb3c44ba8bb85d76162a8f9 to your computer and use it in GitHub Desktop.
Save oinak/2231c4986fb3c44ba8bb85d76162a8f9 to your computer and use it in GitHub Desktop.

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 seires on constraints, namely:

  • 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

There are things that could be done but I left out:

Leveling up

The basic win-mechanics on the snake game is that when you eat an apple your tail grows.

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
  }

This was originally published as a Post on dev.to

<!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>
// 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);
}
}
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();
}
}
}
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;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment