Currently the dungeon is fully visible at all times. To add a sense of exploration we will begin tracking which tiles are explored or in view at all times.
First is to add two new arrays to GameMap
.
visible
is a boolean array of all currently visible tiles.
explored
is a boolean array of all tiles that have ever been seen.
Add these changes to /game/game_map.py
:
self.entities: Set[game.entity.Entity] = set()
self.enter_xy = (width // 2, height // 2) # Entrance coordinates.
+ self.visible = np.full((width, height), fill_value=False, order="F") # Tiles the player can currently see
+ self.explored = np.full((width, height), fill_value=False, order="F") # Tiles the player has seen before
+
def in_bounds(self, x: int, y: int) -> bool:
An alternative way to create the above arrays is with np.zeros((width, height), dtype=bool, order="F")
.
Python-tcod ports Libtcod’s FOV algorithms with the tcod.map.compute_fov function.
This function takes a 2D array where the zero values are opaque walls, and all non-zero tiles are transparent.
Because we’ve already used 0 for walls and 1 for floors the tiles of GameMap.tiles
can be passed directory to this function.
The function returns which tiles are visible as a boolean array.
We now add a method to Engine
to handle updating the FOV.
Make the following changes to /game/engine.py
:
import random
+import tcod
+
import game.entity
...
player: game.entity.Entity
rng: random.Random
+
+ def update_fov(self) -> None:
+ """Recompute the visible area based on the players point of view."""
+ self.game_map.visible[:] = tcod.map.compute_fov(
+ self.game_map.tiles,
+ (self.player.x, self.player.y),
+ radius=8,
+ algorithm=tcod.FOV_SYMMETRIC_SHADOWCAST,
+ )
+ # If a tile is currently "visible" it will also be marked as "explored".
+ self.game_map.explored |= self.game_map.visible
We must call this function after every action is performed in /game/input_handlers.py
:
def handle_action(self, action: game.actions.Action) -> EventHandler:
"""Handle actions returned from event methods."""
action.perform()
+ self.engine.update_fov()
return self
The above will only update the visible area after the player moves.
Because of that we also need to update it before the first turn, so another call gets added to /main.py
after the map and player is setup:
engine.player = game.entity.Entity(engine.game_map, *engine.game_map.enter_xy, "@", (255, 255, 255))
+ engine.update_fov()
event_handler = game.input_handlers.EventHandler(engine)
Now it is time to modify the rendering function.
We need a default graphic for the unexplored area so a NumpPy scalar called SHROUD
(a thing that obscures) is made, which is a blank black tile.
Tiles in the current view will have the graphics stay the same as before.
We use tile_graphics[gamemap.tiles]
as before to generate the entire array but will render only the visible area by the end, this will be called light
.
A copy of light
is made called dark
and this array modified to be darker with the foreground at half brightness and the background at 1/8th brightness.
Now np.select is used to determine which
If a value on gamemap.visible
is True then the value at light
will be shown, then gamemap.explored
is checked and if that is True then dark
is shown, otherwise SHROUD
is used.
Because SHROUD
is a scalar it is broadcast along the entire array.
The default
parameter could have been another 2D array, or the arrays in condlist
could have been scalars like SHROUD
, the important part is that these fit the shape of console.rgb[0 : gamemap.width, 0 : gamemap.height]
acorind to the broadcasting rules.
The final step is the entities, any entity that is not on a visible tile is not printed to the screen.
With this in mind we can edit /game/rendering.py
:
+# SHROUD represents unexplored, unseen tiles
+SHROUD = np.array((ord(" "), (255, 255, 255), (0, 0, 0)), dtype=tcod.console.rgb_graphic)
+
def render_map(console: tcod.Console, gamemap: game.game_map.GameMap) -> None:
- console.rgb[0 : gamemap.width, 0 : gamemap.height] = tile_graphics[gamemap.tiles]
+ # The default graphics are of tiles that are visible.
+ light = tile_graphics[gamemap.tiles]
+
+ # Apply effects to create a darkened map of tile graphics.
+ dark = light.copy()
+ dark["fg"] //= 2
+ dark["bg"] //= 8
+
+ # If a tile is in the "visible" array, then draw it with the "light" colors.
+ # If it isn't, but it's in the "explored" array, then draw it with the "dark" colors.
+ # Otherwise, the default graphic is "SHROUD".
+ console.rgb[0 : gamemap.width, 0 : gamemap.height] = np.select(
+ condlist=[gamemap.visible, gamemap.explored],
+ choicelist=[light, dark],
+ default=SHROUD,
+ )
for entity in gamemap.entities:
+ if not gamemap.visible[entity.x, entity.y]:
+ continue # Skip entities that are not in the FOV.
console.print(entity.x, entity.y, entity.char, fg=entity.color)
You can see the current progress of this code in its entirety here.