RPG 1: los héroes
En resumen: Herencia en un juego: las cinco razas de héroes extienden una clase abstracta Character, cada una con su propia habilidad, y un bucle las llama a todas.
Problema
Cinco razas, un grupo
En el mundo de Arcanum, cinco razas luchan por sus tierras. Cada una tiene sus propios héroes:
| Raza | Héroe | Qué los distingue |
|---|---|---|
| humanos | paladín | daño medio, se cura |
| elfos | amazona | rápida, golpea más fuerte que los humanos, esquiva golpes |
| orcos | bárbaro | daño alto, recibe menos daño, entra en furia |
| enanos | nigromante | daño bajo, toma fuerza de los muertos |
| elfos de sangre | bruja | daño medio, se recupera con el poder de los elementos |
Quieres un grupo con los cinco y un bucle que los muestre y los mande a la batalla. Si cada raza es una clase aparte sin nada en común, tendrás cinco arrays, cinco bucles y cinco copias del mismo código para la vida y el daño. Y una sexta raza querrá una sexta copia.
La idea
Lo común va en Character
Todo héroe tiene un nombre, vida, fuerza y una raza. Todo héroe recibe daño, puede estar vivo o muerto y puede mostrar quién es. Eso es lo que tienen en común, y va en el padre:
abstract class Character { protected String name; protected int health; protected int power; protected String race;
public Character(String name, int health, int power, String race) { this.name = name; this.health = health; this.power = power; this.race = race; }
public String getName() { return name; }
public int getHealth() { return health; }
public int getPower() { return power; }
public void takeDamage(int amount) { health = Math.max(health - amount, 0); }
public boolean isAlive() { return health > 0; }
public void showInfo() { System.out.println(name + ", " + race + ": vida " + health + ", fuerza " + power); }
public abstract String useAbility();}abstract class Character{ public string Name { get; protected set; } public int Health { get; protected set; } public int Power { get; protected set; } public string Race { get; protected set; }
public Character(string name, int health, int power, string race) { Name = name; Health = health; Power = power; Race = race; }
public virtual void TakeDamage(int amount) { Health = Math.Max(Health - amount, 0); }
public bool IsAlive() { return Health > 0; }
public void ShowInfo() { Console.WriteLine(Name + ", " + Race + ": vida " + Health + ", fuerza " + Power); }
public abstract string UseAbility();}from abc import ABC, abstractmethod
class Character(ABC): def __init__(self, name, health, power, race): self._name = name self._health = health self._power = power self._race = race
@property def name(self): return self._name
@property def health(self): return self._health
@property def power(self): return self._power
def take_damage(self, amount): self._health = max(self._health - amount, 0)
def is_alive(self): return self._health > 0
def show_info(self): print(f"{self._name}, {self._race}: vida {self._health}, fuerza {self._power}")
@abstractmethod def use_ability(self): passclass Character { constructor(name, health, power, race) { this._name = name; this._health = health; this._power = power; this._race = race; }
get name() { return this._name; }
get health() { return this._health; }
get power() { return this._power; }
takeDamage(amount) { this._health = Math.max(this._health - amount, 0); }
isAlive() { return this._health > 0; }
showInfo() { console.log(`${this._name}, ${this._race}: vida ${this._health}, fuerza ${this._power}`); }}Solo la habilidad es distinta. Por eso use_ability() es abstracto: cada raza tiene que escribirlo. En Python esto se dice con el decorador @abstractmethod del módulo abc, y Character hereda de ABC. Eso convierte a Character en una clase abstracta, porque no existe un héroe que sea «solo un personaje»: Character("Ardan", 100, 10, "humano") se detiene con TypeError: Can't instantiate abstract class Character without an implementation for abstract method 'use_ability'.
La habilidad devuelve un texto sobre lo que ocurrió, en lugar de imprimirlo. Así el juego decide quién ve qué en la pantalla, no el héroe.
Más precisamente, para curiosos
- Los campos empiezan con
_. Es una convención: «solo para la clase y sus subclases». Python no la impone, pero todos los que leen el código la respetan. Desde fuera se leen con@property:hero.healthlee la vida, yhero.health = 1000se detiene conAttributeError.
- El ejercicio antiguo tenía un getter y un setter para cada campo. Aquí no hay forma de cambiar la vida desde fuera. Solo cambia mediante
take_damagey las habilidades, así que nadie puede volver inmortal a un héroe con una línea. max(self._health - amount, 0)evita que la vida baje de 0.
Míralo
El primer héroe
En su constructor, el paladín dice cuál es su raza y con cuánta vida y fuerza empieza. Todo lo demás lo toma de Character. Solo escribe su habilidad:
class Paladin extends Character { public Paladin(String name) { super(name, 120, 14, "humano"); }
@Override public String useAbility() { health += 20; return name + " se cura: +20 de vida"; }}
Character hero = new Paladin("Ardan");hero.showInfo();hero.takeDamage(30);hero.showInfo();System.out.println(hero.useAbility());hero.showInfo();Ardan, humano: vida 120, fuerza 14Ardan, humano: vida 90, fuerza 14Ardan se cura: +20 de vidaArdan, humano: vida 110, fuerza 14class Paladin : Character{ public Paladin(string name) : base(name, 120, 14, "humano") { }
public override string UseAbility() { Health += 20; return Name + " se cura: +20 de vida"; }}
Character hero = new Paladin("Ardan");hero.ShowInfo();hero.TakeDamage(30);hero.ShowInfo();Console.WriteLine(hero.UseAbility());hero.ShowInfo();Ardan, humano: vida 120, fuerza 14Ardan, humano: vida 90, fuerza 14Ardan se cura: +20 de vidaArdan, humano: vida 110, fuerza 14class Paladin(Character): def __init__(self, name): super().__init__(name, 120, 14, "humano")
def use_ability(self): self._health += 20 return f"{self._name} se cura: +20 de vida"
hero = Paladin("Ardan")hero.show_info()hero.take_damage(30)hero.show_info()print(hero.use_ability())hero.show_info()Ardan, humano: vida 120, fuerza 14Ardan, humano: vida 90, fuerza 14Ardan se cura: +20 de vidaArdan, humano: vida 110, fuerza 14class Paladin extends Character { constructor(name) { super(name, 120, 14, "humano"); }
useAbility() { this._health += 20; return `${this._name} se cura: +20 de vida`; }}
const hero = new Paladin("Ardan");hero.showInfo();hero.takeDamage(30);hero.showInfo();console.log(hero.useAbility());hero.showInfo();Ardan, humano: vida 120, fuerza 14Ardan, humano: vida 90, fuerza 14Ardan se cura: +20 de vidaArdan, humano: vida 110, fuerza 14La variable hero no tiene tipo; apunta a un objeto de la clase Paladin. hero.use_ability() llama al método del paladín, y show_info y take_damage vienen de Character.
La idea
Cada raza a su manera
El bárbaro recibe una quinta parte menos de daño. Sobrescribe take_damage y llama a la versión del padre con el número más pequeño:
class Barbarian extends Character { public Barbarian(String name) { super(name, 110, 20, "orco"); }
@Override public void takeDamage(int amount) { super.takeDamage(amount - amount / 5); }
@Override public String useAbility() { power += power / 2; return name + " entra en furia: fuerza " + power; }}class Barbarian : Character{ public Barbarian(string name) : base(name, 110, 20, "orco") { }
public override void TakeDamage(int amount) { base.TakeDamage(amount - amount / 5); }
public override string UseAbility() { Power += Power / 2; return Name + " entra en furia: fuerza " + Power; }}class Barbarian(Character): def __init__(self, name): super().__init__(name, 110, 20, "orco")
def take_damage(self, amount): super().take_damage(amount - amount // 5)
def use_ability(self): self._power += self._power // 2 return f"{self._name} entra en furia: fuerza {self._power}"class Barbarian extends Character { constructor(name) { super(name, 110, 20, "orco"); }
takeDamage(amount) { super.takeDamage(amount - Math.trunc(amount / 5)); }
useAbility() { this._power += Math.trunc(this._power / 2); return `${this._name} entra en furia: fuerza ${this._power}`; }}La amazona recuerda si está preparada para esquivar. Si lo está, el siguiente golpe se reduce a la mitad y la esquiva se gasta:
class Amazon extends Character { private boolean dodging;
public Amazon(String name) { super(name, 100, 18, "elfo"); }
@Override public void takeDamage(int amount) { if (dodging) { amount = amount / 2; dodging = false; } super.takeDamage(amount); }
@Override public String useAbility() { dodging = true; return name + " se prepara para esquivar: el próximo golpe se reduce a la mitad"; }}class Amazon : Character{ private bool dodging;
public Amazon(string name) : base(name, 100, 18, "elfo") { }
public override void TakeDamage(int amount) { if (dodging) { amount = amount / 2; dodging = false; } base.TakeDamage(amount); }
public override string UseAbility() { dodging = true; return Name + " se prepara para esquivar: el próximo golpe se reduce a la mitad"; }}class Amazon(Character): def __init__(self, name): super().__init__(name, 100, 18, "elfo") self._dodging = False
def take_damage(self, amount): if self._dodging: amount = amount // 2 self._dodging = False super().take_damage(amount)
def use_ability(self): self._dodging = True return f"{self._name} se prepara para esquivar: el próximo golpe se reduce a la mitad"class Amazon extends Character { #dodging = false;
constructor(name) { super(name, 100, 18, "elfo"); }
takeDamage(amount) { if (this.#dodging) { amount = Math.trunc(amount / 2); this.#dodging = false; } super.takeDamage(amount); }
useAbility() { this.#dodging = true; return `${this._name} se prepara para esquivar: el próximo golpe se reduce a la mitad`; }}Las dos usan super().take_damage: la comprobación de que la vida no baje de 0 se queda en un solo lugar.
amount // 5 es una división entera: 30 // 5 es 6, y 24 // 5 es 4. La esquiva de la amazona está en _dodging, un campo que es solo suyo. Un método se sobrescribe escribiéndolo otra vez en la subclase: no hace falta nada delante.
Pruébalo
¿Cuánta vida queda?
¿Qué imprimirá el programa? Character es el de la tarjeta de arriba.
class Amazon(Character): def __init__(self, name): super().__init__(name, 100, 18, "elfo") self._dodging = False
def take_damage(self, amount): if self._dodging: amount = amount // 2 self._dodging = False super().take_damage(amount)
def use_ability(self): self._dodging = True return f"{self._name} se prepara para esquivar: el próximo golpe se reduce a la mitad"
lira = Amazon("Lira")lira.use_ability()lira.take_damage(30)lira.take_damage(30)print(lira.health)Lira empieza con 100. Su habilidad la prepara para esquivar. El primer golpe es 30 / 2 = 15, y la esquiva se gasta: 85. El segundo es completo: 85 − 30 = 55.
Míralo
El grupo
El nigromante y la bruja escriben solo un constructor y una habilidad:
class Necromancer extends Character { public Necromancer(String name) { super(name, 90, 10, "enano"); }
@Override public String useAbility() { power += 3; return name + " toma fuerza de los muertos: +3 de fuerza"; }}class Necromancer : Character{ public Necromancer(string name) : base(name, 90, 10, "enano") { }
public override string UseAbility() { Power += 3; return Name + " toma fuerza de los muertos: +3 de fuerza"; }}class Necromancer(Character): def __init__(self, name): super().__init__(name, 90, 10, "enano")
def use_ability(self): self._power += 3 return f"{self._name} toma fuerza de los muertos: +3 de fuerza"class Necromancer extends Character { constructor(name) { super(name, 90, 10, "enano"); }
useAbility() { this._power += 3; return `${this._name} toma fuerza de los muertos: +3 de fuerza`; }}class Witch extends Character { public Witch(String name) { super(name, 95, 16, "elfo de sangre"); }
@Override public String useAbility() { health += 15; return name + " invoca a los elementos: +15 de vida"; }}class Witch : Character{ public Witch(string name) : base(name, 95, 16, "elfo de sangre") { }
public override string UseAbility() { Health += 15; return Name + " invoca a los elementos: +15 de vida"; }}class Witch(Character): def __init__(self, name): super().__init__(name, 95, 16, "elfo de sangre")
def use_ability(self): self._health += 15 return f"{self._name} invoca a los elementos: +15 de vida"class Witch extends Character { constructor(name) { super(name, 95, 16, "elfo de sangre"); }
useAbility() { this._health += 15; return `${this._name} invoca a los elementos: +15 de vida`; }}Ahora los cinco van a una lista, y un bucle les hace usar su habilidad y recibir un golpe de 30:
Character[] party = { new Paladin("Ardan"), new Amazon("Lira"), new Barbarian("Grok"), new Necromancer("Torin"), new Witch("Morena"),};for (Character hero : party) { System.out.println(hero.useAbility()); hero.takeDamage(30); hero.showInfo();}Ardan se cura: +20 de vidaArdan, humano: vida 110, fuerza 14Lira se prepara para esquivar: el próximo golpe se reduce a la mitadLira, elfo: vida 85, fuerza 18Grok entra en furia: fuerza 30Grok, orco: vida 86, fuerza 30Torin toma fuerza de los muertos: +3 de fuerzaTorin, enano: vida 60, fuerza 13Morena invoca a los elementos: +15 de vidaMorena, elfo de sangre: vida 80, fuerza 16Character[] party ={ new Paladin("Ardan"), new Amazon("Lira"), new Barbarian("Grok"), new Necromancer("Torin"), new Witch("Morena"),};foreach (Character hero in party){ Console.WriteLine(hero.UseAbility()); hero.TakeDamage(30); hero.ShowInfo();}Ardan se cura: +20 de vidaArdan, humano: vida 110, fuerza 14Lira se prepara para esquivar: el próximo golpe se reduce a la mitadLira, elfo: vida 85, fuerza 18Grok entra en furia: fuerza 30Grok, orco: vida 86, fuerza 30Torin toma fuerza de los muertos: +3 de fuerzaTorin, enano: vida 60, fuerza 13Morena invoca a los elementos: +15 de vidaMorena, elfo de sangre: vida 80, fuerza 16party = [ Paladin("Ardan"), Amazon("Lira"), Barbarian("Grok"), Necromancer("Torin"), Witch("Morena"),]for hero in party: print(hero.use_ability()) hero.take_damage(30) hero.show_info()Ardan se cura: +20 de vidaArdan, humano: vida 110, fuerza 14Lira se prepara para esquivar: el próximo golpe se reduce a la mitadLira, elfo: vida 85, fuerza 18Grok entra en furia: fuerza 30Grok, orco: vida 86, fuerza 30Torin toma fuerza de los muertos: +3 de fuerzaTorin, enano: vida 60, fuerza 13Morena invoca a los elementos: +15 de vidaMorena, elfo de sangre: vida 80, fuerza 16const party = [ new Paladin("Ardan"), new Amazon("Lira"), new Barbarian("Grok"), new Necromancer("Torin"), new Witch("Morena"),];for (const hero of party) { console.log(hero.useAbility()); hero.takeDamage(30); hero.showInfo();}Ardan se cura: +20 de vidaArdan, humano: vida 110, fuerza 14Lira se prepara para esquivar: el próximo golpe se reduce a la mitadLira, elfo: vida 85, fuerza 18Grok entra en furia: fuerza 30Grok, orco: vida 86, fuerza 30Torin toma fuerza de los muertos: +3 de fuerzaTorin, enano: vida 60, fuerza 13Morena invoca a los elementos: +15 de vidaMorena, elfo de sangre: vida 80, fuerza 16El bucle no sabe de qué raza es cada uno. Cada héroe decide por sí mismo qué hace su habilidad y cuánto daño recibe. La amazona pierde 15, el bárbaro 24, los demás 30. Una sexta raza entrará en la misma lista sin que el bucle cambie.
party = [ Paladin("Ardan"), Amazon("Lira"), Barbarian("Grok"), Necromancer("Torin"), Witch("Morena"),]for hero in party: print(hero.use_ability()) hero.take_damage(30) hero.show_info()Paso a paso, en palabras
- Hay una lista party, y contiene 5 objetos de 5 clases distintas.
- hero apunta a Ardan, un objeto de la clase Paladin. Se ejecuta el use_ability() de Paladin. take_damage es la común de Character: 140 − 30 = 110.
- hero apunta a Lira, un objeto de la clase Amazon. Se ejecuta el use_ability() de Amazon. Amazon sobrescribe take_damage: del golpe de 30 recibe solo 15, y quedan 85.
- hero apunta a Grok, un objeto de la clase Barbarian. Se ejecuta el use_ability() de Barbarian. Barbarian sobrescribe take_damage: del golpe de 30 recibe solo 24, y quedan 86.
- hero apunta a Torin, un objeto de la clase Necromancer. Se ejecuta el use_ability() de Necromancer. take_damage es la común de Character: 90 − 30 = 60.
- hero apunta a Morena, un objeto de la clase Witch. Se ejecuta el use_ability() de Witch. take_damage es la común de Character: 110 − 30 = 80.
- Hay un solo bucle, y no conoce las razas. Qué método se ejecuta lo decide el objeto al que apunta hero.
Pruébalo
Encuentra el error
El programa se detiene en el primer golpe contra el bárbaro. ¿En qué línea está el error?
Elige la línea con el error.
self. apunta al propio bárbaro, así que el método se llama a sí mismo, no al del padre. Es recursión sin caso base: take_damage llama a take_damage, que lo vuelve a llamar... hasta que la pila se llena y el programa se detiene con RecursionError: maximum recursion depth exceeded. Necesitas super().take_damage(amount - amount // 5).Pruébalo
¿Qué falta?
from abc import ABC, abstractmethod
class Character(ABC):
def __init__(self, name, health, power, race):
self._name = name
self._health = health
self._power = power
self._race = race
@property
def name(self):
return self._name
@property
def health(self):
return self._health
@property
def power(self):
return self._power
def take_damage(self, amount):
self._health = max(self._health - amount, 0)
def is_alive(self):
return self._health > 0
def show_info(self):
print(f"{self._name}, {self._race}: vida {self._health}, fuerza {self._power}")
@
def use_ability(self):
passabstractmethod: el método es solo una promesa, y cada subclase tiene que escribirlo. Si no, no se puede crear un objeto de la subclase, y Python se detiene con TypeError. Para que esto funcione, Character hereda de ABC.Ejercicio
Más héroes
Resuelve estos ejercicios en tu editor. Empieza con las clases de la lección.
1. Un sexto héroe. Añade un druida del pueblo del bosque: 105 de vida, 12 de fuerza. Su habilidad da +8 de vida y +2 de fuerza. No toques Character.
Eli, pueblo del bosque: vida 105, fuerza 12Eli se une al bosque: +8 de vida, +2 de fuerzaEli, pueblo del bosque: vida 113, fuerza 142. La lista del grupo. Escribe una función show_all(party) que muestre los héroes con números y, al final, su vida total. Ejecútalo con el grupo de la lección.
1. Ardan, humano: vida 120, fuerza 142. Lira, elfo: vida 100, fuerza 183. Grok, orco: vida 110, fuerza 204. Torin, enano: vida 90, fuerza 105. Morena, elfo de sangre: vida 95, fuerza 16Vida total: 5153. ¿Quién es el más fuerte? Cada miembro del grupo usa su habilidad una vez. Escribe una función strongest(party) que devuelva al héroe con más fuerza.
Más fuerte: Grok con fuerza 30Más ejercicios
- Un grupo desde la entrada. Lee cinco líneas del tipo «raza nombre», por ejemplo «paladin Ardan», y crea los héroes en una lista. Si la raza no existe, dilo y lee la línea siguiente.
- Furia con medida. Furia tras furia vuelve al bárbaro infinitamente fuerte. Haz que la habilidad suba la fuerza solo la primera vez y que después diga que ya está en furia.
- ¿Quién es el más resistente? Como el ejercicio 3, pero por vida, después de que cada uno haya recibido un golpe de 25.
Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Encapsulamiento · Herencia · Polimorfismo · Clases abstractas
- Ejercicios sobre el tema: Hagamos un juego