home · contact · privacy
Enforce sane create_unfound decisions.
[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, create_unfound=False)
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         self.close_maps = ()
114
115     def _position_set(self, pos):
116         """For player we need to update .close_maps on every move via the
117            self.surroundings property method, to keep their reality
118            bubble in sync with their movement.
119
120         """
121         super()._position_set(pos)
122         if self.id_ == self.game.player_id:
123             if not hasattr(self, '_surroundings'):
124                 self._surroundings = None
125             self.surroundings
126
127     def move_on_dijkstra_map(self, own_pos, targets):
128         visible_map = self.get_visible_map()
129         dijkstra_map = Map(visible_map.size,
130                            start_indented=visible_map.start_indented)
131         n_max = 256
132         dijkstra_map.terrain = [n_max for i in range(dijkstra_map.size_i)]
133         for target in targets:
134             dijkstra_map[target] = 0
135         shrunk = True
136         get_neighbors = self.game.map_geometry.get_neighbors
137         while shrunk:
138             shrunk = False
139             for pos in dijkstra_map:
140                 if visible_map[pos] != '.':
141                     continue
142                 neighbors = get_neighbors((YX(0,0), pos), dijkstra_map.size,
143                                           dijkstra_map.start_indented)
144                 for direction in neighbors:
145                     big_yx, small_yx = neighbors[direction]
146                     if big_yx == YX(0,0) and \
147                        dijkstra_map[small_yx] < dijkstra_map[pos] - 1:
148                         dijkstra_map[pos] = dijkstra_map[small_yx] + 1
149                         shrunk = True
150         #print('DEBUG DIJKSTRA ---------------------', self.id_, self.position)
151         #for y, line in dijkstra_map.lines():
152         #    line_to_print = []
153         #    for x in line:
154         #        line_to_print += ['%3s' % x]
155         #    print(' '.join(line_to_print))
156         neighbors = get_neighbors((YX(0,0), own_pos), dijkstra_map.size,
157                                   dijkstra_map.start_indented)
158         n = n_max
159         target_direction = None
160         for direction in sorted(neighbors.keys()):
161             big_yx, small_yx = neighbors[direction]
162             if big_yx == (0,0):
163                 n_new = dijkstra_map[small_yx]
164                 if n_new < n:
165                     n = n_new
166                     target_direction = direction
167         return target_direction
168
169     #def hunt_player(self):
170     #    visible_things = self.get_visible_things()
171     #    target = None
172     #    for t in visible_things:
173     #        if t.type_ == 'human':
174     #            target = t.position[1] - self.view_offset
175     #            break
176     #    if target is not None:
177     #        try:
178     #            offset_self_pos = self.position[1] - self.view_offset
179     #            target_dir = self.move_on_dijkstra_map(offset_self_pos,
180     #                                                   [target])
181     #            if target_dir is not None:
182     #                self.set_task('MOVE', (target_dir,))
183     #                return True
184     #        except GameError:
185     #            pass
186     #    return False
187
188     def hunt_food_satisfaction(self):
189         for id_ in self.inventory:
190             t = self.game.get_thing(id_, create_unfound=False)
191             if t.type_ == 'food':
192                 self.set_task('EAT', (id_,))
193                 return True
194         for id_ in self.get_pickable_items():
195             t = self.game.get_thing(id_, create_unfound=False)
196             if t.type_ == 'food':
197                 self.set_task('PICKUP', (id_,))
198                 return True
199         visible_things = self.get_visible_things()
200         food_targets = []
201         for t in visible_things:
202             if t.type_ == 'food':
203                 food_targets += [self.game.map_geometry.pos_in_view(t.position,
204                                                                     self.view_offset,
205                                                                     self.game.map_size)]
206         offset_self_pos = self.game.map_geometry.pos_in_view(self.position,
207                                                              self.view_offset,
208                                                              self.game.map_size)
209         target_dir = self.move_on_dijkstra_map(offset_self_pos,
210                                                food_targets)
211         if target_dir:
212             try:
213                 self.set_task('MOVE', (target_dir,))
214                 return True
215             except GameError:
216                 pass
217         return False
218
219     def decide_task(self):
220         #if not self.hunt_player():
221         if not self.hunt_food_satisfaction():
222             self.set_task('WAIT')
223
224     def set_task(self, task_name, args=()):
225         task_class = self.game.tasks[task_name]
226         self.task = task_class(self, args)
227         self.task.check()  # will throw GameError if necessary
228
229     def proceed(self, is_AI=True):
230         """Further the thing in its tasks, decrease its health.
231
232         First, ensures an empty map, decrements .health and kills
233         thing if crossing zero (removes from self.game.things for AI
234         thing, or unsets self.game.player_is_alive for player thing);
235         then checks that self.task is still possible and aborts if
236         otherwise (for AI things, decides a new task).
237
238         Then decrements .task.todo; if it thus falls to <= 0, enacts
239         method whose name is 'task_' + self.task.name and sets .task =
240         None. If is_AI, calls .decide_task to decide a self.task.
241
242         """
243         self.unset_surroundings()
244         self.health -= 1
245         if self.health <= 0:
246             if self is self.game.player:
247                 self.game.player_is_alive = False
248             else:
249                 # TODO: Handle inventory.
250                 del self.game.things[self.game.things.index(self)]
251             return
252         try:
253             self.task.check()
254         except GameError as e:
255             self.task = None
256             self._last_task_result = e
257             if is_AI:
258                 try:
259                     self.decide_task()
260                 except GameError:
261                     self.set_task('WAIT')
262             return
263         self.task.todo -= 1
264         if self.task.todo <= 0:
265             self._last_task_result = self.task.do()
266             self.task = None
267         if is_AI and self.task is None:
268             try:
269                 self.decide_task()
270             except GameError:
271                 self.set_task('WAIT')
272
273     def unset_surroundings(self):
274         self._stencil = None
275         self._surroundings = None
276
277     @property
278     def view_offset(self):
279         return self.game.map_geometry.get_view_offset(self.game.map_size,
280                                                       self.position,
281                                                       self._radius)
282
283     @property
284     def surroundings(self):
285         if self._surroundings is not None:
286             return self._surroundings
287         s, close_maps = self.\
288             game.map_geometry.get_view_and_seen_maps(self.game.map_size,
289                                                      self.game.get_map,
290                                                      self._radius,
291                                                      self.view_offset)
292         self.close_maps = close_maps
293         self._surroundings = s
294         return self._surroundings
295
296     def get_stencil(self):
297         if self._stencil is not None:
298             return self._stencil
299         m = Map(self.surroundings.size, ' ', self.surroundings.start_indented)
300         for pos in self.surroundings:
301             if self.surroundings[pos] in {'.', '~'}:
302                 m[pos] = '.'
303         fov_center = YX((m.size.y) // 2, m.size.x // 2)
304         self._stencil = FovMapHex(m, fov_center)
305         return self._stencil
306
307     def get_visible_map(self):
308         stencil = self.get_stencil()
309         m = Map(self.surroundings.size, ' ', self.surroundings.start_indented)
310         for pos in m:
311             if stencil[pos] == '.':
312                 m[pos] = self.surroundings[pos]
313         return m
314
315     def get_visible_things(self):
316         stencil = self.get_stencil()
317         visible_things = []
318         for thing in self.game.things:
319             pos = self.game.map_geometry.pos_in_view(thing.position,
320                                                      self.view_offset,
321                                                      self.game.map_size)
322             if pos.y < 0 or pos.x < 0 or\
323                pos.y >= stencil.size.y or pos.x >= stencil.size.x:
324                 continue
325             if (not thing.in_inventory) and stencil[pos] == '.':
326                 visible_things += [thing]
327         return visible_things
328
329     def get_pickable_items(self):
330         pickable_ids = []
331         visible_things = self.get_visible_things()
332         neighbor_fields = self.game.map_geometry.get_neighbors(self.position,
333                                                                self.game.map_size)
334         for t in [t for t in visible_things
335                   if isinstance(t, ThingItem) and
336                   (t.position == self.position or
337                    t.position in neighbor_fields.values())]:
338             pickable_ids += [t.id_]
339         return pickable_ids
340
341
342
343 class ThingHuman(ThingAnimate):
344     type_ = 'human'
345     health = 100
346
347
348
349 class ThingMonster(ThingAnimate):
350     type_ = 'monster'
351     health = 50