Skip to content

Commit 3a04143

Browse files
committed
Refactor FOV and pathfinding samples
1 parent 079dd62 commit 3a04143

File tree

1 file changed

+83
-158
lines changed

1 file changed

+83
-158
lines changed

examples/samples_tcod.py

+83-158
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,12 @@
6363
cur_sample = 0 # Current selected sample.
6464
frame_times = [time.perf_counter()]
6565
frame_length = [0.0]
66+
START_TIME = time.perf_counter()
67+
68+
69+
def _get_elapsed_time() -> float:
70+
"""Return time passed since the start of the program."""
71+
return time.perf_counter() - START_TIME
6672

6773

6874
class Sample(tcod.event.EventDispatch[None]):
@@ -207,13 +213,13 @@ def __init__(self) -> None:
207213
)
208214

209215
def on_enter(self) -> None:
210-
self.counter = time.perf_counter()
216+
self.counter = _get_elapsed_time()
211217
# get a "screenshot" of the current sample screen
212218
sample_console.blit(dest=self.screenshot)
213219

214220
def on_draw(self) -> None:
215-
if time.perf_counter() - self.counter >= 1:
216-
self.counter = time.perf_counter()
221+
if _get_elapsed_time() - self.counter >= 1:
222+
self.counter = _get_elapsed_time()
217223
self.x += self.x_dir
218224
self.y += self.y_dir
219225
if self.x == sample_console.width / 2 + 5:
@@ -396,8 +402,8 @@ def get_noise(self) -> tcod.noise.Noise:
396402
)
397403

398404
def on_draw(self) -> None:
399-
self.dx = time.perf_counter() * 0.25
400-
self.dy = time.perf_counter() * 0.25
405+
self.dx = _get_elapsed_time() * 0.25
406+
self.dy = _get_elapsed_time() * 0.25
401407
for y in range(2 * sample_console.height):
402408
for x in range(2 * sample_console.width):
403409
f = [
@@ -518,7 +524,7 @@ def ev_keydown(self, event: tcod.event.KeyDown) -> None:
518524
"##############################################",
519525
)
520526

521-
SAMPLE_MAP: NDArray[Any] = np.array([list(line) for line in SAMPLE_MAP_]).transpose()
527+
SAMPLE_MAP: NDArray[Any] = np.array([[ord(c) for c in line] for line in SAMPLE_MAP_]).transpose()
522528

523529
FOV_ALGO_NAMES = (
524530
"BASIC ",
@@ -555,17 +561,17 @@ def __init__(self) -> None:
555561
map_shape = (SAMPLE_SCREEN_WIDTH, SAMPLE_SCREEN_HEIGHT)
556562

557563
self.walkable: NDArray[np.bool_] = np.zeros(map_shape, dtype=bool, order="F")
558-
self.walkable[:] = SAMPLE_MAP[:] == " "
564+
self.walkable[:] = SAMPLE_MAP[:] == ord(" ")
559565

560566
self.transparent: NDArray[np.bool_] = np.zeros(map_shape, dtype=bool, order="F")
561-
self.transparent[:] = self.walkable[:] | (SAMPLE_MAP == "=")
567+
self.transparent[:] = self.walkable[:] | (SAMPLE_MAP[:] == ord("="))
562568

563569
# Lit background colors for the map.
564570
self.light_map_bg: NDArray[np.uint8] = np.full(SAMPLE_MAP.shape, LIGHT_GROUND, dtype="3B")
565-
self.light_map_bg[SAMPLE_MAP[:] == "#"] = LIGHT_WALL
571+
self.light_map_bg[SAMPLE_MAP[:] == ord("#")] = LIGHT_WALL
566572
# Dark background colors for the map.
567573
self.dark_map_bg: NDArray[np.uint8] = np.full(SAMPLE_MAP.shape, DARK_GROUND, dtype="3B")
568-
self.dark_map_bg[SAMPLE_MAP[:] == "#"] = DARK_WALL
574+
self.dark_map_bg[SAMPLE_MAP[:] == ord("#")] = DARK_WALL
569575

570576
def draw_ui(self) -> None:
571577
sample_console.print(
@@ -586,8 +592,8 @@ def on_draw(self) -> None:
586592
self.draw_ui()
587593
sample_console.print(self.player_x, self.player_y, "@")
588594
# Draw windows.
589-
sample_console.rgb["ch"][SAMPLE_MAP == "="] = 0x2550 # BOX DRAWINGS DOUBLE HORIZONTAL
590-
sample_console.rgb["fg"][SAMPLE_MAP == "="] = BLACK
595+
sample_console.rgb["ch"][SAMPLE_MAP[:] == ord("=")] = 0x2550 # BOX DRAWINGS DOUBLE HORIZONTAL
596+
sample_console.rgb["fg"][SAMPLE_MAP[:] == ord("=")] = BLACK
591597

592598
# Get a 2D boolean array of visible cells.
593599
fov = tcod.map.compute_fov(
@@ -600,7 +606,7 @@ def on_draw(self) -> None:
600606

601607
if self.torch:
602608
# Derive the touch from noise based on the current time.
603-
torch_t = time.perf_counter() * 5
609+
torch_t = _get_elapsed_time() * 5
604610
# Randomize the light position between -1.5 and 1.5
605611
torch_x = self.player_x + self.noise.get_point(torch_t) * 1.5
606612
torch_y = self.player_y + self.noise.get_point(torch_t + 11) * 1.5
@@ -632,7 +638,11 @@ def on_draw(self) -> None:
632638
# Linear interpolation between colors.
633639
sample_console.rgb["bg"] = dark_bg + (light_bg - dark_bg) * light[..., np.newaxis]
634640
else:
635-
sample_console.bg[...] = np.where(fov[:, :, np.newaxis], self.light_map_bg, self.dark_map_bg)
641+
sample_console.bg[...] = np.select(
642+
condlist=[fov[:, :, np.newaxis]],
643+
choicelist=[self.light_map_bg],
644+
default=self.dark_map_bg,
645+
)
636646

637647
def ev_keydown(self, event: tcod.event.KeyDown) -> None:
638648
MOVE_KEYS = { # noqa: N806
@@ -665,174 +675,89 @@ def ev_keydown(self, event: tcod.event.KeyDown) -> None:
665675

666676
class PathfindingSample(Sample):
667677
def __init__(self) -> None:
678+
"""Initialize this sample."""
668679
self.name = "Path finding"
669680

670-
self.px = 20
671-
self.py = 10
672-
self.dx = 24
673-
self.dy = 1
674-
self.dijkstra_dist = 0.0
681+
self.player_x = 20
682+
self.player_y = 10
683+
self.dest_x = 24
684+
self.dest_y = 1
675685
self.using_astar = True
676-
self.recalculate = False
677686
self.busy = 0.0
678-
self.old_char = " "
687+
self.cost = SAMPLE_MAP.T[:] == ord(" ")
688+
self.graph = tcod.path.SimpleGraph(cost=self.cost, cardinal=70, diagonal=99)
689+
self.pathfinder = tcod.path.Pathfinder(graph=self.graph)
679690

680-
self.map = tcod.map.Map(SAMPLE_SCREEN_WIDTH, SAMPLE_SCREEN_HEIGHT)
681-
for y in range(SAMPLE_SCREEN_HEIGHT):
682-
for x in range(SAMPLE_SCREEN_WIDTH):
683-
if SAMPLE_MAP[x, y] == " ":
684-
# ground
685-
self.map.walkable[y, x] = True
686-
self.map.transparent[y, x] = True
687-
elif SAMPLE_MAP[x, y] == "=":
688-
# window
689-
self.map.walkable[y, x] = False
690-
self.map.transparent[y, x] = True
691-
self.path = tcod.path.AStar(self.map)
692-
self.dijkstra = tcod.path.Dijkstra(self.map)
691+
self.background_console = tcod.console.Console(SAMPLE_SCREEN_WIDTH, SAMPLE_SCREEN_HEIGHT)
692+
693+
# draw the dungeon
694+
self.background_console.rgb["fg"] = BLACK
695+
self.background_console.rgb["bg"] = DARK_GROUND
696+
self.background_console.rgb["bg"][SAMPLE_MAP.T[:] == ord("#")] = DARK_WALL
697+
self.background_console.rgb["ch"][SAMPLE_MAP.T[:] == ord("=")] = ord("═")
693698

694699
def on_enter(self) -> None:
695-
# we draw the foreground only the first time.
696-
# during the player movement, only the @ is redrawn.
697-
# the rest impacts only the background color
698-
# draw the help text & player @
699-
sample_console.clear()
700-
sample_console.ch[self.dx, self.dy] = ord("+")
701-
sample_console.fg[self.dx, self.dy] = WHITE
702-
sample_console.ch[self.px, self.py] = ord("@")
703-
sample_console.fg[self.px, self.py] = WHITE
704-
sample_console.print(
705-
1,
706-
1,
707-
"IJKL / mouse :\nmove destination\nTAB : A*/dijkstra",
708-
fg=WHITE,
709-
bg=None,
710-
)
711-
sample_console.print(1, 4, "Using : A*", fg=WHITE, bg=None)
712-
# draw windows
713-
for y in range(SAMPLE_SCREEN_HEIGHT):
714-
for x in range(SAMPLE_SCREEN_WIDTH):
715-
if SAMPLE_MAP[x, y] == "=":
716-
libtcodpy.console_put_char(sample_console, x, y, libtcodpy.CHAR_DHLINE, libtcodpy.BKGND_NONE)
717-
self.recalculate = True
700+
"""Do nothing."""
718701

719702
def on_draw(self) -> None:
720-
if self.recalculate:
721-
if self.using_astar:
722-
libtcodpy.path_compute(self.path, self.px, self.py, self.dx, self.dy)
723-
else:
724-
self.dijkstra_dist = 0.0
725-
# compute dijkstra grid (distance from px,py)
726-
libtcodpy.dijkstra_compute(self.dijkstra, self.px, self.py)
727-
# get the maximum distance (needed for rendering)
728-
for y in range(SAMPLE_SCREEN_HEIGHT):
729-
for x in range(SAMPLE_SCREEN_WIDTH):
730-
d = libtcodpy.dijkstra_get_distance(self.dijkstra, x, y)
731-
self.dijkstra_dist = max(d, self.dijkstra_dist)
732-
# compute path from px,py to dx,dy
733-
libtcodpy.dijkstra_path_set(self.dijkstra, self.dx, self.dy)
734-
self.recalculate = False
735-
self.busy = 0.2
703+
"""Recompute and render pathfinding."""
704+
self.pathfinder = tcod.path.Pathfinder(graph=self.graph)
705+
# self.pathfinder.clear() # Known issues, needs fixing # noqa: ERA001
706+
self.pathfinder.add_root((self.player_y, self.player_x))
707+
736708
# draw the dungeon
737-
for y in range(SAMPLE_SCREEN_HEIGHT):
738-
for x in range(SAMPLE_SCREEN_WIDTH):
739-
if SAMPLE_MAP[x, y] == "#":
740-
libtcodpy.console_set_char_background(sample_console, x, y, DARK_WALL, libtcodpy.BKGND_SET)
741-
else:
742-
libtcodpy.console_set_char_background(sample_console, x, y, DARK_GROUND, libtcodpy.BKGND_SET)
709+
self.background_console.blit(dest=sample_console)
710+
711+
sample_console.print(self.dest_x, self.dest_y, "+", fg=WHITE)
712+
sample_console.print(self.player_x, self.player_y, "@", fg=WHITE)
713+
sample_console.print(1, 1, "IJKL / mouse :\nmove destination\nTAB : A*/dijkstra", fg=WHITE, bg=None)
714+
sample_console.print(1, 4, "Using : A*", fg=WHITE, bg=None)
715+
716+
if not self.using_astar:
717+
self.pathfinder.resolve(goal=None)
718+
reachable = self.pathfinder.distance != np.iinfo(self.pathfinder.distance.dtype).max
719+
720+
# draw distance from player
721+
dijkstra_max_dist = float(self.pathfinder.distance[reachable].max())
722+
np.array(self.pathfinder.distance, copy=True, dtype=np.float32)
723+
interpolate = self.pathfinder.distance[reachable] * 0.9 / dijkstra_max_dist
724+
color_delta = (np.array(DARK_GROUND) - np.array(LIGHT_GROUND)).astype(np.float32)
725+
sample_console.rgb.T["bg"][reachable] = np.array(LIGHT_GROUND) + interpolate[:, np.newaxis] * color_delta
726+
743727
# draw the path
744-
if self.using_astar:
745-
for i in range(libtcodpy.path_size(self.path)):
746-
x, y = libtcodpy.path_get(self.path, i)
747-
libtcodpy.console_set_char_background(sample_console, x, y, LIGHT_GROUND, libtcodpy.BKGND_SET)
748-
else:
749-
for y in range(SAMPLE_SCREEN_HEIGHT):
750-
for x in range(SAMPLE_SCREEN_WIDTH):
751-
if SAMPLE_MAP[x, y] != "#":
752-
libtcodpy.console_set_char_background(
753-
sample_console,
754-
x,
755-
y,
756-
libtcodpy.color_lerp( # type: ignore[arg-type]
757-
LIGHT_GROUND,
758-
DARK_GROUND,
759-
0.9 * libtcodpy.dijkstra_get_distance(self.dijkstra, x, y) / self.dijkstra_dist,
760-
),
761-
libtcodpy.BKGND_SET,
762-
)
763-
for i in range(libtcodpy.dijkstra_size(self.dijkstra)):
764-
x, y = libtcodpy.dijkstra_get(self.dijkstra, i)
765-
libtcodpy.console_set_char_background(sample_console, x, y, LIGHT_GROUND, libtcodpy.BKGND_SET)
728+
path = self.pathfinder.path_to((self.dest_y, self.dest_x))[1:, ::-1]
729+
sample_console.rgb["bg"][tuple(path.T)] = LIGHT_GROUND
766730

767731
# move the creature
768732
self.busy -= frame_length[-1]
769733
if self.busy <= 0.0:
770734
self.busy = 0.2
771-
if self.using_astar:
772-
if not libtcodpy.path_is_empty(self.path):
773-
libtcodpy.console_put_char(sample_console, self.px, self.py, " ", libtcodpy.BKGND_NONE)
774-
self.px, self.py = libtcodpy.path_walk(self.path, True) # type: ignore[assignment]
775-
libtcodpy.console_put_char(sample_console, self.px, self.py, "@", libtcodpy.BKGND_NONE)
776-
elif not libtcodpy.dijkstra_is_empty(self.dijkstra):
777-
libtcodpy.console_put_char(sample_console, self.px, self.py, " ", libtcodpy.BKGND_NONE)
778-
self.px, self.py = libtcodpy.dijkstra_path_walk(self.dijkstra) # type: ignore[assignment]
779-
libtcodpy.console_put_char(sample_console, self.px, self.py, "@", libtcodpy.BKGND_NONE)
780-
self.recalculate = True
735+
if len(path):
736+
self.player_x = int(path.item(0, 0))
737+
self.player_y = int(path.item(0, 1))
781738

782739
def ev_keydown(self, event: tcod.event.KeyDown) -> None:
783-
if event.sym == tcod.event.KeySym.i and self.dy > 0:
784-
# destination move north
785-
libtcodpy.console_put_char(sample_console, self.dx, self.dy, self.old_char, libtcodpy.BKGND_NONE)
786-
self.dy -= 1
787-
self.old_char = sample_console.ch[self.dx, self.dy]
788-
libtcodpy.console_put_char(sample_console, self.dx, self.dy, "+", libtcodpy.BKGND_NONE)
789-
if SAMPLE_MAP[self.dx, self.dy] == " ":
790-
self.recalculate = True
791-
elif event.sym == tcod.event.KeySym.k and self.dy < SAMPLE_SCREEN_HEIGHT - 1:
792-
# destination move south
793-
libtcodpy.console_put_char(sample_console, self.dx, self.dy, self.old_char, libtcodpy.BKGND_NONE)
794-
self.dy += 1
795-
self.old_char = sample_console.ch[self.dx, self.dy]
796-
libtcodpy.console_put_char(sample_console, self.dx, self.dy, "+", libtcodpy.BKGND_NONE)
797-
if SAMPLE_MAP[self.dx, self.dy] == " ":
798-
self.recalculate = True
799-
elif event.sym == tcod.event.KeySym.j and self.dx > 0:
800-
# destination move west
801-
libtcodpy.console_put_char(sample_console, self.dx, self.dy, self.old_char, libtcodpy.BKGND_NONE)
802-
self.dx -= 1
803-
self.old_char = sample_console.ch[self.dx, self.dy]
804-
libtcodpy.console_put_char(sample_console, self.dx, self.dy, "+", libtcodpy.BKGND_NONE)
805-
if SAMPLE_MAP[self.dx, self.dy] == " ":
806-
self.recalculate = True
807-
elif event.sym == tcod.event.KeySym.l and self.dx < SAMPLE_SCREEN_WIDTH - 1:
808-
# destination move east
809-
libtcodpy.console_put_char(sample_console, self.dx, self.dy, self.old_char, libtcodpy.BKGND_NONE)
810-
self.dx += 1
811-
self.old_char = sample_console.ch[self.dx, self.dy]
812-
libtcodpy.console_put_char(sample_console, self.dx, self.dy, "+", libtcodpy.BKGND_NONE)
813-
if SAMPLE_MAP[self.dx, self.dy] == " ":
814-
self.recalculate = True
740+
"""Handle movement and UI."""
741+
if event.sym == tcod.event.KeySym.i and self.dest_y > 0: # destination move north
742+
self.dest_y -= 1
743+
elif event.sym == tcod.event.KeySym.k and self.dest_y < SAMPLE_SCREEN_HEIGHT - 1: # destination move south
744+
self.dest_y += 1
745+
elif event.sym == tcod.event.KeySym.j and self.dest_x > 0: # destination move west
746+
self.dest_x -= 1
747+
elif event.sym == tcod.event.KeySym.l and self.dest_x < SAMPLE_SCREEN_WIDTH - 1: # destination move east
748+
self.dest_x += 1
815749
elif event.sym == tcod.event.KeySym.TAB:
816750
self.using_astar = not self.using_astar
817-
if self.using_astar:
818-
libtcodpy.console_print(sample_console, 1, 4, "Using : A* ")
819-
else:
820-
libtcodpy.console_print(sample_console, 1, 4, "Using : Dijkstra")
821-
self.recalculate = True
822751
else:
823752
super().ev_keydown(event)
824753

825754
def ev_mousemotion(self, event: tcod.event.MouseMotion) -> None:
755+
"""Move destination via mouseover."""
826756
mx = event.tile.x - SAMPLE_SCREEN_X
827757
my = event.tile.y - SAMPLE_SCREEN_Y
828-
if 0 <= mx < SAMPLE_SCREEN_WIDTH and 0 <= my < SAMPLE_SCREEN_HEIGHT and (self.dx != mx or self.dy != my):
829-
libtcodpy.console_put_char(sample_console, self.dx, self.dy, self.old_char, libtcodpy.BKGND_NONE)
830-
self.dx = mx
831-
self.dy = my
832-
self.old_char = sample_console.ch[self.dx, self.dy]
833-
libtcodpy.console_put_char(sample_console, self.dx, self.dy, "+", libtcodpy.BKGND_NONE)
834-
if SAMPLE_MAP[self.dx, self.dy] == " ":
835-
self.recalculate = True
758+
if 0 <= mx < SAMPLE_SCREEN_WIDTH and 0 <= my < SAMPLE_SCREEN_HEIGHT:
759+
self.dest_x = mx
760+
self.dest_y = my
836761

837762

838763
#############################################
@@ -1044,7 +969,7 @@ def on_draw(self) -> None:
1044969
y = sample_console.height / 2
1045970
scalex = 0.2 + 1.8 * (1.0 + math.cos(time.time() / 2)) / 2.0
1046971
scaley = scalex
1047-
angle = time.perf_counter()
972+
angle = _get_elapsed_time()
1048973
if int(time.time()) % 2:
1049974
# split the color channels of circle.png
1050975
# the red channel
@@ -1529,7 +1454,7 @@ def draw_stats() -> None:
15291454
root_console.print(
15301455
root_console.width,
15311456
47,
1532-
f"elapsed : {int(time.perf_counter() * 1000):8d} ms {time.perf_counter():5.2f}s",
1457+
f"elapsed : {int(_get_elapsed_time() * 1000):8d} ms {_get_elapsed_time():5.2f}s",
15331458
fg=GREY,
15341459
alignment=libtcodpy.RIGHT,
15351460
)

0 commit comments

Comments
 (0)