1
0
Fork 0
This repository has been archived on 2024-02-17. You can view files and clone it, but cannot push or open issues or pull requests.
Snake/sources/js/Game.js

334 lines
9.6 KiB
JavaScript
Raw Normal View History

2021-03-18 10:29:12 +01:00
import {Tile, tiles} from "./Tile.js"
import {Snake, directions} from "./Snake.js";
2021-03-26 23:50:47 +01:00
function mod_floor(a, n) {
return ((a % n) + n) % n;
}
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
*/
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");
this.size = [15, 15];
2021-03-27 00:02:54 +01:00
this.snakeSize = 4;
this.direction = directions.RIGHT;
this.snakeSpeed = this.baseSnakeSpeed = 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;
this.onEat = null;
this.onDie = null;
this.onGameOver = null;
2021-03-30 09:05:38 +02:00
this.baseLifeCooldown = this.lifeCooldown = 30000;
2021-03-30 09:18:11 +02:00
this.orangeCooldown = this.baseOrangeCooldown = 60000;
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() {
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;
this.snakeSpeed = this.baseSnakeSpeed;
2021-03-27 00:02:54 +01:00
this.snake = new Snake({startPos: this.size.map(s => Math.floor(s/2)), size: this.snakeSize, startDirection: this.startDirection});
2021-03-18 10:29:12 +01:00
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
setTimeout(() => this.main(), this.snakeSpeed);
2021-03-18 10:29:12 +01:00
}
/**
* Stop the party
*/
stop() {
this.mainBreak = true;
2021-03-18 10:29:12 +01:00
// 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();
2021-03-30 09:05:38 +02:00
this.lifeGenerator();
2021-03-30 09:18:11 +02:00
this.orangeGenerator();
2021-03-18 10:29:12 +01:00
this.drawGrid();
2021-03-30 09:05:38 +02:00
2021-03-18 10:29:12 +01:00
} catch (err) {
if (err instanceof GameOver) {
this.lives--;
if (this.lives <= 0) {
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;
} else {
if (this.onDie && typeof this.onDie === "function")
this.onDie(this.lives);
2021-03-18 10:29:12 +01:00
this.restart();
}
2021-03-18 10:29:12 +01:00
} else {
console.error(err);
alert("An error occurred !");
}
}
if (!this.mainBreak)
setTimeout(() => this.main(), this.snakeSpeed);
else
this.mainBreak = false;
2021-03-18 10:29:12 +01:00
}
/**
* Update the snake on the world and check for collision
*/
updateSnake() {
2021-03-27 00:02:54 +01:00
this.snake.body.forEach(([x,y], i) => {
2021-03-18 10:29:12 +01:00
if (!(x in this.world) || !(y in this.world[x]))
2021-03-26 23:50:47 +01:00
if (this.walls)
throw new GameOver();
else
this.snake.body[i] = [mod_floor(x, this.size[0]), mod_floor(y, this.size[1])];
2021-03-18 10:29:12 +01:00
else {
const t = this.world[x][y];
2021-03-30 09:05:38 +02:00
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;
2021-03-30 09:18:11 +02:00
case tiles.ORANGE:
this.snake.shrink();
break;
2021-03-30 09:05:38 +02:00
default:
haveEat = false;
2021-03-18 10:29:12 +01:00
}
2021-03-30 09:05:38 +02:00
if (haveEat && this.onEat && typeof this.onEat === "function")
this.onEat(this.score);
2021-03-27 12:21:55 +01:00
if (!i)
t.type = tiles.HEAD;
else if (i === this.snake.body.length-1)
t.type = tiles.TAIL;
else
t.type = tiles.BODY;
2021-03-18 10:29:12 +01:00
}
2021-03-27 00:02:54 +01:00
});
2021-03-18 10:29:12 +01:00
}
/**
* Move the snake
*/
moveSnake() {
this.snake.move(this.direction);
this.lastDirection = this.direction;
for (const l of this.world)
for (const t of l)
2021-03-27 12:21:55 +01:00
if (t.type === tiles.HEAD || t.type === tiles.BODY || t.type === tiles.TAIL)
2021-03-18 10:29:12 +01:00
t.type = tiles.EMPTY;
this.updateSnake();
}
/**
2021-03-30 09:05:38 +02:00
* Generate an apple on the world
2021-03-18 10:29:12 +01:00
*/
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
}
2021-03-30 09:05:38 +02:00
/**
* Handel the live generation
*/
lifeGenerator() {
if (!this.world.find(v => v === tiles.LIFE)) {
if (this.lifeCooldown <= 0) {
2021-03-30 09:18:11 +02:00
if (this.lives < 3 && Math.random() < 0.15) {
2021-03-30 09:05:38 +02:00
let pos = this.randCoords();
this.world[pos[0]][pos[1]].type = tiles.LIFE;
this.lifeCooldown = this.baseLifeCooldown;
}
} else
this.lifeCooldown -= this.snakeSpeed;
}
}
2021-03-30 09:18:11 +02:00
/**
* Handel the orange generation
*/
orangeGenerator() {
if (!this.world.find(v => v === tiles.ORANGE)) {
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;
}
}
2021-03-30 09:05:38 +02:00
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;
}
/**
* Generate a new game of Snake
* @param {[int, int]} size
* @param {directions} direction
* @param {int} snakeSpeed
* @param {int} appleSpeed
* @param {int} lives
*/
2021-03-27 00:02:54 +01:00
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");
2021-03-27 00:02:54 +01:00
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");
2021-03-26 23:50:47 +01:00
if (walls && typeof walls === "boolean")
this.walls = walls
}
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
}
}