POO : corrigé commenté du TP Pokémon

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