Snek/src/js/snek.js

248 lines
6 KiB
JavaScript
Raw Normal View History

2020-03-24 12:01:24 +00:00
const assets=require('assets');
2020-03-24 09:46:01 +00:00
2020-03-23 19:11:39 +00:00
const [EMPTY, FOOD, WALL, SNAKE]=Array(4).keys();
const ifNaN=(v, r) => isNaN(v)?r:v;
class SnekGame {
2020-03-25 14:57:20 +00:00
constructor(settings, canvas, rules) {
2020-03-23 19:11:39 +00:00
// build the world
this.dimensions=[...settings.dimensions];
2020-03-25 14:57:20 +00:00
this.world=Array(settings.dimensions[0]);
for(let i=0; i<settings.dimensions[0]; i++) {
this.world[i]=Array(settings.dimensions[1]);
this.world[i].fill(EMPTY);
}
console.log(this);
2020-03-23 19:11:39 +00:00
settings.walls.forEach(([x, y]) => 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=[
2020-03-25 14:57:20 +00:00
ifNaN(settings.snake[0][0]-settings.snake[1][0], 1),
ifNaN(settings.snake[0][1]-settings.snake[1][1], 0)
2020-03-23 19:11:39 +00:00
];
// 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');
2020-03-25 14:57:20 +00:00
// load the custom rules
this.rules=Object.assign({
fruitRegrow: true,
speedIncrease: true,
worldWrap: true,
winCondition: 'none',
scoreSystem: 'fruit'
}, rules, settings);
2020-03-23 19:11:39 +00:00
}
draw() {
// clear the canvas, because it's easier than having to deal with everything
2020-03-25 14:57:20 +00:00
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
2020-03-23 19:11:39 +00:00
// get the cell size and offset
const cellSize=Math.min(
2020-03-25 14:57:20 +00:00
this.canvas.width/this.dimensions[0],
2020-03-23 19:11:39 +00:00
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;
2020-03-25 14:57:20 +00:00
// 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);
2020-03-23 19:11:39 +00:00
// draw our walls
2020-03-24 12:01:24 +00:00
const wall=assets.get('wall');
2020-03-23 19:11:39 +00:00
for(let x=0; x<this.dimensions[0]; x++) {
2020-03-25 14:57:20 +00:00
for(let y=0; y<this.dimensions[1]; y++) {
2020-03-23 19:11:39 +00:00
switch(this.world[x][y]) {
case WALL:
this.ctx.drawImage(
wall,
offsetX+cellSize*x,
offsetY+cellSize*y,
cellSize,
cellSize
);
break;
}
}
}
// draw our snake
2020-03-24 12:01:24 +00:00
const snake=assets.get('snake');
2020-03-23 19:11:39 +00:00
this.ctx.fillStyle=snake.color;
this.ctx.strokeStyle=snake.color;
this.ctx.lineCap=snake.cap;
this.ctx.lineJoin=snake.join;
this.ctx.lineWidth=cellSize*snake.tailSize;
this.ctx.beginPath();
this.ctx.ellipse(
offsetX+cellSize*(this.snake[0][0]+1/2),
offsetY+cellSize*(this.snake[0][1]+1/2),
cellSize/2*snake.headSize,
cellSize/2*snake.headSize,
0,
0,
Math.PI*2
);
this.ctx.fill();
this.ctx.beginPath();
this.snake.forEach(([x, y], i) => {
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();
2020-03-25 14:57:20 +00:00
const fruitScale=Math.sin(ms/400*Math.PI)*.2+1
2020-03-24 12:01:24 +00:00
const fruit=assets.get('fruit');
2020-03-23 19:11:39 +00:00
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;
2020-03-25 14:57:20 +00:00
// 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();
}
}
2020-03-23 19:11:39 +00:00
switch(this.world[head[0]][head[1]]) {
// you hit, you die
case WALL:
case SNAKE:
return this.die();
// you eat, you don't die
2020-03-25 14:57:20 +00:00
case FOOD:
2020-03-23 19:11:39 +00:00
// 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
2020-03-25 14:57:20 +00:00
if(this.rules.fruitRegrow) {
2020-03-23 19:11:39 +00:00
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);
2020-03-25 14:57:20 +00:00
this.world[cell[0]][cell[1]]=FOOD;
2020-03-23 19:11:39 +00:00
}
}
// 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() {
2020-03-25 14:57:20 +00:00
if(!this.playing) return;
2020-03-23 19:11:39 +00:00
if(!this.lastStep) this.lastStep=this.firstStep;
2020-03-25 14:57:20 +00:00
this.draw();
if(this.callback) this.callback();
if(this.lastStep+this.delay<Date.now()) {
this.lastStep+=this.delay;
2020-03-23 19:11:39 +00:00
this.step();
}
requestAnimationFrame(() => 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");
}
2020-03-25 14:57:20 +00:00
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]);
}
2020-03-23 19:11:39 +00:00
start() {
this.firstStep=Date.now();
2020-03-25 14:57:20 +00:00
this.playing=true;
2020-03-23 19:11:39 +00:00
requestAnimationFrame(() => this.tick());
}
}
2020-03-24 09:46:01 +00:00
return SnekGame;