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 |
- 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>
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);
}
}
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();
}
}
}
... 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;
}
}
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
}
What now?
What do you think?
What would you have done differently?
Would you mind inheriting code like this?
Top comments (0)