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.walls = true; this.life = true; this.orange = true; this.world = []; this.score = 0; this.onStart = null; this.onStop = null; this.onEat = null; this.onDie = null; this.onGameOver = null; this.baseLifeCooldown = this.lifeCooldown = 30000; this.orangeCooldown = this.baseOrangeCooldown = 60000; } /** * 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(); if (this.life) this.lifeGenerator(); if (this.orange) this.orangeGenerator(); 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]; let haveEat = true; switch (t.type) { case tiles.APPLE: this.snake.eat(); this.appleGenerator(); this.score++; if (!(this.score % 5) && this.snakeSpeed > 50) { this.snakeSpeed -= 10; } break; case tiles.LIFE: this.lives++; break; case tiles.ORANGE: this.snake.shrink(); break; default: haveEat = false; } if (haveEat && 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(); } /** * Generate an apple on the world */ appleGenerator() { let pos = this.randCoords(); this.world[pos[0]][pos[1]].type = tiles.APPLE; } /** * Handel the live generation */ lifeGenerator() { if (!this.world.find(v => v === tiles.LIFE) && !this.world.find(v => v === tiles.ORANGE)) { if (this.lifeCooldown <= 0) { if (this.lives < 3 && Math.random() < 0.15) { let pos = this.randCoords(); this.world[pos[0]][pos[1]].type = tiles.LIFE; this.lifeCooldown = this.baseLifeCooldown; } } else this.lifeCooldown -= this.snakeSpeed; } } /** * Handel the orange generation */ orangeGenerator() { if (!this.world.find(v => v === tiles.ORANGE) && !this.world.find(v => v === tiles.LIFE)) { if (this.orangeCooldown <= 0) { if (Math.random() < 0.05) { let pos = this.randCoords(); this.world[pos[0]][pos[1]].type = tiles.ORANGE; this.orangeCooldown = this.baseOrangeCooldown; } } else this.orangeCooldown -= this.snakeSpeed; } } /** * 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 * @param {boolean} walls * @param {boolean} life * @param {boolean} orange */ load({size = [15, 15], snakeSize = 4, direction = directions.RIGHT, snakeSpeed = 500, lives = 3, walls = true, life = true, orange = 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 (typeof walls === "boolean") this.walls = walls; else throw new InvalidGameOption("walls"); if (typeof life === "boolean") this.life = life; else throw new InvalidGameOption("life"); if (typeof orange === "boolean") this.orange = orange; else throw new InvalidGameOption("orange"); } } export class InvalidGameOption extends Error { /** * @param {string} name */ constructor(name) { super(`Invalid Game option: ${name}`); } } export class GameOver extends Error { constructor() { super("Game Over"); } }