updates in a minor key (a roguelike in Python #6)

The Python demo has seen a lot of small changes as I attempt to drag it kicking and screaming into something a little more usable for a real game.

centaur01

It’s at about 500 LOC at the moment and not yet incomprehensible to me — a good thing I think since I’m not working on it every day — and posted in full below, but I’ll try to highlight the bigger changes first.

One big change is I’m now using a different structure to hold the level information. I still have a hardcoded level, but when I initalize the world object I run the level through this function:

    def level_make(self):
        self.level_state = []
        for line in self.level:
            line_data = []
            for cell in line:
                line_data.append(self.cell_types[cell])
            self.level_state.append(line_data)

The function level_make() uses the self.cell_types dictionary, which at the moment looks like this:

        self.cell_types = {
                            ' ' : [' ', 'transparent', 'walkable'],
                            '#' : ['#'],
                            '=' : ['=', 'transparent']}

So instead of an array of strings as I had before with just the hardcoded map, now cell data is in an array of arrays. When I want to add a new cell type I’ll add it to the dictionary, and when I want to add new properties to the cells I can include it in the array. I don’t need to add a new property to each cell type — just the one where it counts. I hope this won’t get too confusing in the future.

I felt like I needed this structure to handle cases where the walkability or transparency of an individual cell was changed due to some area affect — it wouldn’t really do to just hope all ground cells always would be walkable.

As you would expect this level structure changes how you test for certain things — for example, in the field-of-view code, I now have code like this:

if 'walkable' in cell and 'transparent' in cell:
                libtcod.map_set_properties(self.fov_map, j, i, True, True)
            elif 'walkable' in cell:
                libtcod.map_set_properties(self.fov_map, j, i, False, True)
            elif 'transparent' in cell:
                libtcod.map_set_properties(self.fov_map, j, i, True, False)

whereas before I tested the cell type directly.

In the screenshot you can see the colors have changed — it’s just temporary at the moment, but I’m trying to add better looking lights to the view. Currently I’m mocking it up with some ad hoc light code in the Fov class, with the goal of keeping track of a separate light map later down the road.

Of course, I can’t not point out that other little creature scampering around — the fearsome ‘?’.

Mr ‘?’ is running on this behaviour handler:

class AI(object):
    def __init__(self, thing, brain):
        self.choices = ['up', 'down', 'left', 'right']
        self.thing = thing
            
    def update(self):
        choice = random.randint(0, 3)
        command_name = self.choices[choice]
        command_func = getattr(game.command, command_name)
        command_func(self.thing)

and I created it with an instancing of the Actor class in the setting up stages of the game object. It’s pretty dumb, as it chooses a direction randomly on each update.

One last small thing is the first attempt to hold all the game’s things in a list — the Actor class adds each new instance of itself to this list, for example. I’m unsure at the moment if the player should be in this list, though at the moment it is — I might change that to keep things simpler.

My next goal is to get some rudimentary saving and loading going.

Here’s the complete demo:

#!/usr/bin/python

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




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

import os, random

import libtcodpy as libtcod

     
        
        
#############################################
# be things 
#############################################

class Thing(object):
    def __init__(self, name, x=-1, y=-1):
        self.name = name
        self.x = x
        self.y = y
        
    def update(self):
        pass
        

        
class World(Thing):
    def __init__(self):        
        self.cell_types = {
                            ' ' : [' ', 'transparent', 'walkable'],
                            '#' : ['#'],
                            '=' : ['=', 'transparent']}
        
        super(World, self).__init__('world')
        self.walkable = [' ']
        
        self.things = []
                
        self.level_width = 46
        self.level_height = 31
        self.level = [
        '##############################################',
        '#######################      #################',
        '#####################    #     ###############',
        '######################  ###        ###########',
        '##################      #####             ####',
        '################       ########    ###### ####',
        '###############      #################### ####',
        '################    ######                  ##',
        '########   #######  ######   #     #     #  ##',
        '########   ######      ###                  ##',
        '########                                    ##',
        '####       ######      ###   #     #     #  ##',
        '#### ###   ########## ####                  ##',
        '#### ###   ##########   ###########=##########',
        '#### ##################   #####          #####',
        '#### ###             #### #####          #####',
        '####           #     ####                #####',
        '########       #     #### #####          #####',
        '########       #####      ####################',
        '##########   #################################',
        '##########   #################################',
        '###########  #################   #############',
        '############  ###############   ##############',
        '#############   ###########     ##############',
        '###############  #########      ##############',
        '################   ####                  #####',
        '################   ###   ###########   #######',
        '###############   ##   ##############  #######',
        '#############   ###  #################   #####',
        '#############       ##################       #',
        '##############################################',
        ]
        
        self.level_make()
                
    
    def level_make(self):
        self.level_state = []
        for line in self.level:
            line_data = []
            for cell in line:
                line_data.append(self.cell_types[cell])
            self.level_state.append(line_data)
            
            
            
class Actor(Thing):
    def __init__(self, name, x=-1, y=-1, *handlers):
        super(Actor, self).__init__(name, x, y)

        self.brain = Brain()
        self.brain.add_handlers(self, *handlers)
        
        self.speed = self.brain.speed
                
        game.world.things.append(self)
                
    def update(self):
        self.brain.update()
        
        
        
#############################################
# do things 
#############################################

class Brain(object):
    def __init__(self):			                    
        self.handlers = []
        self.speed = 'normal'
        
        
    def add_handlers(self, thing, *handlers):
        for handler in handlers:
            self.handlers.append(handler(thing, self))
         
         
    def remove_handlers(self, *handlers):
        for handler in handlers:
            self.handlers.remove(handler)
          
          
    def update(self):
        for handler in self.handlers:
            handler.update()
           
           
            
class Command(object):
    def __init__(self):
        pass
        
    def cell_has_monster(self, x, y):
        for thing in game.world.things:
            if (thing.x, thing.y) == (x, y):
                return True
                
        
    def up(self, thing):
        if not self.cell_has_monster(thing.x, thing.y-1) and 'walkable' in game.world.level_state[thing.y-1][thing.x]:
            thing.y = thing.y - 1
          
          
    def down(self, thing):
        if not self.cell_has_monster(thing.x, thing.y+1) and 'walkable' in game.world.level_state[thing.y+1][thing.x]:
            thing.y = thing.y + 1
        
        
    def right(self, thing):
        if not self.cell_has_monster(thing.x+1, thing.y) and 'walkable' in game.world.level_state[thing.y][thing.x+1]:
            thing.x = thing.x + 1
        
        
    def left(self, thing):
        if not self.cell_has_monster(thing.x-1, thing.y) and 'walkable' in game.world.level_state[thing.y][thing.x-1]:
            thing.x = thing.x - 1
            
          
          
class Fov(object):
    def __init__(self, thing, brain):
        self.brain = brain
        self.fov_map = None
        self.light_map = None
        self.fov_radius = 8

        
    def update(self): 
        player.view.cells_get()
        
        self.fov_map = libtcod.map_new(player.view.width, player.view.height)
        self.light_map = libtcod.map_new(game.world.level_width, game.world.level_height)

                        
        for cells in player.view.cells:
            x, y, j, i = cells
            cell = game.world.level_state[y][x]
            
            if 'walkable' in cell and 'transparent' in cell:
                libtcod.map_set_properties(self.fov_map, j, i, True, True)
            elif 'walkable' in cell:
                libtcod.map_set_properties(self.fov_map, j, i, False, True)
            elif 'transparent' in cell:
                libtcod.map_set_properties(self.fov_map, j, i, True, False)
            
            if 'walkable' in cell and 'transparent' in cell:
                libtcod.map_set_properties(self.light_map, j, i, True, True)
            elif 'walkable' in cell:
                libtcod.map_set_properties(self.light_map, j, i, False, True)
            elif 'transparent' in cell:
                libtcod.map_set_properties(self.light_map, j, i, True, False)
                
        libtcod.map_compute_fov(self.fov_map, player.view.focus_x, player.view.focus_y, self.fov_radius, True)
        libtcod.map_compute_fov(self.light_map, 15, 10, 2, True)

        self.brain.fov_map = self.fov_map
        self.brain.light_map = self.light_map
        
        
        
class AI(object):
    def __init__(self, thing, brain):
        self.choices = ['up', 'down', 'left', 'right']
        self.thing = thing
            
    def update(self):
        choice = random.randint(0, 3)
        command_name = self.choices[choice]
        command_func = getattr(game.command, command_name)
        command_func(self.thing)


        
        



#############################################
# player
#############################################

class Awesome(object):
    def __init__(self):
        self.thing = Actor('player', 26, 16, Fov)
        self.ui = UI()
        self.view = View()
        self.input = Input()

        game.view = self.view
        game.input = self.input
        
        

        
        
class UI(object):
    def __init__(self):
        self.width = 40
        self.height = 22
        
        self.state = self.intro_draw
        
        self.chars = {
                        'player'    : '@',
                        'monster'   : '?',
                        'ground'    : ' ',
                        'wall'      : '#',
                        'window'    : '='}
                        
        self.cell_colors = {
                            'dark wall' : libtcod.Color(0, 0, 0),      
                            'visible wall' : libtcod.Color(130, 110, 50),  
                            'dark ground' : libtcod.Color(0, 0, 0),    
                            'visible ground' : libtcod.Color(180, 180, 180),
                            'lit wall' : libtcod.Color(244, 199, 18),
                            'lit ground' : libtcod.Color(244, 231, 18)}
                        
        font = os.path.join('fonts', 'arial12x12.png')
        libtcod.console_set_custom_font(
                                        font, 
                                        libtcod.FONT_LAYOUT_TCOD | 
                                        libtcod.FONT_TYPE_GREYSCALE, 
                                        32, 
                                        8)

        libtcod.console_init_root(
                                self.width, 
                                self.height, 
                                'Python Demo', 
                                False)
        libtcod.console_credits()
               
               
               
    def draw(self):
        self.state()
        
        
    def intro_draw(self):
        libtcod.console_clear(0)
        libtcod.console_set_foreground_color(0, libtcod.white)
        libtcod.console_print_center(0, 20, 8, libtcod.BKGND_NONE, "A Python Demo")
        libtcod.console_print_center(0, 20, 11, libtcod.BKGND_NONE, "press any key")
        libtcod.console_flush()
        
        
    def gameplay_draw(self):
        libtcod.console_clear(0)
        
        libtcod.console_set_foreground_color(0, libtcod.white)
        libtcod.console_print_left(0, 1, 1, libtcod.BKGND_NONE, "HJKL move around")
        libtcod.console_print_left(0, 20, 3, libtcod.BKGND_NONE, "speed: %s" % player.thing.brain.speed)
        libtcod.console_print_left(0, 20, 4, libtcod.BKGND_NONE, "game turn: %d" % game.turns)
        libtcod.console_print_left(0, 20, 5, libtcod.BKGND_NONE, "phase: %s" % game.phase)
        libtcod.console_print_left(0, 20, 6, libtcod.BKGND_NONE, "phase count: %d" % game.phase_count)


        libtcod.console_set_foreground_color(0, libtcod.black)
                
        libtcod.console_blit(
                                player.view.screen, 
                                0, 
                                0,
                                player.view.width, 
                                player.view.height,
                                0,
                                0,
                                self.height - player.view.height,
                                255)
        libtcod.console_flush()

        
        
class View(object):
    def __init__(self):
        self.width = 19
        self.height = 19
        self.focus_x = None
        self.focus_y = None
        self.style = self.scroll
        self.cells = []

        self.screen = libtcod.console_new(self.width, self.height)	
        libtcod.console_set_foreground_color(self.screen, libtcod.black)
        
                
    def update(self):
        self.draw()
        
        
    def cells_get(self):
        self.cells = []
        
        self.style()

        for i in range(self.height):
            y = self.top_view_frame + i
            for j in range(self.width):
                x = self.left_view_frame + j
                self.cells.append((x, y, j, i))
                
    
    def scroll(self):
        self.left_view_frame = min(max(0, player.thing.x - self.width//2), game.world.level_width - self.width)
        self.top_view_frame = min(max(0, player.thing.y - self.height//2), game.world.level_height - self.height)
        
        self.x_left_offset = min(player.thing.x, self.width//2)
        self.x_right_offset = max(0, (self.width//2 - (game.world.level_width - player.thing.x) + (self.width % 2)))
        self.focus_x =  self.x_left_offset + self.x_right_offset
        
        self.y_top_offset = min(player.thing.y, self.height//2)
        self.y_bottom_offset = max(0, (self.height//2 - (game.world.level_height - player.thing.y) + (self.height % 2)))
        self.focus_y =  self.y_top_offset + self.y_bottom_offset 
        
        
    def draw(self):
        libtcod.console_clear(self.screen)
        
        for cells in self.cells:
            x, y, j, i = cells
            
            affect, cell = 'dark', 'wall'
            if libtcod.map_is_in_fov(player.thing.brain.fov_map, j, i): affect = 'visible'
            if libtcod.map_is_walkable(player.thing.brain.fov_map, j, i): cell = 'ground'
                                
            if (libtcod.map_is_in_fov(player.thing.brain.light_map, x, y) and 
                libtcod.map_is_in_fov(player.thing.brain.fov_map, j, i)): affect = 'lit'
                
            color = player.ui.cell_colors['%s %s' % (affect, cell)]
            libtcod.console_set_back(self.screen, j, i, color, libtcod.BKGND_SET)    
            
            if player.ui.chars['window'] in game.world.level_state[y][x]:
                libtcod.console_put_char(self.screen, j, i, libtcod.CHAR_DHLINE, libtcod.BKGND_NONE)
                
            for thing in game.world.things:
                if not thing.name == 'player' and (thing.x, thing.y) == (x, y):
                    libtcod.console_put_char(self.screen, j, i, player.ui.chars[thing.name], libtcod.BKGND_NONE)
        
                
        libtcod.console_put_char(self.screen, self.focus_x, self.focus_y, player.ui.chars[player.thing.name], libtcod.BKGND_NONE)
                
    
    
class Input(object):
    def __init__(self):
        self.key = None
        self.keyboard = Keyboard()
       
    
    def intro_update(self):
        self.key = libtcod.console_wait_for_keypress(True)
                 
       
    def update(self):
        if player.input.key:
            self.keyboard.update()
        
        
        
class Keyboard(object):
    def __init__(self):
        self.keycfg = {
                        'k' :   'up',
                        'j' :   'down',
                        'h' :   'left',
                        'l' :   'right'}
        
        
    def get_key(self, key):
        if key.vk == libtcod.KEY_CHAR:
            return chr(key.c)
        else:
            return key.vk
                    
                    
    def update(self):
        key = self.get_key(player.input.key)
        if key in self.keycfg:
            command_name = self.keycfg[key]
            command_func = getattr(game.command, command_name)
            command_func(player.thing)
            
        player.input.key = None
                   



#############################################
# game
#############################################

class Game(object):
    def __init__(self):
        self.world = World()
        self.command = Command()
        self.view = None
        self.input = None
        self.state = self.intro_update
        
        self.phases = ['fast', 'normal', 'slow', 'quick', 'normal']
        self.phases_for = {
                            'fast' : ('fast', 'normal', 'slow'),
                            'normal' : ('normal', 'slow'),
                            'slow' : ('normal'),
                            'quick' : ('normal', 'slow', 'quick'),
                            'fast+quick' : ('fast', 'normal', 'slow', 'quick'),
                            'fast+slow' : ('fast', 'normal'),
                            'quick+slow' : ('quick', 'normal'),
                            'fast+quick+slow' : ('fast', 'normal', 'quick')}
                            
        self.phase = self.phases[0]
        self.phase_count = 0
        self.turns = 0
        

    def update(self):
        self.state()
        
    
    def intro_update(self):
        player.ui.draw()

        self.input.intro_update()
            
        game.state = game.gameplay_update
        player.ui.state = player.ui.gameplay_draw
        
        self.monster = Actor('monster', 30, 16, AI)
        
        player.thing.brain.update()
        player.view.update()
        
        libtcod.console_clear(0)
            
        
    def gameplay_update(self):
        self.input.update()
        
        for thing in game.world.things:
            if game.phase in game.phases_for[thing.speed]:
                thing.update()
            
        self.view.update()

        self.phase_count += 1
        self.phase = self.phases[self.phase_count % 5]
        if game.phase in game.phases_for['normal']: self.turns += 1
            
            

   
#############################################
# get it started & run
#############################################

if __name__ == '__main__':
    game = Game()
    player = Awesome()


    while not libtcod.console_is_window_closed():
        game.update()
        player.ui.draw()

        if game.phase in game.phases_for[player.thing.brain.speed]:
            player.input.key = libtcod.console_wait_for_keypress(True)
            if player.input.key.vk == libtcod.KEY_ESCAPE:
                break
Advertisements

No comments yet

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

%d bloggers like this: