Programmation C++ : TP séance 05
- Rendu du TP sur Ametice :
- Suivez les instructions pour le rendu du TP en mettant le bon numéro de séance dans le nom du répertoire.
- N'oubliez pas de rendre votre travail à la fin de la séance comme indiqué, même si vous n'avez pas fini la planche de TP, puis de rendre la version finale avant le début de la séance suivante.
Exercice 1. Sabot de Dominos, template, injection de dépendance
On se propose d'utiliser la classe polymorphe Piece
du TP précédent pour
implémenter un sabot de dominos (en anglais deck) , c'est-à-dire une petite
boîte de laquelle seront extraits visuellement les dominos un à un durant le jeu.
Il est par conséquent indispensable d'avoir achevé le TP précédent pour aborder cette planche.
1.a. Création du sabot
Créer dans des fichiers deck.hpp
et deck.cpp
une classe Deck
dérivant
de la classe Piece
. Le constructeur de Deck
passe au constructeur
de sa classe mère ses dimensions, qui sont DOMINO_SIDE*2
en largeur et
DOMINO_SIDE*3
en hauteur.
Dans la classe Board
, rajouter en donnée membre publique une instance
m_deck
de Deck
.
Dans Board::populate_pieces
, rajouter à la fin de m_pieces
un pointeur
sur m_deck
.
À ce stade, après compilation générale vous devriez voir un gros rectangle vert en dessous des dominos, et vous devriez pouvoir le déplacer ou le mettre par dessus les dominos en le cliquant.
Comme le sabot m_deck
est plus grand que les dominos, nous allons l'empêcher
de passer dessus : il suffit de surcharger la méthode virtuelle on_button_press
de Deck
en lui faisant mémoriser false
dans raise_on_top
.
Enfin, surcharger la méthode virtuelle draw
de Deck
de manière à
afficher une boîte avec une grosse bordure vert foncé, dont l'intérieur
vert pâle est à peine plus grand qu'un domino, et avec un gros ?
gris
affiché en plein milieu.
Affichage de texte avec Cairo
Pour afficher une chaîne de caractères const char* s
avec Cairo,
dans la fonte de taille int fsize
, on écrit :
cr->set_font_size (fsize);
cr->move_to (x, y);
cr->show_text (s);
cr->stroke();
Les coordonnées x,y
correspondent au coin en bas à gauche du texte.
Si on veut centrer le texte par rapport à x,y
, il faut d'abord
calculer la taille que le texte occupera :
Cairo::TextExtents extents;
cr->get_text_extents (s, extents);
puis opérer une translation de la moitié de la hauteur et de la largeur :
cr->move_to (x - extents.x_advance/2, y + extents.height/2);
avant d'appeler show_text
et stroke
.
1.b. Template de mélange aléatoire
Le but est maintenant de faire apparaître les dominos dans un ordre aléatoire.
Nous allons en profiter pour créer une fonction template. Dans un programme
découpé en modules, les templates doivent être déclarés dans un fichier séparé,
qui est ensuite inclus dans chaque fichier .cpp
qui en a l'utilité.
L'extension de ce fichier de templates variant selon les documentations, nous
choisissons d'utiliser l'extension .tpp
.
Créer un fichier utils.tpp
commençant par une garde #pragma once
.
Écrire à la suite dans ce fichier une fonction template shuffle_vector
qui
reçoit par référence un vecteur v
de type générique T
.
La fonction mélange les éléments de v
dans un ordre aléatoire.
Le principe consiste à recopier le vecteur v
dans un vecteur temporaire tmp
,
puis vider v
avec la méthode clear
; ensuite, tant que tmp
est non vide,
on supprime au hasard un élément de tmp
et on l'insère dans v
.
Pour obtenir des valeurs aléatoires entières entre 0 et \(n-1\) on peut s'inspirer de cet exemple en faisant un modulo \(n\) sur le résultat.
Dans le fichier board.cpp
inclure utils.tpp
.
Rajouter dans la classe Board
une méthode shuffle_dominos
qui
appelle shuffle_vector
sur m_dominos
,
puis initialise les angles de tous les dominos à 90 (en position verticale).
Invalidation des pointeurs
Lorsque la méthode clear
d'un vecteur est appelée, toutes les références
et tous les pointeurs sur les éléments du vecteur sont invalidés,
c'est-à-dire qu'il ne faut plus les utiliser car ils sont susceptibles
d'avoir changé.
Par conséquent, il faudra appeler populate_pieces
à la fin de shuffle_vector
pour mettre à jour tous les pointeurs stockés (ce qui implique de commencer
populate_pieces
en vidant m_pieces
).
Écrire une méthode Board::align_dominos_on_grid
qui modifie les coordonnées des
dominos de m_dominos
de manière à ce qu'ils soient rangés sur 4 rangées de
7 colonnes (on peut reprendre le calcul qui a été fait dans populate_dominos
).
Rajouter un bouton Shuffle
en haut à gauche de la fenêtre, en vous inspirant
du code du bouton m_button_click_me
dans le programme démo du TP2.
La callback du bouton mélangera les dominos à l'aide de Board::shuffle_dominos
,
recalculera leurs coordonnées à l'aide de Board::align_dominos_on_grid
puis rafraîchira l'affichage avec MainWindow::repaint_darea
.
1.c. Déplacement du sabot
Le sabot présente deux zones : la bordure très épaisse, et l'intérieur où est
affiché le ?
. Les gestes sont les suivants :
- geste 1 : on peut déplacer le sabot en cliquant sur la bordure puis en tirant la souris ;
- geste 2 : en cliquant sur l'intérieur on fera apparaître un domino exactement au centre du sabot au moment où la souris est relâchée.
On implémente d'abord le geste 1.
Rajouter dans la classe Deck
une donnée membre privée m_motion_flag
initialisée
à false
.
Dans Deck::on_button_press
, mettre m_motion_flag
à true
si la
souris est située sur la bordure du sabot ; remettre systématiquement
m_motion_flag
à false
dans Deck::on_button_release
.
Dans Deck::on_motion_notify
, si m_motion_flag
est true
, appeler
la méthode de la classe de base (en écrivant this->Piece::on_motion_notify
)
pour déplacer le sabot (sinon on ne fait rien).
Tester que le sabot peut encore être déplacé en cliquant sur la bordure (geste 1), mais qu'il reste bien immobile si on clique à l'intérieur.
1.d. Tirage des dominos
On implémente maintenant le geste 2.
Dans Board
, rajouter une donnée membre privée m_dominos_shown_num
initialisée
à 0.
Dans la méthode Board::populate_pieces
, modifier la boucle d'insertion des
adresses de dominos, afin qu'elle n'insère que les adresses des dominos de 0
à
m_dominos_shown_num
exclu (et bien sûr, l'adresse du sabot m_deck
à la fin).
Ceci va pour le moment "cacher" les dominos ; cela permettra également par la
suite de conserver le comportement du bouton Shuffle
, qui aligne les dominos
déjà tirés.
Écrire une méthode Board::reveal_next_domino (x, y)
qui insère en tête de
m_pieces
un pointeur sur le domino numéro m_dominos_shown_num
de
m_dominos
, centre le domino en x,y
, puis incrémente m_dominos_shown_num
.
Pour accomplir le geste 2, il suffira donc d'appeler Board::reveal_next_domino
dans Deck::on_button_release
lorsque m_motion_flag
est false
, en lui
passant les coordonnées du centre du sabot.
Mais il y a un problème, car le sabot (Deck
) ne connaît pas le plateau de jeu
(Board
) ! La solution consiste à réaliser une injection de dépendance,
c'est-à-dire mémoriser l'adresse du plateau de jeu dans une donnée membre privée
Board* m_board
du sabot. Le meilleur endroit pour le faire est au niveau des
constructeurs : rajouter le paramètre dans le constructeur de Deck
et passer
this
dans la liste d'initialisation du constructeur de Board
.
Mais cela pose encore un problème supplémentaire de dépendance circulaire :
pour définir Board
on a besoin de Deck
et pour définir Deck
on a
maintenant besoin de Board
! La solution consiste dans deck.hpp
à ne pas
inclure board.hpp
mais à faire une déclaration forward class Board;
,
et dans deck.cpp
, à inclure board.hpp
.
Tester le geste 2 ; à ce stade, les dominos doivent apparaître encore dans l'ordre.
Rajouter une méthode Board::restart_game
qui vide m_pieces
, insère &m_deck
, met m_dominos_shown_num
à 0
et appelle shuffle_dominos
.
Rajouter un bouton New game
, dont la callback appelle Board::restart_game
puis rafraîchit l'affichage avec MainWindow::repaint_darea
.
Tester le jeu jusqu'à ce que le sabot soit vide. Améliorer l'affichage en remplaçant
le gros ?
affiché au centre du sabot par un X
dès qu'il est vide.
(Indication : comme le sabot connaît m_board
, il peut lors de l'affichage
interroger une méthode de Board
qui compare m_dominos_shown_num
et la
taille de m_dominos
).