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