Lesson 8 - Arena with a mage in Python (inheritance and polymorphism)
In the previous lesson, Inheritance and polymorphism in Python, we went over inheritance and polymorphism
in Python. In today's tutorial, we're going to make a sample program using these
concepts, as promised. We'll get back to our arena and inherit a mage from the
Warrior
class. These next couple of lessons will be among the most
challenging. Which is why you should be working on OOP on your own, solving our
exercises and coming up with your own applications. Try putting all that you've
learned to practice. Make programs that you would find useful,
so it will be more engaging and fun

Before we even get to coding, we'll think about what a mage should be capable of doing. The mage will work just like a warrior, but on top of health, he will also have mana. At first, the mana will be full. When it is, the mage can perform a magic attack which will have a higher damage than a normal attack, depending on how we set it. This attack will bring his mana down to 0. The mana will increase by 10 every round and the mage would only be able to perform regular attacks. Once the mana is full, he'll be able to use his magic attack again. The mana will be displayed using a graphical indicator just like the health bar.
Let's create a Mage
class and inherit it from the
Warrior
class. It'll look something like this (don't forget to add
comments):
class Mage(Warrior):
We don't have access to all the warrior variables in the mage class because
we still have the attributes in the Warrior
class set as private.
We'll have to change the private attributes to protected in the
Warrior
class. We'll have to update this in all methods using these
attributes. All we really need now is the die
and the
name
attributes. Either way, we'll set all of the warrior's
attributes as protected because they might come in handy for future descendants.
On second thought, it wouldn't be wise to set the message
attribute
as protected since it is not related to the warrior directly. With all of that
in mind, the class would look something like this:
class Mage(Warrior): def __init__(self, name, health, damage, defense, die): self._name = name self._health = health self._max_health = health self._damage = damage self._defense = defense self._die = die self.__message = ""
Moving on to the constructor.
Descendant constructor
We can't use the Warrior
's constructor since the
Mage
has 2 extra parameters (mana and the magic damage).
We'll define the constructor in the descendant, which will take both parameters needed to create a warrior and the extra ones needed for the mage.
The descendant constructor must always call the parent constructor. If you forget to do so, the instance may not be properly initialized. The only time we don't call the ancestor constructor is when there isn't one. Our constructor must have all needed parameters for an ancestor and the new ones that the descendant needs. The ancestor's constructor will be executed before the descendant's.
In Python, there is a method known as super()
which allows us to
call a method on the parent. Meaning, that we can call the ancestor constructor
with the given parameters and initialize the mage as well. In Python, calling
the ancestor constructor has to be included in the method body.
Mage's constructor should look something like this:
def __init__(self, name, health, damage, defense, die, mana, magic_damage): super().__init__(name, health, damage, defense, die) self.__mana = mana self.__max_mana = mana self.__magic_damage = magic_damage
Now, let's go at the end of the program and change the second warrior (Shadow) to a mage. Like this:
gandalf = Mage("Gandalf", 60, 15, 12, die, 30, 45)
We will also have to change the line where we put the warrior in the arena.
Polymorphism and overriding methods
It would be nice if the Arena
could work with the mage in the
same way as it does with the warrior. We already know that in order to do so we
must apply the concept of polymorphism. The arena will call the
attack()
method, passing an enemy as a parameter. It won't care
whether the attack will be performed by a warrior or by a mage, the arena will
work with them in the same way. We'll have to override the
ancestor's attack()
method in the mage class. We'll rewrite its
inherited method so the attack will use mana, but the header of the method will
remain the same.
In a descendant, we can simply override any method. In Python, all methods are "virtual" as of the C++/C# language terms, meaning they can be overrided.
Talking about methods, we'll certainly need a set_message()
method which is private now. We have to make it protected:
def _set_message(self, message):
...
When you create a class you should always consider whether it
would have descendants and therefore make appropriate protected attributes and
methods. I didn't mean to overwhelm you with all of this information when we
first made the Warrior
class but now that we understand these
modifiers, we should use them
Now let's go back to the
attack()
method and override its behavior.
It won't be complicated. Depending on the mana value, we'll either perform a
normal attack or a magic attack. The mana value will be either increased by
10
each round or in the case where the mage uses a magic attack, it
will be reduced to 0
:
def attack(self, enemy): # Mana isn't full if self.__mana < self.__max_mana: self.__mana = self.__mana + 10 if self.__mana > self.__max_mana: self.__mana = self.__max_mana hit = self._damage + self._die.roll() message = "{0} attacks with a hit worth {1} hp".format(self._name, hit) self._set_message(message) # Magic damage else: hit = self.__magic_damage + self._die.roll() message = "{0} used magic for {1} hp.".format(self._name, hit) self._set_message(message) self.__mana = 0 enemy.defend(hit)
The code is clear. Notice how we limit the mana
to
maxMana
since it could be that it exceeds the maxim value when
increasing it by 10
each round. When you think about it, the normal
attack is already implemented in the ancestor's attack()
method.
Certainly, it'd be better to just call the ancestor's attack()
instead of copying its behavior. We'll use super()
to do just
that:
def attack(self, enemy): # Mana isn't full if self.__mana < self.__max_mana: self.__mana = self.__mana + 10 if self.__mana > self.__max_mana: self.__mana = self.__max_mana super().attack(enemy) # Magic damage else: hit = self.__magic_damage + self._die.roll() message = "{0} used magic for {1} hp.".format(self._name, hit) self._set_message(message) self.__mana = 0 enemy.defend(hit)
There are lots of time-saving techniques we can set up using inheritance. In this case, all it did is save us a few lines, but in a larger project, it would make a huge difference. Other non-private methods will be inherited automatically.
The application now works as expected.
Console application
-------------- Arena --------------
Warriors health:
Zalgoren [########### ]
Gandalf [############## ]
Gandalf attacks with a hit worth 23 hp
Zalgoren defended against the attack but still lost 9 hp
For completeness' sake, let's make the arena show us the mage's current mana
state using a mana bar. We'll add a public method and name it
mana_bar()
. It will return a string with a graphical mana
indicator.
We'll modify the health_bar()
method in the Warrior
class to avoid writing the same graphical bar logic twice. Let me remind us how
the original method looks:
def health_bar(self): total = 20 count = int(self._health / self._max_health * total) if (count == 0 and self.is_alive()): count = 1 return "[{0}{1}]".format("#"*count, " "*(total-count))
The health_bar()
method doesn't really depend on a character's
health. All it needs is a current value and a maximum value. Let's rename the
method to graphical_bar()
and give it two parameters: current value
and maximum value. We'll rename the health
and
max_health
variables to current
and
maximum
.
def graphical_bar(self, current, maximum): total = 20 count = int(current / maximum * total) if (count == 0 and self.is_alive()): count = 1 return "[{0}{1}]".format("#"*count, " "*(total - count))
Let's implement the health_bar()
method in the
Warrior
class again. It'll be a one-liner now. All we have to do
now is call the graphical_bar()
method and fill the parameters
accordingly:
def health_bar(self) return self.graphical_bar(self._health, self._max_health);
Of course, I could add the graphical_bar()
method in the
Warrior
before, but I wanted to show you how to deal with cases
where we would need to accomplish similar functionality multiple times. You'll
need to put this kind of parametrization in practice since you never know
exactly what you'll need from your program at any given moment during the design
stage. .[hint
Now, we can easily draw graphical bars as needed. Let's move to the
Mage
class and implement the mana_bar()
method:
def mana_bar(self): return self.graphical_bar(self.__mana, self.__max_mana)
Simple, isn't it? Our mage is done now, all that's left to do is tell the
arena to show mana in case the warrior is a mage. Let's move to the
Arena
class.
Recognizing the object type
We'll add a separate printing method for warriors,
print_warrior()
, to keep things nice and neat. Its parameter will
be a Warrior
instance:
def __print_warrior(self, warrior): print(warrior) print("Health: {0}".format(warrior.health_bar()))
Now, let's tell it to show the mana bar if the warrior is a mage. We'll use
the isinstance
operator to do just that:
def __print_warrior(self, warrior): print(warrior) print("Health: {0}".format(warrior.health_bar())) if isinstance(warrior, Mage): print("Mana: {0}".format(warrior.mana_bar()))
This is it, print_warrior()
will be called in the
render()
method, which now looks like this:
def __render(self): self.__clear_screen() print("-------------- Arena -------------- \n") print("Warriors: \n") self.__print_warrior(self.__warrior_1) self.__print_warrior(self.__warrior_2) print("")
Done
Console application
-------------- Arena --------------
Warriors:
Gandalf
Health: [####### ]
Mana: [# ]
Zalgoren
Health: [#### ]
Gandalf used magic and took 48 hp off
Zalgoren defended against the attack but still lost 33 hp
You can download the code below. If there is something you don't quite understand, try reading the lesson several times, this content is extremely important for you to know. In the next lesson, Static class members in Python, we'll explain the concept of static class members.
Did you have a problem with anything? Download the sample application below and compare it with your project, you will find the error easily.
Download
By downloading the following file, you agree to the license terms
Downloaded 13x (7.84 kB)
Application includes source codes in language Python