A Pen by Jean-Eudes Nouaille-Degorce on CodePen.
Created
July 5, 2022 19:05
-
-
Save kiritodeveloper/dd2b33506cd486241967e8f8e19ec641 to your computer and use it in GitHub Desktop.
Tetris Game JS & Canvas
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> | |
<meta http-equiv="X-UA-Compatible" content="IE=edge"> | |
<title>Tetris / JS Game by JEND</title> | |
<meta name="description" content="Tetris Game"> | |
<meta content='width=device-width, initial-scale=1.0' name='viewport' /> | |
<link rel="stylesheet" href="style.css"> | |
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.1/css/all.css" integrity="sha384-50oBUHEmvpQ+1lW4y57PTFmhCaXp0ML5d60M1M7uH2+nqUivzIebhndOJK28anvf" crossorigin="anonymous"> | |
</head> | |
<body> | |
<a href="http://tetris.planetcode.fr/" target="_blank"> | |
<img class="logo-img" src="http://tetris.planetcode.fr/LogoTetrisVector.png" alt="Tetris" title="Tetris Game"> | |
</a> | |
<img id="demo-img" src="http://tetris.planetcode.fr/TetrisDemo.png" alt="Demo" title="Demo"> | |
<div class="navbar"> | |
<button id="start_game"><i class="far fa-play-circle"></i></button> | |
<button id="restart_game"><i class="far fa-play-circle"></i></button> | |
<audio id="myAudio" src="TetrisRagtime.mp3" preload="auto"> | |
</audio> | |
<!--<audio controls autoplay style="outline-style: none; width:250px;position:fixed;right:0;bottom:0;z-index=1000;"> | |
<source src="TetrisRagtime.mp3" type="audio/mpeg"> | |
</audio>--> | |
<button id="music_game" onClick="togglePlay()"><i class="fas fa-music"></i></button> | |
<button id="pause_game"><i class="far fa-pause-circle"></i></button> | |
<button id="continue_game"><i class='fas fa-redo'></i></button> | |
<div class="dropdown"> | |
<button id="gamepad_controls" class="dropdown"><i class="fas fa-gamepad"></i></button> | |
<div class="dropdown-content"> | |
<p style="margin-top:px;"><i class="fas fa-caret-right"></i> Move Right</p> | |
<p><i class="fas fa-caret-left"></i> Move Left</p> | |
<p><i class="fas fa-caret-down"></i> Move Down</p> | |
<p> <span style="font-weight:bold;"> Ctrl</span> = Rotate</p> | |
</div> | |
</div> | |
</div> | |
<div class="gamepad"> | |
<div class="controls_1"> | |
<button id="arrow-right"><i class="fas fa-caret-right"></i></button> | |
<button id="arrow-left"><i class="fas fa-caret-left"></i></button> | |
</div> | |
<div class="controls_2"> | |
<button id="arrow-down"><i class="fas fa-caret-down"></i></button> | |
<button id="rotate_piece"><i class="fas fa-cubes"></i></button> | |
</div> | |
</div> | |
<br /> | |
<br /> | |
<div id="score"></div> | |
<p id="levels"></p> | |
<canvas id="tetris" width="240" height="400" /> | |
<script src="tetris.js"></script> | |
</body></html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Réalisation inspirée du tutoriel Canvas Game Youtube : https://www.youtube.com/watch?v=H2aW5V46khA | |
// Améliorations, modifications JS, ajouts de fonctionnalités au jeu -> notamment : réglages et design CSS, apparences des pièces et du canvas, Controls/Gamepad, responsive design, 5 Levels avec accélérations du jeu, activation et désactivation de la musique, événements Start/Restart + Pause/Continue | |
var myAudio = document.getElementById("myAudio"); | |
function togglePlay() { | |
return myAudio.paused ? myAudio.play() : myAudio.pause(); | |
}; | |
document.getElementById('tetris').style.border = '0px solid transparent'; | |
document.getElementById("start_game").addEventListener("click", function () { | |
document.getElementById('tetris').style.border = 'solid .2em #fff'; | |
document.getElementById('start_game').style.display = "none"; | |
document.getElementById('demo-img').style.display = "none"; | |
document.getElementById('restart_game').style.display = "block"; | |
/* | |
// Bouton éventuel pour recommencer la partie par un reload de la page | |
document.getElementById("restart_game").addEventListener("click", function () { | |
window.location.reload(); | |
}); | |
*/ | |
// Bouton pour recommencer la partie sans reload de la page | |
document.getElementById("restart_game").addEventListener("click", function () { | |
player.pos.y = 0; | |
merge(arena, player); | |
dropInterval = 500; | |
document.getElementById('levels').innerText = "Level 1"; | |
}); | |
const canvas = document.getElementById('tetris'); | |
const context = canvas.getContext('2d'); | |
// Ici on définit les ombres des dessins des pièces du jeu | |
// Voir : https://developer.mozilla.org/fr/docs/Web/API/CanvasRenderingContext2D/shadowColor | |
context.shadowColor = 'black'; | |
context.shadowOffsetX = 0.5; | |
context.shadowOffsetY = 0.5; | |
// On définit l'échelle du Tetris | |
context.scale(20, 20); | |
// Exemple : on entre les coordonnées du tetrominoe en forme de T | |
/* | |
const matrix = [ | |
[0, 0, 0], | |
[1, 1, 1], | |
[0, 1, 0], | |
]; | |
*/ | |
// Fonction qui permet de retirer les lignes remplies | |
function arenaSweep() { | |
let rowCount = 1; | |
outer: for (let y = arena.length - 1; y > 0; --y) { | |
for (let x = 0; x < arena[y].length; ++x) { | |
if (arena[y][x] === 0) { | |
continue outer; | |
} | |
} | |
const row = arena.splice(y, 1)[0].fill(0); | |
arena.unshift(row); | |
++y; | |
player.score += rowCount * 10; | |
rowCount *= 2; | |
} | |
}; | |
// Fonction de collision | |
function collide(arena, player) { | |
const [m, o] = [player.matrix, player.pos]; | |
// Pour vérifier qu'une collision est détectée.. | |
for (let y = 0; y < m.length; ++y) { | |
for (let x = 0; x < m[y].length; ++x) { | |
// On vérifie où le joueur se trouve.. | |
if (m[y][x] !== 0 && (arena[y + o.y] && arena[y + o.y][x + o.x]) !== 0) { | |
return true; | |
} | |
} | |
} | |
// Ou pas ! | |
return false; | |
}; | |
// Cette fonction prend en paramètres width et height des pièces du jeu | |
function createMatrix(w, h) { | |
const matrix = []; | |
while (h--) { | |
matrix.push(new Array(w).fill(0)); | |
} | |
return matrix; | |
}; | |
function draw() { | |
context.fillStyle = '#e3e3e3'; | |
context.fillRect(0, 0, canvas.width, canvas.height); | |
drawMatrix(arena, { | |
x: 0, | |
y: 0 | |
}); | |
drawMatrix(player.matrix, player.pos); | |
} | |
// Fonction qui crée les différentes pièces (définit les dimensions des tetrominoes) | |
function createPiece(type) { | |
if (type === 'I') { | |
return [ | |
[0, 1, 0, 0], | |
[0, 1, 0, 0], | |
[0, 1, 0, 0], | |
[0, 1, 0, 0], | |
]; | |
} else if (type === 'L') { | |
return [ | |
[0, 2, 0], | |
[0, 2, 0], | |
[0, 2, 2], | |
]; | |
} else if (type === 'J') { | |
return [ | |
[0, 3, 0], | |
[0, 3, 0], | |
[3, 3, 0], | |
]; | |
} else if (type === 'O') { | |
return [ | |
[4, 4], | |
[4, 4], | |
]; | |
} else if (type === 'Z') { | |
return [ | |
[5, 5, 0], | |
[0, 5, 5], | |
[0, 0, 0], | |
]; | |
} else if (type === 'S') { | |
return [ | |
[0, 6, 6], | |
[6, 6, 0], | |
[0, 0, 0], | |
]; | |
} else if (type === 'T') { | |
return [ | |
[0, 7, 0], | |
[7, 7, 7], | |
[0, 0, 0], | |
]; | |
} | |
}; | |
// On écrit une fonction qui va dessiner en prenant en paramètre le "matrix" (c'est-à-dire les coordonnées des pièces du jeu Tetris) et l "offset" : le décalage de la pièce, là où elle va apparaître au départ dans le canvas tetris | |
function drawMatrix(matrix, offset) { | |
// On fait une première boucle qui va itérer les coordonnées du Tetrominoe pour chaque ligne du tableau "row" | |
matrix.forEach((row, y) => { | |
// On fait une deuxième boucle pour chaque ligne-tableau du matrix | |
// On récupère la valeur et l'indice x | |
row.forEach((value, x) => { | |
// On décide que la valeur récupérée égale à 0 doit être ignorée pour les dessins des pièces | |
// Donc on vérifie que la valeur n'est pas égale à 0 | |
// A condition que la valeur soit différente de 0 | |
if (value !== 0) { | |
// Alors on dessine | |
// Choix de la couleur | |
// context.fillStyle = 'red'; | |
context.fillStyle = colors[value]; | |
// On dessine une forme à partir la valeur x (coordonnée gauche), de la valeur y (coordonnée en haut), puis on définit la largeur = 1 et la hauteur = 1 | |
context.fillRect(x + | |
offset.x, | |
y + offset.y, | |
1, 1); | |
} | |
}); | |
}); | |
}; | |
const arena = createMatrix(12, 20); | |
// On appelle la fonction de dessin des pièces, en définissant l "offset" des pièces (la position x à partir la gauche et la position y à partir du haut du canvas) | |
// Exemple -> drawMatrix(matrix, {x:4, y:4}); | |
// On peut remplacer les paramètres de l'appel de la fonction drawMatrix à partir d'une constante "player" | |
const player = { | |
pos: { | |
x: 0, | |
y: 0 | |
}, | |
// Exemple -> matrix: createPiece('S'), | |
matrix: null, | |
score: 0, | |
}; | |
// Cette fonction calcule les positions du joueur en fonction du tableau de positions de l'arène de jeu | |
function merge(arena, player) { | |
player.matrix.forEach((row, y) => { | |
// On récupère les positions | |
row.forEach((value, x) => { | |
if (value !== 0) { | |
arena[y + player.pos.y][x + player.pos.x] = value; | |
} | |
}); | |
}); | |
}; | |
function playerRotate(dir) { | |
const pos = player.pos.x; | |
let offset = 1; | |
rotate(player.matrix, dir); | |
// On modifie le comportement de la pièce en fonction des collisions avec les bords de l'arène ou la présence d'autres pièces déjà posées sur l'arène | |
while (collide(arena, player)) { | |
player.pos.x += offset; | |
offset = -(offset + (offset > 0 ? 1 : -1)); | |
if (offset > player.matrix[0].length) { | |
rotate(player.matrix, -dir); | |
player.pos.x = pos; | |
return; | |
} | |
} | |
} | |
// Fonction de rotation des pièces du jeu | |
// Elle fonctionne par inversion des colonnes de positions qui définissent l'apparition des pièces du jeu | |
function rotate(matrix, dir) { | |
for (let y = 0; y < matrix.length; ++y) { | |
for (let x = 0; x < y; ++x) { | |
[ | |
matrix[x][y], | |
matrix[y][x], | |
] = [ | |
matrix[y][x], | |
matrix[x][y], | |
]; | |
} | |
} | |
// ici on vérifie la direction et effectue la rotation des pièces | |
if (dir > 0) { | |
matrix.forEach(row => row.reverse()); | |
} else { | |
matrix.reverse(); | |
} | |
} | |
// Cette fonction empêche le joueur de sortir des bords gauche et droit de l'arène | |
function playerMove(dir) { | |
player.pos.x += dir; | |
if (collide(arena, player)) { | |
player.pos.x -= dir; | |
} | |
}; | |
// Fonction qui affiche aléatoirement les pièces | |
function playerReset() { | |
const pieces = 'TJLOSZI'; | |
player.matrix = createPiece(pieces[pieces.length * Math.random() | 0]); | |
player.pos.y = 0; | |
player.pos.x = (arena[0].length / 2 | 0) - | |
(player.matrix[0].length / 2 | 0); | |
// Ici on active un retour en début de partie lorsque tout est rempli | |
if (collide(arena, player)) { | |
arena.forEach(row => row.fill(0)); | |
player.score = 0; | |
updateScore(); | |
dropInterval = 500; | |
document.getElementById('levels').innerText = "Level 1"; | |
} | |
} | |
function playerDrop() { | |
player.pos.y++; | |
if (collide(arena, player)) { | |
player.pos.y--; | |
merge(arena, player); | |
playerReset(); | |
arenaSweep(); | |
updateScore(); | |
} | |
dropCounter = 0; | |
} | |
// Fonction qui permet de dessiner continuellement en temps réel | |
// La paramètre time défini à 0 -> on peut observer le résultat dans la console (cela opère un décompte du temps en millisecondes) | |
let dropCounter = 0; | |
// Nous voulons que la pièce tombe selon cette intervalle en millisecondes | |
let dropInterval = 500; | |
// Mise en pause | |
document.getElementById("pause_game").addEventListener("click", function () { | |
dropInterval = 2000000; | |
document.getElementById('pause_game').style.display = "none"; | |
document.getElementById('continue_game').style.display = "block"; | |
}); | |
// Remise en cours du jeu | |
document.getElementById("continue_game").addEventListener("click", function () { | |
dropInterval = 200; | |
document.getElementById('pause_game').style.display = "block"; | |
document.getElementById('continue_game').style.display = "none"; | |
}); | |
let lastTime = 0; | |
function update(time = 0) { | |
// On met à jour le temps | |
const deltaTime = time - lastTime; | |
lastTime = time; | |
//console.log(deltaTime); | |
// On utilise ici l'opérateur affectation après addition | |
// Cela équivaut à : dropCounter = dropCounter + deltaTime; | |
dropCounter += deltaTime; | |
if (dropCounter > dropInterval) { | |
// On déplace la pièce | |
// Exemple -> player.pos.y++; | |
// On rétablit à 0 | |
// dropCounter = 0; | |
playerDrop(); | |
}; | |
draw(); | |
requestAnimationFrame(update); | |
}; | |
let colors = [ | |
null, | |
'#FF0D72', | |
'#0DC2FF', | |
'#0DFF72', | |
'#F538FF', | |
'#FF8E0D', | |
'#FFE138', | |
'#3877FF', | |
]; | |
function updateScore() { | |
document.getElementById('score').innerText = player.score; | |
// Vérifications du score pour accélérer la vitesse et passer à de nouveaux levels | |
const scoring = document.getElementById('score'); | |
const textScore = scoring.textContent; | |
// On convertit le texte en nombre | |
const numberScore = Number(textScore); | |
// console.log(numberScore); | |
// Level 2 | |
if (numberScore >= 100) { | |
// alert(numberScore); | |
dropInterval = 400; | |
// console.log(dropInterval) | |
document.getElementById('levels').innerText = "Level 2"; | |
} | |
// Level 3 | |
if (numberScore >= 200) { | |
dropInterval = 300; | |
document.getElementById('levels').innerText = "Level 3"; | |
} | |
// On vérifie que le nombre du score est supérieur à 100 pour augmenter ensuite la vitesse du jeu | |
if (numberScore >= 300) { | |
dropInterval = 200; | |
document.getElementById('levels').innerText = "Level 4"; | |
} | |
// On vérifie que le nombre du score est supérieur à 100 pour augmenter ensuite la vitesse du jeu | |
if (numberScore >= 400) { | |
dropInterval = 100; | |
document.getElementById('levels').innerText = "Level 5"; | |
} | |
} | |
document.addEventListener('keydown', event => { | |
if (event.keyCode === 37) { | |
playerMove(-1); | |
// player.pos.x--; | |
} else if (event.keyCode === 39) { | |
playerMove(1); | |
// player.pos.x++; | |
} else if (event.keyCode === 40) { | |
playerDrop(); | |
} | |
// Ici éventuellement la possibilité de faire remonter les pièces du jeu | |
/* | |
else if (event.keyCode === 38) { | |
player.pos.y--; | |
dropCounter = 0; | |
} | |
*/ | |
// La touche Ctrl permet de faire une rotation à gauche | |
else if (event.keyCode === 17) { | |
playerRotate(-1); | |
} | |
// La touche W permet de faire une rotation à droite | |
else if (event.keyCode === 87) { | |
playerRotate(1); | |
} | |
}); | |
// Bouton de déplacement vers la gauche | |
document.getElementById("arrow-left").addEventListener("click", function () { | |
playerMove(-1); | |
}); | |
// Bouton de déplacement vers la droite | |
document.getElementById("arrow-right").addEventListener("click", function () { | |
playerMove(1); | |
}); | |
// Bouton de rotation des pièces | |
document.getElementById("rotate_piece").addEventListener("click", function () { | |
playerRotate(1); | |
}); | |
// Bouton de déplacement accéléré vers le bas | |
document.getElementById("arrow-down").addEventListener("click", function () { | |
playerDrop(); | |
// Pour incrémenter quatre fois le changement de position vers le haut, par exemple : | |
// player.pos.y -= 4; | |
// Pour incrémenter quatre fois le changement de position vers le bas, par exemple : | |
// player.pos.y += 4; | |
}); | |
playerReset(); | |
updateScore(); | |
update(); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
body { | |
background: linear-gradient(-45deg, #ee7752, #e73c7e, #23abd5, #23a6d5); | |
background-size: 400% 400%; | |
color: #fff; | |
font-family: sans-serif; | |
font-size: 2em; | |
text-align: center; | |
animation: gradient 150s ease infinite; | |
} | |
@keyframes gradient { | |
0% { | |
background-position: 0% 50%; | |
} | |
50% { | |
background-position: 100% 50%; | |
} | |
100% { | |
background-position: 0% 50%; | |
} | |
} | |
canvas { | |
height: 70vh; | |
} | |
.navbar { | |
height: 62px; | |
width: 100%; | |
position: fixed; | |
background: white; | |
top: 0; | |
left: 0; | |
opacity: 0.65; | |
text-align: center; | |
} | |
.logo-img { | |
z-index: 10; | |
position: fixed; | |
width: 150px; | |
margin-left: -80px; | |
} | |
.gamepad { | |
height: 130px; | |
width: 100%; | |
position: fixed; | |
bottom: 0; | |
left: 0; | |
} | |
.controls_1 { | |
height: 50px; | |
width: 50px; | |
border: 0px solid black; | |
z-index: 10; | |
margin-top: 40px; | |
} | |
#arrow-right { | |
position: absolute; | |
margin-left: 90px; | |
font-size: 60px; | |
background: transparent; | |
border: none; | |
outline-style: none; | |
cursor: pointer; | |
color: #424242; | |
} | |
#arrow-left { | |
position: absolute; | |
font-size: 60px; | |
background: transparent; | |
border: none; | |
outline-style: none; | |
cursor: pointer; | |
color: #424242; | |
} | |
.controls_2 { | |
margin-top: -40px; | |
height: 50px; | |
width: 100%; | |
border: 0px solid black; | |
z-index: 9; | |
} | |
#pause_game { | |
position: absolute; | |
right: 70px; | |
margin-top: 9px; | |
font-size: 40px; | |
background: transparent; | |
color: #424242; | |
border: none; | |
z-index: 10; | |
outline-style: none; | |
cursor: pointer; | |
} | |
#continue_game { | |
display: none; | |
position: absolute; | |
right: 77px; | |
margin-top: 15px; | |
font-size: 30px; | |
background: transparent; | |
color: #424242; | |
border: none; | |
z-index: 10; | |
outline-style: none; | |
cursor: pointer; | |
} | |
#rotate_piece { | |
position: absolute; | |
right: 30px; | |
margin-top: 0px; | |
font-size: 40px; | |
background: transparent; | |
color: #424242; | |
border: none; | |
outline-style: none; | |
cursor: pointer; | |
} | |
#arrow-down { | |
position: absolute; | |
right: 120px; | |
margin-top: -5px; | |
font-size: 55px; | |
background: transparent; | |
color: #424242; | |
border: none; | |
outline-style: none; | |
cursor: pointer; | |
} | |
#start_game { | |
position: absolute; | |
right: 10px; | |
margin-top: 8px; | |
font-size: 40px; | |
background: transparent; | |
color: #424242; | |
border: none; | |
z-index: 10; | |
outline-style: none; | |
cursor: pointer; | |
} | |
#restart_game { | |
display: none; | |
position: absolute; | |
right: 10px; | |
margin-top: 8px; | |
font-size: 40px; | |
background: transparent; | |
color: #424242; | |
border: none; | |
z-index: 10; | |
outline-style: none; | |
cursor: pointer; | |
} | |
#music_game { | |
position: absolute; | |
left: 90px; | |
margin-top: 15px; | |
font-size: 30px; | |
background: transparent; | |
color: #424242; | |
border: none; | |
z-index: 10; | |
outline-style: none; | |
cursor: pointer; | |
} | |
#music_game:hover { | |
opacity: 0.55; | |
} | |
#demo-img { | |
position: fixed; | |
height: 300px; | |
margin-left: -92px; | |
margin-top: 150px; | |
filter: drop-shadow(0 0 0.15rem black); | |
} | |
#gamepad_controls { | |
font-size: 30px; | |
background: transparent; | |
color: #424242; | |
border: none; | |
z-index: 10; | |
outline-style: none; | |
cursor: pointer; | |
} | |
.dropdown { | |
position: absolute; | |
left: 10px; | |
margin-top: 8px; | |
display: inline-block; | |
} | |
.dropdown-content { | |
display: none; | |
position: absolute; | |
background-color: #fff; | |
margin-top: 40px; | |
min-width: 160px; | |
height: 170px; | |
box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2); | |
padding: 12px 16px; | |
z-index: 1; | |
color: black; | |
font-size: 18px; | |
border-radius: 5px; | |
} | |
.dropdown:hover .dropdown-content { | |
display: block; | |
} | |
#score { | |
border: 0px solid black; | |
font-size: 24px; | |
margin-top: -10px; | |
margin-bottom: 5px; | |
} | |
#levels { | |
border: 0px solid black; | |
position: fixed; | |
top: 60px; | |
right: 10px; | |
font-size: 15px; | |
} | |
@media all and (max-width: 800px) { | |
.dropdown { | |
display: none; | |
} | |
#music_game { | |
left: 10px; | |
} | |
#start_game { | |
margin-top: 70px; | |
} | |
#restart_game { | |
opacity: 0; | |
} | |
#pause_game { | |
right: 10px; | |
} | |
#continue_game { | |
right: 15px; | |
} | |
} | |
@media all and (min-width: 801px) { | |
.controls_1 { | |
display: none; | |
} | |
.controls_2 { | |
display: none; | |
} | |
} | |
@media all and (max-width: 500px) { | |
canvas { | |
height: 60vh; | |
} | |
} | |
@media all and (max-width: 350px) { | |
#rotate_piece { | |
font-size: 40px; | |
} | |
#arrow-down { | |
display: none; | |
} | |
#arrow-right { | |
font-size: 50px; | |
} | |
#arrow-left { | |
font-size: 50px; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment