Skip to content
wannacode

RPG 2: the battle

Problem

Who do the heroes fight?

The heroes from the first part already have health and abilities. Now they need enemies: the monsters rising from the depths of Arcanum. The Fallen Shaman is weak, the Skeleton King is covered in bone armor, and the Butcher gets stronger with every hit.

A monster is not a hero. It has no ability, and besides, heroes fight against monsters. So it cannot inherit from Character.

But the hit has to work in both directions. And some of the fighters defend themselves: the king blocks part of the damage, and so does the barbarian. If the attack checks who is who with an if for every class, every new monster will need a new if.

The idea

The monsters

Monsters have their own abstract parent. It looks like Character: a name, health, power, a race. Its attack hits the hero:

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

The Fallen Shaman is the simplest monster: only names and numbers. The Butcher overrides the attack: after every hit his power goes up by 1.

class FallenShaman extends Monster {
public FallenShaman() {
super("Fallen Shaman", 60, 12, "demon");
}
}
class Butcher extends Monster {
public Butcher() {
super("Butcher", 130, 10, "demon");
}
@Override
public void attack(Character target) {
super.attack(target);
power += 1;
}
}

The idea

Three contracts

What heroes and monsters share is not a parent but abilities. Each one is an interface:

interface IAttackable {
void takeDamage(int amount);
}
interface IDefendable {
void defend(int attackPower);
}
interface IHealable {
void heal(Character target, int amount);
}
  • IAttackable: can be hit. Both Character and Monster implement it.
  • IDefendable: can defend. The defense decides how much of the hit gets through. Every hero implements it through Character, and among the monsters only the Skeleton King does:
class SkeletonKing extends Monster implements IDefendable {
public SkeletonKing() {
super("Skeleton King", 90, 16, "undead");
}
@Override
public void defend(int attackPower) {
takeDamage(Math.max(attackPower - 5, 0));
}
}
  • IHealable: can heal, including itself. Only some heroes implement it. The paladin heals with it, and the barbarian does not have it, but he overrides the defense: he takes one fifth less from every hit.
class Paladin extends Character implements IHealable {
public Paladin(String name) {
super(name, 120, 14, "human");
}
@Override
public void heal(Character target, int amount) {
target.restore(amount);
}
@Override
public String useAbility() {
heal(this, 20);
return name + " heals: +20 health";
}
}
class Barbarian extends Character {
public Barbarian(String name) {
super(name, 110, 20, "orc");
}
@Override
public void defend(int attackPower) {
takeDamage(attackPower - attackPower / 5);
}
@Override
public String useAbility() {
power += power / 2;
return name + " flies into a rage: power " + power;
}
}
More precisely, if you are curious
  • In C# interface names start with I. In Java the usual style is without the I: Attackable, Defendable, Healable. Here the names are the same in both languages, so you read the same code.
  • In the first part, the barbarian overrode takeDamage. Now the defense has its own name. takeDamage only subtracts the damage, and defend decides how much damage gets through. That way there can also be damage that goes around the defense: poison, a trap.
  • restore is in Character, so that every hero can be healed. Only someone who implements IHealable can do the healing.

See it

One hit, two targets

Here is the new Character. Besides the contracts, it also has an attack:

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();
}

The attack asks the target: can you defend? target instanceof IDefendable defender is true if the object implements the interface. Then defender is the same object, but as an IDefendable, and you can call defend on it. Otherwise the damage is subtracted directly.

Grok the barbarian hits the shaman and the king. Then the shaman hits him:

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());
Output
Fallen Shaman: 40
Skeleton King: 75
Grok: 100

The shaman loses Grok's full power, 20. The king blocks 5 and loses 15. The shaman hits with 12, and Grok takes 12 − 2 = 10. The attack knows nothing about the king or the barbarian: it only knows the contract.

Try it

Who can heal?

What will the program print? The classes are from the cards above.

Character[] party = { new Paladin("Ardan"), new Barbarian("Grok"), new Witch("Morena") };
for (Character hero : party) {
if (hero instanceof IHealable) {
System.out.println(hero.getName());
}
}
Answer

See it

A fight to the death

Ardan the paladin against the Fallen Shaman. Each round is a hit by the hero and, if the monster is still alive, a hit by the monster. The fight ends when one of them falls:

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() + " hits: " + monster.getName() + " has " + monster.getHealth());
if (monster.isAlive()) {
monster.attack(hero);
System.out.println(" " + monster.getName() + " hits: " + hero.getName() + " has " + hero.getHealth());
}
round++;
}
System.out.println(hero.isAlive() ? hero.getName() + " wins" : monster.getName() + " wins");
Output
1. Ardan hits: Fallen Shaman has 46
Fallen Shaman hits: Ardan has 108
2. Ardan hits: Fallen Shaman has 32
Fallen Shaman hits: Ardan has 96
3. Ardan hits: Fallen Shaman has 18
Fallen Shaman hits: Ardan has 84
4. Ardan hits: Fallen Shaman has 4
Fallen Shaman hits: Ardan has 72
5. Ardan hits: Fallen Shaman has 0
Ardan wins

The loop is the game loop from the lesson "How to think about a game": the state is the health of the two fighters, the rules are the attacks, and the end is the condition of the while.

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() + " hits: " + monster.getName() + " has " + monster.getHealth());
if (monster.isAlive()) {
monster.attack(hero);
System.out.println(" " + monster.getName() + " hits: " + hero.getName() + " has " + hero.getHealth());
}
round++;
}
System.out.println(hero.isAlive() ? hero.getName() + " wins" : monster.getName() + " wins");
Step by step, in words
  1. Ardan starts with 120 health, Fallen Shaman with 60. The rounds start at 1.
  2. Round 1: Ardan hits and takes away 14. Fallen Shaman drops from 60 to 46.
  3. Fallen Shaman is alive and strikes back. It takes away 12: Ardan drops from 120 to 108.
  4. Round 2: Ardan hits and takes away 14. Fallen Shaman drops from 46 to 32.
  5. Fallen Shaman is alive and strikes back. It takes away 12: Ardan drops from 108 to 96.
  6. Round 3: Ardan hits and takes away 14. Fallen Shaman drops from 32 to 18.
  7. Fallen Shaman is alive and strikes back. It takes away 12: Ardan drops from 96 to 84.
  8. Round 4: Ardan hits and takes away 14. Fallen Shaman drops from 18 to 4.
  9. Fallen Shaman is alive and strikes back. It takes away 12: Ardan drops from 84 to 72.
  10. Round 5: Ardan hits and Fallen Shaman drops from 4 to 0. monster.isAlive() is false, so it does not strike back.
  11. The condition of the while is now false: one of them is not alive. The loop stops, and Ardan wins.

See it

10 turns and the player's choice

Against the Butcher, attacks alone are not enough. So the player chooses every turn: 1 is an attack, 2 is the hero's ability. The fight is at most 10 turns, and if both are still alive at the end, the healthier one wins:

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() + " hits: " + monster.getName() + " has " + monster.getHealth());
}
if (monster.isAlive()) {
monster.attack(hero);
System.out.println(" " + monster.getName() + " hits: " + hero.getName() + " has " + hero.getHealth());
}
}
String winner = hero.getHealth() > monster.getHealth() ? hero.getName() : monster.getName();
System.out.println(winner + " wins");
Output
1. Ardan hits: Butcher has 116
Butcher hits: Ardan has 110
2. Ardan hits: Butcher has 102
Butcher hits: Ardan has 99
3. Ardan hits: Butcher has 88
Butcher hits: Ardan has 87
4. Ardan heals: +20 health
Butcher hits: Ardan has 94
5. Ardan hits: Butcher has 74
Butcher hits: Ardan has 80
6. Ardan hits: Butcher has 60
Butcher hits: Ardan has 65
7. Ardan heals: +20 health
Butcher hits: Ardan has 69
8. Ardan hits: Butcher has 46
Butcher hits: Ardan has 52
9. Ardan heals: +20 health
Butcher hits: Ardan has 54
10. Ardan hits: Butcher has 32
Butcher hits: Ardan has 35
Ardan wins

The input for this example is:

Input (one choice per line)
1 1 1 2 1 1 2 1 2 1

If the player chooses only attacks, 1 ten times, the paladin falls on turn 9, and the Butcher has 4 health left. With three heals the turns last until the end, and the paladin wins with 35 health against 32.

More precisely, if you are curious
  • The choice is read every turn, but if the hero falls, the loop stops and the remaining input lines are not read.
  • choice.equals("2") compares the text. Everything else, even an empty line, is an attack: the game cannot stop on an unknown choice. You will see how to reject a wrong choice with an exception in the third part.

Try it

Find the bug

The barbarian hits the Skeleton King with power 20, and the king loses 20 instead of 15. Which line in the attack is the bug on?

Pick the line with the bug.

Try it

What is missing?

Which word does the attack use to ask whether the target implements the interface?
public void attack(Monster target) {
    if (target  IDefendable defender) {
        defender.defend(power);
    } else {
        target.takeDamage(power);
    }
}

Exercise

More battles

Solve these in your editor. Start from the classes in the lesson.

1. A new monster. Andariel, a demon, 100 health and 12 power. Her hit is poisoned: after the normal attack the target takes another 4 damage directly through takeDamage, going around the defense. Run the fight to the death with Grok the barbarian.

Output
1. Grok hits: Andariel has 80
Andariel hits: Grok has 96
2. Grok hits: Andariel has 60
Andariel hits: Grok has 82
3. Grok hits: Andariel has 40
Andariel hits: Grok has 68
4. Grok hits: Andariel has 20
Andariel hits: Grok has 54
5. Grok hits: Andariel has 0
Grok wins

Grok takes 10 from the hit (12 minus one fifth) and 4 from the poison: 14 per round.

2. A healing potion. A class Potion with an amount of health. use(Character target) heals the target and returns text. The potion can be drunk once: after that it says "The potion is empty". In the paladin's fight with the Butcher, the hero drinks a potion for 40 instead of hitting when his health is below 30.

Output
1. Ardan hits: Butcher has 116
Butcher hits: Ardan has 110
2. Ardan hits: Butcher has 102
Butcher hits: Ardan has 99
3. Ardan hits: Butcher has 88
Butcher hits: Ardan has 87
4. Ardan hits: Butcher has 74
Butcher hits: Ardan has 74
5. Ardan hits: Butcher has 60
Butcher hits: Ardan has 60
6. Ardan hits: Butcher has 46
Butcher hits: Ardan has 45
7. Ardan hits: Butcher has 32
Butcher hits: Ardan has 29
8. Ardan drinks a potion: +40 health
Butcher hits: Ardan has 52
9. Ardan hits: Butcher has 18
Butcher hits: Ardan has 34
10. Ardan hits: Butcher has 4
Butcher hits: Ardan has 15
11. Ardan hits: Butcher has 0
Ardan wins

3. A battle report. Instead of a line for every hit, print at the end how many rounds the fight lasted, how much damage the hero dealt, how much he took and how much health he has left. Grok the barbarian against the Skeleton King:

Output
Rounds: 6
Damage dealt: 90
Damage taken: 65
Health left: 45

Hint: remember the target's health before the hit and subtract its health after the hit.

More exercises

  • Witch against king. Run the fight of Morena the witch with the Skeleton King, with the choices from input. Can she win in 10 turns?
  • Party healer. The paladin heals the hero with the least health in an array of three by 10, instead of himself.
  • Who hits first? With new Random(5), decide at the start of each round who hits first.

Test

Check yourself

1. Why does Monster not inherit from Character?
2. The Skeleton King implements IDefendable and blocks 5 of every hit. A barbarian with power 20 hits him. How much health does the king lose?
3. The class implements IDefendable, but the attack calls takeDamage directly. What happens?
4. What does target instanceof IDefendable defender do?

Made by Ivo Gergov as part of Evo Tech's campaign for accessible programming education ·