Skip to content

Commit 705c8cb

Browse files
committed
parts 4 and 5 done!
1 parent 6be216e commit 705c8cb

File tree

9 files changed

+170
-31
lines changed

9 files changed

+170
-31
lines changed

actions.py

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,20 +19,31 @@ def perform(self, engine: Engine, entity: Entity) -> None:
1919
"""
2020
raise NotImplementedError()
2121

22+
class ActionWithDirection(Action):
23+
def __init__(self, dx: int, dy: int):
24+
super().__init__()
25+
26+
self.dx = dx
27+
self.dy = dy
2228

29+
def perform(self, engine: Engine, entity: Entity) -> None:
30+
raise NotImplementedError()
2331

2432
class EscapeAction(Action):
2533
def perform(self, engine: Engine, entity: Entity) -> None:
2634
raise SystemExit()
2735

36+
class MeleeAction(ActionWithDirection):
37+
def perform(self, engine: Engine, entity: Entity) -> None:
38+
dest_x = entity.x + self.dx
39+
dest_y = entity.y + self.dy
40+
target = engine.game_map.get_blocking_entity_at_location(dest_x, dest_y)
41+
if not target:
42+
return # No entity to attack.
2843

29-
class MovementAction(Action):
30-
def __init__(self, dx: int, dy: int):
31-
super().__init__()
32-
33-
self.dx = dx
34-
self.dy = dy
44+
print(f"You punch the {target.name}, it does not seem to be doing much damage...")
3545

46+
class MovementAction(ActionWithDirection):
3647
def perform(self, engine: Engine, entity: Entity) -> None:
3748
dest_x = entity.x + self.dx
3849
dest_y = entity.y + self.dy
@@ -41,5 +52,20 @@ def perform(self, engine: Engine, entity: Entity) -> None:
4152
return # Destination is out of bounds.
4253
if not engine.game_map.tiles["walkable"][dest_x, dest_y]:
4354
return # Destination is blocked by a tile.
55+
if engine.game_map.get_blocking_entity_at_location(dest_x, dest_y):
56+
return # Destination is blocked by an entity.
57+
58+
59+
entity.move(self.dx, self.dy)
60+
61+
62+
class BumpAction(ActionWithDirection):
63+
def perform(self, engine: Engine, entity: Entity) -> None:
64+
dest_x = entity.x + self.dx
65+
dest_y = entity.y + self.dy
66+
67+
if engine.game_map.get_blocking_entity_at_location(dest_x, dest_y):
68+
return MeleeAction(self.dx, self.dy).perform(engine, entity)
4469

45-
entity.move(self.dx, self.dy)
70+
else:
71+
return MovementAction(self.dx, self.dy).perform(engine, entity)

engine.py

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,25 @@
1-
from typing import Set, Iterable, Any
1+
from typing import Iterable, Any
22

33
from tcod.context import Context
44
from tcod.console import Console
5+
from tcod.map import compute_fov
6+
import tcod.constants
57

68
from entity import Entity
79
from game_map import GameMap
810
from input_handlers import EventHandler
911

1012

1113
class Engine:
12-
def __init__(self, entities: Set[Entity], event_handler: EventHandler, game_map: GameMap, player: Entity):
13-
self.entities = entities
14+
def __init__(self, event_handler: EventHandler, game_map: GameMap, player: Entity):
1415
self.event_handler = event_handler
1516
self.game_map = game_map
1617
self.player = player
18+
self.update_fov()
19+
20+
def handle_enemy_turns(self) -> None:
21+
for entity in self.game_map.entities - {self.player}:
22+
print(f'The {entity.name} wonders when it will get to take a real turn.')
1723

1824
def handle_events(self, events: Iterable[Any]) -> None:
1925
for event in events:
@@ -23,13 +29,25 @@ def handle_events(self, events: Iterable[Any]) -> None:
2329
continue
2430

2531
action.perform(self, self.player)
32+
self.handle_enemy_turns()
33+
self.update_fov() # Update the FOV before the players next action.
34+
35+
def update_fov(self) -> None:
36+
"""Recompute the visible area based on the players point of view."""
37+
self.game_map.visible[:] = compute_fov(
38+
self.game_map.tiles["transparent"],
39+
(self.player.x, self.player.y),
40+
radius=8,
41+
light_walls=True,
42+
algorithm=tcod.constants.FOV_SHADOW
43+
)
44+
# If a tile is "visible" it should be added to "explored".
45+
self.game_map.explored |= self.game_map.visible
46+
2647

2748
def render(self, console: Console, context: Context) -> None:
2849
self.game_map.render(console)
2950

30-
for entity in self.entities:
31-
console.print(entity.x, entity.y, entity.char, fg=entity.color)
32-
3351
context.present(console)
3452

3553
console.clear()

entity.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,43 @@
1-
from typing import Tuple
1+
from __future__ import annotations
22

3+
import copy
4+
from typing import Tuple, TypeVar, TYPE_CHECKING
5+
6+
if TYPE_CHECKING:
7+
from game_map import GameMap
8+
9+
T = TypeVar("T", bound="Entity")
310

411
class Entity:
512
"""
613
A generic object to represent players, enemies, items, etc.
714
"""
8-
def __init__(self, x: int, y: int, char: str, color: Tuple[int, int, int]):
15+
def __init__(
16+
self,
17+
x: int = 0,
18+
y: int = 0,
19+
char: str = "?",
20+
color: Tuple[int, int, int] = (255, 255, 255),
21+
name: str = "<Unnamed>",
22+
blocks_movement: bool = False,
23+
):
24+
"""
25+
Create a new entity with the given properties.
26+
"""
927
self.x = x
1028
self.y = y
1129
self.char = char
1230
self.color = color
31+
self.name = name
32+
self.blocks_movement = blocks_movement
33+
34+
def spawn(self: T, gamemap: GameMap, x: int, y: int) -> T:
35+
"""Spawn a copy of this instance at the given location."""
36+
clone = copy.deepcopy(self)
37+
clone.x = x
38+
clone.y = y
39+
gamemap.entities.add(clone)
40+
return clone
1341

1442
def move(self, dx: int, dy: int) -> None:
1543
"""

entity_factories.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from entity import Entity
2+
3+
player = Entity(char="@", color=(255, 255, 0), name="Player", blocks_movement=True)
4+
5+
smile_mold = Entity(char="m", color=(255, 80, 80), name="Slime Mold", blocks_movement=True)
6+
rusty_automaton = Entity(char="a", color=(200, 174, 137), name="Rusty Automaton", blocks_movement=True)

game_map.py

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,36 @@
1+
from __future__ import annotations
2+
3+
from typing import Iterable, Optional, TYPE_CHECKING
4+
15
import numpy as np # type: ignore
26
from tcod.console import Console
37

48
import tile_types
59

10+
if TYPE_CHECKING:
11+
from entity import Entity
612

713
class GameMap:
8-
def __init__(self, width: int, height: int):
14+
def __init__(self, width: int, height: int, entities: Iterable[Entity] = ()):
15+
self.entities = set(entities)
916
self.width, self.height = width, height
1017
self.tiles = np.full((width, height), fill_value=tile_types.wall, order="F")
1118

19+
self.visible = np.full((width, height), fill_value=False, order="F") # Tiles the player can currently see
20+
self.explored = np.full((width, height), fill_value=False, order="F") # Tiles the player has seen before
21+
1222
def in_bounds(self, x: int, y: int) -> bool:
1323
"""Return True if x and y are inside of the bounds of this map."""
1424
return 0 <= x < self.width and 0 <= y < self.height
25+
26+
def get_blocking_entity_at_location(self, location_x: int, location_y: int) -> Optional[Entity]:
27+
"""Return the blocking entity at a given location, if any."""
28+
for entity in self.entities:
29+
if entity.blocks_movement and entity.x == location_x and entity.y == location_y:
30+
return entity
31+
32+
return None
33+
1534
def set_tile(self, x: int, y: int, tile: str) -> None:
1635
# if the tile has autotile then set the autotile'
1736
self.tiles[x, y] = tile
@@ -103,6 +122,23 @@ def update_tile_at(self, x: int, y: int, tile: str) -> None:
103122

104123
# Set the first int if the "dark" nparray with the unicode of the ch
105124
tile["dark"]["ch"] = ord(ch)
125+
tile["light"]["ch"] = ord(ch)
106126

107127
def render(self, console: Console) -> None:
108-
console.rgb[0:self.width, 0:self.height] = self.tiles["dark"]
128+
"""
129+
Renders the map.
130+
131+
If a tile is in the "visible" array, then draw it with the "light" colors.
132+
If it isn't, but it's in the "explored" array, then draw it with the "dark" colors.
133+
Otherwise, the default is "SHROUD".
134+
"""
135+
console.tiles_rgb[0:self.width, 0:self.height] = np.select(
136+
condlist=[self.visible, self.explored],
137+
choicelist=[self.tiles["light"], self.tiles["dark"]],
138+
default=tile_types.SHROUD
139+
)
140+
141+
for entity in self.entities:
142+
# Only print entities that are in the FOV
143+
if self.visible[entity.x, entity.y]:
144+
console.print(x=entity.x, y=entity.y, string=entity.char, fg=entity.color)

input_handlers.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import tcod.event
44

5-
from actions import Action, EscapeAction, MovementAction
5+
from actions import Action, BumpAction, EscapeAction
66

77

88
class EventHandler(tcod.event.EventDispatch[Action]):
@@ -15,13 +15,13 @@ def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[Action]:
1515
key = event.sym
1616

1717
if key == tcod.event.KeySym.UP:
18-
action = MovementAction(dx=0, dy=-1)
18+
action = BumpAction(dx=0, dy=-1)
1919
elif key == tcod.event.KeySym.DOWN:
20-
action = MovementAction(dx=0, dy=1)
20+
action = BumpAction(dx=0, dy=1)
2121
elif key == tcod.event.KeySym.LEFT:
22-
action = MovementAction(dx=-1, dy=0)
22+
action = BumpAction(dx=-1, dy=0)
2323
elif key == tcod.event.KeySym.RIGHT:
24-
action = MovementAction(dx=1, dy=0)
24+
action = BumpAction(dx=1, dy=0)
2525

2626
elif key == tcod.event.KeySym.ESCAPE:
2727
action = EscapeAction()

main.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
#!/usr/bin/env python3
22
import tcod
3+
import copy
34

45
from engine import Engine
56
from entity import Entity
67
from procgen import generate_dungeon
78
from input_handlers import EventHandler
9+
import entity_factories
810

911
def main():
1012
screen_width = 80
@@ -17,24 +19,24 @@ def main():
1719
map_width = 80
1820
map_height = 45
1921

22+
max_monsters_per_room = 2
2023

2124
tileset = tcod.tileset.load_tilesheet("data/zaratustra_msx.png", 16, 16, tcod.tileset.CHARMAP_CP437)
2225

2326
event_handler = EventHandler()
2427

25-
player = Entity(int(screen_width / 2), int(screen_height / 2), "@", (255, 255, 0))
26-
entities = {player}
27-
28+
player = copy.deepcopy(entity_factories.player)
2829
game_map = generate_dungeon(
2930
max_rooms=max_rooms,
3031
room_min_size=room_min_size,
3132
room_max_size=room_max_size,
3233
map_width=map_width,
3334
map_height=map_height,
35+
max_monsters_per_room=max_monsters_per_room,
3436
player=player
3537
)
3638

37-
engine = Engine(entities=entities, event_handler=event_handler, game_map=game_map, player=player)
39+
engine = Engine(event_handler=event_handler, game_map=game_map, player=player)
3840

3941
with tcod.context.new_terminal(
4042
screen_width,

procgen.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import tile_types
77
import tcod
88

9+
import entity_factories
10+
911
if TYPE_CHECKING:
1012
from entity import Entity
1113

@@ -37,6 +39,20 @@ def intersects(self, other: RectangularRoom) -> bool:
3739
and self.y2 >= other.y1
3840
)
3941

42+
def place_entities(
43+
room: RectangularRoom, dungeon: GameMap, maximum_monsters: int,
44+
) -> None:
45+
number_of_monsters = random.randint(0, maximum_monsters)
46+
47+
for i in range(number_of_monsters):
48+
x = random.randint(room.x1 + 1, room.x2 - 1)
49+
y = random.randint(room.y1 + 1, room.y2 - 1)
50+
51+
if not any(entity.x == x and entity.y == y for entity in dungeon.entities):
52+
if random.random() < 0.8:
53+
entity_factories.smile_mold.spawn(dungeon, x, y)
54+
else:
55+
entity_factories.rusty_automaton.spawn(dungeon, x, y)
4056

4157
def tunnel_between(
4258
start: Tuple[int, int], end: Tuple[int, int]
@@ -64,10 +80,11 @@ def generate_dungeon(
6480
room_max_size: int,
6581
map_width: int,
6682
map_height: int,
83+
max_monsters_per_room: int,
6784
player: Entity,
6885
) -> GameMap:
6986
"""Generate a new dungeon map."""
70-
dungeon = GameMap(map_width, map_height)
87+
dungeon = GameMap(map_width, map_height, entities=[player])
7188

7289
rooms: List[RectangularRoom] = []
7390

@@ -99,6 +116,8 @@ def generate_dungeon(
99116
for x, y in tunnel_between(rooms[-1].center, new_room.center):
100117
dungeon.set_tile(x, y, tile_types.floor)
101118

119+
place_entities(new_room, dungeon, max_monsters_per_room)
120+
102121
# Finally, append the new room to the list.
103122
rooms.append(new_room)
104123

tile_types.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,25 +17,29 @@
1717
("walkable", bool), # True if this tile can be walked over.
1818
("transparent", bool), # True if this tile doesn't block FOV.
1919
("dark", graphic_dt), # Graphics for when this tile is not in FOV.
20-
("autotile", bool) # If the Tile will be set to this symbols to make boders: ─│┌┐└┘
20+
("light", graphic_dt), # Graphics for when the tile is in FOV.
21+
("autotile", bool), # If the Tile will be set to this symbols to make boders: ─│┌┐└┘
2122
]
2223
)
2324

25+
# SHROUD represents unexplored, unseen tiles
26+
SHROUD = np.array((ord(" "), (255, 255, 255), (0, 0, 0)), dtype=graphic_dt)
2427

2528
def new_tile(
2629
*, # Enforce the use of keywords, so that parameter order doesn't matter.
2730
walkable: int,
2831
transparent: int,
2932
dark: Tuple[int, Tuple[int, int, int], Tuple[int, int, int]],
33+
light: Tuple[int, Tuple[int, int, int], Tuple[int, int, int]],
3034
autotile: bool,
3135
) -> np.ndarray:
3236
"""Helper function for defining individual tile types """
33-
return np.array((walkable, transparent, dark, autotile), dtype=tile_dt)
37+
return np.array((walkable, transparent, dark, light, autotile), dtype=tile_dt)
3438

3539

3640
floor = new_tile(
37-
walkable=True, transparent=True, dark=(ord("."), (102, 0, 51), (0, 0, 0)), autotile=False
41+
walkable=True, transparent=True, light=(ord("."), (102, 0, 51), (0, 0, 0)), dark=(ord("."), (51, 51, 51), (0, 0, 0)), autotile=False
3842
)
3943
wall = new_tile(
40-
walkable=False, transparent=False, dark=(ord(" "), (153, 0, 153), (0,0,0)), autotile=True
44+
walkable=False, transparent=False, light=(ord(" "), (153, 0, 153), (0,0,0)), dark=(ord(" "), (51, 51, 51), (0, 0, 0)), autotile=True
4145
)

0 commit comments

Comments
 (0)