home · contact · privacy
1e23809aec1af513056012fcc4925b0c5a6212cf
[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                            start_indented=visible_map.start_indented)
118         n_max = 256
119         dijkstra_map.terrain = [n_max for i in range(dijkstra_map.size_i)]
120         for target in targets:
121             dijkstra_map[target] = 0
122         shrunk = True
123         get_neighbors = self.game.map_geometry.get_neighbors
124         while shrunk:
125             shrunk = False
126             for pos in dijkstra_map:
127                 if visible_map[pos] != '.':
128                     continue
129                 neighbors = get_neighbors((YX(0,0), pos), dijkstra_map.size,
130                                           dijkstra_map.start_indented)
131                 for direction in neighbors:
132                     big_yx, small_yx = neighbors[direction]
133                     if big_yx == YX(0,0) and \
134                        dijkstra_map[small_yx] < dijkstra_map[pos] - 1:
135                         dijkstra_map[pos] = dijkstra_map[small_yx] + 1
136                         shrunk = True
137         get_neighbors((YX(0,0), own_pos), dijkstra_map.size,
138                       dijkstra_map.start_indented)
139         n = n_max
140         target_direction = None
141         for direction in sorted(neighbors.keys()):
142             big_yx, small_yx = neighbors[direction]
143             if big_yx == (0,0):
144                 n_new = dijkstra_map[small_yx]
145                 if n_new < n:
146                     n = n_new
147                     target_direction = direction
148         return target_direction
149
150     def hunt_player(self):
151         visible_things = self.get_visible_things()
152         target = None
153         for t in visible_things:
154             if t.type_ == 'human':
155                 target = t.position[1] - self.view_offset
156                 break
157         if target is not None:
158             try:
159                 offset_self_pos = self.position[1] - self.view_offset
160                 target_dir = self.move_on_dijkstra_map(offset_self_pos,
161                                                        [target])
162                 if target_dir is not None:
163                     self.set_task('MOVE', (target_dir,))
164                     return True
165             except GameError:
166                 pass
167         return False
168
169     def hunt_food_satisfaction(self):
170         for id_ in self.inventory:
171             t = self.game.get_thing(id_)
172             if t.type_ == 'food':
173                 self.set_task('EAT', (id_,))
174                 return True
175         for id_ in self.get_pickable_items():
176             t = self.game.get_thing(id_)
177             if t.type_ == 'food':
178                 self.set_task('PICKUP', (id_,))
179                 return True
180         visible_things = self.get_visible_things()
181         food_targets = []
182         for t in visible_things:
183             if t.type_ == 'food':
184                 food_targets += [t.position[1] - self.view_offset]
185         offset_self_pos = self.position[1] - self.view_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._surroundings = None
252
253     @property
254     def view_offset(self):
255         return self.game.map_geometry.get_view_offset(self.game.map_size,
256                                                       self.position,
257                                                       self._radius)
258
259     @property
260     def surroundings(self):
261         if self._surroundings is not None:
262             return self._surroundings
263         s = self.game.map_geometry.get_view(self.game.map_size,
264                                             self.game.get_map,
265                                             self._radius, self.view_offset)
266         self._surroundings = s
267         return self._surroundings
268
269     def get_stencil(self):
270         if self._stencil is not None:
271             return self._stencil
272         m = Map(self.surroundings.size, ' ', self.surroundings.start_indented)
273         for pos in self.surroundings:
274             if self.surroundings[pos] in {'.', '~'}:
275                 m[pos] = '.'
276         fov_center = YX((m.size.y) // 2, m.size.x // 2)
277         self._stencil = FovMapHex(m, fov_center)
278         return self._stencil
279
280     def get_visible_map(self):
281         stencil = self.get_stencil()
282         m = Map(self.surroundings.size, ' ', self.surroundings.start_indented)
283         for pos in m:
284             if stencil[pos] == '.':
285                 m[pos] = self.surroundings[pos]
286         return m
287
288     def get_visible_things(self):
289         stencil = self.get_stencil()
290         visible_things = []
291         for thing in self.game.things:
292             pos = self.game.map_geometry.pos_in_view(thing.position,
293                                                      self.view_offset,
294                                                      self.game.map_size)
295             if pos.y < 0 or pos.x < 0 or\
296                pos.y >= stencil.size.y or pos.x >= stencil.size.x:
297                 continue
298             if (not thing.in_inventory) and stencil[pos] == '.':
299                 visible_things += [thing]
300         return visible_things
301
302     def get_pickable_items(self):
303         pickable_ids = []
304         visible_things = self.get_visible_things()
305         neighbor_fields = self.game.map_geometry.get_neighbors(self.position,
306                                                                self.game.map_size)
307         for t in [t for t in visible_things
308                   if isinstance(t, ThingItem) and
309                   (t.position == self.position or
310                    t.position in neighbor_fields.values())]:
311             pickable_ids += [t.id_]
312         return pickable_ids
313
314
315
316 class ThingHuman(ThingAnimate):
317     type_ = 'human'
318     health = 100
319
320
321
322 class ThingMonster(ThingAnimate):
323     type_ = 'monster'
324     health = 50