Ir al contenido
wannacode

RPG 1: los héroes

En resumen: Herencia en un juego: las cinco razas de héroes extienden una clase Character común, 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();
}

Solo la habilidad es distinta. Por eso no está en Character: cada raza escribe su propio useAbility(). JavaScript no tiene clases ni métodos abstractos. Si una raza olvida su habilidad, el programa se detiene solo cuando se llama a la habilidad: TypeError: hero.useAbility is not a function.

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». JavaScript también tiene campos privados de verdad, con #, pero ni siquiera las subclases los ven, por eso los campos compartidos usan aquí _. Desde fuera se leen con get: hero.health lee la vida, y hero.health = 1000 no la cambia, porque no hay set. En un archivo sin import la asignación no hace nada, sin avisar. En un archivo con import (un módulo, como los programas con ask) se detiene con TypeError: Cannot set property health of #<Character> which has only a getter.
  • 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 takeDamage y las habilidades, así que nadie puede volver inmortal a un héroe con una línea.
  • Math.max(this._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();
Salida
Ardan, humano: vida 120, fuerza 14
Ardan, humano: vida 90, fuerza 14
Ardan se cura: +20 de vida
Ardan, humano: vida 110, fuerza 14

La variable hero no tiene tipo; apunta a un objeto de la clase Paladin. hero.useAbility() llama al método del paladín, y showInfo y takeDamage vienen de Character.

La idea

Cada raza a su manera

El bárbaro recibe una quinta parte menos de daño. Sobrescribe takeDamage 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;
}
}

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";
}
}

Las dos usan super.takeDamage: la comprobación de que la vida no baje de 0 se queda en un solo lugar.

En JavaScript / también da fracciones: 24 / 5 es 4,8. Math.trunc corta la parte decimal, como la división entera de Java. La esquiva de la amazona está en #dodging: un campo privado de verdad que es solo suyo y que ni siquiera Character puede ver. 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 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`;
}
}
const lira = new Amazon("Lira");
lira.useAbility();
lira.takeDamage(30);
lira.takeDamage(30);
console.log(lira.health);
Respuesta

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 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";
}
}

Ahora los cinco van a un array, 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();
}
Salida
Ardan se cura: +20 de vida
Ardan, humano: vida 110, fuerza 14
Lira se prepara para esquivar: el próximo golpe se reduce a la mitad
Lira, elfo: vida 85, fuerza 18
Grok entra en furia: fuerza 30
Grok, orco: vida 86, fuerza 30
Torin toma fuerza de los muertos: +3 de fuerza
Torin, enano: vida 60, fuerza 13
Morena invoca a los elementos: +15 de vida
Morena, elfo de sangre: vida 80, fuerza 16

El 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 el mismo array sin que el bucle cambie.

const 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();
}
Paso a paso, en palabras
  1. Hay un array party, y contiene 5 objetos de 5 clases distintas.
  2. hero apunta a Ardan, un objeto de la clase Paladin. Se ejecuta el useAbility() de Paladin. takeDamage es la común de Character: 140 − 30 = 110.
  3. hero apunta a Lira, un objeto de la clase Amazon. Se ejecuta el useAbility() de Amazon. Amazon sobrescribe takeDamage: del golpe de 30 recibe solo 15, y quedan 85.
  4. hero apunta a Grok, un objeto de la clase Barbarian. Se ejecuta el useAbility() de Barbarian. Barbarian sobrescribe takeDamage: del golpe de 30 recibe solo 24, y quedan 86.
  5. hero apunta a Torin, un objeto de la clase Necromancer. Se ejecuta el useAbility() de Necromancer. takeDamage es la común de Character: 90 − 30 = 60.
  6. hero apunta a Morena, un objeto de la clase Witch. Se ejecuta el useAbility() de Witch. takeDamage es la común de Character: 110 − 30 = 80.
  7. 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.

Pruébalo

¿Qué falta?

JavaScript no puede obligar a una raza a tener una habilidad. Pero el paladín tiene que tomar de Character todo lo común. ¿Qué va en el hueco?
class Paladin  Character {
  constructor(name) {
    super(name, 120, 14, "humano");
  }

  useAbility() {
    this._health += 20;
    return `${this._name} se cura: +20 de vida`;
  }
}

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.

Salida
Eli, pueblo del bosque: vida 105, fuerza 12
Eli se une al bosque: +8 de vida, +2 de fuerza
Eli, pueblo del bosque: vida 113, fuerza 14

2. La lista del grupo. Escribe una función showAll(party) que muestre los héroes con números y, al final, su vida total. Ejecútalo con el grupo de la lección.

Salida
1. Ardan, humano: vida 120, fuerza 14
2. Lira, elfo: vida 100, fuerza 18
3. Grok, orco: vida 110, fuerza 20
4. Torin, enano: vida 90, fuerza 10
5. Morena, elfo de sangre: vida 95, fuerza 16
Vida total: 515

3. ¿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.

Salida
Más fuerte: Grok con fuerza 30

Má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 un array. 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

1. ¿Por qué Character no tiene useAbility()?
2. El array party contiene objetos de cinco clases. ¿Qué takeDamage se llama dentro de él para el bárbaro?
3. La amazona se prepara para esquivar, después recibe un golpe de 40 y luego otro de 40. ¿Cuánta vida pierde?
4. Barbarian sobrescribe takeDamage. ¿Cómo llama a la versión de Character?

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·