Files
TermNSI/POO/TP/Corrige_TP_Pokemon.md
T

18 KiB
Raw Blame History

Corrigé du TP : Pokémon Arena


Partie 1 : La classe Pokémon

Exercice 1 : Créer la classe de base

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.

    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.

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

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

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

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

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 :

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

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.