home · contact · privacy
More refactoring.
[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._surroundings = 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     @property
258     def surroundings(self):
259         if self._surroundings is not None:
260             return self._surroundings
261         s = self.game.map_geometry.get_view(self.game.map_size,
262                                             self.game.get_map,
263                                             self._radius, self.view_offset)
264         self._surroundings = s
265         return self._surroundings
266
267     def get_stencil(self):
268         if self._stencil is not None:
269             return self._stencil
270         m = Map(self.surroundings.size, ' ')
271         for pos in self.surroundings:
272             if self.surroundings[pos] in {'.', '~'}:
273                 m[pos] = '.'
274         fov_center = YX((m.size.y) // 2, m.size.x // 2)
275         self._stencil = FovMapHex(m, fov_center)
276         return self._stencil
277
278     def get_visible_map(self):
279         stencil = self.get_stencil()
280         m = Map(self.surroundings.size, ' ')
281         for pos in m:
282             if stencil[pos] == '.':
283                 m[pos] = self.surroundings[pos]
284         return m
285
286     def get_visible_things(self):
287         stencil = self.get_stencil()
288         visible_things = []
289         for thing in self.game.things:
290             pos = self.game.map_geometry.pos_in_view(thing.position,
291                                                      self.view_offset,
292                                                      self.game.map_size)
293             if pos.y < 0 or pos.x < 0 or\
294                pos.y >= stencil.size.y or pos.x >= stencil.size.x:
295                 continue
296             if (not thing.in_inventory) and stencil[pos] == '.':
297                 visible_things += [thing]
298         return visible_things
299
300     def get_pickable_items(self):
301         pickable_ids = []
302         visible_things = self.get_visible_things()
303         neighbor_fields = self.game.map_geometry.get_neighbors(self.position,
304                                                                self.game.map_size)
305         for t in [t for t in visible_things
306                   if isinstance(t, ThingItem) and
307                   (t.position == self.position or
308                    t.position in neighbor_fields.values())]:
309             pickable_ids += [t.id_]
310         return pickable_ids
311
312
313
314 class ThingHuman(ThingAnimate):
315     type_ = 'human'
316     health = 100
317
318
319
320 class ThingMonster(ThingAnimate):
321     type_ = 'monster'
322     health = 50