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

Lesson 5 - Warrior for the arena in Kotlin

In the previous lesson, Reference data types in Kotlin, we went over the reference data types. 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 have to decide what he (or she) will be able to do, and then, write our code.

Properties

  • The warrior will have a name.
  • 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 a 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/her 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 battle. 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 project and add properties to it accordingly. Let's also create a constructor for the properties, it won't be difficult. I'll leave out comments this time, you can add them by yourself as always. I won't write them for other methods as well so that the tutorial won't get unnecessary too long and remains clear. All the properties will be private:

class Warrior(private val name: String, private var health: Int, private val damage: Int,
    private val defense: Int, private val die: RollingDie) {

    private val maxHealth = health

}

Of course, the RollingDie class has to be in our project.

Notice that the max health can be derived so it's not declared in the class header. We assume that the warrior has a full health once he's created, so all we need to know is its health and the maximum health will be the same.

Methods

Let's move to the methods. 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 it's name every time something happens. We can use the toString() method which will return the name of our warrior.
  • Then, we would need a method that returns whether a 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 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 toString(), alive() and healthBar(). We'll start with toString(). Which should look familiar, since we did the exact same thing last time:

override fun toString(): 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:

fun alive(): Boolean {
    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:

fun alive(): Boolean {
    return (alive > 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:

fun healthBar(): String {
    var s = "["
    val total = 20
    var count = round((health.toDouble()/maxHealth) * total).toInt()
    if ((count == 0) && (alive()))
        count = 1
    s = s.padEnd(count + s.length, '#')
    s = s.padEnd(total - count + s.length, ' ')
    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 and cast one of the operands to Double, so Kotlin understood the division should result in real numbers.

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 use the padEnd() method which adds a given character to the end of a String as many times as to make it of a specified length. We add the indicator characters like that and the empty characters as well.

Now we'll put our classes to the test! We'll go to the main() method 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 its health bar:

val die = RollingDie(10)
val warrior = Warrior("Zalgoren", 100, 20, 10, die)
println("Warrior: $warrior") // toString() test
println("Alive: ${warrior.alive()}") // alive() test
println("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:

fun defend(hit: Int) {
    val injury = 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 a 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 Kotlin, 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:

fun attack(enemy: Warrior) {
    val hit = damage + 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:

val die = RollingDie(10)
val warrior = Warrior("Zalgoren", 100, 20, 10, die)
println("Warrior: $warrior") // toString() test
println("Alive: ${warrior.alive()}") // alive() test
println("Health: ${warrior.healthBar()}") // healthBar() test

warrior.attack(warrior) // attack test
println("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 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 class properties:

private var message = ""

Now, let's create the two methods. Private setMessage() will take a string as a parameter and set the message to the private property:

private fun setMessage(message: String) {
    this.message = message
}

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

fun getLastMessage(): String {
    return message
}

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

fun attack(enemy: Warrior) {
    val hit = damage + die.roll()
    setMessage("$name attacks with a hit worth $hit hp")
    enemy.defend(hit)
}

fun defend(hit: Int) {
    val injury = hit - (defense + die.roll())
    if (health > 0) {
        health -= injury
        message = "$name defended against the attack but still lost $injury hp"
        if (health <= 0)
            health = 0
    } else
        setMessage("$name blocked the hit")
    setMessage(message)
}

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

val die = RollingDie(10)
val warrior = Warrior("Zalgoren", 100, 20, 10, die)

println("Health after attack: ${warrior.healthBar()}") // healthBar() test

// warrior attack phase
val enemy = Warrior("Shadow", 60, 18, 15, die)
enemy.attack(warrior)

println(enemy.getLastMessage())

println(warrior.getLastMessage())

println("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 Kotlin, we'll create the arena.


 

Previous article
Reference data types in Kotlin
All articles in this section
Object-Oriented Programming in Kotlin
Skip article
(not recommended)
Arena with warriors in Kotlin
Article has been written for you by Samuel Kodytek
Avatar
User rating:
1 votes
I'm intereseted in JVM languages and Swift
Activities