1 from plomrogue.errors import GameError
2 from plomrogue.mapping import YX
9 def __init__(self, world, id_=None, position=(YX(0,0), YX(0,0))):
12 self.id_ = self.world.new_thing_id()
15 self.position = position
21 def _position_set(self, pos):
22 """Set self._position to pos.
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
34 def position(self, pos):
35 self._position_set(pos)
39 class Thing(ThingBase):
43 def __init__(self, *args, **kwargs):
46 super().__init__(*args, **kwargs)
51 def _position_set(self, pos):
52 super()._position_set(pos)
53 for t_id in self.inventory:
54 t = self.world.get_thing(t_id)
55 t.position = self.position
56 if not self.id_ == self.world.player_id:
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
63 self.world.get_map(self.position[0] - YX(1,-1))
64 self.world.get_map(self.position[0] - YX(0,-1))
65 self.world.get_map(self.position[0] - YX(-1,-1))
66 if edge_right >= self.world.map_size.x:
67 self.world.get_map(self.position[0] + YX(1,1))
68 self.world.get_map(self.position[0] + YX(0,1))
69 self.world.get_map(self.position[0] + YX(-1,1))
71 self.world.get_map(self.position[0] - YX(-1,1))
72 self.world.get_map(self.position[0] - YX(-1,0))
73 self.world.get_map(self.position[0] - YX(-1,-1))
74 if edge_down >= self.world.map_size.y:
75 self.world.get_map(self.position[0] + YX(1,1))
76 self.world.get_map(self.position[0] + YX(1,0))
77 self.world.get_map(self.position[0] + YX(1,-1))
81 class ThingItem(Thing):
86 class ThingFood(ThingItem):
91 class ThingAnimate(Thing):
94 def __init__(self, *args, **kwargs):
95 super().__init__(*args, **kwargs)
97 self._last_task_result = None
98 self.unset_surroundings()
100 def move_on_dijkstra_map(self, own_pos, targets):
101 visible_map = self.get_visible_map()
102 dijkstra_map = self.world.game.map_type(visible_map.size)
104 dijkstra_map.terrain = [n_max for i in range(dijkstra_map.size_i)]
105 for target in targets:
106 dijkstra_map[target] = 0
110 for pos in dijkstra_map:
111 if visible_map[pos] != '.':
113 neighbors = dijkstra_map.get_neighbors(pos)
114 for direction in neighbors:
115 yx = neighbors[direction]
116 if yx is not None and dijkstra_map[yx] < dijkstra_map[pos] - 1:
117 dijkstra_map[pos] = dijkstra_map[yx] + 1
119 neighbors = dijkstra_map.get_neighbors(own_pos)
121 target_direction = None
122 for direction in sorted(neighbors.keys()):
123 yx = neighbors[direction]
125 n_new = dijkstra_map[yx]
128 target_direction = direction
129 return target_direction
131 def hunt_player(self):
132 visible_things = self.get_visible_things()
133 offset = self.get_surroundings_offset()
135 for t in visible_things:
136 if t.type_ == 'human':
137 target = t.position[1] - offset
139 if target is not None:
141 offset_self_pos = self.position[1] - offset
142 target_dir = self.move_on_dijkstra_map(offset_self_pos,
144 if target_dir is not None:
145 self.set_task('MOVE', (target_dir,))
151 def hunt_food_satisfaction(self):
152 for id_ in self.inventory:
153 t = self.world.get_thing(id_)
154 if t.type_ == 'food':
155 self.set_task('EAT', (id_,))
157 for id_ in self.get_pickable_items():
158 t = self.world.get_thing(id_)
159 if t.type_ == 'food':
160 self.set_task('PICKUP', (id_,))
162 visible_things = self.get_visible_things()
163 offset = self.get_surroundings_offset()
165 for t in visible_things:
166 if t.type_ == 'food':
167 food_targets += [t.position[1] - offset]
168 offset_self_pos = self.position[1] - offset
169 target_dir = self.move_on_dijkstra_map(offset_self_pos,
173 self.set_task('MOVE', (target_dir,))
179 def decide_task(self):
180 #if not self.hunt_player():
181 if not self.hunt_food_satisfaction():
182 self.set_task('WAIT')
184 def set_task(self, task_name, args=()):
185 task_class = self.world.game.tasks[task_name]
186 self.task = task_class(self, args)
187 self.task.check() # will throw GameError if necessary
189 def proceed(self, is_AI=True):
190 """Further the thing in its tasks, decrease its health.
192 First, ensures an empty map, decrements .health and kills
193 thing if crossing zero (removes from self.world.things for AI
194 thing, or unsets self.world.player_is_alive for player thing);
195 then checks that self.task is still possible and aborts if
196 otherwise (for AI things, decides a new task).
198 Then decrements .task.todo; if it thus falls to <= 0, enacts
199 method whose name is 'task_' + self.task.name and sets .task =
200 None. If is_AI, calls .decide_task to decide a self.task.
203 self.unset_surroundings()
206 if self is self.world.player:
207 self.world.player_is_alive = False
209 del self.world.things[self.world.things.index(self)]
213 except GameError as e:
215 self._last_task_result = e
220 self.set_task('WAIT')
223 if self.task.todo <= 0:
224 self._last_task_result = self.task.do()
226 if is_AI and self.task is None:
230 self.set_task('WAIT')
232 def unset_surroundings(self):
234 self._surrounding_map = None
235 self._surroundings_offset = None
237 def must_fix_indentation(self):
238 return self._radius % 2 != self.position[1].y % 2
240 def get_surroundings_offset(self):
241 if self._surroundings_offset is not None:
242 return self._surroundings_offset
243 add_line = self.must_fix_indentation()
244 offset = YX(self.position[1].y - self._radius - int(add_line),
245 self.position[1].x - self._radius)
246 self._surroundings_offset = offset
247 return self._surroundings_offset
249 def get_surrounding_map(self):
250 if self._surrounding_map is not None:
251 return self._surrounding_map
253 def pan_and_scan(size_of_axis, pos, offset):
255 small_pos = pos + offset
258 small_pos = size_of_axis + small_pos
259 elif small_pos >= size_of_axis:
261 small_pos = small_pos - size_of_axis
262 return big_pos, small_pos
264 add_line = self.must_fix_indentation()
265 self._surrounding_map = self.world.game.\
266 map_type(size=YX(self._radius*2+1+int(add_line),
268 size = self.world.map_size
269 offset = self.get_surroundings_offset()
270 for pos in self._surrounding_map:
271 big_y, small_y = pan_and_scan(size.y, pos.y, offset.y)
272 big_x, small_x = pan_and_scan(size.x, pos.x, offset.x)
273 big_yx = YX(big_y, big_x)
274 small_yx = YX(small_y, small_x)
275 map_ = self.world.get_map(big_yx, False)
277 map_ = self.world.game.map_type(size=self.world.map_size)
278 self._surrounding_map[pos] = map_[small_yx]
279 return self._surrounding_map
281 def get_stencil(self):
282 if self._stencil is not None:
284 surrounding_map = self.get_surrounding_map()
285 m = surrounding_map.new_from_shape(' ')
286 for pos in surrounding_map:
287 if surrounding_map[pos] in {'.', '~'}:
289 offset = self.get_surroundings_offset()
290 fov_center = self.position[1] - offset
291 self._stencil = m.get_fov_map(fov_center)
294 def get_visible_map(self):
295 stencil = self.get_stencil()
296 m = self.get_surrounding_map().new_from_shape(' ')
298 if stencil[pos] == '.':
299 m[pos] = self._surrounding_map[pos]
302 def get_visible_things(self):
304 def calc_pos_in_fov(big_pos, small_pos, offset, size_of_axis):
305 pos = small_pos - offset
307 pos = small_pos - size_of_axis - offset
309 pos = small_pos + size_of_axis - offset
312 stencil = self.get_stencil()
313 offset = self.get_surroundings_offset()
315 size = self.world.map_size
316 fov_size = self.get_surrounding_map().size
317 for thing in self.world.things:
318 big_pos = thing.position[0]
319 small_pos = thing.position[1]
320 pos_y = calc_pos_in_fov(big_pos.y, small_pos.y, offset.y, size.y)
321 pos_x = calc_pos_in_fov(big_pos.x, small_pos.x, offset.x, size.x)
322 if pos_y < 0 or pos_x < 0 or\
323 pos_y >= fov_size.y or pos_x >= fov_size.x:
325 if (not thing.in_inventory) and stencil[YX(pos_y, pos_x)] == '.':
326 visible_things += [thing]
327 return visible_things
329 def get_pickable_items(self):
331 visible_things = self.get_visible_things()
332 for t in [t for t in visible_things if
333 isinstance(t, ThingItem) and
334 (t.position == self.position or
336 self.world.maps[YX(0,0)].get_neighbors(self.position[1]).values())]:
337 pickable_ids += [t.id_]
342 class ThingHuman(ThingAnimate):
348 class ThingMonster(ThingAnimate):