Part 8 - Items and Inventory

So far, our game has movement, dungeon exploring, combat, and AI (okay, we’re stretching the meaning of “intelligence” in artificial intelligence to its limits, but bear with me here). Now it’s time for another staple of the roguelike genre: items! Why would our rogue venture into the dungeons of doom if not for some sweet loot, after all?

We’ll start by placing one type of item, the healing potion in this case, and then we’ll work on our implementation of the inventory. In the next chapter, we’ll add different types of items, but for now, just the healing potion will suffice.

Make the following changes to place_entities to start placing some health potion entities. They won’t do anything yet, but they’ll at least appear on the map.

-   def place_entities(self, room, entities, max_monsters_per_room):
+   def place_entities(self, room, entities, max_monsters_per_room, max_items_per_room):
       number_of_monsters = randint(0, max_monsters_per_room)
+       number_of_items = randint(0, max_items_per_room)

       for i in range(number_of_monsters):
           ...

+       for i in range(number_of_items):
+           x = randint(room.x1 + 1, room.x2 - 1)
+           y = randint(room.y1 + 1, room.y2 - 1)
+
+           if not any([entity for entity in entities if entity.x == x and entity.y == y]):
+               item = Entity(x, y, '!', libtcod.violet, 'Healing Potion', render_order=RenderOrder.ITEM)
+
+               entities.append(item)
    def place_entities(self, room, entities, max_monsters_per_room, max_items_per_room):
        number_of_monsters = randint(0, max_monsters_per_room)
        number_of_items = randint(0, max_items_per_room)

        for i in range(number_of_monsters):
            ...

        for i in range(number_of_items):
            x = randint(room.x1 + 1, room.x2 - 1)
            y = randint(room.y1 + 1, room.y2 - 1)

            if not any([entity for entity in entities if entity.x == x and entity.y == y]):
                item = Entity(x, y, '!', libtcod.violet, 'Healing Potion', render_order=RenderOrder.ITEM)

                entities.append(item)

Update the call to place_entities in make_map:

-               self.place_entities(new_room, entities, max_monsters_per_room)
+               self.place_entities(new_room, entities, max_monsters_per_room, max_items_per_room)
                self.place_entities(new_room, entities, max_monsters_per_room, max_items_per_room)

Update the definition of make_map to include the new max_items_per_room variable.

   def make_map(self, max_rooms, room_min_size, room_max_size, map_width, map_height, player, entities,
-                max_monsters_per_room):
+                max_monsters_per_room, max_items_per_room):
    def make_map(self, max_rooms, room_min_size, room_max_size, map_width, map_height, player, entities,
                 max_monsters_per_room, max_items_per_room):

And finally, the call in engine.py. We’ll also define the variable here.

   ...
   max_monsters_per_room = 3
+   max_items_per_room = 2
   ...
   game_map = GameMap(map_width, map_height)
   game_map.make_map(max_rooms, room_min_size, room_max_size, map_width, map_height, player, entities,
-                     max_monsters_per_room)
+                     max_monsters_per_room, max_items_per_room)

   fov_recompute = True
   ...
    ...
    max_monsters_per_room = 3
    max_items_per_room = 2
    ...
    game_map = GameMap(map_width, map_height)
    game_map.make_map(max_rooms, room_min_size, room_max_size, map_width, map_height, player, entities,
                      max_monsters_per_room, max_items_per_room)

    fov_recompute = True
    ...

You should now see a few health potions here and there in the dungeon. But we can’t pick anything up yet. An obvious place to start would be by giving the player an inventory. Let’s create a new component, called Inventory, which will hold a list of items, along with the maximum amount of items the inventory can have. Create a new file in the components folder, named inventory.py, and put the following in it:

class Inventory:
    def __init__(self, capacity):
        self.capacity = capacity
        self.items = []

The items list will be what holds the actual item entities. Capacity determines how many items we can pick up in total.

But what types of entities can we pick up in the first place? We may or may not want the player picking up every type of entity that can be walked on (corpses for example), so how do we distinguish between the two?

Let’s create a new component, called Item, which we’ll add to entities when we want them to be able to be picked up. Create a file called item.py in components, and add the following class to it:

class Item:
    def __init__(self):
        pass

Wait, what? An empty class? Don’t worry, we’ll add some more interesting stuff here once we move on to using our items. But for now, if an Entity has this component, we can pick it up, and if not, we can’t. Therefore, all we need is an empty class at the moment.

Now let’s modify the Entity class to accept the Item and Inventory components.

class Entity:
-   def __init__(self, x, y, char, color, name, blocks=False, render_order=RenderOrder.CORPSE, fighter=None, ai=None):
+   def __init__(self, x, y, char, color, name, blocks=False, render_order=RenderOrder.CORPSE, fighter=None, ai=None,
+                item=None, inventory=None):
       self.x = x
       self.y = y
       self.char = char
       self.color = color
       self.name = name
       self.blocks = blocks
       self.render_order = render_order
       self.fighter = fighter
       self.ai = ai
+       self.item = item
+       self.inventory = inventory

       if self.fighter:
           self.fighter.owner = self

       if self.ai:
           self.ai.owner = self

+       if self.item:
+           self.item.owner = self
+
+       if self.inventory:
+           self.inventory.owner = self
class Entity:
    def __init__(self, x, y, char, color, name, blocks=False, render_order=RenderOrder.CORPSE, fighter=None, ai=None,
                 item=None, inventory=None):
        self.x = x
        self.y = y
        self.char = char
        self.color = color
        self.name = name
        self.blocks = blocks
        self.render_order = render_order
        self.fighter = fighter
        self.ai = ai
        self.item = item
        self.inventory = inventory

        if self.fighter:
            self.fighter.owner = self

        if self.ai:
            self.ai.owner = self

        if self.item:
            self.item.owner = self

        if self.inventory:
            self.inventory.owner = self

With our Entity class modified, we’ll need to modify the player and the healing potions to have Inventory and Item respectively. This tutorial won’t give monsters an inventory, but you could attach the Inventory component to them for a similar effect.

In engine.py:

   ...
   fighter_component = Fighter(hp=30, defense=2, power=5)
+   inventory_component = Inventory(26)
-   player = Entity(0, 0, '@', libtcod.white, 'Player', blocks=True, render_order=RenderOrder.ACTOR, fighter=fighter_component)
+   player = Entity(0, 0, '@', libtcod.white, 'Player', blocks=True, render_order=RenderOrder.ACTOR,
+                   fighter=fighter_component, inventory=inventory_component)
   entities = [player]
   ...
    ...
    fighter_component = Fighter(hp=30, defense=2, power=5)
    inventory_component = Inventory(26)
    player = Entity(0, 0, '@', libtcod.white, 'Player', blocks=True, render_order=RenderOrder.ACTOR, fighter=fighter_component)
    player = Entity(0, 0, '@', libtcod.white, 'Player', blocks=True, render_order=RenderOrder.ACTOR,
                    fighter=fighter_component, inventory=inventory_component)
    entities = [player]
    ...

Of course, be sure to import Inventory as well:

from components.fighter import Fighter
+from components.inventory import Inventory
from death_functions import kill_monster, kill_player
...
from components.fighter import Fighter
from components.inventory import Inventory
from death_functions import kill_monster, kill_player
...

Then, modify where we put the healing potions in the place_entities function.

+               item_component = Item()
-               item = Entity(x, y, '!', libtcod.violet, 'Healing Potion', render_order=RenderOrder.ITEM)
+               item = Entity(x, y, '!', libtcod.violet, 'Healing Potion', render_order=RenderOrder.ITEM,
+                             item=item_component)
                item_component = Item()
                item = Entity(x, y, '!', libtcod.violet, 'Healing Potion', render_order=RenderOrder.ITEM)
                item = Entity(x, y, '!', libtcod.violet, 'Healing Potion', render_order=RenderOrder.ITEM,
                              item=item_component)

… And don’t forget the import:

...
from components.ai import BasicMonster
from components.fighter import Fighter
+from components.item import Item

from entity import Entity
...
...
from components.ai import BasicMonster
from components.fighter import Fighter
from components.item import Item

from entity import Entity
...

How does our player go about picking things up then? Roguelikes traditionally allow you to pick up an item if you’re standing on top of it, and ours will be no different. Many of them use the ‘g’ key (probably for ‘grab’ or ‘get’) for the pickup command, so we’ll use that as well. Modify handle_keys in input_handlers.py to process the ‘g’ key:

   ...
   elif key_char == 'n':
       return {'move': (1, 1)}

+   if key_char == 'g':
+       return {'pickup': True}

   if key.vk == libtcod.KEY_ENTER and key.lalt:
       ...
    ...
    elif key_char == 'n':
        return {'move': (1, 1)}

    if key_char == 'g':
        return {'pickup': True}

    if key.vk == libtcod.KEY_ENTER and key.lalt:
        ...

Now we’ll need to check for the ‘pickup’ action in our engine.

       ...
       move = action.get('move')
+       pickup = action.get('pickup')
       exit = action.get('exit')
       ...
        ...
        move = action.get('move')
        pickup = action.get('pickup')
        exit = action.get('exit')
        ...

Now we’ll need to actually do something when this ‘pickup’ variable is true. First, let’s create a method for the Inventory class to add an item to the inventory. We’ll use the same “results” pattern as before. If adding the item to the inventory was successful, then we’ll return a result saying we picked up the item. If not, we’ll send a result that indicates a failure. The engine will then have to determine what to do with the item entity.

+import tcod as libtcod

+from game_messages import Message


class Inventory:
   def __init__(self, capacity):
       self.capacity = capacity
       self.items = []

+   def add_item(self, item):
+       results = []
+
+       if len(self.items) >= self.capacity:
+           results.append({
+               'item_added': None,
+               'message': Message('You cannot carry any more, your inventory is full', libtcod.yellow)
+           })
+       else:
+           results.append({
+               'item_added': item,
+               'message': Message('You pick up the {0}!'.format(item.name), libtcod.blue)
+           })
+
+           self.items.append(item)
+
+       return results
import tcod as libtcod

from game_messages import Message


class Inventory:
    def __init__(self, capacity):
        self.capacity = capacity
        self.items = []

    def add_item(self, item):
        results = []

        if len(self.items) >= self.capacity:
            results.append({
                'item_added': None,
                'message': Message('You cannot carry any more, your inventory is full', libtcod.yellow)
            })
        else:
            results.append({
                'item_added': item,
                'message': Message('You pick up the {0}!'.format(item.name), libtcod.blue)
            })

            self.items.append(item)

        return results

Let’s add some code in engine.py to process the results of adding an item to the inventory.

       if move and game_state == GameStates.PLAYERS_TURN:
           ...

+       elif pickup and game_state == GameStates.PLAYERS_TURN:
+           for entity in entities:
+               if entity.item and entity.x == player.x and entity.y == player.y:
+                   pickup_results = player.inventory.add_item(entity)
+                   player_turn_results.extend(pickup_results)
+
+                   break
+           else:
+               message_log.add_message(Message('There is nothing here to pick up.', libtcod.yellow))

       if exit:
           ...
        if move and game_state == GameStates.PLAYERS_TURN:
            ...

        elif pickup and game_state == GameStates.PLAYERS_TURN:
            for entity in entities:
                if entity.item and entity.x == player.x and entity.y == player.y:
                    pickup_results = player.inventory.add_item(entity)
                    player_turn_results.extend(pickup_results)

                    break
            else:
                message_log.add_message(Message('There is nothing here to pick up.', libtcod.yellow))

        if exit:
            ...

We’ll need to import Message for this to work:

-from game_messages import MessageLog
+from game_messages import Message, MessageLog
from game_messages import Message, MessageLog

Basically, we loop through each entity on the map, checking if it’s an item and if it is occupying the same space as the player. If so, we add it to the inventory and tack on the results to player_turn_results, and if not, we create a message informing the player that nothing can be picked up. The ‘break’ statement makes it so that we can only pick up one item at a time (though perhaps in your game, you’ll allow the player to pick up several things at once).

Now let’s process the results of the pickup, in our loop that processes the player’s turn results:

       ...
       for player_turn_result in player_turn_results:
           message = player_turn_result.get('message')
           dead_entity = player_turn_result.get('dead')
+           item_added = player_turn_result.get('item_added')

           if message:
               message_log.add_message(message)

           if dead_entity:
               if dead_entity == player:
                   message, game_state = kill_player(dead_entity)
               else:
                   message = kill_monster(dead_entity)

               message_log.add_message(message)

+           if item_added:
+               entities.remove(item_added)
+
+               game_state = GameStates.ENEMY_TURN

       if game_state == GameStates.ENEMY_TURN:
           ...
        ...
        for player_turn_result in player_turn_results:
            message = player_turn_result.get('message')
            dead_entity = player_turn_result.get('dead')
            item_added = player_turn_result.get('item_added')

            if message:
                message_log.add_message(message)

            if dead_entity:
                if dead_entity == player:
                    message, game_state = kill_player(dead_entity)
                else:
                    message = kill_monster(dead_entity)

                message_log.add_message(message)

            if item_added:
                entities.remove(item_added)

                game_state = GameStates.ENEMY_TURN

        if game_state == GameStates.ENEMY_TURN:
            ...

This just handles removing the entity from the entities list (since it’s now located in the player’s inventory, not the map) and sets the game state to the enemy’s turn. Simple enough.

Run the project, and you should be able to pick the healing potions up off the ground. Of course, this doesn’t do our rogue any good at the moment; we can’t use any of them yet, so we’re really just wasting a turn.

Before we get into how to use items, we need to have a way to look at and select which item to use. We’ll create an inventory interface, which the player can open and select an item from. Let’s start by creating a new file, called menus.py, where we’ll store our menu functions for the inventory and any other menus we’ll need for this tutorial. Put the following code in that file:

import tcod as libtcod


def menu(con, header, options, width, screen_width, screen_height):
    if len(options) > 26: raise ValueError('Cannot have a menu with more than 26 options.')

    # calculate total height for the header (after auto-wrap) and one line per option
    header_height = libtcod.console_get_height_rect(con, 0, 0, width, screen_height, header)
    height = len(options) + header_height

    # create an off-screen console that represents the menu's window
    window = libtcod.console_new(width, height)

    # print the header, with auto-wrap
    libtcod.console_set_default_foreground(window, libtcod.white)
    libtcod.console_print_rect_ex(window, 0, 0, width, height, libtcod.BKGND_NONE, libtcod.LEFT, header)

    # print all the options
    y = header_height
    letter_index = ord('a')
    for option_text in options:
        text = '(' + chr(letter_index) + ') ' + option_text
        libtcod.console_print_ex(window, 0, y, libtcod.BKGND_NONE, libtcod.LEFT, text)
        y += 1
        letter_index += 1

    # blit the contents of "window" to the root console
    x = int(screen_width / 2 - width / 2)
    y = int(screen_height / 2 - height / 2)
    libtcod.console_blit(window, 0, 0, width, height, 0, x, y, 1.0, 0.7)
def menu(con, header, options, width, screen_width, screen_height):
   ...

+def inventory_menu(con, header, inventory, inventory_width, screen_width, screen_height):
+   # show a menu with each item of the inventory as an option
+   if len(inventory.items) == 0:
+       options = ['Inventory is empty.']
+   else:
+       options = [item.name for item in inventory.items]
+
+   menu(con, header, options, inventory_width, screen_width, screen_height)
def menu(con, header, options, width, screen_width, screen_height):
    ...

def inventory_menu(con, header, inventory, inventory_width, screen_width, screen_height):
    # show a menu with each item of the inventory as an option
    if len(inventory.items) == 0:
        options = ['Inventory is empty.']
    else:
        options = [item.name for item in inventory.items]

    menu(con, header, options, inventory_width, screen_width, screen_height)

How might we display this menu? One way is to switch our game state, and when the game state is set to “inventory menu”, we’ll display the menu and accept input for it. So let’s add the option to GameStates:

from enum import Enum


class GameStates(Enum):
   PLAYERS_TURN = 1
   ENEMY_TURN = 2
   PLAYER_DEAD = 3
+   SHOW_INVENTORY = 4
from enum import Enum


class GameStates(Enum):
    PLAYERS_TURN = 1
    ENEMY_TURN = 2
    PLAYER_DEAD = 3
    SHOW_INVENTORY = 4

When do we switch to this new state? Let’s add a new key command to switch to “inventory” mode. As you may have guessed, we’ll press the ‘i’ key to do this.

   ...
   if key_char == 'g':
       return {'pickup': True}

+   elif key_char == 'i':
+       return {'show_inventory': True}

   if key.vk == libtcod.KEY_ENTER and key.lalt:
       ...
    ...
    if key_char == 'g':
        return {'pickup': True}

    elif key_char == 'i':
        return {'show_inventory': True}

    if key.vk == libtcod.KEY_ENTER and key.lalt:
        ...

Not only do we want to switch the game state to SHOW_INVENTORY, we’ll also want to switch back to the previous game state if we exit the menu without doing anything. This makes it so that just opening the inventory doesn’t waste a turn, and so that we can view our inventory after death (this makes it sting that much more). So we’ll need a variable to keep track of the last game state.

   ...
   game_state = GameStates.PLAYERS_TURN
+   previous_game_state = game_state

   while not libtcod.console_is_window_closed():
       ...
    ...
    game_state = GameStates.PLAYERS_TURN
    previous_game_state = game_state

    while not libtcod.console_is_window_closed():
        ...
       ...
       pickup = action.get('pickup')
+       show_inventory = action.get('show_inventory')
       exit = action.get('exit')
       ...
        ...
        pickup = action.get('pickup')
        show_inventory = action.get('show_inventory')
        exit = action.get('exit')
        ...
       elif pickup and game_state == GameStates.PLAYERS_TURN:
           ...

+       if show_inventory:
+           previous_game_state = game_state
+           game_state = GameStates.SHOW_INVENTORY

       if exit:
-           return True
+           if game_state == GameStates.SHOW_INVENTORY:
+               game_state = previous_game_state
+           else:
+               return True
        elif pickup and game_state == GameStates.PLAYERS_TURN:
            ...

        if show_inventory:
            previous_game_state = game_state
            game_state = GameStates.SHOW_INVENTORY

        if exit:
            if game_state == GameStates.SHOW_INVENTORY:
                game_state = previous_game_state
            else:
                return True

We’re modifying our previous “exit” section to just revert back to the previous game state if we open the inventory. That way, the “escape” key just closes the menu, rather than closing the game.

Now we’re switching the game’s state, but we still need to display the menu. Needless to say, we’ll need to modify render_all. Because we’ll only display the inventory when the game state is SHOW_INVENTORY, the render_all function will need to be aware of the state. Modify the call in engine.py:

-render_all(con, panel, entities, player, game_map, fov_map, fov_recompute, message_log, screen_width,
-                  screen_height, bar_width, panel_height, panel_y, mouse, colors)
+render_all(con, panel, entities, player, game_map, fov_map, fov_recompute, message_log, screen_width,
+                  screen_height, bar_width, panel_height, panel_y, mouse, colors, game_state)
render_all(con, panel, entities, player, game_map, fov_map, fov_recompute, message_log, screen_width,
                   screen_height, bar_width, panel_height, panel_y, mouse, colors, game_state)

And now for the definition:

def render_all(con, panel, entities, player, game_map, fov_map, fov_recompute, message_log, screen_width, screen_height,
-              bar_width, panel_height, panel_y, mouse, colors):
+              bar_width, panel_height, panel_y, mouse, colors, game_state):
   ...
   ...
   libtcod.console_blit(panel, 0, 0, screen_width, panel_height, 0, 0, panel_y)

+   if game_state == GameStates.SHOW_INVENTORY:
+       inventory_menu(con, 'Press the key next to an item to use it, or Esc to cancel.\n',
+                      player.inventory, 50, screen_width, screen_height)
def render_all(con, panel, entities, player, game_map, fov_map, fov_recompute, message_log, screen_width, screen_height,
               bar_width, panel_height, panel_y, mouse, colors, game_state):
    ...
    ...
    libtcod.console_blit(panel, 0, 0, screen_width, panel_height, 0, 0, panel_y)

    if game_state == GameStates.SHOW_INVENTORY:
        inventory_menu(con, 'Press the key next to an item to use it, or Esc to cancel.\n',
                       player.inventory, 50, screen_width, screen_height)

We’ll need to import GameStates and inventory_menu for this to work.

import tcod as libtcod

from enum import Enum

+from game_states import GameStates
+
+from menus import inventory_menu


class RenderOrder(Enum):
   ...
import tcod as libtcod

from enum import Enum

from game_states import GameStates

from menus import inventory_menu


class RenderOrder(Enum):
    ...

Run the project now. You should be able to open the inventory menu and see all the items you’ve picked up so far. We can’t quite use them yet, but we’re almost there!

In order to actually select an inventory item, we’ll need to modify handle_keys. Why? Because we’re selecting items using the letter keys, which is fine and good, but some of those keys we’re using for movement and other things. It’d be nice if our function reacted differently depending on what our game’s state was.

Here’s what we’ll do: We’ll split handle_keys up into several different functions, which will return different results depending on the game’s state. Rename the handle_keys function to handle_player_turn_keys:

-def handle_keys(key):
+def handle_player_turn_keys(key):
def handle_keys(key):
def handle_player_turn_keys(key):

Then, create a new handle_keys function, which calls handle_player_turn_keys

import tcod as libtcod

+from game_states import GameStates
+
+
+def handle_keys(key, game_state):
+   if game_state == GameStates.PLAYERS_TURN:
+       return handle_player_turn_keys(key)
+
+   return {}


def handle_player_turn_keys(key):
   ...
import tcod as libtcod

from game_states import GameStates


def handle_keys(key, game_state):
    if game_state == GameStates.PLAYERS_TURN:
        return handle_player_turn_keys(key)

    return {}


def handle_player_turn_keys(key):
    ...

Don’t forget to modify the call to handle_keys in engine.py:

-action = handle_keys(key)
+action = handle_keys(key, game_state)
action = handle_keys(key, game_state)

Before we move on to the inventory part, let’s cover our bases and put in a key handler for when the player is dead.

...
def handle_keys(key, game_state):
   if game_state == GameStates.PLAYERS_TURN:
       return handle_player_turn_keys(key)
+   elif game_state == GameStates.PLAYER_DEAD:
+       return handle_player_dead_keys(key)

   return {}


def handle_player_turn_keys(key):
   ...


+def handle_player_dead_keys(key):
+   key_char = chr(key.c)
+
+   if key_char == 'i':
+       return {'show_inventory': True}
+
+   if key.vk == libtcod.KEY_ENTER and key.lalt:
+       # Alt+Enter: toggle full screen
+       return {'fullscreen': True}
+   elif key.vk == libtcod.KEY_ESCAPE:
+       # Exit the menu
+       return {'exit': True}
+
+   return {}
...
def handle_keys(key, game_state):
    if game_state == GameStates.PLAYERS_TURN:
        return handle_player_turn_keys(key)
    elif game_state == GameStates.PLAYER_DEAD:
        return handle_player_dead_keys(key)

    return {}


def handle_player_turn_keys(key):
    ...


def handle_player_dead_keys(key):
    key_char = chr(key.c)

    if key_char == 'i':
        return {'show_inventory': True}

    if key.vk == libtcod.KEY_ENTER and key.lalt:
        # Alt+Enter: toggle full screen
        return {'fullscreen': True}
    elif key.vk == libtcod.KEY_ESCAPE:
        # Exit the menu
        return {'exit': True}

    return {}

Now, let’s create a new function, called handle_inventory_keys, which will handle our input when the inventory menu is open.

...
def handle_keys(key, game_state):
   if game_state == GameStates.PLAYERS_TURN:
       return handle_player_turn_keys(key)
   elif game_state == GameStates.PLAYER_DEAD:
       return handle_player_dead_keys(key)
+   elif game_state == GameStates.SHOW_INVENTORY:
+       return handle_inventory_keys(key)

   return {}
...

+def handle_inventory_keys(key):
+   index = key.c - ord('a')
+
+   if index >= 0:
+       return {'inventory_index': index}
+
+   if key.vk == libtcod.KEY_ENTER and key.lalt:
+       # Alt+Enter: toggle full screen
+       return {'fullscreen': True}
+   elif key.vk == libtcod.KEY_ESCAPE:
+       # Exit the menu
+       return {'exit': True}
+
+   return {}
...
def handle_keys(key, game_state):
    if game_state == GameStates.PLAYERS_TURN:
        return handle_player_turn_keys(key)
    elif game_state == GameStates.PLAYER_DEAD:
        return handle_player_dead_keys(key)
    elif game_state == GameStates.SHOW_INVENTORY:
        return handle_inventory_keys(key)

    return {}
...

def handle_inventory_keys(key):
    index = key.c - ord('a')

    if index >= 0:
        return {'inventory_index': index}

    if key.vk == libtcod.KEY_ENTER and key.lalt:
        # Alt+Enter: toggle full screen
        return {'fullscreen': True}
    elif key.vk == libtcod.KEY_ESCAPE:
        # Exit the menu
        return {'exit': True}

    return {}

What’s with the ord function? Long story short, we’re converting the key pressed to an index. ‘a’ will be 0, ‘b’ will be 1, and so on. This will allow us to select an item out of the inventory in engine.py. All we need the input handler to do is give us the index of what we picked; it doesn’t need to know anything about the item or what it should do.

Let’s get this index in engine.py and do something with it!

       ...
       show_inventory = action.get('show_inventory')
+       inventory_index = action.get('inventory_index')
       exit = action.get('exit')
       ...
        ...
        show_inventory = action.get('show_inventory')
        inventory_index = action.get('inventory_index')
        exit = action.get('exit')
        ...
       ...
       if show_inventory:
           ...

+       if inventory_index is not None and previous_game_state != GameStates.PLAYER_DEAD and inventory_index < len(
+               player.inventory.items):
+           item = player.inventory.items[inventory_index]
+           print(item)

       if exit:
           ...
        ...
        if show_inventory:
            ...

        if inventory_index is not None and previous_game_state != GameStates.PLAYER_DEAD and inventory_index < len(
                player.inventory.items):
            item = player.inventory.items[inventory_index]
            print(item)

        if exit:
            ...

*Note: The print statement is just a placeholder for now. We’re close to actually using the item, I promise!

We’re taking the index that we selected, and “using” (printing for now) the item selected. Run the project and verify that this works. Now that we have a way of opening the menu and selecting an item, we can, at long last, move on to using the item.

So, how do we use the item? The Item component seems like an obvious place to do something. But, each item should do something different, right? So the Item class won’t actually contain the functions for healing the player, or doing damage to an enemy. Instead, it will just hold the healing and damaging functions, along with whatever arguments we need to make that function work. We’ll then take the results of that function (like usual) and process them.

Modify Item like this:

class Item:
-   def __init__(self):
+   def __init__(self, use_function=None, **kwargs):
-       pass
+       self.use_function = use_function
+       self.function_kwargs = kwargs
class Item:
    def __init__(self, use_function=None, **kwargs):
        pass
        self.use_function = use_function
        self.function_kwargs = kwargs

What about the actual function though? We’ll define that separately, which will allow us (in the next chapter) to freely assign functions to the use_function, thus changing the behavior of each item depending on our needs.

Create a file, called item_functions.py, and put the following function in it:

import tcod as libtcod

from game_messages import Message


def heal(*args, **kwargs):
    entity = args[0]
    amount = kwargs.get('amount')

    results = []

    if entity.fighter.hp == entity.fighter.max_hp:
        results.append({'consumed': False, 'message': Message('You are already at full health', libtcod.yellow)})
    else:
        entity.fighter.heal(amount)
        results.append({'consumed': True, 'message': Message('Your wounds start to feel better!', libtcod.green)})

    return results

We’re taking the entity that’s using the item as the first argument (all of our functions will do this, even if they don’t need it). We’re also extracting the “amount” from “kwargs”, which will be provided by the function_kwargs from the Item component.

We’ll need to add the heal method to the Fighter component for this to work right (note: both functions are called “heal”, but they are not the same).

   ...
   def take_damage(self, amount):
       ...

+   def heal(self, amount):
+       self.hp += amount
+
+       if self.hp > self.max_hp:
+           self.hp = self.max_hp

   def attack(self, target):
       ...
    ...
    def take_damage(self, amount):
        ...

    def heal(self, amount):
        self.hp += amount

        if self.hp > self.max_hp:
            self.hp = self.max_hp

    def attack(self, target):
        ...

This may make more sense once we pass the heal function to the healing potions. Let’s do that now; in the place_entities function in game_map:

           ...
           if not any([entity for entity in entities if entity.x == x and entity.y == y]):
-               item_component = Item()
+               item_component = Item(use_function=heal, amount=4)
               item = Entity(x, y, '!', libtcod.violet, 'Healing Potion', render_order=RenderOrder.ITEM,
                             item=item_component)
            ...
            if not any([entity for entity in entities if entity.x == x and entity.y == y]):
                item_component = Item()
                item_component = Item(use_function=heal, amount=4)
                item = Entity(x, y, '!', libtcod.violet, 'Healing Potion', render_order=RenderOrder.ITEM,
                              item=item_component)

You’ll need to import heal for this.

...
from entity import Entity

+from item_functions import heal

from map_objects.rectangle import Rect
...
...
from entity import Entity

from item_functions import heal

from map_objects.rectangle import Rect
...

Now our item has an actual function to fire off when it gets used. But where does this get called? Why not have our inventory call the function? Add the following functions to Inventory:

   ...
+   def use(self, item_entity, **kwargs):
+       results = []
+
+       item_component = item_entity.item
+
+       if item_component.use_function is None:
+           results.append({'message': Message('The {0} cannot be used'.format(item_entity.name), libtcod.yellow)})
+       else:
+           kwargs = {**item_component.function_kwargs, **kwargs}
+           item_use_results = item_component.use_function(self.owner, **kwargs)
+
+           for item_use_result in item_use_results:
+               if item_use_result.get('consumed'):
+                   self.remove_item(item_entity)
+
+           results.extend(item_use_results)
+
+       return results
+
+   def remove_item(self, item):
+       self.items.remove(item)
    ...
    def use(self, item_entity, **kwargs):
        results = []

        item_component = item_entity.item

        if item_component.use_function is None:
            results.append({'message': Message('The {0} cannot be used'.format(item_entity.name), libtcod.yellow)})
        else:
            kwargs = {**item_component.function_kwargs, **kwargs}
            item_use_results = item_component.use_function(self.owner, **kwargs)

            for item_use_result in item_use_results:
                if item_use_result.get('consumed'):
                    self.remove_item(item_entity)

            results.extend(item_use_results)

        return results

    def remove_item(self, item):
        self.items.remove(item)
       ...
       if inventory_index is not None and previous_game_state != GameStates.PLAYER_DEAD and inventory_index < len(
               player.inventory.items):
           item = player.inventory.items[inventory_index]
-           print(item)
+           player_turn_results.extend(player.inventory.use(item))
        ...
        if inventory_index is not None and previous_game_state != GameStates.PLAYER_DEAD and inventory_index < len(
                player.inventory.items):
            item = player.inventory.items[inventory_index]
            print(item)
            player_turn_results.extend(player.inventory.use(item))

Finally, let’s handle the results of the item use function in engine.py:

       ...
       for player_turn_result in player_turn_results:
           message = player_turn_result.get('message')
           dead_entity = player_turn_result.get('dead')
           item_added = player_turn_result.get('item_added')
+           item_consumed = player_turn_result.get('consumed')
           ...
           if item_added:
               entities.remove(item_added)

               game_state = GameStates.ENEMY_TURN

+           if item_consumed:
+               game_state = GameStates.ENEMY_TURN
           ...
        ...
        for player_turn_result in player_turn_results:
            message = player_turn_result.get('message')
            dead_entity = player_turn_result.get('dead')
            item_added = player_turn_result.get('item_added')
            item_consumed = player_turn_result.get('consumed')
            ...
            if item_added:
                entities.remove(item_added)

                game_state = GameStates.ENEMY_TURN

            if item_consumed:
                game_state = GameStates.ENEMY_TURN
            ...

Run the project now. You can now consume the health potions, and this will take a turn. Potions will not be used if you’re at full health already.

Final thing before we end the chapter: dropping items. This may seem pointless now, but later on, when we have multiple levels in the dungeon, the player may have to make some decisions about which items to keep and which to drop.

First, add a new game state:

class GameStates(Enum):
   PLAYERS_TURN = 1
   ENEMY_TURN = 2
   PLAYER_DEAD = 3
   SHOW_INVENTORY = 4
+   DROP_INVENTORY = 5
class GameStates(Enum):
    PLAYERS_TURN = 1
    ENEMY_TURN = 2
    PLAYER_DEAD = 3
    SHOW_INVENTORY = 4
    DROP_INVENTORY = 5

Then, modify handle_player_turn_keys to respond to the ’d’ key:

   ...
   elif key_char == 'i':
       return {'show_inventory': True}

+   elif key_char == 'd':
+       return {'drop_inventory': True}

   if key.vk == libtcod.KEY_ENTER and key.lalt:
       ...
    ...
    elif key_char == 'i':
        return {'show_inventory': True}

    elif key_char == 'd':
        return {'drop_inventory': True}

    if key.vk == libtcod.KEY_ENTER and key.lalt:
        ...

Update the action handler section to accept this:

       ...
       show_inventory = action.get('show_inventory')
+       drop_inventory = action.get('drop_inventory')
       inventory_index = action.get('inventory_index')
       ...
        ...
        show_inventory = action.get('show_inventory')
        drop_inventory = action.get('drop_inventory')
        inventory_index = action.get('inventory_index')
        ...

We’ll need to switch the game state when the player presses this key:

       ...
       if show_inventory:
           previous_game_state = game_state
           game_state = GameStates.SHOW_INVENTORY

+       if drop_inventory:
+           previous_game_state = game_state
+           game_state = GameStates.DROP_INVENTORY

       if inventory_index is not None and previous_game_state != GameStates.PLAYER_DEAD and inventory_index < len(
               player.inventory.items):
           ...
        ...
        if show_inventory:
            previous_game_state = game_state
            game_state = GameStates.SHOW_INVENTORY

        if drop_inventory:
            previous_game_state = game_state
            game_state = GameStates.DROP_INVENTORY

        if inventory_index is not None and previous_game_state != GameStates.PLAYER_DEAD and inventory_index < len(
                player.inventory.items):
            ...

You might think we need to add another function to handle the keys for dropping inventory, but we actually don’t; we can just use our code for SHOW_INVENTORY for this same purpose. Just modify the “if” statement in handle_keys:

def handle_keys(key, game_state):
   if game_state == GameStates.PLAYERS_TURN:
       return handle_player_turn_keys(key)
   elif game_state == GameStates.PLAYER_DEAD:
       return handle_player_dead_keys(key)
-   elif game_state == GameStates.SHOW_INVENTORY:
+   elif game_state in (GameStates.SHOW_INVENTORY, GameStates.DROP_INVENTORY):
       return handle_inventory_keys(key)

   return {}
def handle_keys(key, game_state):
    if game_state == GameStates.PLAYERS_TURN:
        return handle_player_turn_keys(key)
    elif game_state == GameStates.PLAYER_DEAD:
        return handle_player_dead_keys(key)
    elif game_state == GameStates.SHOW_INVENTORY:
    elif game_state in (GameStates.SHOW_INVENTORY, GameStates.DROP_INVENTORY):
        return handle_inventory_keys(key)

    return {}

Also, modify the exit section:

       ...
       if exit:
-           if game_state in GameStates.SHOW_INVENTORY:
+           if game_state in (GameStates.SHOW_INVENTORY, GameStates.DROP_INVENTORY):
               game_state = previous_game_state
           else:
               return True
       ...
        ...
        if exit:
            if game_state in GameStates.SHOW_INVENTORY:
            if game_state in (GameStates.SHOW_INVENTORY, GameStates.DROP_INVENTORY):
                game_state = previous_game_state
            else:
                return True
        ...

Now for displaying the drop menu. It’s really not different from the inventory menu, so we can use the same function, and send a different title to it.

   ...
-   if game_state == GameStates.SHOW_INVENTORY:
-       inventory_menu(con, 'Press the key next to an item to use it, or Esc to cancel.\n',
-                      player.inventory, 50, screen_width, screen_height)

+   if game_state in (GameStates.SHOW_INVENTORY, GameStates.DROP_INVENTORY):
+       if game_state == GameStates.SHOW_INVENTORY:
+           inventory_title = 'Press the key next to an item to use it, or Esc to cancel.\n'
+       else:
+           inventory_title = 'Press the key next to an item to drop it, or Esc to cancel.\n'
+
+       inventory_menu(con, inventory_title, player.inventory, 50, screen_width, screen_height)
    ...
    if game_state == GameStates.SHOW_INVENTORY:
        inventory_menu(con, 'Press the key next to an item to use it, or Esc to cancel.\n',
                       player.inventory, 50, screen_width, screen_height)

    if game_state in (GameStates.SHOW_INVENTORY, GameStates.DROP_INVENTORY):
        if game_state == GameStates.SHOW_INVENTORY:
            inventory_title = 'Press the key next to an item to use it, or Esc to cancel.\n'
        else:
            inventory_title = 'Press the key next to an item to drop it, or Esc to cancel.\n'

        inventory_menu(con, inventory_title, player.inventory, 50, screen_width, screen_height)

What happens when the player pressed a key in this menu? Let’s modify the part in engine.py that handled the inventory_index to take the game’s state into account. If it’s SHOW_INVENTORY, then use the item, and if it’s DROP_INVENTORY, then call a function to drop the item.

if inventory_index is not None and previous_game_state != GameStates.PLAYER_DEAD and inventory_index < len(
               player.inventory.items):
           item = player.inventory.items[inventory_index]

-           player_turn_results.extend(player.inventory.use(item))

+           if game_state == GameStates.SHOW_INVENTORY:
               player_turn_results.extend(player.inventory.use(item))
+           elif game_state == GameStates.DROP_INVENTORY:
+               player_turn_results.extend(player.inventory.drop_item(item))
if inventory_index is not None and previous_game_state != GameStates.PLAYER_DEAD and inventory_index < len(
                player.inventory.items):
            item = player.inventory.items[inventory_index]

            if game_state == GameStates.SHOW_INVENTORY:
                player_turn_results.extend(player.inventory.use(item))
            elif game_state == GameStates.DROP_INVENTORY:
                player_turn_results.extend(player.inventory.drop_item(item))

We haven’t defined the drop_item method of inventory yet, so let’s do that now. It will remove the item from the inventory, set the item’s coordinates to match the player (since the item gets dropped at the player’s feet), and return the results.

   ...
   def remove_item(self, item):
       self.items.remove(item)

+   def drop_item(self, item):
+       results = []
+
+       item.x = self.owner.x
+       item.y = self.owner.y
+
+       self.remove_item(item)
+       results.append({'item_dropped': item, 'message': Message('You dropped the {0}'.format(item.name),
+                                                                libtcod.yellow)})
+
+       return results
    ...
    def remove_item(self, item):
        self.items.remove(item)

    def drop_item(self, item):
        results = []

        item.x = self.owner.x
        item.y = self.owner.y

        self.remove_item(item)
        results.append({'item_dropped': item, 'message': Message('You dropped the {0}'.format(item.name),
                                                                 libtcod.yellow)})

        return results

Finally, handle the results in engine.py:

       ...
       for player_turn_result in player_turn_results:
           message = player_turn_result.get('message')
           dead_entity = player_turn_result.get('dead')
           item_added = player_turn_result.get('item_added')
           item_consumed = player_turn_result.get('consumed')
+           item_dropped = player_turn_result.get('item_dropped')
           ...
           if item_consumed:
               game_state = GameStates.ENEMY_TURN

+           if item_dropped:
+               entities.append(item_dropped)
+
+               game_state = GameStates.ENEMY_TURN
        ...
        for player_turn_result in player_turn_results:
            message = player_turn_result.get('message')
            dead_entity = player_turn_result.get('dead')
            item_added = player_turn_result.get('item_added')
            item_consumed = player_turn_result.get('consumed')
            item_dropped = player_turn_result.get('item_dropped')
            ...
            if item_consumed:
                game_state = GameStates.ENEMY_TURN

            if item_dropped:
                entities.append(item_dropped)

                game_state = GameStates.ENEMY_TURN

That was quite the chapter! It took a lot of setup to get using the items, but now we’ve got a framework that we can add on to. In the next chapter, we’ll do just that, by adding some scrolls to cast spells at our enemies.

If you want to see the code so far in its entirety, click here.

Click here to move on to the next part of this tutorial.