Skip to content

Commit 31cc962

Browse files
committed
Add parts 4 and 5.
1 parent 668a69f commit 31cc962

File tree

4 files changed

+394
-8
lines changed

4 files changed

+394
-8
lines changed

tutorials/python/2021/index.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,15 @@ You can point out or fix typos by [making an issue or pull request](https://gith
1919
- [Part 1 - Drawing the ‘@’ symbol and moving it around](part-1)
2020
- [Part 2 - The generic Entity, the render functions, and the map](part-2)
2121
- [Part 3 - Generating a dungeon](part-3)
22-
- Part 4 - Field of view (July 13th)
23-
- Part 5 - Placing enemies and kicking them (harmlessly)
24-
- Part 6 - Doing (and taking) some damage (July 20th)
22+
- [Part 4 - Field of view](part-4)
23+
- [Part 5 - Placing enemies and kicking them (harmlessly)](part-5)
24+
- Part 6 - Doing (and taking) some damage
2525
- Part 7 - Creating the Interface
26-
- Part 8 - Items and Inventory (July 27th)
26+
- Part 8 - Items and Inventory
2727
- Part 9 - Ranged Scrolls and Targeting
28-
- Part 10 - Saving and loading (August 3rd)
28+
- Part 10 - Saving and loading
2929
- Part 11 - Delving into the Dungeon
30-
- Part 12 - Increasing Difficulty (August 10th)
30+
- Part 12 - Increasing Difficulty
3131
- Part 13 - Gearing up
3232

3333
## Extras:

tutorials/python/2021/part-3.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ With the generator setup it is now time to update `/main.py` to use it:
174174
```diff
175175

176176
import game.entity
177-
import game.game_map
177+
-import game.game_map
178178
import game.input_handlers
179179
+import game.procgen
180180

@@ -220,6 +220,6 @@ The `enter_xy` tuple is unpacked into the function call, so the tuple is spread
220220

221221
You can see the current progress of this code in its entirety [here](https://github.com/TStand90/tcod_tutorial_v2/tree/2021/part-3).
222222

223-
Part 4 is still in development.
223+
[Continue to part 4](part-4).
224224

225225
[Return to the hub](.).

tutorials/python/2021/part-4.md

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
---
2+
toc: true
3+
---
4+
5+
# Part 4 - Field of view
6+
7+
Currently the dungeon is fully visible at all times.
8+
To add a sense of exploration we will begin tracking which tiles are explored or in view at all times.
9+
10+
First is to add two new arrays to `GameMap`.
11+
`visible` is a boolean array of all currently visible tiles.
12+
`explored` is a boolean array of all tiles that have ever been seen.
13+
Add these changes to `/game/game_map.py`:
14+
```diff
15+
self.entities: Set[game.entity.Entity] = set()
16+
self.enter_xy = (width // 2, height // 2) # Entrance coordinates.
17+
{{' '}}
18+
+ self.visible = np.full((width, height), fill_value=False, order="F") # Tiles the player can currently see
19+
+ self.explored = np.full((width, height), fill_value=False, order="F") # Tiles the player has seen before
20+
+
21+
def in_bounds(self, x: int, y: int) -> bool:
22+
```
23+
An alternative way to create the above arrays is with `np.zeros((width, height), dtype=bool, order="F")`.
24+
25+
Python-tcod ports Libtcod's FOV algorithms with the [tcod.map.compute_fov](https://python-tcod.readthedocs.io/en/latest/tcod/map.html#tcod.map.compute_fov) function.
26+
This function takes a 2D array where the zero values are opaque walls, and all non-zero tiles are transparent.
27+
Because we've already used 0 for walls and 1 for floors the tiles of `GameMap.tiles` can be passed directory to this function.
28+
The function returns which tiles are visible as a boolean array.
29+
30+
We now add a method to `Engine` to handle updating the FOV.
31+
Make the following changes to `/game/engine.py`:
32+
```diff
33+
import random
34+
{{' '}}
35+
+import tcod
36+
+
37+
import game.entity
38+
...
39+
player: game.entity.Entity
40+
rng: random.Random
41+
+
42+
+ def update_fov(self) -> None:
43+
+ """Recompute the visible area based on the players point of view."""
44+
+ self.game_map.visible[:] = tcod.map.compute_fov(
45+
+ self.game_map.tiles,
46+
+ (self.player.x, self.player.y),
47+
+ radius=8,
48+
+ algorithm=tcod.FOV_SYMMETRIC_SHADOWCAST,
49+
+ )
50+
+ # If a tile is currently "visible" it will also be marked as "explored".
51+
+ self.game_map.explored |= self.game_map.visible
52+
```
53+
54+
55+
We must call this function after every action is performed in `/game/input_handlers.py`:
56+
```diff
57+
def handle_action(self, action: game.actions.Action) -> EventHandler:
58+
"""Handle actions returned from event methods."""
59+
action.perform()
60+
+ self.engine.update_fov()
61+
return self
62+
```
63+
The above will only update the visible area after the player moves.
64+
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:
65+
```diff
66+
engine.player = game.entity.Entity(engine.game_map, *engine.game_map.enter_xy, "@", (255, 255, 255))
67+
+ engine.update_fov()
68+
69+
event_handler = game.input_handlers.EventHandler(engine)
70+
```
71+
72+
Now it is time to modify the rendering function.
73+
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.
74+
Tiles in the current view will have the graphics stay the same as before.
75+
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`.
76+
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.
77+
78+
Now [np.select](https://numpy.org/doc/stable/reference/generated/numpy.select.html) is used to determine which
79+
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.
80+
Because `SHROUD` is a scalar it is [broadcast](https://numpy.org/doc/stable/user/basics.broadcasting.html) along the entire array.
81+
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](https://numpy.org/doc/stable/user/basics.broadcasting.html).
82+
83+
The final step is the entities, any entity that is not on a visible tile is not printed to the screen.
84+
85+
With this in mind we can edit `/game/rendering.py`:
86+
```diff
87+
+# SHROUD represents unexplored, unseen tiles
88+
+SHROUD = np.array((ord(" "), (255, 255, 255), (0, 0, 0)), dtype=tcod.console.rgb_graphic)
89+
+
90+
91+
def render_map(console: tcod.Console, gamemap: game.game_map.GameMap) -> None:
92+
- console.rgb[0 : gamemap.width, 0 : gamemap.height] = tile_graphics[gamemap.tiles]
93+
+ # The default graphics are of tiles that are visible.
94+
+ light = tile_graphics[gamemap.tiles]
95+
+
96+
+ # Apply effects to create a darkened map of tile graphics.
97+
+ dark = light.copy()
98+
+ dark["fg"] //= 2
99+
+ dark["bg"] //= 8
100+
+
101+
+ # If a tile is in the "visible" array, then draw it with the "light" colors.
102+
+ # If it isn't, but it's in the "explored" array, then draw it with the "dark" colors.
103+
+ # Otherwise, the default graphic is "SHROUD".
104+
+ console.rgb[0 : gamemap.width, 0 : gamemap.height] = np.select(
105+
+ condlist=[gamemap.visible, gamemap.explored],
106+
+ choicelist=[light, dark],
107+
+ default=SHROUD,
108+
+ )
109+
110+
for entity in gamemap.entities:
111+
+ if not gamemap.visible[entity.x, entity.y]:
112+
+ continue # Skip entities that are not in the FOV.
113+
console.print(entity.x, entity.y, entity.char, fg=entity.color)
114+
```
115+
116+
You can see the current progress of this code in its entirety [here](https://github.com/TStand90/tcod_tutorial_v2/tree/2021/part-4).
117+
118+
[Continue to part 5](part-5).
119+
120+
[Return to the hub](.).

0 commit comments

Comments
 (0)