Aller au contenu

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).