18 KiB
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 :
pvn'est pas un paramètre du constructeur : un Pokémon qui vient d'être créé est en pleine forme, donc on initialiseself.pvavecpv_max.__repr__doit renvoyer une chaîne (return), pas l'afficher avecprint. C'est Python qui l'affiche quand on écritprint(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_korenvoie directement le résultat de la comparaison : pas besoin d'écrireif self.pv <= 0: return True else: return False.- Dans
attaquer, on ne modifie pascible.pvdirectement : on demande à la cible de subir les dégâts aveccible.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)etself.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 erreurKeyErrorpour un type absent du dictionnaire (par exemple"Roche"). - On ne modifie pas
self.attaque: on travaille sur une variable localedegats, 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 :
equipen'est pas un paramètre du constructeur : chaque dresseur commence avec sa propre liste vide, créée dans__init__.- L'attribut
equipeest une liste d'objetsPokemon. Le dresseur peut donc appeler leurs méthodes :pokemon.est_ko(). tous_koréutilisepokemon_disponibles: si la liste des Pokémon disponibles est vide, tous sont KO. Une boucle qui renvoieFalsedès qu'un Pokémon est debout fonctionne aussi.- Dans
__repr__,{pokemon}appelle automatiquement le__repr__de la classePokemon.
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_etatpeut appelerpremier_pokemon_dispo()sans vérifierNone: 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 duwhile).- 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
Combatne manipule jamais les PV elle-même : elle orchestre des objetsDresseur, qui contiennent des objetsPokemon, 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 dePokemon: 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 écritsalameche.attaquer(...), Python cherche d'abord la méthode dansPokemonFeu, et ne remonte àPokemonque s'il ne la trouve pas. - Les trois méthodes
attaquerse 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'
attaquerest 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
Ce cours est mis à disposition selon les termes de la Licence Creative Commons Attribution - Partage dans les Mêmes Conditions 4.0 International.