1
0
Fork 0

Add snake game

This commit is contained in:
Ethanell 2021-03-18 10:29:12 +01:00
parent f9b86b3dab
commit 16da9da3c3
6 changed files with 435 additions and 12 deletions

View file

@ -9,8 +9,18 @@
</head>
<body>
<h1>KyFlo Snake</h1>
<canvas></canvas>
<div class="row menu">
<div class="col s6 offset-s3">
<div class="col s10 offset-s1 input-field">
<input type="text" id="nickname">
<label for="nickname">Nickname :</label>
</div>
<button class="col s8 offset-s2">Level 1</button>
<button class="col s8 offset-s2">Level 2</button>
<button class="col s8 offset-s2">Level 3</button>
</div>
</div>
<canvas id="canvas"></canvas>
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script>
<script type="module" src="sources/js/index.js"></script>

View file

@ -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;
}

View file

@ -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");
}
}

View file

@ -1,6 +1,70 @@
class Snake {
constructor(startPos = [0,0], size = 4) {
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;
this.body = [startPos, startPos*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;
}
/**
* 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}`);
}
}

94
sources/js/Tile.js Normal file
View file

@ -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"
};

View file

@ -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();
});