Part 1 - Drawing the '@' symbol and moving it around

Welcome to part 1 of the Roguelike Tutorial Revised! This series will help you create your very first roguelike game, written in Python!

This tutorial is largely based off the one found on Roguebasin. Many of the design decisions were mainly to keep this tutorial in lockstep with that one (at least in terms of chapter composition and general direction). This tutorial would not have been possible without the guidance of those who wrote that tutorial, along with all the wonderful contributors to libtcod and python-tcod over the years.

This part assumes that you have either checked Part 0 and are already set up and ready to go. If not, be sure to check that page, and make sure that you’ve got Python and TCOD installed, and a file called engine.py created in the directory that you want to work in.

Assuming that you’ve done all that, let’s get started. Modify (or create, if you haven’t already) the file engine.py to look like this:

import tcod as libtcod


def main():
    print('Hello World!')


if __name__ == '__main__':
    main()

You can run the program like any other Python program, but for those who are brand new, you do that by typing python engine.py in the terminal. If you have both Python 2 and 3 installed on your machine, you might have to use python3 engine.py to run (it depends on your default python, and whether you’re using a virtualenv or not).

Okay, not the most exciting program in the world, I admit, but we’ve already got our first major difference from the other tutorial. Namely, this funky looking thing here:

if __name__ == '__main__':
    main()

So what does that do? Basically, we’re saying that we’re only going to run the “main” function when we explicitly run the script, using python engine.py. It’s not super important that you understand this now, but if you want a more detailed explanation, this answer on Stack Overflow gives a pretty good overview.

Confirm that the above program runs (if not, there’s probably an issue with your libtcod setup). Once that’s done, we can move on to bigger and better things. The first major step to creating any roguelike is getting an ‘@’ character on the screen and moving, so let’s get started with that.

Modify engine.py to look like this:

import tcod as libtcod


def main():
    screen_width = 80
    screen_height = 50

    libtcod.console_set_custom_font('arial10x10.png', libtcod.FONT_TYPE_GREYSCALE | libtcod.FONT_LAYOUT_TCOD)

    libtcod.console_init_root(screen_width, screen_height, 'libtcod tutorial revised', False)

    while not libtcod.console_is_window_closed():
        libtcod.console_set_default_foreground(0, libtcod.white)
        libtcod.console_put_char(0, 1, 1, '@', libtcod.BKGND_NONE)
        libtcod.console_flush()

        key = libtcod.console_check_for_keypress()

        if key.vk == libtcod.KEY_ESCAPE:
            return True


if __name__ == '__main__':
    main()

Run engine.py again, and you should see an ‘@’ symbol on the screen. Once you’ve fully soaked in the glory on the screen in front of you, you can hit the `Esc` key to exit the program.

There’s a lot going on here, so let’s break it down line by line.

    screen_width = 80
    screen_height = 50

This is simple enough. We’re defining some variables for the screen size. Eventually, we’ll load these values from a JSON file rather than hard coding them in the source, but we won’t worry about that until we have some more variables like this.

    libtcod.console_set_custom_font('arial10x10.png', libtcod.FONT_TYPE_GREYSCALE | libtcod.FONT_LAYOUT_TCOD)

Here, we’re telling libtcod which font to use. The 'arial10x10.png' bit is the actual file we’re reading from (this should exist in your project folder). The other two parts are telling libtcod which type of file we’re reading.

    libtcod.console_init_root(SCREEN_WIDTH, SCREEN_HEIGHT, 'libtcod tutorial revised', False)

This line is what actually creates the screen. We’re giving it the screen_width and screen_height values from before (80 and 50, respectively), along with a title (change this if you’ve already got your game’s name figured out), and a boolean value that tells libtcod whether to go full screen or not.

    while not libtcod.console_is_window_closed():

This is what’s called our ‘game loop’. Basically, this is a loop that won’t ever end, until we close the screen. Every game has some sort of game loop or another.

        libtcod.console_set_default_foreground(0, libtcod.white)

This line tells libtcod to set the color for our ‘@’ symbol. If you want your character to be a different color, change libtcod.white to something like libtcod.red and see what happens. The ‘0’ in this function is the console we’re drawing to. We’ll go over that more later.

        libtcod.console_put_char(0, 1, 1, '@', libtcod.BKGND_NONE)

The first argument is ‘0’ (again, the console we’re printing to). The next two are x and y coordinates, in this case, 1 and 1 (try changing that and see what happens). Next, we’re printing the ‘@’ symbol, and setting the background to ’none’ with libtcod.BKGND_NONE.

        libtcod.console_flush()

This is the part that presents everything on the screen. Pretty straightforward.

        key = libtcod.console_check_for_keypress()

        if key.vk == libtcod.KEY_ESCAPE:
            return True

This part gives us a way to gracefully exit (i.e. not crashing) the program by hitting the Esc key. The libtcod.console_check_for_keypress() function gets any keyboard input to the program, which we store in the key variable. From there, we check if the key pressed was the Esc key or not. If it was, then we exit the loop, thus ending the program.

So we’ve got our ‘@’ symbol drawn, now let’s get it moving around!

We need to keep track of the player’s position at all times, so let’s create two variables, player_x and player_y to keep track of this.

    ...
    screen_height = 50
+
+   player_x = int(screen_width / 2)
+   player_y = int(screen_height / 2)
+
    libtcod.console_set_custom_font('arial10x10.png', libtcod.FONT_TYPE_GREYSCALE | libtcod.FONT_LAYOUT_TCOD)
    ...
    ...
    screen_height = 50
    
    player_x = int(screen_width / 2)
    player_y = int(screen_height / 2)
    
    libtcod.console_set_custom_font('arial10x10.png', libtcod.FONT_TYPE_GREYSCALE | libtcod.FONT_LAYOUT_TCOD)
    ...

Note: Ellipses denote omitted parts of the code. I’ll include lines around the code to be inserted so that you’ll know exactly where to put new pieces of code, but I won’t be showing the entire file every time. The green lines denote code that you should be adding.

We’re placing the player right in the middle of the screen. What’s with the int() function though? Well, Python 3 doesn’t automatically truncate division like Python 2 does, so we have to cast the division result (a float) to an integer. If we don’t, libtcod will give an error.

We also have to modify the command to put the ‘@’ symbol to use these new coordinates.

        ...
        libtcod.console_set_default_foreground(0, libtcod.white)
-       libtcod.console_put_char(0, 1, 1, '@', libtcod.BKGND_NONE)
+       libtcod.console_put_char(0, player_x, player_y, '@', libtcod.BKGND_NONE)
        libtcod.console_flush()

+       libtcod.console_put_char(0, player_x, player_y, ' ', libtcod.BKGND_NONE)
        ...
        ...
        libtcod.console_set_default_foreground(0, libtcod.white)
        libtcod.console_put_char(0, 1, 1, '@', libtcod.BKGND_NONE)
        libtcod.console_put_char(0, player_x, player_y, '@', libtcod.BKGND_NONE)
        libtcod.console_flush()

        libtcod.console_put_char(0, player_x, player_y, ' ', libtcod.BKGND_NONE)
        ...

Note: The red lines denote code that has been removed.

Run the code now and you should see the ‘@’ in the center of the screen. Let’s take care of moving it around now.

Put the following two lines right above the main game loop.

    ...
    libtcod.console_init_root(screen_width, screen_height, 'libtcod tutorial revised', False)

+   key = libtcod.Key()
+   mouse = libtcod.Mouse()

    while not libtcod.console_is_window_closed():
    ...
    ...
    libtcod.console_init_root(screen_width, screen_height, 'libtcod tutorial revised', False)

    key = libtcod.Key()
    mouse = libtcod.Mouse()

    while not libtcod.console_is_window_closed():
    ...

As the names imply, these variables will hold our keyboard and mouse input. We aren’t implementing the mouse yet, but the function we’re about to add take it into account, so we might as well add it.

    ...
    while not libtcod.console_is_window_closed():
+       libtcod.sys_check_for_event(libtcod.EVENT_KEY_PRESS, key, mouse)

        libtcod.console_set_default_foreground(0, libtcod.white)
    ...
    ...
    while not libtcod.console_is_window_closed():
        libtcod.sys_check_for_event(libtcod.EVENT_KEY_PRESS, key, mouse)

        libtcod.console_set_default_foreground(0, libtcod.white)
    ...

This is the function that actually captures new “events” (user input). It will update the key and mouse variables with what the user inputs. Again, we’re only concerned with key for right now.

Okay, so we’re updating the key variable with the user’s input. But what do we actually do with it? Let’s define a function to handle the user’s input. It will essentially translate the user’s key presses into game actions.

Up until now, this tutorial hasn’t deviated all that much from the original one, but here’s a critical turning point. We’re about to define a function, called handle_keys to take care of keyboard input. We could put this in our engine.py file… but should it be there? I would argue no. The engine (game loop) captures input and should do something with it; but, translating from one to the other is not something it needs to know about.

So rather than putting the handle_keys function in engine.py, let’s create a new file, called input_handlers.py. Put the following code inside that new file.

import tcod as libtcod


def handle_keys(key):
    # Movement keys
    if key.vk == libtcod.KEY_UP:
        return {'move': (0, -1)}
    elif key.vk == libtcod.KEY_DOWN:
        return {'move': (0, 1)}
    elif key.vk == libtcod.KEY_LEFT:
        return {'move': (-1, 0)}
    elif key.vk == libtcod.KEY_RIGHT:
        return {'move': (1, 0)}

    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 game
        return {'exit': True}

    # No key was pressed
    return {}

That’s a lot to take in all at once, so again, let’s break it down a bit.

def handle_keys(key):

We’re defining a function called handle_keys, which takes one argument, key. key in this case will be the key variable we captured earlier.

    if key.vk == libtcod.KEY_UP:

This if statement (along with the other elifs) just tell us which key was pressed. Right now, it’s one of the arrow keys for movement. What’s more interesting is the code inside these if statements

    return {'move': (0, -1)}

So what’s going on here? Well, when we return from this function, the engine is going to have to do something. In this case, we want our character to move. But what if we hit a different key? Then we might not be moving; we may be using an item, casting a spell, or exiting the game. One way to handle all these different possibilities is to return a dictionary from this function, which the engine will read and decide what to do.

In this instance, we’re returning a dictionary with the key 'move', and the value is a pair of numbers. The numbers will tell the engine in what direction to move the player. So for example, the ‘up’ key will move us ‘0’ on the x axis, and ‘-1’ on the y axis.

    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 game
        return {'exit': True}

These are our non-movement actions that we’re allowing for now. If the user pressed ALT+Enter, the game will go full screen. If the user presses ‘Esc’, the game will exit.

    return {}

Because our engine will be expecting a dictionary, we have to return something, even if nothing happened.

This may seem confusing, but it will likely make sense in a minute. Let’s return to our engine.py file and call our handle_keys function.

        ...
        libtcod.console_flush()

-       key = libtcod.console_check_for_keypress()
+       action = handle_keys(key)
+
+       move = action.get('move')
+       exit = action.get('exit')
+       fullscreen = action.get('fullscreen')

+       if move:
+           dx, dy = move
+           player_x += dx
+           player_y += dy

-       if key.vk == libtcod.KEY_ESCAPE:
+       if exit:
+           return True
+
+       if fullscreen:
+           libtcod.console_set_fullscreen(not libtcod.console_is_fullscreen())
        ...
        ...
        libtcod.console_flush()

        key = libtcod.console_check_for_keypress()
        action = handle_keys(key)

        move = action.get('move')
        exit = action.get('exit')
        fullscreen = action.get('fullscreen')

        if move:
            dx, dy = move
            player_x += dx
            player_y += dy

        if key.vk == libtcod.KEY_ESCAPE:
        if exit:
            return True

        if fullscreen:
            libtcod.console_set_fullscreen(not libtcod.console_is_fullscreen())
        ...

Note: I’ll denote lines to delete in red. So in this case, remove the key = libtcod.console_check_for_keypress() and if key.vk == libtcod.KEY_ESCAPE lines.

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

import tcod as libtcod

+from input_handlers import handle_keys
import tcod as libtcod

from input_handlers import handle_keys

Hopefully now the dictionary madness in handle_keys makes a little more sense. We’re capturing the return value of handle_keys in the variable action (which should be a dictionary, no matter what we pressed), and checking what keys are inside it. If it contains a key called ‘move’, then we know to look for the (x, y) coordinates. If it contains ’exit’, then we know we need to exit the game.

Try running the engine.py file now. You should be able to move around. Exciting!

One last thing before we move on. Take a look at our drawing functions. Notice how the first argument is ‘0’? In truth, that represents the current ‘console’ we are drawing to, 0 is the default. Rather than just drawing to the default we’ll want to specify which console to draw to, after initiating a new one. The reasoning is that it will make it easier to make new consoles and draw to them in the future. This will be especially useful when we get to the GUI portion of this series.

Modify the engine.py file like this:

    ...
    libtcod.console_init_root(screen_width, screen_height, 'libtcod tutorial revised', False)

+   con = libtcod.console_new(screen_width, screen_height)

    key = libtcod.Key()
    mouse = libtcod.Mouse()

    while not libtcod.console_is_window_closed():
        libtcod.sys_check_for_event(libtcod.EVENT_KEY_PRESS, key, mouse)
+
+       libtcod.console_set_default_foreground(con, libtcod.white)
+       libtcod.console_put_char(con, player_x, player_y, '@', libtcod.BKGND_NONE)
+       libtcod.console_blit(con, 0, 0, screen_width, screen_height, 0, 0, 0)
-       libtcod.console_set_default_foreground(0, libtcod.white)
-       libtcod.console_put_char(0, player_x, player_y, '@', libtcod.BKGND_NONE)
        libtcod.console_flush()
+
+       libtcod.console_put_char(con, player_x, player_y, ' ', libtcod.BKGND_NONE)
-       libtcod.console_put_char(0, player_x, player_y, ' ', libtcod.BKGND_NONE)
    ...
    libtcod.console_init_root(screen_width, screen_height, 'libtcod tutorial revised', False)

    con = libtcod.console_new(screen_width, screen_height)

    key = libtcod.Key()
    mouse = libtcod.Mouse()

    while not libtcod.console_is_window_closed():
        libtcod.sys_check_for_event(libtcod.EVENT_KEY_PRESS, key, mouse)
        
        libtcod.console_set_default_foreground(con, libtcod.white)
        libtcod.console_put_char(con, player_x, player_y, '@', libtcod.BKGND_NONE)
        libtcod.console_blit(con, 0, 0, screen_width, screen_height, 0, 0, 0)
        libtcod.console_set_default_foreground(0, libtcod.white)
        libtcod.console_put_char(0, player_x, player_y, '@', libtcod.BKGND_NONE)
        libtcod.console_flush()
        
        libtcod.console_put_char(con, player_x, player_y, ' ', libtcod.BKGND_NONE)
        libtcod.console_put_char(0, player_x, player_y, ' ', libtcod.BKGND_NONE)

libtcod.console_blit is used to copy con to libtcod’s root console which is then presented by libtcod.console_flush.

That wraps up part one of this tutorial! If you’re using git or some other form of version control (and I recommend you do), commit your changes now.

If you want to see the code so far in its entirety, click here. The files you’ll want to check are engine.py and input_handlers.py

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