Examen de Conception et Programmation orientées objets

Cet article détaille la modélisation UML et l'implémentation partielle en C++ d'un jeu d'échecs. Il couvre les associations, diagrammes d'objets, contraintes UML, et la programmation des pièces, notamment le pion et la dame.

D'après le document Examen de Conception et Programmation orientées objets

Cet article a été rédigé automatiquement à partir du document source, puis vérifié avant publication.

Examen de Conception et Programmation orientées objets

Document source

Examen de Conception et Programmation orientées objets

Informatique, Programmation · École Nationale des Sciences de l'Informatique (ENSI) · PDF · 12 pages · 2008

Afficher l'aperçu du document

Consulter le document original →

Partie 1 : Modélisation UML

Question 1.1 - Diagramme de classes et navigation

a) Complétion du diagramme de classes

Le diagramme de classes initial présente plusieurs lacunes que nous devons combler pour représenter fidèlement les règles du jeu :

  1. Cardinalités entre Piece et Case : Une case de l'échiquier peut contenir au maximum une pièce, et une pièce (tant qu'elle n'est pas capturée hors de l'échiquier) se trouve sur une seule case. La cardinalité est donc de 0..1 du côté de Case et 0..1 du côté de Piece.
  2. Associations pour la classe Coup : Un coup relie plusieurs éléments. Il doit être associé à la case de départ (1), la case d'arrivée (1), la pièce déplacée (1) et éventuellement la pièce capturée (0..1).
  3. Liens unidirectionnels : Pour faciliter la navigation sans surcharger le modèle, un coup doit pointer vers les cases et les pièces concernées (flèches vers Case et Piece), et l'échiquier doit pouvoir naviguer vers ses cases (flèche vers Case).

b) Ordre de navigation

La navigation permet de passer d'un objet à un autre à travers les associations du diagramme pour extraire une information.

  • Savoir si une case est jouable ou non pour une pièce donnée : La séquence proposée par la source est Pièce -> Case -> Echiquier -> (Case)*. Explication : À partir de la Pièce, on navigue vers sa Case actuelle. De cette case, on remonte à l'Echiquier global. Depuis l'échiquier, on peut interroger l'ensemble de ses cases (Case)* pour évaluer les positions d'arrivée possibles en fonction des règles de mouvement de la pièce et de l'occupation des autres cases.

  • Savoir si une pièce est en risque d’attaque par une pièce de l’adversaire : La séquence de la source est Pièce -> Case -> Echiquier -> (Case)* -> Pièce. Explication : On part de la Pièce menacée pour trouver sa Case, puis l'Echiquier. On examine ensuite toutes les cases de l'échiquier (Case)* pour trouver celles occupées par des pièces adverses Pièce. Enfin, on vérifie si la case de notre pièce initiale fait partie des cases jouables par l'une de ces pièces adverses.

Question 1.2 - Diagramme d'objets (Disposition initiale)

Dans un format textuel, un diagramme d'objets se traduit par l'instanciation des classes et l'établissement de leurs liens à l'état initial (t=0).

  • L'instance centrale : Un objet jeu : JeuEchecs lié à un objet ech : Echiquier.
  • Les instances de Cases : L'échiquier est lié à 64 objets de type Case. Comme demandé, nous ne représentons que les cases occupées (32 cases).
    • Exemples de cases : a1 : Case, e1 : Case, e2 : Case, e7 : Case, e8 : Case.
  • Les instances de Pièces et leurs liens : jeu est lié à 32 objets de type Piece (16 blanches, 16 noires), qui sont eux-mêmes liés à leurs cases respectives.
    • TourBlanche1 : Piece (symbole=Tour, couleur=Blanc) est liée à a1 : Case.
    • RoiBlanc : Piece (symbole=Roi, couleur=Blanc) est lié à e1 : Case.
    • PionBlanc5 : Piece (symbole=Pion, couleur=Blanc) est lié à e2 : Case.
    • PionNoir5 : Piece (symbole=Pion, couleur=Noir) est lié à e7 : Case.
    • RoiNoir : Piece (symbole=Roi, couleur=Noir) est lié à e8 : Case.

Question 1.3 - Diagramme d'objets (Après deux coups)

Ici, nous faisons évoluer l'état du système après deux coups typiques d'une ouverture (par exemple, le pion blanc en e2 avance en e4, puis le pion noir en e7 avance en e5).

  • Les nouveaux objets Coup :
    • coup1 : Coup est instancié. Il est lié à JeuEchecs (qui maintient la séquence). Ses liens internes sont :
      • de (départ) : e2 : Case
      • vers (arrivée) : e4 : Case
      • piece_bougee : PionBlanc5 : Piece
    • coup2 : Coup est instancié et lié à JeuEchecs. Ses liens :
      • de : e7 : Case
      • vers : e5 : Case
      • piece_bougee : PionNoir5 : Piece
  • Mise à jour des liens Piece-Case :
    • Le lien entre PionBlanc5 et e2 est détruit. Un nouveau lien est créé entre PionBlanc5 et e4.
    • Le lien entre PionNoir5 et e7 est détruit. Un nouveau lien est créé entre PionNoir5 et e5.

Question 1.4 - Contraintes UML sur les associations

Les contraintes UML permettent de préciser le comportement des collections et des liens au cours du cycle de vie du programme.

  • JeuEchecs-Piece : {addOnly}. Au cours d'une partie, on ajoute les pièces à la collection du jeu à l'initialisation. Même si une pièce est capturée, elle n'est pas détruite du jeu, elle est simplement retirée de l'échiquier (son pointeur de case devient nul). Cette association n'est catégoriquement pas {notUnique} car chaque pièce physique est une entité distincte (il n'y a pas de doublons du même objet exact).
  • JeuEchecs-Coup : {ordered}. L'ordre des coups est absolument fondamental aux échecs pour retracer la partie ou gérer des règles comme la prise en passant. Ce n'est catégoriquement pas {frozen} car de nouveaux coups sont ajoutés à chaque tour.
  • Echiquier-Case : {frozen}. Une fois l'échiquier de 8x8 construit, la structure des 64 cases ne change jamais. On ne détruit ni n'ajoute de cases en cours de partie. Ce n'est ni {notUnique} (chaque case est unique), ni {addOnly} (on n'ajoute plus rien après la création).

Partie 2 : Codage C++

Remarque préliminaire de compilation : Pour que le code de l'examen soit strictement valide, nous assumons l'existence des énumérations Couleur et Symbole, ainsi que les déclarations anticipées (forward declarations) des classes interagissant entre elles. Le code OCR comportait plusieurs erreurs de syntaxe (parenthèses superflues, types manquants) qui ont été rigoureusement corrigées ici.

#include <iostream>
#include <vector>

// Définitions préalables nécessaires pour la validité du code
enum Couleur { BLANC, NOIR };
enum Symbole { PION, ROI, DAME, FOU, TOUR, CAVALIER };

class Case;
class Echiquier;
class Piece;
class Coup;

Question 2.1 - Choix des structures de données

L'implémentation des multiplicités requiert des conteneurs appropriés.

  • JeuEchecs-Piece : Une collection standard non ordonnée est suffisante. Un std::vector<Piece*> ou un std::set<Piece*> fait l'affaire. La source suggère une collection séquentielle ou non.
  • JeuEchecs-Coup : L'ordre est impératif pour conserver l'historique de la partie. Une collection ordonnée comme std::vector<Coup*> ou std::list<Coup*> est idéale.
  • Echiquier-Case : Le nombre de cases est fixe (8x8) et connu à l'avance. Un tableau statique bidimensionnel Case* tab[8][8] (ou l'allocation de la grille directement) est le plus performant pour un accès direct par coordonnées. Une std::map indexée par une chaîne (ex: "e4") serait possible mais plus coûteuse en ressources.

Question 2.2 - Classe abstraite Piece

La classe Piece est au cœur du polymorphisme du jeu. Elle contient des méthodes virtuelles pures, ce qui en fait une classe abstraite.

class Piece {
public:
    // Constructeur avec liste d'initialisation
    Piece(Symbole symbole, Couleur couleur, Case* caseInitiale = NULL)
        : symbole(symbole), couleur(couleur), dans(caseInitiale) 
    {
    }

    virtual ~Piece() {} // Bonne pratique C++ indispensable pour le polymorphisme

    // Méthodes virtuelles liées à la logique de jeu
    virtual bool peutSeDeplacer(Case* c) = 0;
    
    // Par défaut, la capture suit les mêmes règles que le déplacement,
    // sauf pour le Pion qui devra redéfinir cette méthode.
    virtual bool peutCapturer(Case* c) { 
        return peutSeDeplacer(c); 
    }

    // Accesseurs et mutateurs (Getters / Setters)
    void setCase(Case* c) { dans = c; }
    Case* getCase() const { return dans; }
    Couleur getCouleur() const { return couleur; }
    Symbole getSymbole() const { return symbole; }

private:
    Symbole symbole;
    Couleur couleur;
    Case* dans;
};

Question 2.3 - Modélisation des coordonnées de la Case

a) Avantages et inconvénients de la mémorisation des coordonnées

  • Mémoriser les coordonnées dans la classe Case :
    • Avantage : L'accès est direct en O(1). Une pièce peut instantanément connaître sa position spatiale en consultant l'objet Case sur lequel elle se trouve.
    • Inconvénient : Il y a une redondance d'information. L'échiquier possède déjà la structure spatiale (le tableau d'indices [i][j]), et stocker la coordonnée dans la case duplique cette donnée (risque d'incohérence si mal géré).
  • Ne pas mémoriser les coordonnées :
    • Avantage : Les objets Case sont plus légers en mémoire. Aucune redondance, ce qui garantit l'intégrité structurelle de l'échiquier.
    • Inconvénient : Retrouver les coordonnées d'une case donnée nécessite de parcourir l'échiquier en O(n) ou de passer par des méthodes de l'Echiquier (comme getLigne() et getColonne()) qui effectuent des recherches.

b) Code de l'interface de la classe Case

Nous suivons l'approche de la source, qui choisit de ne pas stocker les coordonnées directement, forçant le passage par l'échiquier.

class Case {
public:
    // Le constructeur prend l'échiquier en paramètre pour la navigation ascendante
    Case(Echiquier* e);
    
    void setPiece(Piece* p);
    Piece* getPiece();
    Echiquier* getEchiquier();

private:
    Piece* contient;
    Echiquier* ech; // Permet de remonter vers l'échiquier pour trouver les coordonnées
};

Question 2.4 - Classe Coup

Le coup mémorise l'état d'un mouvement. C'est l'implémentation classique du patron de conception de type Command.

class Coup {
public:
    Coup(Case* de, Case* vers, Piece* pb, Piece* pc = NULL)
        : de(de), vers(vers), piece_bougee(pb), piece_capturee(pc) 
    {
    }

    // Accesseurs
    Case* getDe() const { return de; }
    Case* getVers() const { return vers; }
    Piece* getPieceBougee() const { return piece_bougee; }
    Piece* getPieceCapturee() const { return piece_capturee; }

private:
    Case* de;
    Case* vers;
    Piece* piece_bougee;
    Piece* piece_capturee;
};

Question 2.5 - Classe Echiquier

L'échiquier gère le tableau des cases et leur cycle de vie. Le code OCR contenait des erreurs de pointeurs (appel d'un constructeur sur un tableau de pointeurs sans allocation).

class Echiquier {
public:
    Echiquier() {
        for (int i = 0; i < 8; i++) {
            for (int j = 0; j < 8; j++) {
                tab[i][j] = new Case(this); // Allocation dynamique pour chaque pointeur
            }
        }
    }

    ~Echiquier() {
        for (int i = 0; i < 8; i++) {
            for (int j = 0; j < 8; j++) {
                delete tab[i][j];
            }
        }
    }

    // Retourne un pointeur vers la case aux coordonnées données
    Case* getCase(char lig, char col);
    
    // Vérifie si une case spécifique contient une pièce
    bool estOccupe(Case* c);
    
    // Positionne les pièces pour le début de la partie
    void initialiser();

    // Renvoie la ligne de la case passée en paramètre
    char getLigne(Case* c);
    
    // Renvoie la colonne de la case passée en paramètre
    char getColonne(Case* c);

private:
    Case* tab[8][8];
};

Question 2.6 - Attributs de JeuEchecs

Pour distinguer les rois, on peut utiliser des pointeurs spécifiques ou les placer à des index fixes dans le vecteur de pièces.

class JeuEchecs {
private:
    Echiquier ech;
    std::vector<Piece*> pieces;
    std::vector<Coup*> coups;
    Couleur TourDeQui;
    
    // Distinction des rois pour accélérer les vérifications d'échec et mat :
    Piece* roiBlanc;
    Piece* roiNoir;
};

Question 2.7 - Logique de déplacement (Méthode jouer)

Le code proposé par la source contient la logique centrale de déplacement, mais nécessitait des corrections de syntaxe importantes (majuscules incohérentes, points-virgules manquants, gestion des espaces de noms).

bool JeuEchecs::jouer() {
    char l1, c1, l2, c2;
    std::cin >> l1 >> c1 >> l2 >> c2; // ex: 2 e 4 e (pour e2 vers e4)
    
    Case* source = ech.getCase(l1, c1);
    Case* cible = ech.getCase(l2, c2);

    Piece* piece_bougee = source->getPiece();

    // Vérification : la case source doit contenir une pièce, 
    // et cette pièce doit appartenir au joueur dont c'est le tour
    if ((piece_bougee == NULL) || (piece_bougee->getCouleur() != TourDeQui)) {
        return false;
    }

    Piece* piece_capturee = cible->getPiece();

    // Vérification de la validité du mouvement (déplacement simple ou capture)
    if (piece_bougee->peutSeDeplacer(cible) || piece_bougee->peutCapturer(cible)) {
        
        // Historisation du coup
        coups.push_back(new Coup(source, cible, piece_bougee, piece_capturee));

        // Mise à jour de l'échiquier
        source->setPiece(NULL);
        cible->setPiece(piece_bougee);
        piece_bougee->setCase(cible);

        // Si une pièce est capturée, on la retire de l'échiquier
        if (piece_capturee != NULL) {
            piece_capturee->setCase(NULL);
        }

        return true;
    } else {
        return false;
    }
}

(Note pédagogique : Cette implémentation simplifiée issue du corrigé officiel assume que peutSeDeplacer et peutCapturer s'occupent de vérifier s'il s'agit d'une tentative de prendre sa propre pièce, ou de vérifier si le chemin est dégagé pour les pièces à longue portée).

Question 2.8 - Implémentation du Pion

a) Attributs spécifiques : Oui, la classe Pion a besoin d'un état spécifique. Contrairement aux autres pièces, son comportement de mouvement dépend de son historique : le tout premier déplacement l'autorise à avancer de deux cases au lieu d'une. Il faut donc un attribut booléen aFaitDeplacement.

b) Code de la classe Pion :

L'algorithme extrait de la source a été corrigé pour retirer les parenthèses excédentaires et uniformiser la logique mathématique (on assume que getLigne() renvoie des entiers ou des caractères où la soustraction donne un entier mathématique exact, comme '3' - '2' = 1).

class Pion : public Piece {
private:
    bool aFaitDeplacement;

public:
    Pion(Couleur couleur, Case* c) : Piece(PION, couleur, c) {
        aFaitDeplacement = false;
    }

    bool peutSeDeplacer(Case* cible) override {
        Case* source = getCase();
        Echiquier* ech = source->getEchiquier();
        
        char l1 = ech->getLigne(source);
        char c1 = ech->getColonne(source);
        char l2 = ech->getLigne(cible);
        char c2 = ech->getColonne(cible);

        // Un pion ne peut avancer tout droit que si la case cible est vide
        if (cible->getPiece() != NULL) return false;

        // Déplacement vertical strict pour 'peutSeDeplacer' (la capture est diagonale)
        if (c1 != c2) return false;

        if (aFaitDeplacement) {
            if (getCouleur() == BLANC) {
                return ((l2 - l1) == 1);
            } else {
                return ((l1 - l2) == 1); // Les noirs avancent en descendant
            }
        } else {
            if (getCouleur() == BLANC) {
                return (((l2 - l1) == 1) || ((l2 - l1) == 2));
            } else {
                return (((l1 - l2) == 1) || ((l1 - l2) == 2));
            }
        }
    }
};

Question 2.9 - Classe Dame et Héritage Multiple

Le déplacement d'une dame est la stricte union du déplacement d'une tour (colonnes et rangées) et d'un fou (diagonales). En C++, nous pouvons utiliser l'héritage multiple pour combiner ces comportements sans réécrire les algorithmes géométriques.

La classe Dame hérite publiquement de Tour et de Fou. Sa redéfinition de peutSeDeplacer délègue simplement l'appel aux classes parentes :

class Tour : virtual public Piece { /* ... */ };
class Fou : virtual public Piece { /* ... */ };

// Héritage multiple. L'héritage virtuel des classes parentes résout le problème du diamant.
class Dame : public Tour, public Fou {
public:
    Dame(Couleur couleur, Case* c) : Piece(DAME, couleur, c), Tour(couleur, c), Fou(couleur, c) {}

    bool peutSeDeplacer(Case* c) override {
        // La Dame peut se déplacer si la Tour le peut OU si le Fou le peut.
        return (Tour::peutSeDeplacer(c) || Fou::peutSeDeplacer(c));
    }
};

Méthode

Pour exceller dans ce type d'épreuve liant modélisation conceptuelle (UML) et implémentation (C++) :

  1. Suivez le flux des dépendances : Ne vous précipitez pas sur le code. Les questions UML posent l'architecture mémoire du programme. Si l'UML définit une contrainte {ordered}, cela doit instantanément déclencher la notion de vector ou list dans la partie C++.
  2. Analysez les implications des flèches : Les liens de navigabilité en UML déterminent directement quels objets possèderont des attributs de pointeurs vers d'autres objets. Un lien de Case vers Echiquier signifie que la classe C++ Case devra stocker un pointeur Echiquier*.
  3. Apprenez à réparer du code : Dans un examen papier, il est fréquent que les algorithmes fournis manquent de gestion des exceptions ou utilisent des raccourcis. Identifiez le sens fonctionnel (ex: avancer vers le nord pour les blancs, vers le sud pour les noirs) et traduisez-le en expressions logiques strictes.
  4. Méfiez-vous de l'héritage multiple : L'astuce de la question 2.9 est élégante sur le papier, mais rappelez-vous qu'elle exige la maîtrise de l'héritage virtuel (virtual public Piece) pour éviter la duplication des attributs de la classe ancêtre Piece (le fameux problème du diamant). Le mentionner démontre une maîtrise supérieure du langage C++.

Partager

Commentaires

Aucun commentaire pour le moment. Posez la première question.

Les commentaires sont relus avant publication. Votre e-mail n'est jamais affiché.

← Toutes les révisions