body {
padding: 0;
margin: 0;
background: #3FA8C6;
color: #fff;
font-family: 'Doppio One', sans-serif;
text-shadow: 0 1px 0 rgba(0,0,0,.3);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
.container {
width: 80%;
margin: 0 auto;
//border : 1px solid;
}
#game {
background: rgba(0,0,0,.1);
border : 1px lightgray solid;
float: left;
margin-top: 2em
}
#highscore {
position: relative;
overflow: hidden;
padding-bottom: 1em;
padding-left: 40px;
}
h1, h2, h3, h4, h5, h6 {
letter-spacing: -0.03em;
font-size: 2em;
}
h1 {
margin: 0.667em 0 0;
//padding-left: 0.5em;
text-align: left;
}
h2 {
font-size: 1.5em;
}
.bubble {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.1);
padding: 0.667em 1em;
position: relative;
}
.bubble:after {
content: "";
position: absolute;
width: 0;
height: 0;
border-top: 20px solid transparent;
border-bottom: 20px solid transparent;
border-right: 20px solid white;
border-right-color: inherit;
top: 50px;
left: -20px;
}
#highscore ol {
margin: 0;
padding: 0;
//overflow: hidden;
list-style: none;
counter-reset: item;
}
#highscore li {
margin: 1.5% 1.5%;
background: rgba(0,0,0,.1);
border-color: rgba(0,0,0,.1);
//border-radius: 2em;
}
#highscore li:before {
//display: block;
width: 2em;
height: 2em;
padding: 0.4em;
margin: 0.667em auto 1em;
content: counter(item);
counter-increment: item;
line-height: 2;
text-align: center;
background: rgba(0,0,0,.1);
border-radius: 2em;
box-shadow: inset 0 0 1em rgba(0,0,0,.1), 0 2px 2px rgba(255,255,255,.1);
}
.player {
display: inline;
padding: 0.3em;
//width: 100%;
//background: rgba(0,0,0,.1);
//border-color: rgba(0,0,0,.1);
}
.playerscore {
float: right;
padding : 0.3em;
}
#bubble-save-score {
display: none;
//background-color: #5cb85c;
margin-bottom: 30px;
}
.input {
font-size: 1em;
}
.button {
background: rgba(0,0,0,.1);
}
//////////////////////////////////
@media screen and (max-width: 1200px) {
.container {
width: 90%;
//margin: 0;
}
}
@media screen and (max-width: 900px) {
.container {
width: auto;
//margin: 0;
}
#game {
float: none;
margin-left: 40px;
}
.bubble:after {
display: none;
}
#highscore {
padding-right: 40px;
}
// On a un canvas de 641 * 641 px divisé en une grille de 40*40 cases
// Chaque case de la grille mesure 15*15 px
// On laisse 1px de vide entre chaque case
// ============================= TODO ==============================
// [ ] Donner une durée de vie aux pommes, pour quelle s'effacent
// automatiquement si elles n'ont pas était prises
//
// [X] Sauvegarder le meilleur score du joueur
// [ ] Sauvegarderle meilleur score et le pseudo dans un cookie en cas de reload de la page
//
// [X] Restart game
//
// [X] Enregistrement des bestScores sur Firebase
//
// [X] Afficher le formulaire de sauvegarde du score
// seulement si score > bestScore
//
// [ ] Ne pas afficher l'aide si on a déjà joué
//
// [ ] Ajouter un curseur dans le formulaire de sauvegarde du score
// [ ] Faire clignotter ce curseur
//
// [ ] Mode hardcore avec les commandes inversées (touche H au démarrage)
//
// [ ] Les coins du serpent en triangle quand il tourne
// =================================================================
// ------ Initialisation du canvas ------
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
// ------ Initialisation de Firebase ------
let config = {
apiKey: "AIzaSyCZakOnkwqfE5CHnR4n9rMz2gQ0SuhMcck",
authDomain: "snakejs-85a40.firebaseapp.com",
databaseURL: "https://snakejs-85a40.firebaseio.com",
projectId: "snakejs-85a40",
storageBucket: "snakejs-85a40.appspot.com",
messagingSenderId: "296755417289"
};
firebase.initializeApp(config);
let database = firebase.database();
let bestScoreRef = database.ref('best_score');
// ------ Classe Snake ------
class Snake {
constructor () {
this.head = [20,20];
this.tail = [[22, 20], [21, 20]];
this.direction = [-1, 0];
this.width = 15;
this.red = 'rgb(200, 0, 0)';
this.grey = 'rgb(211, 211, 211)';
this.color = this.grey;
this.level = 1;
}
drawSquare (x, y, color = this.grey) {
ctx.fillStyle = color;
ctx.fillRect(x*(1+this.width), y*(1+this.width), this.width, this.width);
}
draw () {
this.drawSquare(this.head[0], this.head[1], this.color)
for (let coord of this.tail) {
this.drawSquare(coord[0], coord[1],this.color);
}
}
move () {
for (let i = 0; i < this.tail.length - 1; i++) {
this.tail[i] = this.tail[i + 1];
}
this.tail[this.tail.length - 1] = this.head;
let newHeadX = this.head[0]+this.direction[0];
let newHeadY = this.head[1]+this.direction[1];
if (newHeadX === -1) {newHeadX = 39;}
if (newHeadX === 40) {newHeadX = 0;}
if (newHeadY === -1) {newHeadY = 39;}
if (newHeadY === 40) {newHeadY = 0;}
this.head = [newHeadX, newHeadY];
}
grow () {
// fait grandir le serpent d'un carré depuis la tête
this.tail.push(this.head);
let newHeadX = this.head[0]+this.direction[0];
let newHeadY = this.head[1]+this.direction[1];
if (newHeadX === -1) {newHeadX = 39;}
if (newHeadX === 40) {newHeadX = 0;}
if (newHeadY === -1) {newHeadY = 39;}
if (newHeadY === 40) {newHeadY = 0;}
this.head = [newHeadX, newHeadY];
}
reduce () {
//efface le dernier carré de la queue, taille mini du serpent = 3 carrés
if (this.tail.length > 2) {
let newTail = [];
for (let i = 1; i < this.tail.length; i++) {
newTail.push(this.tail[i]);
}
this.tail = newTail;
}
}
distance (x, y) {
// Donne la distance entre la tete du serpent et un point sur la grille
let xSnake = this.head[0];
let ySnake = this.head[1];
return Math.sqrt(Math.pow(x - xSnake, 2) + Math.pow(y - ySnake, 2));
}
eat (apples) {
// Le serpent mange il une pomme?
//console.log(apples);
let response = [false];
for (let apple of apples) {
let d = this.distance(apple[0], apple[1]);
//console.log(d);
if (d < 1) {response = [true]; response.push(apple);}
}
//console.log(response);
return response;
}
crash (walls) {
// Le serpent se crash t il sur un mur? ou sur sa queue?
let response = false;
let obstacles = [];
obstacles = this.tail.concat(walls);
for (let obstacle of obstacles) {
let d = this.distance(obstacle[0], obstacle[1]);
if (d < 1) {response = true;}
}
return response;
}
}
// ------ Classe Apple ------
class Apple {
constructor () {
this.green = 'rgb(186, 218, 85)';
this.orange = 'rgb(255, 165, 0)';
this.red = 'rgb(200, 0, 0)';
this.width = 15;
this.apples = [];
this.maxApple = 1;
this.maxGenerate = 1;
this.level = 1;
}
drawCircle (x, y, color = this.green) {
ctx.fillStyle = color;
//ctx.fillRect(x*(1+this.width), y*(1+this.width), this.width, this.width);
ctx.beginPath();
ctx.arc(x*(1+this.width)+this.width/2, y*(1+this.width)+this.width/2, this.width/2, 0, Math.PI * 2, true);
ctx.fill();
}
generate () {
//console.log('generate apple');
if (this.apples.length < this.maxApple*this.level) {
//let nbre = Math.floor((Math.random() * this.maxGenerate*this.level/3) + 1);
let nbre = Math.floor((Math.random() * this.maxGenerate*this.level) + 1);
//console.log(nbre);
for (let i = 0; i < nbre; i++) {
let X = Math.floor((Math.random() * 38) + 1);
let Y = Math.floor((Math.random() * 38) + 1);
let appleLevel = 0; // level 0 (vert) par defaut
let percentage = Math.floor((Math.random() * 100) + 1);
if (percentage <= 10*(this.level-1)) {appleLevel = 1;} // orange
if (percentage <= 5*(this.level-1)) {appleLevel = 2;} // rouge
let newApple = [X, Y, appleLevel];
//console.log(newApple);
this.apples.push(newApple);
}
}
}
delete (oneApple) {
//efface une pomme du tableau apples
let newApples = [];
for (let apple of this.apples) {
if (apple[0] !== oneApple[0] && apple[1] !== oneApple[1]) {
newApples.push(apple);
}
}
this.apples = newApples;
}
draw () {
for (let apple of this.apples) {
let color = this.green;
if (apple[2] === 1) {color = this.orange;}
if (apple[2] === 2) {color = this.red;}
this.drawCircle(apple[0], apple[1], color);
}
}
}
// ------ Classe Wall ------
class Wall {
constructor () {
this.darkGrey = 'rgb(77, 77, 77)';
this.level = 1;
this.width = 15;
this.walls = [];
this.leftWalls = [];
this.rightWalls = [];
this.topWalls = [];
this.bottomWalls = [];
}
drawSquare (x, y, color = this.darkGrey) {
ctx.fillStyle = color;
ctx.fillRect(x*(1+this.width), y*(1+this.width), this.width, this.width);
}
draw () {
for (let wall of this.walls) {
this.drawSquare(wall[0], wall[1],this.color);
}
}
expand () {
// Augmente la taille des murs
if (this.level === 2) {
let position = Math.floor((Math.random() * 35));
//console.log(position);
for (let i = 0; i <5; i++) {this.leftWalls.push([0, position+i]);}
//console.log(this.leftWalls);
}
if (this.level === 3) {
let position = Math.floor((Math.random() * 35));
//console.log(position);
for (let i = 0; i <5; i++) {this.topWalls.push([position+i, 0]);}
//console.log(this.topWalls);
}
if (this.level > 3 && this.level <= 10) {
// On ajoute 5 murs sur tous les cotés
let indexTop = this.topWalls[this.topWalls.length-1][0];
let indexLeft = this.leftWalls[this.leftWalls.length-1][1];
//console.log('index : ' + index);
for (let i = 1; i <= 5; i++) {
if ((indexTop+i) > 39) {indexTop = -i;}
if ((indexLeft+i) > 39) {indexLeft = -i;}
this.topWalls.push([indexTop+i, 0]);
this.leftWalls.push([0, indexLeft+i]);
}
}
//Recopie des murs de gauche sur la droite
this.rightWalls = [];
for (let wall of this.leftWalls) {this.rightWalls.push([39, wall[1]]);}
// Recopie des murs du haut en bas
this.bottomWalls = [];
for (let wall of this.topWalls) {this.bottomWalls.push([wall[0], 39]);}
// Recopie de tous les murs
this.walls = [];
this.walls = this.leftWalls.concat(this.topWalls).concat(this.rightWalls).concat(this.bottomWalls);
}
}
// ------ Classe Text ------
class Text {
constructor () {
this.grey = 'rgb(211, 211, 211)';
this.red = 'rgb(255, 0, 0)';
}
drawBegin () {
ctx.font = '48px Doppio One';
ctx.textAlign = 'center';
ctx.fillStyle = this.grey;
let pos = 260;
ctx.fillText('Press ENTER to start', 320, pos);
ctx.font = '20px Doppio One';
ctx.fillText('Le serpent grandi tout seul de plus en plus vite', 320, pos+40);
ctx.fillText('Les pommes réduisent la taille du serpent', 320, pos+40+30);
ctx.fillText('Le serpent préfère les pommes mûres rouges au pomme vertes', 320, pos+40+30+30);
ctx.fillText('Le serpent peut sortir de l\'aire de jeu', 320, pos+40+30+30+30);
ctx.fillText('Mais attention des murs apparaissent au fil du jeu', 320, pos+40+30+30+30+30);
ctx.font = '28px Doppio One';
ctx.fillText('Good Luck !!', 320, pos+40+30+30+30+30+40);
}
drawLevel (level) {
ctx.font = '14px Doppio One';
ctx.textAlign = 'left';
ctx.fillStyle = this.grey;
ctx.fillText('Level : ' + level, 10, 15);
}
drawScore (score) {
ctx.font = '14px Doppio One';
ctx.textAlign = 'left';
ctx.fillStyle = this.grey;
ctx.fillText('Score : ' + score, 90, 15);
}
drawPlayerName (playerName) {
ctx.font = '14px Doppio One';
ctx.textAlign = 'center';
ctx.fillStyle = this.grey;
ctx.fillText(playerName, 320, 15);
}
drawBestScore (bestScore) {
ctx.font = '14px Doppio One';
ctx.textAlign = 'right';
ctx.fillStyle = this.grey;
ctx.fillText('Best score : ' + bestScore, 540, 15);
}
drawGameOver (score, playerName, bestScore) {
ctx.font = '72px Doppio One';
ctx.textAlign = 'center';
ctx.fillStyle = this.grey;
ctx.fillText('GAME OVER', 320, 220);
ctx.font = '48px Doppio One';
ctx.fillText('Score : ' + score, 320, 300);
ctx.font = '20px Doppio One';
if (score > bestScore) {
ctx.fillText('Tappe ton pseudo pour sauvegarder ton score', 320, 360);
ctx.fillText('ENTER pour sauvegarder', 320, 460);
this.drawForm(playerName);
} else {
ctx.fillText('Tappez r pour rejouer', 320, 360);
}
}
drawForm (playerName = '') {
ctx.clearRect(145, 380, 350, 50);
ctx.fillStyle = this.grey;
ctx.fillRect(145, 380, 350, 50);
ctx.lineWidth = 4;
ctx.strokeStyle = this.red;
ctx.strokeRect(145, 380, 350, 50);
ctx.fillStyle = this.red;
ctx.font = '28px Doppio One';
ctx.textAlign = 'center';
ctx.fillText(playerName, 320, 413);
}
draw (level, score, playerName, bestScore) {
this.drawLevel(level);
this.drawScore(score);
this.drawPlayerName(playerName);
this.drawBestScore(bestScore);
}
}
// ------ Classe Game ------
class Game {
constructor (playerName = '', bestScore = 0) {
this.tick = 200;
this.tac = 0;
this.appleEated = 0; // nombre de pommes mangées dans le level
this.intervalId = [];
this.gameStarted = false;
this.gameOver = false;
//this.gameOver = true;
this.level = 1;
this.score = 0;
this.mode = 'normal';
this.playerName = playerName || '';
//this.playerName = 'Nikookni';
this.bestScore = bestScore || 0;
this.snake = new Snake();
// nombre de pommes à manger avant augmentation de level
this.appleToEat = 5;
this.apple = new Apple();
this.apple.generate();
this.wall = new Wall();
this.text = new Text();
ctx.clearRect(0, 0, 641, 641);
this.text.drawBegin();
//this.text.drawGameOver(this.score, this.playerName, this.bestScore);
this.text.draw(this.level, this.score, this.playerName, this.bestScore);
document.addEventListener('keydown', (event) => {
event.preventDefault();
this.keyboard(event.key);
}, false);
console.log ('New game from Game Constuctor');
}
draw () {
ctx.clearRect(0, 0, 641, 641);
this.snake.draw();
this.apple.draw();
this.wall.draw();
this.text.draw(this.level, this.score, this.playerName, this.bestScore);
if (this.gameOver) {
this.text.drawGameOver(this.score, this.playerName, this.bestScore);
//this.showModal();
}
}
gameEvent () {
// Gère les événements du jeu
// Le serpent mange une pomme?
let eated = this.snake.eat(this.apple.apples);
if (eated[0]) {
console.log('Pomme mangée');
this.appleEated++
if (this.appleEated === this.appleToEat) {this.appleEated = 0; this.levelUp();}
this.apple.delete(eated[1]);
let appleLevel = eated[1][2];
let tailReduction = 0;
if (appleLevel === 0) {this.score = this.score+200; tailReduction = 2;}
if (appleLevel === 1) {this.score = this.score+500; tailReduction = 5;}
if (appleLevel === 2) {this.score = this.score+1000; tailReduction = 10;}
for (let i = 0; i < tailReduction; i++) {this.snake.reduce();}
this.apple.generate();
}
// Le serpent se crash sur un mur ou sur sa queue?
if (this.snake.crash(this.wall.walls)) {
console.log('Crash');
this.snake.color = this.snake.red;
this.gameOver = true;
this.stop();
}
}
speedUp () {
// Accélère le jeu en fonction du level
//this.start();
if (this.level === 2) {this.start();}
if (this.level === 4) {this.start();}
if (this.level === 6) {this.start();}
if (this.level === 8) {this.start();}
if (this.level === 9) {this.start();}
if (this.level === 10) {this.start();}
}
scoreUp () {
// augmente le score en fonction du niveau
if (!this.gameOver) {
this.score = this.score + this.level;
}
}
tailUp () {
// augmente la queue du serpent en fonction du niveau
let tacLimit = 13;
if (this.level === 2) {tacLimit = 13}
if (this.level === 3) {tacLimit = 12}
if (this.level === 4) {tacLimit = 12}
if (this.level === 5) {tacLimit = 11}
if (this.level === 6) {tacLimit = 11}
if (this.level === 7) {tacLimit = 10}
if (this.level === 8) {tacLimit = 10}
if (this.level === 9) {tacLimit = 9}
if (this.level === 10) {tacLimit = 7}
if (this.tac > tacLimit) {
this.snake.grow();
this.tac = 0;
this.gameEvent();
}
}
refresh () {
this.snake.move();
this.gameEvent();
this.tac++
this.tailUp();
//this.gameEvent();
//this.speedUp();
this.scoreUp();
this.draw();
}
start () {
// Démarre le jeu
// si cette fonction est appellée pendant le jeu la vitesse augmente
let newIntervalId = setInterval(() => {
this.refresh();
}, this.tick);
this.intervalId.push(newIntervalId);
console.log('intervalId : ' + this.intervalId);
}
stop () {
for (let intId of this.intervalId) {
clearInterval(intId);
this.intervalId = [];
}
//console.log('intervalId after stop : ' + this.intervalId);
}
restartGame () {
console.log('restartGame begin');
//let lastPlayerName = this.playerName;
//let lastBestScore = this.bestScore;
//masterRestart(lastPlayerName, lastBestScore);
this.tac = 0;
this.appleEated = 0; // nombre de pommes mangées dans le level
//this.intervalId = [];
this.gameStarted = false;
this.gameOver = false;
this.level = 1;
this.score = 0;
this.mode = 'normal';
this.snake = new Snake();
// nombre de pommes à manger avant augmentation de level
//this.appleToEat = 1;
this.apple = new Apple();
this.apple.generate();
this.wall = new Wall();
//this.text = new Text();
ctx.clearRect(0, 0, 641, 641);
this.text.drawBegin();
//this.text.drawGameOver(this.score, this.playerName, this.bestScore);
this.text.draw(this.level, this.score, this.playerName, this.bestScore);
console.log ('New game from restartGame');
}
keyboard (key) {
//console.log(key);
// Démarrage du jeu
if (key === 'Enter' && !this.gameStarted && !this.gameOver) {
this.gameStarted = true;
this.start();
}
// Entrée du playerName dans le formulaire quand on est gameOver
//console.log('gameOver : ' + this.gameOver);
if (this.gameOver && (this.score > this.bestScore)) {
//console.log(key);
if (key !=='Enter') {
let letterNumber = /^[0-9a-zA-Z]+$/;
if (key.length === 1 && key.match(letterNumber)) {
this.playerName += key;
//console.log(this.playerName);
this.text.drawForm(this.playerName);
}
if (key === 'Backspace') {
//console.log('On efface');
this.playerName = this.playerName.slice(0, -1);
//console.log(this.playerName);
this.text.drawForm(this.playerName);
}
}
if (key ==='Enter') {
//console.log('On sauvegarde le score');
this.saveScore();
}
}
if (this.gameOver && (this.score <= this.bestScore)) {
//console.log(key);
if(key === 'r') {
//console.log('keyboard want to restartGame');
this.restartGame();
}
}
// Mouvement du serpent
let oldDirection = this.snake.direction;
if (key === 'ArrowLeft' && oldDirection[0] !== 1) {this.snake.direction = [-1, 0];}
if (key === 'ArrowRight' && oldDirection[0] !== -1) {this.snake.direction = [1, 0];}
if (key === 'ArrowUp' && oldDirection[1]!== 1) {this.snake.direction = [0, -1];}
if (key === 'ArrowDown' && oldDirection[1] !== -1) {this.snake.direction = [0, 1];}
}
levelUp () {
//augmente le niveau
if (this.level < 10) {
this.level++
this.snake.level++
this.apple.level++
this.wall.level++
this.wall.expand();
this.speedUp();
console.log('New level : ' + this.level);
}
}
saveScore () {
// sauvegarde du meilleur score
if (this.score > this.bestScore) {
this.bestScore = this.score;
if(this.playerName.length < 1) {this.playerName = 'Anonymous'}
this.saveScoreToFirebase(response => {
if (response) {
//console.log('saveScore want to restart game before Firebase save');
this.restartGame();}
});
} else {
//console.log('saveScore want to restart game');
this.restartGame();
}
}
saveScoreToFirebase (callback) {
// sauvegarde du score sur Firebase
let data = {
playerName: this.playerName,
bestScore: this.bestScore,
mode:this.mode
};
console.log('Saving score to Firebase... ');
bestScoreRef.push(data)
.then(function() {
console.log('Save succeeded :');
console.log(data);
callback(true);
})
.catch(function(error) {
console.log('Save failed');
});
//callback(true);
}
}
// ------ Initialisation du jeu ------
let game = new Game();
//game.draw();
// ----- Affichage des bestScores ------
let playerScores = [];
const printBestScores = (playerScores) => {
let bestScores = document.getElementById("best-scores");
let html = '';
for (let playerScore of playerScores) {
html += `<li>
<div class="player">
<span class="playername"><strong>${playerScore.playerName}</strong></span>
<span class="playerscore">${playerScore.bestScore}</span>
</div>
</li>`;
}
bestScores.innerHTML = html;
}
// ------- Récupération des bestScores sur Firebase -------
bestScoreRef.orderByChild("bestScore").on("child_added", function(snapshot) {
let data = snapshot.val();
console.log(data);
playerScores.push(data);
playerScores = _.orderBy(playerScores, ['bestScore'], ['desc']);
printBestScores(playerScores);
});