Python week November Black Friday
This week up to 80% off on Python courses. More info
Black friday is here! Get up to 80 % extra points for free! More info

Lesson 5 - Warrior for the arena in Python

In the previous lesson, Object References, Cloning, and Garbage Collector in Python, we went over how object references work. Which will be very useful for us today. We're going to finish up our arena in the next two lessons. We already have a rolling die, but we're still missing two essential objects: the warrior and the arena itself. Today, we're going to focus mainly on the warrior. First, we'll decide what he (or she) will be able to do, and then we'll write our code.

Attributes

The warrior will have a name and a starting HP (which stands for health points/hit points, e.g. 80hp). We'll store his maximum health, which will vary each instance, and his current health, e.g. a wounded warrior will have 40hp from 80 total. The warrior will have a damage and defense, which will be both defined in hp. When a warrior, with 20hp damage, attacks another warrior with 10hp defense, he takes 10 points of his health. The warrior will have a reference to the rolling die instance. We will always roll the die and add a particular random number to his attack/defense to make the game more unpredictable. Of course, each warrior could have their own rolling die, but I wanted this to be as close to a real board game as possible and show you how OOP simulates reality. The warriors will share a single rolling die instance, which will add an element of being lucky to the game and make the game a bit more realistic. Last of all, we'll make the warriors send messages about what is happening in the fight. The message will look something like this: "Zalgoren attacks with a hit worth 25 hp." However, we'll put the message part off for now, we'll mainly focus on creating the warrior object.

Now when we've got a good idea of what we want, let's get right into it! :) Let's add a Warrior class to the arena project so we can add the attributes to it accordingly. All of them will be private.

Methods

Let's start off by creating a constructor for the attributes. It's gonna be easy. I'll omit further comments in the article to make it clear and short. Just don't forget to add them to your project in the same way as we did before.

class Warrior:
    """
    The class represents an arena warrior
    """

    def __init__(self, name, health, damage, defense, die):
        """
        name - the warrior's name
        health - maximum health
        damage - damage
        defense - defense
        die - a rolling die instance
        """
        self.__name = name
        self.__health = health
        self.__max_health = health
        self.__damage = damage
        self.__defense = defense
        self.__die = die

We assume that the warrior has a full health once he's created, so the constructor doesn't need a max_health parameter. It's easier to just set the max_health to whatever the starting health is.

Again, we should think about what our warrior will be able to do before writing anything. Let's start it easy, we'll need a textual representation of the warrior, i.e. a way of printing his name every time something happens. We can override the __str__() method which will return the name of our warrior. Then, we'd need a method that returns whether the warrior is alive, a boolean value would work best, and will definitely come in handy. To make it a little more interesting, we'll literally draw the warrior's health to the console, so we'll have a cool little visual representation:

[#########    ]

The health shown above is at 70%. The methods we mentioned didn't require any parameters so far. We'll get into the damage and defense methods later. Now, let's implement __str__(), is_alive() and health_bar(). We'll start with __str__(). Which should look familiar, since we did the exact same thing last time:

def __str__(self):
    return str(self.__name)

Try to create the __repre__() method by yourself. Now, let's implement the is_alive() method, there's nothing difficult about it either. We'll just ask whether the health points are greater than 0 and act according to it.

def is_alive(self):
    if self.__health > 0:
       return True
    else:
       return False

Due to the fact that the expression (self.__health > 0) is actually a logical value, we can return it and the code will become shorter:

def is_alive(self):
    return self.__health > 0

Health Bar

As I've already mentioned, the health_bar() method will allow us to display the graphical health indicator. We already know it's usually not a good practice to let a method printing directly to the console, unless printing is its sole responsibility. That's why we'll rather add the characters to a string variable and return them to print them later. Let's take a look at the code and describe it:

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

We specify the maximum amount of characters the health bar can hold and store it to a total variable (e.g. 20). Basically, all we need now is the rule of three. If max_health equals the total number of characters, health equals the count number of characters. Meaning that, the count variable contains the number of characters representing the current health.

Mathematically, here's what the calculation would look like: count = (health / max_health) * total;. We also add casting to a whole number.

It might happen that the warrior's health was so low, it would be printed as 0 characters, but the warrior would be still alive. In this case, we'll draw it as 1 character, otherwise, it'd seem like the warrior has already died. We also use fromatting and replication.

Now we'll put our classes to the test! We'll go to the end of the file and create a warrior and a "rolling die" since we need to pass one as a parameter in the warrior's constructor. Then we'll print whether he's alive and print his health bar:

die = RollingDie(10)
warrior = Warrior("Zalgoren", 100, 20, 10, die)
print("Warrior: {0}".format(warrior)) #test __str__()
print("Alive: {0}".format(warrior.is_alive())) #test is_alive()
print("Health: {0}".format(warrior.health_bar())) #test health_bar()
input()

The output:

Console application
Warrior: Zalgoren
Alive: True
health: [####################]

Fight

It's time to implement methods for attack and defense!

Defense

Let's start with the defense. The defend() method will resist hits whose power will be passed as a parameter. The method should look something like this:

def defend(self, hit):
    injury = hit - (self.__defense + self.__die.roll())
    if injury > 0:
        self.__health = self.__health - injury
        if self.__health < 0:
            self.__health = 0

First, we calculate the injury. To do this, we subtract our defense and whatever number the die rolled from the enemy's attack (hit). If our defense wasn't enough to resist the enemy's attack, (injury > 0), we take points off our health. This condition is important, because if we endured the hit and the injury was -2, our health would increase instead. After reducing the health, we check whether it's not negative and eventually set it to zero.

Attack

The attack() method will take the enemy as a parameter. That's because we need to call his defend() method which reacts to our attack and reduces the enemy's health. Here we can see the benefits of references in Python, we can simply pass instances and call methods on them without having to copy these instances. First, we calculate the hit, like we did in defense. Our hit will be the damage + whatever value the die rolled. Then we'll call the defend() method on the enemy and pass the hit value to it:

def attack(self, enemy):
    hit = self.__damage + self.__die.roll()
    enemy.defend(hit)

That's pretty much it. Now, let's try to attack our warrior and redraw his health in our program. To keep things simple, we won't create another warrior yet and just let our warrior attack himself:

die = RollingDie(10)
warrior = Warrior("Zalgoren", 100, 20, 10, die)
print("Warrior: {0}".format(warrior)) #test __str__()
print("Alive: {0}".format(warrior.is_alive())) #test is_alive()
print("Health: {0}".format(warrior.health_bar())) #test health_bar()
warrior.attack(warrior)
print("Health after the hit: {0}".format(warrior.health_bar()))
input()

The output:

Console application
Warrior: Zalgoren
Alive: True
health: [####################]
Health after the hit: [##################  ]

It seems to work as expected. Let's proceed to the last part of today's lesson - messages:

Messages

As planned, we'll notify the user about attacks and defenses through the console. The printing will not be performed by the Warrior class, it'll only return messages as strings. One approach could be to make the attack() and defend() methods return the message when these methods are called. However, what if we wanted to return a message from a method that already returns some other value? A method can't return 2 things and if it somehow did, it'd be very unreadable...

We'll make a universal solution, the message will be stored in a private variable __message and we'll create set and get methods for it. We could make the variable public, but there's no reason to allow its modification from outside the class. Concatenating complex messages could also become problematic without the set method.

Let's add the __message to the __init__() method:

self.__message = ""

Now, let's create the two methods. Private __set_message() will take a string as a parameter and set the message to the private attribute. Notice that private methods start with a double underscore __ as well:

def __set_message(self, message):
    self.__message = message

There's nothing difficult about it. A public method for getting the message is easy, too:

def get_last_message(self):
    return self.__message

Let's upgrade our attack() and defend() methods to set the messages, now they look like this:

def defend(self, hit):
    injury = hit - (self.__defense + self.__die.roll())
    if injury > 0:
        message = "{0} defended against the attack but still lost {1} hp.".format(self.__name, injury)

        self.__health = self.__health - injury
        if self.__health < 0:
            self.__health = 0
            message = message[:-1] + " and died."
        else:
            message = "{0} blocked the hit.".format(self.__name)
        self.__set_message(message)

def attack(self, enemy):
    hit = self.__damage + self.__die.roll()
    message = "{0} attacks with a hit worth {1} hp.".format(self.__name, hit)
    self.__set_message(message)
    enemy.defend(hit)

Let's add a second warrior, just for completeness' sake:

die = RollingDie(10)
warrior = Warrior("Zalgoren", 100, 20, 10, die)
print("Health: {0}".format(warrior.health_bar())) #test health_bar()
# warrior attack phase
enemy = Warrior("Shadow", 60, 18, 15, die)
enemy.attack(warrior)
print(enemy.get_last_message())
print(warrior.get_last_message())
print("Health: {0}".format(warrior.health_bar()))
input()

The result:

Console application
Health: [####################]
Shadow attacks with a hit worth 27 hp
Zalgoren defended against the attack but still lost 12 hp
Health: [##################  ]

Now we have the rolling die and the warriors. In the next lesson, Arena with warriors in Python, we'll create the arena.


 

Download

Downloaded 3x (4.33 kB)
Application includes source codes in language Python

 

 

Article has been written for you by David Capka
Avatar
Do you like this article?
No one has rated this quite yet, be the first one!
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 College The author learned IT at the Unicorn College - a prestigious college providing education on IT and economics.
Previous article
Object References, Cloning, and Garbage Collector in Python
All articles in this section
Object-Oriented Programming in Python
Thumbnail
Next article
Arena with warriors in Python
Activities (2)

 

 

Comments

To maintain the quality of discussion, we only allow registered members to comment. Sign in. If you're new, Sign up, it's free.

No one has commented yet - be the first!