home · contact · privacy
Prepare map logic extension, limit FOV distance.
[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 = 16
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, offset = self.get_visible_things()
84         target = None
85         for t in visible_things:
86             if t.type_ == 'human':
87                 target = (t.position[1][0] - offset[0],
88                           t.position[1][1] - offset[1])
89                 break
90         if target is not None:
91             try:
92                 offset_self_pos = (self.position[1][0] - offset[0],
93                                    self.position[1][1] - offset[1])
94                 target_dir = self.move_on_dijkstra_map(offset_self_pos,
95                                                        [target])
96                 if target_dir is not None:
97                     self.set_task('MOVE', (target_dir,))
98                     return True
99             except GameError:
100                 pass
101         return False
102
103     def hunt_food_satisfaction(self):
104         for id_ in self.inventory:
105             t = self.world.get_thing(id_)
106             if t.type_ == 'food':
107                 self.set_task('EAT', (id_,))
108                 return True
109         for id_ in self.get_pickable_items():
110             t = self.world.get_thing(id_)
111             if t.type_ == 'food':
112                 self.set_task('PICKUP', (id_,))
113                 return True
114         visible_things, offset = self.get_visible_things()
115         food_targets = []
116         for t in visible_things:
117             if t.type_ == 'food':
118                 food_targets += [(t.position[1][0] - offset[0],
119                                   t.position[1][1] - offset[1])]
120         offset_self_pos = (self.position[1][0] - offset[0],
121                            self.position[1][1] - offset[1])
122         target_dir = self.move_on_dijkstra_map(offset_self_pos,
123                                                food_targets)
124         if target_dir:
125             try:
126                 self.set_task('MOVE', (target_dir,))
127                 return True
128             except GameError:
129                 pass
130         return False
131
132     def decide_task(self):
133         #if not self.hunt_player():
134         if not self.hunt_food_satisfaction():
135             self.set_task('WAIT')
136
137     def set_task(self, task_name, args=()):
138         task_class = self.world.game.tasks[task_name]
139         self.task = task_class(self, args)
140         self.task.check()  # will throw GameError if necessary
141
142     def proceed(self, is_AI=True):
143         """Further the thing in its tasks, decrease its health.
144
145         First, ensures an empty map, decrements .health and kills
146         thing if crossing zero (removes from self.world.things for AI
147         thing, or unsets self.world.player_is_alive for player thing);
148         then checks that self.task is still possible and aborts if
149         otherwise (for AI things, decides a new task).
150
151         Then decrements .task.todo; if it thus falls to <= 0, enacts
152         method whose name is 'task_' + self.task.name and sets .task =
153         None. If is_AI, calls .decide_task to decide a self.task.
154
155         """
156         self.unset_surroundings()
157         self.health -= 1
158         if self.health <= 0:
159             if self is self.world.player:
160                 self.world.player_is_alive = False
161             else:
162                 del self.world.things[self.world.things.index(self)]
163             return
164         try:
165             self.task.check()
166         except GameError as e:
167             self.task = None
168             self._last_task_result = e
169             if is_AI:
170                 try:
171                     self.decide_task()
172                 except GameError:
173                     self.set_task('WAIT')
174             return
175         self.task.todo -= 1
176         if self.task.todo <= 0:
177             self._last_task_result = self.task.do()
178             self.task = None
179         if is_AI and self.task is None:
180             try:
181                 self.decide_task()
182             except GameError:
183                 self.set_task('WAIT')
184
185     def unset_surroundings(self):
186         self._stencil = None
187         self._surrounding_map = None
188         self._surroundings_offset = None
189
190     def must_fix_indentation(self):
191         return self._radius % 2 != self.position[1][0] % 2
192
193     def get_surroundings_offset(self):
194         if self._surroundings_offset is not None:
195             return self._surroundings_offset
196         add_line = self.must_fix_indentation()
197         offset_y = self.position[1][0] - self._radius - int(add_line)
198         offset_x = self.position[1][1] - self._radius
199         self._surroundings_offset = (offset_y, offset_x)
200         return self._surroundings_offset
201
202     def get_surrounding_map(self):
203         if self._surrounding_map is not None:
204             return self._surrounding_map
205         offset = self.get_surroundings_offset()
206         add_line = self.must_fix_indentation()
207         self._surrounding_map = self.world.game.\
208                                 map_type(size=(self._radius*2+1+int(add_line),
209                                                self._radius*2+1))
210         for pos in self._surrounding_map:
211             offset_pos = (pos[0] + offset[0], pos[1] + offset[1])
212             if offset_pos[0] >= 0 and \
213                offset_pos[0] < self.world.maps[(0,0)].size[0] and \
214                offset_pos[1] >= 0 and \
215                offset_pos[1] < self.world.maps[(0,0)].size[1]:
216                 self._surrounding_map[pos] = self.world.maps[(0,0)][offset_pos]
217         return self._surrounding_map
218
219     def get_stencil(self):
220         if self._stencil is not None:
221             return self._stencil
222         m = self.get_surrounding_map()
223         offset = self.get_surroundings_offset()
224         fov_center = (self.position[1][0] - offset[0],
225                       self.position[1][1] - offset[1])
226         self._stencil = m.get_fov_map(fov_center)
227         return self._stencil
228
229     def get_visible_map(self):
230         stencil = self.get_stencil()
231         m = self.get_surrounding_map().new_from_shape(' ')
232         for pos in m:
233             if stencil[pos] == '.':
234                 m[pos] = self._surrounding_map[pos]
235         return m
236
237     def get_visible_things(self):
238         stencil = self.get_stencil()
239         offset = self.get_surroundings_offset()
240         visible_things = []
241         for thing in self.world.things:
242             if abs(thing.position[1][0] - self.position[1][0]) > self._radius or\
243                abs(thing.position[1][1] - self.position[1][1]) > self._radius:
244                 continue
245             offset_pos = (thing.position[1][0] - offset[0],
246                           thing.position[1][1] - offset[1])
247             if (not thing.in_inventory) and stencil[offset_pos] == '.':
248                 visible_things += [thing]
249         return visible_things, offset
250
251     def get_pickable_items(self):
252         pickable_ids = []
253         visible_things, _ = self.get_visible_things()
254         for t in [t for t in visible_things if
255                   isinstance(t, ThingItem) and
256                   (t.position == self.position or
257                    t.position[1] in
258                    self.world.maps[(0,0)].get_neighbors(self.position[1]).values())]:
259             pickable_ids += [t.id_]
260         return pickable_ids
261
262
263
264 class ThingHuman(ThingAnimate):
265     type_ = 'human'
266     health = 100
267
268
269
270 class ThingMonster(ThingAnimate):
271     type_ = 'monster'
272     health = 50