# 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):
"""
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`.
```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: # 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.
```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 # 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`.
```python
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.
```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 ! ***")
```
??? 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.
```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 !")
```
??? 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
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] # 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
```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
Ce cours est mis à disposition selon les termes de la Licence Creative Commons Attribution - Partage dans les Mêmes Conditions 4.0 International.