Snake monochrome (JS1024)
JS1024 is an annually held code golfing competition where you're basically required to create something creative in 1024 bytes or less of raw "JavaScript/HTML/WebGL"
I participated for JS1024 2020 competition with my First entry titled "Snake monochrome" Originally inspired by the old Nokia game going by the same name "Snake" which i used to play as far back as 2007-2008
Another entry making it Second is named "Tetris monochrome" i was compiled to do this game as a dare from my Girlfriend on June 6 the "Tetris 1984" anniversary
Third one is F1-racer it's inspired by my bestfriend who wasn't impressed with how i hooked up controls on "Tetris Monochrome" so he requested "i modify controls a bit", and i improved "input/output" by adding smooth touch input to it as requested
Snake game contains two entities. The Snake and the Apple, the Snake is an Object with properties position (vector), velocity (vector) and trial an Array of vectors each with two coordinates {x: (number), y: (number)}, as position and velocity
Same applies to the Apple, additionally the Apple has a special function named "reset", which gets called when the snake collides with it and basically resets its state and computes a new position
Apart from the two entities i added some functions for calculating circle collision detection, relaying "input/output" as well as a main game loop with functions responsible for setting scores and updating individual entities
Entities are rendered in monochrome style "dark-greenish" and "grayish", a single color is used for entities and another is used as background clear color
For the sake of simplicity under the hood all entities are composed of circles "dots"
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Constants | |
// ---------- | |
const { | |
innerWidth, | |
innerHeight | |
} = window; | |
const width = innerWidth; | |
const height = innerHeight; | |
// Apple | |
class Apple { | |
constructor(params) { | |
const { | |
color, | |
radius | |
} = params; | |
this['color'] = color; | |
this['radius'] = radius; | |
this['update'] = params => { | |
}; | |
this['render'] = params => { | |
const { | |
context | |
} = params; | |
context.save(); | |
context.beginPath(); | |
context.fillStyle = this.color; | |
context.arc(this.x, this.y, this.radius, 0, Math.PI * 2); | |
context.fill(); | |
context.closePath(); | |
context.restore(); | |
}; | |
this['reset'](); | |
} | |
reset() { | |
const radii = this.radius; | |
const radius = (radii * 2); | |
this.x = (Math.random() * (width - radius)) + radii; | |
this.y = (Math.random() * (height - radius)) + radii; | |
} | |
collides(c) { | |
const { | |
radius | |
} = this; | |
const a = this.x - c.x; | |
const b = this.y - c.y; | |
const d = ((a * a) + (b * b)); | |
const radii = radius + c.radius; | |
return radii * radii >= d | |
} | |
} | |
// Snake | |
class Snake { | |
constructor(params) { | |
const { | |
id, | |
color, | |
radius | |
} = params; | |
this['x'] = width / 2; | |
this['y'] = height / 2; | |
this['id'] = id; | |
this['color'] = color; | |
this['radius'] = radius; | |
this['length'] = 10; | |
this['trail'] = []; | |
this['velocity'] = [0, 0]; | |
this['update'] = params => { | |
const { | |
length, | |
radius, | |
velocity | |
} = this; | |
const speed = 10; | |
this.x += ((radius * velocity[0] * speed) / 60); | |
this.y += ((radius * velocity[1] * speed) / 60); | |
this.trail.unshift({ | |
x: this.x, | |
y: this.y, | |
radius | |
}); | |
if (this.y > height - radius) | |
this.y = radius; | |
if (this.y < radius) | |
this.y = height - radius; | |
if (this.x > width - radius) | |
this.x = radius; | |
if (this.x < radius) | |
this.x = width - radius; | |
}; | |
this['render'] = params => { | |
const { | |
context | |
} = params; | |
this.trail = this.trail.splice(0, this.length); | |
this.trail.forEach((cell, index) => { | |
const opacity = ((index / this.length) - 1) * -1; | |
const { | |
radius | |
} = cell; | |
context.save(); | |
context.beginPath(); | |
context.fillStyle = `rgba(67,77,67,${opacity})`; | |
context.arc(cell.x, cell.y, ((radius + radius) / 2), 0, Math.PI * 2); | |
context.fill(); | |
context.closePath(); | |
context.restore(); | |
}); | |
}; | |
} | |
} | |
// Game Loop Module | |
// This module contains the game loop, which handles | |
// updating the game state and re-rendering the canvas | |
// (using the updated state) at the configured tframe. | |
class Loop { | |
constructor(scope, tframe = 60) { | |
this['delta'] = (1000 / tframe), | |
this['elapsed'] = 0; | |
this['tframe'] = (1000 / tframe), | |
this['nframe'] = tframe, | |
this['before'] = window.performance.now(); | |
this['animate'] = params => { | |
this.loop = window.requestAnimationFrame(this.animate); | |
this.delta = Math.round(((1000 / (window.performance.now() - this.before) * 100) / 100)); | |
if (window.performance.now() < this.before + this.tframe) return | |
this.before = window.performance.now(); | |
scope.update(this.elapsed, this.delta); | |
scope.render(this.elapsed, this.delta); | |
this.elapsed++; | |
}; | |
this['loop'] = window.requestAnimationFrame(this.animate); | |
this['stop'] = params => window.cancelAnimationFrame(this.loop); | |
} | |
} | |
// Game | |
class Game { | |
constructor(params) { | |
this['score'] = 0; | |
this['entities'] = []; | |
this['context'] = params.context; | |
this['loop'] = new Loop(this, 60); | |
} | |
render(elapsed, delta) { | |
const context = this.context; | |
context.fillStyle = "#acbeac"; | |
context.fillRect(0, 0, | |
canvas.width, | |
canvas.height | |
); | |
const { | |
entities | |
} = this; | |
entities | |
.map(entity => entity.render({ | |
context, | |
elapsed, | |
delta | |
})); | |
context.font = '1em digit'; | |
context.fillStyle = "#0f0f0f"; | |
context.fillText('score: ' + this.score, (16), (50)); | |
} | |
update(elapsed, delta) { | |
const { | |
entities | |
} = this; | |
entities | |
.map(entity => entity.update(elapsed, delta)); | |
} | |
addEntity(entity) { | |
const index = this.entities | |
.push(entity); | |
return entity | |
} | |
} | |
// Input | |
// ------ | |
class Input { | |
constructor(params) { | |
const { | |
snake | |
} = params; | |
document.addEventListener('keydown', event => { | |
const velocity = [0, 0]; | |
// Ignore | |
if (event.keyCode === 37) { | |
velocity[0] = -1; | |
snake.velocity = velocity; | |
} else if (event.keyCode === 39) { | |
velocity[0] = 1; | |
snake.velocity = velocity; | |
} else if (event.keyCode === 38) { | |
velocity[1] = -1; | |
snake.velocity = velocity; | |
} else if (event.keyCode === 40) { | |
velocity[1] = 1; | |
snake.velocity = velocity; | |
} | |
}); | |
document.addEventListener("click", (point) => { | |
const { | |
pageX, | |
pageY | |
} = point; | |
const i = { | |
x: pageX, | |
y: pageY | |
}; | |
const ii = { | |
x: snake.x, | |
y: snake.y | |
}; | |
const velocity = [0, 0]; | |
const dy = ii.y - i.y; | |
const dx = ii.x - i.x; | |
const ay = Math.abs(dy); | |
const ax = Math.abs(dx); | |
if (ay > ax) { | |
velocity[1] = (dy > 0) ? (-1) : (1); | |
} else { | |
velocity[0] = (dx > 0) ? (-1) : (1); | |
} | |
snake.velocity = velocity; | |
}); | |
} | |
} | |
const main = params => { | |
const canvas = document.querySelector('#canvas'); | |
const context = canvas.getContext('2d'); | |
const backingStores = ['webkitBackingStorePixelRatio', 'mozBackingStorePixelRatio', 'msBackingStorePixelRatio', 'oBackingStorePixelRatio', 'backingStorePixelRatio']; | |
const deviceRatio = window.devicePixelRatio; | |
const backingRatio = backingStores.reduce(function(prev, curr) { | |
return (Object.prototype.hasOwnProperty.call(context, curr) ? context[curr] : 1) | |
}); | |
const ratio = deviceRatio / backingRatio; | |
canvas.width = Math.round(width * ratio); | |
canvas.height = Math.round(height * ratio); | |
canvas.style.width = width + 'px'; | |
canvas.style.height = height + 'px'; | |
context.setTransform(ratio, 0, 0, ratio, 0, 0); | |
const game = class extends Game { | |
constructor(params) { | |
super(params); | |
this['apple'] = this | |
.addEntity(new Apple({ | |
radius: 10, | |
color: 'rgba(67, 77, 67, 1)' | |
})); | |
const snake = this | |
.addEntity(new Snake({ | |
radius: 10 | |
})); | |
const input = new Input({ | |
snake | |
}); | |
} | |
update(elapsed, delta) { | |
this.entities.map(entity => { | |
if (entity instanceof Snake) | |
for (const block of entity.trail) { | |
const { | |
apple | |
} = this; | |
if (apple.collides(block)) { | |
apple.reset(); | |
if (entity.trail.indexOf(block) === 0) { | |
entity.length++; | |
this.score++; | |
} | |
} | |
} | |
}); | |
super.update(elapsed, delta); | |
} | |
}; | |
window.game = new game({ | |
context | |
}); | |
}; | |
window.onload = () => main(); |
Comments
Post a Comment