How to write a roguelike in Python using libtcod (#1)

UPDATE, 1/20/2010: There is a newer libtcod and Python tutorial aimed at beginners at the Roguebasin wiki. What follows is a less comprehensive approach. If you follow this series of posts here you’ll find many detours and returns, which may be educational, but if you want a good tutorial I recommend the above link.

The original post:

I’ve known about the libtcod roguelike library for a while, mainly through the Python port in development by Kyle Stewart. However that project seemed to be in stasis. Then just a week or so ago I looked at libtcod again, because of some beautiful roguelikes that are using the C/C++ version of the library.

ur_001-medium

Pretty amazing, no? And that’s all textmode!

So it turns out, in the meantime the developer jice created a Python wrapper and some Python samples to go with it. From there it was quite easy to use the samples as a base for a simple ‘get your @ on the screen moving around’ demo.

libtcodtut1

I know for some people a lot of the fun of programming roguelikes is doing everything yourself — the console, the input and output routines, and so on. It’s not that I don’t find that fun — just that I’m bunk at programming, and I’d rather learn and make some progress with the actual game. As I go I do find myself learning more about the nitty-gritty programming details. It’s a combination of active learning and some sort of bottom-up osmosis I guess.

So let’s create a roguelike in Python with libtcod.

The first thing to do is download the libtcod library and python module (one download, choosing your platform). Of course, you’ll need Python as well if you don’t have it. Remember, if you’re on Windows, set your PATH environment variable to include the Python directory.

The total download for me was about 4 1/2 MB, so I copied out files to create a minimal directory of about 2 MB. You can lose the docs, samples, and miscellaneous .PNGs (but not the terminal.png, as that’s the default font — you don’t need it, but it makes things simpler to start).

You’ll notice that much of this demo is straight from samples_py.py, so if you’re feeling like you want to make a go of it yourself, make your own basic demo from samples_py — you can do it! If you’d rather listen to me blather on, please continue. The full listing is at the bottom, but first I’ll explain as I go.

Create a Python file and put it in your minimal libtcod directory. Let’s start from the top.

#!/usr/bin/python

'''
libtcod python tutorial
This code modifies samples_py.py from libtcod 1.4.1. It shows a '@'
walking around with a source of light giving simple FOV.
It's in the public domain.
'''

#############################################
# imports
#############################################

import os

import libtcodpy as libtcod

Import the os module, as we’ll use it to create the path for our font file later; import libtcod, naming it whatever you like, but for simplicity’s sake I’ll call it libtcod.

The rest of the code is in four main parts: global constants and variables, the drawing routine, the update routine, and the main loop. For each cycle of the main loop, the code runs the drawing and update routines once, using the global constants and changing the global variables in the process.

First, though, there’s one utility function I created to make things easier:

#############################################
# utility functions
#############################################

def get_key(key):
    if key.vk == libtcod.KEY_CHAR:
        return chr(key.c)
    else:
        return key.vk

get_key is a simple function I wrote to make handling keyboard input a little easier. The properties key.vk and key.c are from libtcod’s TCOD_key_t structure — you can read up on them in the ‘Console emulator’ chapter of the documentation.

Unlike some game libraries I’ve used, in libtcod there is not a symbol for something like KEY_h, the keyboard character ‘h’. However there are symbols for keyboard keys like the up arrow (KEY_UP), or the ‘7’ on the number pad (KEY_KP7).

This means that if you want to test for a key in your code that is a printable character, such as ‘h’, you need to use its ASCII code, not a unique libtcod symbol — with, for example, something like ord(‘h’) instead of KEY_h. It’s simpler for me to just think of letters and other printable characters as the characters themselves, as a string like ‘h’, though, so I wrote get_key to let me use simple strings whenever I’m working with keyboard input. You’ll see what I mean in a moment.

Next define some constants:

#############################################
# global constants and variables
#############################################

window_width = 46
window_height = 20

first = True
fov_px = 20
fov_py = 10
fov_recompute = True
fov_map = None
fov_colors =    {
                'dark wall' : libtcod.Color(0, 0, 100),
                'light wall' : libtcod.Color(130, 110, 50),
                'dark ground' : libtcod.Color(50, 50, 150),
                'light ground' : libtcod.Color(200, 180, 50)
                }
fov_init = False
fov_radius = 4

move_controls = {
                'i' : (0, -1), 	# up
		'k' : (0, 1), 	# down
		'j' : (-1, 0),	# left
		'l' : (1, 0),	# right
                libtcod.KEY_UP : (0, -1), # example of alternate key
                libtcod.KEY_KP8 : (0, -1) # example of alternate key
		}

Don’t worry too much about what these mean right now, just notice a couple of things.

First, fov_colors defines the colors of different types of cells (that is, tiles) on our map. If you look at samples_py.py you’ll see cell colors are defined differently, as individual constants instead. I used a Python dictionary type here for two reasons. In my mind it makes the constants themselves easier to read, and it simplifies the code where the constants are used. If you compare the code in this demo to the samples_py.py field-of-view demo you’ll see what I mean. This way of doing it may not be to your taste, so this would be a good spot to compare this demo and the library’s demo to see which you prefer.

Second, move_controls defines which keys do what in the demo (where movement is the only thing going on, so that’s all we’re concerned with). Again, this is different from samples_py — samples_py uses an if/elif control structure to define what keys do what (in fact, it deals with cell colors in a similar way). What I like about using a dictionary like this is it makes changing keyboard commands very easy; rather than editing a long if structure, I change the entries in the dictionary instead.

There’s one more constant to define — the map. I just copied this from samples_py. A hardcoded map is easy to work with. This would be the entry point for a generated map.

smap = ['##############################################',
        '#######################      #################',
        '#####################    #     ###############',
        '######################  ###        ###########',
        '##################      #####             ####',
        '################       ########    ###### ####',
        '###############      #################### ####',
        '################    ######                  ##',
        '########   #######  ######   #     #     #  ##',
        '########   ######      ###                  ##',
        '########                                    ##',
        '####       ######      ###   #     #     #  ##',
        '#### ###   ########## ####                  ##',
        '#### ###   ##########   ###########=##########',
        '#### ##################   #####          #####',
        '#### ###             #### #####          #####',
        '####           #     ####                #####',
        '########       #     #### #####          #####',
        '########       #####      ####################',
        '##############################################',
        ]

Next comes the code that draws the map and its contents to the console.

#############################################
# drawing
#############################################

def draw(first):
	global fov_px, fov_py, fov_map
	global fov_init, fov_recompute, smap

	if first:
		libtcod.console_clear(0)
		libtcod.console_set_foreground_color(0, libtcod.white)
		libtcod.console_print_left(0, 1, 1, libtcod.BKGND_NONE,
				       "IJKL : move aroundn")
		libtcod.console_set_foreground_color(0, libtcod.black)
		libtcod.console_put_char(0, fov_px, fov_py, '@',
					 libtcod.BKGND_NONE)

		for y in range(window_height):
		    for x in range(window_width):
			if smap[y][x] == '=':
			    libtcod.console_put_char(0, x, y,
						     libtcod.CHAR_DHLINE,
						     libtcod.BKGND_NONE)

You’ll notice I’m using global variables in these functions — it’s a simple way to do it for this demo, but not necessarily how you’ll want to do it for a complete game.

In the first if structure we test if first is true. Then the code draws the first characters that we see — some simple instructions that appear on the console, the ‘@’ (that’s you), and the ‘=’ characters, representing windows in the map display (look at the smap array above). All of these libtcod functions are described in the libtcod documentation, so give that a look as you follow along. You’ll notice that first it sets the foreground color to white — this is so it draws the instructions in white. Then it changes the foreground color to black for the rest of the drawing. This routine only needs to run once, which is why it’s controlled by the test of first.

Now we want to do two things in the drawing routine — draw the rest of the map, and test if we need to recalculate the player’s field-of-view. We recalculate when the player moves.

	if not fov_init:
		fov_init = True
		fov_map = libtcod.map_new(window_width, window_height)
		for y in range(window_height):
		    for x in range(window_width):
			if smap[y][x] == ' ':
			    libtcod.map_set_properties(fov_map, x, y, True, True)
			elif smap[y][x] == '=':
			    libtcod.map_set_properties(fov_map, x, y, True, False)

	if fov_recompute:
		fov_recompute = False
		libtcod.map_compute_fov(fov_map, fov_px, fov_py, fov_radius, True)

You can think of fov_init as a checkbox. If it’s False, the program hasn’t been run yet and the checkbox isn’t checked. Once we do run the program, the code puts a check in the checkbox (setting fov_init to True) and draws the rest of the map to the console. Note that the code only draws the entire map once, when the game begins. In the future, we only need to draw those parts of the map that have changed. This is a simple technique common to most games.

The code also tests fov_recompute to see if it should recalculate the field-of-view of our player. Look map_compute_fov up in the documentation to see what all its arguments mean.

After the field-of-view is computed (or not — in that case, it remains the same as before), the code needs to draw the cells affected by the fov. We have four types of cells here, dark walls, light walls, dark ground, and light ground — the entries in our fov_colors dictionary above. Here is the code:

	for y in range(window_height):
		for x in range(window_width):
                    affect, cell = 'dark', 'ground'
		    if libtcod.map_is_in_fov(fov_map, x, y): affect = 'light'
		    if (smap[y][x] == '#'): cell = 'wall'
                    color = fov_colors['%s %s' % (affect, cell)]
                    libtcod.console_set_back(0, x, y, color, libtcod.BKGND_SET)

Again we loop through each cell of the smap. The first thing the code does is set the variables affect and cell to ‘dark’ and ‘ground’ respectively. Then if the cell is within the field-of-view, it sets the affect variable to ‘light’ instead, and similarly if the cell is a wall. Finally, it creates a variable called color that is the entry in the fov_colors dictionary corresponding to the key ‘affect cell‘. The code uses that color variable to set the background color of the cell.

If we wanted to add a different color — say, making the windows pink when in the fov — we would just need to add a test to find ‘=’ characters, and in that test change the ‘cell‘ to ‘window’ appropriately. Then, define ‘light window’ in the fov_colors dictionary as pink, and define its counterpart — perhaps ‘dark window’.

What I like about this method, as I described above, is it avoids a long if/else structure to do the same thing.

The drawing routine is complete; now on to the update function.

#############################################
# game state update
#############################################

def update(key):
    global fov_py, fov_px, fov_recompute, smap

    key = get_key(key)
    if key in move_controls:
            dx, dy = move_controls[key]
            if smap[fov_py+dy][fov_px+dx] == ' ':
                    libtcod.console_put_char(0, fov_px, fov_py, ' ',
                                                 libtcod.BKGND_NONE)
                    fov_px = fov_px + dx
                    fov_py = fov_py + dy
                    libtcod.console_put_char(0, fov_px, fov_py, '@',
                                                 libtcod.BKGND_NONE)
                    fov_recompute = True

This update function is quite simple, as all it’s doing is updating the position of the player if they move. You’ll notice the libtcod drawing function console_put_char does the main work, drawing over the map cells if the player moves. It’s here that I use the get_key function defined above to set the value of the key variable. The key variable will be either a special libtcod symbol, or a string, depending on what key the player presses on the keyboard. Look again at get_key to see what that function returns if you need to.

An important note

If you look again at move_controls defined above in the global constants section, you’ll notice that pressing an up key sets (dx, dy) as (0, -1) — in effect, reducing the value of fov_py (fov_py is the y-point from where we do the field-of-view calculation, and not incidentally where the @ character is drawn too). If you’re not familiar with the SDL library (which libtcod uses under the hood) it might seem strange to reduce the y value as you go upward, but it’s simply because SDL defines the (0,0) x,y coordinate position to be in the upper left corner of whatever you’re drawing to. Conversely, if you were to use a library such as OpenGL, you would increase the y value as you go up, as in OpenGL the (0,0) position is in the lower left corner of the display.

If the player does move, the code will need to recompute the field-of-view on the next cycle of the game loop.

That’s it for the update function — now for the main loop.

#############################################
# initialization and main loop
#############################################

font = os.path.join('fonts', 'arial12x12.png')
libtcod.console_set_custom_font(font, libtcod.FONT_LAYOUT_TCOD | libtcod.FONT_TYPE_GREYSCALE)

libtcod.console_init_root(window_width, window_height, 'Python Tutorial', False)

while not libtcod.console_is_window_closed():
    draw(first)
    libtcod.console_flush()
    key = libtcod.console_wait_for_keypress(True)
    update(key)
    if key.vk == libtcod.KEY_ESCAPE:
        break

There’s not much to it. One thing we do first, before creating the console with console_init_root, is set a custom font. This is why I imported the os module earlier, as the font is located in a fonts directory located inside the working directory.

The game loop itself just draws to the console, waits for a key press, then calls the update function. If the player presses the escape key the game exits. This is a classic turn-based game loop, but libtcod also supports real-time game loops, and you can read more about that in the documentation.

So that’s everything! A functional roguelike demo in Python in 173 lines of code. Remember, much of this code is directly from the samples_py.py file included with the download — there’s a lot more to learn there, and on the libtcod site as well. I’m hoping to expand on this demo as I create a functional game, so any feedback (on my hacky code or this hacky tutorial!) would be terrific. Thanks for reading.

Here’s the complete listing:

#!/usr/bin/python

'''
libtcod python tutorial
This code modifies samples_py.py from libtcod 1.4.1. It shows a '@'
walking around with a source of light giving simple FOV.
It's in the public domain.
'''

#############################################
# imports
#############################################

import os

import libtcodpy as libtcod

#############################################
# utility functions
#############################################

def get_key(key):
    if key.vk == libtcod.KEY_CHAR:
        return chr(key.c)
    else:
        return key.vk

#############################################
# global constants and variables
#############################################

window_width = 46
window_height = 20

first = True
fov_px = 9
fov_py = 10
fov_recompute = True
fov_map = None
fov_colors =    {
                'dark wall' : libtcod.Color(0, 0, 100),
                'light wall' : libtcod.Color(130, 110, 50),
                'dark ground' : libtcod.Color(50, 50, 150),
                'light ground' : libtcod.Color(200, 180, 50)
                }
fov_init = False
fov_radius = 4

do =    {
        'up'    : (0, -1),
        'down'  : (0, 1),
        'right' : (1, 0),
        'left'  : (-1, 0)
        }

keys =  {
        'i' :                   do['up'],
        'k' :                   do['down'],
        'j' :                   do['left'],
        'l' :                   do['right'],
        libtcod.KEY_UP :        do['up'],
        libtcod.KEY_KP8 :       do['up']
        }

smap = ['##############################################',
        '#######################      #################',
        '#####################    #     ###############',
        '######################  ###        ###########',
        '##################      #####             ####',
        '################       ########    ###### ####',
        '###############      #################### ####',
        '################    ######                  ##',
        '########   #######  ######   #     #     #  ##',
        '########   ######      ###                  ##',
        '########                                    ##',
        '####       ######      ###   #     #     #  ##',
        '#### ###   ########## ####                  ##',
        '#### ###   ##########   ###########=##########',
        '#### ##################   #####          #####',
        '#### ###             #### #####          #####',
        '####           #     ####                #####',
        '########       #     #### #####          #####',
        '########       #####      ####################',
        '##############################################',
        ]

#############################################
# drawing
#############################################

def draw():
	global fov_px, fov_py, fov_map, first
	global fov_init, fov_recompute, smap

	if first:
                first = False
		libtcod.console_clear(0)
		libtcod.console_set_foreground_color(0, libtcod.white)
		libtcod.console_print_left(0, 1, 1, libtcod.BKGND_NONE,
				       "IJKL : move around")
		libtcod.console_set_foreground_color(0, libtcod.black)
		libtcod.console_put_char(0, fov_px, fov_py, '@',
					 libtcod.BKGND_NONE)

		for y in range(window_height):
		    for x in range(window_width):
			if smap[y][x] == '=':
			    libtcod.console_put_char(0, x, y,
						     libtcod.CHAR_DHLINE,
						     libtcod.BKGND_NONE)

	if not fov_init:
		fov_init = True
		fov_map = libtcod.map_new(window_width, window_height)
		for y in range(window_height):
		    for x in range(window_width):
			if smap[y][x] == ' ':
			    libtcod.map_set_properties(fov_map, x, y, True, True)
			elif smap[y][x] == '=':
			    libtcod.map_set_properties(fov_map, x, y, True, False)

	if fov_recompute:
		fov_recompute = False
		libtcod.map_compute_fov(fov_map, fov_px, fov_py, fov_radius, True)

	for y in range(window_height):
		for x in range(window_width):
                    affect, cell = 'dark', 'ground'
		    if libtcod.map_is_in_fov(fov_map, x, y): affect = 'light'
		    if (smap[y][x] == '#'): cell = 'wall'
                    color = fov_colors['%s %s' % (affect, cell)]
                    libtcod.console_set_back(0, x, y, color, libtcod.BKGND_SET)

#############################################
# game state update
#############################################

def update(key):
    global fov_py, fov_px, fov_recompute, smap

    key = get_key(key)
    if key in keys:
            dx, dy = keys[key]
            if smap[fov_py+dy][fov_px+dx] == ' ':
                    libtcod.console_put_char(0, fov_px, fov_py, ' ',
                                                 libtcod.BKGND_NONE)
                    fov_px = fov_px + dx
                    fov_py = fov_py + dy
                    libtcod.console_put_char(0, fov_px, fov_py, '@',
                                                 libtcod.BKGND_NONE)
                    fov_recompute = True

#############################################
# initialization and main loop
#############################################

font = os.path.join('fonts', 'arial12x12.png')
libtcod.console_set_custom_font(font, libtcod.FONT_LAYOUT_TCOD | libtcod.FONT_TYPE_GREYSCALE)

libtcod.console_init_root(window_width, window_height, 'Python Tutorial', False)

while not libtcod.console_is_window_closed():
    draw()
    libtcod.console_flush()
    key = libtcod.console_wait_for_keypress(True)
    update(key)
    if key.vk == libtcod.KEY_ESCAPE:
        break

About these ads

9 comments so far

  1. Pacian on

    I’ve played around in libtcod a bit myself, mostly because I’m not sure I can be bothered to learn field of view and line of sight algorithms.

    Good luck with that game. :-)

  2. […] Posted April 5, 2009 Filed under: roguelike | Tags: libtcod | So after making a simple @ walking demo with libtcod, I took a look at it again and thought, you know what would make this cooler? If the map scrolled! […]

  3. nine on

    If you could attach the full code in a block at the bottom it would be nice. Copying and pasting all those segments caused me some (fixable, but annoying) indent pain.

  4. georgek on

    you’re right nine — I’ll fix that here, but I should start doing that in the new posts too.

  5. pedestrian on

    thanks for the tutorial, helped clear alot of things up, but when I put the script through python i keep on getting the syntax error, “There’s an error in your program: unindent does not match any outer indentation level” on line 129 of your code.

    i’m a new python user and am not really sure what this means or what i’m supposed to do to fix it. any help would be appreciated.

  6. pedestrian on

    haha, nevermind. seemed to fix it.

  7. georgek on

    hi pedestrian (and anyone else reading the tutorial), I think the WordPress code box doesn’t always handle cut and paste well, so the best thing to do is mouse over the top right of the box to get to one of the source viewing options.

  8. Micheal MacLean on

    So glad you’re listening to Earth during this.

  9. […] If you are interested in creating a roguelike (a genre that is sort of like the Diablo games except with ASCII-art graphics), the libtcod module will be very helpful. There are tutorials here and here. […]


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: