const [EMPTY, FOOD, WALL, SNAKE]=Array(4).keys(); const ifNaN=(v, r) => isNaN(v)?r:v; class SnekGame { constructor(settings, canvas, rules) { // setup the delay this.delay=settings.delay; // world is given in the level if(settings.world) { // explicitly // convert the world this.world=Array(settings.world[0].length); for(let x=0; x { switch(settings.world[y][x]) { case ' ': return EMPTY; case 'f': return FOOD; case 'w': return WALL; } })(); } } // // extract the dimensions this.dimensions=[this.world.length, this.world[0].length]; // extract the fruits this.fruits=[]; this.world .forEach((l, x) => l.forEach( (c, y) => { if(c==FOOD) this.fruits.push([x, y]); } )); } else { // dimension and objects // get the dimensions this.dimensions=[...settings.dimensions]; // build an empty world this.world=Array(settings.dimensions[0]); for(let i=0; i this.world[x][y]=WALL); // add the food settings.food.forEach(([x, y]) => this.world[x][y]=FOOD); this.fruits=[...settings.food]; } // add the snake to the world settings.snake.forEach(([x, y]) => this.world[x][y]=SNAKE); // 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) ]; this.lastDirection=this.direction // store the snake this.snake=[...settings.snake]; // get our canvas, like, if we want to actually draw this.canvas=canvas; this.ctx=canvas.getContext('2d'); // load the custom rules this.rules=Object.assign({ fruitRegrow: true, speedIncrease: true, worldWrap: true, winCondition: 'none', scoreSystem: 'fruit', netPlay: false, autoSizeGrow: false, autoSpeedIncrease: false }, rules, settings.rules || {}); } get playTime() { return Date.now()-this.firstStep; } draw() { const assets=require('assets'); // 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) ); if(i!=0 && Math.hypot(x-a[i-1][0], y-a[i-1][1])>1) { this.ctx.lineWidth=cellSize*snake.tailWrapSize; } else { this.ctx.lineWidth=cellSize*snake.tailSize; } this.ctx.stroke(); this.ctx.beginPath() this.ctx.moveTo( 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*y+(1-fruitScale)*cellSize/2, cellSize*fruitScale, cellSize*fruitScale ); }); } step() { this.tickId++; this.lastDirection=this.direction; // 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=this.fruits.filter( ([x, y]) => !(x==head[0] && y==head[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; } if(this.rules.speedIncrease) { this.delay*=this.rules.speedMultiplier; if(this.delay50 && this.tickId%this.rules.autoSpeadIncreaseTicks==0) this.delay--; } // automatic size grow if(this.rules.autoSizeGrow) { if(this.tickId%this.rules.autoSizeGrowTicks==0) this.snake.push(tail); } // victory condition if(this.rules.winCondition=='fruit') { if(!this.fruits.length) return this.win(); } if(this.rules.winCondition=='time') { if(this.playTime>=this.rules.gameDuration) return this.win(); } if(this.rules.winCondition=='score') { if(this.score>=this.rules.scoreObjective) return this.win(); } } tick() { if(!this.playing) return; if(!this.lastStep) this.lastStep=this.firstStep; this.draw(); if(this.callback) this.callback('tick'); if(this.lastStep+this.delay this.tick()); } win() { this.playing=false; this.endPlayTime=this.playTime; if(this.callback) this.callback('win'); } die() { this.playing=false; this.endPlayTime=this.playTime; if(this.callback) this.callback('die'); } handleInputs(inputs) { const trySet=(dir) => { if(!dir.every((e, i) => e==this.lastDirection[i] || e==-this.lastDirection[i])) { this.direction=dir; return true; } } Object .keys(inputs) .forEach(k => { let v=inputs[k]; if(v===true) v=5; v--; if(!v) delete inputs[k]; else inputs[k]=v; }); if(inputs.left && trySet([-1, 0])) return delete inputs.left; else if(inputs.right && trySet([ 1, 0])) return delete inputs.right; else if(inputs.up && trySet([ 0,-1])) return delete inputs.up; else if(inputs.down && trySet([ 0, 1])) return delete inputs.down; } start() { this.firstStep=Date.now(); this.tickId=0; this.playing=true; requestAnimationFrame(() => this.tick()); } } module.exports=SnekGame; return SnekGame;