A "simple" Snake, done as my final JS class project back in DUT
https://snek.s.codinget.me
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
194 lines
4.6 KiB
194 lines
4.6 KiB
5 years ago
|
const [EMPTY, FOOD, WALL, SNAKE]=Array(4).keys();
|
||
|
|
||
|
const ifNaN=(v, r) => isNaN(v)?r:v;
|
||
|
|
||
|
class SnekGame {
|
||
|
constructor(settings, canvas) {
|
||
|
// build the world
|
||
|
this.dimensions=[...settings.dimensions];
|
||
|
this.world=Array(settings.dimensions[0])
|
||
|
.forEach((_, i, a) => a[i]=Array(settings.dimensions[1]).fill(EMPTY));
|
||
|
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=[
|
||
|
ifNaN(settings.snake[1][0]-settings.snake[0][0], 1),
|
||
|
ifNaN(settings.snake[1][1]-settings.snake[0][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');
|
||
|
}
|
||
|
|
||
|
draw() {
|
||
|
// clear the canvas, because it's easier than having to deal with everything
|
||
|
this.ctx.clearRect(0, 0, this.canvas.with, this.canvas.height);
|
||
|
|
||
|
// get the cell size and offset
|
||
|
const cellSize=Math.min(
|
||
|
this.canvas.with/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 our walls
|
||
|
const wall=Assets.get('wall');
|
||
|
for(let x=0; x<this.dimensions[0]; x++) {
|
||
|
for(let y=0; x<this.dimensions[1]; y++) {
|
||
|
switch(this.world[x][y]) {
|
||
|
case WALL:
|
||
|
this.ctx.drawImage(
|
||
|
wall,
|
||
|
offsetX+cellSize*x,
|
||
|
offsetY+cellSize*y,
|
||
|
cellSize,
|
||
|
cellSize
|
||
|
);
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// draw our snake
|
||
|
const snake=Assets.get('snake');
|
||
|
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();
|
||
|
const fruitScale=Math.sin(ms/1000*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;
|
||
|
|
||
|
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 FRUIT:
|
||
|
// 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.regrowFruits) {
|
||
|
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]]=FRUIT;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// 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.lastStep) this.lastStep=this.firstStep;
|
||
|
if(this.lastStep+delay<Date.now()) {
|
||
|
this.lastStep+=delay;
|
||
|
this.step();
|
||
|
}
|
||
|
this.draw();
|
||
|
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");
|
||
|
}
|
||
|
|
||
|
start() {
|
||
|
this.firstStep=Date.now();
|
||
|
requestAnimationFrame(() => this.tick());
|
||
|
}
|
||
|
}
|