Projet TETRIS
3. Variables
relatives à la pièce en mouvement :
1.
Gestion du clavier et du délai d'affichage :
TetrisEvent
AttendEvenement( int Delai )
2.
Gestion du programme principal :
void
AfficheInfos( int Score, int Niveau, TetrisValeur ProchainePiece )
void
AfficheMorceauPiece( int Ligne, int Colonne )
void
AfficheMeilleursScores() :
TetrisValeur
Couleur( int ligne, int colonne )
static
int LigneComplete( int iLigne )
static
void DecaleTableau(int iLigne)
static
int PiecePeutBouger( int ligIncrement, int colIncrement, int rotIncrement )
static
void PieceBouge( int ligIncrement, int colIncrement, int rotIncrement )
static
void AffichePiece( TetrisValeur couleur )
static
int GereMouvementPiece( int ligIncrement, int colIncrement, int rotIncrement )
static
int EcritFichierDeScore()
static
int LitFichierDeScore()
void
MeilleursScores( SCORE aScores[10] )
void
AjouteScore( SCORE score )
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.
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];
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.
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;
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 !
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.
Compléter
le fichier clavier.cpp, en codant la fonction
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().
Compléter
le fichier main.cpp en finissant d'implémenter la fonction 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.
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.
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.
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.
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.
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.
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.
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.
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.
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 :
Cette
fonction renvoie le score actuel (variable s_Score).
Cette
fonction renvoie le niveau actuel (variable s_Niveau).
Cette
fonction renvoie le temps qui s'écoule entre 2 mouvements (variable s_Delai).
Cette
fonction renvoie le code de couleur pour la position ligne, colonne (variable s_aTetris[ligne][colonne]).
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.
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.
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 :
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().
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().
Cette
fonction est appelée au début du jeu, puis chaque fois qu'une pièce est arrivée
en bas.
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.
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.
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 :
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
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).
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.
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.
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.
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().
Ces
fonctions appellent simplement GereMouvementPiece() et ne renvoient rien.
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 :
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).
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.
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
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.
Cette
fonction ajoute un score dans le fichier.
Cette
fonction ne renvoie rien.
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 :