Rogue C# – Building the Inventory

In the last chapter, we laid out the properties for an Inventory class in our roguelike game that would cover everything from food to armor but there are a few more steps to building the inventory system. The inventory items have a complete cycle throughout this game, depending on the item, and each step has to be coded so that the inventory exists and is managed just like inventory items would be in real life. Every part of the program will be affected in some way.

This post is part of a series on creating a roguelike game in C#. For the latest chapters and more information, please visit the Official Project Page on ComeauSoftware.com. The current code for this project is also available on Github.

The Requirements

Let’s take a look at what happens with inventory in the original game.

  • Specifications: The specs for each item, such as a potion of Restore Strength, must be stored somewhere in the program so they can be used to instantiate objects.
  • Production: When each map is generated, it’s populated with a random selection of inventory items for the player to pick up. Each one of these items will represent an instance of the new Inventory class.
  • Acquisition: When the player moves onto the same space as an inventory item, the program must decide if the player’s inventory has room for it. If it does, the player picks it up. Otherwise, they see a message stating their inventory is full.
  • Storage: The player’s ability to carry stuff is limited so that needs to be enforced. The player should also be able to view what they’re carrying for reference and select from it.
  • Use and Disposal: If a player drops, throws or consumes an object, it needs to come out of inventory and either go back on the map or disappear. If they wear something like armor, it’s listed in the correct property of the Player class but also remains in their inventory because it’s something the player is carrying. When the player does use an item, each type does something different and the program needs a way to carry out those actions.
  • Expiration: If a player leaves an object on the map, it disappears when they move to the next level and the instantiated object needs to go away.

Specifications

The Algorithm

Every software project is different and this part of the project is certainly different from anything I’ve previously coded from scratch. It was the kind of challenge that made me appreciate the growing complexity of the project itself.

As I said earlier, we need to store the specifications for the different items and we need to do it in a way that allows new items to be easily added to the program. These specifications include both standard properties such as the name and whether its been identified yet. It also includes references to functions that will be called when the item is used. Each item type has a different one and there are more than a dozen types for many categories such as potions and scrolls.

I finally decided to use a List of Inventory objects that would act as templates for each new Inventory object. When the program creates a new inventory object, it does so through a function in the Inventory class which selects a random template from the list and returns the new item. The object will also hold references to the delegate functions to be run when the object is used.

The Code

First, we need a few more properties and a constructor for the Inventory class.

public string PluralName { get; set; }
public int AppearancePct { get; set; }
public char DisplayCharacter { get; set; }

The PluralName property simply supplies a plural form of the RealName, i.e. “mango” / “mangoes” and it will make it a bit easier when showing inventory descriptions. Different items have different chances of appearing on the map and the AppearancePct property can store that for each one instead of a bunch of constants. Finally the DisplayCharacter property will also avoid the need for constants when determining what to show for a specific item.

This brings us to the constructors. There are currently two overloaded constructors with the first one accepting parameters for all 19 of the properties now in the class and assigning them to the object properties. I put it in there just in case I want to build an object from scratch. The second one is slightly more interesting.

public Inventory(InvCategory InvType, string CodeName, string RealName, string PluralName, char DisplayChar, int AppearancePct,
Func<Player, Inventory?, bool>? mainFunction, 
Func<Player, MapLevel.Direction, bool>? Throw = null, 
Func<Player, MapLevel.Direction, bool>? Zap = null)
{
    // Apply parameters and most common settings
    this.ItemType = InvType;
    this.CodeName = CodeName;
    this.RealName = RealName;
    this.PluralName = PluralName;
    this.DisplayCharacter = DisplayChar;
    this.IsGroupable = true;
    this.AppearancePct = AppearancePct;

    // If it's a weapon, it's wieldable.
    this.IsWieldable = (InvType == InvCategory.Weapon);

    // If the two names are the same, it's identified.
    this.IsIdentified = (this.RealName == this.CodeName);

    this.MainFunction = mainFunction;
    this.ThrowFunction = (Throw != null) ? Throw : null;
    this.ZapFunction = (Zap != null) ? Zap : null;
}

This constructor takes parameters for the essential properties and the three possible delegate functions currently defined. In C#, integers are 0 by default and boolean properties are False by default so properties like IsIdentified and Increment don’t need to be specifically assigned values unless they differ from the default. Their default values work for the majority of items.

Also notice the last two delegate functions are nullable and are defined as optional in the constructor by assigning them default values of null.

Now that we have a constructor, we can create the List<Inventory> collection at class level. For now, we’ll just add a couple of food items. The ConsumeFood function shown will need to be written.

private static List<Inventory> invItems = new List<Inventory>()
{
    new Inventory(InvCategory.Food, "some food", "some food", 
        "rations of food", '♣', 95, Player.ConsumeFood),
    new Inventory(InvCategory.Food, "a mango", "a mango", 
        "mangoes", '♣', 95, Player.ConsumeFood)
};

public static ReadOnlyCollection<Inventory> InventoryItems => invItems.AsReadOnly();

The chance of food being on the map is usually around 18% according to the sources I saw and that seems right; the game can be pretty stingy with it. I’ll probably set it to around 20% to 25% since I don’t like starving. For now, I’m putting it at 95% for testing.

The second declaration above makes the private list available outside the class as a read-only collection, meaning that items cannot be added, deleted or changed. However, the properties of the objects in this list, such as the RealName or DisplayCharacter can be changed by any code accessing the list. One way to stop this would be to declare the property in the class with a private set accessor.

public string RealName { get; private set; }

This would mean that any changes to the properties of Inventory objects would need to be changed from code within the Inventory class. In this project, it’s not really necessary but this is something to keep in mind for security of information in projects where you really need to restrict the ability to change data.

Then, to request new objects on command, we have an overloaded function that will pull items from the new InventoryItems list. The first specifies the inventory category and the second just gets anything.

public static Inventory GetInventoryItem(InvCategory InvType)
{
    // Get a random item from a specific inventory type.
    List<Inventory> invSelect = (from Inventory item in InventoryItems
                                    where item.ItemCategory == InvType
                                    select item).ToList();
            
    return invSelect[rand.Next(invSelect.Count)]; 
}

public static Inventory GetInventoryItem()
{
    // Get a random item from the inventory types.
    return InventoryItems[rand.Next(InventoryItems.Count)];
}

Note that these are static so they can be called without instantiating an Inventory object but then, so is the InventoryItems list.

There’s one problem with this solution and it goes back to object variables being passed by reference. When the GetInventoryItem() function returns Inventory objects, it’s not actually returning new objects. It’s returning references to the objects defined in the InventoryItems list which is held at the class level.

This means that if we call GetInventoryItem() ten times to get ten arrows, we’re not really getting ten arrows; we’re getting ten references to the same arrow object. The problem with this won’t even show up for awhile in development until we have scrolls and potions that change the properties of things like armor and weapons. It means that if you have two swords and you apply an enchantment to the one that you’re wielding, increasing its damage potential for example, you’re actually applying it to both swords because they’re just references to the same object. Worse, if an Aquator attacks you and rusts your armor, any other armor you have of that type will be damaged as well.

We need to force the instantiation of a new object every time something is added to inventory. There are a few ways to do this but the simple method I’m using is to create another constructor that will accept the reference to the item in InventoryItems and clone it.

public Inventory(Inventory Original)
{        
    this.InvCategory = Original.InvCategory;

    this.CodeName = Original.CodeName;
    this.RealName = Original.RealName;
    this.PluralName = Original.PluralName;
    this.IsIdentified = Original.IsIdentified;
    this.IsGroupable = Original.IsGroupable;
    this.IsWieldable = Original.IsWieldable;
    this.IsCursed = Original.IsCursed;
    this.ArmorClass = Original.ArmorClass;
    this.MinDamage = Original.MinDamage;
    this.MaxDamage = Original.MaxDamage;
    this.AppearancePct = Original.AppearancePct;
    this.DisplayCharacter = Original.DisplayCharacter;
    this.ThrowFunction = Original.ThrowFunction;
    this.ZapFunction = Original.ZapFunction;
    this.MainFunction = Original.MainFunction;
}

This returns an actual copy of the original object that can be worked with separately. Then in GetInventoryItem(), the return statement needs to change so it uses this new constructor.

// Clone a new object from template.
return new Inventory(invSelect[rand.Next(invSelect.Count)]); 

Production

With the specifications in place, we now need to scatter some inventory on the map to be picked up by the player. This isn’t like gold where we can just change the MapSpace.ItemCharacter and randomly specify an amount when it’s picked up. These are entire objects with their own properties that will need to be transferred into the player’s inventory. The MapSpace class gets a new nullable property.

public Inventory? MapInventory { get; set; }

Now we need to put the proper amount of inventory on the map. This could be done room by room or it could be done at the map level after all the rooms are generated. I found that doing it for the entire map caused many of the map items to be clumped into one or two rooms which doesn’t really work. Also, the original game had a feature where 1 in 20 levels would have a treasure room filled with inventory (and monsters) so we’ll need an easy way to implement that feature later.

Finally, I decided to assign inventory to each room as it’s created, right after the gold is placed. Normally, I don’t want more than three inventory items per room so the MapLevel class gets a new constant.

private const int MAX_INVENTORY = 3;

Then the MapLevel.RoomGeneration() method is updated to place some inventory items.

// Inventory variables.
int maxInventoryItems = rand.Next(1, MAX_INVENTORY + 1);
int mapInventory = 0;
Inventory invItem;
MapSpace itemSpace;

... 

while (mapInventory < maxInventoryItems)
{
    invItem = Inventory.GetInventoryItem();

    // Place the inventory according to its chances of showing up.
    if (rand.Next(1, 101) <= invItem.AppearancePct)
    {
        invX = westWallX; invY = northWallY;
        itemSpace = levelMap[invX, invY];

        // Look for an interior space that hasn't been used by gold.
        while (itemSpace.MapCharacter != ROOM_INT || itemSpace.ItemCharacter != null)
        {
            invX = rand.Next(westWallX + 1, eastWallX);
            invY = rand.Next(northWallY + 1, southWallY);
            itemSpace = levelMap[invX, invY];
        }
                    
        // Update the space and increment the count.
        itemSpace.MapInventory = invItem;
        itemSpace.ItemCharacter = itemSpace.MapInventory!.DisplayCharacter;

        mapInventory++;
    }
}

The effect is that the code randomly selects potential inventory items and decides the chance of it showing up by it’s AppearancePct property. The first three items to pass go into the room, hopefully meaning that those items with higher probability settings will be more likely to get through. Right now, the list of templates only contains food so that’s all that shows up but at least the distribution between rooms seems right, if a little abundant.

Now, our player needs a way to collect the food.

Acquisition and Storage

A new property is added to the Player class:

public List<Inventory> PlayerInventory { get; set; }

The Game.AddInventory() method was previously just handling the Amulet and gold has its own method so AddInventory() needs to be updated to handle other types of inventory. Its algorithm goes like this when a player lands on a space:

  • Does the space contain the Amulet?
    • CurrentPlayer.HasAmulet = True
    • Remove the ItemCharacter in the current space.
    • Set the status to notify the player.
  • Is there something else in the space? (ItemCharacter != null)
    • Is there room in the player’s inventory according to the INVENTORY_LIMIT constant?
      • Remove the ItemCharacter in the current space.
      • Move the Inventory object reference to the PlayerInventory List.
      • Notify the player.
    • If there’s no room …
      • Do nothing and notify the player.

Running the program, everything seems to go smoothly. I’ll probably change the algorithm above because I exempted the Amulet from the inventory limits, which isn’t justified, and I want to clean the code a little.

Next Steps

We have a way to define specifications for inventory items, add them to a map and let the player collect them. In the next chapter, I’ll look at how the delegate functions in the Inventory class work to provide effects for each type of item; working out the code for that was a challenge in itself.

Our player also needs to be able to view their inventory when desired and drop unneeded items back onto the map. Once we have that, we can define some additional item types for variety and see if the code in this chapter actually gives us a good variety of inventory on the map.


C# 13 and .NET 9 – Modern Cross-Platform Development Fundamentals: Start building websites and services with ASP.NET Core 9, Blazor, and EF Core 9
  • Explore the newest additions to C# 13, the .NET 9 class libraries, and Entity Framework Core 9
  • Build professional websites and services with ASP.NET Core 9 and Blazor
  • Enhance your skills with step-by-step code examples and best practices tips
ComeauSoftware.com uses affiliate links through which we earn commissions for sales.

Sign up for our newsletter to receive updates about new projects, including the upcoming book "Self-Guided SQL"!

We respect your privacy and will never share your information with third-parties. See our privacy policy for more information.

×