Get up to 80 % extra points for free! More info:

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 :)

Mage - Object-Oriented Programming in Python

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

 

Previous article
Inheritance and polymorphism in Python
All articles in this section
Object-Oriented Programming in Python
Skip article
(not recommended)
Static class members in Python
Article has been written for you by David Capka Hartinger
Avatar
User rating:
1 votes
The author is a programmer, who likes web technologies and being the lead/chief article writer at ICT.social. He shares his knowledge with the community and is always looking to improve. He believes that anyone can do what they set their mind to.
Unicorn university David learned IT at the Unicorn University - a prestigious college providing education on IT and economics.
Activities