diff --git a/index.html b/index.html
index e6f551d..2eb9169 100644
--- a/index.html
+++ b/index.html
@@ -9,8 +9,18 @@
KyFlo Snake
-
-
+
+
diff --git a/sources/css/style.css b/sources/css/style.css
index a252740..41e5d9d 100644
--- a/sources/css/style.css
+++ b/sources/css/style.css
@@ -1,17 +1,44 @@
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');
body{
- height: 100%;
+ height: 100vh;
width: 100%;
margin: 0;
padding: 0;
background-image: url("../images/yargon-kerman-webp-net-gifmaker-39.gif");
background-repeat: no-repeat;
- background-size: 100%;
+ background-size: cover;
+ backdrop-filter: blur(4px);
}
h1{
font-family: 'Press Start 2P', cursive;
text-align: center;
color: yellow;
+ margin: 0;
}
+
+.menu>div{
+ background-color: black;
+}
+
+.menu button{
+ display: block;
+ text-align: center;
+ background-color: transparent;
+ border: 3px double #FF4294;
+ margin-bottom: 10px;
+ margin-top: 10px;
+ padding: 10px;
+ font-size: 20px;
+ color: #FF4294;
+}
+
+#canvas {
+ height: 100%;
+ width: 100%;
+ right: 0;
+ top: 0;
+ position: fixed;
+ background: white;
+}
\ No newline at end of file
diff --git a/sources/js/Game.js b/sources/js/Game.js
index 90a7db2..85b7ea3 100644
--- a/sources/js/Game.js
+++ b/sources/js/Game.js
@@ -1,5 +1,231 @@
-export class Game {
- constructor(options) {
+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.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;
+ }
+
+ /**
+ * 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.width = window.innerWidth;
+ this.ctx.canvas.height = window.innerHeight;
+
+ 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() {
+ 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";
+ }
+
+ /**
+ * 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");
}
}
diff --git a/sources/js/Snake.js b/sources/js/Snake.js
index d454848..40192d3 100644
--- a/sources/js/Snake.js
+++ b/sources/js/Snake.js
@@ -1,6 +1,70 @@
-class Snake {
- constructor(startPos = [0,0], size = 4) {
- this.size = size;
- this.body = [startPos, startPos*size]
+import {GameOver} from "./Game.js";
+
+export class Snake {
+ /**
+ * The snake of the game
+ * @param options
+ */
+ constructor({startPos = [8,8], size = 4, startDirection = directions.RIGHT} = {}) {
+ if (startPos && Array.isArray(startPos) && startPos.length === 2 && startPos.filter(s => typeof s === "number" && s > 0 && s % 1 === 0).length === startPos.length)
+ this.body = [startPos];
+ else
+ throw new InvalidSnakeOption("startPos");
+
+ if (size && typeof size === "number" && size > 0 && size % 1 === 0)
+ this.size = size;
+ else
+ throw new InvalidSnakeOption("size");
+
+ // Generate snake body
+ for (let s = 1; s < this.size; s++) {
+ const pos = this.body[this.body.length - 1].map((n, i) => n - startDirection[i]);
+ if (pos.find(v => v < 0))
+ throw new InvalidSnakeOption("startPos too small");
+ this.body.push(pos);
+ }
+
+ this.eating = false;
}
-}
\ No newline at end of file
+
+ /**
+ * Move the snake body
+ * @param {directions} direction
+ */
+ move(direction) {
+ if (!this.eating)
+ this.body.pop();
+ else
+ this.eating = false;
+
+ const pos = this.body[0].map((n, i) => n + direction[i]);
+
+ if (this.body.find(([x,y]) => pos[0] === x && pos[1] === y))
+ throw new GameOver();
+
+ this.body.unshift(pos);
+ }
+
+ /**
+ * Enable eating
+ */
+ eat() {
+ this.eating = true;
+ }
+}
+
+export const directions = {
+ UP: [0, -1],
+ RIGHT: [1, 0],
+ DOWN: [0, 1],
+ LEFT: [-1, 0]
+};
+
+export class InvalidSnakeOption extends Error {
+ /**
+ * @param {string} name
+ */
+ constructor(name) {
+ super(`Invalid Snake option: ${name}`);
+ }
+}
diff --git a/sources/js/Tile.js b/sources/js/Tile.js
new file mode 100644
index 0000000..bdd0837
--- /dev/null
+++ b/sources/js/Tile.js
@@ -0,0 +1,94 @@
+import {Game} from "./Game.js";
+
+export class Tile {
+ /**
+ * A tile of the game grid
+ * @param {int} x
+ * @param {int} y
+ * @param {tiles} type
+ * @param {Game} game
+ */
+ constructor(x, y, type = tiles.EMPTY, game) {
+ if (typeof x === "number" && x >= 0 && x % 1 === 0)
+ this.x = x;
+ else
+ throw new InvalidTileOption("x");
+
+ if (typeof y === "number" && y >= 0 && y % 1 === 0)
+ this.y = y;
+ else
+ throw new InvalidTileOption("y");
+
+ if (type && Object.values(tiles).find(t => t === type))
+ this.type = type;
+ else
+ throw new InvalidTileOption("type");
+
+ if (game && game instanceof Game)
+ this.game = game;
+ else
+ throw new InvalidTileOption("game");
+ }
+
+ /**
+ * Draw the tile on the grid
+ */
+ draw() {
+ const canvasPos = this.getCanvasPos(), size = this.getSize();
+
+ this.game.ctx.beginPath();
+
+ this.game.ctx.globalCompositeOperation = "destination-out";
+ this.game.ctx.fillRect(...canvasPos, ...size);
+ this.game.ctx.globalCompositeOperation = "source-over";
+
+ switch (this.type) {
+ case tiles.EMPTY:
+ this.game.ctx.strokeStyle = "#000000";
+ this.game.ctx.rect(...canvasPos, ...size);
+ break;
+ case tiles.APPLE:
+ this.game.ctx.fillStyle = "#ff0000";
+ this.game.ctx.fillRect(...canvasPos, ...size);
+ break;
+ case tiles.SNAKE:
+ this.game.ctx.fillStyle = "#00ff5f";
+ this.game.ctx.fillRect(...canvasPos, ...size);
+ break;
+ }
+
+ this.game.ctx.stroke();
+ }
+
+ /**
+ * Get the tile position on the canvas
+ * @returns {[int, int]}
+ */
+ getCanvasPos() {
+ return [this.getSize()[0]*this.x, this.getSize()[1]*this.y]
+ }
+
+ /**
+ * Get the tile size on the canvas
+ * @returns {[int, int]}
+ */
+ getSize() {
+ const s = Math.min(Math.round(this.game.ctx.canvas.width/this.game.size[0]), Math.round(this.game.ctx.canvas.height/this.game.size[1]));
+ return [s, s]
+ }
+}
+
+export class InvalidTileOption extends Error {
+ /**
+ * @param {string} name
+ */
+ constructor(name) {
+ super(`Invalid Tile option: ${name}`);
+ }
+}
+
+export const tiles = {
+ EMPTY: "empty",
+ APPLE: "apple",
+ SNAKE: "snake"
+};
diff --git a/sources/js/index.js b/sources/js/index.js
index 9c94b92..53a4e9f 100644
--- a/sources/js/index.js
+++ b/sources/js/index.js
@@ -1,6 +1,8 @@
-import { Game } from "/sources/js/Game"
+import { Game } from "./Game.js"
+const game = new Game(document.getElementById("canvas"), {snakeSpeed: 200});
document.addEventListener('DOMContentLoaded', function() {
M.AutoInit();
+ game.start();
});