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!

newfov

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,
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.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 [/sourcecode]

Advertisements

10 comments so far

  1. Elwro on

    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?

  2. Elwro on

    Sorry, somehow one line is missing from the above comment. The python feedback is this:

    File “proba2.py”, line 302

    ^
    SyntaxError: invalid syntax

  3. Elwro on

    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…

  4. georgek on

    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,
    8)

    Somehow WordPress interpreted the ‘8)’ as a smiley face, within sourcecode tags no less! I believe that qualifies as a bug :D.

  5. georgek on

    Wow, it did it again…let’s try this:

            libtcod.console_set_custom_font(
                                            font, 
                                            libtcod.FONT_LAYOUT_TCOD | 
                                            libtcod.FONT_TYPE_GREYSCALE, 
                                            32, 
                                            8)
    
  6. georgek on

    OK, all I can say is — after the 32, it should be ‘8)’.

  7. Elwro on

    Thanks! Works great now!

  8. Javier on

    hi,
    the post is old so I hope I can reach you…
    I found your series just a few weeks ago and I’m following it…

    The thing is that at this point, I get the following error, just when the player is moving around and something (can’t say what exactly, thought it was when the window was about to be displayed, but also happened to me again, perhaps when some of the leftmost lamps are to be displayed? can’t tell…)

    python: src/console_c.c:334: TCOD_console_set_back: Assertion `dat != ((void *)0) && (unsigned)(x) w && (unsigned)(y) h’ failed.

    As far as I can tell, it’s not a problem with your code, but with the library. I’m using libtcod 1.5.0 …

    any ideas?

    (anyway, wonderful series of posts! :))

  9. georgek on

    hi Javier, thanks for the positive comment. I can’t remember which version I was using with that code, but I’m guessing it was 1.4.

    If you want to follow a tutorial my best advice actually is to go to the Roguebasin wiki tutorial:

    http://roguebasin.roguelikedevelopment.org/index.php?title=Complete_Roguelike_Tutorial,_using_python%2Blibtcod

    It’s a great resource and should be more current with the latest libtcod.

  10. Javier on

    Thank you for the reply!
    In fact I’ve been following roguebasin’s tutorial too, so I guess I decided to follow yours, just like an experiment, to practice some python, and to find out what other ideas were there about roguelike development …

    I downloaded version 1.4 of libtcod library, and it now works, so that was that…

    I’ll keep on this just to learn more as I said, but I’ll keep following the newer one too đŸ™‚

    Thanks!


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: