Activité pratique
Durée3h45Consignes globales
Dans cette séance, vous allez implémenter en Python une version simplifiée du jeu de société Citadelles pour 2 joueurs. Cette activité vous permettra de mettre en pratique les concepts de programmation orientée objet vus en Algo 1 et d’appliquer les bonnes pratiques de développement logiciel acquises dans les séances précédentes (documentation, tests, programmation défensive).
Vous partirez de la conception POO déjà réalisée en Algo 1 (identification des classes, attributs, méthodes et relations) pour aboutir à un programme exécutable permettant de jouer une partie complète.
A la fin de la séance, vous devrez être capable de :
- Implémenter des classes Python à partir d’un modèle de conception
- Structurer un projet multi-fichiers
- Écrire et exécuter des tests unitaires pour valider votre code
- Créer une interface simple en ligne de commande pour interagir avec votre programme
- Jouer une partie à 2 rôles (en incarnant les deux joueurs)
Important
Le but de cette session est de vous aider à maîtriser des notions importantes en informatique. Un assistant de programmation intelligent tel que GitHub Copilot, que vous avez peut-être déjà installé, sera capable de vous fournir une solution à ces exercices basée uniquement sur un nom de fichier judicieusement choisi.
Dans un but d’entraînement, nous vous conseillons de désactiver ces outils.
Contenu de l’activité
Préparation de l’environnement
Avant de commencer, créez un projet uv dédié à cette séance. Dans un terminal, placez-vous dans le dossier de la séance (par exemple imt/s5/info/prog/session4), puis lancez la commande suivante :
Vous écrirez tous les fichiers de cette séance dans ce projet, en ajoutant les bibliothèques dont vous avez besoin avec uv add et en exécutant vos programmes dans l’environnement virtuel associé. Au besoin, revoyez la fiche Gestion moderne de projet Python avec uv.
Organisation de l’activité
Vous allez travailler individuellement ou en binôme pour implémenter le jeu. Le TP est découpé en 6 étapes progressives :
| Étape | Durée | Objectif | Livrable |
|---|---|---|---|
| 0 | 15 min | Rappel de la conception et mise en place | Schéma de classes, dépôt Git |
| 1 | 45 min | Implémentation des classes de base | quartier.py, personnage.py |
| 2 | 45 min | Collections de cartes | pioche.py, jeu.py, cite.py |
| 3 | 45 min | Classe Joueur | joueur.py + tests |
| 4 | 45 min | Classe Partie et intégration | partie.py, main.py |
| 5 | 30 min | Tests complets et jeu | Programme exécutable |
Conseil
Ne passez à l’étape suivante que lorsque tous les tests de l’étape précédente sont validés. Cela vous évitera des problèmes d’intégration complexes.
Étape 0 : Rappel de la conception et mise en place (15 min)
Objectif : S’assurer que tout le monde part sur de bonnes bases en rappelant la conception POO déjà réalisée en Algo 1.
Approche incrémentale
Toutes les méthodes d’une classe n’ont pas besoin d’être implémentées immédiatement.
Adoptez une approche progressive :
- Commencez par créer le squelette de vos classes avec leurs attributs et des méthodes vides (avec
pass) - Implémentez d’abord les méthodes qui ne dépendent d’aucune autre classe
- Implémentez ensuite les méthodes qui dépendent de classes déjà disponibles
- Laissez pour plus tard les méthodes qui dépendent de classes pas encore créées
Exemple : Pour la classe Joueur, vous pouvez implémenter recevoir_or() et payer() tout de suite (elles ne dépendent que de l’attribut or), mais construire_quartier() devra attendre que Jeu et Cite soient implémentées.
Cela évite les blocages et correspond à la manière dont on développe réellement du code.
Rappel des classes incontournables
En Algo 1, vous avez identifié les classes principales pour modéliser Citadelles. Dans ce TP, nous avons fait un choix de conception, afin de simplifier l’exercice. Ce choix est discutable, vous pouvez d’ailleurs en discuter avec votre encadrant(e). Pour démarrer, voici un rappel des classes incontournables :
Carte: classe de base pour tous les types de cartesQuartieretPersonnage: sous-classes deCarteJoueur(ouPlayer) : représente un·e joueur·eusePartie: orchestrate le déroulement du jeu
Création des classes
N’anticipez pas la création des différentes classes, l’ordre de création vous est donné tout au long du TP, veillez à bien le respecter.
Question de rappel : Quelles autres classes ou regroupements de cartes aviez-vous identifiés ?
Indice
Pensez aux différents regroupements de cartes mentionnés dans les règles :
- les quartiers construits par un joueur (la cité)
- les cartes en main d’un joueur (la main)
- les cartes disponibles pour être piochées (la pioche)
- les personnages non utilisés (l’écart)
- les personnages choisis par un joueur
Schéma conceptuel de base
Voici deux schémas simplifiés des relations entre les principales classes (à compléter et affiner) :
Relations d’héritages entre les cartes :
classDiagram
class Carte
class Quartier
class Personnage
Carte <|-- Quartier
Quartier <|-- Noble
Quartier <|-- Religieux
Quartier <|-- Commercial
Quartier <|-- Militaire
Quartier <|-- Prestige
Carte <|-- Personnage
Personnage <|-- Assassin
Personnage <|-- Voleur
Personnage <|-- Magicien
Personnage <|-- Roi
Personnage <|-- Eveque
Personnage <|-- Marchand
Personnage <|-- Architecte
Personnage <|-- Condottiere
En programmation orientée objet (POO), le mécanisme des classes abstraites permet de définir des comportements (méthodes) dont l’implémentation (le code dans la méthode) se fait dans les classes filles. Ainsi, on a l’assurance que les classes filles respecteront le contrat défini par la classe mère abstraite [src: Wikipedia]. C’est un intermédiaire entre l’interface et la classe.
Classe abstraite ?
classDiagram
Partie -- "2" Joueur
Joueur -- Jeu
Joueur -- Cité
Joueur -- "2" Personnage
Partie -- Personnage
Partie -- Pioche
Vérification de la compréhension
Avant de commencer à coder, assurez-vous de pouvoir répondre à ces questions :
- Quels attributs une carte
Quartierdoit-elle avoir ? - Quels attributs une carte
Personnagedoit-elle avoir ? - Comment modéliser la relation entre un
Joueuret ses cartes (main, cité, personnages) ? - Où stocker les cartes Quartier qui n’ont pas encore été piochées ?
Mise en place de l’environnement
-
Création du projet :
- Dans le projet
uvde la séance, créez un répertoirecitadelles/pour votre code - Dans ce répertoire, créez un sous-répertoire
tests/pour les tests unitaires
- Dans le projet
-
Création des fichiers
__init__.py: Pour faire decitadelles/un package Python, créez un fichier vide__init__.pyà la racine et un autre identique dans le dossiertests. -
Première version dans Git (optionnel) : le dépôt Git a déjà été créé par
uv init, il ne reste donc qu’à enregistrer une première version. Depuis la racine du projet :
Étape 1 : Implémentation des classes de base (45 min)
Objectif : Coder les classes les plus simples et indépendantes.
Attributs protegés
Les attributs des classes seront toujours considérés comme protégés, ainsi leur nom devra toujours commencer par le caractère _.
Ca ne sera cependant pas le cas des paramètres (noms utilisés dans la signature d’une méthode) ni des arguments (éléments passés lors de l’appel de la méthode).
Toutes les méthodes peuvent être implémentées
Ces classes ne dépendent d’aucune autre classe du projet. Vous pouvez implémenter toutes leurs méthodes dès maintenant.
Aide
Pour créer une énumération en Python, utilisez le module enum (les Enums en Python):
Pour créer une classe abstraite, utilisez le module abc (les classes abstraites en Python.) :
Classe Carte (classe de base)
Créez la classe Carte dans carte.py :
Carte(classe abstraite avec attribut :_nom)
Exigences :
- Chaque classe doit être documentée (docstring).
- Cette classe servira de base pour
QuartieretPersonnage.
Code pour Carte
Voici le code complet pour carte.py :
Conseil : Testez la création d’une carte et l’affichage avec __str__.
Tests pour Carte
Voici des tests à copier dans le fichier test_carte.py (dans le dossier ./tests/) :
Conseil : Testez aussi les limites (_nom absent) et les types incorrects.
Remarques: Pour lancer les tests, vous pouvez cliquer sur l’icône à gauche de l’éditeur, cliquez sur Configure Python Tests et séléctionnez consécutivement unittest, tests et test_*.py afin de générer (ou modifier) le fichier .vscode/settings.json. Ensuite, cliquez sur la double flèche en haut de la vue Tests pour exécuter tous les tests.
Sinon, depuis la racine du projet, lancez la commande suivante dans le terminal:
Hiérarchie des Quartiers
Créez les classes suivantes dans quartier.py :
Couleur(énumération pour les 5 couleurs : Noble, Religieux, Commercial, Militaire, Prestige)Quartier(classe héritant deCarteavec attributs :_nom,_couleur,_cout,_points)- Sous-classes de
Quartierpour chaque type (ex:Noble,Religieux, etc.)
Exigences :
Quartierhérite deCarte(utilisezfrom carte import Carte)- Chaque classe doit être documentée (docstring).
- Vérifiez que les arguments
coutetpointssont des entiers positifs (programmation défensive). - Écrivez des tests unitaires dans
test_quartier.py.
Conseils :
- Veillez à typer les paramètres des méthodes, pour une meilleure lisibilité et maintenabilité du code.
- Indiquez également le type retour des méthodes.
- Utilisez les exceptions de type
ValueErrorpour la programmation défensive. - Appuyez-vous sur les tests pour développer la classe, en particulier respectez l’ordre et les noms des paramètres.
- L’héritage impose, dans la méthode
__init__(self, ...), d’appelersuper().__init(...)avec les arguments attendus par la classe mère.
Exemples de tests pour Quartier
Voici des exemples de tests que votre fichier test_quartier.py pourrait contenir :
Conseil : Testez aussi les limites (cout <= 0, points <= 0) et les types incorrects.
Code de chargement des quartiers
Pour vous faire gagner du temps, voici une fonction utilitaire pour créer toutes les cartes Quartier. Ajoutez-la à la fin de votre fichier quartier.py :
Utilisation : Dans votre classe Pioche, vous pourrez faire :
Hiérarchie des Personnages
Créez les classes suivantes dans personnage.py :
Personnage(classe héritant deCarteavec attributs :_nom,_description,_ordre)- Sous-classes pour chaque rôle :
Assassin,Voleur,Magicien,Roi,Eveque,Marchand,Architecte,Condottiere
Exigences :
Personnagehérite deCarte(utilisezfrom carte import Carte)- L’attribut
_ordredétermine l’ordre de jeu (Assassin=1, Voleur=2, …, Condottiere=8). - Implémentez la méthode
__eq__dans la classePersonnagepour permettre la comparaison d’instances (ex:if p1 == p2). Deux personnages sont égaux s’ils ont le même ordre. - Documentez chaque classe.
- Écrivez des tests dans
test_personnage.py.
Conseils :
- Les conseils précédents s’appliquent ici également.
- N’hésitez pas à copier/coller depuis d’autres fichiers (en adaptant les parties dupliquées) et depuis les Règles simplifiées pour 2 joueurs.
Exemples de tests pour Personnage
Voici des exemples de tests pour votre fichier test_personnage.py :
Conseil : Testez aussi que les descriptions sont bien définies et que les noms sont corrects.
Code de chargement des personnages
Pour vous faire gagner du temps, voici une fonction utilitaire pour créer toutes les cartes Personnage. Ajoutez-la à la fin de votre fichier personnage.py :
Utilisation : Dans votre classe Pioche, vous pourrez faire :
Étape 2 : Collections de cartes (45 min)
Objectif : Implémenter les classes qui gèrent les groupes de cartes.
Toutes les méthodes peuvent être implémentées
Ces classes dépendent uniquement de Quartier et Personnage (déjà implémentées en Étape 1). Vous pouvez implémenter toutes leurs méthodes dès maintenant.
Pioche
Créez la classe Pioche dans pioche.py avec les méthodes :
melanger(self): mélange les cartes de la piochepiocher(self) -> Carte: pioche la première carte (retourneNonesi vide)ajouter_carte(self, carte:Carte): ajoute une carte à la piocheest_vide(self) -> bool: retourneTruesi la pioche est vide
De plus, le constructeur prendra en paramètre une liste de cartes qui sera stocké dans l’attribut _cartes.
Remarque : Dans une pioche, nous ne souhaitons manipuler que des objets de type de Carte. C’est pourquoi dans le constructeur, le paramètre cartes devra être typé avec cartes : list[Carte]. Cela implique d’importer correctement la classe Carte dans Pioche.
Conseil : Le package random dispose d’une méthode nommée shuffle permettant de mélanger (en place) les éléments d’une liste passée en paramètre (Documentation).
Tests : test_pioche.py
Exemples de tests pour Pioche
Voici des exemples de tests pour votre fichier test_pioche.py :
Conseil : Testez aussi que piocher() retourne bien None quand la pioche est vide.
Jeu
À quoi sert la classe Jeu ?
La classe Jeu représente le jeu de cartes en main d’un joueur, c’est-à-dire l’ensemble des cartes Quartier qu’il a piochées mais pas encore utilisées. Elle permet de stocker temporairement les cartes disponibles pour un joueur pendant son tour. Cette classe doit simplement gérer l’ajout et le retrait de cartes, sans connaître leur utilisation future.
Responsabilité unique : Jeu ne gère que la collection de cartes. La logique de construction ou de défausse est gérée par la classe Joueur.
Créez la classe Jeu dans jeu.py avec les méthodes :
ajouter_carte(self, carte: Carte): ajoute une carte Quartier au jeu (quand le joueur pioche)retirer_carte(self, carte: Carte) -> Carte: retire une carte du jeu (retourne la carte retirée)get_cartes() -> list[Carte]: retourne la liste des cartes du jeu
Le constructeur déclare un paramètre optionel nommé cartes qui, s’il est présent, intialisera l’attribut (protegé) de la classe _cartes à celle déclarée ou, s’il est absent, devra initialiser cet attribut à une liste (de cartes) vide.
Tests : test_jeu.py
Exemples de tests pour Jeu
Voici des exemples de tests pour votre fichier test_jeu.py :
Conseil : Testez aussi les cas limites (main vide, carte déjà retirée, carte retirée abasente, etc.).
Cité
Créez la classe Cite dans cite.py avec les méthodes :
construire(self, quartier: Quartier): construit un quartier dans la cité.get_quartiers(self): retourne la liste des quartiers construits.get_points(self): retourne le total des points.
A ce stade, nous considérerons que seuls les quartiers autorisés peuvent être construits par appel à la méthode construire(self, quartier: Quartier). Il n’est donc pas nécessaire de vérifier que le quartier n’est pas déjà existant dans la cité.
Conseils :
- N’oubliez pas d’initialiser les attributs.
- Pensez à ajouter, si ce n’est pas déjà fait, une méthode
get_pointsdansQuartier.
Tests : test_cite.py
Exemples de tests pour Cité
Voici des exemples de tests pour votre fichier test_cite.py :
Conseil : Testez aussi que la construction d’un quartier qui est déjà dans la cité est interdite (programmation défensive).
Étape 3 : Classe Joueur (45 min)
Objectif : Implémenter la classe centrale Joueur.
La classe Joueur dépend de Jeu et Cite (implémentées en Étape 2). Dans la version de Citadelles à deux joueurs, chaque joueur gère deux personnages.
Créez la classe Joueur dans joueur.py avec :
- Attributs :
_nom,_jeu(instance deJeu),_cite(instance deCite),_pieces(entier),_personnages(liste dePersonnageou vide). Seul le nom du joueur est passé en paramètre du constructeur, les autres attributs sont initialisés dans le constructeur. - Méthodes :
recevoir_or(self, montant : int): ajoute de l’or (vérifier que le montant est strictement positif ou lever uneValueError)payer(self, montant : int) -> bool: retire de l’or (retourneTruesi succès). Vérifier que les conditions sont réunies avant de procéder au retrait du montant.construire_quartier(self, quartier: Quartier) -> bool: construit un quartier du jeu, indique par retour si la construction a eu lieu.get_points() -> int: retourne les points totaux
Exigences :
- Vérifiez que le joueur a assez d’or avant de construire.
- Vérifiez que le quartier est dans le jeu du joueur.
- Écrivez des tests complets dans
test_joueur.py. - Vérifier bien les imports.
Remarques: Le mot or est reservé en Python (opérateur booléen), nous ne pouvons donc pas l’utiliser comme nom d’attribut ou de paramètre. C’est pourquoi nous utilisons le terme pieces.
Exemples de tests pour Joueur
Voici des exemples de tests pour votre fichier test_joueur.py :
Conseil : Testez aussi les cas limites (montant négatif, quartier déjà construit, etc.) avec la programmation défensive.
Étape 4 : Classe Partie et intégration (45 min)
Objectif : Orchestrer le déroulement du jeu en utilisant les fichiers fournis.
Fichiers à utiliser : Ci-après, vous trouverez 4 fichiers Python à utiliser à partir de cette étape :
Fichiers fournis
Téléchargez et déplacez ces 4 fichiers vers votre projet citadelles/.
Structure attendue :
Classe Partie (partie.py) :
La classe Partie fournie gère le déroulement complet d’une partie. Plusieurs méthodes sont déjà implémentées :
__init__(): Initialise la partie avec 2 joueurs, la couronne, les personnages et la pioche_distribuer_quartiers_initiaux(): Distribue 4 quartiers à chaque joueur (ordre dépend de la couronne)_distribuer_personnages(): Gère le choix des personnages par les joueurs_lancer_tour(): Orchestre un tour complet (appelle_jouer_tourpour chaque personnage)_choisir_personnage(): Permet à un joueur de choisir un personnage_jouer_tour(): Gère le tour d’un joueur avec un personnage_choisir_revenu(): Permet de choisir entre or ou quartiers_construire_quartier(): Gère la construction d’un quartier
Méthodes à compléter dans partie.py :
Vous devez implémenter les 3 méthodes marquées TODO :
-
lancer_partie(): Méthode principale qui orchestrerait :- Distribution initiale des quartiers (4 par joueur)
- Distribution initiale de l’or (2 pièces par joueur)
- Boucle des tours jusqu’à ce qu’un joueur ait 8 quartiers
- Comptage des points en fin de partie
-
_conditions_arret(): RetourneTruesi un joueur a construit 8 quartiers dans sa cité -
_comptage_points(): Calcule et affiche les points de chaque joueur en fin de partie
Conseils pour compléter :
- Utilisez les méthodes déjà implémentées (
_distribuer_quartiers_initiaux,_distribuer_personnages,_lancer_tour) - Inspirez-vous des tests dans
test_partie.pypour comprendre le comportement attendu - La condition d’arrêt :
len(joueur._cite.get_quartiers()) >= 8 - Pour le comptage des points : utilisez
joueur.get_points()
Règles appliquées dans le code fourni :
- Chaque joueur choisit 2 personnages par tour (méthode
_distribuer_personnages()) - Ordre de jeu : selon l’ordre des personnages (Assassin=1 joue en premier)
- Tour de jeu pour chaque personnage :
- Choix du revenu (2 pièces ou piocher 2 quartiers et en garder 1)
- Construction d’un quartier (optionnelle)
- Application du pouvoir spécial (à implémenter si temps)
- La couronne est attribuée aléatoirement au début et change si le Roi est choisi
Important
Le fichier partie.py fourni utilise la classe Jeu pour gérer les personnages disponibles pendant le choix. Assurez-vous que votre classe Jeu (créée en Étape 2) implémente bien :
ajouter_carte(self, carte: Carte)retirer_carte(self, carte: Carte) -> Carteget_cartes() -> list[Carte]
Le fichier ui.py nécessite d’importer Carte et Quartier. Votre projet doit donc avoir ces classes définies.
Fichier main.py fourni :
Fichier ui.py fourni : Classe utilitaire pour un affichage console agréable avec méthodes statiques pour les titres, menus, messages de succès/erreur, etc.
Tests fournis : Le fichier test_partie.py contient des tests pour valider votre implémentation. Exécutez-le avec python -m unittest test_partie.py.
Étape 5 : Intégration et test complet (30 min)
Objectif : Valider que tout fonctionne ensemble.
- Exécutez le programme :
python main.py - Jouez une partie complète à 2 rôles (vous incarnez les deux joueurs).
- Corrigez les bugs en utilisant les messages d’erreur.
- Ajoutez des améliorations (si temps) :
- Implémentez les pouvoirs des personnages (Assassin, Voleur, etc.)
- Déplacez la logique du joueur depuis la classe
Partievers la classeJoueur - Améliorez l’interface (couleurs, tableaux)
- Vérifiez que la logique du jeu est respectée (et testée), comme la validité des actions par exemple.
Après le TP
Compléter l’activité
Avant la prochaine séance, vous devez :
- Terminer les parties obligatoires : Les étapes 0 à 4 doivent être fonctionnelles.
- Pousser votre code sur un dépôt Git (local ou distant) :
- Reviser les concepts POO et les bonnes pratiques (documentation, tests).
Pour aller plus loin (optionnel)
Si vous avez terminé à temps, vous pouvez :
- Ajouter un système de sauvegarde/chargement de partie.
- Créer une interface graphique avec
tkinteroupygame.
Structure finale du projet
Astuce
Pour executer tous les tests d’un coup, vous pouvez créer un fichier run_tests.py :