2 from plomrogue.errors import ArgError
6 class YX(collections.namedtuple('YX', ('y', 'x'))):
8 def __add__(self, other):
9 return YX(self.y + other.y, self.x + other.x)
11 def __sub__(self, other):
12 return YX(self.y - other.y, self.x - other.x)
15 return 'Y:%s,X:%s' % (self.y, self.x)
21 def __init__(self, size):
25 def get_directions(self):
27 for name in dir(self):
28 if name[:5] == 'move_':
29 directions += [name[5:]]
32 def get_neighbors(self, pos):
34 for direction in self.get_directions():
35 neighbors[direction] = self.move(pos, direction)
38 def get_neighbors_i(self, i):
39 if i in self.neighbors_i:
40 return self.neighbors_i[i]
41 pos = YX(i // self.size.x, i % self.size.x)
42 neighbors_pos = self.get_neighbors(pos)
44 for direction in neighbors_pos:
45 pos = neighbors_pos[direction]
47 neighbors_i[direction] = None
49 neighbors_i[direction] = pos.y * self.size.x + pos.x
50 self.neighbors_i[i] = neighbors_i
51 return self.neighbors_i[i]
53 def move(self, start_pos, direction):
54 mover = getattr(self, 'move_' + direction)
55 target = mover(start_pos)
56 if target.y < 0 or target.x < 0 or \
57 target.y >= self.size.y or target.x >= self.size.x:
63 class MapGeometryWithLeftRightMoves(MapGeometry):
65 def move_LEFT(self, start_pos):
66 return YX(start_pos.y, start_pos.x - 1)
68 def move_RIGHT(self, start_pos):
69 return YX(start_pos.y, start_pos.x + 1)
73 class MapGeometrySquare(MapGeometryWithLeftRightMoves):
75 def __init__(self, *args, **kwargs):
76 super().__init__(*args, **kwargs)
77 self.fov_map_class = FovMapSquare
78 self.dijkstra_map_class = DijkstraMapSquare
80 def define_segment(self, source_center, radius):
81 size = YX(2 * radius + 1, 2 * radius + 1)
82 offset = YX(source_center.y - radius, source_center.x - radius)
83 center = YX(radius, radius)
84 return size, offset, center
86 def move_UP(self, start_pos):
87 return YX(start_pos.y - 1, start_pos.x)
89 def move_DOWN(self, start_pos):
90 return YX(start_pos.y + 1, start_pos.x)
93 class MapGeometryHex(MapGeometryWithLeftRightMoves):
95 def __init__(self, *args, **kwargs):
96 super().__init__(*args, **kwargs)
97 self.fov_map_class = FovMapHex
98 self.dijkstra_map_class = DijkstraMapHex
100 def define_segment(self, source_center, radius):
101 indent = 1 if (source_center.y % 2) else 0
102 size = YX(2 * radius + 1 + indent, 2 * radius + 1)
103 offset = YX(source_center.y - radius - indent, source_center.x - radius)
104 center = YX(radius + indent, radius)
105 return size, offset, center
107 def move_UPLEFT(self, start_pos):
108 start_indented = start_pos.y % 2
110 return YX(start_pos.y - 1, start_pos.x)
112 return YX(start_pos.y - 1, start_pos.x - 1)
114 def move_UPRIGHT(self, start_pos):
115 start_indented = start_pos.y % 2
117 return YX(start_pos.y - 1, start_pos.x + 1)
119 return YX(start_pos.y - 1, start_pos.x)
121 def move_DOWNLEFT(self, start_pos):
122 start_indented = start_pos.y % 2
124 return YX(start_pos.y + 1, start_pos.x)
126 return YX(start_pos.y + 1, start_pos.x - 1)
128 def move_DOWNRIGHT(self, start_pos):
129 start_indented = start_pos.y % 2
131 return YX(start_pos.y + 1, start_pos.x + 1)
133 return YX(start_pos.y + 1, start_pos.x)
139 def __init__(self, map_size):
141 self.terrain = '.' * self.size_i
143 def __getitem__(self, yx):
144 return self.terrain[self.get_position_index(yx)]
146 def __setitem__(self, yx, c):
147 pos_i = self.get_position_index(yx)
149 self.terrain = self.terrain[:pos_i] + c + self.terrain[pos_i + 1:]
151 self.terrain[pos_i] = c
154 """Iterate over YX position coordinates."""
155 for y in range(self.size.y):
156 for x in range(self.size.x):
159 # TODO: use this for more refactoring
160 def inside(self, yx):
161 if yx.y < 0 or yx.x < 0 or yx.y >= self.size.y or yx.x >= self.size.x:
167 return self.size.y * self.size.x
169 def set_line(self, y, line):
170 height_map = self.size.y
171 width_map = self.size.x
173 raise ArgError('too large row number %s' % y)
174 width_line = len(line)
175 if width_line != width_map:
176 raise ArgError('map line width %s unequal map width %s' % (width_line, width_map))
177 self.terrain = self.terrain[:y * width_map] + line +\
178 self.terrain[(y + 1) * width_map:]
180 def get_position_index(self, yx):
181 return yx.y * self.size.x + yx.x
185 for y in range(self.size.y):
186 yield (y, self.terrain[y * width:(y + 1) * width])
190 class SourcedMap(Map):
192 def __init__(self, source_map, source_center, radius):
193 self.source_map = source_map
195 self.size, self.offset, self.center = \
196 self.geometry_class.define_segment(None, source_center, radius)
197 self.geometry = self.geometry_class(self.size)
199 def source_yx(self, yx, check=False):
200 source_yx = yx + self.offset
201 if check and not self.source_map.inside(source_yx):
205 def target_yx(self, yx, check=False):
206 target_yx = yx - self.offset
207 if check and not self.inside(target_yx):
213 class DijkstraMap(SourcedMap):
215 def __init__(self, *args, **kwargs):
216 super().__init__(*args, **kwargs)
217 self.terrain = [255] * self.size_i
218 self[self.center] = 0
220 source_map_segment = ''
222 yx_in_source = self.source_yx(yx, True)
224 source_map_segment += self.source_map[yx_in_source]
226 source_map_segment += 'X'
229 for i in range(self.size_i):
230 if source_map_segment[i] == 'X':
232 neighbors = self.geometry.get_neighbors_i(i)
233 for direction in [d for d in neighbors if neighbors[d]]:
234 j = neighbors[direction]
235 if self.terrain[j] < self.terrain[i] - 1:
236 self.terrain[i] = self.terrain[j] + 1
238 #print('DEBUG Dijkstra')
241 #for n in self.terrain:
242 # line_to_print += ['%3s' % n]
244 # if x >= self.size.x:
246 # print(' '.join(line_to_print))
251 class DijkstraMapHex(DijkstraMap):
252 geometry_class = MapGeometryHex
256 class DijkstraMapSquare(DijkstraMap):
257 geometry_class = MapGeometrySquare
261 class FovMap(SourcedMap):
262 # TODO: player visibility asymmetrical (A can see B when B can't see A):
263 # does this make sense, or not?
265 def __init__(self, *args, **kwargs):
266 super().__init__(*args, **kwargs)
267 self.terrain = '?' * self.size.y * self.size.x
268 self[self.center] = '.'
269 self.shadow_cones = []
270 self.circle_out(self.center, self.shadow_process)
272 def throws_shadow(self, source_yx):
273 return self.source_map[source_yx] == 'X'
275 def shadow_process(self, yx, source_yx, distance_to_center, dir_i, dir_progress):
276 # Possible optimization: If no shadow_cones yet and self[yx] == '.',
278 CIRCLE = 360 # Since we'll float anyways, number is actually arbitrary.
280 def correct_arm(arm):
285 def in_shadow_cone(new_cone):
286 for old_cone in self.shadow_cones:
287 if old_cone[0] <= new_cone[0] and \
288 new_cone[1] <= old_cone[1]:
290 # We might want to also shade tiles whose middle arm is inside a
291 # shadow cone for a darker FOV. Note that we then could not for
292 # optimization purposes rely anymore on the assumption that a
293 # shaded tile cannot add growth to existing shadow cones.
296 def merge_cone(new_cone):
298 for old_cone in self.shadow_cones:
299 if new_cone[0] < old_cone[0] and \
300 (new_cone[1] > old_cone[0] or
301 math.isclose(new_cone[1], old_cone[0])):
302 old_cone[0] = new_cone[0]
304 if new_cone[1] > old_cone[1] and \
305 (new_cone[0] < old_cone[1] or
306 math.isclose(new_cone[0], old_cone[1])):
307 old_cone[1] = new_cone[1]
312 if in_shadow_cone(cone):
315 if self.throws_shadow(source_yx):
317 while merge_cone(cone):
320 self.shadow_cones += [cone]
322 step_size = (CIRCLE/len(self.circle_out_directions)) / distance_to_center
323 number_steps = dir_i * distance_to_center + dir_progress
324 left_arm = correct_arm(step_size/2 + step_size*number_steps)
325 right_arm = correct_arm(left_arm + step_size)
327 # Optimization potential: left cone could be derived from previous
328 # right cone. Better even: Precalculate all cones.
329 if right_arm < left_arm:
330 eval_cone([left_arm, CIRCLE])
331 eval_cone([0, right_arm])
333 eval_cone([left_arm, right_arm])
335 def basic_circle_out_move(self, pos, direction):
336 #"""Move position pos into direction. Return whether still in map."""
337 mover = getattr(self.geometry, 'move_' + direction)
340 def circle_out(self, yx, f):
341 # Optimization potential: Precalculate movement positions. (How to check
342 # circle_in_map then?)
343 # Optimization potential: Precalculate what tiles are shaded by what tile
344 # and skip evaluation of already shaded tile. (This only works if tiles
345 # shading implies they completely lie in existing shades; otherwise we
346 # would lose shade growth through tiles at shade borders.)
350 while distance <= self.radius:
351 yx = self.basic_circle_out_move(yx, 'RIGHT')
352 for dir_i in range(len(self.circle_out_directions)):
353 for dir_progress in range(distance):
354 direction = self.circle_out_directions[dir_i]
355 yx = self.circle_out_move(yx, direction)
356 source_yx = self.source_yx(yx, True)
358 f(yx, source_yx, distance, dir_i, dir_progress)
363 class FovMapHex(FovMap):
364 circle_out_directions = ('DOWNLEFT', 'LEFT', 'UPLEFT',
365 'UPRIGHT', 'RIGHT', 'DOWNRIGHT')
366 geometry_class = MapGeometryHex
368 def circle_out_move(self, yx, direction):
369 return self.basic_circle_out_move(yx, direction)
373 class FovMapSquare(FovMap):
374 circle_out_directions = (('DOWN', 'LEFT'), ('LEFT', 'UP'),
375 ('UP', 'RIGHT'), ('RIGHT', 'DOWN'))
376 geometry_class = MapGeometrySquare
378 def circle_out_move(self, yx, direction):
379 yx = self.basic_circle_out_move(yx, direction[0])
380 return self.basic_circle_out_move(yx, direction[1])