Lesson 8 - Arena with a mage in C# .NET (inheritance and polymorphism)
In the previous lesson, Inheritance and polymorphism in C# .NET, we went over inheritance and polymorphism in C# .NET. 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.
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.cs 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):
class Mage: 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
C# 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 C# .NET, there is a keyword known as base which is similar to this. Unlike this which refers to the current instance, base refers to the ancestor. Meaning, that we can call the ancestor constructor with the given parameters and initialize the mage as well. In C#, 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): base(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 base.
Now, let's switch to Program.cs 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.
In order to override a method, it must be marked as virtual in the ancestor. Which basically just tells C# to allow descendants to override this method. Now, the method header in Warrior.cs should look like this:
public virtual void Attack(Warrior enemy)
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 make appropriate protected fields and virtual methods. A method that can be overridden in the descendant must be marked with the 'virtual' keyword. 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.
The Attack() method in the warrior will be public virtual. We would normally re-declare the method in Mage.cs. However, thanks to the override keyword, 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.
public override void Attack(Warrior enemy)
Similarly, we have overridden the ToString() method in our objects earlier,
each object in C# is in fact implicitly inherited from the System.Object class
which contains four methods, one of them is ToString(). So we have to 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.
public override 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(String.Format("{0} attacks with a hit worth {1} hp", name, hit)); } else // Magic damage { hit = magicDamage + die.Roll(); SetMessage(String.Format("{0} used magic for {1} hp", name, hit)); 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 base keyword to do just that:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
namespace OnlineApp
{
class RollingDie
{
private Random random;
private int sidesCount;
public RollingDie()
{
sidesCount = 6;
random = new Random();
}
public RollingDie(int sidesCount)
{
this.sidesCount = sidesCount;
random = new Random();
}
public int GetSidesCount()
{
return sidesCount;
}
public int Roll()
{
return random.Next(1, sidesCount + 1);
}
public override string ToString()
{
return String.Format("Rolling die with {0} sides", sidesCount);
}
}
class Warrior
{
protected string name;
protected int health;
protected int maxHealth;
protected int damage;
protected int defense;
protected RollingDie die;
private string message;
public Warrior(string name, int health, int damage, int defense, RollingDie die)
{
this.name = name;
this.health = health;
this.maxHealth = health;
this.damage = damage;
this.defense = defense;
this.die = die;
}
public override string ToString()
{
return name;
}
public bool Alive()
{
return (health > 0);
}
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 += "#";
s = s.PadRight(total + 1);
s += "]";
return s;
}
public virtual void Attack(Warrior enemy)
{
int hit = damage + die.Roll();
SetMessage(String.Format("{0} attacks with a hit worth {1} hp", name, hit));
enemy.Defend(hit);
}
public void Defend(int hit)
{
int injury = hit - (defense + die.Roll());
if (injury > 0)
{
health -= injury;
message = String.Format("{0} defended against the attack but still lost {1} hp", name, injury);
if (health <= 0)
{
health = 0;
message += " and died";
}
} else
message = String.Format("{0} blocked the hit", name);
SetMessage(message);
}
protected void SetMessage(string message)
{
this.message = message;
}
public string GetLastMessage()
{
return message;
}
}
class Arena
{
private Warrior warrior1;
private Warrior warrior2;
private RollingDie die;
public Arena(Warrior warrior1, Warrior warrior2, RollingDie die)
{
this.warrior1 = warrior1;
this.warrior2 = warrior2;
this.die = die;
}
private void Render()
{
Console.Clear();
Console.WriteLine("-------------- Arena -------------- \n");
Console.WriteLine("Warriors health: \n");
Console.WriteLine("{0} {1}", warrior1, warrior1.HealthBar());
Console.WriteLine("{0} {1}", warrior2, warrior2.HealthBar());
}
private void PrintMessage(string message)
{
Console.WriteLine(message);
Thread.Sleep(500);
}
public void Fight()
{
// The original order
Warrior w1 = warrior1;
Warrior w2 = warrior2;
Console.WriteLine("Welcome to the Arena!");
Console.WriteLine("Today {0} will battle against {1}! \n", warrior1, warrior2);
// swapping the warriors
bool warrior2Starts = (die.Roll() <= die.GetSidesCount() / 2);
if (warrior2Starts)
{
w1 = warrior2;
w2 = warrior1;
}
Console.WriteLine("{0} goes first! \nLet the battle begin...", w1);
Console.ReadKey();
// fight loop
while (w1.Alive() && w2.Alive())
{
w1.Attack(w2);
Render();
PrintMessage(w1.GetLastMessage()); // attack message
PrintMessage(w2.GetLastMessage()); // defense message
if (w2.Alive())
{
w2.Attack(w1);
Render();
PrintMessage(w2.GetLastMessage()); // attack message
PrintMessage(w1.GetLastMessage()); // defense message
}
Console.WriteLine();
}
}
}
class Mage: Warrior
{
private int mana;
private int maxMana;
private int magicDamage;
public Mage(string name, int health, int damage, int defense, RollingDie die, int mana, int magicDamage): base(name, health, damage, defense, die)
{
this.mana = mana;
this.maxMana = mana;
this.magicDamage = magicDamage;
}
public override void Attack(Warrior enemy)
{
// Mana isn't full
if (mana < maxMana)
{
mana += 10;
if (mana > maxMana)
mana = maxMana;
base.Attack(enemy);
}
else // Magic damage
{
int hit = magicDamage + die.Roll();
SetMessage(String.Format("{0} used magic and took {1} hp off", name, hit));
enemy.Defend(hit);
mana = 0;
}
}
}
{CSHARP_MAIN_BLOCK_START}
// creating objects
RollingDie die = new RollingDie(10);
Warrior zalgoren = new Warrior("Zalgoren", 100, 20, 10, die);
Warrior gandalf = new Mage("Gandalf", 60, 15, 12, die, 30, 45);
Arena arena = new Arena(zalgoren, gandalf, die);
// fight
arena.Fight();
Console.ReadKey();
{CSHARP_MAIN_BLOCK_END}
{CSHARP_OOP_END}
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 name it ManaBar(). It will return a string with a graphical mana indicator.
We'll modify the HealthBar() method in Warrior.cs 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 += "#"; s = s.PadRight(total + 1); s += "]"; return s; }
Changed from "doesn't really dependend"
The HealthBar()
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 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 += "#"; s = s.PadRight(total + 1); s += "]"; return s; }
Let's implement the HealthBar() method in Warrior.cs 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.cs 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.cs.
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) { Console.WriteLine(w); Console.Write("Health: "); Console.WriteLine(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 void PrintWarrior(Warrior w) { Console.WriteLine(w); Console.Write("Health: "); Console.WriteLine(w.HealthBar()); if (w is Mage) { Console.Write("Mana: "); Console.WriteLine(((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:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
namespace OnlineApp
{
class RollingDie
{
private Random random;
private int sidesCount;
public RollingDie()
{
sidesCount = 6;
random = new Random();
}
public RollingDie(int sidesCount)
{
this.sidesCount = sidesCount;
random = new Random();
}
public int GetSidesCount()
{
return sidesCount;
}
public int Roll()
{
return random.Next(1, sidesCount + 1);
}
public override string ToString()
{
return String.Format("Rolling die with {0} sides", sidesCount);
}
}
class Warrior
{
protected string name;
protected int health;
protected int maxHealth;
protected int damage;
protected int defense;
protected RollingDie die;
private string message;
public Warrior(string name, int health, int damage, int defense, RollingDie die)
{
this.name = name;
this.health = health;
this.maxHealth = health;
this.damage = damage;
this.defense = defense;
this.die = die;
}
public override string ToString()
{
return name;
}
public bool Alive()
{
return (health > 0);
}
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 += "#";
s = s.PadRight(total + 1);
s += "]";
return s;
}
public string HealthBar()
{
return GraphicalBar(health, maxHealth);
}
public virtual void Attack(Warrior enemy)
{
int hit = damage + die.Roll();
SetMessage(String.Format("{0} attacks with a hit worth {1} hp", name, hit));
enemy.Defend(hit);
}
public void Defend(int hit)
{
int injury = hit - (defense + die.Roll());
if (injury > 0)
{
health -= injury;
message = String.Format("{0} defended against the attack but still lost {1} hp", name, injury);
if (health <= 0)
{
health = 0;
message += " and died";
}
} else
message = String.Format("{0} blocked the hit", name);
SetMessage(message);
}
protected void SetMessage(string message)
{
this.message = message;
}
public string GetLastMessage()
{
return message;
}
}
class Arena
{
private Warrior warrior1;
private Warrior warrior2;
private RollingDie die;
public Arena(Warrior warrior1, Warrior warrior2, RollingDie die)
{
this.warrior1 = warrior1;
this.warrior2 = warrior2;
this.die = die;
}
private void Render()
{
Console.Clear();
Console.WriteLine("-------------- Arena -------------- \n");
Console.WriteLine("Warriors: \n");
PrintWarrior(warrior1);
Console.WriteLine();
PrintWarrior(warrior2);
Console.WriteLine();
}
private void PrintMessage(string message)
{
Console.WriteLine(message);
Thread.Sleep(500);
}
public void Fight()
{
// The original order
Warrior w1 = warrior1;
Warrior w2 = warrior2;
Console.WriteLine("Welcome to the Arena!");
Console.WriteLine("Today {0} will battle against {1}! \n", warrior1, warrior2);
// swapping the warriors
bool warrior2Starts = (die.Roll() <= die.GetSidesCount() / 2);
if (warrior2Starts)
{
w1 = warrior2;
w2 = warrior1;
}
Console.WriteLine("{0} goes first! \nLet the battle begin...", w1);
Console.ReadKey();
// fight loop
while (w1.Alive() && w2.Alive())
{
w1.Attack(w2);
Render();
PrintMessage(w1.GetLastMessage()); // attack message
PrintMessage(w2.GetLastMessage()); // defense message
if (w2.Alive())
{
w2.Attack(w1);
Render();
PrintMessage(w2.GetLastMessage()); // attack message
PrintMessage(w1.GetLastMessage()); // defense message
}
Console.WriteLine();
}
}
private void PrintWarrior(Warrior w)
{
Console.WriteLine(w);
Console.Write("Health: ");
Console.WriteLine(w.HealthBar());
if (w is Mage)
{
Console.Write("Mana: ");
Console.WriteLine(((Mage)w).ManaBar());
}
}
}
class Mage: Warrior
{
private int mana;
private int maxMana;
private int magicDamage;
public Mage(string name, int health, int damage, int defense, RollingDie die, int mana, int magicDamage): base(name, health, damage, defense, die)
{
this.mana = mana;
this.maxMana = mana;
this.magicDamage = magicDamage;
}
public string ManaBar()
{
return GraphicalBar(mana, maxMana);
}
public override void Attack(Warrior enemy)
{
// Mana isn't full
if (mana < maxMana)
{
mana += 10;
if (mana > maxMana)
mana = maxMana;
base.Attack(enemy);
}
else // Magic damage
{
int hit = magicDamage + die.Roll();
SetMessage(String.Format("{0} used magic and took {1} hp off", name, hit));
enemy.Defend(hit);
mana = 0;
}
}
}
{CSHARP_MAIN_BLOCK_START}
// creating objects
RollingDie die = new RollingDie(10);
Warrior zalgoren = new Warrior("Zalgoren", 100, 20, 10, die);
Warrior gandalf = new Mage("Gandalf", 60, 15, 12, die, 30, 45);
Arena arena = new Arena(zalgoren, gandalf, die);
// fight
arena.Fight();
Console.ReadKey();
{CSHARP_MAIN_BLOCK_END}
{CSHARP_OOP_END}
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. Also, I colored the graphical bars using foreground and background colors. Lastly, I modified the rendering method of the indicators so it prints filled 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: <span style="color:red">████</span><span style="color:maroon">████████████████</span>
Gandalf
Health: <span style="color:red">███████</span><span style="color:maroon">█████████████</span>
Mana: <span style="color:blue">█</span><span style="color:navy">███████████████████</span>
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, Solved tasks for OOP in C# .NET lessons 5-8, we'll explain the concept of static class members.
In the following exercise, Solved tasks for OOP in C# .NET lessons 5-8, we're gonna practice our knowledge from previous lessons.
Did you have a problem with anything? Download the sample application below and compare it with your project, you will find the error easily.
Download
By downloading the following file, you agree to the license terms
Downloaded 73x (49.16 kB)
Application includes source codes in language C#