diff --git a/actions.py b/actions.py index a5ba58e..a54096c 100644 --- a/actions.py +++ b/actions.py @@ -25,13 +25,29 @@ class EscapeAction(Action): raise SystemExit() -class MovementAction(Action): +class ActionWithDirection(Action): def __init__(self, dx: int, dy: int): super().__init__() self.dx = dx self.dy = dy + def perform(self, engine: Engine, entity: Entity) -> None: + raise NotImplementedError() + + +class MeleeAction(ActionWithDirection): + def perform(self, engine: Engine, entity: Entity) -> None: + dest_x = entity.x + self.dx + dest_y = entity.y + self.dy + target = engine.game_map.get_blocking_entity_at_location(dest_x, dest_y) + if not target: + return # No entity to attack. + + print(f"You kick the {target.name}, much to its annoyance!") + + +class MovementAction(ActionWithDirection): def perform(self, engine: Engine, entity: Entity) -> None: dest_x = entity.x + self.dx dest_y = entity.y + self.dy @@ -40,5 +56,18 @@ class MovementAction(Action): return # Destination is out of bounds if not engine.game_map.tiles["walkable"][dest_x, dest_y]: return # Destination is blocked by a tile. + if engine.game_map.get_blocking_entity_at_location(dest_x, dest_y): + return # Destination is blocked by an entity. entity.move(self.dx, self.dy) + + +class BumpAction(ActionWithDirection): + def perform(self, engine: Engine, entity: Entity) -> None: + dest_x = entity.x + self.dx + dest_y = entity.y + self.dy + + if engine.game_map.get_blocking_entity_at_location(dest_x, dest_y): + return MeleeAction(self.dx, self.dy).perform(engine, entity) + else: + return MovementAction(self.dx, self.dy).perform(engine, entity) diff --git a/entity.py b/entity.py index 5404984..b171549 100644 --- a/entity.py +++ b/entity.py @@ -1,15 +1,42 @@ -from typing import Tuple +from __future__ import annotations + +import copy +from typing import Tuple, TypeVar, TYPE_CHECKING + +if TYPE_CHECKING: + from game_map import GameMap + +T = TypeVar("T", bound="Entity") 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]): + + def __init__( + self, + x: int = 0, + y: int = 0, + char: str = "?", + color: Tuple[int, int, int] = (255, 255, 255), + name: str = "", + blocks_movement: bool = False, + ): self.x = x self.y = y self.char = char self.color = color + self.name = name + self.blocks_movement = blocks_movement + + def spawn(self: T, gamemap: GameMap, x: int, y: int) -> T: + """Spawn a copy of this instance at the given location.""" + clone = copy.deepcopy(self) + clone.x = x + clone.y = y + gamemap.entities.add(clone) + return clone def move(self, dx: int, dy: int): # Move the entity by a given amount diff --git a/entity_factories.py b/entity_factories.py new file mode 100644 index 0000000..c505af3 --- /dev/null +++ b/entity_factories.py @@ -0,0 +1,21 @@ +from entity import Entity + +player = Entity( + char="@", + color=(255, 255, 255), + name="PLayer", + blocks_movement=True +) + +orc = Entity( + char="o", + color=(63, 127, 63), + name="Orc", + blocks_movement=True +) +troll = Entity( + char="T", + color=(0, 127, 0), + name="Troll", + blocks_movement=True +) diff --git a/game_map.py b/game_map.py index 6c43059..1bef8c4 100644 --- a/game_map.py +++ b/game_map.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Iterable, TYPE_CHECKING +from typing import Iterable, Optional, TYPE_CHECKING import numpy as np # type: ignore from tcod.console import Console @@ -14,12 +14,19 @@ if TYPE_CHECKING: class GameMap: def __init__(self, width: int, height: int, entities: Iterable[Entity] = ()): self.width, self.height = width, height - self.entities = entities + self.entities = set(entities) self.tiles = np.full((width, height), fill_value=tile_types.wall, order="F") 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 get_blocking_entity_at_location(self, location_x: int, location_y: int) -> Optional[Entity]: + for entity in self.entities: + if entity.blocks_movement and entity.x == location_x and entity.y == location_y: + return entity + + return None + def in_bounds(self, x: int, y: int) -> bool: """Return True if x and y are inside the bounds of the map.""" return 0 <= x < self.width and 0 <= y < self.height diff --git a/input_handlers.py b/input_handlers.py index 7fe6682..d33e672 100644 --- a/input_handlers.py +++ b/input_handlers.py @@ -2,7 +2,7 @@ from typing import Optional import tcod.event -from actions import Action, EscapeAction, MovementAction +from actions import Action, EscapeAction, BumpAction class EventHandler(tcod.event.EventDispatch[Action]): @@ -15,13 +15,13 @@ class EventHandler(tcod.event.EventDispatch[Action]): key = event.sym if key == tcod.event.K_UP: - action = MovementAction(dx=0, dy=-1) + action = BumpAction(dx=0, dy=-1) elif key == tcod.event.K_DOWN: - action = MovementAction(dx=0, dy=1) + action = BumpAction(dx=0, dy=1) elif key == tcod.event.K_LEFT: - action = MovementAction(dx=-1, dy=0) + action = BumpAction(dx=-1, dy=0) elif key == tcod.event.K_RIGHT: - action = MovementAction(dx=1, dy=0) + action = BumpAction(dx=1, dy=0) elif key == tcod.event.K_ESCAPE: action = EscapeAction() diff --git a/main.py b/main.py index f69545e..0b12831 100755 --- a/main.py +++ b/main.py @@ -1,8 +1,10 @@ #!/usr/bin/env python3 +import copy + import tcod from engine import Engine -from entity import Entity +import entity_factories from input_handlers import EventHandler from procgen import generate_dungeon @@ -18,6 +20,8 @@ def main() -> None: room_min_size = 6 max_rooms = 30 + max_monsters_per_room = 2 + tileset = tcod.tileset.load_tilesheet( "dejavu10x10_gs_tc.png", 32, @@ -27,7 +31,7 @@ def main() -> None: event_handler = EventHandler() - player = Entity(int(screen_width / 2), int(screen_height / 2), "@", (255, 255, 255)) + player = copy.deepcopy(entity_factories.player) game_map = generate_dungeon( max_rooms, @@ -35,6 +39,7 @@ def main() -> None: room_max_size, map_width, map_height, + max_monsters_per_room, player, ) diff --git a/procgen.py b/procgen.py index c746e89..d2343af 100644 --- a/procgen.py +++ b/procgen.py @@ -5,6 +5,7 @@ from typing import Iterator, List, Tuple, TYPE_CHECKING import tcod +import entity_factories from game_map import GameMap import tile_types @@ -41,6 +42,24 @@ class RectangularRoom: ) +def place_entities( + room: RectangularRoom, + dungeon: GameMap, + maximum_monsters: int, +) -> None: + number_of_monsters = random.randint(0, maximum_monsters) + + for i in range(number_of_monsters): + x = random.randint(room.x1 + 1, room.x2 - 1) + y = random.randint(room.y1 + 1, room.y2 - 1) + + if not any(entity.x == x and entity.y == y for entity in dungeon.entities): + if random.random() < 0.8: + entity_factories.orc.spawn(dungeon, x, y) + else: + entity_factories.troll.spawn(dungeon, x, y) + + def tunnel_between(start: Tuple[int, int], end: Tuple[int, int]) -> Iterator[Tuple[int, int]]: """Return an L-shaped tunnel between these two points.""" x1, y1 = start @@ -66,6 +85,7 @@ def generate_dungeon( room_max_size: int, map_width: int, map_height: int, + max_monsters_per_room: int, player: Entity, ) -> GameMap: """Generate a new dungeon map.""" @@ -99,6 +119,8 @@ def generate_dungeon( for x, y in tunnel_between(rooms[-1].center, new_room.center): dungeon.tiles[x, y] = tile_types.floor + place_entities(new_room, dungeon, max_monsters_per_room) + # Finally, append the new room to the list. rooms.append(new_room)