home · contact · privacy
Fix FOV floating point bugs by using fractions.Fraction for fractions.
[plomrogue2-experiments] / server_ / map_.py
1 import sys
2 sys.path.append('../')
3 import game_common
4 import server_.game
5 import fractions
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]:
147                     #print('DEBUG merging to', old_cone)
148                     old_cone[0] = new_cone[0]
149                     #print('DEBUG merged cone:', old_cone)
150                     return True
151                 if new_cone[1] < old_cone[1] and \
152                     new_cone[0] >= old_cone[1]:
153                     #print('DEBUG merging to', old_cone)
154                     old_cone[1] = new_cone[1]
155                     #print('DEBUG merged cone:', old_cone)
156                     return True
157             return False
158
159         def eval_cone(cone):
160             new_cone = [left_arm, right_arm]
161             #print('DEBUG CONE', cone, '(', step_size, distance_to_center, number_steps, ')')
162             if in_shadow_cone(cone):
163                 return
164             self[yx] = '.'
165             if self.source_map[yx] != '.':
166                 #print('DEBUG throws shadow', cone)
167                 unmerged = True
168                 while merge_cone(cone):
169                     unmerged = False
170                 if unmerged:
171                     self.shadow_cones += [cone]
172
173         #print('DEBUG', yx)
174         step_size = fractions.Fraction(CIRCLE, 6) / distance_to_center
175         number_steps = dir_i * distance_to_center + dir_progress
176         left_arm = correct_arm(-(step_size/2) - step_size*number_steps)
177         right_arm = correct_arm(left_arm - step_size)
178         # Optimization potential: left cone could be derived from previous
179         # right cone. Better even: Precalculate all cones.
180         if right_arm > left_arm:
181             eval_cone([left_arm, 0])
182             eval_cone([CIRCLE, right_arm])
183         else:
184             eval_cone([left_arm, right_arm])
185
186     def circle_out(self, yx, f):
187         # Optimization potential: Precalculate movement positions. (How to check
188         # circle_in_map then?)
189         # Optimization potential: Precalculate what hexes are shaded by what hex
190         # and skip evaluation of already shaded hexes. (This only works if hex
191         # shading implies they completely lie in existing shades; otherwise we
192         # would lose shade growth through hexes at shade borders.)
193
194         def move(pos, direction):
195             """Move position pos into direction. Return whether still in map."""
196             mover = getattr(self, 'move_' + direction)
197             pos[:] = mover(pos)
198             if pos[0] < 0 or pos[1] < 0 or \
199                pos[0] >= self.size[0] or pos[1] >= self.size[1]:
200                 return False
201             return True
202
203         # TODO: Start circling only in earliest obstacle distance.
204         directions = ('DOWNLEFT', 'LEFT', 'UPLEFT', 'UPRIGHT', 'RIGHT', 'DOWNRIGHT')
205         circle_in_map = True
206         distance = 1
207         yx = yx[:]
208         #print('DEBUG CIRCLE_OUT', yx)
209         while circle_in_map:
210             circle_in_map = False
211             move(yx, 'RIGHT')
212             for dir_i in range(len(directions)):
213                 for dir_progress in range(distance):
214                     direction = directions[dir_i]
215                     if move(yx, direction):
216                         f(yx, distance, dir_i, dir_progress)
217                         circle_in_map = True
218             distance += 1
219
220
221 class MapSquare(Map):
222
223     # The following is used nowhere, so not implemented.
224     #def are_neighbors(self, pos_1, pos_2):
225     #    return abs(pos_1[0] - pos_2[0]) <= 1 and abs(pos_1[1] - pos_2[1] <= 1)
226
227     def move_UP(self, start_pos):
228         return [start_pos[0] - 1, start_pos[1]]
229
230     def move_DOWN(self, start_pos):
231         return [start_pos[0] + 1, start_pos[1]]
232
233
234 class MapFovSquare(MapSquare):
235     """Just a marginally and unsatisfyingly adapted variant of MapFovHex."""
236
237     def __init__(self, source_map, yx):
238         self.source_map = source_map
239         self.size = self.source_map.size
240         self.terrain = '?' * self.size_i
241         self[yx] = '.'
242         self.shadow_cones = []
243         self.circle_out(yx, self.shadow_process_hex)
244
245     def shadow_process_hex(self, yx, distance_to_center, dir_i, dir_progress):
246         CIRCLE = 360  # Since we'll float anyways, number is actually arbitrary.
247
248         def correct_arm(arm):
249             if arm < 0:
250                 arm += CIRCLE
251             return arm
252
253         def in_shadow_cone(new_cone):
254             for old_cone in self.shadow_cones:
255                 if old_cone[0] >= new_cone[0] and \
256                     new_cone[1] >= old_cone[1]:
257                     #print('DEBUG shadowed by:', old_cone)
258                     return True
259             return False
260
261         def merge_cone(new_cone):
262             for old_cone in self.shadow_cones:
263                 if new_cone[0] > old_cone[0] and \
264                     new_cone[1] <= old_cone[0]:
265                     #print('DEBUG merging to', old_cone)
266                     old_cone[0] = new_cone[0]
267                     #print('DEBUG merged cone:', old_cone)
268                     return True
269                 if new_cone[1] < old_cone[1] and \
270                     new_cone[0] >= old_cone[1]:
271                     #print('DEBUG merging to', old_cone)
272                     old_cone[1] = new_cone[1]
273                     #print('DEBUG merged cone:', old_cone)
274                     return True
275             return False
276
277         def eval_cone(cone):
278             new_cone = [left_arm, right_arm]
279             #print('DEBUG CONE', cone, '(', step_size, distance_to_center, number_steps, ')')
280             if in_shadow_cone(cone):
281                 return
282             self[yx] = '.'
283             if self.source_map[yx] != '.':
284                 #print('DEBUG throws shadow', cone)
285                 unmerged = True
286                 while merge_cone(cone):
287                     unmerged = False
288                 if unmerged:
289                     self.shadow_cones += [cone]
290
291         #print('DEBUG', yx)
292         step_size = fractions.Fraction(CIRCLE, 4) / distance_to_center
293         number_steps = dir_i * distance_to_center + dir_progress
294         left_arm = correct_arm(-(step_size/2) - step_size*number_steps)
295         right_arm = correct_arm(left_arm - step_size)
296         if right_arm > left_arm:
297             eval_cone([left_arm, 0])
298             eval_cone([CIRCLE, right_arm])
299         else:
300             eval_cone([left_arm, right_arm])
301
302     def circle_out(self, yx, f):
303
304         def move(pos, direction):
305             """Move position pos into direction. Return whether still in map."""
306             mover = getattr(self, 'move_' + direction)
307             pos[:] = mover(pos)
308             if pos[0] < 0 or pos[1] < 0 or \
309                pos[0] >= self.size[0] or pos[1] >= self.size[1]:
310                 return False
311             return True
312
313         directions = (('DOWN', 'LEFT'), ('LEFT', 'UP'),
314                       ('UP', 'RIGHT'), ('RIGHT', 'DOWN'))
315         circle_in_map = True
316         distance = 1
317         yx = yx[:]
318         #print('DEBUG CIRCLE_OUT', yx)
319         while circle_in_map:
320             circle_in_map = False
321             move(yx, 'RIGHT')
322             for dir_i in range(len(directions)):
323                 for dir_progress in range(distance):
324                     direction = directions[dir_i]
325                     move(yx, direction[0])
326                     if move(yx, direction[1]):
327                         f(yx, distance, dir_i, dir_progress)
328                         circle_in_map = True
329             distance += 1
330
331
332 map_manager = game_common.MapManager(globals())