new fov (a roguelike in Python #10)
As I began in the last few days to add more things to the map with a field-of-view (starting with some lamps) it seemed like it was getting harder to work in the field-of-view structure I had. Most of this had to do with translation of world to viewport coordinates, and I realized I was calculating things in terms of viewport coordinates in the field-of-view code. Conceptually this seemed a little strange — I decided to keep all game calculations in world coordinates, and only clip to the view port when drawing to the screen.
After a day of thinking and a night of rewriting I think I have it!

Some more notes after the cut.
The full source in quite a rough state is below. As I mentioned the basic idea of the rewrite is to keep everything in world coordinates until I draw to the player’s view. So let’s start with the Fov class.
In the Fov update method I call cells_get(), which now is a method of World. This function grabs a section of the map depending on how big the field-of-view radius is; that’s another change from the old Fov, where for each update I would make an fov_map as big as the viewport.
Most of the code in cells_get deals with clipping off the section in case it’s near the level borders. Also I made provision for calling the function with no arguments (though I may get rid of that).
Whereas before this function returned a mapping of viewport coordinates and world coordinates, now it just returns world coordinates.
The next step in the Fov update is to account for the possibility that the thing calculating field-of-view is up against the level border, in which case the fov_map won’t be its full size. Finally with the size determined I allocate the map.
Next I needed to take the world coordinates returned by cells_get and determine field-of-view properties; by incrementing coordinates for the fov_map at the same time I create a mapping of world coordinates and fov_map coordinates, which I put in self.fov_map_to_world_coordinates (take these long variable names as my method of commenting!).
With this done, the next step was to rewrite the View’s draw method. So far this is what I’m doing; first, I make sure the viewport is updated with self.style() (I imagine I’ll get rid of this property before long and just directly call self.scroll()). Then I create a structure with maps that holds each fov_map with its mapping of fov and world coordinates.
I append the player’s maps last, as this ensures their view is drawn on top of everthing else. I think once I’m fully blending affects this may not be as important as it is now, though I’m still hazy on how this will work.
Next I look at each set of maps and the world coordinates in the maps structure, first determining if the coordinates are actually in the viewport at all; if not, I skip the following calculations for now (though I may want to do them later depending on what affects I have). Then I run the usual calculations for affect and cell types, finding the appropriate colors. I added a utility function to find viewport coordinates from world coordinates, though I could easily fold that into this code (which I may still do).
Currently I just draw all field-of-views, and of course I’ll change this to hide those not visible to the player.
That’s pretty much it. I like the logic of this much better.
And now I have lamps!
It feels good to get this done.
#!/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 random
import yaml
import libtcodpy as libtcod
#############################################
# be things
#############################################
class Thing(object):
def __init__(self, name, x=-1, y=-1):
self.name = name
self.speed = 'normal'
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.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)
def cells_get(self, thing=None, radius=None):
cells = []
r = 0
if radius:
r = radius
y_start = 0
x_start = 0
y_end = game.world.level_height
x_end = game.world.level_width
if thing:
y_start = max(thing.y - r, 0)
x_start = max(thing.x - r, 0)
y_end = min(game.world.level_height, thing.y + r)+1
x_end = min(game.world.level_width, thing.x + r)+1
for y in range(y_start, y_end):
for x in range(x_start, x_end):
cells.append((x, y))
return cells
def cell_has_monster(self, x, y):
for thing in game.world.things:
if (thing.x, thing.y) == (x, y) and (thing.name == 'monster' or thing.name == 'player'):
return True
def cell_is_unwalkable(self, x, y):
if not 'walkable' in game.world.level_state[y][x]:
return True
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)
game.world.things.append(self)
def update(self):
self.brain.update()
#############################################
# do things
#############################################
class Brain(object):
def __init__(self):
self.handlers = []
def add_handlers(self, thing, *handlers):
for string in handlers:
handler = eval(string)
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):
self.rulebook = {
'move' : ('cell has monster', 'cell is unwalkable')}
self.rules = {
'cell has monster' : 'game.world.cell_has_monster(thing.x+dx, thing.y+dy)',
'cell is unwalkable' : 'game.world.cell_is_unwalkable(thing.x+dx, thing.y+dy)'}
def north(self, thing):
self.move(thing, 0, -1)
def south(self, thing):
self.move(thing, 0, 1)
def east(self, thing):
self.move(thing, 1, 0)
def west(self, thing):
self.move(thing, -1, 0)
def move(self, thing, dx, dy):
if True in [eval(self.rules[rule]) for rule in self.rulebook['move']]:
pass
else:
thing.x = thing.x + dx
thing.y = thing.y + dy
class Fov(object):
def __init__(self, thing, brain):
self.thing = thing
self.brain = brain
self.fov_map = None
self.fov_radius = 1
self.fov_map_to_world_coordinates = []
def update(self):
self.fov_map_to_world_coordinates = []
cells = game.world.cells_get(self.thing, self.fov_radius)
cell_first = cells[0]
cell_last = cells[len(cells)-1]
difference_right = cell_last[0] - self.thing.x
difference_bottom = cell_last[1] - self.thing.y
difference_left = self.thing.x - cell_first[0]
difference_top = self.thing.y - cell_first[1]
fov_map_size_x = difference_right + difference_left + 1
fov_map_size_y = difference_bottom + difference_top + 1
self.fov_map = libtcod.map_new(fov_map_size_x, fov_map_size_y)
for i in range(len(cells)):
x, y = cells[i]
fov_map_x = i % fov_map_size_x
fov_map_y = i // fov_map_size_x
cell = game.world.level_state[y][x]
if 'walkable' in cell and 'transparent' in cell:
libtcod.map_set_properties(self.fov_map, fov_map_x, fov_map_y, True, True)
elif 'walkable' in cell:
libtcod.map_set_properties(self.fov_map, fov_map_x, fov_map_y, False, True)
elif 'transparent' in cell:
libtcod.map_set_properties(self.fov_map, fov_map_x, fov_map_y, True, False)
if (x, y) == (self.thing.x, self.thing.y):
fov_x_focus = fov_map_x
fov_y_focus = fov_map_y
self.fov_map_to_world_coordinates.append(((fov_map_x, fov_map_y), (x, y)))
libtcod.map_compute_fov(self.fov_map, fov_x_focus, fov_y_focus, self.fov_radius, True)
self.brain.fov_map = self.fov_map
self.brain.fov_radius = self.fov_radius
self.brain.fov_map_to_world_coordinates = self.fov_map_to_world_coordinates
class AI(object):
def __init__(self, thing, brain):
self.choices = ['north', 'south', 'west', 'east']
self.thing = thing
def update(self):
choice = random.randint(0, 3)
command_name = self.choices[choice]
command = getattr(game.command, command_name)
command(self.thing)
class Light(object):
def __init__(self, thing, brain):
pass
def update(self):
pass
#############################################
# player
#############################################
class Awesome(object):
def __init__(self):
self.ui = UI()
self.view = View()
self.input = Input()
game.view = self.view
game.input = self.input
if os.path.isfile('centaur.sav'):
file = open('centaur.sav', 'r')
game.world.things = yaml.load(file)
for thing in game.world.things:
if thing.name == 'player':
self.thing = thing
file.close()
else:
self.thing = Actor('player', 26, 16, 'Fov')
class UI(object):
def __init__(self):
self.width = 40
self.height = 22
self.state = self.intro_draw
self.chars = {
'player' : '@',
'monster' : '?',
'ground' : ' ',
'wall' : '#',
'window' : '=',
'lamp' : 'Q'}
self.cell_colors = {
'wall' : libtcod.Color(67, 65, 52),
'ground' : libtcod.Color(145, 140, 140),
'dark': libtcod.Color(20, 20, 24),
'lit': libtcod.Color(255, 255, 33),
'visible': libtcod.Color(254, 254, 233)}
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 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.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.screen = libtcod.console_new(self.width, self.height)
libtcod.console_set_foreground_color(self.screen, libtcod.black)
def update(self):
self.draw()
def scroll(self):
self.x_offset = min(max(0, player.thing.x - self.width//2), game.world.level_width - self.width)
self.y_offset = 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 world_xy_to_view_xy(self, x, y):
x = x - self.x_offset
y = y - self.y_offset
return (x, y)
def draw(self):
libtcod.console_clear(self.screen)
self.style()
maps = []
for thing in game.world.things:
if not thing.name == 'player':
for handler in thing.brain.handlers:
if 'Fov' in str(handler):
maps.append((thing.brain.fov_map, thing.brain.fov_map_to_world_coordinates))
maps.append((player.thing.brain.fov_map, player.thing.brain.fov_map_to_world_coordinates))
for tuple in maps:
fov_map, fov_map_to_world_coordinates = tuple
for cell in fov_map_to_world_coordinates:
x, y = cell[1][0], cell[1][1]
if (self.x_offset <= x <= self.x_offset+self.width and
self.y_offset <= y <= self.y_offset+self.height):
fov_map_x, fov_map_y = cell[0][0], cell[0][1]
i, j = self.world_xy_to_view_xy(x, y)
affect, cell_type = 'dark', 'wall'
if libtcod.map_is_in_fov(fov_map, fov_map_x, fov_map_y):
affect = 'visible'
if libtcod.map_is_walkable(fov_map, fov_map_x, fov_map_y):
cell_type = 'ground'
color = player.ui.cell_colors[affect] * player.ui.cell_colors[cell_type]
libtcod.console_set_back(self.screen, i, j, color, libtcod.BKGND_SET)
if player.ui.chars['window'] in game.world.level_state[y][x]:
libtcod.console_put_char(self.screen, i, j, 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, i, j, 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()
self.update_state = self.intro_update
def update(self):
self.update_state()
def gameplay_update(self):
if player.input.key:
self.keyboard.update()
def intro_update(self):
self.key = libtcod.console_wait_for_keypress(True)
class Keyboard(object):
def __init__(self):
self.keycfg = {
'k' : 'north',
'j' : 'south',
'h' : 'west',
'l' : 'east'}
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 = getattr(game.command, command_name)
command(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.update_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.update_state()
def intro_update(self):
player.ui.draw()
self.input.update()
if os.path.isfile('centaur.sav'):
for thing in game.world.things: thing.brain.update()
else:
self.monster = Actor('monster', 20, 9, 'AI')
self.lamp1 = Actor('lamp', 29, 7, 'Fov', 'Light')
self.lamp1.brain.update()
self.lamp2 = Actor('lamp', 41, 7, 'Fov', 'Light')
self.lamp2.brain.update()
self.lamp3 = Actor('lamp', 29, 12, 'Fov', 'Light')
self.lamp3.brain.update()
self.lamp4 = Actor('lamp', 41, 12, 'Fov', 'Light')
self.lamp4.brain.update()
player.thing.brain.update()
player.view.update()
self.update_state = self.gameplay_update
player.ui.state = player.ui.gameplay_draw
player.input.update_state = player.input.gameplay_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.speed]:
player.input.key = libtcod.console_wait_for_keypress(True)
if player.input.key.vk == libtcod.KEY_ESCAPE:
file = open('centaur.sav', 'w')
yaml.dump(game.world.things, file)
file.close()
break
7 comments so far
Leave a reply
Subscribe to Kooneiform




Hello,
thanks very much for your efforts! I’d very much try to learn something from your code, but I have some troubles compiling it. Shouldn’t the parantheses after “libtcod.console_set_custom_font” be closed somewhere after the line with the image code?
Anyway, regardless of whether I put the parentheses in or not, I get the following error (Python 2.6, Ubuntu):
“File “proba2.py”, line 302
SyntaxError: invalid syntax”
and the “^” sign is right below the “8″ after “alt=”". What should I do?
Sorry, somehow one line is missing from the above comment. The python feedback is this:
File “proba2.py”, line 302
^
SyntaxError: invalid syntax
All right, so it seems the code won’t be displayed in the comments. Anyway, as I said, python points to some syntax error inside the img code…
holy cow, WordPress screwed up that code! Sorry Elwo, but I didn’t even look at that line after I pasted it in — somehow the WordPress blog software screwed it up.
This is what it should like:
libtcod.console_set_custom_font(
font,
libtcod.FONT_LAYOUT_TCOD |
libtcod.FONT_TYPE_GREYSCALE,
32,
Somehow WordPress interpreted the ‘8)’ as a smiley face, within sourcecode tags no less! I believe that qualifies as a bug
.
Wow, it did it again…let’s try this:
libtcod.console_set_custom_font( font, libtcod.FONT_LAYOUT_TCOD | libtcod.FONT_TYPE_GREYSCALE, 32,
OK, all I can say is — after the 32, it should be ‘8)’.
Thanks! Works great now!