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