2021-03-18 10:29:12 +01:00
|
|
|
import {Tile, tiles} from "./Tile.js"
|
|
|
|
import {Snake, directions} from "./Snake.js";
|
|
|
|
|
|
|
|
|
2021-03-11 09:39:52 +01:00
|
|
|
export class Game {
|
2021-03-18 10:29:12 +01:00
|
|
|
/**
|
|
|
|
* Generate a new game of Snake
|
|
|
|
* @param {HTMLCanvasElement} canvas
|
|
|
|
*/
|
2021-03-23 10:27:15 +01:00
|
|
|
constructor(canvas) {
|
2021-03-18 10:29:12 +01:00
|
|
|
if (canvas && canvas.nodeName === "CANVAS")
|
|
|
|
this.ctx = canvas.getContext("2d");
|
|
|
|
else
|
|
|
|
throw new InvalidGameOption("canvas");
|
|
|
|
|
2021-03-23 10:27:15 +01:00
|
|
|
this.size = [15, 15];
|
|
|
|
this.direction = directions.RIGHT;
|
|
|
|
this.snakeSpeed = 500;
|
|
|
|
this.lives = 3;
|
2021-03-18 10:29:12 +01:00
|
|
|
|
|
|
|
this.world = [];
|
|
|
|
this.score = 0;
|
2021-03-23 09:23:37 +01:00
|
|
|
this.onStart = null;
|
|
|
|
this.onStop = null;
|
2021-03-23 10:27:15 +01:00
|
|
|
this.onEat = null;
|
|
|
|
this.onDie = null;
|
|
|
|
this.onGameOver = null;
|
2021-03-18 10:29:12 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Init the world with empty tiles
|
|
|
|
*/
|
|
|
|
initWorld() {
|
|
|
|
for (let x = 0; x < this.size[0]; x++)
|
|
|
|
for (let y = 0; y < this.size[1]; y++) {
|
|
|
|
if (!this.world[x])
|
|
|
|
this.world[x] = [];
|
|
|
|
this.world[x][y] = new Tile(x, y, tiles.EMPTY, this);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Init the canvas
|
|
|
|
*/
|
|
|
|
initCanvas() {
|
2021-03-23 10:27:15 +01:00
|
|
|
this.ctx.canvas.ownerDocument.addEventListener("keydown", ev => {
|
2021-03-18 10:29:12 +01:00
|
|
|
switch (ev.key) {
|
|
|
|
case "ArrowUp":
|
|
|
|
if (this.lastDirection !== directions.DOWN)
|
|
|
|
this.direction = directions.UP;
|
|
|
|
break;
|
|
|
|
case "ArrowRight":
|
|
|
|
if (this.lastDirection !== directions.LEFT)
|
|
|
|
this.direction = directions.RIGHT;
|
|
|
|
break;
|
|
|
|
case "ArrowDown":
|
|
|
|
if (this.lastDirection !== directions.UP)
|
|
|
|
this.direction = directions.DOWN;
|
|
|
|
break;
|
|
|
|
case "ArrowLeft":
|
|
|
|
if (this.lastDirection !== directions.RIGHT)
|
|
|
|
this.direction = directions.LEFT;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Draw the grid
|
|
|
|
*/
|
|
|
|
drawGrid() {
|
|
|
|
for (const l of this.world)
|
|
|
|
for (const t of l)
|
|
|
|
t.draw()
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Start the party
|
|
|
|
*/
|
|
|
|
start() {
|
2021-03-23 09:23:37 +01:00
|
|
|
if (this.onStart && typeof this.onStart === "function")
|
|
|
|
this.onStart();
|
|
|
|
|
|
|
|
this.direction = this.lastDirection = this.startDirection;
|
2021-03-18 10:29:12 +01:00
|
|
|
this.snake = new Snake({startPos: this.size.map(s => Math.floor(s/2))});
|
|
|
|
|
|
|
|
this.initWorld();
|
|
|
|
this.updateSnake();
|
|
|
|
this.drawGrid();
|
|
|
|
this.initCanvas();
|
2021-03-25 10:13:26 +01:00
|
|
|
this.appleGenerator();
|
2021-03-18 10:29:12 +01:00
|
|
|
|
|
|
|
this.mainInterval = setInterval(() => this.main(), this.snakeSpeed);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Stop the party
|
|
|
|
*/
|
|
|
|
stop() {
|
|
|
|
clearInterval(this.mainInterval);
|
|
|
|
|
|
|
|
// Clear the grid
|
|
|
|
this.ctx.globalCompositeOperation = "destination-out";
|
|
|
|
this.ctx.fillRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
|
|
|
|
this.ctx.globalCompositeOperation = "source-over";
|
2021-03-23 09:23:37 +01:00
|
|
|
if (this.onStop && typeof this.onStop === "function")
|
|
|
|
this.onStop();
|
2021-03-18 10:29:12 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Restart the party
|
|
|
|
*/
|
|
|
|
restart() {
|
|
|
|
this.stop();
|
|
|
|
this.start();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The main loop pf the game
|
|
|
|
*/
|
|
|
|
main() {
|
|
|
|
try {
|
|
|
|
this.moveSnake();
|
|
|
|
this.drawGrid();
|
|
|
|
} catch (err) {
|
|
|
|
if (err instanceof GameOver) {
|
|
|
|
this.lives--;
|
|
|
|
if (this.lives <= 0) {
|
2021-03-23 10:27:15 +01:00
|
|
|
if (this.onGameOver && typeof this.onGameOver === "function")
|
|
|
|
this.onGameOver(this.score);
|
2021-03-18 10:29:12 +01:00
|
|
|
this.stop();
|
2021-03-25 09:48:57 +01:00
|
|
|
this.score = 0;
|
2021-03-23 10:27:15 +01:00
|
|
|
} else {
|
|
|
|
if (this.onDie && typeof this.onDie === "function")
|
|
|
|
this.onDie(this.lives);
|
2021-03-18 10:29:12 +01:00
|
|
|
this.restart();
|
2021-03-23 10:27:15 +01:00
|
|
|
}
|
2021-03-18 10:29:12 +01:00
|
|
|
} else {
|
|
|
|
console.error(err);
|
|
|
|
alert("An error occurred !");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Update the snake on the world and check for collision
|
|
|
|
*/
|
|
|
|
updateSnake() {
|
|
|
|
for (const [x, y] of this.snake.body)
|
|
|
|
if (!(x in this.world) || !(y in this.world[x]))
|
|
|
|
throw new GameOver();
|
|
|
|
else {
|
|
|
|
const t = this.world[x][y];
|
|
|
|
if (t.type === tiles.APPLE) {
|
|
|
|
this.snake.eat();
|
2021-03-25 10:13:26 +01:00
|
|
|
this.appleGenerator();
|
2021-03-18 10:29:12 +01:00
|
|
|
this.score++;
|
2021-03-23 10:27:15 +01:00
|
|
|
if (this.onEat && typeof this.onEat === "function")
|
|
|
|
this.onEat(this.score);
|
2021-03-18 10:29:12 +01:00
|
|
|
}
|
|
|
|
t.type = tiles.SNAKE;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Move the snake
|
|
|
|
*/
|
|
|
|
moveSnake() {
|
|
|
|
this.snake.move(this.direction);
|
|
|
|
this.lastDirection = this.direction;
|
|
|
|
|
|
|
|
for (const l of this.world)
|
|
|
|
for (const t of l)
|
|
|
|
if (t.type === tiles.SNAKE)
|
|
|
|
t.type = tiles.EMPTY;
|
|
|
|
|
|
|
|
this.updateSnake();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Main loop fir apple generation
|
|
|
|
*/
|
|
|
|
appleGenerator() {
|
2021-03-25 10:13:26 +01:00
|
|
|
let pos = this.randCoords();
|
2021-03-18 10:29:12 +01:00
|
|
|
|
2021-03-25 10:13:26 +01:00
|
|
|
this.world[pos[0]][pos[1]].type = tiles.APPLE;
|
2021-03-18 10:29:12 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Generate random coordinates of empty tiles
|
|
|
|
* @returns {[int, int]}
|
|
|
|
*/
|
|
|
|
randCoords() {
|
|
|
|
let pos;
|
|
|
|
|
|
|
|
do {
|
|
|
|
pos = [];
|
|
|
|
|
|
|
|
for (const s of this.size)
|
|
|
|
pos.push(Math.floor(Math.random() * s))
|
|
|
|
} while (this.world[pos[0]][pos[1]].type !== tiles.EMPTY);
|
|
|
|
|
|
|
|
return pos;
|
|
|
|
}
|
2021-03-23 10:27:15 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Generate a new game of Snake
|
|
|
|
* @param {[int, int]} size
|
|
|
|
* @param {directions} direction
|
|
|
|
* @param {int} snakeSpeed
|
|
|
|
* @param {int} appleSpeed
|
|
|
|
* @param {int} lives
|
|
|
|
*/
|
2021-03-25 10:13:26 +01:00
|
|
|
load({size = [15, 15], direction = directions.RIGHT, snakeSpeed = 500, lives = 3} = {}) {
|
2021-03-23 10:27:15 +01:00
|
|
|
if (size && Array.isArray(size) && size.length === 2 && size.filter(s => typeof s === "number" && s > 0 && s % 1 === 0).length === size.length)
|
|
|
|
this.size = size;
|
|
|
|
else
|
|
|
|
throw new InvalidGameOption("size");
|
|
|
|
|
|
|
|
if (direction && Object.values(directions).find(([x,y]) => direction[0] === x && direction[1] === y))
|
|
|
|
this.direction = this.startDirection = this.lastDirection = direction;
|
|
|
|
else
|
|
|
|
throw new InvalidGameOption("direction");
|
|
|
|
|
|
|
|
if (snakeSpeed && typeof snakeSpeed === "number" && snakeSpeed > 0 && snakeSpeed % 1 === 0)
|
|
|
|
this.snakeSpeed = snakeSpeed;
|
|
|
|
else
|
|
|
|
throw new InvalidGameOption("snakeSpeed");
|
|
|
|
|
|
|
|
if (lives && typeof lives === "number" && lives > 0 && lives % 1 === 0)
|
|
|
|
this.lives = lives;
|
|
|
|
else
|
|
|
|
throw new InvalidGameOption("lives");
|
|
|
|
}
|
2021-03-18 10:29:12 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
export class InvalidGameOption extends Error {
|
|
|
|
/**
|
|
|
|
* @param {string} name
|
|
|
|
*/
|
|
|
|
constructor(name) {
|
|
|
|
super(`Invalid Game option: ${name}`);
|
|
|
|
}
|
|
|
|
}
|
2021-03-11 09:39:52 +01:00
|
|
|
|
2021-03-18 10:29:12 +01:00
|
|
|
export class GameOver extends Error {
|
|
|
|
constructor() {
|
|
|
|
super("Game Over");
|
2021-03-11 09:39:52 +01:00
|
|
|
}
|
|
|
|
}
|