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
when I run the TestEngine I get the error: Message:
Test method did not throw expected exception System.ArgumentException.
I tried to add the parameters int minimumDamage, int maximumDamage to the test constructor to see if it would help but then I get another error:
Message:
Test method threw exception Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel.TestFailedException, but exception System.ArgumentException was expected. Exception message: Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel.TestFailedException: Only data driven test methods can have parameters. Did you intend to use [DataRow] or [DynamicData]?
I don’t know how to get rid of the error, and its the only code that makes an error.
Can you upload your solution (including the directories under it, and all the files in those directories) to GitHub, Dropbox, or some other file-sharing location so I can look at it?
I’m having a few problems getting my project to you. I’ve tried github and dropbox and this link is the best what I could do. I hope it works
REMOVED FOR PRIVACY
Hi Anthony,
The link you sent (I removed it, for privacy) was to the Dropbox files on your computer – which I cannot access. Here is how you can share a Dropbox folder: https://help.dropbox.com/share/share-with-others
I’ve tried a few different ways to do it, but I can’t get my code into dropbox. the files and folders are not accepted or maybe it’s too big for the free version. I have a github account but I find github confusing to use. maybe you can help me get my files onto github and I can send them to you, or allow you to view the code there? or maybe there is an easier way to do things?
Here’s a video that might help you upload the solution to GitHub: https://youtu.be/0si9ElYQv8I
sorry for the long wait, I was finally able to get my project uploaded to github. I accidently discovered the problem and ended up fixing it, turns out that for some reason the maximumDamage was staying higher then the minimumDamage, so when I changed the value to -1 it fixed the problem. I’m giving the link anyway if you would like or have the time to take a look. I hope the link works.
https://github.com/SDKargas/Clandestine
Cool! Thanks for sharing your code, too.