From 21df8c5109a26d9cc3ec0b1454bf788a1c859f8c Mon Sep 17 00:00:00 2001 From: Florian Mathieu Date: Mon, 12 Oct 2026 13:26:52 +0200 Subject: [PATCH] =?UTF-8?q?POO=20:=20corrig=C3=A9=20Pok=C3=A9mon,=20pas=20?= =?UTF-8?q?=C3=A0=20pas=20et=20erreurs=20fr=C3=A9quentes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- POO/TP/Corrige_TP_Pokemon.md | 341 +++++++++++++++++++++++++++++------ 1 file changed, 288 insertions(+), 53 deletions(-) diff --git a/POO/TP/Corrige_TP_Pokemon.md b/POO/TP/Corrige_TP_Pokemon.md index 35285bc..afabca7 100644 --- a/POO/TP/Corrige_TP_Pokemon.md +++ b/POO/TP/Corrige_TP_Pokemon.md @@ -1,11 +1,87 @@ # Corrigé du TP : Pokémon Arena +Ce corrigé donne le code de chaque exercice, mais surtout **la façon d'y arriver**. Sous chaque question, vous trouverez des blocs à déplier : + +- **Par où commencer** : comment réfléchir avant d'écrire la moindre ligne ; +- **Ce qui se passe à l'exécution** : le code suivi pas à pas sur un exemple. + +Si les mots *classe*, *objet* ou *méthode* ne sont pas clairs, relisez d'abord la [fiche de révision POO](../FICHE_REVISION.md). + +--- + +## Avant de commencer : la méthode + +### Quatre mots de vocabulaire + +| Mot | Ce que c'est | Dans le TP | +|:--|:--|:--| +| **Classe** | Le moule, le plan de fabrication | `Pokemon` | +| **Objet** (ou instance) | Une chose fabriquée avec le moule | `pikachu`, `salameche` | +| **Attribut** | Une information rangée dans l'objet | `self.pv`, `self.nom` | +| **Méthode** | Une fonction rangée dans la classe, qui agit sur l'objet | `attaquer`, `soigner` | + +### Que veut dire `self` ? + +`self` désigne **l'objet sur lequel on appelle la méthode**. Quand vous écrivez `pikachu.soigner(20)`, Python exécute la méthode `soigner` en remplaçant `self` par `pikachu`. Lire `self.pv`, c'est donc lire « les PV de **ce** Pokémon-là ». + +Règle à retenir : **dans une méthode, chaque attribut s'écrit avec `self.` devant**, sans exception. + +### Par où commencer une méthode ? + +Devant une méthode vide, ne cherchez pas le code tout de suite. Répondez dans l'ordre à ces trois questions, en français, sur votre brouillon : + +1. **De quoi ai-je besoin ?** Regardez les paramètres (entre parenthèses) et les attributs de l'objet (`self.quelque_chose`). +2. **Que dois-je produire ?** Trois cas possibles, la docstring vous le dit : + - **renvoyer** une valeur (`:return:` dans la docstring), donc il faut un `return` ; + - **modifier** un attribut, donc il faut une ligne `self.attribut = ...` ; + - **afficher** un message, donc il faut un `print`. +3. **Quelles sont les étapes ?** Écrivez-les en français, une par ligne. Ensuite seulement, traduisez chaque étape en Python. + +C'est exactement ce que font les blocs « Par où commencer » de ce corrigé. + +### Les trois erreurs les plus fréquentes + +!!! warning "Oublier `self`" + Trois formes différentes, trois messages d'erreur différents : + + | Ce que vous avez écrit | Ce qu'il faut écrire | Message de Python | + |:--|:--|:--| + | `nom = nom` dans `__init__` | `self.nom = nom` | `AttributeError: 'Pokemon' object has no attribute 'nom'` (l'attribut n'a jamais été créé) | + | `return pv <= 0` | `return self.pv <= 0` | `NameError: name 'pv' is not defined` | + | `def est_ko():` | `def est_ko(self):` | `TypeError: Pokemon.est_ko() takes 0 positional arguments but 1 was given` | + + Le dernier message est déroutant : vous n'avez passé aucun argument, et Python dit en avoir reçu un. Cet argument, c'est l'objet lui-même, que Python place automatiquement dans `self`. + +!!! warning "Les virgules dans les f-strings" + Dans une f-string, **chaque paire d'accolades contient une seule valeur**. Tout ce qui est entre les accolades (espaces, parenthèses, tirets, virgules) est recopié tel quel. + + | Ce que vous avez écrit | Ce qui s'affiche | + |:--|:--| + | `f"{self.nom, self.type_pokemon}"` | `('Pikachu', 'Electrik')` : la virgule dans l'accolade crée un tuple | + | `return f"{self.nom}", f"({self.type_pokemon})"` | `TypeError: __str__ returned non-string (type tuple)` : la virgule après le `return` renvoie deux chaînes au lieu d'une | + | `"{self.nom} ({self.type_pokemon})"` | `{self.nom} ({self.type_pokemon})` : sans le `f` devant, les accolades ne sont pas remplacées | + | `f"{self.nom} ({self.type_pokemon})"` | `Pikachu (Electrik)` : correct | + + Méthode sûre : **écrivez d'abord le texte attendu**, par exemple `Pikachu (Electrik) - Nv.25 - 80/80 PV`, puis remplacez chaque valeur variable par une accolade : `{self.nom} ({self.type_pokemon}) - Nv.{self.niveau} - {self.pv}/{self.pv_max} PV`. Ajoutez le `f` et les guillemets. + +!!! warning "Afficher au lieu de renvoyer" + `print` montre une valeur à l'écran, mais le programme ne peut plus s'en servir. `return` donne la valeur au programme. Quand la docstring contient `:return:`, il faut un `return`. + --- ## Partie 1 : La classe Pokémon ### Exercice 1 : Créer la classe de base +??? tip "Par où commencer" + 1. **De quoi ai-je besoin ?** Les paramètres de `__init__` : `nom`, `type_pokemon`, `niveau`, `pv_max`, `attaque`. + 2. **Que dois-je produire ?** `__init__` ne renvoie rien : son rôle est de **ranger** chaque information dans l'objet, sous forme d'attribut. + 3. **Les étapes :** + - pour chaque paramètre, créer l'attribut correspondant : `self.nom = nom`, etc. ; + - créer l'attribut `pv`, qui n'est pas un paramètre : un Pokémon neuf a tous ses PV, donc `self.pv = pv_max`. + + Pour `__repr__` : la docstring demande une chaîne. Écrivez le texte attendu de l'énoncé, `Pikachu (Electrik) - Nv.25 - 80/80 PV`, puis remplacez chaque valeur par l'attribut correspondant entre accolades. + ```python class Pokemon: def __init__(self, nom, type_pokemon, niveau=1, pv_max=100, attaque=10): @@ -18,11 +94,11 @@ class Pokemon: :param pv_max: (int) Points de vie maximum :param attaque: (int) Puissance d'attaque """ - self.nom = nom + self.nom = nom # on range le paramètre nom dans l'objet self.type_pokemon = type_pokemon self.niveau = niveau self.pv_max = pv_max - self.pv = pv_max + self.pv = pv_max # pas de paramètre pv : on démarre avec tous ses PV self.attaque = attaque def __repr__(self): @@ -30,16 +106,52 @@ class Pokemon: return f"{self.nom} ({self.type_pokemon}) - Nv.{self.niveau} - {self.pv}/{self.pv_max} PV" ``` -**Explications :** +??? example "Ce qui se passe à l'exécution" + ```python + pikachu = Pokemon("Pikachu", "Electrik", niveau=25, pv_max=80, attaque=15) + ``` -- `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)`. + 1. Python fabrique un objet vide, et le place dans `self`. + 2. Il exécute `__init__` avec `nom = "Pikachu"`, `type_pokemon = "Electrik"`, `niveau = 25`, `pv_max = 80`, `attaque = 15`. + 3. Chaque ligne `self.... = ...` remplit l'objet. À la fin, l'objet contient : `nom` vaut `"Pikachu"`, `type_pokemon` vaut `"Electrik"`, `niveau` vaut 25, `pv_max` vaut 80, `pv` vaut 80, `attaque` vaut 15. + 4. L'objet est rangé dans la variable `pikachu`. + + Ensuite, `print(pikachu)` appelle automatiquement `__repr__` : `{self.nom}` est remplacé par `Pikachu`, `{self.pv}` par `80`, etc. On obtient `Pikachu (Electrik) - Nv.25 - 80/80 PV`. + + Remarquez que `__init__` a 6 paramètres (`self` compris) mais qu'on ne lui en donne que 5 : `self`, c'est Python qui le fournit. --- ### Exercice 2 : Méthodes de combat -Ces méthodes s'ajoutent dans la classe `Pokemon`. +Ces méthodes s'ajoutent dans la classe `Pokemon`, avec la même indentation que `__repr__`. + +??? tip "Par où commencer : `est_ko`" + 1. **De quoi ai-je besoin ?** Aucun paramètre à part `self`. L'attribut utile est `self.pv`. + 2. **Que dois-je produire ?** La docstring dit `:return: (bool)` : il faut **renvoyer** `True` ou `False`. + 3. **Les étapes :** regarder si les PV sont inférieurs ou égaux à 0, et renvoyer la réponse. + + `self.pv <= 0` est déjà une expression qui vaut `True` ou `False` : on peut la renvoyer directement. + +??? tip "Par où commencer : `subir_degats`" + 1. **De quoi ai-je besoin ?** Le paramètre `degats`, et l'attribut `self.pv`. + 2. **Que dois-je produire ?** Pas de `:return:` dans la docstring. On **modifie** un attribut : `self.pv`. + 3. **Les étapes :** + - enlever les dégâts aux PV ; + - si les PV sont passés sous 0, les remettre à 0. + +??? tip "Par où commencer : `attaquer`" + 1. **De quoi ai-je besoin ?** Le paramètre `cible`, qui est **un objet `Pokemon`** (la docstring le précise : `(Pokemon)`). On a donc accès à `cible.nom`, `cible.pv`, et à ses méthodes. On a aussi `self.attaque` et `self.nom`. + 2. **Que dois-je produire ?** Un **affichage**, et une **modification** : les PV de la cible baissent. + 3. **Les étapes :** + - afficher le message de l'énoncé : `Pikachu attaque Salamèche et inflige 15 dégâts !` ; + - faire subir les dégâts à la cible. Pas besoin de réécrire le calcul : la méthode `subir_degats` existe déjà, on l'appelle **sur la cible**. + +??? tip "Par où commencer : `soigner`" + C'est le miroir de `subir_degats` : + + - ajouter le soin aux PV ; + - si les PV dépassent `pv_max`, les ramener à `pv_max`. ```python def est_ko(self): @@ -58,7 +170,7 @@ Ces méthodes s'ajoutent dans la classe `Pokemon`. :param degats: (int) Nombre de dégâts subis """ self.pv = self.pv - degats - if self.pv < 0: + if self.pv < 0: # on ne descend pas sous 0 self.pv = 0 def attaquer(self, cible): @@ -69,7 +181,7 @@ Ces méthodes s'ajoutent dans la classe `Pokemon`. :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) + cible.subir_degats(self.attaque) # c'est la cible qui perd des PV def soigner(self, soin): """ @@ -79,20 +191,41 @@ Ces méthodes s'ajoutent dans la classe `Pokemon`. :param soin: (int) Nombre de PV récupérés """ self.pv = self.pv + soin - if self.pv > self.pv_max: + if self.pv > self.pv_max: # on ne dépasse pas le maximum self.pv = self.pv_max ``` -**Points importants :** +??? example "Ce qui se passe à l'exécution" + ```python + pikachu = Pokemon("Pikachu", "Electrik", niveau=25, pv_max=80, attaque=15) + salameche = Pokemon("Salamèche", "Feu", niveau=20, pv_max=70, attaque=12) + pikachu.attaquer(salameche) + ``` -- `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. + 1. On appelle `attaquer` **sur `pikachu`** : dans la méthode, `self` vaut `pikachu`, et `cible` vaut `salameche`. + 2. Le `print` affiche `Pikachu attaque Salamèche et inflige 15 dégâts !` : `self.nom` donne `Pikachu`, `cible.nom` donne `Salamèche`, `self.attaque` donne 15. + 3. La ligne `cible.subir_degats(self.attaque)` appelle `subir_degats` **sur `salameche`** avec `degats = 15`. Dans cette nouvelle méthode, `self` vaut maintenant `salameche`. + 4. `self.pv = self.pv - degats` : les PV de Salamèche passent de 70 à 55. 55 n'est pas négatif, le `if` ne fait rien. + 5. `salameche.est_ko()` calcule `55 <= 0`, ce qui vaut `False`. + + Le point clé : `self` ne désigne pas toujours le même Pokémon. Il désigne celui **devant le point** au moment de l'appel. + +!!! note "Une version plus courte" + `self.pv = max(0, self.pv - degats)` fait la même chose que les trois lignes de `subir_degats` : on garde le plus grand entre 0 et le résultat. De même, `self.pv = min(self.pv_max, self.pv + soin)` pour `soigner`. Les deux versions sont correctes. --- ### Exercice 3 : Système de faiblesses (Bonus) +??? tip "Par où commencer" + 1. **Comprendre le dictionnaire.** `FAIBLESSES["Eau"]` vaut `["Feu"]` : l'Eau est super efficace contre le Feu. La clé est le type de **l'attaquant**, la valeur est la liste des types **qu'il bat**. + 2. **Que change cette version ?** Les dégâts ne valent plus toujours `self.attaque` : ils peuvent doubler. On a donc besoin d'une variable `degats`. + 3. **Les étapes :** + - mettre `self.attaque` dans une variable `degats` ; + - si le type de l'attaquant est dans le dictionnaire, **et** si le type de la cible est dans sa liste, doubler `degats` et afficher `C'est super efficace !` ; + - afficher le message d'attaque avec `degats` ; + - faire subir `degats` à la cible. + Le dictionnaire est placé **en dehors** de la classe, en début de programme. ```python @@ -113,7 +246,7 @@ La nouvelle méthode `attaquer` remplace celle de l'exercice 2 : :param cible: (Pokemon) Pokémon ciblé par l'attaque """ - degats = self.attaque + degats = self.attaque # variable locale : on ne touche pas à self.attaque # Vérifier si le type de la cible est faible contre notre type if self.type_pokemon in FAIBLESSES: @@ -125,18 +258,22 @@ La nouvelle méthode `attaquer` remplace celle de l'exercice 2 : cible.subir_degats(degats) ``` -**Explications :** +??? example "Ce qui se passe à l'exécution" + ```python + tortank = Pokemon("Tortank", "Eau", 45, 140, 22) + dracaufeu = Pokemon("Dracaufeu", "Feu", 50, 150, 25) + tortank.attaquer(dracaufeu) + ``` -- 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. + 1. `self` vaut `tortank`, `cible` vaut `dracaufeu`. `degats` vaut 22. + 2. `self.type_pokemon in FAIBLESSES` : `"Eau"` est bien une clé du dictionnaire. + 3. `FAIBLESSES["Eau"]` vaut `["Feu"]`, et `cible.type_pokemon` vaut `"Feu"` : il est dans la liste. `degats` passe à 44, et Python affiche `C'est super efficace !`. + 4. Puis `Tortank attaque Dracaufeu et inflige 44 dégâts !`, et Dracaufeu passe de 150 à 106 PV. -```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 ! -``` + Si un Onix (type `"Roche"`) attaquait, le premier `if` serait faux, car `"Roche"` n'est pas une clé : les dégâts resteraient normaux. Sans ce premier test, `FAIBLESSES["Roche"]` provoquerait une erreur `KeyError`. + +!!! warning "Erreur fréquente" + Écrire `self.attaque = self.attaque * 2` au lieu de passer par `degats`. L'attaque du Pokémon serait doublée **pour toujours**, et redoublée à chaque coup super efficace. --- @@ -144,6 +281,36 @@ Tortank attaque Dracaufeu et inflige 44 dégâts ! ### Exercice 4 : Créer la classe Dresseur +??? tip "Par où commencer : `__init__`" + Deux attributs : `nom` est donné en paramètre, mais `equipe` ne l'est pas. Un dresseur commence sans Pokémon : `self.equipe = []`. + +??? tip "Par où commencer : `ajouter_pokemon`" + 1. **De quoi ai-je besoin ?** Le paramètre `pokemon`, et l'attribut `self.equipe`. + 2. **Que dois-je produire ?** **Modifier** l'équipe si c'est possible, et **renvoyer** `True` ou `False` (`:return:`). + 3. **Les étapes :** + - si l'équipe compte moins de 6 Pokémon, ajouter le Pokémon à la liste et renvoyer `True` ; + - sinon, renvoyer `False`. + +??? tip "Par où commencer : `pokemon_disponibles`, `premier_pokemon_dispo`, `tous_ko`" + Ces trois méthodes parcourent l'équipe avec une boucle `for pokemon in self.equipe:`. À chaque tour, `pokemon` est un objet `Pokemon` : on peut donc lui demander `pokemon.est_ko()`. + + - `pokemon_disponibles` : partir d'une liste vide, y ajouter chaque Pokémon qui n'est pas KO, renvoyer la liste. + - `premier_pokemon_dispo` : dès qu'on trouve un Pokémon qui n'est pas KO, le renvoyer. Si la boucle se termine sans rien trouver, renvoyer `None`. + - `tous_ko` : tous les Pokémon sont KO quand la liste des disponibles est vide. On réutilise la méthode précédente. + +??? tip "Par où commencer : `__repr__`" + Le texte attendu tient sur plusieurs lignes : + + ``` + Dresseur Sacha - 2 Pokémon + - Pikachu (Electrik) - Nv.25 - 80/80 PV + - Dracaufeu (Feu) - Nv.50 - 150/150 PV + ``` + + - Construire la première ligne dans une variable `texte`. + - Pour chaque Pokémon de l'équipe, ajouter à `texte` un retour à la ligne (`\n`), deux espaces, un tiret, et le Pokémon. + - Renvoyer `texte`. + ```python class Dresseur: def __init__(self, nom): @@ -153,7 +320,7 @@ class Dresseur: :param nom: (str) Nom du dresseur """ self.nom = nom - self.equipe = [] + self.equipe = [] # pas de paramètre : chaque dresseur démarre avec une équipe vide def ajouter_pokemon(self, pokemon): """ @@ -175,7 +342,7 @@ class Dresseur: """ disponibles = [] for pokemon in self.equipe: - if not pokemon.est_ko(): + if not pokemon.est_ko(): # on demande au Pokémon s'il est KO disponibles.append(pokemon) return disponibles @@ -187,8 +354,8 @@ class Dresseur: """ for pokemon in self.equipe: if not pokemon.est_ko(): - return pokemon - return None + return pokemon # le return arrête la boucle immédiatement + return None # on n'arrive ici que si aucun n'est disponible def tous_ko(self): """ @@ -202,16 +369,26 @@ class Dresseur: """Représentation du dresseur.""" texte = f"Dresseur {self.nom} - {len(self.equipe)} Pokémon" for pokemon in self.equipe: - texte = texte + f"\n - {pokemon}" + texte = texte + f"\n - {pokemon}" # {pokemon} utilise le __repr__ de Pokemon return texte ``` -**Points importants :** +??? example "Ce qui se passe à l'exécution" + ```python + sacha = Dresseur("Sacha") + sacha.ajouter_pokemon(Pokemon("Pikachu", "Electrik", 25, 80, 15)) + ``` -- `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`. + 1. `sacha` est créé avec `nom` qui vaut `"Sacha"` et `equipe` qui vaut `[]`. + 2. `ajouter_pokemon` : `len(self.equipe)` vaut 0, qui est inférieur à 6. Le Pokémon est ajouté, la méthode renvoie `True`. + + Si Pikachu tombe KO et qu'on appelle `sacha.premier_pokemon_dispo()` : + + 1. premier tour de boucle, `pokemon` vaut Pikachu, `pokemon.est_ko()` vaut `True`, donc `not True` vaut `False` : on passe au suivant ; + 2. deuxième tour, `pokemon` vaut Dracaufeu, qui n'est pas KO : la méthode renvoie Dracaufeu et s'arrête. + +!!! warning "Erreur fréquente" + Mettre le `return None` **dans** la boucle, au même niveau que le `if`. La méthode s'arrêterait alors dès le premier Pokémon KO, sans regarder les suivants. Le `return None` se place **après** la boucle, aligné avec le `for`. --- @@ -219,6 +396,22 @@ class Dresseur: ### Exercice 5 : Simulateur de combat +??? tip "Par où commencer" + Une bonne partie du code est déjà donnée. Avant de compléter, **jouez un tour à la main** sur votre brouillon : Pikachu (80 PV, attaque 15) contre Onix (120 PV, attaque 18). + + - Pikachu attaque Onix. Onix est-il KO ? Non : il riposte. + - Onix attaque Pikachu. Pikachu est-il KO ? Non : on passe au tour suivant. + + Traduisez ensuite chaque commentaire `# À compléter` : + + 1. **Le Pokémon 1 attaque** : une seule ligne, `poke1.attaquer(poke2)`. + 2. **Vérifier si le Pokémon 2 est KO** : `if poke2.est_ko():`. Si oui, on l'annonce, et on annonce le remplaçant s'il y en a un. + 3. **Le Pokémon 2 attaque (s'il n'est pas KO)** : c'est le `else` du test précédent. Un Pokémon KO ne riposte pas. + 4. **Vérifier si le Pokémon 1 est KO** : même chose que l'étape 2, dans l'autre sens. + 5. **Afficher le vainqueur** : après la boucle, l'un des deux dresseurs n'a plus de Pokémon. Si c'est le dresseur 1, le dresseur 2 a gagné, et inversement. + + Pour `afficher_etat`, on récupère le Pokémon actif de chaque dresseur avec `premier_pokemon_dispo()`, puis on affiche son nom et ses PV. + ```python class Combat: def __init__(self, dresseur1, dresseur2): @@ -290,12 +483,26 @@ class Combat: print(f"*** {self.dresseur1.nom.upper()} REMPORTE LE COMBAT ! ***") ``` -**Points importants :** +??? example "Ce qui se passe à l'exécution" + Sacha (Pikachu, Dracaufeu, Tortank) contre Pierre (Onix, Racaillou). -- 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. + **Tour 1.** Les Pokémon actifs sont Pikachu (80 PV) et Onix (120 PV). + + - Pikachu attaque : Onix passe à 105 PV. Il n'est pas KO, donc on entre dans le `else`. + - Onix riposte : Pikachu passe à 62 PV. Il n'est pas KO. + - Fin du tour. La condition du `while` est toujours vraie : chaque dresseur a encore des Pokémon debout. + + **Quelques tours plus tard**, Pikachu tombe à 0 PV pendant la riposte d'Onix. On affiche `Pikachu est KO !`, puis `premier_pokemon_dispo()` renvoie Dracaufeu : `Sacha envoie Dracaufeu !`. + + **Au tour suivant**, on n'a rien eu à faire pour changer de Pokémon : `premier_pokemon_dispo()` saute Pikachu, qui est KO, et renvoie Dracaufeu. + + **À la fin**, Racaillou tombe KO. `pierre.tous_ko()` vaut `True`, la condition du `while` devient fausse et la boucle s'arrête. Le `if` final affiche que Sacha remporte le combat. + +!!! warning "Erreur fréquente" + Oublier le `else` et faire riposter le Pokémon 2 même s'il vient d'être mis KO. On voit alors un Pokémon à 0 PV attaquer, ce qui est le signe de cette erreur. + +!!! note "Pourquoi `afficher_etat` ne vérifie pas `None`" + `afficher_etat` est appelée au début d'un tour, donc seulement quand la condition du `while` est vraie : chaque dresseur a forcément un Pokémon disponible. `premier_pokemon_dispo()` ne peut pas renvoyer `None` à ce moment-là. --- @@ -303,6 +510,16 @@ class Combat: ### Exercice 6 : Centre Pokémon +??? tip "Par où commencer" + 1. **De quoi ai-je besoin ?** Le paramètre `dresseur`. Son équipe est accessible par `dresseur.equipe`. + 2. **Que dois-je produire ?** **Modifier** les PV de chaque Pokémon de l'équipe, et afficher un message d'accueil. + 3. **Les étapes :** + - afficher un message de bienvenue ; + - pour chaque Pokémon de l'équipe, remettre ses PV au maximum ; + - afficher un message de fin. + + Pour remettre les PV au maximum, on peut réutiliser `soigner` : soigner de `pv_max` PV ramène forcément à `pv_max`, puisque `soigner` plafonne. + ```python class CentrePokemon: def __init__(self, ville): @@ -323,12 +540,25 @@ class CentrePokemon: 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. +??? example "Ce qui se passe à l'exécution" + Après le combat, Dracaufeu a 96 PV sur 150. `centre.soigner_equipe(sacha)` parcourt l'équipe de Sacha. Arrivé à Dracaufeu, il appelle `soigner(150)` : 96 + 150 = 246, ce qui dépasse 150, donc les PV sont ramenés à 150. + + Remarquez la chaîne d'objets : le **centre** reçoit un **dresseur**, qui contient une **équipe**, qui contient des **Pokémon**, sur lesquels on appelle `soigner`. + +On aurait aussi pu écrire `pokemon.pv = pokemon.pv_max`. Passer par la méthode `soigner` est plus propre : c'est le Pokémon qui gère ses propres PV. --- ### Exercice 7 : Capacités spéciales (Pour aller plus loin) +??? tip "Par où commencer" + L'énoncé est plus ouvert : c'est à vous de choisir les méthodes. Posez-vous la question « qu'est-ce qu'un Pokémon doit pouvoir **faire** avec ses capacités ? ». + + - **Apprendre** une capacité : l'ajouter à une liste, 4 au maximum. C'est le même principe que `ajouter_pokemon` du dresseur. + - **Attaquer avec** une capacité : choisir laquelle, vérifier qu'il reste des PP, calculer les dégâts, les infliger. + + Pour stocker les capacités, il faut un nouvel attribut dans `Pokemon` : une liste vide, créée dans `__init__`. + On complète la classe `Capacite` avec une méthode qui consomme un PP : ```python @@ -383,8 +613,8 @@ Côté `Pokemon`, on ajoute la ligne `self.capacites = []` à la fin de `__init_ :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(): + capacite = self.capacites[numero] # un objet Capacite + if capacite.utiliser(): # consomme un PP, renvoie False s'il n'en reste plus degats = self.attaque + capacite.puissance print(f"{self.nom} utilise {capacite.nom} et inflige {degats} dégâts !") cible.subir_degats(degats) @@ -392,18 +622,23 @@ Côté `Pokemon`, on ajoute la ligne `self.capacites = []` à la fin de `__init_ 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] -``` +??? example "Ce qui se passe à l'exécution" + ```python + pikachu = Pokemon("Pikachu", "Electrik", 25, 80, 15) + onix = Pokemon("Onix", "Roche", 30, 120, 18) + pikachu.apprendre(Capacite("Tonnerre", 30, "Electrik", 2)) + pikachu.attaquer_avec(0, onix) + ``` -**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. + 1. `apprendre` : Pikachu n'a aucune capacité, il apprend Tonnerre, la méthode renvoie `True`. + 2. `attaquer_avec(0, onix)` : `self.capacites[0]` est Tonnerre. + 3. `capacite.utiliser()` : il reste 2 PP, on passe à 1, la méthode renvoie `True`. + 4. Les dégâts valent 15 + 30 = 45. Python affiche `Pikachu utilise Tonnerre et inflige 45 dégâts !`, et Onix passe de 120 à 75 PV. + 5. Au troisième appel, `utiliser()` renverra `False` et Python affichera `Tonnerre n'a plus de PP !`. + + Remarquez le `if capacite.utiliser():` : la méthode **fait** quelque chose (elle consomme un PP) **et** renvoie une réponse qu'on teste directement. + +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. ---