Jeu classique, le Tétris


INTRODUCTION

Qui ne connaît pas le jeu tétris ? Il y a un article sur ce jeu dans :
ICHBIAN Daniel, La saga des jeux vidéo de pong à Lara Croft, vuibert, Paris 2004, p.55.

TELECHARGER

aaaTexte des explications, aaaExecutable (Windows), aaaSource (utilise la librairie Allegro)

 

EXPLICATIONS

1. Principe

Des formes simples, comme des legos emboîtables, descendent du haut d’un espace de jeu rectangulaire. Arrivées en bas, soit que la forme s’appuie sur une forme déjà là, soit qu’elle touche la limite en base de la zone du jeu, elle s’immobilise et une autre pièce commence à descendre. Pendant la descente de chaque pièce le joueur essaye de les orienter afin qu’elles s’encastrent en bas et constituent des lignes pleines. Chaque constitution d’une ligne pleine qui couvre la largeur de la surface de jeu disparaît, donne des points mais fait croître la rapidité de la descente et donc augmente la difficulté du jeu.


2. Mise en forme et initialisation

L’idée ici est d’appuyer la construction du tétris sur des matrices. Une matrice 2D pour la zone de jeu et un ensemble de quatre matrices 2D pour chaque forme. L’algorithme sera essentiellement de faire coïncider les mouvements des formes (rotations, déplacement latéraux, descente) avec le plan du jeu. C'est-à-dire contrôler que la forme reste dans le plan et qu’aucune autre forme précédente n’entrave son mouvement.

Plan 2D du jeu

Le plan du jeu est définie en globale par une matrice de TY hauteur sur TX largeur :

#define TY 10
#define TX 20
char PLAN[TY][TX];

Codage des formes

Chaque pièce peut-être codée avec une matrice 2D de 4 lignes sur 4 colonnes mais du fait des rotations chaque pièce doit pouvoir apparaître sous quatre angles différents. Si l’on prend par exemple la forme barre « L envers » ça donne :

Il y a moyen de coder chaque pièce dans ses quatre positions par une matrice à 3 dimensions :



Dans le jeu classique il y a sept formes différentes et nous aurons autant de tableaux 3D dans le jeu nommés : la barre IF, le L à l’envers JF, le L à l’endroit LF, le carré OF, le T TF, la marche escalier SF, l’autre marche ZF.

Ils sont tous initialisés en dur sur un fichier C à part, par exemple la barre donne :

char IF[4][4][4]={
aaaaaaaaaaaaaaaaaaa{
aaaaaaaaaaaaaaaaaaaaaa{0, 0, 6, 0},
aaaaaaaaaaaaaaaaaaaaaa{0, 0, 6, 0},
aaaaaaaaaaaaaaaaaaaaaa{0, 0, 6, 0},
aaaaaaaaaaaaaaaaaaaaaa{0, 0, 6, 0}
aaaaaaaaaaaaaaaaaaa},
aaaaaaaaaaaaaaaaaaa{
aaaaaaaaaaaaaaaaaaaaaa{0, 0, 0, 0},
aaaaaaaaaaaaaaaaaaaaaa{0, 0, 0, 0},
aaaaaaaaaaaaaaaaaaaaaa{6, 6, 6, 6},
aaaaaaaaaaaaaaaaaaaaaa{0, 0, 0, 0}
aaaaaaaaaaaaaaaaaaa},
aaaaaaaaaaaaaaaaaaa{
aaaaaaaaaaaaaaaaaaaaaa{0, 0, 6, 0},
aaaaaaaaaaaaaaaaaaaaaa{0, 0, 6, 0},
aaaaaaaaaaaaaaaaaaaaaa{0, 0, 6, 0},
aaaaaaaaaaaaaaaaaaaaaa{0, 0, 6, 0}
aaaaaaaaaaaaaaaaaaa},
aaaaaaaaaaaaaaaaaaa{
aaaaaaaaaaaaaaaaaaaaaa{0, 0, 0, 0},
aaaaaaaaaaaaaaaaaaaaaa{0, 0, 0, 0},
aaaaaaaaaaaaaaaaaaaaaa{6, 6, 6, 6},
aaaaaaaaaaaaaaaaaaaaaa{0, 0, 0, 0}
aaaaaaaaaaaaaaaaaaa}
aaaaaaaaaaaaaaa};

Et ils font l’objet ensuite d’une déclaration « extern » dans le fichier C avec l’algorithme qui les utilise :

extern char IF[4][4][4];
extern char JF[4][4][4];
extern char LF[4][4][4];
extern char OF[4][4][4];
extern char TF[4][4][4];
extern char SF[4][4][4];
extern char ZF[4][4][4];


La forme dans le plan



Trois variables globales vont permettre de contrôler le mouvement de chaque forme :

int XP ;aaaaaaaaapour la position horizontale et
int YP ;aaaaaaaaapour la position verticale à partir du coin haut gauche dans le plan
aaaaaaaaaaaaaaaAu départ XP centre à l’horizontal la forme dans l’espace du jeu et YP
aaaaaaaaaaaaaaaplace la forme en haut de l’espace de jeu :
aaaaaaaaaaaaaaaXP=3 ;
aaaaaaaaaaaaaaaYP=0 ;

int FCOUR ;aaaaapour la position de rotation (les indices 0,1,2,3 de la première dimension
aaaaaaaaaaaaaaadu tableau 3D FORME
aaaaaaaaaaaaaaaAu départ FCOUR est initialisé sur la première position de la forme :
aaaaaaaaaaaaaaaFCOUR=0 ;

D’autre part une seule forme est utilisée à la fois dans le plan, c’est la forme « courante » et la forme « suivante » est affichée sur le côté du plan pendant le mouvement de la forme courante. Il est donc nécessaire de pouvoir manipuler les tableaux à l’aide de deux variables de type pointeur sur tableau 2D, un pointeur pour la forme courante et un pointeur pour la forme suivante.

Le type pointeur de tableau 2D est définit comme suit :

aaaaatypedef char (*forme)[ 4 ][ 4 ] ;

et nous avons en globale les deux pointeurs :

aaaaaforme courante ;
aaaaaforme suivante ;

Au début de la partie ces pointeurs prennent aléatoirement chacun l’adresse d’une forme avec la fonction suivante :

aaaaaforme init_piece()
aaaaa{
aaaaaaaaaaswitch(rand()%7){

aaaaaaaaaaaaacase 0 : return SF;
aaaaaaaaaaaaacase 1 : return ZF;
aaaaaaaaaaaaacase 2 : return LF;
aaaaaaaaaaaaacase 3 : return JF;
aaaaaaaaaaaaacase 4 : return TF;
aaaaaaaaaaaaacase 5 : return IF;
aaaaaaaaaaaaacase 6 : return OF;
aaaaaaaaaa}
aaaaaaaaaareturn NULL;
aaaaa}

L’appel se fait de la façon suivante avant la boucle d’événements du jeu :
aaaaa(…)
aaaaacourante = init_piece();
aaaaasuivante = init_piece();
aaaaa(…)

Lorsque la forme courante a terminé sa course et se trouve bloquée en bas, le changement de forme courante est opéré de la façon suivante :

aaaaavoid init_piece_suivante()
aaaaa{
aaaaaaaaaacourante=suivante;
aaaaaaaaaasuivante=init_piece();
aaaaaaaaaaXP=3;
aaaaaaaaaaYP=0;
aaaaaaaaaaFCOUR=0;
aaaaa}

« courante » prend la valeur de « suivante », suivante est réinitialisée avec la fonction init_piece() et les valeurs de départ sont réaffectées aux variables globales XP, YP et FCOUR.

3. Processus générateur

Dépôt des formes dans la zone de jeu
Le principe du jeu est que les formes arrivent en bas de la zone de jeu et y soient stockées selon l’adresse du joueur plus ou moins bien encastrées les unes dans les autres. Le stockage des formes dans le plan consiste en une simple recopie de la matrice forme à partir de la position finale dans la zone de jeu donnée par les variables globales (XP, YP).
Plus précisément ne seront copiées que les valeurs des cases positives et différentes de zéro. C’est le rôle de la fonction fixer_piece() :

aaaaavoid fixer_piece(forme F)
aaaaa{
aaaaaint x,y;

(1)aaaaafor (y=0; y<4; y++)
aaaaaaaaaaafor (x=0; x<4; x++)
(2)
aaaaaaaaaaaif ( F[FCOUR][y][x] )
(3)
aaaaaaaaaaaaaaPLAN[YP+y][XP+x]=F[FCOUR][y][x];
aaaaa}

aaaaa(1) Toutes les positions de la matrice forme « F » sont passées en revue,
aaaaa(2) Si l’on est sur une position (x,y) différente de zéro
aaaaa(3) La valeur de cette position est recopiée à la position (XP+x, YP+y) de la matrice PLAN.

Les mouvements

Trois types de mouvements possibles dans le jeu : latéraux, rotation de la pièce et descente.

La situation est la suivante :

aaaaa

(XP,YP) donne la position de la matrice « forme » (en gris) dans le plan. (x, y) donnent la taille de la matrice forme.
Pour bouger la matrice forme il faut modifier XP et YP et vérifier que les toutes les positions de la matrice forme (XP+x, YP+y ) :
aaaa1) restent dans la zone de jeu
aaaa2) ne se superposent pas avec des formes déjà déposées dans la zone de jeu

Mouvements latéraux

Il suffit de modifier la variable XP :
La touche clavier flèche gauche diminue XP de 1, la touche flèche droite augmente XP de 1
Le test vérifie d’abord si la forme reste dans la surface de jeu, c'est-à-dire la matrice PLAN et ensuite vérifie que la forme ne se superpose pas à une autre forme déjà déposée dans la matrice PLAN. Ce sont les deux fonctions move_gauche() et move_droite() :

aaaaavoid move_gauche(forme F)
aaaaa{
aaaaaint x,y;

(1)aaaaafor (y=0; y<4; y++)
aaaaaaaaaaafor (x=0; x<4; x++)
(2)
aaaaaaaaaaaif ( F[FCOUR][y][x] && ( (XP-1+x<0) || (PLAN[YP+y][XP-1+x]) ) )
aaaaaaaaaaaaaaaaareturn;
(3)
aaaaaXP--;
aaaaa}

(1) Toutes les positions de la matrice forme sont passées en revue
(2) Si on est sur une position différente de zéro, le mouvement n’aura pas lieu si :
aaaaa- si cette position transposée dans le plan du jeu avec ajout de la variable XP diminuée de 1 (cause mouvement gauche) est aaaaaastrictement inférieure à 0
aaaaa- ou si la position correspondante dans la matrice est déjà occupée
aaaDans ces deux cas sortie de la fonction sans modification de la variable XP.
(3) Si la forme reste dans la zone de jeux et que les positions correspondantes sont
aaalibres XP est diminuée de 1

Pour la fonction move_droite le principe est identique avec une modification du test qui porte maintenant sur le bord droit (TX) et non plus gauche ( avec 0 )

aaaaavoid move_droite(forme F)
aaaaa{
aaaaaint x,y;

(1)aaaaaafor (y=0; y<4; y++)
aaaaaaaaaaaafor (x=0; x<4; x++)
(2)
aaaaaaaaaaaaif ( F[FCOUR][y][x] && ( (XP+1+x>=TX) || (PLAN[YP+y][XP+1+x]) ) )
aaaaaaaaaaaaaaaaaareturn;
(3)
aaaaaaXP++;
aaaaa}

(1) Toutes les positions de la matrice forme sont passées en revue
(2) Si on est sur une position différente de zéro, le mouvement n’aura pas lieu si :
aaaaa- si cette position transposée dans le plan du jeu avec ajout de la variable XP augmentée de 1 (cause mouvement droite) est aaaaaastrictement inférieure ou égal à TX
aaaaa- ou si la position correspondante dans la matrice est déjà occupée
aaaDans ces deux cas sortie de la fonction sans modification de la variable XP.
(3) Si la forme reste dans la zone de jeux et que les positions correspondantes sont
aaalibres XP est augmentée de 1

Rotation de la pièce

La rotation de la pièce laisse inchangée la position XP,YP en revanche la représentation de la forme change. Cette représentation est donnée par FCOUR qui indique le numéro d’ordre de la matrice forme courante. La rotation à gauche diminue de 1 FCOUR afin de venir à la matrice précédente. Ce mouvement est obtenue par pression de la touche alt. La rotation à droite augmente de 1 FCOUR afin de passer à la matrice suivante.
La fonction rotation() prend deux arguments, la forme concernée et le sens (gauche ou droite, -1 ou 1) de la rotation :

aaaaavoid rotation ( forme F, int g_d)
aaaaa{
aaaaaint suiv, x, y;
(1)
aaaaasuiv = ((FCOUR+g_d)+4)%4;
(2)
aaaaafor (y=0; y<4; y++)
aaaaaaaaaafor (x=0; x<4; x++)
aaaaaaaaaaaaaif ( (F[suiv][y][x]) && a( XP+x <0 || XP+x >=TX || aYP+y >=TY || PLAN[YP+y][XP+x] ) )
aaaaaaaaaaaaaaaareturn;

(3)
aaaaaFCOUR=suiv;
aaaaa}

(1) La rotation ne pourra avoir lieu que si la pièce dans sa nouvelle position reste dans la zone de jeu et ne se superpose pas avec une forme déjà fixée dans la zone de jeu. Pour ce faire l’éventuelle position suivante, si la rotation a lieu, est d’ abord récupérée dans une variable temporaire « suiv » qui va permettre de faire tous les tests nécessaires. La formule ((FCOUR+d_G)+4)%4 permet de rester dans l’interval 0-3 quelque soit la valeur -1 ou 1 de la variable « g_d ».

(2) Ensuite toutes les positions de la matrice forme suivante sont examinées :
aaaaa- Si la position est position différente de zéro,
aaaaa- Le mouvement n’aura pas lieu si cette position transposée à la matrice par ajout de XP est soit inférieure à 0 soit supérieure ou aaaaaaégal à TX.
aaaaa
- Le mouvement n’auraapas lieu non plus si la position dans la zone de jeu, c’est dire dans la matrice PLAN, n’est pas libre.
aaaaaDans tous ces cas la sortie de la fonction est provoquée sans modification de la variable FCOUR.

(3) Sinon la variable FCOUR prend la valeur de la variable « suiv ».

Descente

Horizontalement il n’y que la descente de possible et elle s’effectue de toute façon automatiquement à un rythme qui dépend du niveau atteint dans la partie (le code correspondant est expliqué plus bas avec le main() et la boucle d’événements). Inexorablement la pièce descend et il faut absolument lui trouver une place dans l’empilement en bas.
La fonction descendre() contrôle le processus de descente. Elle renvoie la valeur 0 si la forme, au coup d’après, va sortir du plan ou se superposer à une autre, c'est-à-dire lorsqu’elle doit s’immobiliser et qu’une autre fore soit envoyée dans le jeu. Elle renvoie la valeur -1 si en plus de cette immobilité la variable YP est égale à 1 c'est-à-dire s’il n’y a plus possibilité de faire descendre des nouvelles formes et que la partie est finie et perdue.

aaaaaint descendre( forme F)
aaaaa{
aaaaaint x,y,res=1;

(1)aaaaaaYP++;
(2)
aaaaaafor (y=0; y<4; y++)
aaaaaaaaaaafor (x=0; x<4; x++){
aaaaaaaaaaaaaaif ( F[FCOUR][y][x] && a( (YP+1+y>=TY)||(PLAN[YP+1+y][XP+x]) ) ){
aaaaaaaaaaaaaaaaaif (YP==1)
aaaaaaaaaaaaaaaaaaaares=-1;
aaaaaaaaaaaaaaaaaelse
aaaaaaaaaaaaaaaaaaaares=0;
aaaaaaaaaaaaaaaaa// fin boucle éventuellement
aaaaaaaaaaaaaaaaay=4;
aaaaaaaaaaaaaaaaax=4;
aaaaaaaaaaaaaa}
aaaaaaaaa}
(3)
aaaaaareturn res;
aaaaa}

(1) La variable YP est incrémentée dés le début.

(2) Toutes les positions correspondant à la forme courante sont passées en revue. Pour montrer que la forme ne peut plus descendre les deux conditions suivantes doivent être réunies :
aaaaa- on est sur une position différente de 0 dans la matrice forme
aaaaa- et soit la position verticale transposée à l’espace de jeu sort de l’espace de jeu, c'est-à-dire si la variable YP+1 dépasse la taille aaaaaaverticale TY de la matrice PLAN soit cette position se superpose à une valeur différente de 0 dans la matrice PLAN (c'est-à-dire si aaaaaaune forme y a déjà été déposée auparavant).

S’il apparaît que la forme ne peut plus descendre il faut en plus vérifier que la partie n’est pas finie à savoir que YP est différent de 1. En effet l’initialisation de YP est 0 pour une forme au départ et comme YP est premièrement incrémentée de 1 au début de la fonction, au minimum YP est égal à 1. Si donc YP est en plus égal à 1 la partie est finie et le code de retour -1 est stocké dans la variable res. Si YP est différent de 1 la partie n’est pas finie, le code de retour est alors 0, la pièce courante devient la pièce suivante, la pièce suivante est réinitialisée.

(3) Retour du résultat.


Contrôle des mouvements au clavier

Nous avons vu l’implémentation des différents mouvements possibles dans le jeu, ces mouvements sont commandés avec le clavier

contrôle du clavier

Le clavier est abordé dans l’environnement allegro avec les scancodes, ce qui est très rapide et simple d’utilisation :
aaaaaif (key[KEY_UP])
aaaaaaaa// actions
Cependant il est nécessaire ici de contrôler la répétition des touches d’où l’ajout de la fonction suivante :

(1)aavoid cntl_clavier(int k, int *m){

(2)aaaaaif ( ! key[k])
aaaaaaaaaa*m=0; // si non appuyée m à 0
aaaaaaaelse
aaaaaaaaaa(*m)++; // si appuyée m augmenté de 1
aaaaa}

(1) La fonction cntl_clavier() prend deux arguments. « k » va correspondre à une valeur de scancode et désigner une touche du clavier. « m » est une variable entière passée par référence dont on pourra commettre la valeur dans le contexte d’appel : si m vaut 0 c’est que la touche n’est pas appuyée et sinon c’est que la touche est déjà appuyée. Dans le contexte d’appel cette variable sera déclarée de type static.

(2) Pour savoir la touche « k » est appuyée ou non il suffit de mettre « m » à 0 lorsqu’elle n’est pas appuyée et sinon d’incrémenter « m » (ce qui va permettre éventuellement de contrôler le nombre de tours pendant lesquels la touche reste appuyée).

Dans la fonction appelante nous avons une utilisation de la fonction cntl_clavier() de la façon suivante, par exemple pour le contrôle de la touche flèche droite :

aaaaastatic int cntl=0 ;

aaaaaaaacntl_clavier( KEY_RIGHT, &cntl) ;
aaaaaaaaif (cntl==1)
aaaaaaaaaaamove_droite(courante);

Dans cet exemple move_droite() n’est appelée que si cntl vaut 1 et n’est pas appelé lorsque la touche n’est pas pressée si cntl vaut 0 ou en cas de répétition si cntl vaut plus que 1.
Autre possibilité de contrôle, qui accepte la répétions à partir d’une pression longue :

aaaaaaaaif (cntl==1 || cntl>15)
aaaaaaaaaaamove_droite(courante);

contrôle des mouvements lattéraux, rotation et descente

Les contrôles des mouvements latéraux, de rotation et de descente sont regroupés dans la fonction move() suivante :

aaaaavoid move()
aaaaa{
(1)
aaa#define GAUCHE -1
aaaaa#define DROITE 1
aaaaastatic int mg=0,md=0,rg=0,rd=0,dcs=0;

(2)aaaaaacntl_clavier(KEY_RIGHT,&md);
aaaaaaaaif( md==1 || md>15)
aaaaaaaaaaamove_droite(courante);

aaaaaaaacntl_clavier(KEY_LEFT, &mg);
aaaaaaaaif(mg==1 || mg >15)
aaaaaaaaaaamove_gauche(courante);

(3)aaaaacntl_clavier(KEY_LCONTROL, &rg);
aaaaaaaaif (rg==1)
aaaaaaaaaaarotation(courante,GAUCHE);a

aaaaaaaacntl_clavier(KEY_ALT, &rd);
aaaaaaaaif (rd==1)
aaaaaaaaaaarotation(courante,DROITE);

(4)
aaaaacntl_clavier(KEY_DOWN, &dcs);
aaaaaaaaif (dcs)
aaaaaaaaaaat_piece=200;
aaaaa}

(1) La fonction move() ne prend pas d’argument et ne renvoie rien. Une suite de variables entières static sont déclarée et initialisée lors du premier appel de la fonction à la valeur 0. Deux noms symboliques sont définis GAUCHE et DROITE pour les valeurs de -1 et 1.

(2) Contrôles des mouvements latéraux avec les flèches droites et gauche

(3) Contrôle des mouvement de rotation avec les flèches Alt et Ctrl de gauche

(4) Contrôle de la descente avec la flèche bas. Indépendamment du contrôle clavier la descente s’effectue de toute façon automatiquement et plus ou moins vite selon l’avancement de la partie.
Les rythmes correspondent aux différents niveaux de la partie. Chaque rythme est une valeur entière par exemple 70 (premier niveau). Dans le main les niveaux sont stockés dans un tableau d’entiers :

aaaaaaaaint niveau[25]= {70, 65, 60, 55, 50, 45, 40, 35, 30, 26, 22, 19, 16, 14, 12, 10, 8, 7, 6, 5, 4, 3, 2, 1, 0};

et le niveau courant est géré via une variable « speed » qui indique à quel indice on est dans le tableau.
Une variable globale « t_piece » est incrémentée régulièrement via un timer et dans la boucle d’événements du main() la descente est effectuée à chaque fois que la variable t_piece est supérieure à la valeur du niveau courant de la façon suivante :

aaaaaaaaif (t_piece>niveau[speed]){
aaaaaaaaaaat_piece=0 ;
aaaaaaaaaaa// action descente
aaaaaaaa}

Ainsi donc mettre la variable globale t_piece à 200 si la touche flèche bas est appuyée équivaut à forcer la vitesse de descente indépendamment du rythme courant.

« game logic », suppression des lignes horizontales pleines

La « logique » du jeu correspond à la gestion de la zone de jeux, la matrice PLAN. Quand une pièce ne peut plus descendre elle est « fixée » dans la zone de jeu c’est dire qu’elle est recopiée dans la matrice PLAN à sa position finale avec la fonction fixer_piece() vue précédemment. La pièce courante prend alors la valeur de la pièce suivante et une nouvelle future pièce suivante est initialisée avec la fonction init_piece(). La transition et réalisée avec un appel unique à la fonction init_piece_suivante() également vu précédemment. Il reste la question de la suppression des lignes horizontales pleines, chaque ligne horizontale pleine est en effet une victoire du joueur et leurs suppressions permet de durer contre l’empilement inexorable...

Compter les lignes finies

A chaque fin de tour, lorsqu’une pièce est immobilisée et qu’une nouvelle va être introduite dans la partie, il faut vérifier s’il n’y a pas une ou plusieurs lignes horizontales qui seraient constituées. Il faut donc parcourir chaque ligne de la matrice PLAN et vérifier si elle est pleine, si oui conserver le numéro de ligne afin de pouvoir la supprimer ensuite.
Parcourir chaque ligne est assez simple, la ligne est pleine si toutes ses positions dans la matrice contiennent une valeur différente de zéro. L’idée pour le stockage consiste à avoir un tableau d’entier de la taille du nombre de lignes TY initialisé à 0, à chaque fois qu’une ligne pleine est trouvée, la valeur 1 est affectée à sa position dans le tableau, ensuite il suffit de parcourir ce tableau pour savoir quelles sont les lignes pleines à retirer de la zone de je, la matrice PLAN. La situation est la suivante :

aaaaa

Répertorier les lignes pleines peut s’effectuer avec la fonction suivante :

(1)aaaint compter_lignes_finies(int *lgns)
aaaaa{
aaaaaint y,x,cmpt=0;
(2)aaaaaamemset(lgns,0,sizeof(int)*TY);
(3)
aaaaaafor (y=0; y<TY; y++){
aaaaaaaaaaafor (x=0; x<TX; x++)
aaaaaaaaaaaaaaif(! PLAN[y][x])
aaaaaaaaaaaaaaaaabreak;
(4)
aaaaaaaaaif (x==TX){
aaaaaaaaaaaaaalgns[y]=1;
aaaaaaaaaaaaaacmpt++;
aaaaaaaaaaa}
aaaaaaaa}
(5)
aaaaareturn cmpt;
aaaaa}

(1) La fonction compter_ligne_finie() est appelée dans la boucle d’événements, dans le main(). Elle prend un tableau en argument et il est déclaré dans le main. La fonction retourne une valeur entière, le nombre de lignes et la suppression des lignes pourra n’être appelée ensuite que s’il y a des lignes à supprimer.

(2) Initialisation à 0 du tableau avec la fonction memset().

(3) Le parcours de la matrice PLAN pour chaque ligne y toutes les positions horizontales x sont évaluées mais dés qu’une valeur de 0 est trouvé sur une ligne le passage à la ligne suivante est forcé avec l’instruction break ;

(4) A la fin du parcours de chaque ligne si x à la valeur de TX c’est que la ligne « y » est pleine dans ce cas la valeur 1 est affectée à l’indice y dans le tableau « lgns » et le compteur de lignes est augmenté de 1.

(5) Le nombre de lignes pleines trouvées est retourné.

Effacer les lignes finies

Pour faire disparaître les lignes finies l’idée est simplement de ne recopier que les autres dans l’ordre où elles se présentent selon le principe suivant :

aaaaa

Dans une matrice miroir de la zone de jeu « PLAN », les lignes non finies uniquement sont recopiées les unes à la suite des autres en partant du bas. La fonction de recopie des lignes non finies est la suivante :

(1)aaavoid effacer_lignes_finies(int *lgns)
aaaaa{
aaaaachar plan[TY][TX];
aaaaaint y,k;

(2)aaaaaamemset(plan,0,sizeof(char)*TY*TX);
(3)
aaaaaafor (k=y=TY-1; y>0; y--)
aaaaaaaaaaaif(!lgns[y]){
aaaaaaaaaaaaaamemcpy(&plan[k],&PLAN[y],sizeof(char)*TX);
aaaaaaaaaaaaaak--;
aaaaaaaaaaa}
(4)
aaaaaamemcpy(PLAN,plan,sizeof(char)*TY*TX);
aaaaaa}

(1) La fonction ne renvoie rien et a en paramètre un pointeur d’entier. Ce pointeur est destiné à recevoir un le tableau utilisé dans la fonction précédente compter_ligne_finie(), un tableau d’entiers de TY taille. Local à la fonction une matrice miroir de la matrice PLAN qui représente la jeu de jeu.

(2) La matrice locale destinée à recevoir les résultats de la copie des lignes non finies est d’abord initialisée à 0.

(3) Dans la boucle for deux variables k et y sont initialisées sur l’indice de la dernière ligne TY-1, y va suivre les lignes de la matrice PLAN et k va progresser en fonction des lignes non finies trouvées. Le tableau « lgns » contient des valeurs de 0 pour une ligne non finie et 1 pour une ligne finie. Chaque ligne non finie est recopiée avec la fonction memcpy() dans la matrice locale « plan ».

(4) A l’issu la matrice résultante « plan » est recopiée dans la matrice « PLAN » zone de jeu.

Affichage

Les aspects graphiques consiste en huit bitmaps : sept carreaux de couleurs avec un léger relief qui servent à constituer les différentes pièces et le dessin du pourtour de la zone de jeu. Toutes ces bitmaps sont regroupées dans un fichier « * .dat » créé avec l’utilitaire Grabber de la librairie Allegro.

L’affichage se fait dans un double buffer, une BITMAP* déclarée en globale et nommée PAGE :

aaaaaaaaBITMAP*PAGE ;

Tout ce qui concerne l’affichage est regroupé finalement dans une seule procédure : cntl_affichage() et cette procédure est simplement appelée une fois au début dans la boucle d’événements. Cette procédure regroupe et coordonne les appels de trois sous-fonctions :
aaaaaaaa- dessine_plan() est chargée d’afficher la zone de jeu c'est-à-dire le contenu de la matrice PLAN
aaaaaaaa- dessine_forme_dans_plan() affiche la forme courante à sa position courante
aaaaaaaa- dessine_forme_hors_plan() affiche la forme suivante en attente sur le bord de la zone de jeu.


Gestion des ressources bitmap

Créer un fichier dat avec le Grabber renvoie à la documentation du Grabber. Dans le programme il apparaît via la structure de donnée suivante :

aaaaaaaatypedef struct DATAFILE
aaaaaaaa{
aaaaaaaaaaavoid *dat; - pointeur sur la donnée actuelle
aaaaaaaaaaaint type; - le type de la donnée
aaaaaaaaaaalong size; - la taille de la donnée en octets
aaaaaaaaaaavoid *prop; - une des propriétés de l’objet
aaaaaaaa} DATAFILE;

A titre indicatif, le champ type peut contenir une des valeurs suivantes (voir documentation allegro à datafile routines ) :

aaaaaaaaDAT_FILE - dat pointe vers a nested datafile
aaaaaaaaDAT_DATA - dat pointe vers un bloc de données binaires
aaaaaaaaDAT_FONT - dat pointe vers un objet fonte
aaaaaaaaDAT_SAMPLE - dat pointe vers une structure SAMPLE
aaaaaaaaDAT_MIDI - dat pointe vers un fichier MIDI
aaaaaaaaDAT_PATCH - dat pointe vers un « GUS patch file »
aaaaaaaaDAT_FLI - dat pointe vers une animation FLI/FLC
aaaaaaaaDAT_BITMAP - dat pointe vers une structure BITMAP
aaaaaaaaDAT_RLE_SPRITE - dat pointe vers une structure RLE_SPRITE
aaaaaaaaDAT_C_SPRITE - dat pointe vers un « linear compiled sprite »
aaaaaaaaDAT_XC_SPRITE - dat pointe vers un « mode-X compiled sprite »
aaaaaaaaDAT_PALETTE - dat pointe vers an tableau de 256 RGB structures
aaaaaaaaDAT_END - flag spécial qui marque la fin de la liste des données

En ce qui concerne notre programme, une fois ce fichier créé il est utilisé dans le programme via un pointeur du type DATAFILE *, déclaré en globale :

aaaaaaaaDATAFILE *GRPH;

L’initialisation du pointeur se fait avec la fonction load_datafile() de l’environnement Allegro de la façon suivante :

aaaaaaaaGRPH=load_datafile(".//graphic.dat");
aaaaaaaaif(!GRPH ){
aaaaaaaaaaaallegro_message("erreur GRPH");
aaaaaaaaaaaallegro_exit();
aaaaaaaaaaaexit(1);
aaaaaaaa}


Cette initialisation a lieu dans la fonction init_env() présentée plus bas dans la section « mise en œuvre dans le main et boucle d’événements.
Dans le programme, le fichier dat apparaît alors sous la forme d’un tableau, chaque ressource correspond à un indice du tableau et peut être récupérée de la façon suivante :

aaaaaaaaBITMAP * bmp ;

aaaaaaaaaaabmp=(BITMAP*)GRPH[ 1 ].dat

Attention le cast est nécessaire car le champ dat est un void*. Dans le programme les carreaux de couleurs correspondent aux indices 1 à 7 compris, valeurs utilisées pour la définition des formes (voir section « codage des formes » plus haut). L’indice 8 correspond à l’image de la zone de jeu.

Commande de l’affichage et du rafraîchissement dans la boucle des événements

Avant de chacune des fonctions qui composent l’affichage, voici tout d’abord la fonction qui les regroupe et les articule. Cette fonction du contrôle global de l’affichage cntl_affichage() est appelée une fois au début de la boucle d’événement :

aaaaavoid cntl_affichage()
aaaaa{
(1)
aaaaaaclear(PAGE);
(2)
aaaaaadraw_sprite(PAGE,(BITMAP*)GRPH[8].dat, 210,30);
(3)
aaaaaadessine_plan();
(4)
aaaaaadessine_forme_dans_plan(courante);
(5)
aaaaaadessine_forme_hors_plan(suivante,500,100);
(6)
aaaaaablit(PAGE,screen,0,0,0,0,640,480);
aaaaa}

(1) Tout d’abord le double buffer, c'est-à-dire la bitmap PAGE déclarée en globale est effacée. C’est la bitmap qui sert à assembler tous les éléments de l’image avant affichage à l’écran.

(2) Ensuite comme premier élément graphique de l’assemblage vient le dessin du cadre de la zone de jeu. Ce dessin est une bitmap à l’indice 8 du tableau GRPH[ ] des ressources du jeu.

(3) La fonction dessine_plan() s’occupe de dessiner dans PAGE la matrice PLAN[ ][ ]

(4) La fonction dessine_forme_dans_plan() prend en paramètre le pointeur courante et dessine la forme en train de descendre à sa position courante dans la zone de jeu

(5) La fonction dessine_forme_hors_plan() concerne l’affichage de la forme suivante déjà visible sur le bord de la zone de jeu. Elle prend en paramètre le pointeur suivante et la position à l’écran.

(6) L’affichage final de la bitmap PAGE complète à l’écran avec la fonction allegro blit().

Le plan et l’accumulation des formes

La fonction dessine_plan() permet de transposer graphiquement l’état de la matrice PLAN. Il s’agit simplement de parcourir la matrice PLAN. Lorsqu’une position est différente de 0, à savoir une valeur entre 1 et 7, afficher la bitmap à l’indice correspondant dans le tableau GRPH[ ] :

aaaaavoid dessine_plan()
aaaaa{
aaaaaBITMAP *bmp;
aaaaaint x,y,x2,y2,k;

(1)aaaaaafor (y=0; y<TY; y++)
aaaaaaaaaaafor (x=0; x<TX; x++)
aaaaaaaaaaaaaaif (k=PLAN[y][x]){ // attention c'est bien une affectation
(2)
aaaaaaaaaaaaaabmp=(BITMAP*)GRPH[k].dat;
aaaaaaaaaaaaaaaaax2= MARGE_HOR + x*bmp->w;
aaaaaaaaaaaaaaaaay2= MARGE_VER + y*bmp->h;
aaaaaaaaaaaaaaaaablit(bmp,PAGE,0,0,x2,y2,bmp->w,bmp->h);
aaaaaaaaaaaaaa}
aaaaa}

(1) Chaque position de la matrice PLAN est évaluée avec une double boucle for. La valeur de la position est récupérée dans la variable
« k ». Si sa valeur est supérieure à 0 les instructions suivantes sont exécutées :

(2) L’adresse de la bitmap correspondante, c'est-à-dire à l’indice « k » dans le tableau GRPH[ ] de ressources dat, est récupérée dans un BITMAP* local « bmp » et sa position est calculée. MARGE_HOR et MARGE_VER sont deux macros qui définissent un décalage horizontal et un décalage vertical. Ces décalage sont respectivement ajoutés à la position horizontal en x dans la matrice multipliée par la longueur de la bitmap et à la position verticale en y multipliée par la hauteur de la bitmap. Ensuite la bitmap « bmp » est copiée à l’écran à la position résultante (x2,y2) avec la fonction blit() d’allegro.

La forme courante à sa position dans la zone de jeu

Après la zone de jeu et le contenu de matrice PLAN, vient l’affichage de la pièce courante qui descend régulièrement. La position de la pièce courante est donnée par les variables globales XP et YP, il n’y a qu’à parcourir la matrice correspondant à la forme courante donnée par FCOUR et afficher comme précédemment une bitmap du fichier ressource dans le tableau GRPH[ ]. La fonction dessine_forme_dans_plan() prend un pointeur de matrice du type forme en argument « forme F » et ainsi :


aaaaavoid dessine_forme_dans_plan(forme F)
aaaaa{
aaaaaBITMAP *bmp;
aaaaaint x,y,k,x2,y2;

(1)aaaaaafor (y=0; y<4; y++)
aaaaaaaaaaafor (x=0; x<4; x++)
aaaaaaaaaaaaaaif ( k = F[FCOUR][y][x]){// attention c'est bien une affectation
(2)
aaaaaaaaaaaaaabmp=(BITMAP*)GRPH[k].dat;
aaaaaaaaaaaaaaaaax2 = (XP*bmp->w) + (x*bmp->w) + MARGE_HOR;
aaaaaaaaaaaaaaaaay2 = (YP*bmp->h) + (y*bmp->h) + MARGE_VER;
aaaaaaaaaaaaaaaaablit(bmp, PAGE, 0,0,x2,y2,bmp->w,bmp->h);
aaaaaaaaaaaaaa}
aaaaa}

(1) Chaque position de la matrice F[FCOUR][y][x] est évaluée avec une double boucle for. La valeur de la position est récupérée dans la variable « k ». Si sa valeur est supérieure à 0 les instructions suivantes sont exécutées :

(2) L’adresse de la bitmap correspondante, c'est-à-dire à l’indice « k » dans le tableau GRPH[ ] de ressources dat, est récupérée dans un BITMAP* local « bmp » et sa position est calculée. XP multiplié par la longueur de la bitmap plus le décalage horizontal donne la position horizontale. YP multiplié par la hauteur de la bitmap plus le décalage vertical donne la position vertical. Ensuite recopie Ensuite la bitmap « bmp » est copiée à l’écran à la position résultante (x2,y2) avec la fonction blit() d’allegro.


La prochaine forme en attente en dehors de la zone de jeu

L’affichage de la forme en attente à l’extérieur de la zone de jeu consiste à parcourir la matrice qui correspond à la forme suivante dans sa première position (indice 0) et comme précédent afficher les bitmaps qui la constituent mais cette fois à une position fixe en dehors de la zone de jeu :

(1)aaavoid dessine_forme_hors_plan(forme F, int xp, int yp)
aaaaa{
aaaaaBITMAP *bmp;
aaaaaint x,y,k;

(2)aaaaaafor (y=0; y<4; y++)
aaaaaaaaaaafor (x=0; x<4; x++)
aaaaaaaaaaaaaaif ( k = F[0][y][x]){aaaaa// attention c'est bien une affectation
(3)
aaaaaaaaaaaaaabmp=(BITMAP*)GRPH[k].dat;
aaaaaaaaaaaaaaaaablit(bmp,PAGE,0,0,xp+(x*bmp->w),yp+(y*bmp->h),bmp->w,bmp->h);
aaaaaaaaaaaaaa}
aaaaa}

(1) La fonction dessine_forme_hors_plan() prend en argument un pointeur de type forme et deux variables entières xp et yp qui vont correspondre aux coordonnées écran de l’affichage de la forme.

(2) Comme dans les fonction précédentes, double boucle for avec récupération de la valeur de la matrice à la position (x,y) et teste sur cette valeur. Positive elle désigne un indice dans le tableau GRPH des ressources.

(3) à l’indice k l’adresse de la bitmap correspondante est récupérée dans le pointeur bmp afin de simplifier l’écriture de la recopie écran avec la fonction blit() à la position (xp, yp)

Mise en place d’un timer, gestion du temps

Nous avons déjà évoqué la variable « t_piece » qui fait l’objet d’une incrémentation automatique à intervals réguliers (voir « Contrôle des mouvements latéraux, rotation et descente » plus haut). Une seconde variable du même type est utilisée dans le jeu, c’est la variable « t_jeu » qui va servir à stabiliser la vitesse d’exécution du jeu indépendamment de la vitesse du processeur. Le timer ou plus exactement la mise en place d’un « interrupt handler » désigne le mécanisme qui va permettre l’incrémentation en quasi parallèle de ces variables. Ces variables vont servir de frein avec une construction du type :

aaaaaaaawhile (t_jeu>0){
aaaaaaaaaaat_jeu-- ;
aaaaaaaaaaa// instructions
aaaaaaaa}

Lorsque t_jeu a été incrémenté les instructions sont exécutées et si elles sont exécutées plus rapidement que l’incrémentation suivante le programme attend la prochaine incrémentation. Ce mécanisme permet de rythmer le déroulement du programme sans pour autant paralyser le processeur dans une boucle d’attente.

Des variables de type « volatile »

L'utilisation d'un interrupt handler nécessite des variables "volatile", nos deux variables sont déclarées en globale et du type volatile int :

aaaaaaaavolatile int t_piece=0 ;
aaaaaaaavolatile int t_jeu=0 ;

Une fonction appelée à intervalle régulier


Leur incrémentation à intervals réguliers dans le temps est produite à l’aide de la fonction Timer() ci-dessous. Cette fonction doit être la plus simple possible et en aucun cas faire appel à de gros calculs :

aaaaaaaavoid Timer(void)
aaaaaaaa{
aaaaaaaaaaat_jeu++;
aaaaaaaaaaat_piece++;
aaaaaaaa}
aaaaaaaaEND_OF_FONCTION(Timer);

Son écriture est close par une macro END_OF_FONCTION(Timer) (se reporter à la documentation Allegro ainsi qu’à son code source pour avoir des informations à ce sujet.)

Définition de l’intervalle des appels

Deux fonctions dans l’environnement Allegro permettent de définir un intervalle de temps :
aaaaaaaaint install_int(void (*proc)(), int speed)
aaaaaaaaint install_int_ex(void (*proc)(), int speed)

Chacune a deux arguments. Le premier est un pointeur de fonction qui va désigner la fonction à appeler régulièrement par interruption dans le temps. Le second est une valeur entière qui définit la durée de l’intervalle de temps.
Pour la première l’intervalle de temps est donné uniquement en milliseconde et pour la seconde fonction plusieurs options pour définir le temps d’attente sont possibles, définies par les macros suivantes :

aaaaaaaaSECS_TO_TIMER(secs)aaaaa - donne le nombre de secondes entre chaque appel
aaaaaaaaMSEC_TO_TIMER(msec)aaaa - donne le nombre de millisecondes entre chaque appel
aaaaaaaaBPS_TO_TIMER(bps)aaaaaaa - donne le nombre d’appels par seconde
aaaaaaaaBPM_TO_TIMER(bpm)aaaaaa - donne le nombre d’appels par minute

Dans le programme nous avons choisi un appel tous les 1/60 de seconde définit avec la fonction install_in_ex() de la façon suivante :
aaaaaaaainstall_int_ex(Timer, BPS_TO_TIMER(60));

Initialisation au début du programme

Pour mettre en œuvre le timer, nous avons ensuite les lignes suivantes au moment des initialisations du début du main() dans la fonction init_env() déjà évoquée qui regroupe toutes les initialisations nécessaires en début de progra
mme :
aaaaaaaa{
aaaaaaaaaaa(…)
(1)aaaaaaaainstall_timer();

(2)aaaaaaaaLOCK_VARIABLE(t_jeu);
aaaaaaaaaaaLOCK_VARIABLE(t_piece);
aaaaaaaaaaaLOCK_FUNCTION(Timer);

(3)aaaaaaaainstall_int_ex(Timer, BPS_TO_TIMER(60));
aaaaaaaaaaa(…)
aaaaaaaa}

(1) Appel de la fonction donnée par Allegro et qui permet d’avoir un interrupt handler

(2) Toutes les variables et le code utilisés dans les interrupt handlers doivent être "locked",
verrouillées

(3) Le nombre d’appels par seconde (Beats Per Second) de notre fonction « Timer() ». Ici
60 appels par seconde.


4. Mise en œuvre dans le main et la boucle d’événements

Initialisation de l’environnement, timer et ressources au démarrage

aaaaavoid init_env()
aaaaa{
(1)
aaaaaasrand(time(NULL));
(2)
aaaaaaallegro_init();
aaaaaaaaainstall_keyboard();
(3)
aaaaaainstall_timer();
aaaaaaaaaLOCK_VARIABLE(t_jeu);
aaaaaaaaaLOCK_VARIABLE(t_piece);
aaaaaaaaaLOCK_FUNCTION(Timer);
aaaaaaaaainstall_int_ex(Timer, BPS_TO_TIMER(60));

(4)
aaaaaaatext_mode(-1);

(5)
aaaaaaaset_color_depth(16);
aaaaaaaaaaif (set_gfx_mode( GFX_AUTODETECT_WINDOWED, ECRAN_X,ECRAN_Y,0,0) !=0 ){
aaaaaaaaaaaaaallegro_message("erreur gfx_mode");
aaaaaaaaaaaaaallegro_exit() ;
aaaaaaaaaaaaaexit(1);
aaaaaaaaaa}
(6)
aaaaaaaPAGE=create_bitmap(ECRAN_X,ECRAN_Y);

(7)
aaaaaaaGRPH=load_datafile(".//graphic.dat");
aaaaaaaaaaif(!GRPH || !PAGE){
aaaaaaaaaaaaallegro_message("erreur GRPH ou PAGE");
aaaaaaaaaaaaallegro_exit() ;
aaaaaaaaaaaaexit(1);
aaaaaaaaaa}
aaaaa}

(1) Fonction du C standard pour l’initialisation d’une liste de nombres pseudo-aléatoires

(2) Fonction d’initialisation pour l’utilisation de l’environnement allegro et fonction
d’initialisation pour l’utilisation du clavier sous Allegro.

(3) Tout ce qui concerne la mise en place d’un interrupt handler et que nous avons vu
précédemment.

(4) Choix qui concerne l’affichage du texte, ici sur fond transparent.

(5) Choix de la profondeur de couleur, ici 16 bits et Définition du mode graphique : mode
fenêtre, résolution donnée par les macros ECRAN_X et ECRAN_Y et ensuite contrôle
d’erreurs

(6) Création bitmap globale PAGE destinée à servir de double buffer.

(7) Chargement des ressources stockées dans le fichier « graphique.dat » et contrôle
d’erreurs

Le main, initialisations, boucle d’événements

aaaaa int main()
aaaaa{
(1)
aaaint i, desc, speed=0, nb_lignes=0, fin=0;

aaaaqaint lignes[TY];
aaaaaaint niveau[25]= {70, 65, 60, 55, 50, 45, 40, 35, 30, 26, 22, 19, 16, 14, 12, 10, 8, 7, 6, 5, 4, 3, 2, 1, 0};

(2)
aaaaaainit_env() ;
aaaaaaaacourante = init_piece();
aaaaaaaasuivante = init_piece();
aaaaaaaamemset(PLAN,0,TX*TY*sizeof(char));

(3)
aaaaawhile (!fin){

(4)
aaaaaaaaif (key[KEY_ESC])
aaaaaaaaaaaaaafin=1;

(5)aaaaaaaacntl_affichage();
(6)
aaaaaaaawhile (t_jeu>0){
aaaaaaaaaaaaaat_jeu--;
(7)
aaaaaaaaaaamove();

(8)
aaaaaaaaaaaif (t_piece>niveau[speed]){
aaaaaaaaaaaaaaaaat_piece=0;
(9)
aaaaaaaaaaaaaadesc=descendre(courante);

(10)
aaaaaaaaaaaaaif (desc==-1)
aaaaaaaaaaaaaaaaaaaafin=1;
aaaaaaaaaaaaaaaaaelse if (desc==0){
aaaaaaaaaaaaaaaaaaaafixer_piece(courante);
aaaaaaaaaaaaaaaaaaaainit_piece_suivante();
aaaaaaaaaaaaaaaaaaaaif (i=compter_lignes_finies(lignes)){
aaaaaaaaaaaaaaaaaaaaaaaeffacer_lignes_finies(lignes);
aaaaaaaaaaaaaaaaaaaaaaanb_lignes+=i;
aaaaaaaaaaaaaaaaaaaaaaaspeed=nb_lignes/2;
aaaaaaaaaaaaaaaaaaaaaaaif (speed>=25){
aaaaaaaaaaaaaaaaaaaaaaaaaa//prévoir qqchose : jeu gagné
aaaaaaaaaaaaaaaaaaaaaaa}
aaaaaaaaaaaaaaaaaaaa}
aaaaaaaaaaaaaaaaa}
aaaaaaaaaaaaaa}
aaaaaaaaaaa}
aaaaaaaa}
(11)
aaaaanettoyage_sortie();
aaaaaaaaareturn 0;
aaaaa}
aaaaaEND_OF_MAIN();

(1) Déclarations, initialisation des variables nécessaires à partir du main. « desc » sert à récupérer la valeur de retour de la fonction de descente de la piece courante. « speed » donne le niveau actuel de la partie c'est-à-dire un indice dans le tableau « niveau » des temps d’attente de plus en plus court pour la descente de la pièce. « nb_lignes » va compter tout au long de la partie le nombre de lignes finies et plus il sera grand plus le niveau va augmenter, en fait la valeur de la variable speed est calculée à partir du nombre des lignes finies. « fin » permet de contrôler la boucle principale et d’aasurer la fin de la partie. Le tableau « lignes » sert à compter les lignes finies.

(2) Toutes les initialisations, appel de la fonction init_env() vue précédement mais également, choix d’une pièce courante et d’une pièce suivante, mise à 0 de toutes les possitions de la matrice PLAN, la zone de jeu.

(3) Boucle d’événements, prend fin si la variable fin devient différente de 0

(4) Possibilité de mettre fin à la boucle principale en appuyant sur la touche echap.

(5) Appel de la fonction de contrôle de l’affichage.

(6) Boucle pour la régulation dans le temps du jeu, les instructions ne sont accessibles que si la variable t_jeu a été incrémentée via la fonction Timer() et l’interrupt handler. Si tel est le cas une fois dans la boucle la première instruction consiste à décrémenter t_jeu.

(7) Contrôle du mouvement des pièces.

(8) La rapidité de la descente automatique de la pièce est en fonction du niveau contrôlée par l’incrémentation régulière en parallèle de la variable t_pièce.

(9) Le retour de la descente automatique est récupéré dans la variable desc.

(10) S’il est égal à -1 (impossibilité de faire descendre une nouvelle pièce) la partie est finie.
S’il est égal à 0 la pièce est bloquée et il faut passer à la pièce suivante. Dans ce cas il faut également compter les lignes finies et les enlever. La variable « i » stocke le nombre de lignes finies. Le nombre total des lignes finies pendant la partie est incrémenté ensuite de « i ». Le niveau a peut-être augmenté et l’indice « speed » pour le tableau « niveau » est mis à jour (deux lignes finies font progresser de un). Dans le cas peu probable ou quelqu’un attendrait le niveau 25, il serait amusant de créer une petite anime de félicitation.

(11) Juste la déallocation des ressources, PAGE et GRPH. Détail de la fonction nettoyage_sortie() :

aaaaavoid nettoyage_sortie()
aaaaa{
aaaaaaaaaif (GRPH)
aaaaaaaaaaaaunload_datafile(GRPH);
aaaaaaaaaif (PAGE)
aaaaaaaaaaaadestroy_bitmap(PAGE);
aaaaa}