Skip to content

Instantly share code, notes, and snippets.

@kiritodeveloper
Created July 5, 2022 19:05
Show Gist options
  • Save kiritodeveloper/dd2b33506cd486241967e8f8e19ec641 to your computer and use it in GitHub Desktop.
Save kiritodeveloper/dd2b33506cd486241967e8f8e19ec641 to your computer and use it in GitHub Desktop.
Tetris Game JS & Canvas
<!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>&nbsp;&nbsp;Move Right</p>
<p><i class="fas fa-caret-left"></i>&nbsp;&nbsp;Move Left</p>
<p><i class="fas fa-caret-down"></i>&nbsp;&nbsp;Move Down</p>
<p> <span style="font-weight:bold;">&nbsp;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>
// 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();
});
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