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,
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.
No comments yet
Leave a reply
Subscribe to Kooneiform



