Lesson 5 - Warrior for the arena in Swift
In the previous lesson, Reference and value data types in Swift, we went over the differences between reference and value data types in Swift.
We already know how references work and how we can deal with objects. 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.
Fields
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
RollingDie
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 randomness 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 ArenaFight
project and add fields to it accordingly.
All of them will be private
:
class Warrior { private var name : String private var health : Double private var maxHealth : Double private var damage : Int private var defense : Int private var die : RollingDie }
Of course, the RollingDie
class has to be in our project.
Methods
Let's start off by creating a constructor for the fields so that our build would work. It's not hard.
init(name: String, health: Int, damage: Int, defense: Int, die: RollingDie) { self.name = name self.health = Double(health) self.maxHealth = self.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 maxHealth
parameter. It's easier to just
set the maxHealth
to whatever the starting health is. We require
the health
and maxHealth
variables as
Int
s in the constructor but then convert them to
Double
internally. We do this to make it easier to divide them
later.
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. Let's
create the description
property which we already know (don't forget
to add : CustomStringConvertible
after the class similarly we did
it in RollingDie
). The property will return the name of our
warrior. Then, we would need a method that returns whether the warrior is alive,
a Bool
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 description
, alive()
and
healthBar()
. We'll start with description
. Which
should look familiar, since we did the exact same thing last time:
var description: String {
return name
}
Now, let's implement the 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. It would probably look something
like this:
func alive() -> Bool { if health > 0 { return true } else { return false } }
Due to the fact that the expression health > 0
is actually a
logical value, we can return it and the code will become shorter:
func alive() -> Bool { return health > 0 }
HealthBar
As I've already mentioned, the healthBar()
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:
func healthBar() -> String { var s = "[" let total : Double = 20 var count : Double = round((health / maxHealth) * total) if (count == 0) && (alive()) { count = 1 } for _ in 0..<Int(count) { s += "#" } s = s.padding(toLength: Int(total) + 1, withPad: " ", startingAt: 0) s += "]" return s }
We prepare a String
s
and assign a leading
character [
to it. Then, 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
maxHealth
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 / maxHealth) * total
. We round it
up to the nearest 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.
Then we just simply concatenate the right number of indicator characters to
the String s
, using a for
loop. We'll add spaces using
padding()
and make the length total + 1
long (we have
to convert it to Int
again) where the extra character is the first
[
character. Finally, we add the trailing bracket and return the
string.
We got to the padding()
method, which we missed in the basic
constructs course. Swift has only one such method that works quite simply, but
can only add characters from the right side. If we wanted to add characters from
the left side, then we'd have to write our own logic.
Now we'll put our classes to the test! We'll go to the
main.swift
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:
let die = RollingDie(sidesCount: 10) let warrior = Warrior(name: "Zalgoren", health: 100, damage: 20, defense: 10, die: die) print("Warrior: \(warrior)") // description test print("Alive: \(warrior.alive())") // alive() test print("Health: \(warrior.healthBar())") // healthBar() test
The output:
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:
func defend(hit: Int) { let injury = Double(hit - (defense + die.roll())) if (health > 0) { health -= injury if (health <= 0) 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 Swift, we can simply pass instances and call methods on them
without having to copy these instances. First, we calculate the hit, like 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:
func attack(enemy: Warrior) { let hit = damage + die.roll() enemy.defend(hit: 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:
let die = RollingDie(sidesCount: 10) let warrior = Warrior(name: "Zalgoren", health: 100, damage: 20, defense: 10, die: die) print("Warrior: \(warrior)") // description test print("Alive: \(warrior.alive())") // alive() test print("Health: \(warrior.healthBar())") // healthBar() test warrior.attack(enemy: warrior) // attack test print("Health after attack: \(warrior.healthBar())")
The output:
Warrior: Zalgoren Alive: true Health: [####################] Health after attack: [################## ]
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 set the return
type of the attack()
and defend()
methods to
String
and 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
field message
and we'll create set and get methods for it. We could
make the field 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 class fields:
private var message : String = ""
Now, let's create the two methods. Private setMessage()
will
take a string as a parameter and set the message to the private field:
private func setMessage(_ message: String) {
self.message = message
}
There's nothing difficult about it. A public method for getting the message is easy, too:
func getLastMessage() -> String {
return message
}
Let's upgrade our attack()
and defend()
methods to
set the messages, now they look like this:
func attack(enemy: Warrior) { let hit = damage + die.roll() setMessage("\(name) attacks with a hit worth \(hit) hp") enemy.defend(hit: hit) } func defend(hit: Int) { let injury = Double(hit - (defense + die.roll())) var message = "" if (health > 0) { health -= injury message = "\(name) defended against the attack but still lost \(Int(injury)) hp" if (health <= 0) { health = 0 } } else { message = "\(name) blocked the hit" } setMessage(message) }
Let's add a second warrior, just for completeness' sake:
let die = RollingDie(sidesCount: 10) let warrior = Warrior(name: "Zalgoren", health: 100, damage: 20, defense: 10, die: die) print("Health: \(warrior.healthBar())") // healthBar() test // warrior attack phase let enemy = Warrior(name: "Shadow", health: 60, damage: 18, defense: 15, die: die) enemy.attack(enemy: warrior) print(enemy.getLastMessage()) print(warrior.getLastMessage()) print("Health: \(warrior.healthBar())")
The output:
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 Swift, we'll create the arena.