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))):
11 self.position = position
13 self.id_ = self.world.new_thing_id()
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):
45 super().__init__(*args, **kwargs)
50 def _position_set(self, pos):
51 super()._position_set(pos)
52 for t_id in self.inventory:
53 t = self.world.get_thing(t_id)
54 t.position = self.position
58 class ThingItem(Thing):
63 class ThingFood(ThingItem):
68 class ThingAnimate(Thing):
71 def __init__(self, *args, **kwargs):
72 super().__init__(*args, **kwargs)
74 self._last_task_result = None
76 self.unset_surroundings()
78 def move_on_dijkstra_map(self, own_pos, targets):
79 visible_map = self.get_visible_map()
80 dijkstra_map = self.world.game.map_type(visible_map.size)
82 dijkstra_map.terrain = [n_max for i in range(dijkstra_map.size_i)]
83 for target in targets:
84 dijkstra_map[target] = 0
88 for pos in dijkstra_map:
89 if visible_map[pos] != '.':
91 neighbors = dijkstra_map.get_neighbors(pos)
92 for direction in neighbors:
93 yx = neighbors[direction]
94 if yx is not None and dijkstra_map[yx] < dijkstra_map[pos] - 1:
95 dijkstra_map[pos] = dijkstra_map[yx] + 1
97 neighbors = dijkstra_map.get_neighbors(own_pos)
99 target_direction = None
100 for direction in sorted(neighbors.keys()):
101 yx = neighbors[direction]
103 n_new = dijkstra_map[yx]
106 target_direction = direction
107 return target_direction
109 def hunt_player(self):
110 visible_things = self.get_visible_things()
111 offset = self.get_surroundings_offset()
113 for t in visible_things:
114 if t.type_ == 'human':
115 target = t.position[1] - offset
117 if target is not None:
119 offset_self_pos = self.position[1] - offset
120 target_dir = self.move_on_dijkstra_map(offset_self_pos,
122 if target_dir is not None:
123 self.set_task('MOVE', (target_dir,))
129 def hunt_food_satisfaction(self):
130 for id_ in self.inventory:
131 t = self.world.get_thing(id_)
132 if t.type_ == 'food':
133 self.set_task('EAT', (id_,))
135 for id_ in self.get_pickable_items():
136 t = self.world.get_thing(id_)
137 if t.type_ == 'food':
138 self.set_task('PICKUP', (id_,))
140 visible_things = self.get_visible_things()
141 offset = self.get_surroundings_offset()
143 for t in visible_things:
144 if t.type_ == 'food':
145 food_targets += [t.position[1] - offset]
146 offset_self_pos = self.position[1] - offset
147 target_dir = self.move_on_dijkstra_map(offset_self_pos,
151 self.set_task('MOVE', (target_dir,))
157 def decide_task(self):
158 #if not self.hunt_player():
159 if not self.hunt_food_satisfaction():
160 self.set_task('WAIT')
162 def set_task(self, task_name, args=()):
163 task_class = self.world.game.tasks[task_name]
164 self.task = task_class(self, args)
165 self.task.check() # will throw GameError if necessary
167 def proceed(self, is_AI=True):
168 """Further the thing in its tasks, decrease its health.
170 First, ensures an empty map, decrements .health and kills
171 thing if crossing zero (removes from self.world.things for AI
172 thing, or unsets self.world.player_is_alive for player thing);
173 then checks that self.task is still possible and aborts if
174 otherwise (for AI things, decides a new task).
176 Then decrements .task.todo; if it thus falls to <= 0, enacts
177 method whose name is 'task_' + self.task.name and sets .task =
178 None. If is_AI, calls .decide_task to decide a self.task.
181 self.unset_surroundings()
184 if self is self.world.player:
185 self.world.player_is_alive = False
187 del self.world.things[self.world.things.index(self)]
191 except GameError as e:
193 self._last_task_result = e
198 self.set_task('WAIT')
201 if self.task.todo <= 0:
202 self._last_task_result = self.task.do()
204 if is_AI and self.task is None:
208 self.set_task('WAIT')
210 def unset_surroundings(self):
212 self._surrounding_map = None
213 self._surroundings_offset = None
215 def must_fix_indentation(self):
216 return self._radius % 2 != self.position[1].y % 2
218 def get_surroundings_offset(self):
219 if self._surroundings_offset is not None:
220 return self._surroundings_offset
221 add_line = self.must_fix_indentation()
222 offset = YX(self.position[1].y - self._radius - int(add_line),
223 self.position[1].x - self._radius)
224 self._surroundings_offset = offset
225 return self._surroundings_offset
227 def get_surrounding_map(self):
228 if self._surrounding_map is not None:
229 return self._surrounding_map
231 def pan_and_scan(size_of_axis, pos, offset):
233 small_pos = pos + offset
236 small_pos = size_of_axis + small_pos
237 elif small_pos >= size_of_axis:
239 small_pos = small_pos - size_of_axis
240 return big_pos, small_pos
242 add_line = self.must_fix_indentation()
243 self._surrounding_map = self.world.game.\
244 map_type(size=YX(self._radius*2+1+int(add_line),
246 size = self.world.map_size
247 offset = self.get_surroundings_offset()
248 for pos in self._surrounding_map:
249 big_y, small_y = pan_and_scan(size.y, pos.y, offset.y)
250 big_x, small_x = pan_and_scan(size.x, pos.x, offset.x)
251 big_yx = YX(big_y, big_x)
252 small_yx = YX(small_y, small_x)
253 self._surrounding_map[pos] = self.world.maps[big_yx][small_yx]
254 return self._surrounding_map
256 def get_stencil(self):
257 if self._stencil is not None:
259 surrounding_map = self.get_surrounding_map()
260 m = surrounding_map.new_from_shape(' ')
261 for pos in surrounding_map:
262 if surrounding_map[pos] in {'.', '~'}:
264 offset = self.get_surroundings_offset()
265 fov_center = self.position[1] - offset
266 self._stencil = m.get_fov_map(fov_center)
269 def get_visible_map(self):
270 stencil = self.get_stencil()
271 m = self.get_surrounding_map().new_from_shape(' ')
273 if stencil[pos] == '.':
274 m[pos] = self._surrounding_map[pos]
277 def get_visible_things(self):
279 def calc_pos_in_fov(big_pos, small_pos, offset, size_of_axis):
280 pos = small_pos - offset
282 pos = small_pos - size_of_axis - offset
284 pos = small_pos + size_of_axis - offset
287 stencil = self.get_stencil()
288 offset = self.get_surroundings_offset()
290 size = self.world.map_size
291 fov_size = self.get_surrounding_map().size
292 for thing in self.world.things:
293 big_pos = thing.position[0]
294 small_pos = thing.position[1]
295 pos_y = calc_pos_in_fov(big_pos.y, small_pos.y, offset.y, size.y)
296 pos_x = calc_pos_in_fov(big_pos.x, small_pos.x, offset.x, size.x)
297 if pos_y < 0 or pos_x < 0 or\
298 pos_y >= fov_size.y or pos_x >= fov_size.x:
300 if (not thing.in_inventory) and stencil[YX(pos_y, pos_x)] == '.':
301 visible_things += [thing]
302 return visible_things
304 def get_pickable_items(self):
306 visible_things = self.get_visible_things()
307 for t in [t for t in visible_things if
308 isinstance(t, ThingItem) and
309 (t.position == self.position or
311 self.world.maps[YX(0,0)].get_neighbors(self.position[1]).values())]:
312 pickable_ids += [t.id_]
317 class ThingHuman(ThingAnimate):
323 class ThingMonster(ThingAnimate):