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.

 ?

 

 

Lesson Steps

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

Several months ago, I started a new project at a new client. We’re doing some cool things, but it’s been taking a lot of my time learning the existing applications and the technologies we’re using for the new applications. Plus, my daily commute is now around two hours, round-trip.

I’m mostly up to speed with the things I need to learn, and have some free time now, but am looking for ways to spend more of my time working on the code, and less time working on recording the lessons, editing them, etc.

One thing I’ve considered is doing live coding on Twitch and uploading the videos (unedited) to YouTube. Then, the posts on my website would mostly be the code and brief explanations of how and why the code changed in the video.

I’ve also 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.

If you have any ideas about how to manage the workload, so I can get more done with the game, please let me know.

Thanks!

 

Return to main page

Leave a Reply

Your email address will not be published. Required fields are marked *