Présentation
Tetris est un jeu vidéo de puzzle conçu en 1984 par Alekseï Pajitnov. Bâti sur des règles simples et exigeant intelligence et adresse, il est l’un des jeux vidéo les plus populaires au monde. Ses versions sont innombrables, y compris en 3D, et cette multiplicité se décline sur tous les types d’ordinateurs.
Il est considéré comme un des grands classiques de l’histoire des jeux vidéo aux côtés de Pong, Space Invaders ou encore Pac-Man.
Des pièces de couleur (parfois il n’y a pas de couleur, et ce sont les motifs sur les pièces qui changent) et de formes différentes descendent du haut de l’écran. Le joueur ne peut pas ralentir ou empêcher cette chute mais peut l’accélérer ou décider à quel angle de rotation (0°, 90°, 180°, 270°) et à quel emplacement latéral l’objet peut atterrir. Lorsqu’une ligne horizontale est complétée sans vide, elle disparaît et les blocs supérieurs tombent. Si le joueur ne parvient pas à faire disparaître les lignes assez vite et que l’écran se remplit jusqu’en haut, il est submergé et la partie est finie.
Le jeu ne se termine donc jamais par la victoire du joueur. Avant de perdre, le joueur doit tenter de compléter un maximum de lignes. Faire une seule ligne ne rapporte que 40 points, alors qu’en faire 2 en rapporte 100, 3 lignes rapportent 300 et 4 lignes (le maximum) en rapportent 1200. Le nombre de points est augmenté à chaque niveau selon l’équation f(p, n)= p(n+1) où p est le nombre de points au niveau 0 et n le niveau.
Les pièces de Tetris, sur lesquelles repose entièrement le jeu, sont des tétrominos. Il en existe sept formes différentes, toutes basées sur un assemblage de quatre carrés – le mot « Tetris » (du préfixe grec tetra-, qui signifie quatre) prend donc tout son sens. Le joueur peut faire tourner plusieurs fois, à gauche et/ou à droite selon la version, de 90° n’importe quel bloc pour le poser de la façon désirée pendant que le bloc descend. Chacune des sept pièces dispose d’une couleur qui lui est propre, et certains joueurs se réfèrent aux pièces seulement par ce détail. Au désarroi de ceux-ci, la couleur des pièces varie généralement d’une version de Tetris à une autre. Cependant, d’après les consignes de la Tetris Company, ceci n’a aucune incidence sur le jeu.
Le champ de jeu, aussi connu sous l’appellation « puits » dans les anciens Tetris et en tant que « matrice » dans les plus récents, est l’espace dans lequel tombent les pièces. Il dispose toujours d’une grille en arrière-plan, visible ou non, dont les cases sont de la même grandeur que les carrés des pièces, et que celles-ci suivent dans leur chute. Il est également entouré par une armature appelée « tétrion », infranchissable, qui pose les limites du champ de jeu.
La vitesse de la chute des pièces est déterminée par le niveau auquel vous êtes. Plus le niveau est élevé, plus les pièces tombent vite. Au niveau 0 on peut faire 5-6 déplacements latéraux avant que la pièce tombe d’un rang, au niveau 9 on ne peut plus faire que 1-2 mouvements latéraux. Le niveau 15 équivaut au niveau de la vitesse à tout le temps appuyer sur la flèche du bas, il n’y a plus de déplacements latéraux possibles directement. Le joueur peut alors dans ce cas tenter de les réaliser par des successions rapides de rotations, une pièce n’est définitivement posée sur l’écran de jeu que dans le cas où le joueur cesse toute action sur celle-ci. En effectuant continuellement des rotations sur une pièce, le joueur l’empêche donc de se poser définitivement, il est alors possible d’opérer des déplacements latéraux et selon les cas il peut être envisageable de faire passer une pièce par dessus une autre déjà posée. Ainsi il est toujours possible de placer et d’orienter de façon entièrement libre une pièce quel que soit le niveau de jeu en cours.
Nous n’allons bien entendu pas reproduire la totalité du comportement du jeu, d’une part car il s’agit d’un exercice et non d’un moteur tout prêt, et d’autre part car il faut bien qu’il vous reste un peu de grain à moudre de votre côté
Le code Javascript de la classe jeu.js
// variables var canvas,ctx,W,H,posX,posY,images,grille,pieces,lateral,vitesse,timer,piece,nextP,nextColor,stock,preview,cadence,T,jeuX,jeuY,previewX,previewY,sounds; var fond = new Image(); fond.src = "assets/fond.jpg"; // importer un fichier function include(fileName){ document.write("<script type='text/javascript' src='js/"+fileName+".js'></script>" ); } include("piece"); // importer la classe Piece include("bloc"); // importer la classe Bloc include("sounds"); // importer la classe Sounds window.onload = function() { // Préparer le jeu canvas = document.getElementById('canvas'); // récupére le canvas ctx = canvas.getContext('2d'); // récupére le context W = 480; // largeur du jeu H = 480; // hauteur du jeu posX = canvas.offsetLeft; // décalage du canvas sur X posY = canvas.offsetTop; // décalage du canvas sur Y canvas.width = W; // largeur du canvas canvas.height = H; // hauteur du canvas T = 21; // largeur des tuiles jeuX = 4*T; // position de la zone de jeu sur x jeuY = 2*T; // position de la zone de jeu sur y previewX = 15*T; // position de la zone de preview sur x previewY = 2*T; // position de la zone de preview sur y canvas.setAttribute('tabindex','1'); // sélectionne le canvas dans la page canvas.focus(); // donne le focus au canvas loadImages(5); // charge 5 images } function loadImages(nbImg){ // Charger des images images = []; // vide le tableau contenant les images for(i=1; i<nbImg+1; i++){ // boucle sur le nombre d'images var b = new Image(); // crée une nouvelle image b.src = "assets/tuile"+i+".jpg"; // associe son bitmap b.onload = function() { // quand l'image est chargée images.push(this); // ajoute la au stock d'images if(--nbImg==0) init(); // s'il n'y a plus d'image à charger initialise le jeu }; } } function init() { // Initialer le jeu stock = []; // liste des blocs de la zone de jeu preview = []; // liste des blocs de la zone de preview grille = []; // grille affichée à l'écran pieces = []; // liste des différents types de pieces vitesse = 18; // vitesse du jeu timer = 0; // décompte le temps passé piece = null; // la piece en cours de jeu nextP = Math.floor(Math.random()*7); // référence de la prochaine piece nextColor = Math.floor(Math.random()*5); // référence de la prochaine couleur cadence = setInterval(update, 15); // cadence du jeu for(var i=0; i<20; i++) {grille.push([0,0,0,0,0,0,0,0,0,0])}; // création dela grille de jeu for(var i=0; i<7; i++) {pieces.push(new Piece(i,T,grille,stock))}; // création des types de pieces document.addEventListener("keydown", keyDown, false); // écoute l'appui d'une touche sur la page sounds = new Sounds(); } function keyDown(e){ // Gestion du clavier if (e.keyCode == 37 && piece.checkMove("g")) lateral=true; // déplace la piece à gauche if (e.keyCode == 39 && piece.checkMove("d")) lateral=true; // déplace la piece à droite if (e.keyCode == 38) piece.rotate(), sounds.rotate.play(); // oriente la piece if (e.keyCode == 40) while(piece.checkMove("b")) timer = 0; // fait checkLine la piece } function update(e) { // Mise à jour du jeu piece == null ? newPiece() : movePiece(); // crée une piece si aucune en cours, sinon déplace la lateral = false; // annule les mouvements latéraux checkLine(); // vérifie si une ligne est remplie render(); // rendu graphique du jeu }; function newPiece(){ // Créer une piece preview = []; // vide le tableau de preview piece = pieces[nextP]; // la piece devient la nouvelle piece piece.init(nextColor); // initialise la piece avec la bonne couleur nextP = Math.floor(Math.random()*7); // référence de la nouvelle piece nextColor = Math.floor(Math.random()*5); // référence de la nouvelle couleur pieces[nextP].buildPreview(preview,nextColor); // construit la preview de la nouvelle piece if(!piece.buildPiece()) gameover(); // construit la piece, fin de partie si un bloc touche le haut } function movePiece(){ // Déplacer la piece if (timer>=vitesse || lateral) { // si mouvement latéral ou temps écoulé if (piece.checkMove("b")) { // si la piece peut descendre elle descend lateral ? piece.Y-- : timer = 0; // si mouvement latéral la piece remonte sinon fin du temps } else { // sinon sounds.touchDown.play(); // bruitage piece.drawInGrid(); // ajoute les blocs de la piece à la grille piece = null; // vide la piece en cours for (var i=0;i<stock.length;i++){ // parcours tous les blocs var b = stock[i]; // référence le bloc testé if (b.T == "piece") { // si le bloc fait partie de la piece stock[i--] = new Bloc("tuile",b.couleur,b.x/T,b.y/T); // remplace le bloc par une tuile } } } } else { // sinon timer++; // incrémente le temps } } function checkLine(){ // Vérifier si une ligne est remplie var full,couleur,C,L,b,i, ligne = 19; // variables locales while (ligne>=0) { // teste toutes les lignes de l'aire de jeu à partir de la dernière full = true; // par défaut la ligne est remplie for(C=0; C<10; C++){ // vérifie chaque colonne if(!grille[ligne][C]) full = false; // si une case est vide la ligne n'est pas remplie } if (full) { // si la ligne est remplie sounds.full.play(); // bruitage for (i=stock.length-1;i>=0; i--){ // parcours tous les blocs if(stock[i].L == ligne) stock.splice(i,1); // retire tous les blocs concernant cette ligne } for (L=ligne; L>0; L--) { // remonte les lignes depuis la dernière for (C=0; C<10; C++) { // parcours toutes les colonnes de la ligne grille[L][C] = grille[L-1][C]; // la case prend la référence de celle du dessus if (grille[L][C]) { // si la case n'est pas vide for (i=0;i<stock.length;i++){ // parcours tous les blocs b = stock[i]; // référence le bloc en cours if(b.L == L-1 && b.C == C) { // si il s'agit du bloc au dessus de la case couleur = b.couleur; // récupére la couleur du bloc stock.splice(i,1); // supprime la le bloc } } stock.push(new Bloc("tuile",couleur,C,L)); // crée une nouvelle tuile pour cette case } } } for (C=0; C<10; C++) grille[0][C] = 0; // vide la première ligne de l'aire de jeu ligne++; // indécrement la ligne } ligne--; // décrémente la ligne } } function gameover(){ // Terminer la partie clearInterval(cadence); // stoppe la boucle principale alert("game over"); // signale au joueur qu'il a perdu init(); // relance le jeu } function render() { // Dessiner le jeu ctx.fillStyle = "rgb(51,51,51)"; ctx.fillRect(0, 0, W, H); ctx.drawImage(fond, 0, 0); for(var i=0; i<stock.length; i++){ ctx.drawImage(images[stock[i].couleur], stock[i].x+jeuX, stock[i].y+jeuY); } for(var i=0; i<preview.length; i++){ ctx.drawImage(images[preview[i].couleur], preview[i].x+previewX, preview[i].y+previewY); } }
Le code Javascript de la classe blocs.js
// l'objet Bloc function Bloc(type,color,C,L,cX=0,cY=0){ this.couleur = color; // couleur du bloc this.C = C; // colonne this.L = L; // ligne this.x = (C+cX)*T; // position sur x this.y = (L+cY)*T; // position sur y this.T = type; // type de bloc this.move = function(X,Y){ // Déplacer le bloc if (this.T == "piece") { // si le bloc fait partie de la piece en cours this.x = (this.C+Math.floor(X))*T; // nouvelle position sur x this.y = (this.L+Math.floor(Y))*T; // nouvelle position sur y } } }
Le code Javascript de la classe sounds.js
// l'objet Sounds function Sounds(){ this.full = new Audio('assets/SFX_line.ogg'); this.touchDown = new Audio('assets/SFX_PieceTouchDown.ogg'); this.rotate = new Audio('assets/SFX_rotate.ogg'); }
Le code Javascript de la classe blocs.js
// l'objet Piece function Piece(mapNum, T, grid, stock){ this.X = 4; this.Y = 0; this.T = T; this.grille = grid; this.stock = stock; this.color = Math.floor(Math.random()*5); var C; var L; var temp; var maps = [ [[1,1,1,1],[0,0,0,0],[0,0,0,0],[0,0,0,0]], [[1,1,1,0],[0,0,1,0],[0,0,0,0],[0,0,0,0]], [[0,0,1,0],[1,1,1,0],[0,0,0,0],[0,0,0,0]], [[0,1,0,0],[1,1,1,0],[0,0,0,0],[0,0,0,0]], [[1,1,0,0],[1,1,0,0],[0,0,0,0],[0,0,0,0]], [[1,0,0,0],[1,1,0,0],[0,1,0,0],[0,0,0,0]], [[0,1,0,0],[1,1,0,0],[1,0,0,0],[0,0,0,0]] ] var map = maps[mapNum]; this.init = function(color){ // Initialiser la piece this.X = 4; // colonne this.Y = 0; // ligne this.color = color; // couleur } this.drawInGrid = function(){ // Ajouter la piece dans la grille for (L=0; L<4; L++) { // lignes de la piece for (C=0; C<4; C++) { // colonnes de la ligne if (map[L][C]) { // si la case est remplie this.grille[this.Y+L][C+this.X] = map[L][C]; // mise a jour de la grille } } } } this.buildPiece = function(){ // Construire la piece for (L=0; L<4; L++) { // lignes de la piece for (C=0; C<4; C++) { // colonnes de la piece if (map[L][C]) { // si la case est remplie this.stock.push(new Bloc("piece",this.color,C,L,this.X,this.Y)); // ajoute le bloc correspondant if (this.grille[this.Y+L][this.X+C]) return false; // si le bloc touche le haut de la zone de jeu, partie perdue } } } return true; // la piece a été construire } this.buildPreview = function(tab,color){ // Construire la preview for (L=0; L<4; L++) { // lignes de la piece for (C=0; C<4; C++) { // colonnes de la piece if (map[L][C]) { // si la case est remplie tab.push(new Bloc("preview",color,C,L)); // ajoute le bloc correspondant } } } } this.checkMove = function (dir) { // Vérifier si la piece peut bouger var col = 0; // la nouvelle colone var lig = 0; // la nouvelle ligne if(dir=="b") lig = 1; // si la piece descend if(dir=="g") col = -1; // si la piece va a gauche if(dir=="d") col = 1; // si la piece va a droite for (var L=0; L<4; L++) { // lignes de la piece for (var C=0; C<4; C++) { // colonnes de la piece if(map[L][C]) { // si la case n'est pas vide if(dir=="b" && this.Y+L+1>=20) return false; // si le bloc sort par le bas, ne bouge pas if(dir=="g" && this.X<=0) return false; // si le bloc sort par la gauche, ne bouge pas if(dir=="d" && this.X+C+1>9) return false; // si le bloc sort par le droite, ne bouge pas if(this.grille[this.Y+L+lig][this.X+C+col]) return false; // si le bloc est occupée dans la grille, ne bouge pas } } } this.X += col; // nouvelle colone this.Y += lig; // nouvelle ligne if(dir=="b") { // si la piece descend for (var i=0; i<this.stock.length; i++) { // parcours tous les blocs this.stock[i].move(this.X,this.Y); // déplace le bloc } } return true; // la piece peut bouger } this.rotate = function() { // Changer le sens de la piece if (this.checkRotation()){ // si la piece peut bouger for (var i=this.stock.length-1; i>=0;i--){ // parcours tous les blocs if (this.stock[i].T == "piece") this.stock.splice(i,1); // retire les blocs de la piece } this.buildPiece(); // construit la nouvelle piece } } this.checkRotation = function() { // Tester si la nouvelle orientation est possible temp = [[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0]]; // orientation temporaire de la piece for (L=0; L<4; L++) { // lignes de la piece for (C=0; C<4; C++) { // colonnes de la ligne temp[C][3-L] = map[L][C]; // rotation de la piece } } while (!(temp[0][0] || temp[1][0] || temp[2][0] || temp[3][0])) { // Tant que la première colonne est vide for (L=0; L<4; L++) { // lignes de la piece for (C=1; C<4; C++) { // colonnes de la ligne temp[L][C-1] = temp[L][C]; // décale les colonnes vers la gauche } temp[L][3] = 0; // vide la dernière colonne } } for (L=0; L<4; L++) { // lignes de la piece for (C=0; C<4; C++) { // colonnes de la ligne if (temp[L][C]){ // si la case est remplie if (this.grille[L + this.Y][C + this.X] || C + this.X > 9 || L + this.Y > 19) return false; // si une erreur est détectée la piece n'est pas tournée } } } map = temp; // valide la nouvelle orientation de la piece return true; // la piece est tournée } }