Programmation C++ : TP séance 06
- 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 deadline indiqué sur Ametice.
Exercice 1. Grille magnétique et règles du jeu de domino
Le but de cette dernière planche est de parachever le programme du TP précédent de manière à pouvoir jouer une partie de domino en solitaire.
Nous allons donc implémenter une grille magnétique pour positionner précisément les pièces, et vérifier le placement correct du domino joué par rapport aux dominos déjà placés.
1.a. Grille magnétique
L'idée est de pouvoir déplacer librement une pièce à la souris, mais lorsque le bouton de la souris est relâché, d'essayer de positionner son coin supérieur gauche sur le sommet magnétique le plus proche.
Les sommets magnétiques sont les points d'intersection
d'une grille rectilinéaire, appelée grille magnétique.
Leurs coordonnées sont des multiples entiers du pas de la grille,
c'est-à-dire de la taille d'une case de la grille.
Étant donné que chaque pièce (domino ou sabot) a une largeur et hauteur
multiples de DOMINO_SIDE
, et que l'on doit pouvoir placer les dominos doubles
perpendiculairement à un autre domino, on choisit comme pas de la grille
DOMINO_SIDE/2
, comme au TP1.
Traçons d'abord la grille magnétique.
Dans MainWindow
rajouter des données membre m_darea_width
et
m_darea_height
. Récupérer et mémoriser ces dimensions dans la callback
darea_on_size_allocate
, qui est appelée automatiquement par GTK à chaque
changement de taille du DrawingArea
.
Dans Board
rajouter une méthode
draw_magnetic_grid
recevant en paramètres le contexte Cairo cr
ainsi que la
largeur et la hauteur du DrawingArea
. La méthode trace en gris très clair
les traits horizontaux et verticaux de la grille magnétique sur tout le
DrawingArea
.
Appeler la méthode draw_magnetic_grid
dans darea_on_draw
au début.
Passons maintenant au placement magnétique d'une pièce.
Recopier dans le module utils
la fonction suivante, qui renvoie la
coordonnée grille entière la plus proche de la coordonnée écran t
,
où grid_side
est le pas de la grille :
int get_magnet_coord (double grid_side, double t)
{
return t / grid_side + (t >= 0 ? 0.5 : -0.5);
}
Rajouter dans la classe Piece
une méthode
void get_closer_magnet (double grid_side, int& x_mag, int& y_mag)
qui mémorise dans x_mag
et y_mag
le résultat de get_magnet_coord
pour les coordonnées de la pièce.
Rajouter également dans Piece
une méthode
bool place_on_magnetic_grid (double grid_side, double dist_threshold)
qui calcule les coordonnées grille (entières) du point magnétique le plus proche
(le point cible) à l'aide de get_closer_magnet
, puis multiplie les
coordonnées grille par grid_side
pour obtenir les coordonnées écran réelles du
point cible. La méthode calcule ensuite la distance entre le point cible et la
pièce. Si la distance obtenue est inférieure au seuil dist_threshold
, alors la
méthode déplace la pièce sur les coordonnées du point cible et renvoie true
,
sinon elle laisse les coordonnées de la pièce inchangées et renvoie false
.
Appeler place_on_magnetic_grid
dans Deck::on_button_release
lorsque
m_motion_flag
est true
, avec le pas DOMINO_SIDE/2
, et un seuil
de distance DOMINO_SIDE/4
.
Appeler aussi place_on_magnetic_grid
dans Domino::on_button_release
,
après le test sur le rivet, avec les mêmes pas et seuil de distance.
Vérifier que le sabot et les dominos se placent bien sur la grille magnétique si on les relâche à proximité, et que l'on peut aussi les placer "entre" les points magnétiques sans qu'ils soient attirés, grâce au seuil de distance choisi.
1.b. Modification de l'injection de dépendance
Au TP précédent nous avions effectué une injection de dépendance
en passant l'adresse de l'instance du plateau de jeu (classe Board
) au
constructeur du sabot (Deck
).
Or, il s'avère que les dominos auront également besoin d'accéder au
plateau de jeu. Pour uniformiser, nous allons déplacer cette injection
de dépendance au niveau des pièces (Piece
) :
- rajouter dans
piece.cpp
etdomino.cpp
: l'inclusion deboard.hpp
; - déplacer de
deck.hpp
verspiece.hpp
: la déclaration forward deBoard
, et la déclaration dem_board
(en première donnée membre pour éviter un warning par la suite) ; - rajouter le paramètre
Board* board
dans les constructeurs dePiece
etDomino
, en le mettant en premier ; - modifier les listes d'initialisation des constructeurs de
Piece
etDomino
; - enfin dans
Board::populate_dominos
, passerthis
en paramètre à l'instanciation des dominos.
Vérifier que tout compile bien et sans warning avant de passer à la suite.
1.c. Validation du placement
1.c.1. Dominos validés
Pour distinguer les dominos bien placés, de ceux qui n'ont pas encore été
mis à une place valide, on introduit dans la classe Domino
une donnée
membre privée m_validated
, et les accesseurs.
Initialiser la donnée membre à false
dans le constructeur et dans
Board::reveal_next_domino
.
Dans Domino::draw
afficher le domino dans une couleur différente selon
m_validated
, par exemple en jaune paille (non validé) ou en bleu pâle (validé).
Dans Board
, rajouter une méthode bool validate_domino_position (Domino& domino)
qui renvoie pour le moment true
.
Dans Domino::on_button_release
,
si place_on_magnetic_grid
a renvoyé true
,
alors stocker dans m_validated
le résultat de validate_domino_position
,
sinon stocker false
.
À ce stade, lorsqu'on clique sur le sabot, le nouveau domino doit apparaître en jaune ; si on le place entre les sommets magnétiques, il doit rester en jaune, et si on le place sur la grille magnétique il doit passer en bleu ; il doit repasser en jaune si on le remet entre les points magnétiques.
1.c.2. Principe
Occupons-nous maintenant de Board::validate_domino_position
, qui constitue
la petite partie algorithmique du projet. On suppose que lorsque la méthode
est appelée, le domino est situé sur la grille magnétique. Le but est
d'établir si le domino est bien placé par rapport aux dominos déjà validés
(il peut y avoir des dominos non validés sur le plateau de jeu).
L'idée est de construire un tableau temporaire grid
en deux dimensions,
plaqué sur la grille magnétique : chaque domino occupe 8 cases, les points
du domino sont enregistrés chacun dans les 4 cases correspondantes ;
la valeur spéciale -1
signale les cases vides, c'est-à-dire sans domino.
De la sorte, pour valider
le domino il suffira de vérifier que toutes les cases de grid
sont vides
en dessous du domino, et que les cases autour du domino sont compatibles avec
le nombre de points du domino, et qu'il y a un domino voisin pour
l'accrocher.
La méthode Board::validate_domino_position
commence par compter le
nombre de dominos déjà affichés qui sont validés. Si ce nombre est 0, la méthode
renvoie true
car il s'agit du premier domino aligné sur la grille magnétique.
1.c.3. Boîte englobante
Ensuite, la méthode va calculer la boîte englobante des dominos déjà validés,
c'est-à-dire le plus petit rectangle rectilinéaire qui les contient tous,
en coordonnées écran.
Pour ce faire, déclarer un struct BoundingBox
dans le module utils
,
avec comme données membre m_xmin, m_xmax, m_ymin, m_ymax
de type double
initialisées à 0
.
La méthode init (double xmin, double xmax, double ymin, double ymax)
initialise les 4 données membre avec les paramètres ;
la méthode update_min (double x, double y)
met à jour m_xmin
et m_ymin
en faisant un min sur les coordonnées, et de même pour la méthode
update_max
avec un max.
Écrire une méthode Board::find_bounding_box (BoundingBox &bbox)
qui
parcourt les dominos déjà validés ; au premier rencontré elle appelle
bbox.init
en lui passant les coordonnées des extrémités du domino,
et pour les suivants, elle appelle les méthodes update_min
et update_max
pour le coin supérieur gauche et le coin inférieur droit, respectivement,
de ces dominos.
Appeler find_bounding_box
dans validate_domino_position
,
et à la suite, "élargir" bbox
de l'épaisseur d'un domino
(autrement dit, diminuer m_xmin
et m_ymin
de DOMINO_SIDE
,
et augmenter m_xmax
et m_ymax
de DOMINO_SIDE
).
1.c.4. Grille 2D
On passe à la création de grid
.
À l'aide de get_magnet_coord
, transformer les coordonnées de la boîte
englobante en coordonnées entières gxmin, gymin, gxmax, gymax
sur la grille
magnétique. Les dimensions de grid
seront donc
int grid_w = gxmax - gxmin, grid_h = gymax - gymin
.
Pour la beauté du geste, on déclare le type de grid
sous la forme
d'une classe template Grid2D
pour un type générique T
, dans le
fichier utils.tpp
, avec le cahier des charges suivant :
- les données membre privées sont : un vecteur de
T
nommém_data
, les dimensionsm_width
etm_height
, et la valeur par défaut d'une caseempty
de typeT
; - le constructeur reçoit en paramètre
width
,height
etempty
, les mémorise dans les données membre, puis réserve un espace dewidth*height
cases initialisées àempty
(méthodeassign
destd::vector
) ; - la méthode
check_coordinates
reçoit des coordonnées entièresx
ety
puis renvoietrue
si les coordonnées sont dans les bornes ; - les getters
width()
,height()
etdata(x,y)
; cette dernière renvoie la valeur de la casem_data[y*m_width+x]
si les coordonnées sont dans les bornes, sinonempty
; - le setter
data(x,y,value)
qui affecte la casem_data[y*m_width+x]
àvalue
si les coordonnées sont dans les bornes, sinon àempty
.
1.c.5. Marquage
On retourne dans Board::validate_domino_position
; on peut maintenant
déclarer grid
avec le type Grid2D<int>
, en passant au constructeur
les dimensions précédemment calculées grid_w
et grid_h
, et la valeur
-1
pour les cases vides.
Ensuite on appelle Board::fill_marking_pieces
en lui passant par
référence grid
et bbox
. Cette méthode est chargée de réaliser le
marquage des pièces dans grid
, c'est-à-dire de mémoriser
pour chaque domino affiché et validé, les points du dominos dans grid
,
sur 2x4 ou 4x2 cases selon l'angle.
Étant donné un domino d
aligné sur la grille magnétique,
la case correspondant au coin supérieur gauche du domino a
pour coordonnées dans grid
:
int gx1 = get_magnet_coord (DOMINO_SIDE/2, d.xb()) - gxmin;
int gy1 = get_magnet_coord (DOMINO_SIDE/2, d.yb()) - gymin;
(on peut retrouver gxmin
et gymin
à partir de bbox
, voir
au début de 1.c.4).
Chaque demi-domino est un carré de taille 2x2. Petite astuce : si on déclare
int xa = angle == 180, ya = angle == 270, xb = angle == 0, yb = angle == 90;
alors le coin en haut à gauche du carré 2x2 pour les points m_a
du domino
ont pour coordonnées gx1+xa*2, gy1+ya*2
, et celles pour m_b
sont
gx1+xb*2, gy1+yb*2
.
À ce stade dans validate_domino_position
, afficher grid
dans le
terminal de manière à tester le remplissage de grid
lorsqu'on place
des dominos validés. (Bon à savoir : pour afficher quelque chose avec une largeur
fixe de x
caractères, envoyer std::setw(x)
avant la valeur dans std::cout
).
1.c.6. Critères
Écrire la méthode Board::marking_is_empty
, qui reçoit
par référence les paramètres grid
, bbox
et un domino d
.
La méthode renvoie true
si les deux carrés 2x2 correspondant à d
dans grid
sont vides, c'est-à-dire si les 8 cases sont à -1
.
Appeler marking_is_empty
dans validate_domino_position
; si le résultat
est false
, renvoyer false
.
On y est presque : écrire la méthode Board::check_neighbours
qui reçoit
par référence les paramètres grid
, bbox
et un domino d
.
La méthode examine pour chaque carré 2x2 du domino, les 8 voisins adjacents
au carré (il y en aura 2 parmi les 8 qui seront dans l'autre carré 2x2,
ça n'a pas d'importance puisque les 2 carrés sont vides).
Si l'un des 8 voisins du carré correspondant aux points m_a
est différent
de -1
et de m_a
, on renvoie false
; idem pour le carré de m_b
.
Soit n1
le nombre de voisins à m_a
du premier carré, et n2
le nombre
de voisins à m_b
du second carré ; la méthode renvoie true
si n1+n2 == 2
,
autrement dit si le nombre de cases voisines à la bonne valeur est exactement 2.
Appeler check_neighbours
tout à la fin de validate_domino_position
et
renvoyer le résultat.
1.c.7. Conclusion
On peut enfin tester le jeu et faire une partie. Notre implémentation vérifie les règles du jeu de façon simplifiée et il manque la détection pour certaines configurations.
C'est la fin de cette série de TP pour l'apprentissage du langage C++, félicitations pour être parvenu jusque là !!!