Press "Enter" to skip to content

Lesson 15.1: Bug Fixes, Unit Tests, and Tooltips

Let’s fix a few of the bugs in the program and add some tooltips, to help the player. I’ll also talk a bit about the future of this project.

Step 1: Edit Engine\Actions\AttackWithWeapon.cs

In the constructor, the first thing we do is check that the parameters are not out of bounds (less than zero, or maximum damage is less than minimum damage). At least, that’s what I wanted to do.

Instead of checking the parameters sent in, the code is checking the backing variables that we assign the parameters to.

On lines 19 and 24, change “_minimumDamage” to “minimumDamage” and “_maximumDamage” to “maximumDamage” – remove the underscores.

AttackWithWeapon.cs
using System;
using Engine.Models;
namespace Engine.Actions
{
    public class AttackWithWeapon : BaseAction, IAction
    {
        private readonly int _maximumDamage;
        private readonly int _minimumDamage;
        public AttackWithWeapon(GameItem itemInUse, int minimumDamage, int maximumDamage) 
            : base(itemInUse)
        {
            if(itemInUse.Category != GameItem.ItemCategory.Weapon)
            {
                throw new ArgumentException($"{itemInUse.Name} is not a weapon");
            }
            if(minimumDamage < 0)
            {
                throw new ArgumentException("minimumDamage must be 0 or larger");
            }
            if(maximumDamage < minimumDamage)
            {
                throw new ArgumentException("maximumDamage must be >= minimumDamage");
            }
            _minimumDamage = minimumDamage;
            _maximumDamage = maximumDamage;
        }
        public void Execute(LivingEntity actor, LivingEntity target)
        {
            int damage = RandomNumberGenerator.NumberBetween(_minimumDamage, _maximumDamage);
            string actorName = (actor is Player) ? "You" : $"The {actor.Name.ToLower()}";
            string targetName = (target is Player) ? "you" : $"the {target.Name.ToLower()}";
            if(damage == 0)
            {
                ReportResult($"{actorName} missed {targetName}.");
            }
            else
            {
                ReportResult($"{actorName} hit {targetName} for {damage} point{(damage > 1 ? "s" : "")}.");
                target.TakeDamage(damage);
            }
        }
    }
}

Step 2: Create TestEngine\Actions\TestWithWeapon.cs

The bug in step 1 could have been avoided if I had unit tests. Those would have let me know my parameter checking wasn’t working.

So, let’s add some unit tests now.

In the TestEngine project, create an “Actions” folder, and add a unit test class named “TestAttackWithWeapon.cs”

I added four unit tests to this class.

The first one (Test_Constructor_GoodParameters) instantiates an AttackWithWeapon object, passing in good parameters. The Assert at the end of this test just makes sure the object is not null.

The next three test functions pass in bad parameters and expect to see an exception.

Notice the “[ExpectedException(typeof(ArgumentException))]” attribute in front of each function. That attribute is how we tell the unit test we’re expecting an exception when we run this test function.

TestAttackWithWeapon.cs
using System;
using Engine.Actions;
using Engine.Factories;
using Engine.Models;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace TestEngine.Actions
{
    [TestClass]
    public class TestAttackWithWeapon
    {
        [TestMethod]
        public void Test_Constructor_GoodParameters()
        {
            GameItem pointyStick = ItemFactory.CreateGameItem(1001);
            AttackWithWeapon attackWithWeapon = new AttackWithWeapon(pointyStick, 1, 5);
            Assert.IsNotNull(attackWithWeapon);
        }
        [TestMethod]
        [ExpectedException(typeof(ArgumentException))]
        public void Test_Constructor_ItemIsNotAWeapon()
        {
            GameItem granolaBar = ItemFactory.CreateGameItem(2001);
            // A granola bar is not a weapon.
            // So, the constructor should throw an exception.
            AttackWithWeapon attackWithWeapon = new AttackWithWeapon(granolaBar, 1, 5);
        }
        [TestMethod]
        [ExpectedException(typeof(ArgumentException))]
        public void Test_Constructor_MinimumDamageLessThanZero()
        {
            GameItem pointyStick = ItemFactory.CreateGameItem(1001);
            // This minimum damage value is less than 0.
            // So, the constructor should throw an exception.
            AttackWithWeapon attackWithWeapon = new AttackWithWeapon(pointyStick, -1, 5);
        }
        [TestMethod]
        [ExpectedException(typeof(ArgumentException))]
        public void Test_Constructor_MaximumDamageLessThanMinimumDamage()
        {
            GameItem pointyStick = ItemFactory.CreateGameItem(1001);
            // This maximum damage is less than the minimum damage.
            // So, the constructor should throw an exception.
            AttackWithWeapon attackWithWeapon = new AttackWithWeapon(pointyStick, 2, 1);
        }
    }
}

Step 3: Modify TestEngine\ViewModels\TestGameSession.cs

On line 15, fix the unit test so it checks that the location is named “Town Square” (with an upper-case “S”), not “Town square” (with a lower-case “s”).

TestGameSession.cs
using Engine.ViewModels;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace TestEngine.ViewModels
{
    [TestClass]
    public class TestGameSession
    {
        [TestMethod]
        public void TestCreateGameSession()
        {
            GameSession gameSession = new GameSession();
            Assert.IsNotNull(gameSession.CurrentPlayer);
            Assert.AreEqual("Town Square", gameSession.CurrentLocation.Name);
        }
        [TestMethod]
        public void TestPlayerMovesHomeAndIsCompletelyHealedOnKilled()
        {
            GameSession gameSession = new GameSession();
            gameSession.CurrentPlayer.TakeDamage(999);
            Assert.AreEqual("Home", gameSession.CurrentLocation.Name);
            Assert.AreEqual(gameSession.CurrentPlayer.Level * 10, gameSession.CurrentPlayer.CurrentHitPoints);
        }
    }
}

Step 4: Modify Engine\Models\ItemQuantity.cs

Let’s make the game more user-friendly by adding tool tips over the Quests and Recipes datagrids. This way, the player can hover their cursor over the name and see more information.

Add an expression-bodied property “QuantityItemDescription” (lines 10 and 11). We’ll use this to convert an ItemQuantity object into a friendlier text string, like “1 snake fang”.

Be sure to also include the “using Engine.Factories;” on line 1, so this class can get access to the ItemFactory class.

ItemQuantity.cs
using Engine.Factories;
namespace Engine.Models
{
    public class ItemQuantity
    {
        public int ItemID { get; }
        public int Quantity { get; }
        public string QuantityItemDescription => 
            $"{Quantity} {ItemFactory.ItemName(ItemID)}";
        public ItemQuantity(int itemID, int quantity)
        {
            ItemID = itemID;
            Quantity = quantity;
        }
    }
}

Step 5: Modify Engine\Models\Quest.cs

Add the expression-bodied property “ToolTipContents” to lines 20-30. This creates the string to display in the tooltip.

The “Environment.NewLine”s insert a line feed into the text, moving the following text onto a new line. For the List properties, we use “string.Join” to add an Environment.NewLine (the first parameter of string.Join) after each item’s QuantityItemDescription.

Quest.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Engine.Factories;
namespace Engine.Models
{
    public class Quest
    {
        public int ID { get; }
        public string Name { get; }
        public string Description { get; }
        public List<ItemQuantity> ItemsToComplete { get; }
        public int RewardExperiencePoints { get; }
        public int RewardGold { get; }
        public List<ItemQuantity> RewardItems { get; }
        public string ToolTipContents =>
            Description + Environment.NewLine + Environment.NewLine +
            "Items to complete the quest" + Environment.NewLine +
            "===========================" + Environment.NewLine +
            string.Join(Environment.NewLine, ItemsToComplete.Select(i => i.QuantityItemDescription)) +
            Environment.NewLine + Environment.NewLine +
            "Rewards\r\n" +
            "===========================" + Environment.NewLine +
            $"{RewardExperiencePoints} experience points" + Environment.NewLine +
            $"{RewardGold} gold pieces" + Environment.NewLine +
            string.Join(Environment.NewLine, RewardItems.Select(i => i.QuantityItemDescription));
        public Quest(int id, string name, string description, List<ItemQuantity> itemsToComplete,
                     int rewardExperiencePoints, int rewardGold, List<ItemQuantity> rewardItems)
        {
            ID = id;
            Name = name;
            Description = description;
            ItemsToComplete = itemsToComplete;
            RewardExperiencePoints = rewardExperiencePoints;
            RewardGold = rewardGold;
            RewardItems = rewardItems;
        }
    }
}

Step 6: Modify Engine\Models\Recipe.cs

We’ll do the same thing for the Recipes class, in the property “ToolTipContents”, on lines 14-21.

Recipe.cs
using System;
using System.Collections.Generic;
using System.Linq;
namespace Engine.Models
{
    public class Recipe
    {
        public int ID { get; }
        public string Name { get; }
        public List<ItemQuantity> Ingredients { get; } = new List<ItemQuantity>();
        public List<ItemQuantity> OutputItems { get; } = new List<ItemQuantity>();
        public string ToolTipContents =>
            "Ingredients" + Environment.NewLine +
            "===========" + Environment.NewLine +
            string.Join(Environment.NewLine, Ingredients.Select(i => i.QuantityItemDescription)) +
            Environment.NewLine + Environment.NewLine +
            "Creates" + Environment.NewLine +
            "===========" + Environment.NewLine +
            string.Join(Environment.NewLine, OutputItems.Select(i => i.QuantityItemDescription));
        public Recipe(int id, string name)
        {
            ID = id;
            Name = name;
        }
        public void AddIngredient(int itemID, int quantity)
        {
            if(!Ingredients.Any(x => x.ItemID == itemID))
            {
                Ingredients.Add(new ItemQuantity(itemID, quantity));
            }
        }
        public void AddOutputItem(int itemID, int quantity)
        {
            if(!OutputItems.Any(x => x.ItemID == itemID))
            {
                OutputItems.Add(new ItemQuantity(itemID, quantity));
            }
        }
    }
}

Step 7: Modify WPFUI\MainWindow.xaml

Finally, add the tooltips into the datagrid columns.

We add the Quest object’s tooltip at line 199, and the Recipe object’s tooltip at line 222 – both inside DatagridTextColumn.CellStyle sections.

MainWindow.xaml (starting at line 190 – Quests TabItem)

                <TabItem Header="Quests"
                         x:Name="QuestsTabItem">
                    <DataGrid ItemsSource="{Binding CurrentPlayer.Quests}"
                              AutoGenerateColumns="False"
                              HeadersVisibility="Column">
                        <DataGrid.Columns>
                            <DataGridTextColumn Header="Name"
                                                Binding="{Binding PlayerQuest.Name, Mode=OneWay}"
                                                Width="*">
                                <DataGridTextColumn.CellStyle>
                                    <Style TargetType="DataGridCell">
                                        <Setter Property="ToolTip" 
                                                Value="{Binding PlayerQuest.ToolTipContents}"/>
                                    </Style>
                                </DataGridTextColumn.CellStyle>
                            </DataGridTextColumn>
                            <DataGridTextColumn Header="Done?"
                                                Binding="{Binding IsCompleted, Mode=OneWay}"
                                                Width="Auto"/>
                        </DataGrid.Columns>
                    </DataGrid>
                </TabItem>
                <TabItem Header="Recipes"
                         x:Name="RecipesTabItem">
                    <DataGrid ItemsSource="{Binding CurrentPlayer.Recipes}"
                              AutoGenerateColumns="False"
                              HeadersVisibility="Column">
                        <DataGrid.Columns>
                            <DataGridTextColumn Header="Name"
                                                Binding="{Binding Name, Mode=OneWay}"
                                                Width="*">
                                <DataGridTextColumn.CellStyle>
                                    <Style TargetType="DataGridCell">
                                        <Setter Property="ToolTip" 
                                                Value="{Binding ToolTipContents}"/>
                                    </Style>
                                </DataGridTextColumn.CellStyle>
                            </DataGridTextColumn>
                            <DataGridTemplateColumn MinWidth="75">
                                <DataGridTemplateColumn.CellTemplate>
                                    <DataTemplate>
                                        <Button Click="OnClick_Craft"
                                                Width="55"
                                                Content="Craft"/>
                                    </DataTemplate>
                                </DataGridTemplateColumn.CellTemplate>
                            </DataGridTemplateColumn>
                        </DataGrid.Columns>
                    </DataGrid>
                </TabItem>

FUTURE PLANS

I’ve started a Discord channel at https://discord.gg/AUYXYtH. This might be a better way to get answers to questions you have. I can’t monitor it 24/7. But, just like most things, we can try it out and see how it works. If it helps, keep doing it. If it doesn’t, then stop.

NEXT LESSON: Lesson 15.2: Catch and log exceptions

PREVIOUS LESSON: Lesson 14.5: Move Remaining Game Data to XML Files

    Leave a Reply

    Your email address will not be published.