home · contact · privacy
Flatten game->world hierarchy.
[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         offset = YX(self.position[0].y * self.game.map_size.y +
258                     self.position[1].y - self._radius,
259                     self.position[0].x * self.game.map_size.x +
260                     self.position[1].x - self._radius)
261         self._surroundings_offset = offset
262         return self._surroundings_offset
263
264     def get_surrounding_map(self):
265         if self._surrounding_map is not None:
266             return self._surrounding_map
267         self._surrounding_map = Map(size=YX(self._radius*2+1, self._radius*2+1))
268         offset = self.get_surroundings_offset()
269         for pos in self._surrounding_map:
270             offset_pos = pos + offset
271             absolutize = self.game.map_geometry.absolutize_coordinate
272             big_yx, small_yx = absolutize(self.game.map_size, (0,0), offset_pos)
273             map_ = self.game.get_map(big_yx, False)
274             if map_ is None:
275                 map_ = Map(size=self.game.map_size)
276             self._surrounding_map[pos] = map_[small_yx]
277         return self._surrounding_map
278
279     def get_stencil(self):
280         if self._stencil is not None:
281             return self._stencil
282         surrounding_map = self.get_surrounding_map()
283         m = Map(surrounding_map.size, ' ')
284         for pos in surrounding_map:
285             if surrounding_map[pos] in {'.', '~'}:
286                 m[pos] = '.'
287         fov_center = YX((m.size.y) // 2, m.size.x // 2)
288         self._stencil = FovMapHex(m, fov_center)
289         return self._stencil
290
291     def get_visible_map(self):
292         stencil = self.get_stencil()
293         m = Map(self.get_surrounding_map().size, ' ')
294         for pos in m:
295             if stencil[pos] == '.':
296                 m[pos] = self._surrounding_map[pos]
297         return m
298
299     def get_visible_things(self):
300         stencil = self.get_stencil()
301         offset = self.get_surroundings_offset()
302         visible_things = []
303         for thing in self.game.things:
304             pos = self.game.map_geometry.pos_in_projection(thing.position,
305                                                            offset,
306                                                            self.game.map_size)
307             if pos.y < 0 or pos.x < 0 or\
308                pos.y >= stencil.size.y or pos.x >= stencil.size.x:
309                 continue
310             if (not thing.in_inventory) and stencil[pos] == '.':
311                 visible_things += [thing]
312         return visible_things
313
314     def get_pickable_items(self):
315         pickable_ids = []
316         visible_things = self.get_visible_things()
317         neighbor_fields = self.game.map_geometry.get_neighbors(self.position,
318                                                                self.game.map_size)
319         for t in [t for t in visible_things
320                   if isinstance(t, ThingItem) and
321                   (t.position == self.position or
322                    t.position in neighbor_fields.values())]:
323             pickable_ids += [t.id_]
324         return pickable_ids
325
326
327
328 class ThingHuman(ThingAnimate):
329     type_ = 'human'
330     health = 100
331
332
333
334 class ThingMonster(ThingAnimate):
335     type_ = 'monster'
336     health = 50