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

Java OOP Arena with a mage in Java (inheritance and polymorphism)

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

Mage

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 will 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.java class, inherit it from the Warrior class and give it extra fields (that warriors don't have). I will look something like this (don't forget to add comments):

public class Mage extends Warrior {
        private int mana;
        private int maxMana;
        private int magicDamage;
}

We don't have access to all the warrior variables in the mage class because we still have the fields in the Warrior class set to private. We'll have to change the private field modifiers to protected in the Warrior class. All we really need now is the die and the name fields. Either way, we'll set all of the warrior's fields to protected because they might come in handy for future descendants. On second thought, it wouldn't be wise to set the message field as protected since it is not related to the warrior directly. With all of that in mind, the class would look something like this:

protected String name;
protected int health;
protected int maxHealth;
protected int damage;
protected int defense;
protected RollingDie die;
private String message;

...

Moving on to the constructor.

Descendant constructor

Java does not inherit constructors! This is probably because it assumes the descendant will have extra fields 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 only time we don't call the ancestor constructor is when there isn't one. Our constructor must have all needed parameters for an ancestor and the new ones that the descendant needs. The ancestor's constructor will be executed before the descendant's.

In Java, there is a keyword known as super which is similar to this. Unlike this which refers to the current instance, super refers to the ancestor. Meaning, that we can call the ancestor constructor with the given parameters and initialize the mage as well. In Java, calling the ancestor constructor has to be included in the method header.

Mage's constructor should look something like this:

public Mage(String name, int health, int damage, int defense, RollingDie die, int mana, int magicDamage) {
        super(name, health, damage, defense, die);
        this.mana = mana;
        this.maxMana = mana;
        this.magicDamage = magicDamage;
}

Note: we can call another constructor of the same class, not from the ancestor, using this instead of super.

Now, let's switch to ArenaFight.java and change the second warrior to a mage. Like this:

Warrior gandalf = new Mage("Gandalf", 60, 15, 12, die, 30, 45);

We will 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 is 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 a setMessage() method which is private now. We have to make it protected:

protected void setMessage(String message)

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

We would normally re-declare the method in Mage.java. However, we are able to override it now. All it does is indicate that we're aware the method was inherited, and that we want to change its behavior.

Let's override the mage's attack() method. We declare the method in Mage.java as we're used to and attach the @Override annotation to it:

@Override
public void attack(Warrior enemy)

Similarly, we have overridden the toString() method in our objects earlier, each object in Java is in fact implicitly inherited from the java.lang.Object class which contains several methods, one of them is toString(). So we should use @Override to re-implement it.

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
public void attack(Warrior enemy) {
        int 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 { // Magic damage
                hit = magicDamage + die.roll();
                setMessage(name + " used magic for " + hit + " hp");
                mana = 0;
        }
        enemy.defend(hit);
}

Notice how we limit the 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. 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
public void attack(Warrior enemy) {
        // Mana isn't full
        if (mana < maxMana) {
                mana += 10;
                if (mana > maxMana) {
                        mana = maxMana;
                }
                super.attack(enemy);
        }
        else { // Magic damage
                int hit = magicDamage + die.roll();
                setMessage(name + " used magic and took " + hit + " hp off");
                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.

Console application
-------------- 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 call it manaBar(). It will return a string with a graphical mana indicator.

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

public String healthBar() {
        String s = "[";
        int total = 20;
        double count = Math.round(((double)health / maxHealth) * total);
        if ((count == 0) && (alive())) {
                count = 1;
        }
        for (int i = 0; i < count; i++) {
                s += "#";
        }
        for (int i = 0; i < total - count; i++) {
                s += " ";
        }
        s += "]";
        return s;
}
Changed from "doesn't really dependend"

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 will also make the method protected so we could use it in descendants:

public String graphicalBar(int current, int maximum) {
        String s = "[";
        int total = 20;
        double count = Math.round(((double)current / maximum) * total);
        if ((count == 0) && (alive())) {
                count = 1;
        }
        for (int i = 0; i < count; i++) {
                s += "#";
        }
        for (int i = 0; i < total - count; i++) {
                s += " ";
        }
        s += "]";
        return s;
}

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

public String healthBar() {
        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.java and implement the manaBar() method:

public String manaBar() {
        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.java.

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 void printWarrior(Warrior w) {
        System.out.println(w);
        System.out.print("Health: ");
        System.out.println(w.healthBar());
}

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

private void printWarrior(Warrior w) {
        System.out.println(w);
        System.out.print("Health: ");
        System.out.println(w.healthBar());
        if (w instanceof Mage) {
                System.out.print("Mana: ");
                System.out.println(((Mage)w).manaBar());
        }
}

We had to cast our warrior to the mage type in order to access the manaBar() method. The warrior class doesn't have it. This is it, printWarrior() will be called in the render() method, which now looks like this:

private void render() {
        System.out.println("-------------- Arena -------------- \n");
        System.out.println("Warriors: \n");
        printWarrior(warrior1);
        System.out.println();
        printWarrior(warrior2);
        System.out.println();
}

Done :)

Console application
-------------- 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 in the indicators so it prints full rectangles instead of # (you could type full rectangles using Alt + 219). Mine looks like this now, but you could do whatever you want with yours:

Console application
                __    ____  ____  _  _    __
               /__\  (  _ \( ___)( \( )  /__\
              /(__)\  )   / )__)  )  (  /(__)\
             (__)(__)(_)\_)(____)(_)\_)(__)(__)
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, Static class members in Java, we'll explain the concept of static class members.


 

 

Article has been written for you by David Capka
Avatar
Do you like this article?
1 votes
The author is a programmer, who likes web technologies and being the lead/chief article writer at ICT.social. He shares his knowledge with the community and is always looking to improve. He believes that anyone can do what they set their mind to.
Unicorn College The author learned IT at the Unicorn College - a prestigious college providing education on IT and economics.
Activities (4)

 

 

Comments

Avatar
Sajjad Sajjad Khan:10. September 22:23

sorry sir i did not understand the arena game yet

 
Reply 10. September 22:23
To maintain the quality of discussion, we only allow registered members to comment. Sign in. If you're new, Sign up, it's free.

1 messages from 1 displayed.