import {Tile, tiles} from "./Tile.js" import {Snake, directions} from "./Snake.js"; export class Game { /** * Generate a new game of Snake * @param {HTMLCanvasElement} canvas * @param {[int, int]} size * @param {directions} direction * @param {int} snakeSpeed * @param {int} appleSpeed * @param {int} lives */ constructor(canvas, {size = [15, 15], direction = directions.RIGHT, snakeSpeed = 500, appleSpeed = 5000, lives = 3} = {}) { if (canvas && canvas.nodeName === "CANVAS") this.ctx = canvas.getContext("2d"); else throw new InvalidGameOption("canvas"); 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 (appleSpeed && typeof appleSpeed === "number" && appleSpeed > 0 && appleSpeed % 1 === 0) this.appleSpeed = appleSpeed; else throw new InvalidGameOption("appleSpeed"); this.world = []; this.apple = false; this.score = 0; this.lives = lives; this.onStart = null; this.onStop = null; } /** * 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() { this.ctx.canvas.ownerDocument.addEventListener("keyup", ev => { 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() { if (this.onStart && typeof this.onStart === "function") this.onStart(); this.direction = this.lastDirection = this.startDirection; this.snake = new Snake({startPos: this.size.map(s => Math.floor(s/2))}); this.initWorld(); this.updateSnake(); this.drawGrid(); this.initCanvas(); this.mainInterval = setInterval(() => this.main(), this.snakeSpeed); this.appleInterval = setInterval(() => this.appleGenerator(), this.appleSpeed); } /** * Stop the party */ stop() { clearInterval(this.mainInterval); clearInterval(this.appleInterval); // 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"; if (this.onStop && typeof this.onStop === "function") this.onStop(); } /** * 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) { alert(`Game over !\nYour score is: ${this.score}`); this.stop(); } else this.restart(); } 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(); this.apple = false; this.score++; } 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() { if (!this.apple) { let pos = this.randCoords(); this.world[pos[0]][pos[1]].type = tiles.APPLE; this.apple = true; } } /** * 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; } } export class InvalidGameOption extends Error { /** * @param {string} name */ constructor(name) { super(`Invalid Game option: ${name}`); } } export class GameOver extends Error { constructor() { super("Game Over"); } }