home · contact · privacy
Use trivially re-seedable PRNG.
[plomrogue2-experiments] / new / plomrogue / things.py
1 from plomrogue.errors import GameError
2
3
4
5 class ThingBase:
6     type_ = '?'
7
8     def __init__(self, world, id_=None, position=((0,0), (0,0))):
9         self.world = world
10         self.position = position
11         if id_ is None:
12             self.id_ = self.world.new_thing_id()
13         else:
14             self.id_ = id_
15
16
17
18 class Thing(ThingBase):
19     blocking = False
20     in_inventory = False
21
22     def __init__(self, *args, **kwargs):
23         super().__init__(*args, **kwargs)
24         self.inventory = []
25
26     def proceed(self):
27         pass
28
29
30
31 class ThingItem(Thing):
32     pass
33
34
35
36 class ThingFood(ThingItem):
37     type_ = 'food'
38
39
40
41 class ThingAnimate(Thing):
42     blocking = True
43
44     def __init__(self, *args, **kwargs):
45         super().__init__(*args, **kwargs)
46         self.set_task('WAIT')
47         self._last_task_result = None
48         self._radius = 8
49         self.unset_surroundings()
50
51     def move_on_dijkstra_map(self, own_pos, targets):
52         visible_map = self.get_visible_map()
53         dijkstra_map = self.world.game.map_type(visible_map.size)
54         n_max = 256
55         dijkstra_map.terrain = [n_max for i in range(dijkstra_map.size_i)]
56         for target in targets:
57             dijkstra_map[target] = 0
58         shrunk = True
59         while shrunk:
60             shrunk = False
61             for pos in dijkstra_map:
62                 if visible_map[pos] != '.':
63                     continue
64                 neighbors = dijkstra_map.get_neighbors(tuple(pos))
65                 for direction in neighbors:
66                     yx = neighbors[direction]
67                     if yx is not None and dijkstra_map[yx] < dijkstra_map[pos] - 1:
68                         dijkstra_map[pos] = dijkstra_map[yx] + 1
69                         shrunk = True
70         neighbors = dijkstra_map.get_neighbors(own_pos)
71         n = n_max
72         target_direction = None
73         for direction in sorted(neighbors.keys()):
74             yx = neighbors[direction]
75             if yx is not None:
76                 n_new = dijkstra_map[yx]
77                 if n_new < n:
78                     n = n_new
79                     target_direction = direction
80         return target_direction
81
82     def hunt_player(self):
83         visible_things = self.get_visible_things()
84         offset = self.get_surroundings_offset()
85         target = None
86         for t in visible_things:
87             if t.type_ == 'human':
88                 target = (t.position[1][0] - offset[0],
89                           t.position[1][1] - offset[1])
90                 break
91         if target is not None:
92             try:
93                 offset_self_pos = (self.position[1][0] - offset[0],
94                                    self.position[1][1] - offset[1])
95                 target_dir = self.move_on_dijkstra_map(offset_self_pos,
96                                                        [target])
97                 if target_dir is not None:
98                     self.set_task('MOVE', (target_dir,))
99                     return True
100             except GameError:
101                 pass
102         return False
103
104     def hunt_food_satisfaction(self):
105         for id_ in self.inventory:
106             t = self.world.get_thing(id_)
107             if t.type_ == 'food':
108                 self.set_task('EAT', (id_,))
109                 return True
110         for id_ in self.get_pickable_items():
111             t = self.world.get_thing(id_)
112             if t.type_ == 'food':
113                 self.set_task('PICKUP', (id_,))
114                 return True
115         visible_things = self.get_visible_things()
116         offset = self.get_surroundings_offset()
117         food_targets = []
118         for t in visible_things:
119             if t.type_ == 'food':
120                 food_targets += [(t.position[1][0] - offset[0],
121                                   t.position[1][1] - offset[1])]
122         offset_self_pos = (self.position[1][0] - offset[0],
123                            self.position[1][1] - offset[1])
124         target_dir = self.move_on_dijkstra_map(offset_self_pos,
125                                                food_targets)
126         if target_dir:
127             try:
128                 self.set_task('MOVE', (target_dir,))
129                 return True
130             except GameError:
131                 pass
132         return False
133
134     def decide_task(self):
135         #if not self.hunt_player():
136         if not self.hunt_food_satisfaction():
137             self.set_task('WAIT')
138
139     def set_task(self, task_name, args=()):
140         task_class = self.world.game.tasks[task_name]
141         self.task = task_class(self, args)
142         self.task.check()  # will throw GameError if necessary
143
144     def proceed(self, is_AI=True):
145         """Further the thing in its tasks, decrease its health.
146
147         First, ensures an empty map, decrements .health and kills
148         thing if crossing zero (removes from self.world.things for AI
149         thing, or unsets self.world.player_is_alive for player thing);
150         then checks that self.task is still possible and aborts if
151         otherwise (for AI things, decides a new task).
152
153         Then decrements .task.todo; if it thus falls to <= 0, enacts
154         method whose name is 'task_' + self.task.name and sets .task =
155         None. If is_AI, calls .decide_task to decide a self.task.
156
157         """
158         self.unset_surroundings()
159         self.health -= 1
160         if self.health <= 0:
161             if self is self.world.player:
162                 self.world.player_is_alive = False
163             else:
164                 del self.world.things[self.world.things.index(self)]
165             return
166         try:
167             self.task.check()
168         except GameError as e:
169             self.task = None
170             self._last_task_result = e
171             if is_AI:
172                 try:
173                     self.decide_task()
174                 except GameError:
175                     self.set_task('WAIT')
176             return
177         self.task.todo -= 1
178         if self.task.todo <= 0:
179             self._last_task_result = self.task.do()
180             self.task = None
181         if is_AI and self.task is None:
182             try:
183                 self.decide_task()
184             except GameError:
185                 self.set_task('WAIT')
186
187     def unset_surroundings(self):
188         self._stencil = None
189         self._surrounding_map = None
190         self._surroundings_offset = None
191
192     def must_fix_indentation(self):
193         return self._radius % 2 != self.position[1][0] % 2
194
195     def get_surroundings_offset(self):
196         if self._surroundings_offset is not None:
197             return self._surroundings_offset
198         add_line = self.must_fix_indentation()
199         offset_y = self.position[1][0] - self._radius - int(add_line)
200         offset_x = self.position[1][1] - self._radius
201         self._surroundings_offset = (offset_y, offset_x)
202         return self._surroundings_offset
203
204     def get_surrounding_map(self):
205         if self._surrounding_map is not None:
206             return self._surrounding_map
207
208         def pan_and_scan(size_of_axis, pos, offset):
209             big_pos = 0
210             small_pos = pos + offset
211             if small_pos < 0:
212                 big_pos = -1
213                 small_pos = size_of_axis + small_pos
214             elif small_pos >= size_of_axis:
215                 big_pos = 1
216                 small_pos = small_pos - size_of_axis
217             return big_pos, small_pos
218
219         add_line = self.must_fix_indentation()
220         self._surrounding_map = self.world.game.\
221                                 map_type(size=(self._radius*2+1+int(add_line),
222                                                self._radius*2+1))
223         size = self.world.maps[(0,0)].size
224         offset = self.get_surroundings_offset()
225         for pos in self._surrounding_map:
226             big_y, small_y = pan_and_scan(size[0], pos[0], offset[0])
227             big_x, small_x = pan_and_scan(size[1], pos[1], offset[1])
228             big_yx = (big_y, big_x)
229             small_yx = (small_y, small_x)
230             self._surrounding_map[pos] = self.world.maps[big_yx][small_yx]
231         return self._surrounding_map
232
233     def get_stencil(self):
234         if self._stencil is not None:
235             return self._stencil
236         surrounding_map = self.get_surrounding_map()
237         m = surrounding_map.new_from_shape(' ')
238         for pos in surrounding_map:
239             if surrounding_map[pos] in {'.', '~'}:
240                 m[pos] = '.'
241         offset = self.get_surroundings_offset()
242         fov_center = (self.position[1][0] - offset[0],
243                       self.position[1][1] - offset[1])
244         self._stencil = m.get_fov_map(fov_center)
245         return self._stencil
246
247     def get_visible_map(self):
248         stencil = self.get_stencil()
249         m = self.get_surrounding_map().new_from_shape(' ')
250         for pos in m:
251             if stencil[pos] == '.':
252                 m[pos] = self._surrounding_map[pos]
253         return m
254
255     def get_visible_things(self):
256
257         def calc_pos_in_fov(big_pos, small_pos, offset, size_of_axis):
258             pos = small_pos - offset
259             if big_pos == -1:
260                 pos = small_pos - size_of_axis - offset
261             elif big_pos == 1:
262                 pos = small_pos + size_of_axis - offset
263             return pos
264
265         stencil = self.get_stencil()
266         offset = self.get_surroundings_offset()
267         visible_things = []
268         size = self.world.maps[(0,0)].size
269         fov_size = self.get_surrounding_map().size
270         for thing in self.world.things:
271             big_pos = thing.position[0]
272             small_pos = thing.position[1]
273             pos_y = calc_pos_in_fov(big_pos[0], small_pos[0], offset[0], size[0])
274             pos_x = calc_pos_in_fov(big_pos[1], small_pos[1], offset[1], size[1])
275             if pos_y < 0 or pos_x < 0 or pos_y >= fov_size[0] or pos_x >= fov_size[1]:
276                 continue
277             if (not thing.in_inventory) and stencil[(pos_y, pos_x)] == '.':
278                 visible_things += [thing]
279         return visible_things
280
281     def get_pickable_items(self):
282         pickable_ids = []
283         visible_things = self.get_visible_things()
284         for t in [t for t in visible_things if
285                   isinstance(t, ThingItem) and
286                   (t.position == self.position or
287                    t.position[1] in
288                    self.world.maps[(0,0)].get_neighbors(self.position[1]).values())]:
289             pickable_ids += [t.id_]
290         return pickable_ids
291
292
293
294 class ThingHuman(ThingAnimate):
295     type_ = 'human'
296     health = 100
297
298
299
300 class ThingMonster(ThingAnimate):
301     type_ = 'monster'
302     health = 50