Object-oriented, second attempt (a roguelike in Python #4)

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 demo. There are a few main differences with the first attempt; my goal here was to do a better job of separating the game logic from the game state, and the view on the screen from the working of the game itself. I’ll just run quickly through the whole thing:

First the boilerplate:

#!/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

import libtcodpy as libtcod

Now the representation of the game state:

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

class Thing(object):
    def __init__(self, x=-1, y=-1):
        self.x = x
        self.y = y
        
        
class World(Thing):
    def __init__(self):
        super(World, self).__init__()
        self.walkable = [' ']
        self.level_width = 46
        self.level_height = 31
        self.level = [
        '##############################################',
        '#######################      #################',
        '#####################    #     ###############',
        '######################  ###        ###########',
        '##################      #####             ####',
        '################       ########    ###### ####',
        '###############      #################### ####',
        '################    ######                  ##',
        '########   #######  ######   #     #     #  ##',
        '########   ######      ###                  ##',
        '########                                    ##',
        '####       ######      ###   #     #     #  ##',
        '#### ###   ########## ####                  ##',
        '#### ###   ##########   ###########=##########',
        '#### ##################   #####          #####',
        '#### ###             #### #####          #####',
        '####           #     ####                #####',
        '########       #     #### #####          #####',
        '########       #####      ####################',
        '##########   #################################',
        '##########   #################################',
        '###########  #################   #############',
        '############  ###############   ##############',
        '#############   ###########     ##############',
        '###############  #########      ##############',
        '################   ####                  #####',
        '################   ###   ###########   #######',
        '###############   ##   ##############  #######',
        '#############   ###  #################   #####',
        '#############       ##################       #',
        '##############################################',
        ]
        
        
class Actor(Thing):
    def __init__(self, x=-1, y=-1, *handlers):
        self.brain = Brain()
        self.brain.add_handlers(*handlers)
        super(Actor, self).__init__(x, y)

The Actor class is mostly a convenience for defining a thing with a brain. I also added a walkable property to the World class to allow for walkable tiles other than empty spaces in the map. My goal currently is to forgo a tile class and just assign properties of tiles to the type and its character that represents the tile. I think it’ll keep things a little simpler; we’ll see how that goes.

Next the game logic, rather simple at the moment:

#############################################
# do things 
#############################################

class Brain(object):
    def __init__(self):			                    
        self.handlers = []
        
    def add_handlers(self, *handlers):
        for handler in handlers:
            self.handlers.append(handler(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 up(self, thing):
        if game.world.level[thing.y-1][thing.x] in game.world.walkable:
            thing.y = thing.y - 1
            
    def down(self, thing):
        if game.world.level[thing.y+1][thing.x] in game.world.walkable:
            thing.y = thing.y + 1
        
    def right(self, thing):
        if game.world.level[thing.y][thing.x+1] in game.world.walkable:
            thing.x = thing.x + 1
        
    def left(self, thing):
        if game.world.level[thing.y][thing.x-1] in game.world.walkable:
            thing.x = thing.x - 1
            
            
class Fov(object):
    def __init__(self, owner):
        self.owner = owner
        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.owner.fov_colors = self.fov_colors
        
                
    def update(self): 
        game.player.view.cells_get()
        
        self.fov_map = libtcod.map_new(game.player.view.width, game.player.view.height)
        
        for cells in game.player.view.cells:
            x, y, j, i = cells
            if game.world.level[y][x] == game.player.ui.chars['ground']:
                libtcod.map_set_properties(self.fov_map, j, i, True, True)
            elif game.world.level[y][x] == game.player.ui.chars['window']:
                libtcod.map_set_properties(self.fov_map, j, i, True, False)

        libtcod.map_compute_fov(self.fov_map, game.player.view.focus_x, game.player.view.focus_y, self.fov_radius, True)
    
        self.owner.fov_map = self.fov_map

Notice here that the code which draws characters and colors the map is no longer in this method — you’ll see it later. In short, I’ve managed to make the fov update method only calculate fov and do no drawing — though this relies on a somewhat convoluted expression of self.owner.fov_map = self.fov_map, which is how the viewing code refers to this fov_map (as otherwise the Fov class instance has no reference I could figure out to call).

Moving on, I’ve attempted to separate the demo into two broad parts — a player, which includes all the code that determines how things are drawn to the screen and how input is handled, and a game, which handles world creation and the order of how input, thing updates, and view updates are executed. I think of it as the game being a large lumbering animal, and the player like a blood-sucking insect that zooms over to the game and latches on.

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

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

        
        game.connect(self)
        
        game.view_add(self.view)
        game.actor_add(self.actor)
        game.input_add(self.input)

        
class UI(object):
    def __init__(self, owner):
        self.owner = owner
        self.width = 30
        self.height = 22
        
        self.chars = {
                        'player'    : '@',
                        'ground'    : ' ',
                        'wall'      : '#',
                        'window'    : '='}
                        
        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 Tutorial', 
                                False)
                                
    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)
                
        libtcod.console_blit(
                                self.owner.view.screen, 
                                0, 
                                0,
                                self.owner.view.width, 
                                self.owner.view.height,
                                0,
                                0,
                                self.height - self.owner.view.height,
                                255)
        libtcod.console_flush()

        
        
class View(object):
    def __init__(self, owner):
        self.owner = owner
        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))
                
        print 'self.cells calculated'

    
    
    def scroll(self):
        self.left_view_frame = min(max(0, self.owner.thing.x - self.width//2), game.world.level_width - self.width)
        self.top_view_frame = min(max(0, self.owner.thing.y - self.height//2), game.world.level_height - self.height)
        
        self.x_left_offset = min(self.owner.thing.x, self.width//2)
        self.x_right_offset = max(0, (self.width//2 - (game.world.level_width - self.owner.thing.x) + (self.width % 2)))
        self.focus_x =  self.x_left_offset + self.x_right_offset
        
        self.y_top_offset = min(self.owner.thing.y, self.height//2)
        self.y_bottom_offset = max(0, (self.height//2 - (game.world.level_height - self.owner.thing.y) + (self.height % 2)))
        self.focus_y =  self.y_top_offset + self.y_bottom_offset 
        
        
    def draw(self):
        for cells in self.cells:
            x, y, j, i = cells
            libtcod.console_put_char(self.screen, j, i, game.player.ui.chars['ground'], libtcod.BKGND_NONE)
            if game.world.level[y][x] == game.player.ui.chars['window']:
                libtcod.console_put_char(self.screen, j, i, libtcod.CHAR_DHLINE, libtcod.BKGND_NONE)

            affect, cell = 'dark', 'wall'
            if libtcod.map_is_in_fov(self.owner.actor.brain.fov_map, j, i): affect = 'light'
            if libtcod.map_is_walkable(self.owner.actor.brain.fov_map, j, i): cell = 'ground'
            color = self.owner.actor.brain.fov_colors['%s %s' % (affect, cell)]
            libtcod.console_set_back(self.screen, j, i, color, libtcod.BKGND_SET)    

        libtcod.console_put_char(self.screen, self.focus_x, self.focus_y, game.player.ui.chars['player'], libtcod.BKGND_NONE)
        
        print 'shading and characters drawn'
        
        
class Input(object):
    def __init__(self, owner):
        self.owner = owner
        self.key = None
        self.keyboard = Keyboard(self.owner)
        
    def update(self):
        self.keyboard.update()
        
        
class Keyboard(object):
    def __init__(self, owner):
        self.thing = owner.thing
        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):
        if clyde.input.key:
            key = self.get_key(game.player.input.key)
            if key in self.keycfg:
                command_name = self.keycfg[key]
                command_func = getattr(game.command, command_name)
                command_func(self.thing)
                print 'character moved'

The main thing to highlight here is that draw() method of the View class, previously found in the Fov class instead. I think it makes more sense to have the fov code solely determine field-of-view, and not draw to the screen.

The Awesome class is composed of an Actor (the player object you run around with), a UI which wraps a View, and an instance of the Input class. The passing of self as an argument when constructing these instances allows me to back-reference the instance of Awesome when necessary; I have a feeling this is kind of a hack, so maybe I can improve this later. Instead of passing self, I could of course use the name of the awesome instance instead, and this may well be the better solution in the end.

One other thing to note is the game.connect() method, which gives the instance of the game class something to refer to when it needs to refer to the player client.

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

class Game(object):
    def __init__(self):
        self.world = World()
        self.command = Command()
        self.views = []
        self.actors = []
        self.inputs = []
        
    def connect(self, who):
        self.player = who
        
    def view_add(self, *views):
        self.views.append(*views)
        
    def actor_add(self, *actors):
        self.actors.append(*actors)
        
    def input_add(self, *inputs):
        self.inputs.append(*inputs)
        
    def update(self):
        for input in self.inputs:
            input.update()

        for actor in self.actors: 
            actor.brain.update()
            
        for view in self.views:
            view.update()

There isn’t much to note here, except a bit of a struggle I had — conceptually I wanted to keep the order of input, actor update, view update, as it seemed to make the most sense to me, but it required a bit of acrobatics in the code itself, especially when separating the fov code from the drawing code (since one relies on the other and vice-versa). As you can see above I ultimately solved this by including a call to view.cells_get() in the fov update code itself.

Finally, the main loop:

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

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


    while not libtcod.console_is_window_closed():
        game.update()
        clyde.ui.draw()
        
        clyde.input.key = libtcod.console_wait_for_keypress(True)
        if clyde.input.key.vk == libtcod.KEY_ESCAPE:
            break

Most of what was in this section before has been shuffled off into previous sections of the demo; nothing really different, but I think it’s a little easier to read.

The idea is that this conceptual organization will pay some dividends down the road. I think I did a little better with the object-orientation this time around; we’ll see how it goes in the future.

Advertisements

1 comment so far

  1. Nehal J. Wani on

    To learn how to implement object oriented concepts in scenarios like developing a game, one can refer to the tutorial: https://www.youtube.com/watch?v=UBblRog4MGY


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: