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. In order to follow along with this tutorial, you'll need to follow the instructions on that site for setting up Python and tdl (Part 1 up until the code starts). Once you're set up, come back here to get started.

Before we start, I want to clarify: You should not attempt this tutorial if you are new to programming! I have written this tutorial assuming that the reader is at least somewhat familiar with basic programming principles (Classes, functions, imports, etc.). If you are a first timer, then I highly recommend reading the tutorial on Roguebasin instead. It does a much better job of easing first time programmers into creating their first roguelike.

... Still here? Great! Let's start by importing the tdl library and getting our 'main' function in place. Create a file in your folder called 'engine.py', and put the following code in it.

import tdl


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


if __name__ == '__main__':
    main()

You can run the program by 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 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 tdl


def main():
    screen_width = 80
    screen_height = 50

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

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

    while not tdl.event.is_window_closed():
        root_console.draw_char(1, 1, '@', bg=None, fg=(255, 255, 255))
        tdl.flush()

        root_console.draw_char(1, 1, ' ', bg=None)

        for event in tdl.event.get():
            if event.type == 'KEYDOWN':
                user_input = event
                break
        else:
            user_input = None

        if not user_input:
            continue

        if user_input.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.

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

Here, we're telling tdl 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 tdl which type of file we're reading.

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

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). We assign all this to the root_console variable so that we can reference it when drawing.

    while not tdl.event.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.

        root_console.draw_char(1, 1, '@', bg=None, fg=(255, 255, 255))

The first two arguments 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'. Finally, we're specifying which color to draw in the foreground; (255, 255, 255) is RGB for white.

        tdl.flush()

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

        root_console.draw_char(1, 1, ' ', bg=None)

We use this to 'erase' the character before drawing it again. This will make more sense once we add movement in (without it, every time we move, we'd leave an '@' symbol behind in the old place!).

        for event in tdl.event.get():
            if event.type == 'KEYDOWN':
                user_input = event
                break
        else:
            user_input = None

This is where we're getting our user input, if there is any. Our 'for' loop goes through each 'event' (key presses, mouse motions, etc.) that we've captured, and checks if any of them match 'KEYDOWN' (a key being held down). If so, set the user_input variable to the event, so that we can process it down the line.

What's with the else statement? Python has a lesser-known feature where you can put an 'else' statement after a for loop, and that else statement only executes if we didn't break out of the loop! So in this scenario, if we didn't encounter any 'KEYDOWN' event, then we set user_input to None by default.

        if not user_input:
            continue

All we're doing here is returning back to the top of the loop if no input was detected. The rest of the loop deals with handling the user's input, so if there wasn't any input captured, then we don't need to waste time going through the rest of the loop.

        if user_input.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 for-else loop we had above will capture the user's input, and this is where we check if the Escape key was indeed pressed. 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)

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

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, tdl will give an error.

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

        ...
        root_console.draw_char(1, 1, '@', bg=None, fg=(255, 255, 255))
        root_console.draw_char(player_x, player_y, '@', bg=None, fg=(255, 255, 255))
        tdl.flush()

        root_console.draw_char(1, 1, ' ', bg=None)
        root_console.draw_char(player_x, player_y, ' ', bg=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.

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. Put the following code inside that new file.

def handle_keys(user_input):
    # Movement keys
    if user_input.key == 'UP':
        return {'move': (0, -1)}
    elif user_input.key == 'DOWN':
        return {'move': (0, 1)}
    elif user_input.key == 'LEFT':
        return {'move': (-1, 0)}
    elif user_input.key == 'RIGHT':
        return {'move': (1, 0)}

    if user_input.key == 'ENTER' and user_input.alt:
        # Alt+Enter: toggle full screen
        return {'fullscreen': True}
    elif user_input.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(user_input):

We're defining a function called handle_keys, which takes one argument, user_input. user_input in this case will be the same as the user_input variable we captured earlier.

    if user_input.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 user_input.key == 'ENTER' and user_input.alt:
        # Alt+Enter: toggle full screen
        return {'fullscreen': True}
    elif user_input.key == 'ESCAPE':
        # Exit the game
        return {'exit': True}

These are our non-movement actions that we're allowing for now. If the user presset 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.

        ...
        if not user_input:
            continue

        action = handle_keys(user_input)

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

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

        if user_input.key == 'ESCAPE':
        if exit:
            return True

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

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

import tdl

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 bit of housekeeping before we move on to Part 2. Currently, we're drawing everything directly on to the 'root' console. But later on, we'll want to draw multiple independent consoles (for the message log and other things). Let's set ourselves up for that now, by creating a new console that we'll draw to.

Modify the engine.py file like this:

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

    while not tdl.event.is_window_closed():
        root_console.draw_char(player_x, player_y, '@', bg=None, fg=(255, 255, 255))
        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)
        tdl.flush()

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

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.