INFO505 Programmation C
|
L'objectif de ce TP, ainsi que du prochain, est de réaliser une version simplifiée du jeu Tetris. Si vous ne connaissez pas ce jeu, vous pouvez en apprendre les principes à l'adresse :
http://fr.wikipedia.org/wiki/Tetris
Il est important de LIRE ATTENTIVEMENT ET EN ENTIER CE DOCUMENT AVANT DE COMMENCER A CODER!!
L'idée ici est de faire un jeu Tetris console, qui codera toute la logique du jeu, sans les aspects graphiques ni temps réel. Ces deux derniers aspects seront traités dans le TP suivant (TP3 - Mini-tetris graphique avec GTK). Ici, l'objectif est de vous faire travailler sur les structures de données, avec d'abord une représentation tableau à 2 dimensions, puis ensuite une représentation liste, pour stocker la grille du tetris.
Plusieurs contraintes sur la structure de votre programme vous sont imposées.
Pour ce TP, vous devrez remettre au moins trois fichiers sous forme d'archive: tp2-tableau.c
, tp2-liste.c
, et un README.txt
. Les points suivants seront évalués :
gcc
sans erreurs ni warnings, et fonctionner sous Linux.Dans le cadre de ce TP, le jeu sera implémenté uniquement en texte et son déroulement se fera entièrement dans le terminal. Vous aurez ainsi toute la partie logique du jeu qui fonctionne à la fin du TP. Pour cette partie, vous écrirez tout dans un fichier tp2-[votre nom]-tableau.c
Tout d'abord nous allons simplifier grandement le déroulement du jeu.
Lorsqu'un joueur a choisi la colonne dans laquelle faire tomber la pièce, celle-ci est directement ajouté à la position la plus basse qu'elle peut atteindre.
Voici un exemple :
Si le joueur choisit, par exemple, la colonne 8 alors la pièce est insérée de manière à ce que sa colonne la plus à gauche (celle indiquée par une flèche dans l'exemple) soit dans la colonne #8.
Par convention, la grille de jeu est modélisée de bas en haut, c'est-à-dire que la ligne 0 est en bas, la ligne 1 est juste au-dessus, etc. Attention, pour l'affichage sur la console, il faudra donc partir de la dernière ligne et afficher jusqu'à la première ligne. Ci-dessous, vous avez à gauche le numéro de ligne et en bas le numéro de colonne de chaque case. Les barres |
ne font pas partie de la grille et sont juste affichés.
Comme nous le verrons plus tard, cette contrainte simplifie grandement le positionnement des pièces.
Remarques :
'|'
(pipe)' '
) ou un pipe ('|'
).Déclarez 4 constantes avec la directive #define
:
Piece
: une struct contenant les champs suivantshauteur
: le nombre de lignes qu'occupe la pièce.largeur
: le nombre de colonnes qu'occupe la pièce.forme
: un tableau de chaînes de caractères, permettant de dessiner la pièce. Ce sera un tableau de HAUTEUR_MAX_DES_PIECES cases, chaque case étant de type char*
, qui pointe donc vers une chaîne de caractères.Grille
: Un tableau bidimensionnel de caractères qui représente l'espace de jeu.void initialiseGrille( Grille G )
qui remplit une grille de jeu G
passé en paramètre avec le caractère ' '
(espace).char lireCase( Grille G, int i, int j )
qui, pour une grille, un numéro de ligne et un numéro de colonne passés en paramètre, retourne le contenu de la case correspondante de cette grille. Vous devez toujours utiliser cette procédure lorsque vous voulez lire le contenu d'une case la grille de jeu. Ajoutez dans cette procédure un test vérifiant que la ligne et la colonne sont valides. Si tel n'est pas le cas, une erreur doit être affichée.void afficheGrille( Grille G )
afin d'afficher le contenu d'une grille donnée en entrée en l'encadrant à gauche, à droite et en dessous avec le caractère '|'
(pipe). Ajoutez également des nombres qui indiquent le numéro de chacune des colonnes (voir l'exemple plus haut).Écrivez une procédure main
qui vous permet de tester le fonctionnement des deux procédures précédentes. C'est au début du main
qu'il faudra instancier une grille.
TESTEZ VOTRE PROGRAMME À CHAQUE ÉTAPE DE SON DÉVELOPPEMENT!!
générerPieces
qui initialise un tableau de pièce avec chacune des pièces qui apparaîtront dans le jeu. Les pièces sont écrites une par une "à la main" dans cette fonction. Voici un exemple d'initilisation de pièces : Il est important que chacune des chaînes de caractères du tableau forme
aient exactement la même taille, cette taille étant la valeur donnée au champs largeur
(1 ou 3 dans cet exemple). Votre jeu doit fournir au moins cinq pièces différentes, à vous de décider lesquelles. Notez que comme le joueur ne peut pas encore tourner les pièces, les quatre pièces suivantes sont considérées comme étant différentes :
l l l %% l %%
affichePiece
qui affiche une pièce passée en paramètre et ajoute une flèche (ou tout autre symbole) sous la colonne la plus à gauche de manière à indiquer au joueur où sera inséré la pièce. void ecrireCase( Grille G, int i, int j,
char c )
qui, pour une grille, un numéro de ligne et un numéro de colonne passés en paramètre, inscrit dans la case correspondante de la grille de jeu un caractère également spécifié en paramètre. Vous devez toujours utiliser cette procédure pour écrire dans la grille de jeu. Il est là aussi fortement conseillé de tester la validité des numéros de ligne et colonne et d'afficher une erreur dans le cas contraire.Lorsqu'un joueur décide de placer une pièce dans une colonne donnée, il faut être en mesure de déterminer à quelle hauteur la pièce sera déposée. On a imposé une forme particulière aux pièces de manière à simplifier cette étape. Comme la ligne la plus basse d'une pièce est forcément la plus large, il suffit de déterminer, pour chacune des colonnes que va occuper cette pièce, quelle est hauteur de la plus haute case occupée.
int hauteurPlat( Grille g, int c1, int c2
)
qui, étant donné une grille et un intervalle de colonnes, retourne la ligne la plus basse où on peut placer une pièce sans écraser une pièce déjà placée. C'est équivalent à retourner un plus la hauteur maximale où se trouve une case occupée entre les colonnes spécifiées. Cette procédure sera mise à jour dans un deuxième temps pour prendre en compte des pièces plus complexes (voir hauteurExacte
plus loin).void ecrirePiece( Grille G, Piece P, int
c, int h )
qui reçoit en paramètre une grille, une pièce, un numéro de colonne ainsi qu'une hauteur et ajoute cette pièce à la grille de manière à ce que la colonne la plus à gauche de la pièce corresponde au numéro de colonne spécifiée. pieceAleatoire
qui choisit une pièce au hasard parmi celles que vous avez définies. Pour choisir un nombre aléatoirement dans l'ensemble {0,1,2,...,n-1} on peut utiliser la commande suivante : main
en y ajoutant une boucle principale de manière à :main
: ecrirePiece
votre programme détecte si la pièce va dépasser de la grille de jeu. Si tel est le cas, on affiche un message informant le joueur qu'il a perdu la partie ainsi que le nombre de pièces qu'il a réussi à placer. La grille est alors réinitialisée et une nouvelle partie démarre.void supprimerLigne( Grille G, int i )
qui efface le contenu d'une ligne dont le numéro est passé en paramètre et fait descendre toutes celles au-dessus. Plus précisément, lorsqu'une ligne est supprimée
, le contenu de chacune des lignes au dessus de celle-ci doit être recopié dans la ligne d'en dessous et la ligne la plus haute de la grille de jeu est remplacée par une ligne vide.int nettoyer( Grille G )
qui supprime toutes les lignes ne contenant aucune case vide et qui retourne le nombre de lignes supprimées (utile si on veut gérer le score).nettoyer
soit effectué.Vous devriez avoir maintenant une version jouable. Malheureusement, les pièces étant toutes plus larges à la base, vous allez voir qu'il est difficile de gagner à ce Tetris.
On autorise maintenant le type de pièces ci-dessous:
Il n'est plus suffisant d'utiliser hauteurPlat
pour détecter la position où va tomber la pièce. On peut juste dire que hauteurPlat
donne la "pire" hauteur possible, mais potentiellement la pièce peut aller plus bas. Ecrivez donc la fonction
qui vous retourne cette hauteur exacte. Substituez ensuite hauteurExacte
à hauteurMax
dans le code. Si la procédure ecrirePiece
est correctement écrite, alors cela devrait fonctionner du premier coup.
H 01 colonne piece ## 0 : hauteur_grille(2+0) = 3, hauteur_piece(0) = 2 # => hauteur_exacte(0) = 3 - 2 = 1 # 1 : hauteur_grille(2+1) = 2, hauteur_piece(1) = 0 => hauteur_exacte(1) = 2 - 0 = 2 3 L => hauteur_exacte = max(1,2) = 2 2 AA AAL 1 LLLLAAL 0 BBAABBAA 01234567 colonne grille
On peut aussi implémenter les rotations. Cela se fait simplement en créant les rotations d'une pièce comme autant de pièces différentes. On mémorise ensuite dans un tableau rotD
que la rotation d'une pièce à droite donne le numéro d'une autre pièce et similairement avec un tableau rotG
. Enfin, l'utilisateur tape 'g' ou 'd' pour tourner la pièce avant de choisir la colonne. La lecture au clavier se fera plutôt en lisant une chaîne de caractères et en l'analysant. En pseudo-code, ça peut ressembler à ça:
Cette partie est à faire dans un deuxième temps, une fois que le tetris "tableau" fonctionne (en effet, il servira pour le tetris graphique).
On propose une autre structure de données pour représenter la grille de jeu, où chaque ligne est indépendante des autres et où elles sont placées dans une liste chaînée. Ainsi, lorsqu'une ou plusieurs lignes seront supprimées, il suffira de supprimer un ou des éléments de la liste, les autres lignes seront donc automatiquement déplacées. Pour cela, nous allons remplacer le tableau servant à représenter la grille de jeu par une liste doublement chaînée.
Avant de modifier votre code, récupérez les codes suivants (Liste.h, Liste.c, test-Liste.c), compilez-les avec
et exécutez le programme.
On constate que, contrairement à ce que l'on espérerait, la commande affiche(L)
n'affiche rien du tout.
valgrind
de la manière suivante :Lorsque vos listes fonctionnent correctement, vous pouvez les adapter afin de les utiliser pour représenter efficacement la grille de jeu de votre Tetris. Chaque noeud de votre liste représentera une ligne du jeu tetris, la première ligne (celle du bas) étant la première cellule utile de votre liste chaînée. Supprimer une ligne se fera donc en supprimant la cellule correspondante et vous devrez rajouter une ligne composée d'espaces en fin de liste. Voilà à quoi ressemble votre liste pour représenter le tetris ci-contre.
|| || ... || l || || @l$$ || || @@@$$ ||
À ce stade, la compilation de votre programme échouera. C'est normal. Il faut maintenant modifier les procédures qui manipulent la grille de jeu.
ecrireCase
et lireCase
. Il peut s'avérer pratique d'ajouter un return bidon (ex : return
' ';) à la procédure lireCase.Votre programme devrait maintenant compiler à nouveau tout en restant inutilisable.
construireGrille
qui construit la grille de jeu.détruireGrille
qui libère la mémoire occupée par la grille.lireCase
et ecrireCase
afin de les adapter à la nouvelle structure de données employée pour la grille de jeu. Ces dexu procédures nécessite de parcourir la liste pour arriver à la bonne ligne.Votre programme devrait maintenant être redevenu fonctionnel.
supprimerLigne
de manière à ne plus recopier la grille case par case mais plutôt en supprimant l'élément approprié de la liste chaînée et en ajoutant une nouvelle ligne vide en bout de liste.Renommez votre fichier source
afficherGrille
, initialiser
, ecrirePiece
et hauteurMax
afin de remplacer les appels à ecrireCase
, lireCase
par une utilisation séquentielle de la liste chaînée.vrai
Tetris ;-): toutes les pièces, score (en fonction du nombre de lignes supprimé en même temps). L'aspect temps réél sera abordé dans le TP suivant."____________"
(où les '_' sont des espaces). Le problème est que c'est une chaîne constante, donc non modifiable. Il faut donc bien allouer dynamiquement une chaîne de la bonne longueur et la remplir d'espaces.