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.

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.