We have a dungeon now, and we can move about it freely. But are we really exploring the dungeon if we can just see it all from the beginning?

Most roguelikes (not all!) only let you see within a certain range of your character, and our will be no different. We need to implement a way to calculate the "Field of View" for our adventurer, and fortunately, libtcod makes that easy!

We'll need to define a few variables before we get started. Add these in the same section as our screen and map variables:

    ...
    max_rooms = 30

    fov_algorithm = 'BASIC'
    fov_light_walls = True
    fov_radius = 10

    colors = {
    ...

'BASIC' is just the default algorithm that libtcod uses; it has more, and I encourage you to experiment with them later. fov_light_walls just tells us whether or not to 'light up' the walls we see; you can change it if you don't like the way it looks. fov_radius is somewhat obvious, it tells us how far we can actually see.

We also need to update the colors dictionary, because now we need two more colors for the 'light' versions of both walls and floors. Walls and floors in our fov will be 'lit', distinguishing them from the ones outside what we can see.

    colors = {
        'dark_wall': (0, 0, 100),
        'dark_ground': (50, 50, 150),
        'light_wall': (130, 110, 50),
        'light_ground': (200, 180, 50)
    }

* Don't forget to add the comma after the 'dark_ground' entry; Python will throw an error without it!

If you don't like these colors, feel free to change them to your liking.

The thing about field of view is that it doesn't need to be computed every turn. In fact, it would be quite a waste to do so! We really only need change it when the player moves. Attacking, using an item, or just standing still for a turn doesn't alter FOV. We can handle this by having a boolean variable, which we'll call fov_recompute, which tells us if we need to recompute. We can define it somewhere above our game loop (I put mine right after the map initialization).

    ...
    make_map(game_map, max_rooms, room_min_size, room_max_size, map_width, map_height, player)

    fov_recompute = True

    while not tdl.event.is_window_closed():
    ...

It's True by default, because we have to compute it right when the game starts.

If the fov_recompute condition is True, we'll need to calculate the field of view and store it somewhere. But where does the field of view actually come from? Luckily for us, tdl offers a nice way to get FOV easily, and it comes from its map. The method compute_fov will compute the field of view, based on the location the player is standing in and a few other variables. The FOV is then available for access in a variable called fov, which is stored directly on the map.

Modify the start of the game loop as below. We'll recompute the field of view if we need to, and modify render_all to accept the fov_recompute variable as a parameter.

    ...
    while not tdl.event.is_window_closed():
        if fov_recompute:
            game_map.compute_fov(player.x, player.y, fov=fov_algorithm, radius=fov_radius, light_walls=fov_light_walls)

        render_all(con, entities, game_map, root_console, screen_width, screen_height, colors)
        render_all(con, entities, game_map, fov_recompute, root_console, screen_width, screen_height, colors)
        tdl.flush()

        clear_all(con, entities)

        fov_recompute = False

        for event in tdl.event.get():
            ...

Note that after the screen is rendered, we're setting fov_recompute to false no matter what. But where does it get set to True? Let's put that right after the player's move function gets called, just a bit farther down the game loop.

            ...
            if game_map.walkable[player.x + dx, player.y + dy]:
                player.move(dx, dy)

                fov_recompute = True

        if exit:
            ...

Now we've ensured that the field of view is only recalculated after the player moves. This should save us some CPU cycles, making our game faster. How much faster will largely depend on the radius of the field of view, and the complexity of our algorithm.

Computing the field of view is all fine and good, but it doesn't do much for us if we don't actually display it. Open up render_functions.py and modify render_all like this:

def render_all(con, entities, game_map, root_console, screen_width, screen_height, colors):
def render_all(con, entities, game_map, fov_recompute, root_console, screen_width, screen_height, colors):
    if fov_recompute:
        for x, y in game_map:
            wall = not game_map.transparent[x, y]

            if game_map.fov[x, y]:
                if wall:
                    con.draw_char(x, y, None, fg=None, bg=colors.get('light_wall'))
                else:
                    con.draw_char(x, y, None, fg=None, bg=colors.get('light_ground'))
            else:
                if wall:
                    con.draw_char(x, y, None, fg=None, bg=colors.get('dark_wall'))
                else:
                    con.draw_char(x, y, None, fg=None, bg=colors.get('dark_ground'))

    # Draw all entities in the list
    for entity in entities:
        draw_entity(con, entity, game_map.fov)

    root_console.blit(con, 0, 0, screen_width, screen_height, 0, 0)

* Note: Blue denotes lines that are exactly the same as before, expect for their indentation. The if statements for fov_recompute and visible force certain lines to be indented farther than they were before. Remember, this is Python, indentation matters!

Now our render_all function will display tiles differently, depending on if they're in our field of view or not. If a tile falls in the fov_map, we draw it with the 'light' colors, and if not, we draw the 'dark' version.

Run the project now. The player's field of view is now visible! But, despite being able to "see" the FOV, it still doesn't really do anything. We can still see the entire map, along with our NPC. Luckily, the changes we have to make to fix this are fairly minimal.

Let's start with our NPC. We should just be able to modify our draw_entity function to account for the field of view, which would solve our problem.

def draw_entity(con, entity):
def draw_entity(con, entity, fov):
    if fov[entity.x, entity.y]:
        con.draw_char(entity.x, entity.y, entity.char, entity.color, bg=None)

* Again, the blue means the line is the same as before, except the indentation has changed.

Also be sure to update the part where we call the function:

    for entity in entities:
        draw_entity(con, entity)
        draw_entity(con, entity, game_map.fov)

Run the project again, and you won't see the NPC unless it's in your field of view.

Now for the map. In traditional roguelikes, your character can only see whats inside its field of view, but it will "remember" areas that were explored previously.

The default Map class in tdl does not include something like this, so we'll need to add it ourselves. One method of doing so is to subclass the Map class, and add it to this new subclass. We'll call our subclass GameMap, and put it in map_utils.py. Don't forget to import the map class as well, from tdl.map:

from tdl.map import Map

from random import randint


class GameMap(Map):
    def __init__(self, width, height):
        super().__init__(width, height)
        self.explored = [[False for y in range(height)] for x in range(width)]

We're really not changing much about the Map class; we're just adding a 2d array called explored, which will inform us of which tiles our player has 'seen' before.

Now we'll put our new GameMap class to use. Modify engine.py to use this class instead of the tdl one:

    ...
    con = tdl.Console(screen_width, screen_height)

    game_map = tdl.map.Map(map_width, map_height)
    game_map = GameMap(map_width, map_height)
    make_map(game_map, max_rooms, room_min_size, room_max_size, map_width, map_height, player)
    ...

Also be sure to import this class into engine.py.

...
from input_handlers import handle_keys
from map_utils import GameMap, make_map
from render_functions import clear_all, render_all
...

Now let's fill this 2d array in each time we see a tile in render_all. We'll also modify our 'else' statement there to take the explored tiles into account.

            ...
            if game_map.fov[x, y]:
                if wall:
                    con.draw_char(x, y, None, fg=None, bg=colors.get('light_wall'))
                else:
                    con.draw_char(x, y, None, fg=None, bg=colors.get('light_ground'))

                game_map.explored[x][y] = True
            else:
            elif game_map.explored[x][y]:
                if wall:
                    con.draw_char(x, y, None, fg=None, bg=colors.get('dark_wall'))
                else:
                    con.draw_char(x, y, None, fg=None, bg=colors.get('dark_ground'))
                ...

Now, if a tile is beyond our sight, and hasn't been seen before, it won't be drawn at all.

We now have a real, explorable dungeon! True, there may not be much in there right now, but this was a major step to a working game. In the next few parts, we'll fill the dungeon with some evil(?) monsters to punch.

If you want to see the code so far in its entirety, click here.

Click here to move on to the next part of this tutorial.