-
Notifications
You must be signed in to change notification settings - Fork 0
/
level.py
329 lines (269 loc) · 11.7 KB
/
level.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from random import randint, choice
from tdl.map import Map
from entities import StairsUp, StairsDown
from misc import Vector
class Room:
"""
Represents a rectangle of walkable space.
Args:
x (int): x coordinate of the top-left corner of the room.
y (int): y coordinate of the top-left corner of the room.
width (int): Width of the room to be created.
height (int): Height of the room to be created.
"""
def __init__(self, x, y, width, height):
self.x1 = x
self.y1 = y
self.x2 = x + width
self.y2 = y + height
def center(self):
"""
Get the center of this room object as a Vector.
Returns:
Vector: The center of this room object.
"""
center_x = int((self.x1 + self.x2) / 2)
center_y = int((self.y1 + self.y2) / 2)
return Vector(center_x, center_y)
def intersect(self, other):
"""
Get whether or not this room intersects with another room.
Args:
other (Room): Room to check for intersections with.
Returns:
True if self and other intersect, false otherwise.
"""
return (self.x1 <= other.x2 and self.x2 >= other.x1 and
self.y1 <= other.y2 and self.y2 >= other.y1)
class Tilemap:
"""
A wrapper to access numpy array elements using vectors.
Args:
array (list): This can actually be either a native matrix or
a numpy matrix, depending on which one is passed-in the logic will
be different, but the result will be the same.
"""
def __init__(self, array):
# Check if it's a numpy matrix or a normal matrix
if isinstance(array, list):
self.is_numpy_array = False
else:
# Assume if it's not a list then it's a numpy array so that we don't
# have to import numpy and check the type explicitly
self.is_numpy_array = True
self._array = array
def __getitem__(self, pos):
if self.is_numpy_array:
return self._array[pos.x, pos.y]
else:
return self._array[pos.x][pos.y]
def __setitem__(self, pos, val):
if self.is_numpy_array:
self._array[pos.x, pos.y] = val
else:
self._array[pos.x][pos.y] = val
class Level:
"""
Represents a level in the dungeon.
A level is a place that the player explores, it is filled with enemies,
items, npcs, secrets. It consists of a series of Room objects interconnected
with narrow corridors. It also has stairs connecting to other levels of the dungeon.
After initialization, the level is "unexplored", i.e. the player can't
see the entire level upon spawning. Once the player moves around and
explores the level, tiles will be revealed and will be "remembered".
Args:
width (int): Max width of the level to be generated.
height (int): Max height of the level to be generated.
room_max_count (int): Max amount of rooms to be generated for this
particular level.
room_min_size (int): Min amount of tiles per room.
room_max_size (int): Max amount of tiles per room.
max_entities_per_room (int): Max amount of entities to be spawned
per room.
registry (Registry): Reference to the game's registry.
"""
def __init__(self, width, height, room_max_count, room_min_size, room_max_size, max_entities_per_room, dungeon,
registry):
# TODO: Instead of using so many variables, use a context which contains them all and depends on the theme
self._map = Map(width, height)
self.width = width
self.height = height
self.explored = Tilemap([[False for y in range(height)] for x in range(width)])
self.rooms = []
self.entities = []
# Add tilemaps for some Map arrays
self.walkable = Tilemap(self._map.walkable)
self.transparent = Tilemap(self._map.transparent)
self.fov = Tilemap(self._map.fov)
# Up and down stairs. These get updated when the level is generated.
self.up_stairs = StairsUp(dungeon)
self.down_stairs = StairsDown(dungeon)
# Get info relative to the level generation
self.room_max_count = room_max_count
self.room_min_size = room_min_size
self.room_max_size = room_max_size
self.max_entities_per_room = max_entities_per_room
# Generate the level
self.generate()
# Throw some monsters and items in it
# TODO: Instead of passing the registry, use a context/theme
self.populate(registry)
def _init_room(self, room):
"""Make the tiles in the map that correspond to the room walkable."""
for x in range(room.x1 + 1, room.x2):
for y in range(room.y1 + 1, room.y2):
pos = Vector(x, y)
self.walkable[pos] = True
self.transparent[pos] = True
def _create_h_tunnel(self, x1, x2, y):
"""Create an horizontal tunnel from x1 to x2 at a fixed y."""
for x in range(min(x1, x2), max(x1, x2) + 1):
pos = Vector(x, y)
self.walkable[pos] = True
self.transparent[pos] = True
def _create_v_tunnel(self, y1, y2, x):
"""Create a vertical tunnel from y1 to y2 at a fixed x."""
for y in range(min(y1, y2), max(y1, y2) + 1):
pos = Vector(x, y)
self.walkable[pos] = True
self.transparent[pos] = True
def generate(self):
"""
Generate the level's layout.
"""
# Initialize map
for x in range(self.width):
for y in range(self.height):
pos = Vector(x, y)
self.walkable[pos] = False
self.transparent[pos] = False
for r in range(self.room_max_count):
# Random width and height
w = randint(self.room_min_size, self.room_max_size)
h = randint(self.room_min_size, self.room_max_size)
# Random position without going out of the boundaries of the map
x = randint(0, self.width - w - 1)
y = randint(0, self.height - h - 1)
new_room = Room(x, y, w, h)
# If it intersects with an existent room, start over
for room in self.rooms:
if new_room.intersect(room):
break
else:
# Room is valid
self._init_room(new_room)
center = new_room.center()
if not self.rooms:
# First room, place up stairs
self.up_stairs.place(self, center)
else:
# Connect room to previous room
previous = self.rooms[-1].center()
# Flip a coin
if randint(0, 1) == 1:
# First move horizontally, then vertically
self._create_h_tunnel(previous.x, center.x, previous.y)
self._create_v_tunnel(previous.y, center.y, center.x)
else:
# First move vertically, then horizontally
self._create_v_tunnel(previous.y, center.y, previous.x)
self._create_h_tunnel(previous.x, center.x, center.y)
self.rooms.append(new_room)
# Place down stairs
random_room = choice(self.rooms)
self.place_entity_randomly(self.down_stairs, random_room)
def populate(self, registry):
"""
Populate the level's rooms with entities.
"""
for room in self.rooms:
self._place_entities(room, registry)
def compute_fov(self, pos, fov, radius, light_walls):
"""
Compute a FOV field from the passed-in position.
The FOV field represents how many tiles ahead can a certain actor see,
this can affect behavior such as following, attacking, etc.
Args:
pos (Vector): The position vector from which the FOV should be
calculated, usually this position is occuppied by an Actor.
fov (str): Type of FOV algorithm to be used, usually supplied by
tdl itself.
radius (int): How far away should the actor be able to see.
light_walls (bool): Whether or not walls within the FOV of the
actor should be lit up or not.
"""
self._map.compute_fov(pos.x, pos.y, fov=fov, radius=radius, light_walls=light_walls)
def compute_path(self, pos1, pos2):
"""
Calculate a path between pos1 and pos2 in the game map.
Args:
pos1 (Vector): Vector representing the starting point.
pos2 (Vector): Vector representing the destination.
Returns:
list(Vector): A list of vectors, where each vector represents the position of the next tile in the path.
The list goes up to the pos2.
"""
# Get current walkable state
pos1_walkable = self.walkable[pos1]
pos2_walkable = self.walkable[pos2]
# Set origin and destination to walkable for the actual computation to work
self.walkable[pos1] = self.walkable[pos2] = True
# Compute path
path = self._map.compute_path(pos1.x, pos1.y, pos2.x, pos2.y)
# Convert path to vectors
path = [Vector(x, y) for x, y in [tile for tile in path]]
# Revert origin and destination walkable state to its original state
self.walkable[pos1] = pos1_walkable
self.walkable[pos2] = pos2_walkable
return path
def _place_entities(self, room, registry):
"""
Spawn and place entities in a single room.
Args:
room (Room): Room in which to spawn the entities.
registry (Registry): Reference to the game's registry.
"""
from registry import Actors, Items
entity_number = randint(0, self.max_entities_per_room)
for _ in range(entity_number):
dice = randint(0, 2)
if dice == 0:
ent = registry.get_actor(Actors.ORC)
elif dice == 1:
ent = registry.get_actor(Actors.POOPY)
else:
ent = registry.get_item(Items.CANDY)
self.place_entity_randomly(ent, room)
def get_blocking_entity_at_location(self, pos):
"""
Check if there's a blocking entity at the specified location.
A blocking entity is one that turns the tile in which it resides into
non-walkable, effectively blocking any other entities from accessing
and or placing themselves into that same tile.
Args:
pos (Vector): Vector of the position to check for blocking entities.
Returns:
Entity: The blocking entity if any, None otherwise.
"""
for entity in self.entities:
if entity.pos == pos and entity.blocks:
return entity
return None
def place_entity_randomly(self, entity, room, allow_overlap=False):
"""
Places the given entity randomly in a given room.
"""
# For now, naively assume that there's a free spot
while True:
# Get a random position inside the room
x = randint(room.x1 + 1, room.x2 - 1)
y = randint(room.y1 + 1, room.y2 - 1)
position = Vector(x, y)
# Check if there's already an entity in this spot
if not allow_overlap and [entity for entity in self.entities if entity.pos == position]:
continue
# We found a valid spot, place the entity
entity.place(self, position)
return