Difference between revisions of "Complete Roguelike Tutorial, using python+libtcod, part 2"

From RogueBasin
Jump to navigation Jump to search
(Created page with '<center><table border="0" cellpadding="10" cellspacing="0" style="background:#F0E68C"><tr><td><center> This is part of a series of tutorials; the main page can be found [[Complet…')
 
m
Line 218: Line 218:
== Field of View (FOV) ==
== Field of View (FOV) ==


== Dungeon generator ==
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.




[[Complete Roguelike Tutorial, using python+libtcod, part 3|Go on to the next part]].
[[Complete Roguelike Tutorial, using python+libtcod, part 3|Go on to the next part]].

Revision as of 03:26, 25 December 2009

This is part of a series of tutorials; the main page can be found here.

The Dungeon

The Map

This part will introduce the map, FOV, and finally a neat dungeon generator! No roguelike is complete without those. We'll start with the map, a two-dimensional array of tiles where all your dungeon adventuring will happen. We'll start by defining its size at the top of the file. It's not quite the same size as the screen, to leave some space for a panel to show up later (where you can show stats and all). We'll try to make this as configurable as possible, this should suffice for now!

MAP_WIDTH = 80
MAP_HEIGHT = 45

Next, the tile colors. For now there are two tile types -- wall and ground. These will be their "dark" colors, which you'll see when they're not in FOV; their "lit" counterparts are not needed right now. Notice that their values are between 0 and 255, if you found colors on the web in hexadecimal format you'll have to convert them with a calculator. Finding RGB values by educated trial-and-error works at first but with time you'll have a set of colors that don't mix together very well (contrast and tone as perceived by the human eye, and all that stuff), so it's usually better to look at a chart of colors; just search for "html colors" and use one you like.

color_dark_wall = libtcod.Color(0, 0, 100)
color_dark_ground = libtcod.Color(50, 50, 150)

What sort of info will each tile hold? We'll start simple, with two values that say whether a tile is passable or not, and whether it blocks sight. In this case, it's better to seperate them early, so later you can have see-through but unpassable tiles such as chasms, or passable tiles that block sight for secret passages. They'll be defined in a Tile class, that we'll add to as we go. Believe me, this class will quickly grow to have about a dozen different values for each tile!

class Tile:
    #a tile of the map and its properties
    def __init__(self, blocked, block_sight = None):
        self.blocked = blocked
		
        #by default, if a tile is blocked, it also blocks sight
        if block_sight is None: block_sight = blocked
        self.block_sight = block_sight

As promised, the map is a two-dimensional array of tiles. The easiest way to do that is to have a list of rows, each row itself being a list of tiles; since there are no native multi-dimensional arrays in Python. We'll build it using a neat trick, list comprehensions. See, the usual way to build lists (from C++ land) is to create an empty list, then iterate with a for and add elements gradually. The syntax [element for index in range], where index and range are the same as what you'd use in a for, will return a list of elements. With two of those, one for rows and another for tiles in each row, we create the map in one fell swoop! The linked page has a ton of examples on that, and also an example of nested list comprehensions like we're using for the map. Well, that's an awful lot of words for such a tiny piece of code!

def make_map():
    global map
	
    #fill map with "unblocked" tiles
    map = [[ Tile(False)
        for y in range(MAP_HEIGHT) ]
            for x in range(MAP_WIDTH) ]

Accessing the tiles is as easy as map[x][y]. Here we add two pillars (blocked tiles) to demonstrate that, and provide a simple test.

    map[30][22].blocked = True
    map[30][22].block_sight = True
    map[50][22].blocked = True
    map[50][22].block_sight = True

Don't worry, we're already close to a playable version! Since we need to draw both the objects and the map, it now makes sense to put them all under a new function instead of directly in the main loop. Take the object rendering code to a new render_all function, and in its place (in the main loop) call render_all().

def render_all():
    #draw all objects in the list
    for object in objects:
        object.draw()

Still in the same function, we can now go through all the tiles and draw them to the screen, with the background color of a console character representing the corresponding tile. This will render the map.

    for y in range(MAP_HEIGHT):
        for x in range(MAP_WIDTH):
            wall = map[x][y].block_sight
            if wall:
                libtcod.console_set_back(0, x, y, color_dark_wall, libtcod.BKGND_SET )
            else:
                libtcod.console_set_back(0, x, y, color_dark_ground, libtcod.BKGND_SET )

Ok! Don't forget to call make_map() before the main loop, to set it up before the game begins. You should be able to see the two pillars and walk around the map now!

But wait, there's something wrong. The pillars show up, but the player can walk over them. That's easy to fix though, add this check to the beginning of the Object 's move method:

if not map[self.x + dx][self.y + dy].blocked:

Here's the code so far.

import libtcodpy as libtcod

#actual size of the window
SCREEN_WIDTH = 80
SCREEN_HEIGHT = 50

#size of the map
MAP_WIDTH = 80
MAP_HEIGHT = 45

LIMIT_FPS = 20  #20 frames-per-second maximum


color_dark_wall = libtcod.Color(0, 0, 100)
color_dark_ground = libtcod.Color(50, 50, 150)


class Tile:
    #a tile of the map and its properties
    def __init__(self, blocked, block_sight = None):
        self.blocked = blocked
        
        #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 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.x + dx][self.y + dy].blocked:
            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 make_map():
    global map
    
    #fill map with "unblocked" tiles
    map = [[ Tile(False)
        for y in range(MAP_HEIGHT) ]
            for x in range(MAP_WIDTH) ]
    
    #place two pillars to test the map
    map[30][22].blocked = True
    map[30][22].block_sight = True
    map[50][22].blocked = True
    map[50][22].block_sight = True


def render_all():
    global color_light_wall
    global color_light_ground

    #draw all objects in the list
    for object in objects:
        object.draw()
    
    #go through all tiles, and set their background color
    for y in range(MAP_HEIGHT):
        for x in range(MAP_WIDTH):
            wall = map[x][y].block_sight
            if wall:
                libtcod.console_set_back(0, x, y, color_dark_wall, libtcod.BKGND_SET )
            else:
                libtcod.console_set_back(0, x, y, color_dark_ground, libtcod.BKGND_SET )
    
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]

#generate map (at this point it's not drawn to the screen)
make_map()


#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
    
    #render the screen
    render_all()
    
    libtcod.console_flush()

Field of View (FOV)

Dungeon generator

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.


Go on to the next part.