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