Ir al contenido
wannacode

RPG 2: la batalla

En resumen: Una batalla en un RPG con interfaces para «puede recibir golpes», «puede defenderse» y «puede curar»: una pelea a muerte y una pelea de 10 turnos en la que elige el jugador.

Problema

¿Contra quién luchan los héroes?

Los héroes de la primera parte ya tienen vida y habilidades. Ahora necesitan enemigos: los monstruos que surgen de las profundidades de Arcanum. El Chamán Caído es débil, el Rey Esqueleto está cubierto de una armadura de huesos, y el Carnicero se hace más fuerte con cada golpe.

Un monstruo no es un héroe. No tiene habilidad, y además los héroes luchan contra los monstruos. Así que no puede heredar de Character.

Pero el golpe tiene que funcionar en los dos sentidos. Y algunos de los combatientes se defienden: el rey bloquea parte del daño, y el bárbaro también. Si el ataque comprueba quién es quién con un if para cada clase, cada monstruo nuevo necesitará un if nuevo.

La idea

Los monstruos

Los monstruos tienen su propio padre abstracto. Se parece a Character: un nombre, vida, fuerza, una raza. Su ataque golpea al héroe:

abstract class Monster implements IAttackable {
protected String name;
protected int health;
protected int power;
protected String race;
public Monster(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;
}
@Override
public void takeDamage(int amount) {
health = Math.max(health - amount, 0);
}
public boolean isAlive() {
return health > 0;
}
public void attack(Character target) {
target.defend(power);
}
}

El Chamán Caído es el monstruo más sencillo: solo nombres y números. El Carnicero sobrescribe el ataque: después de cada golpe su fuerza sube 1.

class FallenShaman extends Monster {
public FallenShaman() {
super("Chamán Caído", 60, 12, "demonio");
}
}
class Butcher extends Monster {
public Butcher() {
super("Carnicero", 130, 10, "demonio");
}
@Override
public void attack(Character target) {
super.attack(target);
power += 1;
}
}

La idea

Tres contratos

Lo que comparten héroes y monstruos no es un padre sino capacidades. Cada una es una interfaz:

interface IAttackable {
void takeDamage(int amount);
}
interface IDefendable {
void defend(int attackPower);
}
interface IHealable {
void heal(Character target, int amount);
}
  • IAttackable: puede recibir golpes. Tanto Character como Monster la implementan.
  • IDefendable: puede defenderse. La defensa decide cuánto del golpe pasa. Todo héroe la implementa a través de Character, y entre los monstruos solo lo hace el Rey Esqueleto:
class SkeletonKing extends Monster implements IDefendable {
public SkeletonKing() {
super("Rey Esqueleto", 90, 16, "muerto viviente");
}
@Override
public void defend(int attackPower) {
takeDamage(Math.max(attackPower - 5, 0));
}
}
  • IHealable: puede curar, también a sí mismo. Solo algunos héroes la implementan. El paladín cura con ella, y el bárbaro no la tiene, pero sobrescribe la defensa: recibe una quinta parte menos de cada golpe.
class Paladin extends Character implements IHealable {
public Paladin(String name) {
super(name, 120, 14, "humano");
}
@Override
public void heal(Character target, int amount) {
target.restore(amount);
}
@Override
public String useAbility() {
heal(this, 20);
return name + " se cura: +20 de vida";
}
}
class Barbarian extends Character {
public Barbarian(String name) {
super(name, 110, 20, "orco");
}
@Override
public void defend(int attackPower) {
takeDamage(attackPower - attackPower / 5);
}
@Override
public String useAbility() {
power += power / 2;
return name + " entra en furia: fuerza " + power;
}
}
Más precisamente, para curiosos
  • En C# los nombres de las interfaces empiezan con I. En Java el estilo habitual es sin la I: Attackable, Defendable, Healable. Aquí los nombres son iguales en los dos lenguajes, para que leas el mismo código.
  • En la primera parte, el bárbaro sobrescribía TakeDamage. Ahora la defensa tiene su propio nombre. TakeDamage solo resta el daño, y Defend decide cuánto daño pasa. Así también puede haber daño que esquiva la defensa: veneno, una trampa.
  • Restore está en Character, para que todo héroe pueda ser curado. Solo puede curar quien implementa IHealable.

Míralo

Un golpe, dos objetivos

Este es el Character nuevo. Además de los contratos, tiene un ataque:

abstract class Character implements IAttackable, IDefendable {
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;
}
@Override
public void takeDamage(int amount) {
health = Math.max(health - amount, 0);
}
@Override
public void defend(int attackPower) {
takeDamage(attackPower);
}
public void restore(int amount) {
health += amount;
}
public boolean isAlive() {
return health > 0;
}
public void attack(Monster target) {
if (target instanceof IDefendable defender) {
defender.defend(power);
} else {
target.takeDamage(power);
}
}
public abstract String useAbility();
}

El ataque le pregunta al objetivo: ¿puedes defenderte? target is IDefendable defender es true si el objeto implementa la interfaz. Entonces defender es el mismo objeto, pero como IDefendable, y puedes llamar a Defend sobre él. Si no, el daño se resta directamente.

Grok el bárbaro golpea al chamán y al rey. Después el chamán lo golpea a él:

Character grok = new Barbarian("Grok");
Monster shaman = new FallenShaman();
Monster king = new SkeletonKing();
grok.attack(shaman);
grok.attack(king);
System.out.println(shaman.getName() + ": " + shaman.getHealth());
System.out.println(king.getName() + ": " + king.getHealth());
shaman.attack(grok);
System.out.println(grok.getName() + ": " + grok.getHealth());
Salida
Chamán Caído: 40
Rey Esqueleto: 75
Grok: 100

El chamán pierde toda la fuerza de Grok, 20. El rey bloquea 5 y pierde 15. El chamán golpea con 12, y Grok recibe 12 − 2 = 10. El ataque no sabe nada del rey ni del bárbaro: solo conoce el contrato.

Pruébalo

¿Quién puede curar?

¿Qué imprimirá el programa? Las clases son las de las tarjetas de arriba.

Character[] party = { new Paladin("Ardan"), new Barbarian("Grok"), new Witch("Morena") };
foreach (Character hero in party)
{
if (hero is IHealable)
{
Console.WriteLine(hero.Name);
}
}
Respuesta

Míralo

Una pelea a muerte

Ardan el paladín contra el Chamán Caído. Cada ronda es un golpe del héroe y, si el monstruo sigue vivo, un golpe del monstruo. La pelea termina cuando uno de los dos cae:

Character hero = new Paladin("Ardan");
Monster monster = new FallenShaman();
int round = 1;
while (hero.isAlive() && monster.isAlive()) {
hero.attack(monster);
System.out.println(round + ". " + hero.getName() + " golpea: " + monster.getName() + " tiene " + monster.getHealth());
if (monster.isAlive()) {
monster.attack(hero);
System.out.println(" " + monster.getName() + " golpea: " + hero.getName() + " tiene " + hero.getHealth());
}
round++;
}
System.out.println(hero.isAlive() ? hero.getName() + " gana" : monster.getName() + " gana");
Salida
1. Ardan golpea: Chamán Caído tiene 46
Chamán Caído golpea: Ardan tiene 108
2. Ardan golpea: Chamán Caído tiene 32
Chamán Caído golpea: Ardan tiene 96
3. Ardan golpea: Chamán Caído tiene 18
Chamán Caído golpea: Ardan tiene 84
4. Ardan golpea: Chamán Caído tiene 4
Chamán Caído golpea: Ardan tiene 72
5. Ardan golpea: Chamán Caído tiene 0
Ardan gana

El bucle es el bucle de juego de la lección «Cómo pensar un juego»: el estado es la vida de los dos combatientes, las reglas son los ataques, y el final es la condición del while.

Character hero = new Paladin("Ardan");
Monster monster = new FallenShaman();
int round = 1;
while (hero.IsAlive() && monster.IsAlive())
{
hero.Attack(monster);
Console.WriteLine(round + ". " + hero.Name + " golpea: " + monster.Name + " tiene " + monster.Health);
if (monster.IsAlive())
{
monster.Attack(hero);
Console.WriteLine(" " + monster.Name + " golpea: " + hero.Name + " tiene " + hero.Health);
}
round++;
}
Console.WriteLine(hero.IsAlive() ? hero.Name + " gana" : monster.Name + " gana");
Paso a paso, en palabras
  1. Ardan empieza con 120 de vida, Chamán Caído con 60. Las rondas empiezan en 1.
  2. Ronda 1: Ardan golpea y quita 14. Chamán Caído baja de 60 a 46.
  3. Chamán Caído está vivo y contraataca. Quita 12: Ardan baja de 120 a 108.
  4. Ronda 2: Ardan golpea y quita 14. Chamán Caído baja de 46 a 32.
  5. Chamán Caído está vivo y contraataca. Quita 12: Ardan baja de 108 a 96.
  6. Ronda 3: Ardan golpea y quita 14. Chamán Caído baja de 32 a 18.
  7. Chamán Caído está vivo y contraataca. Quita 12: Ardan baja de 96 a 84.
  8. Ronda 4: Ardan golpea y quita 14. Chamán Caído baja de 18 a 4.
  9. Chamán Caído está vivo y contraataca. Quita 12: Ardan baja de 84 a 72.
  10. Ronda 5: Ardan golpea y Chamán Caído baja de 4 a 0. monster.IsAlive() es false, así que no contraataca.
  11. La condición del while ahora es false: uno de los dos ya no está vivo. El bucle se detiene, y Ardan gana.

Míralo

10 turnos y la elección del jugador

Contra el Carnicero los ataques solos no bastan. Por eso el jugador elige en cada turno: 1 es un ataque, 2 es la habilidad del héroe. La pelea dura como máximo 10 turnos, y si al final los dos siguen vivos, gana el que tenga más vida:

Scanner in = new Scanner(System.in);
Character hero = new Paladin("Ardan");
Monster monster = new Butcher();
for (int turn = 1; turn <= 10 && hero.isAlive() && monster.isAlive(); turn++) {
String choice = in.nextLine();
if (choice.equals("2")) {
System.out.println(turn + ". " + hero.useAbility());
} else {
hero.attack(monster);
System.out.println(turn + ". " + hero.getName() + " golpea: " + monster.getName() + " tiene " + monster.getHealth());
}
if (monster.isAlive()) {
monster.attack(hero);
System.out.println(" " + monster.getName() + " golpea: " + hero.getName() + " tiene " + hero.getHealth());
}
}
String winner = hero.getHealth() > monster.getHealth() ? hero.getName() : monster.getName();
System.out.println(winner + " gana");
Salida
1. Ardan golpea: Carnicero tiene 116
Carnicero golpea: Ardan tiene 110
2. Ardan golpea: Carnicero tiene 102
Carnicero golpea: Ardan tiene 99
3. Ardan golpea: Carnicero tiene 88
Carnicero golpea: Ardan tiene 87
4. Ardan se cura: +20 de vida
Carnicero golpea: Ardan tiene 94
5. Ardan golpea: Carnicero tiene 74
Carnicero golpea: Ardan tiene 80
6. Ardan golpea: Carnicero tiene 60
Carnicero golpea: Ardan tiene 65
7. Ardan se cura: +20 de vida
Carnicero golpea: Ardan tiene 69
8. Ardan golpea: Carnicero tiene 46
Carnicero golpea: Ardan tiene 52
9. Ardan se cura: +20 de vida
Carnicero golpea: Ardan tiene 54
10. Ardan golpea: Carnicero tiene 32
Carnicero golpea: Ardan tiene 35
Ardan gana

La entrada de este ejemplo es:

Entrada (una elección por línea)
1 1 1 2 1 1 2 1 2 1

Si el jugador elige solo ataques, diez veces 1, el paladín cae en el turno 9, y al Carnicero le quedan 4 de vida. Con tres curaciones los turnos llegan hasta el final, y el paladín gana con 35 de vida contra 32.

Más precisamente, para curiosos
  • La elección se lee en cada turno, pero si el héroe cae, el bucle se detiene y las líneas de entrada que quedan no se leen.
  • choice == "2" compara el texto. Todo lo demás, incluso una línea vacía, es un ataque: el juego no puede detenerse ante una elección desconocida. En la tercera parte verás cómo rechazar una elección incorrecta con una excepción.

Pruébalo

Encuentra el error

El bárbaro golpea al Rey Esqueleto con fuerza 20, y el rey pierde 20 en lugar de 15. ¿En qué línea del ataque está el error?

Elige la línea con el error.

Pruébalo

¿Qué falta?

¿Qué palabra usa el ataque para preguntar si el objetivo implementa la interfaz?
public void Attack(Monster target)
{
    if (target  IDefendable defender)
    {
        defender.Defend(Power);
    }
    else
    {
        target.TakeDamage(Power);
    }
}

Ejercicio

Más batallas

Resuelve estos ejercicios en tu editor. Empieza con las clases de la lección.

1. Un monstruo nuevo. Andariel, una demonia, 100 de vida y 12 de fuerza. Su golpe está envenenado: después del ataque normal, el objetivo recibe otros 4 de daño directamente con TakeDamage, esquivando la defensa. Ejecuta la pelea a muerte con Grok el bárbaro.

Salida
1. Grok golpea: Andariel tiene 80
Andariel golpea: Grok tiene 96
2. Grok golpea: Andariel tiene 60
Andariel golpea: Grok tiene 82
3. Grok golpea: Andariel tiene 40
Andariel golpea: Grok tiene 68
4. Grok golpea: Andariel tiene 20
Andariel golpea: Grok tiene 54
5. Grok golpea: Andariel tiene 0
Grok gana

Grok recibe 10 del golpe (12 menos una quinta parte) y 4 del veneno: 14 por ronda.

2. Una poción curativa. Una clase Potion con una cantidad de vida. Use(Character target) cura al objetivo y devuelve un texto. La poción se bebe una vez: después dice «La poción está vacía». En la pelea del paladín con el Carnicero, el héroe bebe una poción de 40 en lugar de golpear cuando su vida está por debajo de 30.

Salida
1. Ardan golpea: Carnicero tiene 116
Carnicero golpea: Ardan tiene 110
2. Ardan golpea: Carnicero tiene 102
Carnicero golpea: Ardan tiene 99
3. Ardan golpea: Carnicero tiene 88
Carnicero golpea: Ardan tiene 87
4. Ardan golpea: Carnicero tiene 74
Carnicero golpea: Ardan tiene 74
5. Ardan golpea: Carnicero tiene 60
Carnicero golpea: Ardan tiene 60
6. Ardan golpea: Carnicero tiene 46
Carnicero golpea: Ardan tiene 45
7. Ardan golpea: Carnicero tiene 32
Carnicero golpea: Ardan tiene 29
8. Ardan bebe una poción: +40 de vida
Carnicero golpea: Ardan tiene 52
9. Ardan golpea: Carnicero tiene 18
Carnicero golpea: Ardan tiene 34
10. Ardan golpea: Carnicero tiene 4
Carnicero golpea: Ardan tiene 15
11. Ardan golpea: Carnicero tiene 0
Ardan gana

3. Un informe de batalla. En lugar de una línea por cada golpe, imprime al final cuántas rondas duró la pelea, cuánto daño causó el héroe, cuánto recibió y cuánta vida le queda. Grok el bárbaro contra el Rey Esqueleto:

Salida
Rondas: 6
Daño causado: 90
Daño recibido: 65
Vida restante: 45

Pista: recuerda la vida del objetivo antes del golpe y réstale su vida después del golpe.

Más ejercicios

  • Bruja contra rey. Ejecuta la pelea de Morena la bruja con el Rey Esqueleto, con las elecciones desde la entrada. ¿Puede ganar en 10 turnos?
  • Sanador del grupo. El paladín cura 10 al héroe con menos vida en un array de tres, en lugar de curarse a sí mismo.
  • ¿Quién golpea primero? Con new Random(5), decide al principio de cada ronda quién golpea primero.

Test

Ponte a prueba

1. ¿Por qué Monster no hereda de Character?
2. El Rey Esqueleto implementa IDefendable y bloquea 5 de cada golpe. Un bárbaro con fuerza 20 lo golpea. ¿Cuánta vida pierde el rey?
3. La clase implementa IDefendable, pero el ataque llama a takeDamage directamente. ¿Qué pasa?
4. ¿Qué hace target is IDefendable defender?

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