import {Tile, tiles} from "./Tile.js" import {Snake, directions} from "./Snake.js"; function mod_floor(a, n) { return ((a % n) + n) % n; } export class Game { /** * Generate a new game of Snake * @param {HTMLCanvasElement} canvas */ constructor(canvas) { if (canvas && canvas.nodeName === "CANVAS") this.ctx = canvas.getContext("2d"); else throw new InvalidGameOption("canvas"); this.size = [15, 15]; this.snakeSize = 4; this.direction = directions.RIGHT; this.snakeSpeed = this.baseSnakeSpeed = 500; this.lives = 3; this.world = []; this.score = 0; this.onStart = null; this.onStop = null; this.onEat = null; this.onDie = null; this.onGameOver = 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("keydown", 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.snakeSpeed = this.baseSnakeSpeed; this.snake = new Snake({startPos: this.size.map(s => Math.floor(s/2)), size: this.snakeSize, startDirection: this.startDirection}); this.initWorld(); this.updateSnake(); this.drawGrid(); this.initCanvas(); this.appleGenerator(); setTimeout(() => this.main(), this.snakeSpeed); } /** * Stop the party */ stop() { this.mainBreak = true; // 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) { if (this.onGameOver && typeof this.onGameOver === "function") this.onGameOver(this.score); this.stop(); this.score = 0; } else { if (this.onDie && typeof this.onDie === "function") this.onDie(this.lives); this.restart(); } } else { console.error(err); alert("An error occurred !"); } } if (!this.mainBreak) setTimeout(() => this.main(), this.snakeSpeed); else this.mainBreak = false; } /** * Update the snake on the world and check for collision */ updateSnake() { this.snake.body.forEach(([x,y], i) => { if (!(x in this.world) || !(y in this.world[x])) if (this.walls) throw new GameOver(); else this.snake.body[i] = [mod_floor(x, this.size[0]), mod_floor(y, this.size[1])]; else { const t = this.world[x][y]; if (t.type === tiles.APPLE) { this.snake.eat(); this.appleGenerator(); this.score++; if (!(this.score % 5) && this.snakeSpeed > 50) { this.snakeSpeed -= 10; } if (this.onEat && typeof this.onEat === "function") this.onEat(this.score); } if (!i) t.type = tiles.HEAD; else if (i === this.snake.body.length-1) t.type = tiles.TAIL; else t.type = tiles.BODY; } }); } /** * 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.HEAD || t.type === tiles.BODY || t.type === tiles.TAIL) t.type = tiles.EMPTY; this.updateSnake(); } /** * Main loop fir apple generation */ appleGenerator() { let pos = this.randCoords(); this.world[pos[0]][pos[1]].type = tiles.APPLE; } /** * 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; } /** * Generate a new game of Snake * @param {[int, int]} size * @param {directions} direction * @param {int} snakeSpeed * @param {int} appleSpeed * @param {int} lives */ load({size = [15, 15], snakeSize = 4, direction = directions.RIGHT, snakeSpeed = 500, lives = 3, walls = true} = {}) { 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 (snakeSize && typeof snakeSize === "number" && snakeSize > 0 && snakeSize % 1 === 0) this.snakeSize = snakeSize; else throw new InvalidGameOption("snakeSize"); 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 = this.baseSnakeSpeed = snakeSpeed; else throw new InvalidGameOption("snakeSpeed"); if (lives && typeof lives === "number" && lives > 0 && lives % 1 === 0) this.lives = lives; else throw new InvalidGameOption("lives"); if (walls && typeof walls === "boolean") this.walls = walls } } export class InvalidGameOption extends Error { /** * @param {string} name */ constructor(name) { super(`Invalid Game option: ${name}`); } } export class GameOver extends Error { constructor() { super("Game Over"); } }