Whenever you want the computer to react to the player's input dynamically, i.e. not hard-coded but so the player has freedom of action, you have to deal with three aspects:
- a. How to store the data
b. How to use the data
c. How to present the data
DISCLAIMERS:
I add init python: and $ because this tutorial is intended for Ren'Py, even though most of this works in plain Python. Make sure you understand the difference between Python code, Ren'py script language and Ren'Py screen language before you even attempt to do anything that mixes the three. Which an inventory does!
As per Python convention, classes will have uppercase first letters and variables will be all lowercase. A class is a template for a collection of data under a single name.
First, we need to decide what we want to store. Let's say that we want items, of course. So we need to store the item's name: A ball. Let's do this:
Code: Select all
init python:
class Item(object):
def __init__(self, name):
self.name = name
The method __init__ (that's two underscores in front and behind) is automatically called whenever a new instance of this class is created. Keep in mind that every method (a function inside a class) of a class always takes self as its first argument, which is automatically filled during its call!
We can now do this:
Code: Select all
$ inventory = Item('ball')
This is not very flexible because we can only have one item at a time. So let's make a container!
Code: Select all
init python:
class Container(object):
def __init__(self):
self.inventory = []
Code: Select all
$ backpack = Container()
A few words about lists:
Lists are a type of data structure in Python and are the most intuitive type of "data collection" because they can be changed and have an order in which their elements appear. The first element is always number 0, so the first element of a list named inventory is inventory[0], the 5th element is inventory[4]. Lists are defined by using [ ]: [1, 2, 3] is a list with these three numbers in this order.
Other data structures are:
dictionaries - A dictionary is unsorted, meaning it has no order, and translates one piece of data into another. Dictionaries are defined by using { }. We won't use these for the inventory.
Example:
Code: Select all
japanese = {'god': 'kami', 'hair': 'kami', 'cat': 'neko'}
tuples - These are just like lists, but are write-protected. They are best used for data that obviously never changes, like the days of the week. They are defined by using ( ).
sets - Something is either part of a set or it isn't. Sets have no order (they are unsorted) and can't have duplicates. You can use this to keep track of which places the player has already visited, for example, or of he has met a specific character yet. Sets are defined using Set() and offer a variety of different methods.
Now, back to our inventory.
There wouldn't be much of a point in having an entire class if all it does is contain a list, because we could just use a simple list from the start. We will now add functionality:
Code: Select all
init python:
class Container(object):
def __init__(self):
self.inventory = []
def add_item(self, item):
self.inventory.append(item)
We are now able to make any number of Containers, each representing a different inventory, with Items stored in a list.
I will do a few steps at once now.
Code: Select all
init python:
class Item(object):
def __init__(self, name, weight):
self.name = name
self.weight = weight
class InvItem(object):
def __init__(self, item, amount):
self.item = item
self.amount = amount
class Container(object):
def __init__(self, weight_max):
self.inventory = []
self.weight_max = weight_max
def add_item(self, item, amount=1):
self.inventory.append(InvItem(item, amount))
First, Item now has another variable, weight, and the Container one called weight_max. Second, we have another class called InvItem that has two data fields: item and amount.
Of course we could include the amount in the data of the item itself, but there is an important reason why we don't - just be patient!
So let's test our code:
Code: Select all
$ ball = Item('ball', 5)
$ backpack = Container(100)
$ backpack.add_item(ball)
So far, the weight does nothing. We want the game to check whether we can carry the new item, so we change the method add_item:
Code: Select all
def add_item(self, item, amount=1):
if item.weight * amount > self.weight_max - sum(i.item.weight * i.amount for i in self.inventory):
return('too heavy')
else:
self.inventory.append(InvItem(item), amount)
return('success')
Now comes the reason why we didn't include the amount in the item data itself: We want to be able to compare the items in our inventory to the "base items" in our database. If we include the amount, 'ball' with weight 5 and amount 1 would be a different item than 'ball' with weight 5 and amount 2, which would mean that we wouldn't be able to check if we already have a ball or not.
We want to be able to compare items in order to check for duplicates:
Code: Select all
def add_item(self, item, amount=1):
if item.weight * amount > self.weight_max - sum(i.item.weight * i.amount for i in self.inventory):
return('too heavy')
else:
if item in [i.item for i in self.inventory]: # I can't believe I got this line to work with my first try
self.inventory[[i.item for i in self.inventory].index(item)].amount += amount # oh god why
else:
self.inventory.append(InvItem(item, amount))
return('success')
In the first line, you see list brackets, so this is obviously going to be a list, which is created by taking every single element of self.inventory, calling it i, and putting .item behind it. What we get is a list of the pure Item objects in our inventory which we can use to check if the new item already exists in our inventory.
The second line does exactly the same and uses this list of stripped-down InvItem objects (now Item objects) to find the location of the existing item by using [].index. This location is then used to increase that item's amount instead of creating a new item to add to the list.
The complete code so far:
Code: Select all
init python:
class Item(object):
def __init__(self, name, weight):
self.name = name
self.weight = weight
class InvItem(object):
def __init__(self, item, amount):
self.item = item
self.amount = amount
class Container(object):
def __init__(self, weight_max):
self.inventory = []
self.weight_max = weight_max
def add_item(self, item, amount=1):
if item.weight * amount > self.weight_max - sum(i.item.weight * i.amount for i in self.inventory):
return('too heavy')
else:
if item in [i.item for i in self.inventory]: # I can't believe I got this line to work with my first try
self.inventory[[i.item for i in self.inventory].index(item)].amount += amount # oh god why
else:
self.inventory.append(InvItem(item, amount))
return('success')
Next, we will implement these functions (in reverse order, actually). After that, we will make a screen to display the data: An inventory screen!
First, we will implement a check if and how many of a specific item are already in our inventory. We add a method to our Container class:
Code: Select all
def has_item(self, item, amount=1):
if item in [i.item for i in self.inventory]:
if self.inventory[[i.item for i in self.inventory].index(item)].amount >= amount:
return(self.inventory[[i.item for i in self.inventory].index(item)].amount)
else:
return(False)
else:
return(False)
You can see that we have to use self.inventory[[i.item for i in self.inventory].index(item)] a lot. Let's make this a method of our class to make our code more readable:
Code: Select all
def finditem(self, item):
return(self.inventory[[i.item for i in self.inventory].index(item)])
Code: Select all
class Container(object):
def __init__(self, weight_max):
self.inventory = []
self.weight_max = weight_max
def add_item(self, item, amount=1):
if item.weight * amount > self.weight_max - sum(i.item.weight * i.amount for i in self.inventory):
return('too heavy')
else:
if item in [i.item for i in self.inventory]: # I can't believe I got this line to work with my first try
self.finditem(item).amount += amount # oh god why
else:
self.inventory.append(InvItem(item, amount))
return('success')
def has_item(self, item, amount=1):
if item in [i.item for i in self.inventory]:
if self.finditem(item).amount >= amount:
return(self.finditem(item).amount)
else:
return(False)
else:
return(False)
def finditem(self, item):
return(self.inventory[[i.item for i in self.inventory].index(item)])
Code: Select all
def remove_item(self, item, amount=1):
if self.has_item(item):
self.finditem(item).amount -= amount
if self.finditem(item).amount <= 0:
self.inventory.pop(self.inventory.index(self.finditem(item)))
return('gone')
else:
return('more left')
else:
return('not found')
inventory.pop(a) removes the element at position a from the list inventory. So we want the position of our item as a.
This a is inventory.index(b), which returns the position of the first occurrence of the element b in the list inventory.
b finally is finditem(item), which returns the actual InvItem in our inventory which we can then look for.
So this method finds the item in our inventory, decreases its amount by the given amount (if none given, by 1) and then checks if there are 0 or fewer left. If so, it removes the item (by removing its InvItem) from our inventory. After all that, it returns whether no item was found, there are still more left, or it was removed entirely.
EDIT: Added self.-reference to all calls of finditem() and has_item(). It works without in vanilla Python, but Ren'py wants those.
EDIT: Changed "python:" to "init python:". Thanks for pointing that out.