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