Exercise: Inventory System
Now that we've explored various collection types in C#, let's put that knowledge to practical use by building a flexible inventory system that could be used in a game. This exercise will help you understand how different collections can work together to create a robust system.
Exercise Overview
In this exercise, you'll create a console-based inventory system with the following features:
- Add and remove items
- Track item quantities
- Group items by category
- Find items by name or properties
- Sort items by different criteria
- Implement inventory limits (weight and slot-based)
- Support for equipping items
The Item Class
Let's start with a basic Item
class to represent items in our inventory:
public class Item
{
// Basic properties
public string Id { get; }
public string Name { get; }
public string Description { get; }
public ItemType Type { get; }
public float Weight { get; }
public int Value { get; }
// Equipment properties
public bool IsEquippable { get; }
public EquipSlot EquipSlot { get; }
// Constructor
public Item(string id, string name, string description, ItemType type,
float weight, int value, bool isEquippable = false,
EquipSlot equipSlot = EquipSlot.None)
{
Id = id;
Name = name;
Description = description;
Type = type;
Weight = weight;
Value = value;
IsEquippable = isEquippable;
EquipSlot = equipSlot;
}
// Override ToString for easy display
public override string ToString()
{
return $"{Name} ({Type}) - Value: {Value} gold, Weight: {Weight} kg";
}
// Override Equals and GetHashCode for proper collection behavior
public override bool Equals(object obj)
{
if (obj is Item other)
{
return Id == other.Id;
}
return false;
}
public override int GetHashCode()
{
return Id.GetHashCode();
}
}
// Enums for item properties
public enum ItemType
{
Weapon,
Armor,
Potion,
Food,
Material,
Quest,
Miscellaneous
}
public enum EquipSlot
{
None,
Head,
Chest,
Legs,
Feet,
Hands,
MainHand,
OffHand,
Necklace,
Ring
}
Basic Inventory Implementation
Now, let's implement a basic inventory system using the collections we've learned about:
public class Inventory
{
// Store items with their quantities
private Dictionary<Item, int> items = new Dictionary<Item, int>();
// Track items by type for quick access
private Dictionary<ItemType, List<Item>> itemsByType = new Dictionary<ItemType, List<Item>>();
// Track equipped items
private Dictionary<EquipSlot, Item> equippedItems = new Dictionary<EquipSlot, Item>();
// Inventory constraints
private float maxWeight;
private int maxSlots;
// Current inventory stats
public float CurrentWeight { get; private set; } = 0f;
public int UsedSlots => items.Count;
public int AvailableSlots => maxSlots - UsedSlots;
// Constructor
public Inventory(int maxSlots = 20, float maxWeight = 50f)
{
this.maxSlots = maxSlots;
this.maxWeight = maxWeight;
// Initialize itemsByType dictionary
foreach (ItemType type in Enum.GetValues(typeof(ItemType)))
{
itemsByType[type] = new List<Item>();
}
// Initialize equippedItems dictionary
foreach (EquipSlot slot in Enum.GetValues(typeof(EquipSlot)))
{
if (slot != EquipSlot.None)
{
equippedItems[slot] = null;
}
}
}
// Add an item to the inventory
public bool AddItem(Item item, int quantity = 1)
{
// Validate input
if (item == null || quantity <= 0)
{
return false;
}
// Check if adding this item would exceed weight limit
float newWeight = CurrentWeight + (item.Weight * quantity);
if (newWeight > maxWeight)
{
Console.WriteLine($"Cannot add {item.Name}. Weight limit would be exceeded.");
return false;
}
// Check if we already have this item
if (items.ContainsKey(item))
{
// Update quantity
items[item] += quantity;
CurrentWeight = newWeight;
Console.WriteLine($"Added {quantity} {item.Name}(s). New quantity: {items[item]}");
return true;
}
// Check if adding a new item would exceed slot limit
if (UsedSlots >= maxSlots)
{
Console.WriteLine($"Cannot add {item.Name}. Inventory is full.");
return false;
}
// Add new item
items.Add(item, quantity);
itemsByType[item.Type].Add(item);
CurrentWeight = newWeight;
Console.WriteLine($"Added {quantity} {item.Name}(s) to inventory.");
return true;
}
// Remove an item from the inventory
public bool RemoveItem(Item item, int quantity = 1)
{
// Validate input
if (item == null || quantity <= 0)
{
return false;
}
// Check if we have the item
if (!items.TryGetValue(item, out int currentQuantity))
{
Console.WriteLine($"You don't have any {item.Name} in your inventory.");
return false;
}
// Check if we have enough of the item
if (currentQuantity < quantity)
{
Console.WriteLine($"You only have {currentQuantity} {item.Name}(s), but tried to remove {quantity}.");
return false;
}
// Update quantity
if (currentQuantity == quantity)
{
// Remove the item completely
items.Remove(item);
itemsByType[item.Type].Remove(item);
// Unequip if equipped
foreach (EquipSlot slot in equippedItems.Keys.ToList())
{
if (equippedItems[slot] == item)
{
equippedItems[slot] = null;
Console.WriteLine($"Unequipped {item.Name} from {slot}.");
}
}
}
else
{
// Reduce quantity
items[item] = currentQuantity - quantity;
}
// Update weight
CurrentWeight -= item.Weight * quantity;
Console.WriteLine($"Removed {quantity} {item.Name}(s) from inventory.");
return true;
}
// Get the quantity of an item
public int GetItemQuantity(Item item)
{
if (items.TryGetValue(item, out int quantity))
{
return quantity;
}
return 0;
}
// Check if the inventory contains an item
public bool HasItem(Item item, int minQuantity = 1)
{
return GetItemQuantity(item) >= minQuantity;
}
// Get all items of a specific type
public List<Item> GetItemsByType(ItemType type)
{
if (itemsByType.TryGetValue(type, out List<Item> typeItems))
{
return new List<Item>(typeItems); // Return a copy
}
return new List<Item>();
}
// Get all items
public Dictionary<Item, int> GetAllItems()
{
return new Dictionary<Item, int>(items); // Return a copy
}
// Equip an item
public bool EquipItem(Item item)
{
// Check if the item is in the inventory
if (!items.ContainsKey(item))
{
Console.WriteLine($"You don't have {item.Name} in your inventory.");
return false;
}
// Check if the item is equippable
if (!item.IsEquippable)
{
Console.WriteLine($"{item.Name} cannot be equipped.");
return false;
}
// Check if the slot is valid
if (item.EquipSlot == EquipSlot.None)
{
Console.WriteLine($"{item.Name} doesn't have a valid equip slot.");
return false;
}
// Unequip any item in the same slot
if (equippedItems[item.EquipSlot] != null)
{
Console.WriteLine($"Unequipped {equippedItems[item.EquipSlot].Name} from {item.EquipSlot}.");
equippedItems[item.EquipSlot] = null;
}
// Equip the new item
equippedItems[item.EquipSlot] = item;
Console.WriteLine($"Equipped {item.Name} in {item.EquipSlot} slot.");
return true;
}
// Unequip an item
public bool UnequipItem(EquipSlot slot)
{
// Check if the slot has an item
if (equippedItems[slot] == null)
{
Console.WriteLine($"Nothing is equipped in the {slot} slot.");
return false;
}
// Unequip the item
Item item = equippedItems[slot];
equippedItems[slot] = null;
Console.WriteLine($"Unequipped {item.Name} from {slot} slot.");
return true;
}
// Get equipped item in a slot
public Item GetEquippedItem(EquipSlot slot)
{
if (equippedItems.TryGetValue(slot, out Item item))
{
return item;
}
return null;
}
// Get all equipped items
public Dictionary<EquipSlot, Item> GetAllEquippedItems()
{
return new Dictionary<EquipSlot, Item>(equippedItems); // Return a copy
}
// Sort items by a specific criteria
public List<Item> GetSortedItems(ItemSortCriteria sortCriteria)
{
List<Item> sortedItems = new List<Item>(items.Keys);
switch (sortCriteria)
{
case ItemSortCriteria.NameAscending:
sortedItems.Sort((a, b) => string.Compare(a.Name, b.Name));
break;
case ItemSortCriteria.NameDescending:
sortedItems.Sort((a, b) => string.Compare(b.Name, a.Name));
break;
case ItemSortCriteria.ValueAscending:
sortedItems.Sort((a, b) => a.Value.CompareTo(b.Value));
break;
case ItemSortCriteria.ValueDescending:
sortedItems.Sort((a, b) => b.Value.CompareTo(a.Value));
break;
case ItemSortCriteria.WeightAscending:
sortedItems.Sort((a, b) => a.Weight.CompareTo(b.Weight));
break;
case ItemSortCriteria.WeightDescending:
sortedItems.Sort((a, b) => b.Weight.CompareTo(a.Weight));
break;
case ItemSortCriteria.TypeAscending:
sortedItems.Sort((a, b) => a.Type.CompareTo(b.Type));
break;
default:
break;
}
return sortedItems;
}
// Find items by name (partial match)
public List<Item> FindItemsByName(string nameFragment)
{
List<Item> foundItems = new List<Item>();
foreach (Item item in items.Keys)
{
if (item.Name.Contains(nameFragment, StringComparison.OrdinalIgnoreCase))
{
foundItems.Add(item);
}
}
return foundItems;
}
// Display inventory contents
public void DisplayInventory()
{
Console.WriteLine("\n===== INVENTORY =====");
Console.WriteLine($"Slots: {UsedSlots}/{maxSlots}");
Console.WriteLine($"Weight: {CurrentWeight:F1}/{maxWeight:F1} kg");
if (items.Count == 0)
{
Console.WriteLine("Inventory is empty.");
}
else
{
// Group by type
foreach (ItemType type in Enum.GetValues(typeof(ItemType)))
{
List<Item> typeItems = itemsByType[type];
if (typeItems.Count > 0)
{
Console.WriteLine($"\n--- {type} ---");
foreach (Item item in typeItems)
{
string equippedStatus = IsItemEquipped(item) ? " [EQUIPPED]" : "";
Console.WriteLine($"{item} x{items[item]}{equippedStatus}");
}
}
}
}
Console.WriteLine("===================\n");
}
// Display equipped items
public void DisplayEquippedItems()
{
Console.WriteLine("\n===== EQUIPPED ITEMS =====");
bool anyEquipped = false;
foreach (EquipSlot slot in equippedItems.Keys)
{
Item item = equippedItems[slot];
if (item != null)
{
Console.WriteLine($"{slot}: {item}");
anyEquipped = true;
}
}
if (!anyEquipped)
{
Console.WriteLine("No items equipped.");
}
Console.WriteLine("========================\n");
}
// Check if an item is equipped
private bool IsItemEquipped(Item item)
{
return equippedItems.ContainsValue(item);
}
}
// Enum for sorting criteria
public enum ItemSortCriteria
{
NameAscending,
NameDescending,
ValueAscending,
ValueDescending,
WeightAscending,
WeightDescending,
TypeAscending
}
Testing the Inventory System
Now, let's create a simple program to test our inventory system:
class Program
{
static void Main(string[] args)
{
// Create some test items
Item ironSword = new Item("W001", "Iron Sword", "A basic sword made of iron.",
ItemType.Weapon, 3.5f, 50, true, EquipSlot.MainHand);
Item leatherArmor = new Item("A001", "Leather Armor", "Basic armor made of leather.",
ItemType.Armor, 8.0f, 75, true, EquipSlot.Chest);
Item healthPotion = new Item("P001", "Health Potion", "Restores 50 health points.",
ItemType.Potion, 0.5f, 25);
Item bread = new Item("F001", "Bread", "A loaf of bread. Restores 10 health points.",
ItemType.Food, 0.3f, 5);
Item ironOre = new Item("M001", "Iron Ore", "Raw iron ore. Can be smelted into iron ingots.",
ItemType.Material, 2.0f, 10);
Item questItem = new Item("Q001", "Ancient Artifact", "A mysterious artifact needed for a quest.",
ItemType.Quest, 1.0f, 0);
// Create an inventory
Inventory inventory = new Inventory(maxSlots: 10, maxWeight: 50f);
// Add items to the inventory
inventory.AddItem(ironSword);
inventory.AddItem(leatherArmor);
inventory.AddItem(healthPotion, 5);
inventory.AddItem(bread, 3);
inventory.AddItem(ironOre, 10);
inventory.AddItem(questItem);
// Display the inventory
inventory.DisplayInventory();
// Equip some items
inventory.EquipItem(ironSword);
inventory.EquipItem(leatherArmor);
// Display equipped items
inventory.DisplayEquippedItems();
// Display the updated inventory
inventory.DisplayInventory();
// Test sorting
Console.WriteLine("\n===== ITEMS SORTED BY VALUE (DESCENDING) =====");
List<Item> sortedByValue = inventory.GetSortedItems(ItemSortCriteria.ValueDescending);
foreach (Item item in sortedByValue)
{
Console.WriteLine($"{item} x{inventory.GetItemQuantity(item)}");
}
// Test finding items
Console.WriteLine("\n===== ITEMS CONTAINING 'iron' =====");
List<Item> ironItems = inventory.FindItemsByName("iron");
foreach (Item item in ironItems)
{
Console.WriteLine($"{item} x{inventory.GetItemQuantity(item)}");
}
// Test removing items
inventory.RemoveItem(healthPotion, 2);
inventory.RemoveItem(ironOre, 5);
// Display the updated inventory
inventory.DisplayInventory();
// Test unequipping
inventory.UnequipItem(EquipSlot.MainHand);
// Display equipped items
inventory.DisplayEquippedItems();
Console.WriteLine("Press any key to exit...");
Console.ReadKey();
}
}
Challenge Extensions
Once you've implemented the basic inventory system, try these challenge extensions:
-
Item Stacking Rules: Modify the system so that some items can stack (like potions) while others cannot (like weapons).
-
Item Categories: Add support for item categories and subcategories (e.g., Weapon → Sword, Axe, Bow).
-
Item Rarity: Implement a rarity system (Common, Uncommon, Rare, Epic, Legendary) with color-coded display.
-
Item Requirements: Add level or stat requirements for equipping items.
-
Inventory Filters: Create methods to filter items by multiple criteria (e.g., equippable items worth more than 50 gold).
-
Crafting System: Implement a simple crafting system that consumes materials from the inventory to create new items.
-
Save/Load System: Add methods to save the inventory to a file and load it back.
-
Item Comparison: Implement a method to compare two items and show which one is better.
Solution Approach
When implementing this inventory system, consider these key points:
-
Collection Choices:
Dictionary<Item, int>
for items and quantitiesDictionary<ItemType, List<Item>>
for organizing by typeDictionary<EquipSlot, Item>
for equipped items
-
Performance Considerations:
- Fast lookups for checking if an item exists
- Efficient grouping by type
- Quick access to equipped items
-
Constraints Management:
- Weight limit
- Slot limit
- Equipment slot restrictions
-
User Experience:
- Clear feedback messages
- Organized display of items
- Intuitive sorting and filtering
Conclusion
This exercise demonstrates how different collection types can work together to create a robust inventory system. By using dictionaries for fast lookups, lists for ordered collections, and specialized data structures for specific needs, you can build a flexible and efficient system that could be extended for use in a real game.
Remember that there's no single "correct" implementation. The best approach depends on your specific requirements, such as the scale of your game, the complexity of your item system, and the performance characteristics you need to prioritize.
As you work through this exercise, think about how each collection type contributes to the overall functionality and how you might optimize the system for your specific needs.