Lesson 5 - Warrior for the arena in VB.NET
In the previous exercise, Solved tasks for OOP in VB .NET lesson 4, we've practiced our knowledge from previous lessons.
In the previous lesson, Solved tasks for OOP in VB .NET lesson 4, we went over the differences between reference and value data types in Visual Basic .NET. 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 decide what he (or she) will be able to do, and then we'll write our code.
Fields
The warrior will have a name and 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 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 rolling die instance. We will always roll the die and add a particular random number to his 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 fight. 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
arena project and add fields to it accordingly. All of them will be
Private
:
Public Class Warrior ''' <summary> ''' Warrior's name ''' </summary> Private name As String ''' <summary> ''' Health in HP ''' </summary> Private health As Integer ''' <summary> ''' Maximum health in HP ''' </summary> Private maxHealth As Integer ''' <summary> ''' Damage in HP ''' </summary> Private damage As Integer ''' <summary> ''' Defense in HP ''' </summary> Private defense As Integer ''' <summary> ''' The rolling die instance ''' </summary> Private die As RollingDie End Class
We can collapse the comments so they don't take extra space. Just don't forget to add them! They help keep the code readable and neat. Of course, the RollingDie class has to be in our project.
Methods
Let's start off by creating a constructor for the fields. I'll omit further comments in the article to make it clear and short. Just don't forget to add them to your project in the same way as we did to the fields above.
Public Sub New(name As String, health As Integer, damage As Integer, defense As Integer, die As RollingDie) Me.name = name Me.health = health Me.maxHealth = health Me.damage = damage Me.defense = defense Me.die = die End Sub
We assume that the warrior has a full health once he's created, so the
constructor doesn't need a maxHealth
parameter. It's easier to just
set the maxHealth to whatever the starting health is.
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 his 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 the 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'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:
Public Overrides Function ToString() As String Return name End Function
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:
Public Function Alive() As Boolean If health > 0 Then Return True Else Return False End If End Function
Due to the fact that the expression health > 0
is actually a
logical value, we can return it and the code will become shorter:
Public Function Alive() As Boolean Return health > 0 End Function
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:
Public Function HealthBar() As String Dim s As String = "[" Dim total As Integer = 20 Dim count As Double = Math.Round((health / maxHealth) * total) If count = 0 AndAlso Alive() Then count = 1 End If For i As Integer = 1 To count s &= "#" Next s = s.PadRight(total + 1) s &= "]" Return s End Function
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.
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 concatenate the right number of indicator characters to the String s, using a for loop. We'll add spaces to fill the empty part of the health bar. Adding spaces is easy, we just use PadRight() and length + 1, the "+1 character" is added because of the leading "[". Finally, we add the trailing bracket and return the string.
Now we'll put our classes to the test! We'll go to Module1.vb 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 his health bar:
{VBNET_OOP}
Public Class RollingDie
Private random As Random
Private sidesCount As Integer
Public Sub New()
sidesCount = 6
random = New Random()
End Sub
Public Sub New(sidesCount As Integer)
Me.sidesCount = sidesCount
random = New Random()
End Sub
Public Function GetSidesCount() As Integer
Return sidesCount
End Function
Public Function Roll() As Integer
Return random.Next(1, sidesCount + 1)
End Function
Public Overrides Function ToString() As String
Return String.Format("Rolling die with {0} sides", sidesCount)
End Function
End Class
Public Class Warrior
Private name As String
Private health As Integer
Private maxHealth As Integer
Private damage As Integer
Private defense As Integer
Private die As RollingDie
Public Sub New(name As String, health As Integer, damage As Integer, defense As Integer, die As RollingDie)
Me.name = name
Me.health = health
Me.maxHealth = health
Me.damage = damage
Me.defense = defense
Me.die = die
End Sub
Public Overrides Function ToString() As String
Return name
End Function
Public Function Alive() As Boolean
Return health > 0
End Function
Public Function HealthBar() As String
Dim s As String = "["
Dim total As Integer = 20
Dim count As Double = Math.Round((health / maxHealth) * total)
If count = 0 AndAlso Alive() Then
count = 1
End If
For i As Integer = 1 To count
s &= "#"
Next
s = s.PadRight(total + 1)
s &= "]"
Return s
End Function
End Class
{VBNET_MAIN_BLOCK}
Dim die As RollingDie = New RollingDie(10)
Dim warrior As Warrior = New Warrior("Zalgoren", 100, 20, 10, die)
Console.WriteLine("Warrior: {0}", warrior) ' test ToString()
Console.WriteLine("Alive: {0}", warrior.Alive()) ' test Alive()
Console.WriteLine("health: {0}", warrior.HealthBar()) ' test HealthBar()
{/VBNET_MAIN_BLOCK}
{/VBNET_OOP}
The output:
Console application
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:
Public Sub Defend(hit as Integer) Dim injury As Integer = hit - (defense + die.Roll()) If injury > 0 Then health -= injury If health <= 0 Then health = 0 End If End If End Sub
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 the 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 VB.NET, 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:
Public Sub Attack(enemy As Warrior) Dim hit As Integer = damage + die.Roll() enemy.Defend(hit) End Sub
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:
{VBNET_OOP}
Public Class RollingDie
Private random As Random
Private sidesCount As Integer
Public Sub New()
sidesCount = 6
random = New Random()
End Sub
Public Sub New(sidesCount As Integer)
Me.sidesCount = sidesCount
random = New Random()
End Sub
Public Function GetSidesCount() As Integer
Return sidesCount
End Function
Public Function Roll() As Integer
Return random.Next(1, sidesCount + 1)
End Function
Public Overrides Function ToString() As String
Return String.Format("Rolling die with {0} sides", sidesCount)
End Function
End Class
Public Class Warrior
Private name As String
Private health As Integer
Private maxHealth As Integer
Private damage As Integer
Private defense As Integer
Private die As RollingDie
Public Sub New(name As String, health As Integer, damage As Integer, defense As Integer, die As RollingDie)
Me.name = name
Me.health = health
Me.maxHealth = health
Me.damage = damage
Me.defense = defense
Me.die = die
End Sub
Public Overrides Function ToString() As String
Return name
End Function
Public Function Alive() As Boolean
Return health > 0
End Function
Public Function HealthBar() As String
Dim s As String = "["
Dim total As Integer = 20
Dim count As Double = Math.Round((health / maxHealth) * total)
If count = 0 AndAlso Alive() Then
count = 1
End If
For i As Integer = 1 To count
s &= "#"
Next
s = s.PadRight(total + 1)
s &= "]"
Return s
End Function
Public Sub Defend(hit As Integer)
Dim injury As Integer = hit - (defense + die.Roll())
If injury > 0 Then
health -= injury
If health <= 0 Then
health = 0
End If
End If
End Sub
Public Sub Attack(enemy As Warrior)
Dim hit As Integer = damage + die.Roll()
enemy.Defend(hit)
End Sub
End Class
{VBNET_MAIN_BLOCK}
Dim die As RollingDie = New RollingDie(10)
Dim warrior As Warrior = New Warrior("Zalgoren", 100, 20, 10, die)
Console.WriteLine("Warrior: {0}", warrior) ' test ToString()
Console.WriteLine("Alive: {0}", warrior.Alive()) ' test Alive()
Console.WriteLine("health: {0}", warrior.HealthBar()) ' test graphicHealth()
warrior.Attack(warrior) ' attack test
Console.WriteLine("Health after the hit: {0}", warrior.HealthBar())
{/VBNET_MAIN_BLOCK}
{/VBNET_OOP}
The output:
Console application
Warrior: Zalgoren
Alive: True
health: [####################]
Health after the hit: [################## ]
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 fields:
Private message As String
Now, let's create the two methods. Private SetMessage() will take a String as a parameter and set the message to the private field:
Private Sub SetMessage(message As String) Me.message = message End Sub
There's nothing difficult about it. A public method for getting the message is easy, too:
Public Function GetLastMessage() As String Return message End Function
Let's upgrade our Attack() and Defend() methods to set the messages, now they look like this:
Public Sub Attack(enemy As Warrior) Dim hit As Integer = damage + die.Roll() SetMessage(String.Format("{0} attacks with a hit worth {1} hp", name, hit)) enemy.Defend(hit) End Sub Public Sub Defend(hit As Integer) Dim injury As Integer = hit - (defense + die.Roll()) If injury > 0 Then health -= injury message = String.Format("{0} defended against the attack but still lost {1} hp", name, injury) If health <= 0 Then health = 0 message &= " and died" End If Else message = String.Format("{0} blocked the hit", name) End If SetMessage(message) End Sub
Let's add a second warrior, just for completeness' sake:
{VBNET_OOP}
Public Class RollingDie
Private random As Random
Private sidesCount As Integer
Public Sub New()
sidesCount = 6
random = New Random()
End Sub
Public Sub New(sidesCount As Integer)
Me.sidesCount = sidesCount
random = New Random()
End Sub
Public Function GetSidesCount() As Integer
Return sidesCount
End Function
Public Function Roll() As Integer
Return random.Next(1, sidesCount + 1)
End Function
Public Overrides Function ToString() As String
Return String.Format("Rolling die with {0} sides", sidesCount)
End Function
End Class
Public Class Warrior
Private name As String
Private health As Integer
Private maxHealth As Integer
Private damage As Integer
Private defense As Integer
Private die As RollingDie
Private message As String
Public Sub New(name As String, health As Integer, damage As Integer, defense As Integer, die As RollingDie)
Me.name = name
Me.health = health
Me.maxHealth = health
Me.damage = damage
Me.defense = defense
Me.die = die
End Sub
Public Overrides Function ToString() As String
Return name
End Function
Public Function Alive() As Boolean
Return health > 0
End Function
Public Function HealthBar() As String
Dim s As String = "["
Dim total As Integer = 20
Dim count As Double = Math.Round((health / maxHealth) * total)
If count = 0 AndAlso Alive() Then
count = 1
End If
For i As Integer = 1 To count
s &= "#"
Next
s = s.PadRight(total + 1)
s &= "]"
Return s
End Function
Public Sub Attack(enemy As Warrior)
Dim hit As Integer = damage + die.Roll()
SetMessage(String.Format("{0} attacks with a hit worth {1} hp", name, hit))
enemy.Defend(hit)
End Sub
Public Sub Defend(hit As Integer)
Dim injury As Integer = hit - (defense + die.Roll())
If injury > 0 Then
health -= injury
message = String.Format("{0} defended against the attack but still lost {1} hp", name, injury)
If health <= 0 Then
health = 0
message &= " and died"
End If
Else
message = String.Format("{0} blocked the hit", name)
End If
SetMessage(message)
End Sub
Private Sub SetMessage(message As String)
Me.message = message
End Sub
Public Function GetLastMessage() As String
Return message
End Function
End Class
{VBNET_MAIN_BLOCK}
Dim die As RollingDie = New RollingDie(10)
Dim warrior As Warrior = New Warrior("Zalgoren", 100, 20, 10, die)
Console.WriteLine("Health: {0}", warrior.HealthBar()) ' test HealthBar()
' warrior attack phase
Dim enemy As Warrior = New Warrior("Shadow", 60, 18, 15, die)
enemy.Attack(warrior)
Console.WriteLine(enemy.GetLastMessage())
Console.WriteLine(warrior.GetLastMessage())
Console.WriteLine("Health: {0}", warrior.HealthBar())
Console.ReadKey()
{/VBNET_MAIN_BLOCK}
{/VBNET_OOP}
Console application
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 VB.NET, we'll create the arena.