From 691fb60400f0c30ef912145bf7d5c42235d65f78 Mon Sep 17 00:00:00 2001 From: Florian Mathieu Date: Mon, 12 Oct 2026 13:13:35 +0200 Subject: [PATCH] =?UTF-8?q?POO=20:=20corrig=C3=A9=20comment=C3=A9=20du=20T?= =?UTF-8?q?P=20Pok=C3=A9mon?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- POO/TP/Corrige_TP_Pokemon.md | 516 +++++++++++++++++++++++++++++++++++ 1 file changed, 516 insertions(+) create mode 100644 POO/TP/Corrige_TP_Pokemon.md diff --git a/POO/TP/Corrige_TP_Pokemon.md b/POO/TP/Corrige_TP_Pokemon.md new file mode 100644 index 0000000..35285bc --- /dev/null +++ b/POO/TP/Corrige_TP_Pokemon.md @@ -0,0 +1,516 @@ +# Corrigé du TP : Pokémon Arena + +--- + +## Partie 1 : La classe Pokémon + +### Exercice 1 : Créer la classe de base + +```python +class Pokemon: + def __init__(self, nom, type_pokemon, niveau=1, pv_max=100, attaque=10): + """ + Constructeur de la classe Pokemon. + + :param nom: (str) Nom du Pokémon + :param type_pokemon: (str) Type élémentaire + :param niveau: (int) Niveau du Pokémon + :param pv_max: (int) Points de vie maximum + :param attaque: (int) Puissance d'attaque + """ + self.nom = nom + self.type_pokemon = type_pokemon + self.niveau = niveau + self.pv_max = pv_max + self.pv = pv_max + self.attaque = attaque + + def __repr__(self): + """Représentation textuelle du Pokémon.""" + return f"{self.nom} ({self.type_pokemon}) - Nv.{self.niveau} - {self.pv}/{self.pv_max} PV" +``` + +**Explications :** + +- `pv` n'est pas un paramètre du constructeur : un Pokémon qui vient d'être créé est en pleine forme, donc on initialise `self.pv` avec `pv_max`. +- `__repr__` doit **renvoyer** une chaîne (`return`), pas l'afficher avec `print`. C'est Python qui l'affiche quand on écrit `print(pikachu)`. + +--- + +### Exercice 2 : Méthodes de combat + +Ces méthodes s'ajoutent dans la classe `Pokemon`. + +```python + def est_ko(self): + """ + Vérifie si le Pokémon est KO. + + :return: (bool) True si PV <= 0, False sinon + """ + return self.pv <= 0 + + def subir_degats(self, degats): + """ + Inflige des dégâts au Pokémon. + Les PV ne peuvent pas descendre en dessous de 0. + + :param degats: (int) Nombre de dégâts subis + """ + self.pv = self.pv - degats + if self.pv < 0: + self.pv = 0 + + def attaquer(self, cible): + """ + Attaque un autre Pokémon. + Affiche un message et inflige des dégâts à la cible. + + :param cible: (Pokemon) Pokémon ciblé par l'attaque + """ + print(f"{self.nom} attaque {cible.nom} et inflige {self.attaque} dégâts !") + cible.subir_degats(self.attaque) + + def soigner(self, soin): + """ + Soigne le Pokémon. + Les PV ne peuvent pas dépasser pv_max. + + :param soin: (int) Nombre de PV récupérés + """ + self.pv = self.pv + soin + if self.pv > self.pv_max: + self.pv = self.pv_max +``` + +**Points importants :** + +- `est_ko` renvoie directement le résultat de la comparaison : pas besoin d'écrire `if self.pv <= 0: return True else: return False`. +- Dans `attaquer`, on ne modifie pas `cible.pv` directement : on **demande** à la cible de subir les dégâts avec `cible.subir_degats(...)`. C'est la cible qui sait comment gérer ses PV (et notamment le plancher à 0). C'est le principe des objets qui communiquent par leurs méthodes. +- On peut aussi écrire `self.pv = max(0, self.pv - degats)` et `self.pv = min(self.pv_max, self.pv + soin)`, en une ligne. + +--- + +### Exercice 3 : Système de faiblesses (Bonus) + +Le dictionnaire est placé **en dehors** de la classe, en début de programme. + +```python +FAIBLESSES = { + "Eau": ["Feu"], + "Feu": ["Plante"], + "Plante": ["Eau"], + "Electrik": ["Eau"] +} +``` + +La nouvelle méthode `attaquer` remplace celle de l'exercice 2 : + +```python + def attaquer(self, cible): + """ + Attaque un autre Pokémon avec prise en compte des faiblesses. + + :param cible: (Pokemon) Pokémon ciblé par l'attaque + """ + degats = self.attaque + + # Vérifier si le type de la cible est faible contre notre type + if self.type_pokemon in FAIBLESSES: + if cible.type_pokemon in FAIBLESSES[self.type_pokemon]: + degats = degats * 2 + print("C'est super efficace !") + + print(f"{self.nom} attaque {cible.nom} et inflige {degats} dégâts !") + cible.subir_degats(degats) +``` + +**Explications :** + +- Le premier test `self.type_pokemon in FAIBLESSES` évite une erreur `KeyError` pour un type absent du dictionnaire (par exemple `"Roche"`). +- On ne modifie pas `self.attaque` : on travaille sur une variable locale `degats`, sinon l'attaque du Pokémon doublerait définitivement à chaque coup super efficace. + +```python +>>> tortank = Pokemon("Tortank", "Eau", 45, 140, 22) +>>> dracaufeu = Pokemon("Dracaufeu", "Feu", 50, 150, 25) +>>> tortank.attaquer(dracaufeu) +C'est super efficace ! +Tortank attaque Dracaufeu et inflige 44 dégâts ! +``` + +--- + +## Partie 2 : La classe Dresseur + +### Exercice 4 : Créer la classe Dresseur + +```python +class Dresseur: + def __init__(self, nom): + """ + Constructeur de la classe Dresseur. + + :param nom: (str) Nom du dresseur + """ + self.nom = nom + self.equipe = [] + + def ajouter_pokemon(self, pokemon): + """ + Ajoute un Pokémon à l'équipe si possible (max 6). + + :param pokemon: (Pokemon) Pokémon à ajouter + :return: (bool) True si ajouté, False sinon + """ + if len(self.equipe) < 6: + self.equipe.append(pokemon) + return True + return False + + def pokemon_disponibles(self): + """ + Renvoie la liste des Pokémon non KO. + + :return: (list) Liste des Pokémon encore en état de combattre + """ + disponibles = [] + for pokemon in self.equipe: + if not pokemon.est_ko(): + disponibles.append(pokemon) + return disponibles + + def premier_pokemon_dispo(self): + """ + Renvoie le premier Pokémon non KO. + + :return: (Pokemon/None) Premier Pokémon dispo ou None + """ + for pokemon in self.equipe: + if not pokemon.est_ko(): + return pokemon + return None + + def tous_ko(self): + """ + Vérifie si tous les Pokémon sont KO. + + :return: (bool) True si tous KO, False sinon + """ + return len(self.pokemon_disponibles()) == 0 + + def __repr__(self): + """Représentation du dresseur.""" + texte = f"Dresseur {self.nom} - {len(self.equipe)} Pokémon" + for pokemon in self.equipe: + texte = texte + f"\n - {pokemon}" + return texte +``` + +**Points importants :** + +- `equipe` n'est **pas** un paramètre du constructeur : chaque dresseur commence avec sa propre liste vide, créée dans `__init__`. +- L'attribut `equipe` est une liste d'**objets** `Pokemon`. Le dresseur peut donc appeler leurs méthodes : `pokemon.est_ko()`. +- `tous_ko` réutilise `pokemon_disponibles` : si la liste des Pokémon disponibles est vide, tous sont KO. Une boucle qui renvoie `False` dès qu'un Pokémon est debout fonctionne aussi. +- Dans `__repr__`, `{pokemon}` appelle automatiquement le `__repr__` de la classe `Pokemon`. + +--- + +## Partie 3 : La classe Combat + +### Exercice 5 : Simulateur de combat + +```python +class Combat: + def __init__(self, dresseur1, dresseur2): + """ + Constructeur de la classe Combat. + + :param dresseur1: (Dresseur) Premier dresseur + :param dresseur2: (Dresseur) Second dresseur + """ + self.dresseur1 = dresseur1 + self.dresseur2 = dresseur2 + self.tour = 0 + + def afficher_etat(self): + """Affiche l'état actuel du combat.""" + print(f"\n{'='*40}") + print(f"Tour {self.tour}") + print(f"{'='*40}") + poke1 = self.dresseur1.premier_pokemon_dispo() + poke2 = self.dresseur2.premier_pokemon_dispo() + print(f"{self.dresseur1.nom}: {poke1.nom} ({poke1.pv}/{poke1.pv_max} PV)") + print(f"{self.dresseur2.nom}: {poke2.nom} ({poke2.pv}/{poke2.pv_max} PV)") + print() + + def lancer_combat(self): + """ + Lance le combat tour par tour. + Chaque tour, les deux Pokémon actifs s'attaquent. + Quand un Pokémon est KO, le suivant prend sa place. + Le combat s'arrête quand un dresseur n'a plus de Pokémon. + """ + print(f"\n*** COMBAT : {self.dresseur1.nom} VS {self.dresseur2.nom} ***\n") + + while not self.dresseur1.tous_ko() and not self.dresseur2.tous_ko(): + self.tour += 1 + self.afficher_etat() + + # Récupérer les Pokémon actifs + poke1 = self.dresseur1.premier_pokemon_dispo() + poke2 = self.dresseur2.premier_pokemon_dispo() + + # Le Pokémon 1 attaque + poke1.attaquer(poke2) + + # Vérifier si Pokémon 2 est KO + if poke2.est_ko(): + print(f"\n{poke2.nom} est KO !") + suivant = self.dresseur2.premier_pokemon_dispo() + if suivant is not None: + print(f"{self.dresseur2.nom} envoie {suivant.nom} !") + else: + # Le Pokémon 2 attaque (s'il n'est pas KO) + poke2.attaquer(poke1) + + # Vérifier si Pokémon 1 est KO + if poke1.est_ko(): + print(f"\n{poke1.nom} est KO !") + suivant = self.dresseur1.premier_pokemon_dispo() + if suivant is not None: + print(f"{self.dresseur1.nom} envoie {suivant.nom} !") + + # Afficher le vainqueur + print(f"\n{'='*40}") + if self.dresseur1.tous_ko(): + print(f"Tous les Pokémon de {self.dresseur1.nom} sont KO !") + print(f"*** {self.dresseur2.nom.upper()} REMPORTE LE COMBAT ! ***") + else: + print(f"Tous les Pokémon de {self.dresseur2.nom} sont KO !") + print(f"*** {self.dresseur1.nom.upper()} REMPORTE LE COMBAT ! ***") +``` + +**Points importants :** + +- Le piège principal : un Pokémon KO **ne doit pas riposter**. C'est le rôle du `else` : le Pokémon 2 n'attaque que s'il est encore debout. +- `afficher_etat` peut appeler `premier_pokemon_dispo()` sans vérifier `None` : elle n'est appelée qu'au début d'un tour, donc quand les deux dresseurs ont encore un Pokémon disponible (c'est la condition du `while`). +- Après un KO, on n'a rien à faire pour « changer » de Pokémon : au tour suivant, `premier_pokemon_dispo()` renverra automatiquement le suivant de l'équipe. +- La classe `Combat` ne manipule jamais les PV elle-même : elle orchestre des objets `Dresseur`, qui contiennent des objets `Pokemon`, qui s'attaquent entre eux. + +--- + +## Partie 4 : Améliorations + +### Exercice 6 : Centre Pokémon + +```python +class CentrePokemon: + def __init__(self, ville): + """ + :param ville: (str) Ville où se trouve le centre + """ + self.ville = ville + + def soigner_equipe(self, dresseur): + """ + Soigne tous les Pokémon d'un dresseur à 100% de leurs PV. + + :param dresseur: (Dresseur) Dresseur dont l'équipe doit être soignée + """ + print(f"Bienvenue au Centre Pokémon de {self.ville}, {dresseur.nom} !") + for pokemon in dresseur.equipe: + pokemon.soigner(pokemon.pv_max) + print("Vos Pokémon sont en pleine forme !") +``` + +**Explication :** on réutilise la méthode `soigner` de l'exercice 2. Soigner de `pv_max` PV ramène forcément le Pokémon à `pv_max`, puisque `soigner` plafonne les PV. On aurait aussi pu écrire `pokemon.pv = pokemon.pv_max`, mais passer par la méthode est plus propre : c'est le Pokémon qui gère ses PV. + +--- + +### Exercice 7 : Capacités spéciales (Pour aller plus loin) + +On complète la classe `Capacite` avec une méthode qui consomme un PP : + +```python +class Capacite: + def __init__(self, nom, puissance, type_capacite, pp_max): + """ + :param nom: (str) Nom de la capacité + :param puissance: (int) Puissance de base + :param type_capacite: (str) Type de la capacité + :param pp_max: (int) Nombre d'utilisations max + """ + self.nom = nom + self.puissance = puissance + self.type_capacite = type_capacite + self.pp_max = pp_max + self.pp = pp_max + + def utiliser(self): + """ + Consomme un PP si possible. + + :return: (bool) True si la capacité a pu être utilisée, False sinon + """ + if self.pp > 0: + self.pp -= 1 + return True + return False + + def __repr__(self): + return f"{self.nom} ({self.type_capacite}) - {self.puissance} DMG - {self.pp}/{self.pp_max} PP" +``` + +Côté `Pokemon`, on ajoute la ligne `self.capacites = []` à la fin de `__init__`, puis deux méthodes : + +```python + def apprendre(self, capacite): + """ + Ajoute une capacité au Pokémon (4 au maximum). + + :param capacite: (Capacite) Capacité à apprendre + :return: (bool) True si apprise, False sinon + """ + if len(self.capacites) < 4: + self.capacites.append(capacite) + return True + return False + + def attaquer_avec(self, numero, cible): + """ + Attaque la cible avec la capacité numéro numero. + + :param numero: (int) Indice de la capacité dans self.capacites + :param cible: (Pokemon) Pokémon ciblé par l'attaque + """ + capacite = self.capacites[numero] + if capacite.utiliser(): + degats = self.attaque + capacite.puissance + print(f"{self.nom} utilise {capacite.nom} et inflige {degats} dégâts !") + cible.subir_degats(degats) + else: + print(f"{capacite.nom} n'a plus de PP !") +``` + +```python +>>> pikachu = Pokemon("Pikachu", "Electrik", 25, 80, 15) +>>> onix = Pokemon("Onix", "Roche", 30, 120, 18) +>>> pikachu.apprendre(Capacite("Tonnerre", 30, "Electrik", 2)) +True +>>> pikachu.attaquer_avec(0, onix) +Pikachu utilise Tonnerre et inflige 45 dégâts ! +>>> pikachu.capacites +[Tonnerre (Electrik) - 30 DMG - 1/2 PP] +``` + +**Explication :** c'est une **composition** d'objets : un `Pokemon` possède une liste d'objets `Capacite`, comme un `Dresseur` possède une liste d'objets `Pokemon`. La formule de dégâts (`attaque + puissance`) est un choix parmi d'autres : toute règle cohérente et justifiée est acceptée. + +--- + +## Partie 5 : Héritage (hors programme) + +### Exercice 8 : Sous-classes par type + +```python +class PokemonFeu(Pokemon): + def __init__(self, nom, niveau=1, pv_max=100, attaque=10): + super().__init__(nom, "Feu", niveau, pv_max, attaque) + + def attaquer(self, cible): + """ + Attaque avec ×2 dégâts si la cible est de type Plante. + """ + degats = self.attaque + if cible.type_pokemon == "Plante": + degats = degats * 2 + print("C'est super efficace !") + print(f"{self.nom} attaque {cible.nom} et inflige {degats} dégâts !") + cible.subir_degats(degats) + + +class PokemonEau(Pokemon): + def __init__(self, nom, niveau=1, pv_max=100, attaque=10): + super().__init__(nom, "Eau", niveau, pv_max, attaque) + + def attaquer(self, cible): + """ + Attaque avec ×2 dégâts si la cible est de type Feu. + """ + degats = self.attaque + if cible.type_pokemon == "Feu": + degats = degats * 2 + print("C'est super efficace !") + print(f"{self.nom} attaque {cible.nom} et inflige {degats} dégâts !") + cible.subir_degats(degats) + + +class PokemonPlante(Pokemon): + def __init__(self, nom, niveau=1, pv_max=100, attaque=10): + super().__init__(nom, "Plante", niveau, pv_max, attaque) + + def attaquer(self, cible): + """ + Attaque avec ×2 dégâts si la cible est de type Eau. + """ + degats = self.attaque + if cible.type_pokemon == "Eau": + degats = degats * 2 + print("C'est super efficace !") + print(f"{self.nom} attaque {cible.nom} et inflige {degats} dégâts !") + cible.subir_degats(degats) +``` + +**Explications :** + +- `super().__init__(...)` appelle le constructeur de `Pokemon` : les sous-classes n'ont pas à réécrire l'initialisation des attributs, elles fixent seulement le type. +- Chaque sous-classe **redéfinit** `attaquer`. Quand on écrit `salameche.attaquer(...)`, Python cherche d'abord la méthode dans `PokemonFeu`, et ne remonte à `Pokemon` que s'il ne la trouve pas. +- Les trois méthodes `attaquer` se ressemblent beaucoup : c'est justement la question de l'exercice 10. + +--- + +### Exercice 9 : isinstance() en pratique + +```python +>>> salameche = PokemonFeu("Salamèche", niveau=15, pv_max=70, attaque=12) +>>> isinstance(salameche, PokemonFeu) +True +>>> isinstance(salameche, Pokemon) +True +``` + +`salameche` est une instance des deux classes, car `PokemonFeu` **hérite** de `Pokemon` : un Pokémon Feu **est un** Pokémon. Il possède tous ses attributs et toutes ses méthodes (`est_ko`, `soigner`...). C'est pour cela qu'on peut le placer dans l'équipe d'un `Dresseur` sans rien changer à cette classe. + +```python +def compter_par_type(equipe): + """ + Compte les Pokémon de chaque type. + + :param equipe: (list) Liste de Pokémon + :return: (dict) Dictionnaire {type: nombre} + """ + compteur = {} + for pokemon in equipe: + if pokemon.type_pokemon in compteur: + compteur[pokemon.type_pokemon] += 1 + else: + compteur[pokemon.type_pokemon] = 1 + return compteur +``` + +--- + +### Exercice 10 : Refactoring + +Il n'y a pas de réponse unique. Éléments de discussion : + +- **Avec les sous-classes**, chaque type porte son propre comportement : c'est lisible, et l'on peut donner à un type des particularités qui ne se résument pas à des faiblesses. Mais le code d'`attaquer` est recopié trois fois, et ajouter un type oblige à écrire une nouvelle classe. +- **Avec le dictionnaire `FAIBLESSES`**, toutes les règles sont réunies au même endroit et ajouter une faiblesse se fait en une ligne, sans toucher au code. Il gère aussi naturellement un type faible contre plusieurs autres. +- Pour des **données** (qui bat qui), le dictionnaire est plus adapté. Les sous-classes sont intéressantes quand les types ont des **comportements** vraiment différents. On peut combiner les deux : des sous-classes qui consultent le dictionnaire. + +--- + +Auteurs : Florian Mathieu, Enzo Frémeaux, Timothée Decoster + +Licence CC BY-SA + +Licence Creative Commons
Ce cours est mis à disposition selon les termes de la Licence Creative Commons Attribution - Partage dans les Mêmes Conditions 4.0 International.