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