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();}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 + ": health " + Health + ", power " + 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}: health {self._health}, power {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}: health ${this._health}, power ${this._power}`); }}Only the ability is different. That is why use_ability() is abstract: every race has to write it. In Python you say this with the @abstractmethod decorator from the abc module, and Character inherits from ABC. That makes Character an abstract class, because there is no hero who is "just a character": Character("Ardan", 100, 10, "human") stops with TypeError: Can't instantiate abstract class Character without an implementation for abstract method 'use_ability'.
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". Python does not enforce it, but everyone who reads the code respects it. From outside they are read through@property:hero.healthreads the health, andhero.health = 1000stops withAttributeError.
- 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
take_damageand the abilities, so no one can make a hero immortal with one line. max(self._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();Ardan, human: health 120, power 14Ardan, human: health 90, power 14Ardan heals: +20 healthArdan, human: health 110, power 14class Paladin : Character{ public Paladin(string name) : base(name, 120, 14, "human") { }
public override string UseAbility() { Health += 20; return Name + " heals: +20 health"; }}
Character hero = new Paladin("Ardan");hero.ShowInfo();hero.TakeDamage(30);hero.ShowInfo();Console.WriteLine(hero.UseAbility());hero.ShowInfo();Ardan, human: health 120, power 14Ardan, human: health 90, power 14Ardan heals: +20 healthArdan, human: health 110, power 14class Paladin(Character): def __init__(self, name): super().__init__(name, 120, 14, "human")
def use_ability(self): self._health += 20 return f"{self._name} heals: +20 health"
hero = Paladin("Ardan")hero.show_info()hero.take_damage(30)hero.show_info()print(hero.use_ability())hero.show_info()Ardan, human: health 120, power 14Ardan, human: health 90, power 14Ardan heals: +20 healthArdan, human: health 110, power 14class Paladin extends Character { constructor(name) { super(name, 120, 14, "human"); }
useAbility() { this._health += 20; return `${this._name} heals: +20 health`; }}
const hero = new Paladin("Ardan");hero.showInfo();hero.takeDamage(30);hero.showInfo();console.log(hero.useAbility());hero.showInfo();Ardan, human: health 120, power 14Ardan, human: health 90, power 14Ardan heals: +20 healthArdan, human: health 110, power 14The variable hero has no type; it points to an object of class Paladin. hero.use_ability() calls the paladin's method, and show_info and take_damage come from Character.
The idea
Each race in its own way
The barbarian takes one fifth less damage. He overrides take_damage 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; }}class Barbarian : Character{ public Barbarian(string name) : base(name, 110, 20, "orc") { }
public override void TakeDamage(int amount) { base.TakeDamage(amount - amount / 5); }
public override string UseAbility() { Power += Power / 2; return Name + " flies into a rage: power " + Power; }}class Barbarian(Character): def __init__(self, name): super().__init__(name, 110, 20, "orc")
def take_damage(self, amount): super().take_damage(amount - amount // 5)
def use_ability(self): self._power += self._power // 2 return f"{self._name} flies into a rage: power {self._power}"class Barbarian extends Character { constructor(name) { super(name, 110, 20, "orc"); }
takeDamage(amount) { super.takeDamage(amount - Math.trunc(amount / 5)); }
useAbility() { this._power += Math.trunc(this._power / 2); return `${this._name} flies into a rage: power ${this._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"; }}class Amazon : Character{ private bool dodging;
public Amazon(string name) : base(name, 100, 18, "elf") { }
public override void TakeDamage(int amount) { if (dodging) { amount = amount / 2; dodging = false; } base.TakeDamage(amount); }
public override string UseAbility() { dodging = true; return Name + " gets ready to dodge: the next hit is halved"; }}class Amazon(Character): def __init__(self, name): super().__init__(name, 100, 18, "elf") 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} gets ready to dodge: the next hit is halved"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`; }}Both use super().take_damage: the check that health does not go below 0 stays in one place.
amount // 5 is integer division: 30 // 5 is 6, and 24 // 5 is 4. The amazon's dodge is in _dodging, a field that is hers alone. 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(Character): def __init__(self, name): super().__init__(name, 100, 18, "elf") 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} gets ready to dodge: the next hit is halved"
lira = Amazon("Lira")lira.use_ability()lira.take_damage(30)lira.take_damage(30)print(lira.health)Lira starts with 100. Her ability gets her ready to dodge. The first hit is 30 / 2 = 15, and the dodge is used up: 85. The second is full: 85 − 30 = 55.
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 Necromancer : Character{ public Necromancer(string name) : base(name, 90, 10, "dwarf") { }
public override string UseAbility() { Power += 3; return Name + " draws power from the dead: +3 power"; }}class Necromancer(Character): def __init__(self, name): super().__init__(name, 90, 10, "dwarf")
def use_ability(self): self._power += 3 return f"{self._name} draws power from the dead: +3 power"class Necromancer extends Character { constructor(name) { super(name, 90, 10, "dwarf"); }
useAbility() { this._power += 3; return `${this._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"; }}class Witch : Character{ public Witch(string name) : base(name, 95, 16, "blood elf") { }
public override string UseAbility() { Health += 15; return Name + " calls on the elements: +15 health"; }}class Witch(Character): def __init__(self, name): super().__init__(name, 95, 16, "blood elf")
def use_ability(self): self._health += 15 return f"{self._name} calls on the elements: +15 health"class Witch extends Character { constructor(name) { super(name, 95, 16, "blood elf"); }
useAbility() { this._health += 15; return `${this._name} calls on the elements: +15 health`; }}Now all five go into one list, 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();}Ardan heals: +20 healthArdan, human: health 110, power 14Lira gets ready to dodge: the next hit is halvedLira, elf: health 85, power 18Grok flies into a rage: power 30Grok, orc: health 86, power 30Torin draws power from the dead: +3 powerTorin, dwarf: health 60, power 13Morena calls on the elements: +15 healthMorena, blood elf: health 80, power 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 heals: +20 healthArdan, human: health 110, power 14Lira gets ready to dodge: the next hit is halvedLira, elf: health 85, power 18Grok flies into a rage: power 30Grok, orc: health 86, power 30Torin draws power from the dead: +3 powerTorin, dwarf: health 60, power 13Morena calls on the elements: +15 healthMorena, blood elf: health 80, power 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 heals: +20 healthArdan, human: health 110, power 14Lira gets ready to dodge: the next hit is halvedLira, elf: health 85, power 18Grok flies into a rage: power 30Grok, orc: health 86, power 30Torin draws power from the dead: +3 powerTorin, dwarf: health 60, power 13Morena calls on the elements: +15 healthMorena, blood elf: health 80, power 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 heals: +20 healthArdan, human: health 110, power 14Lira gets ready to dodge: the next hit is halvedLira, elf: health 85, power 18Grok flies into a rage: power 30Grok, orc: health 86, power 30Torin draws power from the dead: +3 powerTorin, dwarf: health 60, power 13Morena calls on the elements: +15 healthMorena, blood elf: health 80, power 16The 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 list without the loop changing.
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()Step by step, in words
- There is one list party, and it holds 5 objects of 5 different classes.
- hero points to Ardan, an object of class Paladin. The use_ability() of Paladin runs. take_damage is the shared one from Character: 140 − 30 = 110.
- hero points to Lira, an object of class Amazon. The use_ability() of Amazon runs. Amazon overrides take_damage: from the hit of 30 it takes only 15, and 85 is left.
- hero points to Grok, an object of class Barbarian. The use_ability() of Barbarian runs. Barbarian overrides take_damage: from the hit of 30 it takes only 24, and 86 is left.
- hero points to Torin, an object of class Necromancer. The use_ability() of Necromancer runs. take_damage is the shared one from Character: 90 − 30 = 60.
- hero points to Morena, an object of class Witch. The use_ability() of Witch runs. take_damage is the shared one from Character: 110 − 30 = 80.
- 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.
self. points to the barbarian itself, so the method calls itself, not the parent. That is recursion with no bottom: take_damage calls take_damage, which calls it again... until the stack fills up and the program stops with RecursionError: maximum recursion depth exceeded. You need super().take_damage(amount - amount // 5).Try it
What is missing?
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}: health {self._health}, power {self._power}")
@
def use_ability(self):
passabstractmethod: the method is only a promise, and every subclass has to write it. Otherwise you cannot create an object of the subclass, and Python stops with TypeError. For this to work, Character inherits from ABC.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.
Eli, forest folk: health 105, power 12Eli bonds with the forest: +8 health, +2 powerEli, forest folk: health 113, power 142. The party roster. Write a function show_all(party) that shows the heroes with numbers and, at the end, their total health. Run it for the party from the lesson.
1. Ardan, human: health 120, power 142. Lira, elf: health 100, power 183. Grok, orc: health 110, power 204. Torin, dwarf: health 90, power 105. Morena, blood elf: health 95, power 16Total health: 5153. 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.
Strongest: Grok with power 30More exercises
- A party from input. Read five lines of "race name", for example "paladin Ardan", and create the heroes in a list. 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