Object-oriented, first attempt (a roguelike in Python #3)

If you look at the code for the scrolling demo, it’s obvious that I didn’t use any of the OO features of Python. Instead I just wrote procedures and used global variables.

This was a quick way to get off the ground — and in fact there’s no reason why I couldn’t write the entire roguelike this way. However, since part of the fun of self-taught programming is blindly running down alleys in parts of town you don’t know anything about, I set about to rewrite the scrolling demo in an object-oriented fashion.

As the title of this post says, this is my first attempt at an OO programming style, and I suspect I’m using the style more to organize my code (by putting code in classes) than to take real advantage of an OO framework. Nevertheless, I was surprised that the rewrite was relatively painless and quick. There’s something to be said to choosing one thing and practicing it a few times to get a handle on it, I guess, and my hope is that over time I’ll understand more about how to employ the strengths of object-orientation.

So, I begin this iteration of the demo as before, with imports.

#!/usr/bin/python

'''
libtcod 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

import libtcodpy as libtcod

Next I define some classes. I tried to keep this as simple as possible for the first attempt.

#############################################
# classes
#############################################

class Thing(object):
    def __init__(self, x, y):
	    self.x, self.y = x, y
        self.brain = None
        
    def add_brain(self, brain):
        self.brain = brain
        
    def remove_brain(self, brain):
        self.brain = None

I think inevitably some of my mudding experience is going to bleed through here. I’ve decided that all objects such as the player, monsters, and items in my roguelike will be a Thing. Some things will have a brain attached, and the Thing class just has two simple methods to add and remove a brain.

So here is the brain:

class Brain(object):
    def __init__(self):
        self.fov_map = None
	    self.fov_radius = 4
		
	    self.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)     
			}
			
	    self.do = {
		    'up'    : (0, -1),
		    'down'  : (0, 1),
		    'right' : (1, 0),
		    'left'  : (-1, 0)
		    }
                    
        self.handlers = []
        
    def add_handler(self, handler):
        self.handlers.append(handler)
            
    def remove_handler(self, handler):
        self.handlers.remove(handler)
            
    def update(self, *arguments):
        for handler in self.handlers:
            handler.update(*arguments)

I feel like this class isn’t cleanly defined, but it’s a start. There’s a few things going on here. First the brain is responsible for field-of-view. So a player, monster, or item that needs a field-of-view defined has the information it needs here. Second is the do dictionary that contains information about commands — just movement commands so far.

Third is the code for handlers. You’ll see a more complete example of a handler in a minute, but in short handlers are like tools, and a brain puts the tools to use in service of the thing it’s attached to. So each brain contains a list of handlers, and can add and remove handlers as necessary.

Finally each brain has an update method, that will run the update methods of each of its handlers. The *arguments parameter allows me to optionally pass an argument to these update methods.

Next is an example of a handler:

class keyboardHandler(object):
    def __init__(self, brain, thing):
        self.brain = brain
        self.thing = thing
        self.keycfg = {
                    'k' :   self.brain.do['up'],
                    'j' :   self.brain.do['down'],
                    'h' :   self.brain.do['left'],
                    'l' :   self.brain.do['right']
                    }
                    
    def get_key(self, key):
        if key.vk == libtcod.KEY_CHAR:
            return chr(key.c)
        else:
            return key.vk
                    
    def update(self, key):
        key = self.get_key(key)
        if key in self.keycfg:
            dx, dy = self.keycfg[key]	
            if  world.level[self.thing.y+dy][self.thing.x+dx] == world.cells['ground']:
                self.thing.x = self.thing.x + dx
                self.thing.y = self.thing.y + dy

The keyboardHandler takes input from the keyboard and uses it to move the thing of the brain the handler is attached to. So in theory I could attach this to the brain of a monster, and let the player control it.

You can see the world object named in the keyboardHandler — here it is.

class World(object):
    def __init__(self):
        self.cells = {
                    'ground'        : ' ',
                    'wall'          : '#',
                    'window'        : '='
                    }
        self.level_width = 46
        self.level_height = 31
        self.level = [
        '##############################################',
        '#######################      #################',
        '#####################    #     ###############',
        '######################  ###        ###########',
        '##################      #####             ####',
        '################       ########    ###### ####',
        '###############      #################### ####',
        '################    ######                  ##',
        '########   #######  ######   #     #     #  ##',
        '########   ######      ###                  ##',
        '########                                    ##',
        '####       ######      ###   #     #     #  ##',
        '#### ###   ########## ####                  ##',
        '#### ###   ##########   ###########=##########',
        '#### ##################   #####          #####',
        '#### ###             #### #####          #####',
        '####           #     ####                #####',
        '########       #     #### #####          #####',
        '########       #####      ####################',
        '##########   #################################',
        '##########   #################################',
        '###########  #################   #############',
        '############  ###############   ##############',
        '#############   ###########     ##############',
        '###############  #########      ##############',
        '################   ####         ##############',
        '################   ###   #####################',
        '###############   ##   #######################',
        '#############   ###  #########################',
        '#############       ##########################',
        '##############################################',
        ]

I feel like this class needs a lot of work — just a quick and dirty definition for the moment. What I’d like to do is use it to manage multiple levels and pieces of world state information. Originally it inherited from Thing; I was imagining something like the player carrying around an object they could enter, which would be a new level. But to keep things simple for now it just inherits from object.

The next two classes define the user interface.

class UI(object):
    def __init__(self, view):
        self.view = view
	    self.header = 3
	    self.width = 19
	    self.height = 19
        self.view_x = None
        self.view_y = None
        
        self.font = os.path.join('fonts', 'arial12x12.png')
        libtcod.console_set_custom_font(
                                        self.font, 
                                        libtcod.FONT_LAYOUT_TCOD | 
                                        libtcod.FONT_TYPE_GREYSCALE, 
                                        32, 
                                        8)
                                        
    def draw(self):
        libtcod.console_set_foreground_color(0, libtcod.white)
        libtcod.console_print_left(0, 1, 1, libtcod.BKGND_NONE, "HJKL move around")
        libtcod.console_set_foreground_color(0, libtcod.black)



class scrollingView(object):
    def __init__(self):
        pass
        
    def draw(self):
        player.brain.fov_map = libtcod.map_new(UI.width, UI.height)
        
        UI.view_x, UI.view_y = UI.width//2, UI.height//2

        for m in range(UI.height):
            if player.y >= (world.level_height - UI.height//2):
                y = (world.level_height - UI.height) + m
                UI.view_y = (UI.height//2) + (UI.height//2 - (world.level_height - player.y) + 1)
            elif player.y < UI.height//2:
                y = 0 + m
                UI.view_y = player.y
            else:
                y = player.y - (UI.height//2) + m
                
            for n in range(UI.width):
                if player.x >= (world.level_width - UI.width//2):
                    x = (world.level_width - UI.width) + n
                    UI.view_x = (UI.width//2) + (UI.width//2 - (world.level_width - player.x) + 1)
                elif player.x < UI.width//2:
                    x = 0 + n
                    UI.view_x = player.x
                else:
                    x = player.x - (UI.width//2) + n
                
                mh = m + UI.header
                libtcod.console_put_char(0, n, mh, world.cells&#91;'ground'&#93;, libtcod.BKGND_NONE)
                if world.level&#91;y&#93;&#91;x&#93; == world.cells&#91;'ground'&#93;:
                    libtcod.map_set_properties(player.brain.fov_map, n, m, True, True)
                elif world.level&#91;y&#93;&#91;x&#93; == world.cells&#91;'window'&#93;:
                    libtcod.map_set_properties(player.brain.fov_map, n, m, True, False)
                    libtcod.console_put_char(0, n, mh, libtcod.CHAR_DHLINE, libtcod.BKGND_NONE)

        libtcod.map_compute_fov(player.brain.fov_map, UI.view_x, UI.view_y, player.brain.fov_radius, True)
        
        for m in range(UI.height):
            for n in range(UI.width):
                mh = m + UI.header
                affect, cell = 'dark', 'wall'
                if libtcod.map_is_in_fov(player.brain.fov_map, n, m): affect = 'light'
                if libtcod.map_is_walkable(player.brain.fov_map, n, m): cell = 'ground'
                color = player.brain.fov_colors&#91;'%s %s' % (affect, cell)&#93;
                libtcod.console_set_back(0, n, mh, color, libtcod.BKGND_SET)    

        libtcod.console_put_char(0, UI.view_x, UI.view_y+UI.header, '@', libtcod.BKGND_NONE)
&#91;/sourcecode&#93;

This is all the scrolling code in one big lump. Well, I had to put it somewhere, didn't I?

The flip side is the update and draw procedures are rather simple. 

&#91;sourcecode language="python"&#93;
#############################################
# draw
#############################################
    
def draw():
    UI.view.draw()
    


#############################################
# update
#############################################

def update(key):
    for brain in brains:
        brain.update(key)
&#91;/sourcecode&#93;

I still am not quite sure how to structure the overall program, but for now these two procedures will do much of the work. Additionally, as you'll see in the main loop in a moment, I'm passing the <strong>key </strong>pressed from the main loop, to the <strong>update </strong>procedure, and on to the brains, and then to the keyboardHandler. It feels rather convoluted, but I'm not sure at the moment what a cleaner technique might be. 

Here is the rest of the code and the main loop.


#############################################
# intialize and main loop
#############################################

if __name__ == '__main__':
    UI = UI(scrollingView())
    libtcod.console_init_root(
                                UI.width, 
                                UI.height+UI.header, 
                                'Python Tutorial', 
                                False)
    libtcod.console_clear(0)
    
    UI.draw()
    
    world = World()
    
    player = Thing(26, 16)
    player.add_brain(Brain())
    player.brain.add_handler(keyboardHandler(player.brain, player))
    
    brains = [player.brain]
    
    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

First I create an instance of the UI class and initialize the root console, drawing the immutable parts of the UI on to it (in this case, the instructions on how to move). UI takes an argument of what view it’s using, so if I wanted to I could swap in a fixed view here as well.

Next in a fit of egomania I create the world, and a player, who gets a brain and the keyboardHandler attached to it. The player.brain is added to brains for the update procedure defined earlier, and then the program enters the main loop.

So that’s it!

Advertisements

4 comments so far

  1. foggynotion on

    I’m getting a great deal out of this series of posts. Thanks for taking the time to write these tutorials and good luck with your game!

  2. jice on

    Great series ! And nice clean code. I especially like the OOP version. Keep up the good work 🙂

  3. […] demo. I knew that the scrolling code was kind of a mess. I won’t bother reposting it here (you can see it here), but basically it was a few if statements for each axis inside a for loop, all to determine where […]

  4. […] in Python #4) Posted May 31, 2009 Filed under: roguelike | It’s been a while since the first attempt, but better late than never, here is I think an at least better organized object-oriented roguelike […]


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: