Exercice pratique : le TETRIS
Bonjour,
Aujourd'hui on attaque du lourd avec Tetris. Vous devrez impérativement avoir fait un ou plusieurs exercices précédents avant d'aborder celui-ci car je vais essayer d'aller vite et passerait sur des habitudes prises depuis le début de la série.
Tout d'abord le résultat :
Les sources sont disponibles en fin d'exercice
Les pré-requis
Pour ce programme vous devez connaître :
Fonctions et paramètres : http://help.adobe.com/fr_FR/FlashPlatform/reference/actionscript/3/statements.html#function
Manipulation de tableaux : http://help.adobe.com/fr_FR/FlashPlatform/reference/actionscript/3/Array.html
Ecouteurs d'événements : http://help.adobe.com/fr_FR/FlashPlatform/reference/actionscript/3/flash/events/package-detail.html
Programmation Orienté Objet : http://forums.mediabox.fr/wiki/tutoriaux/flashplatform/programmation/poo_bases/classesqqc
Exercices pratiques précédents de la série : http://forums.mediabox.fr/wiki/tutoriaux/flashplatform/jeux
Etude préliminaire
Tout d'abord le TETRIS c'est quoi ? (merci Wikipedia)
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é
Aborder Tetris est un sujet délicat car il fait partie des jeux par lesquels les apprentis programmeurs veulent commencer, c'est pourquoi je choisi une fois encore d'utiliser l'IDE et de ne pas orienter l'exercice sur une structure POO, bien qu'on utilisera quand même une petite classe ou deux.
Préparation
Dans Tetris la seule chose qui importe vraiment c'est les blocs, ces petits carrés qui composent chaque pièce, aussi nous n'aurons besoin que de trois clips, dont deux sont là uniquement pour nous simplifier la vie.
Ouvrez un nouveau projet Flash et créez :
Un MovieClip “Blocs” comportant un bloc de couleur différente par frame.
Un MovieClip “Jeu” collez-y un fond si vous le souhaitez, c'est le conteneur de la zone de jeu.
Un MovieClip “Preview” collez-y aussi un fond, c'est la zone de preview des pièces.
Tous les objets sont exportés pour AS, laissez les définitions de classes par défaut.
Pour la préparation c'est tout, passons au code.
Programme principal
Le programme est découpé en trois parties, les maps, le code principal et les pièces, on commence par les maps, sur un nouveau calque placez le code suivant :
var grille:Array = [ [0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0] ]; var pieces:Array = [ new Piece ( [ [1,1,1,1], [0,0,0,0], [0,0,0,0], [0,0,0,0] ] ), new Piece ( [ [1,1,1,0], [0,0,1,0], [0,0,0,0], [0,0,0,0] ] ), new Piece ( [ [0,0,1,0], [1,1,1,0], [0,0,0,0], [0,0,0,0] ] ), new Piece ( [ [0,1,0,0], [1,1,1,0], [0,0,0,0], [0,0,0,0] ] ), new Piece ( [ [1,1,0,0], [1,1,0,0], [0,0,0,0], [0,0,0,0] ] ), new Piece ( [ [1,0,0,0], [1,1,0,0], [0,1,0,0], [0,0,0,0] ] ), new Piece ( [ [0,1,0,0], [1,1,0,0], [1,0,0,0], [0,0,0,0] ] ) ]
Pour la grille on utilise un tableau à deux dimensions de 20 lignes et 10 colonnes, il aurait été facile de le remplir avec une boucle, mais l'écrire en entier me permet de vous monter à quoi elle correspond, il s'agit en fait de la zone de jeu où sont stockés les références des pièces en cours et des blocs posés. Le tableau “pieces” (au pluriel) contient quand à lui 7 pièces dotée chacune d'une petite map de 4*4 cases représentant la forme que doit avoir la pièce, la map référence donc la position de chaque bloc au sein d'une pièce, nous détaillerons la création des pièces en temps voulu.
On attaque le programme principale, sur un autre calque de votre projet tapez le code suivant :
var vitesse:int; var compteur:int; var piece:int; var nextPiece:int; var nextPieceColor:int; var pieceL:int; var pieceC:int; var pieceColor:int; var stock:Array; var stockPreview:Array; var jeu:Jeu; var preview:Preview; var N:int; init(); function init(){ vitesse = 18; compteur = 0; nextPiece = 1000; piece = 1000; nextPieceColor = 0; pieceL = 0; pieceC = 4; N = 20; stock = []; stockPreview = []; jeu = new Jeu(); preview = new Preview(); addChild(jeu); addChild(preview); preview.x = 212; addEventListener(Event.ENTER_FRAME, main); stage.addEventListener(KeyboardEvent.KEY_DOWN, appuyer) } // appuyer sur une touche function appuyer(e:KeyboardEvent):void{ if (e.keyCode == 37 && verif(-1,0,"g")) { pieceC--; updatePiece(true); }; if (e.keyCode == 39 && verif(1,0,"d")) { pieceC++; updatePiece(true); }; if (e.keyCode == 38) sensPiece(); if (e.keyCode == 40) placePiece(); } function main(e:Event):void { updatePiece(); descendre(); }; // met à jour les pieces function updatePiece(lateral:Boolean=false) { var L:int; var C:int; var bloc:Blocs; if (piece == 1000) { if (nextPiece == 1000) { piece = Math.random()*7; pieceColor = Math.random()*5+1; } else { piece = nextPiece; pieceColor = nextPieceColor; } prochainePiece(); // si la piece touche le haut le joueur perds for (L=0; L<4; L++) { for (C=0; C<4; C++) { if (pieces[piece].map[L][C] && grille[pieceL+L][pieceC+C]) { removeEventListener(Event.ENTER_FRAME, main); trace("game over"); return; } } } // sinon on ajoute la nouvelle piece créée for (L=0; L<4; L++) { for (C=0; C<4; C++) { if (pieces[piece].map[L][C]) { ajouteBloc(C,L,pieceColor, "piece",pieceC,pieceL); } } } // sinon si il y a une piece en cours } else { if (compteur>=vitesse || lateral) { if (verif(0,1,"b")) { if (!lateral) { pieceL++; compteur = 0; } positionPiece(); } else { for (L=0; L<4; L++) { for (C=0; C<4; C++) { if (pieces[piece].map[L][C]) { grille[pieceL+L][C+pieceC] = pieces[piece].map[L][C]; } } } piece = 1000; pieceL = 0; pieceC = 4; for (var i:int=0;i<stock.length;i++){ bloc = stock[i]; if (bloc.T == "piece") { ajouteBloc(bloc.x/N,bloc.y/N,bloc.couleur,"tuile"); retireBloc(bloc,i); i--; } } } } else { compteur++; } } } // choisi la prochaine piece function prochainePiece():void{ var b:Blocs; nextPiece = Math.random()*7; while (nextPieceColor == pieceColor) nextPieceColor = Math.random()*5+1; for each(b in stockPreview) preview.removeChild(b); stockPreview = []; for (var L:int=0; L<4; L++) { for (var C:int=0; C<4; C++) { if (pieces[nextPiece].map[L][C]) { b = new Blocs(); b.couleur = nextPieceColor; b.C = C; b.L = L; b.x = C*N; b.y = L*N; b.T = "piece"; b.gotoAndStop(b.couleur); preview.addChild(b); stockPreview.push(b); } } } } // faire descendre les lignes si elles sont remplies function descendre():void { var ligne:int = 19; var rempli:Boolean; var couleur:int; var C:int; var L:int; var b:Blocs; var i:int // teste toutes les lignes de l'aire de jeu à partir de la dernière while (ligne>=0) { rempli = true; for(C=0; C<10; C++){ if(!grille[ligne][C]) rempli = false; } if (rempli) { for (i=stock.length-1;i>=0; i--){ if(stock[i].L == ligne) retireBloc(stock[i],i); } for (L=ligne; L>0; L--) { for (C=0; C<10; C++) { grille[L][C] = grille[L-1][C]; if (grille[L][C]) { for (i=0;i<stock.length;i++){ b = stock[i]; if(b.L == L-1 && b.C == C) { couleur = b.couleur; retireBloc(b,i); } } ajouteBloc(C,L,couleur,"tuile"); } } } for (C=0; C<10; C++) grille[0][C] = 0; // vide la première ligne de l'aire de jeu ligne++; } ligne--; } } // retire un bloc function retireBloc(b:Blocs,i:int):void{ jeu.removeChild(b); stock.splice(i,1); } // ajoute un bloc function ajouteBloc(C:int,L:int,c:int,T:String,X:int=0,Y:int=0):void{ var b:Blocs = new Blocs(); b.couleur = c; b.C = C; b.L = L; b.x = (C+X)*N; b.y = (L+Y)*N; b.T = T; b.gotoAndStop(c); jeu.addChild(b); stock.push(b); } // envoie la piece en bas function placePiece():void { while(verif(0,1,"b")) { pieceL++; positionPiece(); } compteur = 0; updatePiece(); } // change la position de la piece function positionPiece():void{ for each(var b:Blocs in stock) { if (b.T == "piece") { b.x = (b.C+pieceC)*N; b.y = (b.L+pieceL)*N; } } } // vérifie si la piece peut bouger function verif(col:int,lig:int,dir:String):Boolean { for (var L:int=0; L<4; L++) { for (var C:int=0; C<4; C++) { if(pieces[piece].map[L][C]) { if(dir=="b" && pieceL+L+1>=20) return false; if(dir=="g" && pieceC<=0) return false; if(dir=="d" && pieceC+C+1>9) return false; if(grille[pieceL+L+lig][pieceC+C+col]) return false; } } } return true; } // change le sens de la piece function sensPiece():void { if (piece != 1000 && pieces[piece].testeForme(grille,pieceL,pieceC)){ for (var i:int=stock.length-1; i>=0;i--){ if (stock[i].T == "piece") retireBloc(stock[i],i); } for (var L:int=0; L<4; L++) { for (var C:int=0; C<4; C++) { if (pieces[piece].map[L][C]) ajouteBloc(C,L,pieceColor,"piece",pieceC,pieceL); } } } }
Ha oui, du coup c'est un peu indigeste lu comme ça, on va se détailler tout ça par la suite, en attendant ce n'est pas fini pour le code du programme, nous allons avoir également besoin d'une petite classe qui permet de construire et gérer les pièces, dans le même dossier que votre FLa, créez un nouveau document AS nommé “Piece” et tapez le code suivant :
package { public class Piece { public var map:Array; private var temp:Array; private var C:int; private var L:int; public function Piece(tab:Array) { map = tab; }; // teste si la nouvelle orientation est possible et valide la public function testeForme(grille:Array,pieceL:int,pieceC:int ):Boolean { temp = [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]; for (L=0; L<4; L++) { for (C=0; C<4; C++) { temp[C][3-L] = map[L][C]; } } while (!(temp[0][0] || temp[1][0] || temp[2][0] || temp[3][0])) { for (L=0; L<4; L++) { for (C=1; C<4; C++) { temp[L][C-1] = temp[L][C]; } temp[L][3] = 0; } } for (L=0; L<4; L++) { for (C=0; C<4; C++) { if (temp[L][C]){ if (grille[L + pieceL][C + pieceC] || C + pieceC > 9 || L + pieceL > 19) return false; } } } map = temp; return true; } } }
Cette fois nous avons tout le code, on peut se lancer dans quelques explications.
Etude du programme
L'étude du programme va commencer directement par le programme principal, les maps n'ont pas besoin d'être expliquées plus que ça, quand à la classe “Piece” on y viendra au moment oportun. Notez simplement pour comprendre ce qui suit que la “grille” stocke les cases remplies ou non sur le plateau de jeu, que le “stock” contient tous les blocs visibles (les objets) du jeu y compris ceux des pièces, et que le tableau “pieces” stocke 7 pièces ayant chacune une map de 4*4 blocs déterminant sa forme.
On commence par les classiques, à savoir la déclaration des variables globales.
var vitesse:int; var compteur:int; var piece:int; var nextPiece:int; var nextPieceColor:int; var pieceL:int; var pieceC:int; var pieceColor:int; var stock:Array; var stockPreview:Array; var jeu:Jeu; var preview:Preview; var N:int;
On détaillera celles qui ont besoin de l'être, au fur et a mesure.
init(); function init(){ vitesse = 18; compteur = 0; nextPiece = 1000; piece = 1000; nextPieceColor = 0; pieceL = 0; pieceC = 4; N = 20; stock = []; stockPreview = []; jeu = new Jeu(); preview = new Preview(); addChild(jeu); addChild(preview); preview.x = 212; addEventListener(Event.ENTER_FRAME, main); stage.addEventListener(KeyboardEvent.KEY_DOWN, appuyer) }
On initialise toutes les variables, on vide les tableaux, on crée la zone de jeu et la preview et on les positionnes, puis on ajoute deux écouteurs, un pour boucler sur le programme principal à chaque frame, l'autre pour savoir quand on a appuyé sur une touche. Si vous avez fait les exercices précédents vous savez tout ça par coeur. Notez certaines particularités, “nextPiece” et “piece” sont initialisées à 1000 et non 0, vous verrez pourquoi par la suite. Vous pouvez également prendre en note dès à présent que le stock et le stockPreview ne vont pas être organisés en tableaux à deux dimensions comme la grille, mais en simple liste, les stocks servent à stocker les blocs alors que la grille stocke les références du plateau de jeu.
function main(e:Event):void { updatePiece(); descendre(); };
Le pilote du programme, à partir de là on lance toutes les fonction utiles qui doivent être rafraîchies à chaque frame.
// appuyer sur une touche function appuyer(e:KeyboardEvent):void{ if (e.keyCode == 37 && verif(-1,0,"g")) { pieceC--; updatePiece(true); }; if (e.keyCode == 39 && verif(1,0,"d")) { pieceC++; updatePiece(true); }; if (e.keyCode == 38) sensPiece(); if (e.keyCode == 40) placePiece(); }
Quand le joueur appuie sur la touche gauche on vérifie qu'on peu bouger la pièce, on la déplace et on met à jour l'affichage, idem pour la droite, pour le haut on change le sens de la pièce, pour le bas on la fait descendre jusqu'à la dernière position disponible. Nous reviendrons en détail sur chaque mouvement par la suite, en attendant suivons l'ordre du programme.
La première chose que fait la fonction “main” lorsqu'elle est appelée, c'est de mettre à jour les pièces.
// met à jour les pieces function updatePiece(lateral:Boolean=false) { var L:int; var C:int; var bloc:Blocs; if (piece == 1000) { if (nextPiece == 1000) { piece = Math.random()*7; pieceColor = Math.random()*5+1; } else { piece = nextPiece; pieceColor = nextPieceColor; } prochainePiece(); // si la piece touche le haut le joueur perds for (L=0; L<4; L++) { for (C=0; C<4; C++) { if (pieces[piece].map[L][C] && grille[pieceL+L][pieceC+C]) { removeEventListener(Event.ENTER_FRAME, main); trace("game over"); return; } } } // sinon on ajoute la nouvelle pièce créée for (L=0; L<4; L++) { for (C=0; C<4; C++) { if (pieces[piece].map[L][C]) { ajouteBloc(C,L,pieceColor, "piece",pieceC,pieceL); } } } // sinon si il y a une piece en cours } else { if (compteur>=vitesse || lateral) { if (verif(0,1,"b")) { if (!lateral) { pieceL++; compteur = 0; } positionPiece(); } else { for (L=0; L<4; L++) { for (C=0; C<4; C++) { if (pieces[piece].map[L][C]) { grille[pieceL+L][C+pieceC] = pieces[piece].map[L][C]; } } } piece = 1000; pieceL = 0; pieceC = 4; for (var i:int=0;i<stock.length;i++){ bloc = stock[i]; if (bloc.T == "piece") { ajouteBloc(bloc.x/N,bloc.y/N,bloc.couleur,"tuile"); retireBloc(bloc,i); i--; } } } } else { compteur++; } } }
Pas de stress, on va étudier ça en détail.
var L:int; var C:int; var bloc:Blocs;
On commence par définir quelques variables locales.
if (piece == 1000) { //... } else { //... }
Puis on regarde si la pièce qu'on souhaite mettre à jour est une nouvelle pièce ou pas. Souvenez-vous que nous avons défini la valeur par défaut de la “piece” à 1000 pour indiquer qu'aucune pièce n'est actuellement en cours.
if (nextPiece == 1000) { piece = Math.random()*7; pieceColor = Math.random()*5+1; } else { piece = nextPiece; pieceColor = nextPieceColor; } prochainePiece();
Si il s'agit d'une nouvelle pièce à jouer, on regarde si pour la preview c'est la première pièce à jouer (là aussi par défaut 1000 signifie qu'aucune pièce n'est en cours dans la preview), si c'est le cas on choisi une pièce et sa couleur aléatoirement, sinon on prend la référence de la pièce qui se trouve dans la preview, la prochaine pièce en somme, et on termine dans tous les cas en tirant une nouvelle pièce pour la preview.
// si la piece touche le haut le joueur perds for (L=0; L<4; L++) { for (C=0; C<4; C++) { if (pieces[piece].map[L][C] && grille[pieceL+L][pieceC+C]) { removeEventListener(Event.ENTER_FRAME, main); trace("game over"); return; } } }
On vérifie immédiatement ensuite si un des blocs de la pièce que l'on va poser touche la référence d'un bloc déjà posé dans la grille, si c'est le cas, la pièce étant toujours posée à la même position lorsqu'elle entre en jeu, c'est qu'une autre pièce bloque le passage et que la partie est terminée, on ne peut plus poser de nouvelle pièce. Pour cela on boucle sur tous les blocs de la pièce (composée de 4*4 blocs) et on compares leur position dans la grille (pieceL et pieceC, respectivement la ligne et la colonne où se trouve le début de la pièce). Si on tombe sur une référence occupée dans la grille c'est qu'une autre pièce bouche le passage, on retire l'écouteur principal et la partie est terminée (à vous de faire un joli panneau de fin de partie et un bouton pour en relancer une…).
// sinon on ajoute la nouvelle pièce créée for (L=0; L<4; L++) { for (C=0; C<4; C++) { if (pieces[piece].map[L][C]) { ajouteBloc(C,L,pieceColor, "piece",pieceC,pieceL); } } }
Puis, si la pièce peut être posée dans la grille, on l'ajoute.
} else {
Si il ne s'agit pas d'une nouvelle pièce, mais d'une pièce en cours que l'on souhaite mettre à jour.
if (compteur>=vitesse || lateral) { // ... } else { compteur++; }
On vérifie d'abord si la pièce doit bouger et on effectue les opérations nécessaires, sinon on met à jour le compteur. Le compteur sert ici de cadence pour les mouvements, une pièce ne bouge que lorsque le compteur à atteint ou dépassé la vitesse actuelle définie pour le jeu, ainsi les pièces descendent à une vitesse contrôlée. “lateral” quand à lui est un simple booléen qui indique lorsque le joueur souhaite déplacer une pièce latéralement (voir la gestion des touches).
if (verif(0,1,"b")) { if (!lateral) { pieceL++; compteur = 0; } positionPiece(); } else { for (L=0; L<4; L++) { for (C=0; C<4; C++) { if (pieces[piece].map[L][C]) { grille[pieceL+L][C+pieceC] = pieces[piece].map[L][C]; } } } piece = 1000; pieceL = 0; pieceC = 4; for (var i:int=0;i<stock.length;i++){ bloc = stock[i]; if (bloc.T == "piece") { ajouteBloc(bloc.x/N,bloc.y/N,bloc.couleur,"tuile"); retireBloc(bloc,i); i--; } } }
Si la pièce doit bouger, on vérifie en premier lieu si elle peut descendre d'une ligne, auquel cas si le joueur n'est pas en train de déplacer la pièce latéralement, on fait descendre la pièce d'une ligne et on remet le compteur à zéro. Dans tous les cas (descente ou déplacement latéral) on repositionne la pièce à son nouvel emplacement. Si la pièce ne peut pas descendre, c'est qu'elle a atteint le sol ou touche un bloc qui se trouve en dessous d'elle, on parcours tous les blocs de la pièce et on référence chaque bloc rempli de la pièce dans la grille, en gros on enregistre la position définitive de chaque bloc de la pièce dans la grille. Puis on indique qu'il n'y a plus de pièce en cours, on remet à jour la position de départ de la pièce (celle que la nouvelle pièce devra avoir quand on la pose). Enfin, il nous faut à présent remplacer tous les blocs du stock identifiés comme appartenant à une pièce, par des blocs identifiés comme étant une simple tuile.
Ok je vois bien ici que je perds une partie d'entre vous, c'est normal puisque je ne vous ai pas encore parlé en détail des pièces et de la manière dont elles étaient construites. Comme je le disais plus haut, le stock ne contient que les blocs de couleur (les objets à afficher), leurs références (couleur et position) se trouvent soit dans la grille soit dans les maps pièces. Dans le stock tout est ajouté en vrac, blocs de la grille ou blocs des pièces, afin de distinguer les deux types chaque bloc est affublé d'un paramètre “type” qui est soit “piece” soit “tuile”, ainsi on peut déterminer facilement dans le stock si le bloc testé fait partie d'une pièce, ou des blocs déjà posés, et ainsi transformer facilement un bloc “piece” en bloc “tuile” lorsqu'on veut poser définitivement une pièce sur le plateau de jeu, il suffit de remplacer tous les blocs de la pièce par le bloc correspondant sur le plateau de jeu et de retirer le bloc de la pièce.
Notez la particularité de la dernière boucle, après avoir échangé un bloc “piece” par un bloc “tuile” on décrémente le pointeur de la boucle pour tester à nouveau le même indice du stock, c'est normal puisqu'en retirant le bloc “piece” du stock tous les index sont descendus d'un cran, tester le même index revient en fait à tester le bloc suivant qui a pris la place du bloc qu'on vient de retirer.
// choisi la prochaine piece function prochainePiece():void{ var b:Blocs; nextPiece = Math.random()*7; while (nextPieceColor == pieceColor) nextPieceColor = Math.random()*5+1; for each(b in stockPreview) preview.removeChild(b); stockPreview = []; for (var L:int=0; L<4; L++) { for (var C:int=0; C<4; C++) { if (pieces[nextPiece].map[L][C]) { b = new Blocs(); b.couleur = nextPieceColor; b.C = C; b.L = L; b.x = C*N; b.y = L*N; b.T = "piece"; b.gotoAndStop(b.couleur); preview.addChild(b); stockPreview.push(b); } } } }
Une des premières choses que doit faire le programme, c'est de déterminer quelle sera la prochaine pièce à apparaître, sans quoi la partie ne peut commencer. On fait donc appel à une fonction dédiée qui commence par choisir une nouvelle pièce parmi les 7 modèles disponibles, tant que la couleur de cette nouvelle pièce est identique à la couleur de la pièce en cours, on tire aléatoirement une nouvelle couleur parmi les 5 couleurs disponibles. On retire ensuite tous les blocs présents dans la préview, au cas où il y aurait déjà une pièce présente, puis on vide le stock de la preview, il s'agit simplement d'un tableau temporaire chargé de conserver en mémoire les blocs de la pièce actuellement dans la preview. Enfin, on rempli le stock de la preview avec les blocs de la nouvelle pièce, chaque bloc étant de type “piece”.
// faire descendre les lignes si elles sont remplies function descendre():void { var ligne:int = 19; var rempli:Boolean; var couleur:int; var C:int; var L:int; var b:Blocs; var i:int // teste toutes les lignes de l'aire de jeu à partir de la dernière while (ligne>=0) { rempli = true; for(C=0; C<10; C++){ if(!grille[ligne][C]) rempli = false; } if (rempli) { for (i=stock.length-1;i>=0; i--){ if(stock[i].L == ligne) retireBloc(stock[i],i); } for (L=ligne; L>0; L--) { for (C=0; C<10; C++) { grille[L][C] = grille[L-1][C]; if (grille[L][C]) { for (i=0;i<stock.length;i++){ b = stock[i]; if(b.L == L-1 && b.C == C) { couleur = b.couleur; retireBloc(b,i); } } ajouteBloc(C,L,couleur,"tuile"); } } } for (C=0; C<10; C++) grille[0][C] = 0; // vide la première ligne de l'aire de jeu ligne++; } ligne--; } }
La seconde chose que fait le programme, en dehors des interactions du joueur, c'est d'effacer les lignes remplies et faire descendre tous les blocs qui se trouvent au dessus. Pour simplifier j'utilise des tableaux à deux dimensions, pas top mais plus clair pour le débutant, du coup ça me fait des imbrications d'accolades à rallonge, je m'en excuse, mais c'est moins compliqué qu'il n'y parait, on va y aller pas à pas :
var ligne:int = 19; var rempli:Boolean; var couleur:int; var C:int; var L:int; var b:Blocs; var i:int;
Bon, ben là on a des variables locales, la seule à noter c'est “ligne” qui commence à 19.
while (ligne>=0) { // ... }
Bon ben on a compris, le level fait 20 lignes de haut, on va tester toutes les lignes une par une depuis la dernière jusqu'à la première, donc en remontant la grille si vous préférez.
rempli = true; for(C=0; C<10; C++){ if(!grille[ligne][C]) rempli = false; }
Ok, on cherches à savoir si une ligne est complète, on commence donc par dire que oui, par défaut une ligne est complète… Puis on boucle sur toutes les colonnes de la ligne et on cherche un trou, si il y en a un la ligne n'est pas complète.
if (rempli) { for (i=stock.length-1;i>=0; i--){ if(stock[i].L == ligne) retireBloc(stock[i],i); } for (L=ligne; L>0; L--) { for (C=0; C<10; C++) { grille[L][C] = grille[L-1][C]; if (grille[L][C]) { for (i=0;i<stock.length;i++){ b = stock[i]; if(b.L == L-1 && b.C == C) { couleur = b.couleur; retireBloc(b,i); } } ajouteBloc(C,L,couleur,"tuile"); } } } for (C=0; C<10; C++) grille[0][C] = 0; // vide la première ligne de l'aire de jeu ligne++; } ligne--;
Si la ligne est bien remplie, il faut supprimer tous les blocs de la ligne et faire descendre tout ce qui est au dessus.
On fait une première boucle pour retirer tous les blocs de la ligne.
On fait une seconde boucle qui va de la ligne actuelle à la seconde ligne à partir du haut de la grille, on ne testera pas la première ligne car par défaut elle sera vide. Pour chaque colonne, on fait descendre le contenu des cases dans la grille, donc la référence des cases pleines ou vides dans le plateau de jeu. Pour la grille de référence c'est fait à présent il faut mettre à jour l'affichage, si le contenu de la case descendue n'est pas vide, on cherche dans le stock le bloc correspondant, on récupère sa couleur et on retire le bloc, puis on rajoute un nouveau bloc de type “tuile” au nouvel emplacement. La grille et l'affichage sont à jour, reste une dernière chose à faire, vider la première ligne du haut de la grille, notre boucle s'arrêtant à la seconde ligne, toutes les cases étant descendues, il faut vider cette ligne.
On a fait le plus gros, à présent on attaque les broutilles avant d'étudier de plus près la classe “Piece”.
// retire un bloc function retireBloc(b:Blocs,i:int):void{ jeu.removeChild(b); stock.splice(i,1); } // ajoute un bloc function ajouteBloc(C:int,L:int,c:int,T:String,X:int=0,Y:int=0):void{ var b:Blocs = new Blocs(); b.couleur = c; b.C = C; b.L = L; b.x = (C+X)*N; b.y = (L+Y)*N; b.T = T; b.gotoAndStop(c); jeu.addChild(b); stock.push(b); }
Retirer ou ajouter un bloc est très simple, les blocs sont des éléments graphiques, ils sont stockés en vrac dans le stock, on peut donc les retirer par un simple removeChild et une suppression de l'index dans le stock. Pour en ajouter un nouveau on lui passe simplement les paramètres utiles comme la ligne et la colonne qu'il doit occuper, ce qui nous donne sa position, et son type, puis on l'ajoute à l'affichage et au stock.
// envoie la piece en bas function placePiece():void { while(verif(0,1,"b")) { pieceL++; positionPiece(); } compteur = 0; updatePiece(); }
Envoyer une pièce vers le bas de la grille est également une futilité, tant que la pièce peut descendre (elle ne rencontre pas d'obstacle), on la fait descendre et on met à jour sa position, on remet le compteur à zéro afin de ne pas avoir de latence et on met à jour la pièce.
// change la position de la piece function positionPiece():void{ for each(var b:Blocs in stock) { if (b.T == "piece") { b.x = (b.C+pieceC)*N; b.y = (b.L+pieceL)*N; } } }
Lorsque l'on doit modifier la position d'une pièce pour l'affichage, par exemple lorsque le joueur la bouge à droite, a gauche ou l'envoie vers le bas, on cherche tous les blocs de type “piece” dans le stock et on met à jour leur position. C'est la fonction “updatePiece” qui se chargera de mettre à jour les références des blocs, là on ne s'occupe que de l'affichage.
// vérifie si la piece peut bouger function verif(col:int,lig:int,dir:String):Boolean { for (var L:int=0; L<4; L++) { for (var C:int=0; C<4; C++) { if(pieces[piece].map[L][C]) { if(dir=="b" && pieceL+L+1>=20) return false; if(dir=="g" && pieceC<=0) return false; if(dir=="d" && pieceC+C+1>9) return false; if(grille[pieceL+L+lig][pieceC+C+col]) return false; } } } return true; }
Avec cette fonction on vérifie si une pièce peut bouger, qu'elle soit en train de descendre, ou que le joueur la déplace à l'aide des touches. Pour cela on boucle sur tous les blocs de la map de la pièce, on ne s'occupe que des blocs pleins, on regarde la direction où doit aller la pièce, et en fonction on vérifie si la mouvement est possible. Pour le bas on vérifie si le bloc n'atteint pas le bas de la grille, pour la gauche on vérifie que le bloc ne sort pas de la grille à gauche, idem pour la droite. Et pour terminer, on vérifie toujours pour chaque bloc, si sa nouvelle position n'occupe pas un emplacement déjà pris dans la grille, si aucune de ces conditions n'est validée alors la pièce peut se déplacer.
// change le sens de la piece function sensPiece():void { if (piece != 1000 && pieces[piece].testeForme(grille,pieceL,pieceC)){ for (var i:int=stock.length-1; i>=0; i--){ if (stock[i].T == "piece") retireBloc(stock[i],i); } for (var L:int=0; L<4; L++) { for (var C:int=0; C<4; C++) { if (pieces[piece].map[L][C]) ajouteBloc(C,L,pieceColor,"piece",pieceC,pieceL); } } } }
Et on attaque la dernière fonction du programme principal, qui va me permettre de faire la transition avec l'étude de la classe “Piece”. Le but ici est de permettre au joueur de faire tourner la pièce sur elle même. Pour cela on commence par vérifier si une pièce est bien en cours de jeu, puis on teste si la forme de la pièce en cours est valide (on verra comment en étudiant la classe “Piece”). On parle ici de forme et non de position ou rotation, une pièce étant toujours une map de 4*4 dans laquelle les blocs sont arrangés selon un schéma différent, il s'agit donc toujours d'une forme et non simplement de la rotation d'une pièce, pour une même pièce les blocs sont simplement réarrangés autrement dans la map. Lorsqu'on est sur qu'on peut modifier l'orientation de la pièce, on cherche dans le stock tous les blocs de la pièce en cours et on les retire, puis on ajoute tous les blocs de la nouvelle forme de la pièce. Là encore il faut bien faire la différence entre stock et grille, ici la grille n'intervient pas car la pièce n'est pas posée, ses blocs font bien partie du stock mais pas de la grille qui ne référence que les blocs déjà posés.
Enfin, on s'attaque à la dernière partie, la classe “Piece”.
package { public class Piece { public var map:Array; private var temp:Array; private var C:int; private var L:int; public function Piece(tab:Array) { map = tab; }; // teste si la nouvelle orientation est possible et valide la public function testeForme(grille:Array,pieceL:int,pieceC:int ):Boolean { temp = [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]; for (L=0; L<4; L++) { for (C=0; C<4; C++) { temp[C][3-L] = map[L][C]; } } while (!(temp[0][0] || temp[1][0] || temp[2][0] || temp[3][0])) { for (L=0; L<4; L++) { for (C=1; C<4; C++) { temp[L][C-1] = temp[L][C]; } temp[L][3] = 0; } } for (L=0; L<4; L++) { for (C=0; C<4; C++) { if (temp[L][C]){ if (grille[L + pieceL][C + pieceC] || C + pieceC > 9 || L + pieceL > 19) return false; } } } map = temp; return true; } } }
On a créé 7 modèles de pièce dans le tableau “pieces”, chacune avec une map différente, et on vient piocher un nouveau modèle de pièce dans ce tableau à chaque fois qu'on en a besoin. La seule variable accessible en dehors de la classe est la “map” de la pièce, autrement dit le tableau où elle stocke sa forme, la position de chaque bloc qui la compose. Lorsqu'on crée une nouvelle pièce on met simplement à jour sa map.
La seule fonction utile de la classe sert à modifier la forme de la pièce et tester si la nouvelle disposition des blocs n'interfère pas avec les cases remplies de la grille. Pour cela on passe trois paramètres à la fonction, la grille et la position de la pièce dans la grille. On commence par mettre à toutes les références d'un tableau temporaire qui va nous servir juste après. On parcours tous les index de la map de la pièce et on met à jour le tableau temporaire, attention, vous remarquez sans doute que L et C sont inversés, on inverse donc les lignes et les colonnes de la map afin de faire pivoter la pièce de 90 degrés, après rotation il faut bien sur corriger L pour que la rotation se fasse à partir du point central de la pièce.
La map temporaire de la pièce vient d'être modifiée pour que la pièce effectue une rotation de 90 degrés, immédiatement après on vérifie si la première colonne de chaque ligne est vide, auquel cas on va déplacer tous les blocs pour que cette ligne soit remplie et on vide la dernière colonne du tableau. Ceci nous permet de repositionner la pièce correctement.
Enfin, on effectue une dernière boucle sur tous les blocs de la map temporaire et on compare tous les blocs pleins avec ceux de la grille à la même position (celle de la pièce dans la grille, plus celle du bloc dans la pièce). Si aucun bloc ne sort de la grille ou ne rencontre une case pleine, la nouvelle position est validée, on met donc la map de la pièce à jour, la pièce peut enfin effectuer une rotation.
Conclusion
Tetris est autant un casse tête à jouer qu'à programmer, et le niveau de difficulté est équivalent, on s'agit beaucoup de jouer avec des tableaux, un pour la grille, un pour les blocs à afficher, un pour la forme de chaque pièce, un temporaire pour la rotation de chaque pièce, … Il y a bien sur différentes manières de procéder et je ne suis pas sur que celle que j'ai tentée avec cet exercice soit la plus simple et la plus propre, mais elle fonctionne. Le tout est de ne pas se perdre dans les différents tableaux et références et penser en petits blocs et non en formes générales pour les pièces.
Les sources
