Inventory system design and development

This tutorial describes the design and development of an inventory system for a dungeon crawler game. An object-oriented paradigm is employed, with a UML class diagram created using the draw.io tool, used to identify the main objects and their relationships. This class diagram will be used as a framework for the development of code.

For this initial iteration of the inventory design, the design consists of four classes. All item objects (represented by the Item class) include name and weight attributes. Two specialisations of the Item class are defined using inheritance. The first of these is the Weapon subclass which adds two additional attributes representing the minimum and maximum amounts of damage done by the weapon. The Armour subclass has one additional attribute that is used to represent the level of protection provided by the armour.

The Inventory class is used to keep track of all items carried by a player character. The directed link from the Inventory class to the Item class indicates that each inventory contains 0 to many items. Attributes in the class are used to represent items that are currently equipped (in use) by the player character. Methods are included that enable items to be added and removed from the inventory. The total_weight method calculates the combined weight of all items in the inventory – this can be used to determine whether a character is encumbered. The last three methods shown are used to equip items.

Item class

The Item class contains an initialiser method for creating new instances. The name of the item and its weight is passed as arguments to the initialiser method, these are then used to set the name and weight attributes. A third attribute, bonus, is used to represent a (typically magical) bonus that improves the capabilites of the item (e.g. a weapon would deal more damage, while armour would provide more protection).

As shown in the class diagram, two subclasses of the Item class are created. Subclasses inherit the attributes and methods of its parent class, but may include additional attributes and/or method, or may indeed override (re-implement) methods of the parent class. In our example the Armour and Weapon subclasses are introduced. They both have additional attributes that make the subclass as specialisation of its parent. Armour includes an additional attribute, armour_class, which models the amount of protection that the armour provides. The Weapon class includes attributes representing the minimum and maximum amount of raw damage done by the weapon.

Both of the subclasses override the initialiser method of the parent class. The initialiser method for the Armour class includes an additional argument, ac, used to the armour_class attribute. The initialiser of the subclasses also calls the initialiser method the parent class using the super method, as shown on lines 10 and 16 of the code listing below.

class Item:
    def __init__(self, name, weight):
        self.name = name
        self.weight = weight
        self.bonus = 0

class Armour(Item):
    def __init__(self, name, weight, ac):
        super().__init__(name, weight)
        self.armour_class = ac

class Weapon(Item):
    def __init__(self, name, weight, min_damage, max_damage):
        super().__init__(name, weight)
        self.min_damage = min_damage
        self.max_damage = max_damage

Inventory class

The Inventory class keeps track of the items a player character has in their possession. The collection of items is stored in a list, which is initially empty. Additional attributes are used to model the items that are currently equipped by the player character. For this initial design, items held in the right and left hands, together with any armour worn are modelled.

A method for adding items to the inventory is defined next, followed by a method that is called to remove an item from the inventory. For the latter method a check is done to ensure that the item is in the inventory before it is dropped.

class Inventory:
    def __init__(self):
        self.contents = []
        self.right_arm = None
        self.left_arm = None
        self.armour = None

    def add_item(self, item):
        self.contents.append(item)

    def drop_item(self, item):
        if item in self.contents:
            self.contents.remove(item)

The following method calculates the total weight of the items in the inventory. This is done using a list comprehension to collect all of the weights into a list. The sum function is then applied to this to find the total weight.

    def total_weight(self):
        return sum([i.weight for i in self.contents])

The following method is used to equip a weapon in the right hand of the player character. A check if done to ensure that the item is in the inventory contents and that the item is a weapon. The method returns a Boolean-valued result indicating whether or not the method was successful.

    def equip_right_arm(self, item):
        if item in self.contents and type(item).__name__ == "Weapon":
            self.right_arm = item
            return True
        else:
            return False

The following method creates a string representation of the inventory. The equipped items are shown first, using an inline if statement to check whether or not the equipment slots contain an item. A for loop is then used to represent the complete contents of the inventory.

    def __str__(self):
        result = "Equipped items\n"
        result += "---------------\n"
        result += "Right Arm ::" + (self.right_arm.name if self.right_arm else "empty") + "\n"
        result += "Left Arm ::" + (self.left_arm.name if self.left_arm else "empty") + "\n"
        result += "Armour ::" + (self.armour.name if self.armour else "empty") + "\n"
        result += "\nOther items\n"
        result += "---------------\n"
        for i in self.contents:
            result += i.name + "\n"
        return result

Driver Code

The driver code shown below is used to test the correctness of the previous code. Instances of armour and weapon classes are created. A new inventory object is created with the item instances added to this inventory. The contents of the inventory is then printed using the string representation method which is called implicitly when the inventory object is passed as the print argument. Further tests are done to check the correctness of the other methods from the inventory class.

from item import *
from inventory import *

armour1 = Armour("Plate Armour", 120, 18)
armour2 = Armour("Leather Armour", 20, 12)

weapon1 = Weapon("Longsword", 15, 1, 8)
weapon2 = Weapon("Axe", 8, 1, 6)

# create new inventory and add items
inventory = Inventory()

inventory.add_item(armour1)
inventory.add_item(armour2)
inventory.add_item(weapon1)
inventory.add_item(weapon2)

print(inventory)

# remove item from inventory

inventory.drop_item(armour2)
print(inventory)

# check the total weight of the inventory
print(inventory.total_weight())

# equip a weapon with right arm
inventory.equip_right_arm(weapon1)
print(inventory)

# try equipping right arm with armour
inventory.equip_right_arm(armour1)

print(inventory)

Developing a text-based adventure game (Part 3)

Implementing an inventory system

In this tutorial we will look at how a simple inventory system can be added to the game. We will then use the contents of the inventory to differentiate between different outcomes for certain events. In particular, possessing the sword from location 2 will allow the player to slay the Ogre, which will in turn allow them to collect the key to the chest in location 1.

Modelling the inventory using a list

We will use a list in Python to represent the player’s inventory. A list represents an ordered collection of items. A variety of operations can be performed on lists, including adding elements to a list, removing elements from a list, determine the size of a list, and checking whether an item is contained in a list. The basic list operations are demonstrated in the following code listing.

New lists are defined within a pair of square brackets. Individual elements of the list are separated by commas.

#define a new list
list1=['a', 'b', 'c']
print(list1)

The number of elements in a list is calculated using the len operator.

#the number of elements in a list
print(len(list1))

To check whether an element is in a list we use the in operator. Because ‘a’ is in list1 the first use of the in operator returns true. The second use of the in operator return false. We will use the in operator later to check whether or not an item is in the player’s inventory.

#check whether elements are in a list
print('a' in list1)
print('d' in list1)

New elements are added to a list using the append operator. Note the usage of the append operation – the name of the variable storing the list is given first, followed by a period, followed by the append operator. The append operator takes a single argument, corresponding to the element to be added, enclosed in brackets. This operator can be used to add new items to the inventory.

#add an element to a list
list1.append('d')
print(list1)
print('d' in list1)

Elements can be removed from a list using the remove operator. The remove operator follows the same syntax as the append operator. The remove operator can be used to remove one time use items from the inventory. An example would be removing a potion once it has been consumed.

#remove an element from a list
list1.remove('a')
print(list1)

The following lines of code demonstrate further the use of the remove operator. It shows that if an element occurs multiple times in a list, then the remove operator only removes the first occurrence of the element. Note that if we attempt to remove an element that does not exist in the list then an error will occur. It is therefore important to test whether the element is present before attempting to remove it.

#remove only removes the first occurrence of an item from the list
list2=['a', 'b', 'a', 'c']
list2.remove('a')
print(list2)

#remove the second occurrence of 'a'
list2.remove('a')
print(list2)

Given that elements can occur multiple times in a list, it is useful to be able to determine how many times an element occurs. The count operator can be used to determine how many times a particular element occurs.

#check how many times an element occurs in a list
list3=['a','a','b', 'a', 'b', 'c']
print(list3.count('a'))
print(list3.count('b'))
print(list3.count('c'))

In some games the player may start off with no items in their inventory. To represent this as a list we can use an empty list. An empty list is represented by a pair of square brackets with nothing in between.

#empty list
list4=[]
list4.append('a')
list4.append('b')
print(list4)

Download source code [List operations]

Implementing the inventory

The code developed in part 2 of the tutorial is updated to make use of an inventory system. There are two items that can be added to the inventory – the sword in location 2 and the key in location 3. The inventory is initialised to an empty list.

inventory=[]

Next we update the playLocation2 function so that the sword is added to the inventory after it is located (line 70). To ensure that the inventory variable can be accessed within the function, we need to define inventory as a global variable (line 55).

def playLocation2():
    global inventory
    print("You stand atop a narrow ledge.")
    print("A rickety rope ladder dangles off the edge.")
    print("You see a pile of rubble at the far end of the ledge.")
    print("You can either (c)limb down the rope ladder or (s)earch the rubble.")
    response=input("> ")
    if (response=="c"):
        print("You carefully climb down the rope ladder.")
        location="location1"
    elif (response=="s"):
        print("You edge your way along the ledge, almost slipping a couple of times.")
        print("You search the pile of rubble, move large rocks until you discover a sword.")
        print("The sword is inscribed with runes and shimmers in the sun.")
        print("You carefully strap the sword to your back.")
        location="location2"
        inventory.append("sword")
    else:
        location="location2"
    return location

We also update the playLocation3 function so that the sword can be used to slay the Ogre if it has added to the inventory. Line 91 checks that the player response is attack and the sword is in the inventory. The two conditions are joined using the and operator, which evaluates to true if both of the conditions are true. Line 97 adds the key to the inventory.

def playLocation3():
    global inventory
    print("You reach what looks like a rudimentary camp site.")
    print("You notice a fire pit in the middle of the camp site.")
    print("Scattered around the fire pit are the many bones picked clean of flesh.")
    print("Ominously some of the bones look human like.")
    print("Just as you finish surveying the scene you here a roar.")
    print("Running towards you, wielding a large club is an Ogre.")
    print("You can either (f)lee to the south, (a)ttack the Ogre or try to (t)alk with the Ogre.")
    response=input("> ")
    if (response=="f"):
        print("You run away from the Ogre at full speed")
        print("The Ogre pursues for a while but eventually gives up.")
        print("You survive another day.")
        print("Once you know you are safe you continue your journey southward at a slower pace.")
        location="location1"
    elif (response=="a" and "sword" in inventory):
        print("As the Ogre draws near you draw your sword.")
        print("With one hefty swing of your sword you slay the Ogre.")
        print("You search the body of the Ogre and find a key.")
        print("You take the key and head back towards the clearing with the chest.")
        location="location1"
        inventory.append("key")
    elif (response=="a"):
        print("You fight valiantly with the Ogre.")
        print("You manage to inflict several telling blows against it but it fights on.")
        print("Having dodged several sweeping blows, the Ogre brings its club down upon you with a crash.")
        print("...")
        location="finish"
    elif (response=="t"):
        print("You attempt to make conversation with the Ogre but this only makes it more angry.")
        location="location3"
    else:
        location="location3"
    return location

The playLocation1 function is updated in a similar way to check whether the player has the key in their inventory when they attempt to open the chest.
The full code listing is given as follows:

location="start"
inventory=[]

def playStart():
    print("You are on a quest to find the lost sippy cup.")
    print("The sippy cup is a priceless treasure that grants\nthe user eternal life")
    print("You come to a junction. You can either go (w)est or (e)ast.")
    print("What do you select?")
    response=input("> ")
    if (response=="w"):
        location="location1"
        print("You travel along the path to the west...")
    elif (response=="e"):
        location="location2"
        print("You travel along the path to the east.")
        print("The path leads gradually upwards until you finally reach a ledge.")
    else:
        location="start"
    return location


def playLocation1():
    global inventory
    print("You are in a large clearing.")
    print("In the centre of the clearing is a chest.")
    print("There is a path leading north.")
    print("To the south you can see a cliff face with a rope ladder leading to a ledge.")
    print("You can go (n)orth, (c)limb the ladder or attempt to (o)pen the chest.")
    print("Enter your selection.")
    response=input("> ")
    
    if (response=="o" and "key" in inventory):
        print("You use the key recovered from the Ogre to unlock the chest.")
        print("As you open the lid of the chest a warm glow flows through your body.")
        print("Inside the chest you see the magical sippy cup. ")
        print("As you bring the sippy cup to your mouth and take a sip you feel instantly invigorated.")
        print("You have completed your quest.")
        location="finish"
    elif (response=="o"):
        print("You are unable to open the chest.")
        print("It has a large padlock which cannot be broken.")
        location="location1"
    elif (response=="n"):
        print("You travel for several hours along a winding path.")
        location="location3"
    elif (response=="c"):
        print("You climb the rope ladder which sways dangerously as you make your way up.")
        print("At the top of a ladder you climb up to a ledge.")
        location="location2"
    else:
        location="location1"
    return location

def playLocation2():
    global inventory
    print("You stand atop a narrow ledge.")
    print("A rickety rope ladder dangles off the edge.")
    print("You see a pile of rubble at the far end of the ledge.")
    print("You can either (c)limb down the rope ladder or (s)earch the rubble.")
    response=input("> ")
    if (response=="c"):
        print("You carefully climb down the rope ladder.")
        location="location1"
    elif (response=="s"):
        print("You edge your way along the ledge, almost slipping a couple of times.")
        print("You search the pile of rubble, move large rocks until you discover a sword.")
        print("The sword is inscribed with runes and shimmers in the sun.")
        print("You carefully strap the sword to your back.")
        location="location2"
        inventory.append("sword")
    else:
        location="location2"
    return location

def playLocation3():
    global inventory
    print("You reach what looks like a rudimentary camp site.")
    print("You notice a fire pit in the middle of the camp site.")
    print("Scattered around the fire pit are the many bones picked clean of flesh.")
    print("Ominously some of the bones look human like.")
    print("Just as you finish surveying the scene you here a roar.")
    print("Running towards you, wielding a large club is an Ogre.")
    print("You can either (f)lee to the south, (a)ttack the Ogre or try to (t)alk with the Ogre.")
    response=input("> ")
    if (response=="f"):
        print("You run away from the Ogre at full speed")
        print("The Ogre pursues for a while but eventually gives up.")
        print("You survive another day.")
        print("Once you know you are safe you continue your journey southward at a slower pace.")
        location="location1"
    elif (response=="a" and "sword" in inventory):
        print("As the Ogre draws near you draw your sword.")
        print("With one hefty swing of your sword you slay the Ogre.")
        print("You search the body of the Ogre and find a key.")
        print("You take the key and head back towards the clearing with the chest.")
        location="location1"
        inventory.append("key")
    elif (response=="a"):
        print("You fight valiantly with the Ogre.")
        print("You manage to inflict several telling blows against it but it fights on.")
        print("Having dodged several sweeping blows, the Ogre brings its club down upon you with a crash.")
        print("...")
        location="finish"
    elif (response=="t"):
        print("You attempt to make conversation with the Ogre but this only makes it more angry.")
        location="location3"
    else:
        location="location3"
    return location

    
while (location!="finish"):
    if (location=="start"):
        location=playStart()
    elif (location=="location1"):
        location=playLocation1()
    elif (location=="location2"):
        location=playLocation2()
    elif (location=="location3"):
        location=playLocation3()
    else:
        break

Download source code [Stage 3 code]

Limitations

When we play through the game we notice that when the player returns to an area we are presented with the same options. For example, when returning to location 3 after having already slaying the Ogre the player encounters a new Ogre. To address this limitation we need some way of tracking the events that have occurred within a particular area.