home · contact · privacy
Use math.isclose for square map FOVs too.
[plomrogue2-experiments] / server_ / map_.py
1 import sys
2 sys.path.append('../')
3 import game_common
4 import server_.game
5 import math
6
7
8 class Map(game_common.Map):
9
10     def __getitem__(self, yx):
11         return self.terrain[self.get_position_index(yx)]
12
13     def __setitem__(self, yx, c):
14         pos_i = self.get_position_index(yx)
15         self.terrain = self.terrain[:pos_i] + c + self.terrain[pos_i + 1:]
16
17     def __iter__(self):
18         """Iterate over YX position coordinates."""
19         for y in range(self.size[0]):
20             for x in range(self.size[1]):
21                 yield [y, x]
22
23     @property
24     def geometry(self):
25         return self.__class__.__name__[3:]
26
27     def lines(self):
28         width = self.size[1]
29         for y in range(self.size[0]):
30             yield (y, self.terrain[y * width:(y + 1) * width])
31
32     def get_fov_map(self, yx):
33         # TODO: Currently only have MapFovHex. Provide MapFovSquare.
34         fov_map_class = map_manager.get_map_class('Fov' + self.geometry)
35         return fov_map_class(self, yx)
36
37     # The following is used nowhere, so not implemented.
38     #def items(self):
39     #    for y in range(self.size[0]):
40     #        for x in range(self.size[1]):
41     #            yield ([y, x], self.terrain[self.get_position_index([y, x])])
42
43     def get_directions(self):
44         directions = []
45         for name in dir(self):
46             if name[:5] == 'move_':
47                 directions += [name[5:]]
48         return directions
49
50     def new_from_shape(self, init_char):
51         import copy
52         new_map = copy.deepcopy(self)
53         for pos in new_map:
54             new_map[pos] = init_char
55         return new_map
56
57     def move(self, start_pos, direction):
58         mover = getattr(self, 'move_' + direction)
59         new_pos = mover(start_pos)
60         if new_pos[0] < 0 or new_pos[1] < 0 or \
61                 new_pos[0] >= self.size[0] or new_pos[1] >= self.size[1]:
62             raise server_.game.GameError('would move outside map bounds')
63         return new_pos
64
65     def move_LEFT(self, start_pos):
66         return [start_pos[0], start_pos[1] - 1]
67
68     def move_RIGHT(self, start_pos):
69         return [start_pos[0], start_pos[1] + 1]
70
71
72 class MapHex(Map):
73
74     # The following is used nowhere, so not implemented.
75     #def are_neighbors(self, pos_1, pos_2):
76     #    if pos_1[0] == pos_2[0] and abs(pos_1[1] - pos_2[1]) <= 1:
77     #        return True
78     #    elif abs(pos_1[0] - pos_2[0]) == 1:
79     #        if pos_1[0] % 2 == 0:
80     #            if pos_2[1] in (pos_1[1], pos_1[1] - 1):
81     #                return True
82     #        elif pos_2[1] in (pos_1[1], pos_1[1] + 1):
83     #            return True
84     #    return False
85
86     def move_UPLEFT(self, start_pos):
87         if start_pos[0] % 2 == 1:
88             return [start_pos[0] - 1, start_pos[1] - 1]
89         else:
90             return [start_pos[0] - 1, start_pos[1]]
91
92     def move_UPRIGHT(self, start_pos):
93         if start_pos[0] % 2 == 1:
94             return [start_pos[0] - 1, start_pos[1]]
95         else:
96             return [start_pos[0] - 1, start_pos[1] + 1]
97
98     def move_DOWNLEFT(self, start_pos):
99         if start_pos[0] % 2 == 1:
100             return [start_pos[0] + 1, start_pos[1] - 1]
101         else:
102             return [start_pos[0] + 1, start_pos[1]]
103
104     def move_DOWNRIGHT(self, start_pos):
105         if start_pos[0] % 2 == 1:
106             return [start_pos[0] + 1, start_pos[1]]
107         else:
108             return [start_pos[0] + 1, start_pos[1] + 1]
109
110
111 class MapFovHex(MapHex):
112
113     def __init__(self, source_map, yx):
114         self.source_map = source_map
115         self.size = self.source_map.size
116         self.terrain = '?' * self.size_i
117         self[yx] = '.'
118         self.shadow_cones = []
119         self.circle_out(yx, self.shadow_process_hex)
120
121     def shadow_process_hex(self, yx, distance_to_center, dir_i, dir_progress):
122         # Possible optimization: If no shadow_cones yet and self[yx] == '.',
123         # skip all.
124         CIRCLE = 360  # Since we'll float anyways, number is actually arbitrary.
125
126         def correct_arm(arm):
127             if arm < 0:
128                 arm += CIRCLE
129             return arm
130
131         def in_shadow_cone(new_cone):
132             for old_cone in self.shadow_cones:
133                 if old_cone[0] >= new_cone[0] and \
134                     new_cone[1] >= old_cone[1]:
135                     #print('DEBUG shadowed by:', old_cone)
136                     return True
137                 # We might want to also shade hexes whose middle arm is inside a
138                 # shadow cone for a darker FOV. Note that we then could not for
139                 # optimization purposes rely anymore on the assumption that a
140                 # shaded hex cannot add growth to existing shadow cones.
141             return False
142
143         def merge_cone(new_cone):
144             for old_cone in self.shadow_cones:
145                 if new_cone[0] > old_cone[0] and \
146                     (new_cone[1] < old_cone[0] or
147                      math.isclose(new_cone[1], old_cone[0])):
148                     #print('DEBUG merging to', old_cone)
149                     old_cone[0] = new_cone[0]
150                     #print('DEBUG merged cone:', old_cone)
151                     return True
152                 if new_cone[1] < old_cone[1] and \
153                     (new_cone[0] > old_cone[1] or
154                      math.isclose(new_cone[0], old_cone[1])):
155                     #print('DEBUG merging to', old_cone)
156                     old_cone[1] = new_cone[1]
157                     #print('DEBUG merged cone:', old_cone)
158                     return True
159             return False
160
161         def eval_cone(cone):
162             #print('DEBUG CONE', cone, '(', step_size, distance_to_center, number_steps, ')')
163             if in_shadow_cone(cone):
164                 return
165             self[yx] = '.'
166             if self.source_map[yx] != '.':
167                 #print('DEBUG throws shadow', cone)
168                 unmerged = True
169                 while merge_cone(cone):
170                     unmerged = False
171                 if unmerged:
172                     self.shadow_cones += [cone]
173
174         #print('DEBUG', yx)
175         step_size = (CIRCLE/6) / distance_to_center
176         number_steps = dir_i * distance_to_center + dir_progress
177         left_arm = correct_arm(-(step_size/2) - step_size*number_steps)
178         right_arm = correct_arm(left_arm - step_size)
179         # Optimization potential: left cone could be derived from previous
180         # right cone. Better even: Precalculate all cones.
181         if right_arm > left_arm:
182             eval_cone([left_arm, 0])
183             eval_cone([CIRCLE, right_arm])
184         else:
185             eval_cone([left_arm, right_arm])
186
187     def circle_out(self, yx, f):
188         # Optimization potential: Precalculate movement positions. (How to check
189         # circle_in_map then?)
190         # Optimization potential: Precalculate what hexes are shaded by what hex
191         # and skip evaluation of already shaded hexes. (This only works if hex
192         # shading implies they completely lie in existing shades; otherwise we
193         # would lose shade growth through hexes at shade borders.)
194
195         def move(pos, direction):
196             """Move position pos into direction. Return whether still in map."""
197             mover = getattr(self, 'move_' + direction)
198             pos[:] = mover(pos)
199             if pos[0] < 0 or pos[1] < 0 or \
200                pos[0] >= self.size[0] or pos[1] >= self.size[1]:
201                 return False
202             return True
203
204         # TODO: Start circling only in earliest obstacle distance.
205         directions = ('DOWNLEFT', 'LEFT', 'UPLEFT', 'UPRIGHT', 'RIGHT', 'DOWNRIGHT')
206         circle_in_map = True
207         distance = 1
208         yx = yx[:]
209         #print('DEBUG CIRCLE_OUT', yx)
210         while circle_in_map:
211             circle_in_map = False
212             move(yx, 'RIGHT')
213             for dir_i in range(len(directions)):
214                 for dir_progress in range(distance):
215                     direction = directions[dir_i]
216                     if move(yx, direction):
217                         f(yx, distance, dir_i, dir_progress)
218                         circle_in_map = True
219             distance += 1
220
221
222 class MapSquare(Map):
223
224     # The following is used nowhere, so not implemented.
225     #def are_neighbors(self, pos_1, pos_2):
226     #    return abs(pos_1[0] - pos_2[0]) <= 1 and abs(pos_1[1] - pos_2[1] <= 1)
227
228     def move_UP(self, start_pos):
229         return [start_pos[0] - 1, start_pos[1]]
230
231     def move_DOWN(self, start_pos):
232         return [start_pos[0] + 1, start_pos[1]]
233
234
235 class MapFovSquare(MapSquare):
236     """Just a marginally and unsatisfyingly adapted variant of MapFovHex."""
237
238     def __init__(self, source_map, yx):
239         self.source_map = source_map
240         self.size = self.source_map.size
241         self.terrain = '?' * self.size_i
242         self[yx] = '.'
243         self.shadow_cones = []
244         self.circle_out(yx, self.shadow_process_hex)
245
246     def shadow_process_hex(self, yx, distance_to_center, dir_i, dir_progress):
247         CIRCLE = 360  # Since we'll float anyways, number is actually arbitrary.
248
249         def correct_arm(arm):
250             if arm < 0:
251                 arm += CIRCLE
252             return arm
253
254         def in_shadow_cone(new_cone):
255             for old_cone in self.shadow_cones:
256                 if old_cone[0] >= new_cone[0] and \
257                     new_cone[1] >= old_cone[1]:
258                     #print('DEBUG shadowed by:', old_cone)
259                     return True
260             return False
261
262         def merge_cone(new_cone):
263             for old_cone in self.shadow_cones:
264                 if new_cone[0] > old_cone[0] and \
265                     (new_cone[1] < old_cone[0] or
266                      math.isclose(new_cone[1], old_cone[0])):
267                     #print('DEBUG merging to', old_cone)
268                     old_cone[0] = new_cone[0]
269                     #print('DEBUG merged cone:', old_cone)
270                     return True
271                 if new_cone[1] < old_cone[1] and \
272                     (new_cone[0] > old_cone[1] or
273                      math.isclose(new_cone[0], old_cone[1])):
274                     #print('DEBUG merging to', old_cone)
275                     old_cone[1] = new_cone[1]
276                     #print('DEBUG merged cone:', old_cone)
277                     return True
278             return False
279
280         def eval_cone(cone):
281             new_cone = [left_arm, right_arm]
282             #print('DEBUG CONE', cone, '(', step_size, distance_to_center, number_steps, ')')
283             if in_shadow_cone(cone):
284                 return
285             self[yx] = '.'
286             if self.source_map[yx] != '.':
287                 #print('DEBUG throws shadow', cone)
288                 unmerged = True
289                 while merge_cone(cone):
290                     unmerged = False
291                 if unmerged:
292                     self.shadow_cones += [cone]
293
294         #print('DEBUG', yx)
295         step_size = (CIRCLE/4) / distance_to_center
296         number_steps = dir_i * distance_to_center + dir_progress
297         left_arm = correct_arm(-(step_size/2) - step_size*number_steps)
298         right_arm = correct_arm(left_arm - step_size)
299         if right_arm > left_arm:
300             eval_cone([left_arm, 0])
301             eval_cone([CIRCLE, right_arm])
302         else:
303             eval_cone([left_arm, right_arm])
304
305     def circle_out(self, yx, f):
306
307         def move(pos, direction):
308             """Move position pos into direction. Return whether still in map."""
309             mover = getattr(self, 'move_' + direction)
310             pos[:] = mover(pos)
311             if pos[0] < 0 or pos[1] < 0 or \
312                pos[0] >= self.size[0] or pos[1] >= self.size[1]:
313                 return False
314             return True
315
316         directions = (('DOWN', 'LEFT'), ('LEFT', 'UP'),
317                       ('UP', 'RIGHT'), ('RIGHT', 'DOWN'))
318         circle_in_map = True
319         distance = 1
320         yx = yx[:]
321         #print('DEBUG CIRCLE_OUT', yx)
322         while circle_in_map:
323             circle_in_map = False
324             move(yx, 'RIGHT')
325             for dir_i in range(len(directions)):
326                 for dir_progress in range(distance):
327                     direction = directions[dir_i]
328                     move(yx, direction[0])
329                     if move(yx, direction[1]):
330                         f(yx, distance, dir_i, dir_progress)
331                         circle_in_map = True
332             distance += 1
333
334
335 map_manager = game_common.MapManager(globals())