Complete Roguelike Tutorial, using python+libtcod
Hi there!
It's by no means finished, but the first parts are available now. |
Complete Roguelike Tutorial, using Python+libtcod
Short introduction
Welcome!
Welcome to this tutorial! As you probably guessed, the goal is to have a one-stop-shop for all the info you need on how to build a good Roguelike from scratch. We hope you find it useful! But first, some quick Q&A.
Why Python?
Anyone familiar with this language will tell you it's fun! This tutorial would probably be much harder without it. We recommend that you install Python 2.6 and go through at least the first parts of the Python Tutorial. This tutorial will be much easier if you experimented with the language first. Remember that the Python Library Reference is your friend -- the standard library has everything you might need and when programming you should be ready to search it for help on any unknown function you might encounter.
Why libtcod?
If you haven't seen it in action yet, check out the features and some projects where it was used successfully. It's extremely easy to use and has tons of useful functions specific to RLs.
Graphics
Setting it up
Ok, now that we got that out of our way let's get our hands dirty! If you haven't yet, download and install Python 2.6. Other versions may work but then you'd have to smite any incompatibilities (though they shouldn't be too many). Then download libtcod and extract it somewhere. If you're on Windows, the choice between the Visual Studio and Mingw version shouldn't matter since we're using Python.
Now to create your project's folder. Create an empty file with a name of your choice, like firstrl.py. The easiest way to use libtcod is to copy the following files to your project's folder:
- libtcodpy.py
- libtcod-mingw.dll or libtcod-VS.dll on Windows, libtcod.so on Linux
- SDL.dll on Windows, SDLlib.so on Linux
- A font from the fonts folder. We chose arial10x10.png.
Showing the @ on screen
This first part will be a bit of a crash-course. The reason is that you need a few lines of boilerplate code that will initialize and handle the basics of a libtcod window. And though there are many options, we won't explain them all or this part will really start to drag out. Fortunately the code involved is not as much as in many other libraries!
First we import the library. The name libtcodpy is a bit funky (sorry Jice!) so we'll rename it to just libtcod.
import libtcodpy as libtcod
Then, a couple of important values. It's good practice to define special numbers that might get reused. Many people capitalize them to distinguish from variables that may change.
SCREEN_WIDTH = 80 SCREEN_HEIGHT = 50 LIMIT_FPS = 20
Now, something libtcod-specific: we're going to use a custom font! It's pretty easy. libtcod comes bundled with a few fonts that are usable right out of the box. Remember however that they can be in different formats, and you'll need to tell it about this. This one is "grayscale" and using the "tcod layout", most fonts are in this format and thus end with _gs_tc. If you wanna use a font with a different layout or make your own, the docs on the subject are really informative. You can worry about that at a later time though. Notice that the size of a font is automatically detected.
libtcod.console_set_custom_font('arial10x10.png', libtcod.FONT_TYPE_GREYSCALE | libtcod.FONT_LAYOUT_TCOD)
This is probably the most important call, initializing the window. We're specifying its size, the title (change it now if you want to), and the last parameter tells it if it should be fullscreen or not.
libtcod.console_init_root(SCREEN_WIDTH, SCREEN_HEIGHT, 'python/libtcod tutorial', False)
For a real-time roguelike, you wanna limit the speed of the game (frames-per-second or FPS). If you want it to be turn-based, ignore this line.
libtcod.sys_set_fps(LIMIT_FPS)
Now the main loop. It will keep running the logic of your game as long as the window is not closed.
while not libtcod.console_is_window_closed():
For each iteration we'll want to print something useful to the window. If your game is turn-based each iteration is a turn; if it's real-time, each one is a frame. Here we're setting the text color to be white. There's a good list of colors you can use here, along with some info about mixing them and all that. The zero is the console we're printing to, in this case the screen; more on that later.
libtcod.console_set_foreground_color(0, libtcod.white)
Don't forget the indentation at the beginning of the line, it's extra-important in Python. Make sure you don't mix tabs with spaces for indentation! This comes up often if you copy-and-paste code from the net, and you'll see an error telling you something about the indentation (that's a pretty big clue right there!). Choose one option and stick with it.
Now print a string, left-aligned, to the coordinates (1,1). Once more the first zero specifies the console, which is the screen in this case. Can you guess what that string is? No, it doesn't move yet!
libtcod.console_print_left(0, 1, 1, libtcod.BKGND_NONE, '@')
At the end of the main loop you'll always need to present the changes to the screen. This is called flushing the console and is done with the following line.
libtcod.console_flush()
Ta-da! You're done. Run that code and give yourself a pat on the back!
Here's the complete code so far:
import libtcodpy as libtcod #actual size of the window SCREEN_WIDTH = 80 SCREEN_HEIGHT = 50 LIMIT_FPS = 20 #20 frames-per-second maximum libtcod.console_set_custom_font('arial10x10.png', libtcod.FONT_TYPE_GREYSCALE | libtcod.FONT_LAYOUT_TCOD) libtcod.console_init_root(SCREEN_WIDTH, SCREEN_HEIGHT, 'python/libtcod tutorial', False) libtcod.sys_set_fps(LIMIT_FPS) while not libtcod.console_is_window_closed(): libtcod.console_set_foreground_color(0, libtcod.white) libtcod.console_print_left(0, 0, 0, libtcod.BKGND_NONE, '@') libtcod.console_flush()
Moving around
That was pretty neat, huh? Now we're going to move around that @ with the keys!
First, we need to keep track of the player's position. We'll use these variables for that, and take the opportunity to initialize them to the center of the screen instead of the top-left corner. This can go just before the main loop.
playerx = SCREEN_WIDTH/2 playery = SCREEN_HEIGHT/2
There are functions to check for pressed keys. When that happens, just change the coordinates accordingly. Then, print the @ at those coordinates. We'll make a separate function to handle the keys.
def handle_keys(): global playerx, playery #movement keys if libtcod.console_is_key_pressed(libtcod.KEY_UP): playery -= 1 elif libtcod.console_is_key_pressed(libtcod.KEY_DOWN): playery += 1 elif libtcod.console_is_key_pressed(libtcod.KEY_LEFT): playerx -= 1 elif libtcod.console_is_key_pressed(libtcod.KEY_RIGHT): playerx += 1
Done! These are the arrow keys, if you want to use other keys here's a reference (pay attention to the Python-specific notes).
While we're at it, why not include keys to toggle fullscreen mode, and exit the game? You can put this at the beginning of the function.
key = libtcod.console_check_for_keypress() if key.vk == libtcod.KEY_ENTER and key.lalt: #Alt+Enter: toggle fullscreen libtcod.console_set_fullscreen(not libtcod.console_is_fullscreen()) elif key.vk == libtcod.KEY_ESCAPE: return True #exit game
Notice a subtle difference here. The console_is_key_pressed function is useful for real-time games, since it checks if a key is pressed with no delays. console_check_for_keypress, on the other hand, treats the key like it's being typed. So after the first press, it will stop working for a fraction of a second. This is the same behavior you see when you type, otherwise pressing a key would result in you typing 3 or 4 letters! It's useful for all commands except movement, which you usually want to react as soon as possible with no delays, and continue for as long as you press the movement keys.
Now here's an important thing: you can use that first line to distinguish between real-time and turn-based gameplay! See, console_check_for_keypress won't block the game. But if you replace it with this line:
key = libtcod.console_wait_for_keypress(True)
Then the game won't go on unless the player presses a key. So effectively you have a turn-based game now.
Now, the main loop needs to call this function in order for it to work. If the returned value is True, then we "break" from the main loop, ending the game. The inside of the main loop should now look like this:
#handle keys and exit game if needed exit = handle_keys() if exit: break libtcod.console_set_foreground_color(0, libtcod.white) libtcod.console_print_left(0, playerx, playery, libtcod.BKGND_NONE, '@') libtcod.console_flush()
One more thing! If you try that, you'll see that moving you leave around a trail of little @'s. That's not what we want! We need to clear the character at the last position before moving to the new one, this can be done by simply printing a space there. Put this just before exit = handle_keys().
libtcod.console_print_left(0, playerx, playery, libtcod.BKGND_NONE, ' ')
A note for those that want a turn-based RL: you'll notice that the @ is not displayed until you press the first key. This is because the game blocks before even printing the first frame! You'll need to add first_time = True before the main loop, and change the part that calls handle_keys to:
if not first_time: exit = handle_keys() if exit: break first_time = False
Here's a rundown of the whole code.
import libtcodpy as libtcod #actual size of the window SCREEN_WIDTH = 80 SCREEN_HEIGHT = 50 LIMIT_FPS = 20 #20 frames-per-second maximum def handle_keys(): global playerx, playery key = libtcod.console_check_for_keypress() #real-time #key = libtcod.console_wait_for_keypress(True) #turn-based if key.vk == libtcod.KEY_ENTER and key.lalt: #Alt+Enter: toggle fullscreen libtcod.console_set_fullscreen(not libtcod.console_is_fullscreen()) elif key.vk == libtcod.KEY_ESCAPE: return True #exit game #movement keys if libtcod.console_is_key_pressed(libtcod.KEY_UP): playery -= 1 elif libtcod.console_is_key_pressed(libtcod.KEY_DOWN): playery += 1 elif libtcod.console_is_key_pressed(libtcod.KEY_LEFT): playerx -= 1 elif libtcod.console_is_key_pressed(libtcod.KEY_RIGHT): playerx += 1 ############################################# # Initialization & Main Loop ############################################# libtcod.console_set_custom_font('arial10x10.png', libtcod.FONT_TYPE_GREYSCALE | libtcod.FONT_LAYOUT_TCOD) libtcod.console_init_root(SCREEN_WIDTH, SCREEN_HEIGHT, 'python/libtcod tutorial', False) libtcod.sys_set_fps(LIMIT_FPS) playerx = SCREEN_WIDTH/2 playery = SCREEN_HEIGHT/2 #first_time = True #for turn-based games while not libtcod.console_is_window_closed(): libtcod.console_print_left(0, playerx, playery, libtcod.BKGND_NONE, ' ') #handle keys and exit game if needed #if not first_time: #for turn-based games exit = handle_keys() if exit: break #first_time = False #for turn-based games libtcod.console_set_foreground_color(0, libtcod.white) libtcod.console_print_left(0, playerx, playery, libtcod.BKGND_NONE, '@') libtcod.console_flush()
Generalizing
Now that we have the @ walking around, it would be a good idea to step back and think a bit about the design. Having variables for the player's coordinates is easy, but it can quickly get out of control when you're defining things such as HP, bonuses, and inventory. We're going to take the opportunity to generalize a bit.
Now, there can be such a thing as over-generalization, but we'll try not to fall in that trap. What we're going to do is define the player as a game Object, by creating that class. It will hold all position and display information (character and color). The neat thing is that the player will just be one instance of the Object class -- it's general enough that you can re-use it to define items on the floor, monsters, doors, stairs; anything representable by a character on the screen. Here's the class, with the initialization, and three common methods move, draw and clear. The code for drawing and erasing is the same as the one we used for the player earlier.
class Object: #this is a generic object: the player, a monster, an item, the stairs... #it's always represented by a character on screen. 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 by the given amount self.x += dx self.y += dy def draw(self): #set the color and then draw the character that represents this object at its position libtcod.console_set_foreground_color(0, self.color) libtcod.console_put_char(0, self.x, self.y, self.char, libtcod.BKGND_NONE) def clear(self): #erase the character that represents this object libtcod.console_put_char(0, self.x, self.y, ' ', libtcod.BKGND_NONE)
Now, before the main loop, instead of just setting the player's coordinates, we create it as an actual Object. We also add it to a list, that will hold all objects that are in the game. While we're at it we'll add a yellow @ that represents a non-playing character, like in an RPG, just to test it out!
player = Object(SCREEN_WIDTH/2, SCREEN_HEIGHT/2, '@', libtcod.white) npc = Object(SCREEN_WIDTH/2 - 5, SCREEN_HEIGHT/2, '@', libtcod.yellow) objects = [npc, player]
We'll have to make a couple of changes now. First, in the handle_keys function, instead of dealing directly with the player's coordinates, we can use the player's move method with the appropriate displacement. Later this will come in handy as it can automatically check if the player (or another object) is about to hit a wall. Secondly, the main loop will now clear all objects like this:
for object in objects: object.clear()
And draw them like this:
for object in objects: object.draw()
Ok, that's all! A fully generic object system. Later, this class can be modified to have all the special info that items, monsters and all that will require. But we can add that as we go along! Here's the code so far.
import libtcodpy as libtcod #actual size of the window SCREEN_WIDTH = 80 SCREEN_HEIGHT = 50 LIMIT_FPS = 20 #20 frames-per-second maximum class Object: #this is a generic object: the player, a monster, an item, the stairs... #it's always represented by a character on screen. 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 by the given amount self.x += dx self.y += dy def draw(self): #set the color and then draw the character that represents this object at its position libtcod.console_set_foreground_color(0, self.color) libtcod.console_put_char(0, self.x, self.y, self.char, libtcod.BKGND_NONE) def clear(self): #erase the character that represents this object libtcod.console_put_char(0, self.x, self.y, ' ', libtcod.BKGND_NONE) def handle_keys(): key = libtcod.console_check_for_keypress() #real-time #key = libtcod.console_wait_for_keypress(True) #turn-based if key.vk == libtcod.KEY_ENTER and key.lalt: #Alt+Enter: toggle fullscreen libtcod.console_set_fullscreen(not libtcod.console_is_fullscreen()) elif key.vk == libtcod.KEY_ESCAPE: return True #exit game #movement keys if libtcod.console_is_key_pressed(libtcod.KEY_UP): player.move(0, -1) elif libtcod.console_is_key_pressed(libtcod.KEY_DOWN): player.move(0, 1) elif libtcod.console_is_key_pressed(libtcod.KEY_LEFT): player.move(-1, 0) elif libtcod.console_is_key_pressed(libtcod.KEY_RIGHT): player.move(1, 0) ############################################# # Initialization & Main Loop ############################################# libtcod.console_set_custom_font('arial10x10.png', libtcod.FONT_TYPE_GREYSCALE | libtcod.FONT_LAYOUT_TCOD) libtcod.console_init_root(SCREEN_WIDTH, SCREEN_HEIGHT, 'python/libtcod tutorial', False) libtcod.sys_set_fps(LIMIT_FPS) #create object representing the player player = Object(SCREEN_WIDTH/2, SCREEN_HEIGHT/2, '@', libtcod.white) #create an NPC npc = Object(SCREEN_WIDTH/2 - 5, SCREEN_HEIGHT/2, '@', libtcod.yellow) #the list of objects with those two objects = [npc, player] #first_time = True #for turn-based games while not libtcod.console_is_window_closed(): #erase all objects at their old locations, before they move for object in objects: object.clear() #handle keys and exit game if needed #if not first_time: #for turn-based games exit = handle_keys() if exit: break #first_time = False #for turn-based games #draw all objects in the list for object in objects: object.draw() libtcod.console_flush()
Missing sections
Here are some quick guidelines for the next sections. Remember the goal is to create a RL that feels complete, but with minimal fluff so anyone can do it. The sections are not set in stone, they're open to debate and will surely go through many changes. One important thing to note is that we shouldn't worry about making the absolutely coolest RL ever, it's nice to leave some blanks deliberately for the reader to fill in (things that are simple enough but by not extending them to the fullest potential we're reducing the tutorial size and motivating the reader to want to change something).
Levels
The code includes a simple algorithm, it's just a sequence of rooms, each one connected to the next through a tunnel. The overlaps make it look more complex than may be apparent at first though.
This section could introduce the map, FOV, and finally the dungeon generator.
import libtcodpy as libtcod # Import Psyco if available try: import psyco psyco.full() except ImportError: pass #actual size of the window SCREEN_WIDTH = 80 SCREEN_HEIGHT = 50 #size of the map MAP_WIDTH = 80 MAP_HEIGHT = 45 #parameters for dungeon generator ROOM_MAX_SIZE = 10 ROOM_MIN_SIZE = 6 MAX_ROOMS = 30 TORCH_RADIUS = 10 SQUARED_TORCH_RADIUS = TORCH_RADIUS * TORCH_RADIUS FOV_ALGO = 0 #default FOV algorithm FOV_LIGHT_WALLS = True #light walls or not LIMIT_FPS = 20 #20 frames-per-second maximum fov_dark_wall = libtcod.Color(0, 0, 100) fov_light_wall = libtcod.Color(130, 110, 50) fov_dark_ground = libtcod.Color(50, 50, 150) fov_light_ground = libtcod.Color(200, 180, 50) class Tile: #a tile of the map and its properties def __init__(self, blocked, block_sight = None): self.blocked = blocked #all tiles start unexplored self.explored = False #by default, if a tile is blocked, it also blocks sight if block_sight is None: block_sight = blocked self.block_sight = block_sight class Rect: #a rectangle on the map. used to characterize a room. def __init__(self, x, y, w, h): self.x1 = x self.y1 = y self.x2 = x + w self.y2 = y + h def center(self): center_x = (self.x1 + self.x2) / 2 center_y = (self.y1 + self.y2) / 2 return (center_x, center_y) def intersect(self, other): #returns true if this rectangle intersects with another one return (self.x1 <= other.x2 and self.x2 >= other.x1 and self.y1 <= other.y2 and self.y2 >= other.y1) class Object: #this is a generic object: the player, a monster, an item, the stairs... #it's always represented by a character on screen. 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 by the given amount, if the destination is not blocked if not map[self.y + dy][self.x + dx].blocked: self.x += dx self.y += dy def draw(self): #only show if it's visible to the player if libtcod.map_is_in_fov(fov_map, self.x, self.y): #set the color and then draw the character that represents this object at its position libtcod.console_set_foreground_color(console, self.color) libtcod.console_put_char(console, self.x, self.y, self.char, libtcod.BKGND_NONE) def clear(self): #erase the character that represents this object libtcod.console_put_char(console, self.x, self.y, ' ', libtcod.BKGND_NONE) def create_room(room): global map #go through the tiles in the rectangle and make them passable for x in range(room.x1 + 1, room.x2): for y in range(room.y1 + 1, room.y2): map[y][x].blocked = False map[y][x].block_sight = False def create_h_tunnel(x1, x2, y): global map #horizontal tunnel. min() and max() are used in case x1>x2 for x in range(min(x1, x2), max(x1, x2) + 1): map[y][x].blocked = False map[y][x].block_sight = False def create_v_tunnel(y1, y2, x): global map #vertical tunnel for y in range(min(y1, y2), max(y1, y2) + 1): map[y][x].blocked = False map[y][x].block_sight = False def make_map(): global map, player, stairs #fill map with "blocked" tiles map = [[ Tile(True) for x in range(MAP_WIDTH) ] for y in range(MAP_HEIGHT) ] rooms = [] num_rooms = 0 for r in range(MAX_ROOMS): #random width and height w = libtcod.random_get_int(0, ROOM_MIN_SIZE, ROOM_MAX_SIZE) h = libtcod.random_get_int(0, ROOM_MIN_SIZE, ROOM_MAX_SIZE) #random position without going out of the boundaries of the map x = libtcod.random_get_int(0, 0, MAP_WIDTH - w - 1) y = libtcod.random_get_int(0, 0, MAP_HEIGHT - h - 1) #"Rect" class makes rectangles easier to work with new_room = Rect(x, y, w, h) #run through the other rooms and see if they intersect with this one failed = False for other_room in rooms: if new_room.intersect(other_room): failed = True break if not failed: #this means there are no intersections, so this room is valid #"paint" it to the map's tiles create_room(new_room) #center coordinates of new room, will be useful later (new_x, new_y) = new_room.center() if num_rooms == 0: #this is the first room, where the player starts at player.x = new_x player.y = new_y else: #all rooms after the first: #connect it to the previous room with a tunnel #center coordinates of previous room (prev_x, prev_y) = rooms[num_rooms-1].center() #draw a coin (random number that is either 0 or 1) if libtcod.random_get_int(0, 0, 1) == 1: #first move horizontally, then vertically create_h_tunnel(prev_x, new_x, prev_y) create_v_tunnel(prev_y, new_y, new_x) else: #first move vertically, then horizontally create_v_tunnel(prev_y, new_y, prev_x) create_h_tunnel(prev_x, new_x, new_y) #finally, append the new room to the list rooms.append(new_room) num_rooms += 1 #after that, place the stairs at the last room stairs.x = new_x stairs.y = new_y def render_all(): global fov_map, fov_dark_wall, fov_light_wall global fov_dark_ground, fov_light_ground global fov_recompute #draw all objects in the list for object in objects: object.draw() if fov_recompute: #recompute FOV if needed (the player moved or something) fov_recompute = False libtcod.map_compute_fov(fov_map, player.x, player.y, TORCH_RADIUS, FOV_LIGHT_WALLS, FOV_ALGO) #go through all tiles, and set their background color according to the FOV for y in range(MAP_HEIGHT): for x in range(MAP_WIDTH): visible = libtcod.map_is_in_fov(fov_map, x, y) wall = map[y][x].block_sight if not visible: #if it's not visible right now, the player can only see it if it's explored if map[y][x].explored: if wall: libtcod.console_set_back(console, x, y, fov_dark_wall, libtcod.BKGND_SET) else: libtcod.console_set_back(console, x, y, fov_dark_ground, libtcod.BKGND_SET) else: #it's visible if wall: libtcod.console_set_back(console, x, y, fov_light_wall, libtcod.BKGND_SET ) else: libtcod.console_set_back(console, x, y, fov_light_ground, libtcod.BKGND_SET ) #since it's visible, explore it map[y][x].explored = True def handle_keys(): global fov_recompute key = libtcod.console_check_for_keypress() if key.vk == libtcod.KEY_ENTER and key.lalt: #Alt+Enter: toggle fullscreen libtcod.console_set_fullscreen(not libtcod.console_is_fullscreen()) elif key.vk == libtcod.KEY_ESCAPE: return True #exit game #movement keys elif libtcod.console_is_key_pressed(libtcod.KEY_UP): player.move(0, -1) fov_recompute = True elif libtcod.console_is_key_pressed(libtcod.KEY_DOWN): player.move(0, 1) fov_recompute = True elif libtcod.console_is_key_pressed(libtcod.KEY_LEFT): player.move(-1, 0) fov_recompute = True elif libtcod.console_is_key_pressed(libtcod.KEY_RIGHT): player.move(1, 0) fov_recompute = True ############################################# # Initialization & Main Loop ############################################# libtcod.console_set_custom_font('arial10x10.png', libtcod.FONT_TYPE_GREYSCALE | libtcod.FONT_LAYOUT_TCOD) libtcod.console_init_root(SCREEN_WIDTH, SCREEN_HEIGHT, 'python/libtcod tutorial', False) console = libtcod.console_new(SCREEN_WIDTH, SCREEN_HEIGHT) libtcod.sys_set_fps(LIMIT_FPS) #create object representing the player (location doesn't matter, make_map will set it appropriately) player = Object(0, 0, '@', libtcod.black) #create object representing the stairs stairs = Object(0, 0, '<', libtcod.white) #the list of objects with those two objects = [player, stairs] #generate map (at this point it's not drawn to the screen) make_map() #create the FOV map, according to the generated map fov_map = libtcod.map_new(MAP_WIDTH, MAP_HEIGHT) for y in range(MAP_HEIGHT): for x in range(MAP_WIDTH): libtcod.map_set_properties(fov_map, x, y, not map[y][x].blocked, not map[y][x].block_sight) #start with a blank console libtcod.console_clear(console) fov_recompute = True credits_end = False while not libtcod.console_is_window_closed(): #erase all objects at their old locations, before they move for object in objects: object.clear() #handle keys and exit game if needed exit = handle_keys() if exit: break #render the screen render_all() libtcod.console_blit(console, 0, 0, MAP_WIDTH, MAP_HEIGHT, 0, 0, 0, 255) # render credits at the bottom if not credits_end: credits_end = libtcod.console_credits_render(0, MAP_HEIGHT, 0) # render stats libtcod.console_set_foreground_color(0, libtcod.grey) libtcod.console_print_right(0, 79, 46, libtcod.BKGND_NONE, 'last frame : %3d ms (%3d fps)' % (int(libtcod.sys_get_last_frame_length() * 1000.0), libtcod.sys_get_fps())) libtcod.console_print_right(0, 79, 47, libtcod.BKGND_NONE, 'elapsed : %8d ms %4.2fs' % (libtcod.sys_elapsed_milli(), libtcod.sys_elapsed_seconds())) #victory screen! if player.x == stairs.x and player.y == stairs.y: libtcod.console_clear(0) libtcod.console_set_foreground_color(0, libtcod.white) libtcod.console_print_center(0, SCREEN_WIDTH/2, SCREEN_HEIGHT/2, libtcod.BKGND_NONE, 'Victory is Yours!!') libtcod.console_flush()
Stats
HP/Attack/Defense, for both the player and every monster. (I'm sure this is one of those areas where a beginner would love to tinker and it's pretty easy to add other stats.)
Items
Additive HP/Attack/Defense modifiers when worn. A string determines its class. Can equip one item of every class (weapon, armor, helmet...). Item screen with drop and use options (use equips/dequips stuff). (Should be relatively easy in python at least, where list support is awesome.)
Combat
Damage = Attack - Defense, or something. Would be cool to have a special graphical effect tied to wands and staffs (which would just be weapons with different names).
AI
Cast ray to player, if unblocked move towards, if near it, attack.