Activité pratique
Durée3h45Présentation & objectifs
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.
Avant le TP
Pré-requis
Pour pouvoir commencer à travailler sur l’activité, vous devez :
- Apportez votre propre ordinateur.
- Assurez-vous d’avoir Python 3 installé.
- Assurez-vous d’avoir une connexion réseau fonctionnelle.
- Avoir revu la conception POO de Citadelles réalisée en Algo 1 (session1/practical2).
Documents à consulter
Pour vous rafraîchir la mémoire sur la conception déjà réalisée :
Pendant le TP
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.
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.
0.1 - Rappel des classes incontournables
En Algo 1, vous avez identifié les classes principales pour modéliser Citadelles. 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
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
0.2 - Schéma conceptuel de base
Voici un schéma simplifié des relations entre les principales classes (à compléter et affiner) :
0.3 - 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 ?
0.4 - Mise en place de l’environnement
-
Création du projet :
- Créez un répertoire
citadelles/pour votre projet - Dans ce répertoire, créez un sous-répertoire
tests/pour les tests unitaires
- Créez un répertoire
-
Création du fichier
__init__.py(optionnel mais recommandé) : Pour faire decitadelles/un package Python, créez un fichier vide__init__.pyà la racine. -
Initialisation Git :
É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 :
Pour créer une classe abstraite, utilisez le module abc :
1.0 - 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, depuis la racine du projet, lancez la commande suivante dans le terminal:
1.1 - 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 :
1.2 - 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 despuis 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 quartiers
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.
2.1 - 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 stocker 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.
2.2 - 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 ne déclare aucun paramètre mais devra initialiser une liste (de cartes) vide comme attribut (protegé) de la classe.
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, etc.).
2.3 - 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 construitsget_points(self): retourne le total des points
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) - Méthodes :
recevoir_or(self, montant : int): ajoute de l’orpayer(self, montant : int) -> bool: retire de l’or (retourneTruesi succès)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)
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 :