const assets=require('assets'); const [EMPTY, FOOD, WALL, SNAKE]=Array(4).keys(); const ifNaN=(v, r) => isNaN(v)?r:v; class SnekGame { constructor(settings, canvas, rules) { // build the world this.dimensions=[...settings.dimensions]; this.world=Array(settings.dimensions[0]); for(let i=0; i this.world[x][y]=WALL); settings.food.forEach(([x, y]) => this.world[x][y]=FOOD); settings.snake.forEach(([x, y]) => this.world[x][y]=SNAKE); // setup the delay this.delay=settings.delay; // get the head and initial direction this.head=[...settings.snake[0]]; this.direction=[ ifNaN(settings.snake[0][0]-settings.snake[1][0], 1), ifNaN(settings.snake[0][1]-settings.snake[1][1], 0) ]; // get the snake and the fruits themselves this.snake=[...settings.snake]; this.fruits=[...settings.food]; // get our canvas, like, if we want to actually draw this.canvas=canvas; this.ctx=canvas.getContext('2d'); //TODO this.gl=canvas.getContext('webgl'); // load the custom rules this.rules=Object.assign({ fruitRegrow: true, speedIncrease: true, worldWrap: true, winCondition: 'none', scoreSystem: 'fruit' }, rules, settings); } draw() { // clear the canvas, because it's easier than having to deal with everything this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); // get the cell size and offset const cellSize=Math.min( this.canvas.width/this.dimensions[0], this.canvas.height/this.dimensions[1] ); const offsetX=(this.canvas.width-cellSize*this.dimensions[0])/2; const offsetY=(this.canvas.height-cellSize*this.dimensions[1])/2; // draw the border around our game area this.ctx.fillStyle='black'; this.ctx.fillRect(0, 0, this.canvas.width, offsetY); this.ctx.fillRect(0, 0, offsetX, this.canvas.height); this.ctx.fillRect(offsetX+cellSize*this.dimensions[0], 0, offsetX, this.canvas.height); this.ctx.fillRect(0, offsetY+cellSize*this.dimensions[1], this.canvas.width, offsetY); // draw our walls const wall=assets.get('wall'); for(let x=0; x { this.ctx.lineTo( offsetX+cellSize*(x+1/2), offsetY+cellSize*(y+1/2) ); }); this.ctx.stroke(); // our fruit has a nice animation to it between .8 and 1.2 scale const ms=Date.now(); const fruitScale=Math.sin(ms/400*Math.PI)*.2+1 const fruit=assets.get('fruit'); this.fruits.forEach(([x, y]) => { this.ctx.drawImage( fruit, offsetX+cellSize*x+(1-fruitScale)*cellSize/2, offsetY+cellSize*x+(1-fruitScale)*cellSize/2, cellSize*fruitScale, cellSize*fruitScale ); }); } step() { // compute our new head const head=[ this.snake[0][0]+this.direction[0], this.snake[0][1]+this.direction[1] ]; // get our tail out of the way const tail=this.snake.pop(); this.world[tail[0]][tail[1]]=EMPTY; // check for out of world conditions if(head[0]<0 || head[0]>=this.dimensions[0] || head[1]<0 || head[1]>=this.dimensions[1]) { if(this.rules.worldWrap) { head[0]=(head[0]+this.dimensions[0])%this.dimensions[0]; head[1]=(head[1]+this.dimensions[1])%this.dimensions[1]; } else { return this.die(); } } switch(this.world[head[0]][head[1]]) { // you hit, you die case WALL: case SNAKE: return this.die(); // you eat, you don't die case FOOD: // re-grow the snake this.snake.push(tail); this.world[tail[0]][tail[1]]=SNAKE; // remove the fruit from existence this.world[head[0]][head[1]]=SNAKE; this.fruits.splice( this.fruits.find( ([x, y]) => x==head[0] && y==head[1] ), 1 ); // custom rules if(this.rules.fruitRegrow) { const emptyCells=this.world .map( (l, x) => l .map( (r, y) => r==EMPTY?[x,y]:null ).filter( a => a ) ).flat(); const cell=emptyCells[Math.floor(Math.random()*emptyCells.length)]; this.fruits.push(cell); this.world[cell[0]][cell[1]]=FOOD; } } // move our head forward this.world[head[0]][head[1]]=SNAKE; this.snake.unshift(head); // victory condition if(!this.fruits.length) return this.win(); } tick() { if(!this.playing) return; if(!this.lastStep) this.lastStep=this.firstStep; this.draw(); if(this.callback) this.callback(); if(this.lastStep+this.delay this.tick()); } win() { this.playing=false; // you gud lol console.log("You gud lol"); console.log(`Won in ${(Date.now()-this.firstStep)/1000} seconds`); } die() { this.playing=false; // you bad lol console.log("You bad lol"); } handleInputs(inputs) { const trySet=(dir) => { if(!(this.direction[0]==-dir[0] && this.direction[1]==-dir[1])) this.direction=dir; } if(inputs.left) { trySet([-1, 0]); } else if(inputs.right) { trySet([ 1, 0]); } else if(inputs.up) { trySet([ 0,-1]); } else if(inputs.down) { trySet([ 0, 1]); } Object.keys(inputs).forEach(k => delete inputs[k]); } start() { this.firstStep=Date.now(); this.playing=true; requestAnimationFrame(() => this.tick()); } } return SnekGame;