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