Now that we can move our little '@' symbol around, we need to give it something to move around in. But before that, let's stop for a moment and think about the player object itself.

Right now, we just represent the player with the '@' symbol, and its x and y coordinates. Shouldn't we tie those things together in an object, along with some other data and functions that pertain to it?

Let's create a generic class to represent not just the player, but just about everything in our game world. Enemies, items, and whatever other foreign entities we can dream of will be part of this class, which we'll call Entity.

Create a new file, and call it entity.py. In that file, put the following class:

class Entity:
    """
    A generic object to represent players, enemies, items, etc.
    """
    def __init__(self, x, y, char, color):
        self.x = x
        self.y = y
        self.char = char
        self.color = color

    def move(self, dx, dy):
        # Move the entity by a given amount
        self.x += dx
        self.y += dy

This is pretty self explanatory. The Entity class holds the x and y coordinates, along with the character (the '@' symbol in the player's case) and the color (white for the player by default). We also have a method called move, which will allow the entity to be moved around by a given x and y.

Let's put our fancy new class into action! Modify the first part of engine.py to look like this:

import tdl

from entity import Entity
from input_handlers import handle_keys


def main():
    screen_width = 80
    screen_height = 50

    player_x = int(screen_width / 2)
    player_y = int(screen_height / 2)

    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]
    ...

We're importing the Entity class into engine.py, and using it to initialize the player and a new NPC. We store these two in a list, which will eventually hold all our entities on the map.

Also modify the part where we handle movement so that the Entity class handles the actual movement.

        if move:
            dx, dy = move
            player_x += dx
            player_x += dy
            player.move(dx, dy)

Now we need to alter the way that the entities are drawn to the screen. Let's write some functions to draw not only the player, but any entity currently in our entities list.

Create a new file called render_functions.py. This will hold our functions for drawing and clearing from the screen. Put the following code in that file.

def render_all(con, entities, root_console, screen_width, screen_height):
    # Draw all entities in the list
    for entity in entities:
        draw_entity(con, entity)

    root_console.blit(con, 0, 0, screen_width, screen_height, 0, 0)


def clear_all(con, entities):
    for entity in entities:
        clear_entity(con, entity)


def draw_entity(con, entity):
    con.draw_char(entity.x, entity.y, entity.char, entity.color, bg=None)


def clear_entity(con, entity):
    # erase the character that represents this object
    con.draw_char(entity.x, entity.y, ' ', entity.color, bg=None)

Here's a quick breakdown of what these functions do:

Now that we've gotten a few functions to assist drawing the entities, let's put them to use. Make the following modifications to the section where we drew the player (in engine.py).

    ...
    while not tdl.event.is_window_closed():
        con.draw_char(player_x, player_y, '@', bg=None, fg=(255, 255, 255))
        root_console.blit(con, 0, 0, screen_width, screen_height, 0, 0)
        render_all(con, entities, root_console, screen_width, screen_height)

        tdl.flush()

        con.draw_char(player_x, player_y, ' ', bg=None)
        clear_all(con, entities)

        for event in tdl.event.get():
        ...

Don't forget to import render_all and clear_all at the top of your file. Your imports section should now look something like this:

import tdl

from entity import Entity
from input_handlers import handle_keys
from render_functions import clear_all, render_all

If you run the project now, you should see your '@' symbol, along with a yellow one representing our NPC. It doesn't do anything, yet, but now we have a method for drawing more than one character to the screen.

It's time to consider our map. Luckily, tdl includes a Map object which we can use to store and interact with our map. With just a few extra functions, we can have a working map up and running quite quickly.

We'd better start by defining the size of our map. Add these variables right below where you defined the screen width and height:

    ...
    screen_height = 50
    map_width = 80
    map_height = 45

Now let's create our map and store it to a variable called game_map.

    ...
    con = tdl.Console(screen_width, screen_height)

    game_map = tdl.map.Map(map_width, map_height)

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

We're going to modify the map to create the structures we want. For that, let's create a new file, called map_utils.py. In there, add the following function:

def make_map(game_map):
    for x, y in game_map:
        game_map.walkable[x, y] = True
        game_map.transparent[x, y] = True

    game_map.walkable[30, 22] = False
    game_map.transparent[30, 22] = False
    game_map.walkable[31, 22] = False
    game_map.transparent[31, 22] = False
    game_map.walkable[32, 22] = False
    game_map.transparent[32, 22] = False

The map is set to not walkable and not transparent (walled off) by default. This makes it easier to do our dungeon generation, but for now, we want to be able to walk around freely, so we set all the tiles to walkable and transparent for the moment. We're setting a three-tile wall in the map, just for demonstration purposes.

Go back to engine.py, where we'll make a few changes so that our map gets initialized and then drawn to the screen.

Firstly, we need to define what colors to draw for blocked and non-blocked tiles. Let's set up a dictionary that holds the colors we'll be using for now (it will expand as this tutorial goes on).

    ...
    map_height = 45

    colors = {
        'dark_wall': (0, 0, 100),
        'dark_ground': (50, 50, 150)
    }

    player = Entity(int(screen_width / 2), int(screen_height / 2), '@', (255, 255, 255))

These colors will serve as our wall and ground outside the FOV, when we get there (hence the 'dark' in the names).

Now let's utilize that make_map function from before.

    con = tdl.Console(screen_width, screen_height)

    game_map = tdl.map.Map(map_width, map_height)
    make_map(game_map)

    while not tdl.event.is_window_closed():

Don't forget to import the function so we can use it here.

from entity import Entity
from input_handlers import handle_keys
from map_utils import make_map
from render_functions import clear_all, render_all

Now that our map object is ready to go, let's pass it to render_all so that we can draw it. We'll also pass the colors dictionary, because render_all will need to know what colors to draw the various parts of the map. Note that the order in which you pass these arguments doesn't matter, it just has to match in the function definition and when you call it.

        render_all(con, entities, root_console, screen_width, screen_height)
    	render_all(con, entities, game_map, root_console, screen_width, screen_height, colors)

Open up render_functions.py and modify render_all like this:

def render_all(con, entities, root_console, screen_width, screen_height):
def render_all(con, entities, game_map, root_console, screen_width, screen_height, colors):
    # Draw all the tiles in the game map
    for x, y in game_map:
        wall = not game_map.transparent[x, y]

        if wall:
            con.draw_char(x, y, None, fg=None, bg=colors.get('dark_wall'))
        else:
            con.draw_char(x, y, None, fg=None, bg=colors.get('dark_ground'))

    # Draw all entities in the list
    for entity in entities:
        draw_entity(con, entity)

    root_console.blit(con, 0, 0, screen_width, screen_height, 0, 0)

render_all now loops through each tile in the game map, and checks if it is transparent or not. If it isn't, then it draws it as a wall, and if it is, it draws a floor.

Run the project now, and you should see the 'map' drawn with some color to it. You'll see our three block wall as well, but there's one problem: We can move through the wall!

We need to add one more thing before we can call it a day. Modify the part where the player's move function gets called to look like this:

            if game_map.walkable[player.x + dx, player.y + dy]:
                player.move(dx, dy)
            player.move(dx, dy)

* Note the indentation change for player.move(dx, dy). This is Python, so indentation matters!

Run the project again, and you will get stuck on the walls.

That's going to do it for this tutorial. It may not look like much, but we've set ourselves up to create some real looking dungeons in the next tutorial.

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.