What good is a dungeon with no monsters to bash? This chapter will focus on placing the enemies throughout the dungeon, and setting them up to be attacked (the actual attacking part we'll save for next time). Before we do that though, we'll need to update the colors dictionary to hold the colors for the monster's we're creating, Orcs and Trolls in this case.

    colors = {
        'dark_wall': (0, 0, 100),
        'dark_ground': (50, 50, 150),
        'light_wall': (130, 110, 50),
        'light_ground': (200, 180, 50),
        'desaturated_green': (63, 127, 63),
        'darker_green': (0, 127, 0)
    }

With that done, let's move on to the map_utils.py file. Modify the import section to include the Entity class; we'll need it in the next code block.

from tdl.map import Map

from random import randint

from entity import Entity


class GameMap(Map):
...

Now let's create our function to place the entities on the map. Add this to map_utils.py, right above the make_map function:

def place_entities(room, entities, max_monsters_per_room, colors):
    # Get a random number of monsters
    number_of_monsters = randint(0, max_monsters_per_room)

    for i in range(number_of_monsters):
        # Choose a random location in the room
        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]):
            if randint(0, 100) < 80:
                monster = Entity(x, y, 'o', colors.get('desaturated_green'))
            else:
                monster = Entity(x, y, 'T', colors.get('darker_green'))

            entities.append(monster)

Let's use this new function in the make_map function:

                    ...
                    create_h_tunnel(game_map, prev_x, new_x, new_y)

            place_entities(new_room, entities, max_monsters_per_room, colors)

            rooms.append(new_room)
            ...

We need to modify the definition of make_map to take in these extra data points, so change its definition like this:

def make_map(game_map, max_rooms, room_min_size, room_max_size, map_width, map_height, player):
def make_map(game_map, max_rooms, room_min_size, room_max_size, map_width, map_height, player, entities,
             max_monsters_per_room, colors):

We're all set up here; now we have to modify engine.py to match this new make_map function. Also, we'll need to create the max_room_per_monsters variable before calling the function. Finally, we'll change our entities list to include only the player at first, and we'll completely remove our dummy NPC from before.

    ...
    fov_radius = 10

    max_monsters_per_room = 3

    colors = {
        'dark_wall': (0, 0, 100),
        'dark_ground': (50, 50, 150),
        'light_wall': (130, 110, 50),
        'light_ground': (200, 180, 50),
        'desaturated_green': (63, 127, 63),
        'darker_green': (0, 127, 0)
    }

    player = Entity(int(screen_width / 2), int(screen_height / 2), '@', (255, 255, 255))
    npc = Entity(int(screen_width / 2 - 5), int(screen_height / 2), '@', (255, 255, 0))
    entities = [npc, player]
    player = Entity(0, 0, '@', (255, 255, 255))
    entities = [player]

    tdl.set_font('arial10x10.png', greyscale=True, altLayout=True)

    root_console = tdl.init(screen_width, screen_height, title='Roguelike Tutorial Revised')
    con = tdl.Console(screen_width, screen_height)

    game_map = GameMap(map_width, map_height)
    make_map(game_map, max_rooms, room_min_size, room_max_size, map_width, map_height, player)
    make_map(game_map, max_rooms, room_min_size, room_max_size, map_width, map_height, player, entities,
             max_monsters_per_room, colors)

    fov_recompute = True
    ...

Let's modify the Entity class to include the "blocks" variable. While we're modifying this class, we should also pass in a "name" for the Entity, which will be useful a little later.

class Entity:
    def __init__(self, x, y, char, color):
    def __init__(self, x, y, char, color, name, blocks=False):
        self.x = x
        self.y = y
        self.char = char
        self.color = color
        self.name = name
        self.blocks = blocks

    def move(self, dx, dy):
    ...

Notice that "blocks" is optional; if we don't pass it on initialization, it will be False by default.

Go back to map_utils.py and modify the place_entities method, where we declare our monsters.

            if randint(0, 100) < 80:
                monster = Entity(x, y, 'o', colors.get('desaturated_green'))
                monster = Entity(x, y, 'o', colors.get('desaturated_green'), 'Orc', blocks=True)
            else:
                monster = Entity(x, y, 'T', colors.get('darker_green'))
                monster = Entity(x, y, 'T', colors.get('darker_green'), 'Troll', blocks=True)

We also need to update the initialization of the player in engine.py:

    player = Entity(0, 0, '@', (255, 255, 255))
    player = Entity(0, 0, '@', (255, 255, 255), 'Player', blocks=True)

With our new attribute in place, we need to make a check if a blocking entity is in the way when we try to move into a tile. One thing that will definitely help is a function to get a "blocking" entity in a tile, given the list of entities and the x and y coordinates. We'll put this function in entity.py, but not in the Entity class itself. The reasoning is that it's a function that relates to the entities, but it doesn't relate to a specific Entity, so it doesn't need to belong to the class.

Add the function to entity.py like this:

class Entity:
    ...


def get_blocking_entities_at_location(entities, destination_x, destination_y):
    for entity in entities:
        if entity.blocks and entity.x == destination_x and entity.y == destination_y:
            return entity

    return None

The function loops through the entities, and if one of them is "blocking" and is at the x and y location we specified, we return it. If none of them match, then we return "None" instead. Note that the function is assuming that only one "blocking" entity will be at each location; this should be fine, as we'll make sure two entities can't move into the same tile.

With that in place, let's return to our movement function. Modify the code that moves the player in engine.py like this:

        if move:
            dx, dy = move
            destination_x = player.x + dx
            destination_y = player.y + dy

            if game_map.walkable[player.x + dx, player.y + dy]:
            if game_map.walkable[destination_x, destination_y]:
                target = get_blocking_entities_at_location(entities, destination_x, destination_y)

                if target:
                    print('You kick the ' + target.name + ' in the shins, much to its annoyance!')
                else:
                    player.move(dx, dy)

                    fov_recompute = True

Also be sure to import the function get_blocking_entities_at_location at the top of engine.py.

from entity import Entity, get_blocking_entities_at_location

Now the player gets blocked when trying to move through another entity. We're putting that humorous (hey, I think it's funny!) print statement as a placeholder, for the moment. We'll implement real combat in the next chapter.

Our player should only be able to move during their turn, and the same applies for the monsters. We'll need a variable to keep track of whose turn it actually is. We could store a string in this variable, say, 'players_turn' and 'enemy_turn', but that seems error prone. If you happen to mistype one of those strings, you'll end up with some bugs. Not to mention our number of game states will inevitably grow, and we'll need a better way to keep track of them all.

Let's keep the game states in an Enum. An "Enum" is a set of named values that won't change, so it's perfect for things like game states. Create a new file called game_states.py and put the following class in it:

from enum import Enum


class GameStates(Enum):
    PLAYERS_TURN = 1
    ENEMY_TURN = 2

This will make our game state switching much easier to manage, especially in the future when we have more than two.

* Note: The numbers for the states don't necessarily mean anything. In fact, if you're using Python 3.6, you can use the 'auto' feature to just increment the number for you. Check it our if you're able to.

Let's put this new GameStates enum into action. Start by importing it at the top.

...
from entity import Entity, get_blocking_entities_at_location
from game_states import GameStates
from input_handlers import handle_keys
...

Then, create a variable called game_state, which we'll set initially to the player's turn.

    ...
    fov_recompute = True

    game_state = GameStates.PLAYERS_TURN

    while not tdl.event.is_window_closed():
    ...

Depending on whether or not its the players turn, we want to control the player's movement. The player can only move on the players turn, so let's modify our if move: section to handle this. After the player successfully moves, we'll set the state to ENEMY_TURN.

        if move:
        if move and game_state == GameStates.PLAYERS_TURN:
            dx, dy = move
            destination_x = player.x + dx
            destination_y = player.y + dy

            if not game_map.is_blocked(destination_x, destination_y):
                target = get_blocking_entities_at_location(entities, destination_x, destination_y)

                if target:
                    print('You kick the ' + target.name + ' in the shins, much to its annoyance!')
                else:
                    player.move(dx, dy)

                    fov_recompute = True

                game_state = GameStates.ENEMY_TURN

If you run the project now, the player will be able to move once... and then get stuck forever. That's because we need to implement the enemy's moves, and set the game_state back to the player's turn afterwards. Note that you can exit the game and make it full screen, because we're not stopping the player from doing those things when it isn't the player's turn.

        ...
        if fullscreen:
            tdl.set_fullscreen(not tdl.get_fullscreen())

        if game_state == GameStates.ENEMY_TURN:
            for entity in entities:
                if entity != player:
                    print('The ' + entity.name + ' ponders the meaning of its existence.')

            game_state = GameStates.PLAYERS_TURN

This is simple enough. Assuming it's the enemy turn, we're looping through each of the entities (excluding the player) and allowing them to take a turn. Right now, we don't have any AI in place for our enemies, so they'll just sit there contemplating their lives for now. In the next chapter, we'll give them some more interesting behavior, but for now, this works as a placeholder.

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.