Projet TETRIS

 

1. Introduction : 3

2. Cas d'utilisations : 3

3. Modélisation des données: 4

1. Tableau Tétris : 4

2. Modélisation des pièces : 4

3. Variables relatives à la pièce en mouvement : 6

4. Autres variables 6

4. Organisation du projet : 7

5. Réalisation : 8

1. Gestion du clavier et du délai d'affichage : 8

TetrisEvent AttendEvenement( int Delai ) 8

2. Gestion du programme principal : 8

void main() 8

3. Gestion de l'affichage : 9

void AfficheInitialise() 9

void AfficheRestaure() 9

void AfficheInfos( int Score, int Niveau, TetrisValeur ProchainePiece ) 9

void AfficheMorceauPiece( int Ligne, int Colonne ) 10

void AfficheTetris() 10

void AfficheTout() 10

void AfficheMeilleursScores() : 10

4. Gestion du jeu 10

int Score() 10

int Niveau() 10

int Delai() 10

TetrisValeur Couleur( int ligne, int colonne ) 11

TetrisValeur ProchainePiece() 11

void Apercu() 11

void NouvellePartie() 11

static int PieceAuHasard() 11

static void ChoisirPiece() 11

void NouvellePiece() 12

static int LigneComplete( int iLigne ) 12

static void DecaleTableau(int iLigne) 12

static void PieceEnBas() 12

static int PiecePeutBouger( int ligIncrement, int colIncrement, int rotIncrement ) 12

static void PieceBouge( int ligIncrement, int colIncrement, int rotIncrement ) 14

static void AffichePiece( TetrisValeur couleur ) 14

static int GereMouvementPiece( int ligIncrement, int colIncrement, int rotIncrement ) 14

int Descend() 14

int Tombe() 15

void Gauche() 15

void Droite() 15

void Tourne() 15

5. Gestion des scores 15

static int EcritFichierDeScore() 16

static int LitFichierDeScore() 16

int BonScore( int score ) 16

void MeilleursScores( SCORE aScores[10] ) 16

void AjouteScore( SCORE score ) 17

Quelques copies d'écrans : 17

 


1. Introduction :

Le but est de programmer le jeu Tétris. Dans un premier temps, on se limite au fonctionnement à un joueur. Par la suite, en fonction de l'avancement, on étudiera la possibilité multi-joueurs sur la même machine puis en réseau.

La plate-forme de développement est Turbo C++ 3.0. Cependant, la logique du jeu ainsi que l'affichage seront découplées pour permettre un portage vers les environnements suivants :

Le document qui suit définit les spécifications de l'implémentation du jeu.

 

2. Cas d'utilisations :

 

 


3. Modélisation des données:

1. Tableau Tétris :

Tétris est modélisé en mémoire par un tableau statique global à 2 dimensions de 20 lignes par 10 colonnes. Chaque case de ce tableau contient un code particulier qui correspond à la couleur de la pièce. S'il n'y a pas de couleur de définie, cela signifie que la case est vide (Valeur TETRIS_RIEN).

Le haut du tableau commence à la ligne 0, le bas est à la ligne 19

 

// Enumération définissant les valeurs que peuvent avoir

// les cellules du tableau s_aTetris

typedef enum

{

    CARRE_BLEU,

    LIGNE_ROUGE,

    TRIANGLE_VERT,

    ZIG1_CYAN,

    ZIG2_GRIS,

    L1_ROSE,

    L2_JAUNE,

    TETRIS_RIEN

} TetrisValeur;

 

// Dimension du tableau

static const int cs_Lignes = 20;

static const int cs_Colonnes = 10;

 

// Déclaration du tableau

static TetrisValeur s_aTetris[cs_Lignes][cs_Colonnes];

 

2. Modélisation des pièces :

Chaque pièce (carré, barre, manivelle gauche, manivelle droite etc. ) en mouvement est modélisée par un sous tableau à 2 dimensions contenant les coordonnées relatives {ligne, colonne} de chaque élément composant la pièce.

Pour chaque pièce, il y a 4 tableaux définissant les positions de la pièces en fonction de la rotation en cours. Ces sous tableaux sont regroupés dans un tableau constant global à 4 dimensions, dont la première contient le type de pièce.

Ce type de modélisation quoique compliqué au premier abord permet d'obtenir rapidement les nouvelles positions de la pièce suite à un mouvement (rotation, gauche, droite, bas) et simplifie les tests de collision à effectuer chaque fois que la pièce bouge dans la mesure où toutes les pièces sont traitées de la même manière.

Ainsi, pour effectuer un test de collision, il suffit de calculer la position suivante pour les 4 éléments composant une pièce, puis de vérifier pour chaque élément que la case du tableau est vide.

 


static const int cs_aPiece[][4][4][2] =

{

     // ...

    {    // Le triangle

     {

                  {-1,0},

         {0,-1}, {0,0},   {0,1}

     },

     {

                  {-1,0},

                   {0,0},   {0,1},

                  {1,0}

     },

     {

         {0,-1}, {0,0},   {0,1},

                  {1,0},

     },

     {

                  {-1,0},

         {0,-1}, {0,0},

                  {1,0}

     }

    },

etc. pour les autres pièces (voir tetris.cpp)

 

Par exemple, le code suivant calcule la nouvelle position pour le 4° élément de la pièce triangle (qui serait la pièce en mouvement) lorsque la pièce descend d'un cran, et qu'on a tourné 2 fois (en gras ci-dessus)

 

// On n'oublie pas qu'en C, les tableaux commencent à 0, donc

// le 3 signifie qu'il s'agit du 4° élément

// Ici, on aurait donc s_Rotation qui vaut 2

// s_Rotation va de 0 (pas de rotation) à 3

int ligneRelative =

cs_aPieces[TRIANGLE_VERT][ s_Rotation][3][0];

int colonneRelative =

cs_aPieces[TRIANGLE_VERT][ s_Rotation][3][1];

 

// On ajoute 1 à ligne puisqu'on descend d'un cran.

// s_ligTetris et s_colTetris sont les coordonnées de la

// pièce courante dans le tableau s_aTetris

int ligne = s_ligTetris + ligneRelative + 1;

int colonne = s_colTetris + colonneRelative;

 

// Il faut maintenant vérifier que cette case est inoccupée

if(ligne < cs_Lignes)

{

// On est dans le tableau

     if( s_aTetris[ligne][colonne] != TETRIS_RIEN )

     {

         return 0; // La cellule est occupée

     }

}

else

{

return 0; // On est en dehors du tableau

}

 

// Si on arrive ici, c'est que la cellule est vide

return 1;

 

On pourra s'inspirer de cette exemple pour programmer la fonction PiecePeutBouger( int ligIncrement, int colIncrement, int rotIncrement ) qui calcule si la pièce en cours peut bouger et renvoie 1 si le déplacement peut avoir lieu.

3. Variables relatives à la pièce en mouvement :

Le type de pièce en mouvement est mémorisé dans une variable statique globale. Il est utilisé comme 1° indice dans le tableau cs_aPieces :

 

// Définit la pièce qui est actuellement affichée et

// en cours de descente

static int s_PieceCourante;

 

La rotation en cours est mémorisée dans une variable statique globale et permet d'accéder aux positions relatives pour un type de pièce bien déterminé. Elle est utilisée comme 2° indice dans le tableau cs_aPieces :

 

// Définit la rotation en cours

static int s_Rotation;

 

Les coordonnées (ligne, colonne) de la pièce en mouvement sont mémorisées dans deux variables statiques globales :

 

// Définit la position de la pièce dans le tableau

static int s_ligTetris;

static int s_colTetris;

 

4. Autres variables

On mémorise la prochaine pièce en vue de l'afficher pour aider le joueur (Aperçu) :

 

// Définit la prochaine pièce qui va s'afficher. Celle

// ci est utilisée afin de la montrer à l'utilisateur

// dans un coin de l'écran

static int s_ProchainePiece;

 

// A 1 si l'aperçu est actif, à 0 sinon

static int s_fAfficheProchainePiece;

 

On mémorise dans une variable s_Score le nombre de lignes réalisées, et dans une variable s_Niveau le niveau en cours :

 

// Score et niveau

static int s_Score;

static int s_Niveau;

 

Le délai en millisecondes entre 2 mouvements est mémorisé dans une variable statique globale. Ce délai est fonction du niveau, lui-même fonction du score :

 

// Délai entre 2 mouvements

static int s_Delai;

 

A noter que ces déclarations sont contenues dans le fichier tetris.cpp. Il est donc inutile de tout retaper !

 

4. Organisation du projet :

Ce projet est découpé en plusieurs fichiers :

 

Dans turbo C++, il faut créer un projet regroupant tous ces fichiers.

Par ailleurs, les prototypes des fonctions publiques sont regroupés dans les fichiers affiche.h, clavier.h, score.h et tetris.h.

 


5. Réalisation :

1. Gestion du clavier et du délai d'affichage :

Compléter le fichier clavier.cpp, en codant la fonction

 

TetrisEvent AttendEvenement( int Delai )

Cette fonction renvoie un événement TetrisEvent. Elle prend en argument un délai en millisecondes. Concrètement, on ne sort pas de cette fonction tant que le délai passé en argument n'est pas écoulé ou que l'une des touches du jeu n'a pas été pressée. Cette fonction est le point central du programme dans la mesure où c'est en fonction de la valeur de retour qu'on va déplacer la pièce d'une manière ou d'une autre.

 

Un évènement TetrisEvent est défini comme suit dans clavier.h :

 

typedef enum

{

    EVT_TIMEOUT,

    EVT_TOUCHE_JEU,

    EVT_TOUCHE_GAUCHE,

    EVT_TOUCHE_DROITE,

    EVT_TOUCHE_TOURNE,

    EVT_TOUCHE_DESCEND,

    EVT_TOUCHE_PAUSE,

    EVT_TOUCHE_APERCU,

    EVT_TOUCHE_SCORE,

    EVT_TOUCHE_QUITTE

} TetrisEvent;

 

Afin de réaliser cette partie, on utilise les fonctions kbhit()pour détecter qu'une touche est pressée et getch() pour extraire le code de la touche.

Pour la gestion du délai, on utilise la fonction clock().

2. Gestion du programme principal :

Compléter le fichier main.cpp en finissant d'implémenter la fonction main().

 

void main()

Cette fonction est une boucle autour de la fonction AttendEvenement() de clavier.cpp :  Dessous cette fonction, on utilise switch() – case afin de gérer les déplacements de la pièce en fonction du type d'événement.

Par exemple, si l'utilisateur appuie sur la flèche droite, AttendEvenement() renvoie la valeur EVT_TOUCHE_DROITE, et le case EVT_TOUCHE_DROITE doit appeler la fonction Droite() de tetris.cpp qui fait bouger la pièce à droite.

 


3. Gestion de l'affichage :

Compléter le fichier affiche.cpp en réalisant les fonctions définies dans affiche.h.

 

L'affichage est complètement indépendant de la modélisation du jeu. Pour obtenir les informations de couleur, de score et de niveau, on n'accède pas directement au contenu des variables de tetris.cpp, mais on utilise les fonctions suivantes du module tetris.cpp. Les prototypes de ces fonctions sont définis dans tetris.h :

 

 

Les dimensions de l'écran sont de 25 lignes par 80 colonnes. Le tableau (l'aire de jeu) est centré sur l'écran sachant qu'une cellule du tableau fait 2 colonnes de largeur et une ligne de hauteur.

 

Si on décide de porter le jeu sous un autre environnement (Windows), c'est principalement ce module qu'il faudra réécrire.

 

On implémente les fonctions suivantes. On se concentre en particulier sur les fonctions AfficheMorceauPiece(), AfficheTetris() et AfficheTout() qui sont indispensables. Les autres peuvent être laissées vides dans un premier temps.

 

void AfficheInitialise()

Cette fonction est appelée en début de programme. Elle doit utiliser les fonctions textmode(), _setcursortype() et clrscr() afin de faire passer l'écran en mode 25 lignes par 80 colonnes, supprimer le curseur et effacer l'écran.

 

void AfficheRestaure()

Cette fonction est appelée en fin de programme. Elle restaure le mode vidéo précédent et le curseur, puis efface l'écran.

 

void AfficheInfos( int Score, int Niveau, TetrisValeur ProchainePiece )

Cette fonction est appelée chaque fois que le score, le niveau ou l'aperçu change. Elle doit afficher ces informations dans une zone inoccupée de l'écran.

 

void AfficheMorceauPiece( int Ligne, int Colonne )

Cette fonction est appelée chaque fois que l'on doit redessiner une case du tableau tétris. Elle utilise la fonction Couleur(int Ligne, int Colonne) afin de connaître la couleur de la case puis dessine un petit carré de 2 colonnes de large sur 1 ligne de haut avec la bonne couleur et au bon endroit.

 

void AfficheTetris()

Cette fonction utilise la fonction AfficheMorceauPiece() afin de redessiner complètement l'aire de jeu.

Cette fonction est notamment appelée chaque fois que des lignes complètes viennent d'être réalisées et qu'il faut redessiner l'écran.

 

void AfficheTout()

Cette fonction est appelée chaque fois que tout l'écran doit être redessiné. Cela se produit au lancement du jeu, ainsi qu'au changement de tableau.

Cette fonction appelle au minimum AfficheTetris() et AfficheInfos(), cependant, on peut aussi afficher un fond d'écran différent en fonction du niveau et aussi une aide sur les touches à utiliser.

 

void AfficheMeilleursScores() :

Cette fonction est appelée lorsque le joueur désire visualiser les 10 meilleurs scores. Elle fait appel au module de gestion des scores pour obtenir ces informations.

 

4. Gestion du jeu

C'est le module central du projet tétris qui contient toute la logique de déplacement des pièces et regroupe toutes les données utilisées pour représenter le jeu.

Afin de garantir que les variables ne sont modifiées que par ce module, toutes les variables globales sont statiques. Une série de fonction permet d'accéder en lecture à ces variables depuis les autres fichiers.

 

Compléter le fichier tetris.cpp en codant les fonctions suivantes :

 

int Score()

Cette fonction renvoie le score actuel (variable s_Score).

 

int Niveau()

Cette fonction renvoie le niveau actuel (variable s_Niveau).

 

int Delai()

Cette fonction renvoie le temps qui s'écoule entre 2 mouvements (variable s_Delai).

 

TetrisValeur Couleur( int ligne, int colonne )

Cette fonction renvoie le code de couleur pour la position ligne, colonne (variable  s_aTetris[ligne][colonne]).

 

TetrisValeur ProchainePiece()

Cette fonction renvoie la prochaine pièce qui descendra une fois la pièce en cours arrivée en bas (variable s_ProchainePiece et s_fAfficheProchainePiece).

Si s_fAfficheProchainePiece  vaut 0, la fonction doit renvoyer TETRIS_RIEN, ce qui signifie que l'aperçu n'est pas disponible.

 

void Apercu()

Cette fonction modifie le statut actuel de l'aperçu (variable s_fAfficheProchainePiece). Si s_fAfficheProchainePiece vaut 1 (aperçu visible), on change sa valeur en 0 et vice-versa.

 

void NouvellePartie()

Cette fonction doit correctement initialiser le tableau ainsi que le score et le niveau de manière qu'une nouvelle partie puisse être commencée :

 

static int PieceAuHasard()

Cette fonction tire au hasard un nombre entre 0 et 6 y-compris (L2_JAUNE)

On utilise les fonctions rand() et srand() pour la gestion du hasard.

Cette fonction est static car elle n'est pas visible à l'extérieur de tetris.cpp.

Elle est utilisée par la fonction ChoisirPiece().

 

static void ChoisirPiece()

Cette fonction est appelée au début du jeu puis chaque fois qu'une pièce arrive en bas. Elle sélectionne au hasard la pièce en cours qui va apparaître en haut de l'écran ainsi que la prochaine pièce en appelant la fonction PieceAuHasard().

 

void NouvellePiece()

Cette fonction est appelée au début du jeu, puis chaque fois qu'une pièce est arrivée en bas.

 

 

static int LigneComplete( int iLigne )

Cette fonction vérifie si la ligne iLigne du tableau est complète. Elle renvoie 1 si la ligne est complète, 0 sinon.

Pour faire, elle inspecte la valeur des cellules du tableau s_aTetris en bouclant sur l'ensemble des colonnes du tableau en fixant la ligne à iLigne.

 

Cette fonction est appelée par la fonction PieceEnBas() décrite plus bas.

 

 

static void DecaleTableau(int iLigne)

Cette fonction décale les lignes du tableau s_aTetris à partir de la ligne iLigne.

Cette fonction est appelée par la fonction PieceEnBas() décrite plus bas, lorsqu'une ligne du tableau est complète.

Par exemple, si la ligne 10 du tableau est complète, on recopie le contenu de la ligne 9 sur la ligne 10, celui de la ligne 8 sur la ligne 7 etc.

 

static void PieceEnBas()

Cette fonction est appelée par Descend() et Tombe(), lorsque la pièce en mouvement ne peut pas descendre plus bas. Son rôle est :

 

static int PiecePeutBouger( int ligIncrement, int colIncrement, int rotIncrement )

Cette fonction est appelée avant qu'un déplacement se réalise. Elle renvoie 1 si le déplacement peut être réalisé, 0 sinon.

Pour la coder, il faut bien comprendre le fonctionnement du tableau cs_aPieces.

De plus, il faut avoir préalablement effacé la pièce en cours afin de ne pas tenir compte de celle-ci lors des tests de collision. (voir GereMouvementPiece() )

Les paramètres de la fonction prennent les valeurs suivantes :

Pour les 4 morceaux composant une pièce, on va calculer la prochaine position dans le tableau : Pour faire, on s'inspire du code donné dans le paragraphe 2. Modélisation des pièces :

 

En particulier, on utilise les variables suivantes :

 

Cela donne le pseudo code (incomplet) suivant :

 

Début fonction

Pour morceau allant de 0 à 3

Début Boucle

  rotationVoulue = (s_Rotation+rotIncrement)%4;

  ligneRelative =

     cs_aPieces[s_PieceCourante][rotationVoulue][morceau][0];

  colonneRelative = … A compléter

    

  ligne = s_ligTetris + ligneRelative + ligIncrement

  colonne = … A compléter

  Si (ligne, colonne) est dans le tableau s_aTetris

     Si s_aTetris[ligne][colonne] est différent de TETRIS_RIEN

       Je renvoie 0 car la cellule du tableau n'est pas vide

     Fin Si

  Sinon

     Je renvoie 0 car je suis en dehors du tableau

  Fin Si

Fin Boucle

 

Je renvoie 1 car les 4 positions sont libres, donc la pièce peut s'y mettre

Fin Fonction

 

static void PieceBouge( int ligIncrement, int colIncrement, int rotIncrement )

Cette fonction ajuste les variables s_Rotation, s_ligTetris et s_colTetris avec ligIncrement, colIncrement et rotIncrement. Elle est appelée si le déplacement peut avoir lieu (PiecePeutBouger() renvoie 1).

 

static void AffichePiece( TetrisValeur couleur )

Cette fonction inscrit dans le tableau s_aTetris les 4 morceaux de la pièce en cours, puis appelle la fonction AfficheMorceauPiece() afin que l'écran soit rafraîchi.

La position de la pièce en cours est indiquée par les variables s_Rotation, s_ligTetris et s_colTetris.

Comme pour la fonction PiecePeutBouger(), on va utiliser le tableau cs_aPieces pour calculer les coordonnées des 4 morceaux.

 

 

static int GereMouvementPiece( int ligIncrement, int colIncrement, int rotIncrement )

 

Cette fonction utilise les fonctions AffichePiece(), PiecePeutBouger() et PieceBouge() afin de déplacer la pièce en mouvement.

 

Cette fonction effectue les opérations suivantes :

 

Cette fonction renvoie 1 si le mouvement demandé a pu être réalisé, 0 sinon.

 

int Descend()

Cette fonction est appelée chaque fois que le délai entre 2 déplacements est écoulé, afin de déplacer et d'afficher la pièce en mouvement.

Elle utilise GereMouvementPiece() pour tenter ce déplacement.

 

Si GereMouvementPiece() renvoie 0, cela signifie que la pièce est arrivée en bas. Il faut alors :

 

La fonction Descend() renvoie 1 si le déplacement a été effectué, 0, si le déplacement n'a pas pu être effectué. Dans ce cas, cela signifie que la partie est perdue.

 

int Tombe()

Cette fonction est appelée quand le joueur désire faire tomber la pièce tout en bas. Elle est implémentée de manière identique à Descend(). La différence tient au fait que l'on appelle GereMouvementPiece() jusqu'à ce que la pièce soit en bas. Ensuite, on procède comme pour Descend().

 

void Gauche()

void Droite()

void Tourne()

Ces fonctions appellent simplement GereMouvementPiece() et ne renvoient rien.

 

5. Gestion des scores

Ce module permet l'enregistrement des 10 meilleurs scores dans un fichier scores.txt. Afin de gérer les scores, on utilise la structure suivante définie dans score.h :

 

typedef struct

{

    int score;          // Nombre de lignes réalisées

    char nom[20];  // Nom du joueur

} SCORE;

 

Dans score.cpp, on déclare la variable statique globale suivante :

 

// Les 10 scores, du plus grand au plus petit

static SCORE s_aScores[10];

 

 

Le format du fichier texte est le suivant :

 

Il est donc possible de visualiser le fichier de scores avec n'importe quel éditeur de texte, par exemple notepad.

 

 

On implémente les fonctions suivantes :

 

static int EcritFichierDeScore()

Cette fonction utilise les API suivantes :

On doit boucler sur les 10 éléments du tableau et créer une ligne de score tant que ce dernier n'est pas égal à 0 (Ce cas se produit quand il y a moins de 10 scores dans le tableau).

 

static int LitFichierDeScore()

Cette fonction utilise les API suivantes :

 

On doit boucler tant que fgets() renvoie une ligne de texte. Par la suite, il faut triturer chaque ligne de texte afin d'en extraire le score (octet 0 à 4)  puis le nom (à partir de l'octet 6), en prenant garde de ne mémoriser au plus 20 caractères.

 

int BonScore( int score )

Cette fonction renvoie 1 si le score fait parti des 10 meilleurs.

Elle appelle LitFichierDeScore() puis vérifie en bouclant sur le tableau s_aScores si le score passé en argument fait partie des 10 meilleurs scores.

Elle renvoie 1 si c'est le cas, 0 sinon

 

void MeilleursScores( SCORE aScores[10] )

Cette fonction appelle LitFichierDeScore()puis remplit le tableau aScores passé en argument à partie du tableau s_aScores rempli par LitFichierDeScore().

Cette fonction ne renvoie aucune valeur. Si le fichier ne peut être lu, elle doit initialiser toutes les valeurs du tableau passé en argument à 0.

 

void AjouteScore( SCORE score )

Cette fonction ajoute un score dans le fichier.

 

Cette fonction ne renvoie rien.

 

Quelques copies d'écrans :

Une partie commencée au niveau 0 :

 

 


Partie perdue au niveau 3 : On demande la saisie du nom car on fait partie des 10 meilleurs scores :

 

 

 

Affichage des 10 meilleurs scores :

 

 


Version Windows :