Skip to content
wannacode

RPG 1: the heroes

Problem

Five races, one party

In the world of Arcanum, five races fight over its lands. Each one has its own heroes:

Race Hero What sets them apart
humans paladin medium damage, heals
elves amazon fast, hits harder than humans, dodges hits
orcs barbarian high damage, takes less damage, flies into a rage
dwarves necromancer low damage, draws power from the dead
blood elves witch medium damage, recovers with the power of the elements

You want a party of all five and one loop that shows them and sends them into battle. If every race is a separate class with nothing in common, you will have five arrays, five loops and five copies of the same code for health and damage. And a sixth race will want a sixth copy.

The idea

What is shared goes in Character

Every hero has a name, health, power and a race. Every hero takes damage, can be alive or dead and can show what it is. That is what they share, and it goes in the parent:

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 + ": health " + health + ", power " + power);
}
public abstract String useAbility();
}

Only the ability is different. That is why it is not in Character: every race writes its own useAbility(). JavaScript has no abstract classes or methods. If a race forgets its ability, the program stops only when the ability is called: TypeError: hero.useAbility is not a function.

The ability returns text about what happened, instead of printing it. That way the game decides who sees what on the screen, not the hero.

More precisely, if you are curious
  • The fields start with _. That is a convention: "only for the class and its subclasses". JavaScript also has truly private fields, with #, but even the subclasses cannot see those, so the shared fields here use _. From outside they are read with get: hero.health reads the health, and hero.health = 1000 does not change it, because there is no set. In a file without import the assignment silently does nothing. In a file with import (a module, like the programs with ask) it stops with TypeError: Cannot set property health of #<Character> which has only a getter.
  • The old exercise had a getter and a setter for every field. Here there is no way to change the health from outside. It changes only through takeDamage and the abilities, so no one can make a hero immortal with one line.
  • Math.max(this._health - amount, 0) keeps the health from going below 0.

See it

The first hero

In its constructor, the paladin says what its race is and how much health and power it starts with. It takes everything else from Character. It only writes its ability:

class Paladin extends Character {
public Paladin(String name) {
super(name, 120, 14, "human");
}
@Override
public String useAbility() {
health += 20;
return name + " heals: +20 health";
}
}
Character hero = new Paladin("Ardan");
hero.showInfo();
hero.takeDamage(30);
hero.showInfo();
System.out.println(hero.useAbility());
hero.showInfo();
Output
Ardan, human: health 120, power 14
Ardan, human: health 90, power 14
Ardan heals: +20 health
Ardan, human: health 110, power 14

The variable hero has no type; it points to an object of class Paladin. hero.useAbility() calls the paladin's method, and showInfo and takeDamage come from Character.

The idea

Each race in its own way

The barbarian takes one fifth less damage. He overrides takeDamage and calls the parent's version with the smaller number:

class Barbarian extends Character {
public Barbarian(String name) {
super(name, 110, 20, "orc");
}
@Override
public void takeDamage(int amount) {
super.takeDamage(amount - amount / 5);
}
@Override
public String useAbility() {
power += power / 2;
return name + " flies into a rage: power " + power;
}
}

The amazon remembers whether she is ready to dodge. If she is, the next hit is halved and the dodge is used up:

class Amazon extends Character {
private boolean dodging;
public Amazon(String name) {
super(name, 100, 18, "elf");
}
@Override
public void takeDamage(int amount) {
if (dodging) {
amount = amount / 2;
dodging = false;
}
super.takeDamage(amount);
}
@Override
public String useAbility() {
dodging = true;
return name + " gets ready to dodge: the next hit is halved";
}
}

Both use super.takeDamage: the check that health does not go below 0 stays in one place.

In JavaScript / also gives fractions: 24 / 5 is 4.8. Math.trunc cuts off the fractional part, like integer division in Java. The amazon's dodge is in #dodging: a truly private field that is hers alone and that not even Character can see. You override a method by simply writing it again in the subclass: nothing is needed in front of it.

Try it

How much health is left?

What will the program print? Character is from the card above.

class Amazon extends Character {
#dodging = false;
constructor(name) {
super(name, 100, 18, "elf");
}
takeDamage(amount) {
if (this.#dodging) {
amount = Math.trunc(amount / 2);
this.#dodging = false;
}
super.takeDamage(amount);
}
useAbility() {
this.#dodging = true;
return `${this._name} gets ready to dodge: the next hit is halved`;
}
}
const lira = new Amazon("Lira");
lira.useAbility();
lira.takeDamage(30);
lira.takeDamage(30);
console.log(lira.health);
Answer

See it

The party

The necromancer and the witch write only a constructor and an ability:

class Necromancer extends Character {
public Necromancer(String name) {
super(name, 90, 10, "dwarf");
}
@Override
public String useAbility() {
power += 3;
return name + " draws power from the dead: +3 power";
}
}
class Witch extends Character {
public Witch(String name) {
super(name, 95, 16, "blood elf");
}
@Override
public String useAbility() {
health += 15;
return name + " calls on the elements: +15 health";
}
}

Now all five go into one array, and one loop makes them use their ability and take a hit of 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();
}
Output
Ardan heals: +20 health
Ardan, human: health 110, power 14
Lira gets ready to dodge: the next hit is halved
Lira, elf: health 85, power 18
Grok flies into a rage: power 30
Grok, orc: health 86, power 30
Torin draws power from the dead: +3 power
Torin, dwarf: health 60, power 13
Morena calls on the elements: +15 health
Morena, blood elf: health 80, power 16

The loop does not know who is of which race. Each hero decides by itself what its ability does and how much damage it takes. The amazon loses 15, the barbarian 24, the rest 30. A sixth race will go into the same array without the loop changing.

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();
}
Step by step, in words
  1. There is one array party, and it holds 5 objects of 5 different classes.
  2. hero points to Ardan, an object of class Paladin. The useAbility() of Paladin runs. takeDamage is the shared one from Character: 140 − 30 = 110.
  3. hero points to Lira, an object of class Amazon. The useAbility() of Amazon runs. Amazon overrides takeDamage: from the hit of 30 it takes only 15, and 85 is left.
  4. hero points to Grok, an object of class Barbarian. The useAbility() of Barbarian runs. Barbarian overrides takeDamage: from the hit of 30 it takes only 24, and 86 is left.
  5. hero points to Torin, an object of class Necromancer. The useAbility() of Necromancer runs. takeDamage is the shared one from Character: 90 − 30 = 60.
  6. hero points to Morena, an object of class Witch. The useAbility() of Witch runs. takeDamage is the shared one from Character: 110 − 30 = 80.
  7. There is one loop, and it does not know the races. Which method runs is decided by the object hero points to.

Try it

Find the bug

The program stops at the very first hit against the barbarian. Which line is the bug on?

Pick the line with the bug.

Try it

What is missing?

JavaScript cannot require a race to have an ability. But the paladin has to take everything shared from Character. What goes in the blank?
class Paladin  Character {
  constructor(name) {
    super(name, 120, 14, "human");
  }

  useAbility() {
    this._health += 20;
    return `${this._name} heals: +20 health`;
  }
}

Exercise

More heroes

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

1. A sixth hero. Add a druid from the forest folk: 105 health, 12 power. Its ability gives +8 health and +2 power. Do not touch Character.

Output
Eli, forest folk: health 105, power 12
Eli bonds with the forest: +8 health, +2 power
Eli, forest folk: health 113, power 14

2. The party roster. Write a function showAll(party) that shows the heroes with numbers and, at the end, their total health. Run it for the party from the lesson.

Output
1. Ardan, human: health 120, power 14
2. Lira, elf: health 100, power 18
3. Grok, orc: health 110, power 20
4. Torin, dwarf: health 90, power 10
5. Morena, blood elf: health 95, power 16
Total health: 515

3. Who is the strongest? Each member of the party uses their ability once. Write a function strongest(party) that returns the hero with the most power.

Output
Strongest: Grok with power 30

More exercises

  • A party from input. Read five lines of "race name", for example "paladin Ardan", and create the heroes in an array. For an unknown race, say so and read the next line.
  • Rage in moderation. Rage after rage makes the barbarian endlessly strong. Make the ability raise the power only the first time, and after that say that he is already in a rage.
  • Who is the toughest? Like exercise 3, but by health, after each one has taken a hit of 25.

Test

Check yourself

1. Why is there no useAbility() in Character?
2. The array party holds objects of five classes. Which takeDamage is called for the barbarian in it?
3. The amazon gets ready to dodge, then she is hit for 40, and then for 40 again. How much health does she lose?
4. Barbarian overrides takeDamage. How does it call the Character version?

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