Examen de Conception et Programmation orientées objets
Partie 1 : Modélisation UML Question 1.1 - Complétions du diagramme de classes Le diagramme initialement proposé (bien qu'omis visuellement, son contenu se déduit de l'énoncé) manque de plusieurs éléments cruciaux de modélisation orientée objet pour un jeu d'échecs.
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.

Document source
Programmation, Informatique, Jeux d'Échecs · École Nationale des Sciences de l'Informatique (ENSI) · PDF · 6 pages · 2009
Afficher l'aperçu du document
Partie 1 : Modélisation UML
Question 1.1 - Complétions du diagramme de classes
Le diagramme initialement proposé (bien qu'omis visuellement, son contenu se déduit de l'énoncé) manque de plusieurs éléments cruciaux de modélisation orientée objet pour un jeu d'échecs.
a) Les trois manques à compléter :
- Cardinalités
Piece-Case: Une pièce se trouve à un instant donné sur au maximum une case (0 si elle est capturée, 1 si elle est sur l'échiquier). Une case contient au maximum une pièce (0 si elle est vide, 1 si elle est occupée).- Les cardinalités sont donc
0..1du côtéCaseet0..1du côtéPiece.
- Les cardinalités sont donc
- Associations de la classe
Coup: Un coup modifie l'état du jeu. Il doit donc être relié à :- La
Piecedéplacée (cardinalité1..1). - La
Casede départ (cardinalité1..1). - La
Cased'arrivée (cardinalité1..1). - La
Pieceadverse potentiellement capturée (cardinalité0..1).
- La
- Liens unidirectionnels :
Afin de minimiser le couplage, certaines associations doivent être navigables dans un seul sens. Par exemple, le
Coupa besoin de connaître sesCasede départ et d'arrivée, mais uneCasen'a pas à mémoriser l'historique des coups qui l'ont impliquée. Les flèches de navigabilité pointeront donc deCoupversCaseetPiece. De même, il est pratique quePiecepuisse naviguer versCasepour connaître sa position (lien unidirectionnel ou bidirectionnel selon l'implémentation, mais l'énoncé suggère d'imposer des navigabilités strictes).
b) Ordre de navigation nécessaire :
- Savoir si une case est jouable ou non pour une pièce donnée :
Depuis l'objet
Piece, on navigue vers l'objetCasecible pour vérifier si elle est occupée. Si un lien vers unePiecey existe, on navigue vers cettePiecepour vérifier sa couleur (qui doit être différente de la pièce attaquante). - Savoir si une pièce est en risque d'attaque :
Depuis l'objet
JeuEchecs, on navigue vers l'ensemble des objetsPieceappartenant à l'adversaire. Pour chacune de ces pièces, on appelle sa logique de mouvement pour voir si elle peut atteindre laCaseoù se trouve notre pièce (navigationPieceadverses ->Caseactuelle).
Question 1.2 - Diagramme d'objets à la disposition initiale
Puisque nous ne représentons que les cases occupées, voici la modélisation sous forme tabulaire des instances et de leurs liens à l'état initial.
| Instance de Piece | Attributs (Symbole, Couleur) | Lien vers l'instance de Case (Occupée) |
|---|---|---|
pionBlanc1 : Piece |
Pion, Blanc | caseA2 : Case (a, 2) |
tourBlanche1 : Piece |
Tour, Blanc | caseA1 : Case (a, 1) |
roiBlanc : Piece |
Roi, Blanc | caseE1 : Case (e, 1) |
pionNoir1 : Piece |
Pion, Noir | caseA7 : Case (a, 7) |
roiNoir : Piece |
Roi, Noir | caseE8 : Case (e, 8) |
(Note : Dans un examen écrit classique, ceci serait dessiné avec des boîtes nom:Classe reliées par des traits simples. Les 32 pièces sont instanciées et reliées à leurs cases respectives).
Question 1.3 - Diagramme d'objets après deux coups
Si l'on imagine un début de partie classique (ex: le pion du roi blanc avance de deux cases, puis le pion du roi noir avance de deux cases).
| Instance de Piece | État après 2 coups | Lien vers la nouvelle instance de Case |
|---|---|---|
pionBlancRoi : Piece |
A bougé (e2 -> e4) | caseE4 : Case (e, 4) |
pionNoirRoi : Piece |
A bougé (e7 -> e5) | caseE5 : Case (e, 5) |
roiBlanc : Piece |
N'a pas bougé | caseE1 : Case (e, 1) |
Le reste des instances reste inchangé par rapport à la question 1.2, mais deux objets Coup ont été instanciés et ajoutés à la collection de l'objet JeuEchecs.
Question 1.4 - Contraintes des associations
JeuEchecs-Piece:{frozen}. L'ensemble des 32 pièces d'une partie est instancié à la création du jeu et ne change jamais. Même capturée, une pièce fait toujours partie de la collection des pièces de la partie (elle est juste retirée de l'échiquier).JeuEchecs-Coup:{ordered}, {addOnly}. L'ordre des coups est strictement chronologique et crucial pour rejouer la partie ou vérifier la légalité d'une position. On ne fait qu'ajouter de nouveaux coups à cet historique.Echiquier-Case:{frozen}. Les 64 cases d'un échiquier sont immuables. Elles sont créées lors de l'instanciation de l'échiquier et ne sont ni détruites ni modifiées dans leur essence.
Partie 2 : Codage C++
Question 2.1 - Choix des structures de données
JeuEchecs-Piece:Piece* pieces[32];(oustd::vector<Piece*>). Commentaire : Le nombre de pièces d'un jeu d'échecs est strictement fixe et connu à l'avance (32 pièces). Un tableau statique C++ est donc parfaitement adapté et très performant.JeuEchecs-Coup:std::vector<Coup*> coups;. Commentaire : Le nombre de coups est inconnu à l'avance et la liste grandit au fil du temps par ajouts successifs à la fin. Unstd::vector(collection de la STL) est la structure idéale pour le{ordered}et{addOnly}.Echiquier-Case:Case* cases[8][8];. Commentaire : La grille est fixe (8 colonnes sur 8 rangées). Un tableau statique à deux dimensions reflète naturellement la géométrie spatiale de l'échiquier.
Question 2.2 - Classe Piece
a) Code C++ de la classe Piece
#include <iostream>
enum Symbole { Pion, Roi, Dame, Fou, Tour, Cavalier };
enum Couleur { Blanc, Noir };
class Case; // Déclaration anticipée
class Piece {
protected:
Symbole symbole;
Couleur couleur;
Case* position; // Pointeur vers la case actuelle (0..1)
public:
// Constructeur : interface + implémentation
Piece(Symbole s, Couleur c, Case* p = nullptr)
: symbole(s), couleur(c), position(p) {}
virtual ~Piece() {}
virtual bool peutSeDeplacer(Case* cible) = 0;
virtual bool peutCapturer(Case* c) { return peutSeDeplacer(c); }
// les méthodes : interface + implémentation
Case* getPosition() const { return position; }
void setPosition(Case* c) { position = c; }
Couleur getCouleur() const { return couleur; }
Symbole getSymbole() const { return symbole; }
};
Question 2.3 - La classe Case et ses coordonnées
a) Discussion sur la mémorisation des coordonnées
- Avantage : Mémoriser les coordonnées (
colonneetrangée) directement dans la classeCasepermet à toute méthode recevant un objetCase*de calculer instantanément des trajectoires et des distances (par exemple danspeutSeDeplacer), sans avoir à interrogerEchiquierpour retrouver où se trouve cette case dans le tableau 2D. - Inconvénient : Il y a une redondance de l'information si l'échiquier maintient déjà les cases dans un tableau indicé (ex:
cases[3][4]). Cela demande de la rigueur à l'initialisation pour garantir que la case stockée en[3][4]possède bien les coordonnées correspondantes, sous peine d'incohérences.
b) Code C++ de l'interface de la classe Case
class Case {
private:
char colonne; // 'a' à 'h'
int rangee; // 1 à 8
Piece* piece; // Pointeur vers la pièce posée (0..1)
public:
Case(char col, int ran);
~Case();
char getColonne() const;
int getRangee() const;
Piece* getPiece() const;
void setPiece(Piece* p);
bool estOccupe() const;
};
Question 2.4 - Classe Coup
class Coup {
private:
Piece* pieceDeplacee;
Case* depart;
Case* arrivee;
Piece* pieceCapturee; // Peut être nullptr s'il n'y a pas de prise
public:
// Constructeur
Coup(Piece* deplacee, Case* dep, Case* arr, Piece* capturee = nullptr);
~Coup();
// Méthodes (getters)
Piece* getPieceDeplacee() const;
Case* getDepart() const;
Case* getArrivee() const;
Piece* getPieceCapturee() const;
};
Question 2.5 - Classe Echiquier
class Echiquier {
private:
Case* cases[8][8]; // Matrice des 64 cases
public:
// Constructeur implémenté
Echiquier() {
for(int i = 0; i < 8; i++) {
for(int j = 0; j < 8; j++) {
// i correspond aux colonnes ('a' + i)
// j correspond aux rangées (1 + j)
cases[i][j] = new Case('a' + i, 1 + j);
}
}
}
~Echiquier();
// Interfaces des méthodes demandées
// Vérifie si la case fournie en paramètre contient une pièce
bool estOccupe(Case* c) const;
// Place une pièce sur la case c
void placer(Piece* p, Case* c);
// (Optionnel pour l'interface : un getter pour récupérer une case spécifique)
Case* getCase(char colonne, int rangee) const;
};
Question 2.6 - Classe JeuEchecs et attributs
Pour identifier rapidement si un joueur est en situation d'échec sans parcourir toutes les pièces à chaque tour, il est judicieux de maintenir des pointeurs directs vers les deux Rois.
#include <vector>
class JeuEchecs {
private:
Echiquier* echiquier;
std::vector<Piece*> pieces; // Collection des 32 pièces ({frozen})
std::vector<Coup*> coups; // Historique des coups ({ordered, addOnly})
Couleur tourActuel; // Indique à quel joueur de jouer
// Distinguer les rois des deux joueurs pour la vérification rapide du Mat/Echec
Piece* roiBlanc;
Piece* roiNoir;
public:
JeuEchecs();
~JeuEchecs();
bool jouer();
};
Question 2.7 - Méthode jouer() de JeuEchecs
bool JeuEchecs::jouer() {
// 1. Demander à l'utilisateur (ou interface) les positions (Simulation)
Case* depart = nullptr; /* = interface.demanderCaseDepart() */
Case* arrivee = nullptr; /* = interface.demanderCaseArrivee() */
// Vérifications de base (simplifiées pour la logique de l'examen)
if (depart == nullptr || arrivee == nullptr) return false;
if (!depart->estOccupe()) return false;
Piece* pieceAJouer = depart->getPiece();
// Vérifier que la pièce appartient au joueur dont c'est le tour
if (pieceAJouer->getCouleur() != tourActuel) return false;
// 2. Valider le coup selon les règles de mouvement ou de capture
bool coupValide = false;
Piece* pieceAdverse = arrivee->getPiece();
if (pieceAdverse != nullptr) {
if (pieceAdverse->getCouleur() != pieceAJouer->getCouleur()) {
coupValide = pieceAJouer->peutCapturer(arrivee);
}
} else {
coupValide = pieceAJouer->peutSeDeplacer(arrivee);
}
// 3. Effectuer le déplacement si valide
if (coupValide) {
// Enregistrement de la capture si existante
if (pieceAdverse != nullptr) {
pieceAdverse->setPosition(nullptr); // Sortie de l'échiquier
}
// Mise à jour des liens
depart->setPiece(nullptr);
arrivee->setPiece(pieceAJouer);
pieceAJouer->setPosition(arrivee);
// Historisation
coups.push_back(new Coup(pieceAJouer, depart, arrivee, pieceAdverse));
// Changement de tour
tourActuel = (tourActuel == Blanc) ? Noir : Blanc;
return true;
}
return false;
}
Question 2.8 - Implémentation du Pion
a) Attributs spécifiques
Oui, le pion possède une mécanique unique : "La première fois qu'il se déplace, il peut avancer de deux cases". Il a donc besoin d'un attribut spécifique booléen (par exemple bool premierDeplacement) initialisé à true et passant à false après son premier coup.
b) Code de la classe Pion
#include <cmath> // pour std::abs
class Pion : public Piece {
private:
bool premierDeplacement;
public:
// Constructeur implémenté
Pion(Couleur c, Case* p = nullptr)
: Piece(Pion, c, p), premierDeplacement(true) {}
// Méthode de déplacement
bool peutSeDeplacer(Case* cible) override {
if (cible == nullptr) return false;
Case* posActuelle = getPosition();
if (posActuelle == nullptr) return false;
int deplacementY = cible->getRangee() - posActuelle->getRangee();
int deplacementX = cible->getColonne() - posActuelle->getColonne();
// Le pion ne bouge pas de colonne en déplacement normal
if (deplacementX != 0) return false;
// La cible ne doit pas être occupée
if (cible->estOccupe()) return false;
// Sens de déplacement selon la couleur (Blanc monte, Noir descend)
int direction = (getCouleur() == Blanc) ? 1 : -1;
// Avancer d'une case
if (deplacementY == direction) {
premierDeplacement = false; // (Généralement mis à jour par JeuEchecs, simplifié ici)
return true;
}
// Avancer de deux cases (premier mouvement uniquement)
if (premierDeplacement && (deplacementY == 2 * direction)) {
// Note : En toute rigueur, il faut aussi vérifier que la case intermédiaire est vide
return true;
}
return false;
}
// Remarque : Le pion aurait également besoin d'une surcharge de peutCapturer()
// car il capture en diagonale contrairement à son déplacement normal.
};
Question 2.9 - Classe Dame par réutilisation
Pour réutiliser le code de déplacement de la Tour (lignes droites) et du Fou (diagonales), la solution classique en C++ est l'héritage multiple. La classe Dame va hériter publiquement de Tour et de Fou.
Attention : Puisque Tour et Fou héritent toutes deux de Piece, l'utilisation de l'héritage multiple nécessite d'utiliser l'héritage virtuel (virtual public Piece) pour la classe Tour et la classe Fou afin d'éviter le problème du diamant (dédoublement des attributs de base de Piece).
Code de la méthode peutSeDeplacer de la Dame :
// (Hypothèse: class Dame : public Tour, public Fou { ... })
bool Dame::peutSeDeplacer(Case* cible) {
// La dame combine les deux déplacements :
// elle peut se déplacer si la logique de la Tour l'autorise OU si la logique du Fou l'autorise.
return Tour::peutSeDeplacer(cible) || Fou::peutSeDeplacer(cible);
}
Méthode
Face à une épreuve de conception UML couplée à l'implémentation (souvent en C++ ou Java) :
- Identifiez le domaine métier : Ici, le jeu d'échecs dicte ses règles immuables (la direction des pions, le nombre de cases, etc.). Si le diagramme est incomplet, servez-vous de ces règles pour déduire les cardinalités.
- Respectez le couplage faible : C'est le principe central de la Q1.1 sur les liens unidirectionnels et de la Q2.3 sur l'emplacement des coordonnées. Posez-vous toujours la question : "Cet objet a-t-il vraiment besoin de connaître l'existence de celui-là pour remplir sa fonction ?"
- Exploitez le polymorphisme : La méthode virtuelle pure
peutSeDeplacerde la classePieceest le cœur de ce patron de conception. Toute la logique spécifique (comment bouge un Cavalier, comment la Dame utilise l'héritage multiple) découle de la bonne définition de ce contrat de base. - Justifiez vos structures : Lors du passage de l'UML au code, justifiez vos collections (tableaux statiques vs listes dynamiques) en fonction des contraintes du problème (nombre d'éléments fixes, nécessité d'ordonner, opérations d'ajout/retrait).
Commentaires
Aucun commentaire pour le moment. Posez la première question.