Files
TermNSI/POO/TP/Corrige_TP_Pokemon.md
T

34 KiB
Raw Blame History

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.


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.
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                    # 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                  # pas de paramètre pv : on démarre avec tous ses PV
        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"

??? example "Ce qui se passe à l'exécution" python pikachu = Pokemon("Pikachu", "Electrik", niveau=25, pv_max=80, attaque=15)

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, 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`.
    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:         # on ne descend pas sous 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)    # c'est la cible qui perd des PV

    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:     # on ne dépasse pas le maximum
            self.pv = self.pv_max

??? 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)

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.

FAIBLESSES = {
    "Eau": ["Feu"],
    "Feu": ["Plante"],
    "Plante": ["Eau"],
    "Electrik": ["Eau"]
}

La nouvelle méthode attaquer remplace celle de l'exercice 2 :

    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     # 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:
            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)

??? 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)

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.

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.


Partie 2 : La classe Dresseur

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`.
class Dresseur:
    def __init__(self, nom):
        """
        Constructeur de la classe Dresseur.

        :param nom: (str) Nom du dresseur
        """
        self.nom = nom
        self.equipe = []     # pas de paramètre : chaque dresseur démarre avec une équipe vide

    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():     # on demande au Pokémon s'il 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           # le return arrête la boucle immédiatement
        return None                      # on n'arrive ici que si aucun n'est disponible

    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}"     # {pokemon} utilise le __repr__ de Pokemon
        return texte

??? example "Ce qui se passe à l'exécution" python sacha = Dresseur("Sacha") sacha.ajouter_pokemon(Pokemon("Pikachu", "Electrik", 25, 80, 15))

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.


Partie 3 : La classe Combat

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.
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 ! ***")

??? example "Ce qui se passe à l'exécution" Sacha (Pikachu, Dracaufeu, Tortank) contre Pierre (Onix, Racaillou).

**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à.


Partie 4 : Améliorations

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.
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 !")

??? 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 :

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 :

    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]     # 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)
        else:
            print(f"{capacite.nom} n'a plus de 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)

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.


Partie 5 : Héritage (hors programme)

Exercice 8 : Sous-classes par type

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

>>> 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.

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.