Aller au contenu

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 et domino.cpp : l'inclusion de board.hpp ;
  • déplacer de deck.hpp vers piece.hpp : la déclaration forward de Board, et la déclaration de m_board (en première donnée membre pour éviter un warning par la suite) ;
  • rajouter le paramètre Board* board dans les constructeurs de Piece et Domino, en le mettant en premier ;
  • modifier les listes d'initialisation des constructeurs de Piece et Domino ;
  • enfin dans Board::populate_dominos, passer this 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 dimensions m_width et m_height, et la valeur par défaut d'une case empty de type T ;
  • le constructeur reçoit en paramètre width, height et empty, les mémorise dans les données membre, puis réserve un espace de width*height cases initialisées à empty (méthode assign de std::vector) ;
  • la méthode check_coordinates reçoit des coordonnées entières x et y puis renvoie true si les coordonnées sont dans les bornes ;
  • les getters width(), height() et data(x,y) ; cette dernière renvoie la valeur de la case m_data[y*m_width+x] si les coordonnées sont dans les bornes, sinon empty ;
  • le setter data(x,y,value) qui affecte la case m_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à !!!