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.
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 53x (4.33 kB)
Application includes source codes in language Python