Lesson 15.3: Building a “functional” inventory class

I was going to do internationalization for this lesson but found out it’s very different nowadays. So, I’ll need to learn the new method before I can make that change.

Instead, we’ll create an Inventory class. We can build some better logic into it, along with adding unit testing. Previously, I had a bug when trying to group items. With better testing, we would have caught that error.

In the future, we might want to add more inventory features, like limited inventory space, stacking similar items, maximum weight, etc. With a dedicated Inventory class, we’ll have all that logic in one place.

We’ll use a new technique with this Inventory class. I will be more like a “functional” language, like F#.

One aspect of functional languages is that they don’t modify state – they don’t change an object’s property values. This is called “immutability”. Instead, they create a new instance of the object, with the modified value. This can slow performance. But, for a small program like this, we won’t notice.

So, why are we doing something that will slow down the game, even if we shouldn’t ever notice the difference? Because programming in a functional style will make our program much easier to understand and test. Our “change” functions become service functions that take in an object and return an object – without modifying anything else. There are no side effects.

This might not be clear now. But, when you see the final code, with 100% unit test coverage, you should have a better idea of how (and why) to write C# code in a functional style.

 

 

 

Lesson Steps

Step 1: Create new class Engine\Models\Inventory.cs

This class is going to replace our Player.Inventory property, whose datatype is currently List<GameItem>.

I added regions to the class, to hopefully make it a little easier to understand what the class is doing.

Lines 9 to 17 has our backing variables, _backingInventory and _backingGroupedInventoryItems. These will hold the items in the inventory, which will be exposed by readonly properties on lines 19 through 34.

Notice that the properties are all readonly. The item lists are all returned as ReadOnly collections. This prevents us from directly adding items to, or removing items from, the inventory. We also have the Boolean HasConsumable property, which is an expression-bodied property with no setter. Again, keeping the values in this object immutable.

Line 36 through 53 have our constructor, which takes a list of items as a parameter. This is the only way to set the backing values – the inventory lists.

When we want to add an item to the inventory, we create a new Inventory object, passing in the list of the current Inventory’s items, plus the new item(s) to add. If we want to remove an item from an Inventory, we get a list of the current Inventory’s items, remove the item(s) we want to get rid of, and create a new Inventory object with the shorter list of items.

After creating the new Inventory object, with the longer or shorter list of items, we’ll set the Player’s Inventory property (now an Inventory datatype, instead of a List<GameItem>) to the new/updated Inventory object.

When we do this, and raise a property changed notification, the UI will know there is a new Inventory, with new values, and update the data-bound labels.

The UI will also refresh all the properties of the Inventory object. So, we don’t need to raise individual property changed notifications for GroupedInventory, Weapons, Consumables, and HasConsumable. This will be helpful if we add more properties in the future, like Armor, Jewelry, etc.

On lines 55 through 62, we have the public function HasAllTheseItems. This acts like a property, returning a Boolean result. But it takes in a list of items to check for. It’s still a read-only function that doesn’t modify the backing values – keeping our object immutable.

Lines 64 through 84 just has a private function to populate the backing variable for the GroupedInventory, which is called from the constructor when we instantiate a new Inventory object with GameItems.

 

Inventory.cs

 

 

Step 2: Create new Engine\Services\InventoryService.cs

This is where we hold the functions to create new Inventory objects, with more or fewer items. I wrote the functions as extension functions, so we can call them from an Inventory object. These functions return new Inventory objects, which we will put into the Player’s Inventory property.

All the AddItem functions ultimately call the version on line 24, which instantiates a new Inventory object with the current Inventory object’s items concatenated with the list of new items.

The RemoveItems functions create a working list of the current items, remove any items passed into the RemoveItems function, and creates a new Inventory object with the shortened list of GameItems.

I also added the ItemsThatAre() function on line 85. It’s not like the other functions in this class. It’s used to get a list of GameItems in the Inventory that are a certain type (Weapon, Consumable, etc.). This could go into a general-purpose extension method class. But, because it’s only one short function, I didn’t want to create a new class for it.

If we start to have more extension methods for the Inventory class, we’ll create a new class then.

 

InventoryService.cs

 

 

Step 3: Modify Engine\Models\LivingEntity.cs

Now that we have a nice, clean Inventory class, we’ll get rid of the inventory functions from the LivingEntity class.

We add and delete a lot of code for this change, and the line numbers will change. So, I’m going to try to describe the general areas and functions where to make the change.

First, in the “using” sections at the top, add “using Engine.Services;” You can also remove the using directives for System.Collections.ObjectModel and System.Linq.

Next, delete the following properties: Inventory, GroupedInventory, Weapons, Consumables, and HasConsumable.

Then, add the new backing variable “_inventory” (line 18, in the code below) and the Inventory property that exposes it (lines 70-78 below). Notice that the setter for the Inventory property is private. This is another way to prevent modifying the Player’s inventory without going through the new functional methods.

In the constructor, remove the two lines that set the values of the Inventory and GroupedInventory properties and add the new line that populates the new Inventory property (line 138 below).

Finally, replace all the inventory-changing code in AddItemToInventory(), RemoveItemFromInventory(), and RemoveItemsFromInventory() with calls to the new InventoryService extension methods. These methods return the new, modified Inventory object that we will populate into the Player’s Inventory property (which forces the UI to refresh, from the property changed notification).

 

LivingEntity.cs

 

 

Step 4: Modify Engine\ViewModels\GameSession.cs

We need to make a few changes to the GameSession class.

 

On line 127 (where we check if the player does not have a weapon), change:

“!CurrentPlayer.Weapons.Any()” to “!CurrentPlayer.Inventory.Weapons.Any()”

 

On line 185 (where we see if the player has the items to complete a quest), change:

“CurrentPlayer.HasAllTheseItems” to “CurrentPlayer.Inventory.HasAllTheseItems”

 

On line 284 (where we see if the player has all the ingredients to craft an item), change:

“CurrentPlayer.HasAllTheseItems” to “CurrentPlayer.Inventory.HasAllTheseItems”

 

On line 338 (where we get the defeated monster’s loot items), change:

“CurrentMonster.Inventory” to “CurrentMonster.Inventory.Items”

 

GameSession.cs

 

 

Step 5: Create TestEngine\Models\TestInventory.cs

I created this class while writing the code, since unit tests help me make sure I don’t make any breaking changes. But you can add your copy now.

These unit tests try out the different ways to add and remove items in an Inventory object. They also test a possible error condition, if we try to remove more items than the player has in their inventory.

The nice thing about these tests is that they test 100% of the code. So, if we make changes in the future, we can run the tests and be reasonably-confident our change didn’t break any existing behavior.

If we find any problems with the Inventory or InventoryService code, we can add another test here.

You can see in the tests how the “functional” inventory is easy to test. We don’t need to create a Player object, or any other infrastructure – just so we can test one two classes. This is one of the cool things about functional programming.

 

TestInventory.cs

 

 

Step 6: Modify WPFUI\MainWindow.xaml

Now that we’ve moved some of the inventory-related properties into the Inventory class, we need to update the bindings in the XAML.

 

On line 273 (the weapon combobox), change:

“CurrentPlayer.Weapons” to “CurrentPlayer.Inventory.Weapons”

 

On lines 284 and 285 (the consumable combobox), change:

“CurrentPlayer.HasConsumables” to “CurrentPlayer.Inventory.HasConsumables”

“CurrentPlayer.Consumables” to “CurrentPlayer.Inventory.Consumables”

 

On line 290 (the consumables “Use” button”, change:

“CurrentPlayer.HasConsumables” to “CurrentPlayer.Inventory.HasConsumables”

 

Step 7: Modify WPFUI\TradeScreen.xaml

We also need to update the inventory bindings in the trade screen.

 

On line 40 (the player’s inventory datagrid ItemsSource), change:

“CurrentPlayer.GroupedInventory” to “CurrentPlayer.Inventory.GroupedInventory”

 

On line 75 (the trader’s inventory datagrid ItemsSource), change:

“CurrentTrader.GroupedInventory” to “CurrentTrader.Inventory.GroupedInventory”

 

TEST THE GAME

We made a lot of changes, so let’s test the game. Everything should work the same as before.

 

Additional links for this project

Source code: https://github.com/ScottLilly/SOSCSRPG

Project plan: https://github.com/ScottLilly/SOSCSRPG/projects/1

Discord: https://discord.gg/AUYXYtH

Return to main page

Leave a Reply

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