From ceb000f229bc81634ebac5cd31bbfb1f41f21390 Mon Sep 17 00:00:00 2001 From: Tyrel Souza Date: Sun, 2 Jan 2022 10:23:25 -0500 Subject: [PATCH] Initial commit http://rogueliketutorials.com/tutorials/tcod/v2/part-5/ --- .idea/.gitignore | 8 ++ .../inspectionProfiles/profiles_settings.xml | 6 ++ .idea/misc.xml | 4 + .idea/modules.xml | 8 ++ .idea/roguelike.iml | 10 ++ .idea/vcs.xml | 6 ++ actions.py | 43 +++++++++ dejavu10x10_gs_tc.png | Bin 0 -> 8439 bytes engine.py | 43 +++++++++ entity.py | 15 +++ game_map.py | 23 +++++ input_handlers.py | 29 ++++++ main.py | 57 +++++++++++ procgen.py | 91 ++++++++++++++++++ requirements.txt | 3 + tile_types.py | 49 ++++++++++ 16 files changed, 395 insertions(+) create mode 100644 .idea/.gitignore create mode 100644 .idea/inspectionProfiles/profiles_settings.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/roguelike.iml create mode 100644 .idea/vcs.xml create mode 100644 actions.py create mode 100644 dejavu10x10_gs_tc.png create mode 100644 engine.py create mode 100644 entity.py create mode 100644 game_map.py create mode 100644 input_handlers.py create mode 100644 main.py create mode 100644 procgen.py create mode 100644 requirements.txt create mode 100644 tile_types.py diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..4940481 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..76a72c6 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/roguelike.iml b/.idea/roguelike.iml new file mode 100644 index 0000000..74d515a --- /dev/null +++ b/.idea/roguelike.iml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/actions.py b/actions.py new file mode 100644 index 0000000..4e6ef12 --- /dev/null +++ b/actions.py @@ -0,0 +1,43 @@ +from __future__ import annotations +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from engine import Engine + from entity import Entity + + +class Action: + def perform(self, engine: Engine, entity: Entity) -> None: + """Perform this action with the objects needed to determine its scope. + + `engine` is the scope this action is being performed in. + + `entity` is the object performing the action. + + This method must be overridden by Action subclasses. + """ + raise NotImplementedError() + + +class EscapeAction(Action): + def perform(self, engine: Engine, entity: Entity) -> None: + raise SystemExit() + + +class MovementAction(Action): + def __init__(self, dx: int, dy: int): + super().__init__() + + self.dx = dx + self.dy = dy + + def perform(self, engine: Engine, entity: Entity) -> None: + dest_x = entity.x + self.dx + dest_y = entity.y + self.dy + + if not engine.game_map.in_bounds(dest_x, dest_y): + return # OOB + if not engine.game_map.tiles["walkable"][dest_x, dest_y]: + return # can't walk + + entity.move(self.dx, self.dy) diff --git a/dejavu10x10_gs_tc.png b/dejavu10x10_gs_tc.png new file mode 100644 index 0000000000000000000000000000000000000000..ea3adbe8661bf28ac1199ce662588fa4d9317162 GIT binary patch literal 8439 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 None: + for event in events: + action = self.event_handler.dispatch(event) + if action is None: + continue + action.perform(self, self.player) + self.update_fov() + + def update_fov(self) -> None: + self.game_map.visible[:] = compute_fov( + self.game_map.tiles["transparent"], + (self.player.x, self.player.y), + radius=8, + ) + self.game_map.explored |= self.game_map.visible + + def render(self, console: Console, context: Context) -> None: + self.game_map.render(console) + for entity in self.entities: + if self.game_map.visible[entity.x, entity.y]: + console.print(entity.x, entity.y, entity.char, fg=entity.color) + + context.present(console) + console.clear() \ No newline at end of file diff --git a/entity.py b/entity.py new file mode 100644 index 0000000..61338db --- /dev/null +++ b/entity.py @@ -0,0 +1,15 @@ +from typing import Tuple + +class Entity: + """ + A generic object to represent players, enemies, items, etc. + """ + def __init__(self, x: int, y: int, char: str, color: Tuple[int, int, int]): + self.x = x + self.y = y + self.char = char + self.color = color + + def move(self, dx: int, dy: int) -> None: + self.x += dx + self.y += dy diff --git a/game_map.py b/game_map.py new file mode 100644 index 0000000..4660a86 --- /dev/null +++ b/game_map.py @@ -0,0 +1,23 @@ +import numpy as np # type: ignore +from tcod.console import Console + +import tile_types + + +class GameMap: + def __init__(self, width: int, height: int): + self.width, self.height = width, height + self.tiles = np.full((width,height), fill_value=tile_types.wall, order="F") + + self.visible = np.full((width, height), fill_value=False, order="F") + self.explored = np.full((width, height), fill_value=False, order="F") + + def in_bounds(self, x: int, y: int) -> bool: + return 0 <= x < self.width and 0 <= y < self.height + + def render(self, console: Console) -> None: + console.tiles_rgb[0:self.width, 0:self.height] = np.select( + condlist=[self.visible, self.explored], + choicelist=[self.tiles["light"], self.tiles["dark"]], + default=tile_types.SHROUD + ) diff --git a/input_handlers.py b/input_handlers.py new file mode 100644 index 0000000..51ccfee --- /dev/null +++ b/input_handlers.py @@ -0,0 +1,29 @@ +from typing import Optional + +import tcod.event + +from actions import Action, EscapeAction, MovementAction + + +class EventHandler(tcod.event.EventDispatch[Action]): + def ev_quit(self, event: tcod.event.Quit) -> Optional[Action]: + raise SystemExit() + + def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[Action]: + action: Optional[Action] = None + + key = event.sym + + if key == tcod.event.K_UP: + action = MovementAction(dx=0, dy=-1) + elif key == tcod.event.K_DOWN: + action = MovementAction(dx=0, dy=1) + elif key == tcod.event.K_LEFT: + action = MovementAction(dx=-1, dy=0) + elif key == tcod.event.K_RIGHT: + action = MovementAction(dx=1, dy=0) + + elif key == tcod.event.K_ESCAPE: + action = EscapeAction() + + return action diff --git a/main.py b/main.py new file mode 100644 index 0000000..29a3be9 --- /dev/null +++ b/main.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +import tcod + +from entity import Entity +from engine import Engine +from input_handlers import EventHandler +from procgen import generate_dungeon + + +def main(): + screen_width = 80 + screen_height = 50 + + map_width = 80 + map_height = 45 + + room_max_size = 10 + room_min_size = 6 + max_rooms = 30 + + tileset = tcod.tileset.load_tilesheet( + "dejavu10x10_gs_tc.png", 32, 8, tcod.tileset.CHARMAP_TCOD + ) + + event_handler = EventHandler() + + player = Entity(screen_width // 2, screen_height // 2, "@", (255, 255, 255)) + npc = Entity(screen_width // 2 - 5, screen_height // 2, "@", (255, 255, 0)) + entities = {npc, player} + + game_map = generate_dungeon( + max_rooms=max_rooms, + room_min_size=room_min_size, + room_max_size=room_max_size, + map_width=map_width, + map_height=map_height, + player=player + ) + + engine = Engine(entities=entities, event_handler=event_handler, game_map=game_map, player=player) + + with tcod.context.new_terminal( + screen_width, + screen_height, + tileset=tileset, + title="Yet Another Roguelike Tutorial", + vsync=True + ) as context: + root_console = tcod.Console(screen_width, screen_height, order="F") + while True: + engine.render(console=root_console, context=context) + events = tcod.event.wait() + engine.handle_events(events) + + +if __name__ == "__main__": + main() diff --git a/procgen.py b/procgen.py new file mode 100644 index 0000000..cf2d2ba --- /dev/null +++ b/procgen.py @@ -0,0 +1,91 @@ +from __future__ import annotations + +import random +from typing import Tuple, Iterator, TYPE_CHECKING, List + +import tcod.los + +import tile_types +from game_map import GameMap + +if TYPE_CHECKING: + from entity import Entity + + +class RectangularRoom: + def __init__(self, x: int, y: int, width: int, height: int): + self.x1 = x + self.y1 = y + self.x2 = x + width + self.y2 = y + height + + @property + def center(self) -> Tuple[int, int]: + return (self.x1 + self.x2) // 2, (self.y1 + self.y2) // 2 + + @property + def inner(self) -> Tuple[slice, slice]: + return slice(self.x1 + 1, self.x2), slice(self.y1 + 1, self.y2) + + def intersects(self, other: RectangularRoom) -> bool: + """Return True if this room overlaps with another RectangularRoom.""" + return ( + self.x1 <= other.x2 + and self.x2 >= other.x1 + and self.y1 <= other.y2 + and self.y2 >= other.y1 + ) + + +def tunnel_between( + start: Tuple[int, int], end: Tuple[int, int] +) -> Iterator[Tuple[int, int]]: + """ return an L shape tunel between points""" + x1, y1 = start + x2, y2 = end + if random.random() < 0.5: + corner_x, corner_y = x2, y1 + else: + corner_x, corner_y = x1, y2 + + for x, y in tcod.los.bresenham((x1, y1), (corner_x, corner_y)).tolist(): + yield x, y + + for x, y in tcod.los.bresenham((corner_x, corner_y), (x2, y2)).tolist(): + yield x, y + + +def generate_dungeon( + max_rooms: int, + room_min_size: int, + room_max_size: int, + map_width: int, + map_height: int, + player: Entity +) -> GameMap: + dungeon = GameMap(map_width, map_height) + rooms: List[RectangularRoom] = [] + + for room in range(max_rooms): + room_width = random.randint(room_min_size, room_max_size) + room_height = random.randint(room_min_size, room_max_size) + x = random.randint(0, dungeon.width - room_width - 1) + y = random.randint(0, dungeon.height - room_height - 1) + + new_room = RectangularRoom(x,y, room_width,room_height) + + if any(new_room.intersects(other_room) for other_room in rooms): + continue + + dungeon.tiles[new_room.inner] = tile_types.floor + + if len(rooms) == 0: + # first room + player.x, player.y = new_room.center + else: + for x,y in tunnel_between(rooms[-1].center, new_room.center): + dungeon.tiles[x,y] = tile_types.floor + + rooms.append(new_room) + + return dungeon diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..a3fd497 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +tcod>=11.14 +numpy>=1.18 +black \ No newline at end of file diff --git a/tile_types.py b/tile_types.py new file mode 100644 index 0000000..4b5c617 --- /dev/null +++ b/tile_types.py @@ -0,0 +1,49 @@ +from typing import Tuple + +import numpy as np # type: ignore + +graphic_dt = np.dtype( + [ + ("ch", np.int32), # unicode codepoint + ("fg", "3B"), # RGB + ("bg", "3B"), + ] +) + +tile_dt = np.dtype( + [ + ("walkable", np.bool), # True if can be walked over + ("transparent", np.bool), # True if doesn't block FOV + ("dark", graphic_dt), # graphics for when this tile is not in FOV + ("light", graphic_dt), # graphics for when this tile is not in FOV + ] +) + + +def new_tile( + *, # keywords only + walkable: int, + transparent: int, + dark: Tuple[int, Tuple[int, int, int], Tuple[int, int, int]], + light: Tuple[int, Tuple[int, int, int], Tuple[int, int, int]], +) -> np.ndarray: + """helper function for making tiles""" + return np.array((walkable, transparent, dark, light), dtype=tile_dt) + + +# SHROUD represents unexplored, unseen tiles +SHROUD = np.array((ord(" "), (255, 255, 255), (0, 0, 0)), dtype=graphic_dt) + +floor = new_tile( + walkable=True, + transparent=True, + dark=(ord(" "), (255, 255, 255), (50, 50, 150)), + light=(ord(" "), (255, 255, 255), (200, 180, 50)), +) + +wall = new_tile( + walkable=False, + transparent=False, + dark=(ord(" "), (255, 255, 255), (0, 0, 150)), + light=(ord(" "), (255, 255, 255), (130, 110, 50)), +)