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

Lesson 8 - Arena with a mage in Kotlin (inheritance and polymorphism)

In the previous lesson, Inheritance and polymorphism in Kotlin, we went over inheritance and polymorphism in Kotlin. 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'll be more engaging and fun. :)

Mage - Object-Oriented Programming in Kotlin

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'll 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.kt class. Since we want to inherit it from the Warrior class, we have to tell Kotlin that Warrior can be inherited first. We'll do that using the open modifier:

The beginning of the Warrior class:

open class Warrior(private val name: String, private var health: Int, private val damage: Int, private val defense: Int, private val die: RollingDie) {
    // the rest of the implementation...
}

To the Mage class, we'll add extra properties in addition to the ones from the Warrior. The Mage class could look like this for now:

class Mage : Warrior {
    private val mana: Int
    private val maxMana: Int
    private val magicDamage: Int
}

We won't be able to compile the code yet, because we haven't created a constructor.

We don't have access to all the warrior variables in the Mage class because we still have the properties in the Warrior class set to private. We'll have to change the private property modifiers to protected in the Warrior class. All we really need now is the die and the name properties. Either way, we'll set all of the warrior's properties to protected because they might come in handy for future descendants. On second thought, it wouldn't be wise to set the message property as protected since it's not related to the warrior directly.

With all of that in mind, the class would look something like this:

open class Warrior(protected val name: String, protected var health: Int, protected val damage: Int, protected val defence: Int, protected val die: RollingDie) {
    // ...
}

Moving on to the constructor.

Descendant constructor

Kotlin does not inherit constructors! This is probably because it assumes the descendant will have extra properties and would make the original constructor irrelevant. Which is correct in our case, since the mage's constructor will have 2 extra parameters (mana and 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 parent constructor will be executed before our constructor and we can call it using the : Warrior(...) syntax, passing the parameters needed.

The mage constructor will look like this:

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

    private val maxMana: Int

    init {
        maxMana = mana
    }
}

Our constructor has now all parameters the parent needs and the new ones, that the child needs, as well. We'll pass some of them to the parent and process some of them by ourselves.

Now, let's switch to Main.kt and change the second warrior (Shadow) to a mage. Like this:

val gandalf: Warrior = Mage("Gandalf", 60, 15, 12, die, 30, 45)

We'll also have to change the line where we put the warrior in the arena. Note that we are still able to store the mage into a variable of the Warrior type because it's its ancestor. We could also change the variable type to Mage. When you run the application now, it'll work exactly as it did before. Mage inherits everything from the warrior and behaves just like a warrior.

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.

Talking about methods, we'll certainly need the setMessage() method from Warrior.kt which is private now. Let's make it protected:

protected fun setMessage(message: String) {

When you create a class you should always consider whether it would have descendants and therefore mark appropriate properties and methods as protected. 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. :)

Let's overwrite the attack() method of the warrior in the mage. We must first mark the method as open in the parent:

open fun attack(enemy: Warrior) {

Now we'll declare the method in Mage.kt as we're used to and use the override keyword with it:

override fun attack(enemy: Warrior) {

Similarly, we have overridden the toString() method in our objects earlier.

Our descendant's attack() method won't be all that different. 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.

override fun attack(enemy: Warrior) {
    var hit = 0
    // Mana isn't full
    if (mana < maxMana) {
        mana += 10
        if (mana > maxMana) {
            mana = maxMana
        }
        hit = damage + die.roll()
        setMessage("$name attacks with a hit worth $hit hp")
    } else {
        hit = magicDamage + die.roll()
        setMessage("$name used magic for $hit hp")
        mana = 0
    }
    enemy.defend(hit)
}

Notice how we limit 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.

In Kotlin, there's also the super keyword similar to this we already know. Unlike this, which refers to a particular class instance, super refers to the parent**. That means we can call parent methods even though the child has overridden them.

Certainly, it'd be better to just call the ancestor's attack() instead of copying its behavior. We'll use the super keyword to do just that:

override fun attack(enemy: Warrior) {
    // Mana isn't full
    if (mana < maxMana) {
        mana += 10
        if (mana > maxMana) {
            mana = maxMana
        }
        super.attack(enemy)
    } enemy { // Magic attack
        val hit = magicDamage + die.roll()
        setMessage("$name used magic for $hit hp")
        enemy.defend(hit)
        mana = 0
    }
}

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.

The application now works as expected.

-------------- 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 manaBar(). It will return a String with a graphical mana indicator.

We'll modify the healthBar() method in Warrior.kt to avoid writing the same graphical bar logic twice. Let me remind us how the original method looks:

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 += "]"
    s.length
    return s
}

The health 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 graphicalBar() and give it two parameters: current value and maximum value. We'll rename the health and maxHealth variables to current and maximum. We'll also make the method protected so we could use it in descendants:

protected fun graphicalBar(current: Int, maximum: Int): String {
    var s = "["
    val total = 20
    var count = round((current.toDouble()/maximum) * total).toInt()
    if ((count == 0) && (alive()))
        count = 1
    s = s.padEnd(count + s.length, '#')
    s = s.padEnd(total - count + s.length, ' ')
    s += "]"
    s.length
    return s
}

Let's implement the healthBar() method in Warrior.kt again. It'll be a one-liner now. All we have to do now is call the graphicalBar() method and fill the parameters accordingly:

fun healthBar(): String {
    return graphicalBar(health, maxHealth)
}

Of course, I could add the graphicalBar() method in the Warrior class like I did with attack() 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.

Now, we can easily draw graphical bars as needed. Let's move to Mage.kt and implement the manaBar() method:

fun manaBar(): String {
    return graphicalBar(mana, maxMana)
}

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 Arena.

Recognizing the object type

We'll add a separate printing method for warriors, printWarrior(), to keep things nice and neat. Its parameter will be a Warrior instance:

private fun printWarrior(w: Warrior) {
    println(w)
    print("Health: ")
    println(w.healthBar())
}

Now, let's tell it to show the mana bar if the warrior is a mage. We'll use the is operator to do just that:

private fun printWarrior(w: Warrior) {
    println(w)
    print("Health: ")
    println(w.healthBar())
    if (w is Mage)
        print("Mana: ${w.manaBar()}") // Kotlin is smart, casting the Warrior to Mage automatically
}

This is it, we'll cal printWarrior() in the render() method, which now looks like this:

private fun render() {
    println("-------------- Arena -------------- \n")
    println("Warriors: \n")
    printWarrior(warrior1)

    println()
    printWarrior(warrior2);

    println("\n")
}

Done :)

-------------- Arena --------------

Warriors:

Zalgoren
Health: [####                ]

Gandalf
Health: [#######             ]
Mana:  [#                   ]

Gandalf used magic and took 48 hp off
Zalgoren defended against the attack but still lost 33 hp

I added an ASCIIart Arena heading that I made using this application, http://patorjk.com/software/taag. I also modified the rendering method of the indicators so it prints filled rectangles instead of # (you can type full rectangles using Alt + 219). Mine looks like this now, but you could do whatever you want with yours:

                __    ____  ____  _  _    __
               /__\  (  _ \( ___)( \( )  /__\
              /(__)\  )   / )__)  )  (  /(__)\
             (__)(__)(_)\_)(____)(_)\_)(__)(__)
Warriors:

Zalgoren
Health: ████░░░░░░░░░░░░░░░░

Gandalf
Health: ███████░░░░░░░░░░░░░
Mana:   █░░░░░░░░░░░░░░░░░░░

Gandalf used magic and took 48 hp off
Zalgoren received and blow and 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, Companion objects in Kotlin, we'll explain the concept of static class members.


 

Previous article
Inheritance and polymorphism in Kotlin
All articles in this section
Object-Oriented Programming in Kotlin
Skip article
(not recommended)
Companion objects 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