home · contact · privacy
Fix broken dijkstra mapping / AI searches.
[plomrogue2-experiments] / new / plomrogue / things.py
1 from plomrogue.errors import GameError
2 from plomrogue.mapping import YX, Map, FovMapHex
3
4
5
6 class ThingBase:
7     type_ = '?'
8
9     def __init__(self, game, id_=None, position=(YX(0,0), YX(0,0))):
10         self.game = game
11         if id_ is None:
12             self.id_ = self.game.new_thing_id()
13         else:
14             self.id_ = id_
15         self.position = position
16
17     @property
18     def position(self):
19         return self._position
20
21     def _position_set(self, pos):
22         """Set self._position to pos.
23
24         We use this setter as core to the @position.setter property
25         method due to property setter subclassing not yet working
26         properly, see <https://bugs.python.org/issue14965>. We will
27         therefore super() _position_set instead of @position.setter in
28         subclasses.
29
30         """
31         self._position = pos
32
33     @position.setter
34     def position(self, pos):
35         self._position_set(pos)
36
37
38
39 class Thing(ThingBase):
40     blocking = False
41     in_inventory = False
42
43     def __init__(self, *args, **kwargs):
44         self.inventory = []
45         self._radius = 8
46         super().__init__(*args, **kwargs)
47
48     def proceed(self):
49         pass
50
51     def _position_set(self, pos):
52         super()._position_set(pos)
53         for t_id in self.inventory:
54             t = self.game.get_thing(t_id)
55             t.position = self.position
56         if not self.id_ == self.game.player_id:
57             return
58         edge_left = self.position[1].x - self._radius
59         edge_right = self.position[1].x + self._radius
60         edge_up = self.position[1].y - self._radius
61         edge_down = self.position[1].y + self._radius
62         if edge_left < 0:
63             self.game.get_map(self.position[0] + YX(1,-1))
64             self.game.get_map(self.position[0] + YX(0,-1))
65             self.game.get_map(self.position[0] + YX(-1,-1))
66         if edge_right >= self.game.map_size.x:
67             self.game.get_map(self.position[0] + YX(1,1))
68             self.game.get_map(self.position[0] + YX(0,1))
69             self.game.get_map(self.position[0] + YX(-1,1))
70         if edge_up < 0:
71             self.game.get_map(self.position[0] + YX(-1,1))
72             self.game.get_map(self.position[0] + YX(-1,0))
73             self.game.get_map(self.position[0] + YX(-1,-1))
74         if edge_down >= self.game.map_size.y:
75             self.game.get_map(self.position[0] + YX(1,1))
76             self.game.get_map(self.position[0] + YX(1,0))
77             self.game.get_map(self.position[0] + YX(1,-1))
78         #alternative
79         #if self.position[1].x < self._radius:
80         #    self.game.get_map(self.position[0] - YX(0,1))
81         #if self.position[1].y < self._radius:
82         #    self.game.get_map(self.position[0] - YX(1,0))
83         #if self.position[1].x > self.game.map_size.x - self._radius:
84         #    self.game.get_map(self.position[0] + YX(0,1))
85         #if self.position[1].y > self.game.map_size.y - self._radius:
86         #    self.game.get_map(self.position[0] + YX(1,0))
87         #if self.position[1].y < self._radius and \
88         #   self.position[1].x <= [pos for pos in
89         #                          diagonal_distance_edge
90         #                          if pos.y == self.position[1].y][0].x:
91         #    self.game.get_map(self.position[0] - YX(1,1))
92
93
94
95 class ThingItem(Thing):
96     pass
97
98
99
100 class ThingFood(ThingItem):
101     type_ = 'food'
102
103
104
105 class ThingAnimate(Thing):
106     blocking = True
107
108     def __init__(self, *args, **kwargs):
109         super().__init__(*args, **kwargs)
110         self.set_task('WAIT')
111         self._last_task_result = None
112         self.unset_surroundings()
113
114     def move_on_dijkstra_map(self, own_pos, targets):
115         visible_map = self.get_visible_map()
116         dijkstra_map = Map(visible_map.size,
117                            start_indented=visible_map.start_indented)
118         n_max = 256
119         dijkstra_map.terrain = [n_max for i in range(dijkstra_map.size_i)]
120         for target in targets:
121             dijkstra_map[target] = 0
122         shrunk = True
123         get_neighbors = self.game.map_geometry.get_neighbors
124         while shrunk:
125             shrunk = False
126             for pos in dijkstra_map:
127                 if visible_map[pos] != '.':
128                     continue
129                 neighbors = get_neighbors((YX(0,0), pos), dijkstra_map.size,
130                                           dijkstra_map.start_indented)
131                 for direction in neighbors:
132                     big_yx, small_yx = neighbors[direction]
133                     if big_yx == YX(0,0) and \
134                        dijkstra_map[small_yx] < dijkstra_map[pos] - 1:
135                         dijkstra_map[pos] = dijkstra_map[small_yx] + 1
136                         shrunk = True
137         #print('DEBUG DIJKSTRA ---------------------', self.id_, self.position)
138         #for y, line in dijkstra_map.lines():
139         #    line_to_print = []
140         #    for x in line:
141         #        line_to_print += ['%3s' % x]
142         #    print(' '.join(line_to_print))
143         neighbors = get_neighbors((YX(0,0), own_pos), dijkstra_map.size,
144                                   dijkstra_map.start_indented)
145         n = n_max
146         target_direction = None
147         for direction in sorted(neighbors.keys()):
148             big_yx, small_yx = neighbors[direction]
149             if big_yx == (0,0):
150                 n_new = dijkstra_map[small_yx]
151                 if n_new < n:
152                     n = n_new
153                     target_direction = direction
154         return target_direction
155
156     #def hunt_player(self):
157     #    visible_things = self.get_visible_things()
158     #    target = None
159     #    for t in visible_things:
160     #        if t.type_ == 'human':
161     #            target = t.position[1] - self.view_offset
162     #            break
163     #    if target is not None:
164     #        try:
165     #            offset_self_pos = self.position[1] - self.view_offset
166     #            target_dir = self.move_on_dijkstra_map(offset_self_pos,
167     #                                                   [target])
168     #            if target_dir is not None:
169     #                self.set_task('MOVE', (target_dir,))
170     #                return True
171     #        except GameError:
172     #            pass
173     #    return False
174
175     def hunt_food_satisfaction(self):
176         for id_ in self.inventory:
177             t = self.game.get_thing(id_)
178             if t.type_ == 'food':
179                 self.set_task('EAT', (id_,))
180                 return True
181         for id_ in self.get_pickable_items():
182             t = self.game.get_thing(id_)
183             if t.type_ == 'food':
184                 self.set_task('PICKUP', (id_,))
185                 return True
186         visible_things = self.get_visible_things()
187         food_targets = []
188         for t in visible_things:
189             if t.type_ == 'food':
190                 food_targets += [self.game.map_geometry.pos_in_view(t.position,
191                                                                     self.view_offset,
192                                                                     self.game.map_size)]
193         offset_self_pos = self.game.map_geometry.pos_in_view(self.position,
194                                                              self.view_offset,
195                                                              self.game.map_size)
196         target_dir = self.move_on_dijkstra_map(offset_self_pos,
197                                                food_targets)
198         if target_dir:
199             try:
200                 self.set_task('MOVE', (target_dir,))
201                 return True
202             except GameError:
203                 pass
204         return False
205
206     def decide_task(self):
207         #if not self.hunt_player():
208         if not self.hunt_food_satisfaction():
209             self.set_task('WAIT')
210
211     def set_task(self, task_name, args=()):
212         task_class = self.game.tasks[task_name]
213         self.task = task_class(self, args)
214         self.task.check()  # will throw GameError if necessary
215
216     def proceed(self, is_AI=True):
217         """Further the thing in its tasks, decrease its health.
218
219         First, ensures an empty map, decrements .health and kills
220         thing if crossing zero (removes from self.game.things for AI
221         thing, or unsets self.game.player_is_alive for player thing);
222         then checks that self.task is still possible and aborts if
223         otherwise (for AI things, decides a new task).
224
225         Then decrements .task.todo; if it thus falls to <= 0, enacts
226         method whose name is 'task_' + self.task.name and sets .task =
227         None. If is_AI, calls .decide_task to decide a self.task.
228
229         """
230         self.unset_surroundings()
231         self.health -= 1
232         if self.health <= 0:
233             if self is self.game.player:
234                 self.game.player_is_alive = False
235             else:
236                 del self.game.things[self.game.things.index(self)]
237             return
238         try:
239             self.task.check()
240         except GameError as e:
241             self.task = None
242             self._last_task_result = e
243             if is_AI:
244                 try:
245                     self.decide_task()
246                 except GameError:
247                     self.set_task('WAIT')
248             return
249         self.task.todo -= 1
250         if self.task.todo <= 0:
251             self._last_task_result = self.task.do()
252             self.task = None
253         if is_AI and self.task is None:
254             try:
255                 self.decide_task()
256             except GameError:
257                 self.set_task('WAIT')
258
259     def unset_surroundings(self):
260         self._stencil = None
261         self._surroundings = None
262
263     @property
264     def view_offset(self):
265         return self.game.map_geometry.get_view_offset(self.game.map_size,
266                                                       self.position,
267                                                       self._radius)
268
269     @property
270     def surroundings(self):
271         if self._surroundings is not None:
272             return self._surroundings
273         s = self.game.map_geometry.get_view(self.game.map_size,
274                                             self.game.get_map,
275                                             self._radius, self.view_offset)
276         self._surroundings = s
277         return self._surroundings
278
279     def get_stencil(self):
280         if self._stencil is not None:
281             return self._stencil
282         m = Map(self.surroundings.size, ' ', self.surroundings.start_indented)
283         for pos in self.surroundings:
284             if self.surroundings[pos] in {'.', '~'}:
285                 m[pos] = '.'
286         fov_center = YX((m.size.y) // 2, m.size.x // 2)
287         self._stencil = FovMapHex(m, fov_center)
288         return self._stencil
289
290     def get_visible_map(self):
291         stencil = self.get_stencil()
292         m = Map(self.surroundings.size, ' ', self.surroundings.start_indented)
293         for pos in m:
294             if stencil[pos] == '.':
295                 m[pos] = self.surroundings[pos]
296         return m
297
298     def get_visible_things(self):
299         stencil = self.get_stencil()
300         visible_things = []
301         for thing in self.game.things:
302             pos = self.game.map_geometry.pos_in_view(thing.position,
303                                                      self.view_offset,
304                                                      self.game.map_size)
305             if pos.y < 0 or pos.x < 0 or\
306                pos.y >= stencil.size.y or pos.x >= stencil.size.x:
307                 continue
308             if (not thing.in_inventory) and stencil[pos] == '.':
309                 visible_things += [thing]
310         return visible_things
311
312     def get_pickable_items(self):
313         pickable_ids = []
314         visible_things = self.get_visible_things()
315         neighbor_fields = self.game.map_geometry.get_neighbors(self.position,
316                                                                self.game.map_size)
317         for t in [t for t in visible_things
318                   if isinstance(t, ThingItem) and
319                   (t.position == self.position or
320                    t.position in neighbor_fields.values())]:
321             pickable_ids += [t.id_]
322         return pickable_ids
323
324
325
326 class ThingHuman(ThingAnimate):
327     type_ = 'human'
328     health = 100
329
330
331
332 class ThingMonster(ThingAnimate):
333     type_ = 'monster'
334     health = 50