behaviour.py 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444
  1. # coding: utf-8
  2. import typing
  3. from random import choice
  4. from sandbox.engulf.const import COLLECTION_GRASS, COLLECTION_CELL, COLLECTION_ALIVE, COLLECTION_PREY
  5. from sandbox.engulf.exceptions import NotFoundWhereToGo
  6. from synergine2.simulation import Event
  7. from synergine2.simulation import SubjectBehaviour, SimulationMechanism, SimulationBehaviour, SubjectBehaviourSelector
  8. from synergine2.utils import ChunkManager
  9. from synergine2_xyz.xyz import DIRECTIONS, DIRECTION_SLIGHTLY, \
  10. get_direction_from_north_degree
  11. from synergine2_xyz.mechanism import ProximitySubjectMechanism
  12. from synergine2_xyz.utils import get_around_positions_of_positions, get_position_for_direction
  13. # Import for typing hint
  14. if False:
  15. from sandbox.engulf.subject import Grass
  16. from sandbox.engulf.subject import PreyCell
  17. class GrassGrownUp(Event):
  18. def __init__(self, subject_id, density, *args, **kwargs):
  19. super().__init__(*args, **kwargs)
  20. self.subject_id = subject_id
  21. self.density = density
  22. def repr_debug(self) -> str:
  23. return '{}: subject_id:{}, density:{}'.format(
  24. self.__class__.__name__,
  25. self.subject_id,
  26. self.density,
  27. )
  28. class GrassSpawn(Event):
  29. def __init__(self, subject_id, position, density, *args, **kwargs):
  30. super().__init__(*args, **kwargs)
  31. self.subject_id = subject_id
  32. self.position = position
  33. self.density = density
  34. def repr_debug(self) -> str:
  35. return '{}: subject_id:{}, position:{}, density:{}'.format(
  36. self.__class__.__name__,
  37. self.subject_id,
  38. self.position,
  39. self.density,
  40. )
  41. class GrassSpotablePositionsMechanism(SimulationMechanism):
  42. parallelizable = True
  43. def run(self, process_number: int=None, process_count: int=None):
  44. chunk_manager = ChunkManager(process_count)
  45. positions = list(self.simulation.subjects.grass_xyz.keys())
  46. positions_chunks = chunk_manager.make_chunks(positions)
  47. spotables = []
  48. for position in positions_chunks[process_number]:
  49. arounds = get_around_positions_of_positions(position)
  50. from_subject = self.simulation.subjects.grass_xyz[position][0]
  51. around_data = {
  52. 'from_subject': from_subject,
  53. 'around': [],
  54. }
  55. for around in arounds:
  56. if around not in self.simulation.subjects.grass_xyz and self.simulation.is_possible_position(around):
  57. around_data['around'].append(around)
  58. if around_data['around']:
  59. spotables.append(around_data)
  60. return spotables
  61. class GrowUp(SubjectBehaviour):
  62. frequency = 20
  63. def run(self, data):
  64. return True
  65. def action(self, data) -> [Event]:
  66. self.subject.density += 1
  67. return [GrassGrownUp(
  68. self.subject.id,
  69. self.subject.density,
  70. )]
  71. class GrassSpawnBehaviour(SimulationBehaviour):
  72. frequency = 100
  73. use = [GrassSpotablePositionsMechanism]
  74. def run(self, data):
  75. spawns = []
  76. for around_data in data[GrassSpotablePositionsMechanism]:
  77. from_subject = around_data['from_subject']
  78. arounds = around_data['around']
  79. if from_subject.density >= 40:
  80. spawns.extend(arounds)
  81. return spawns
  82. @classmethod
  83. def merge_data(cls, new_data, start_data=None):
  84. start_data = start_data or []
  85. start_data.extend(new_data)
  86. return start_data
  87. def action(self, data) -> [Event]:
  88. from sandbox.engulf.subject import Grass # cyclic
  89. events = []
  90. for position in data:
  91. if position not in list(self.simulation.subjects.grass_xyz.keys()):
  92. new_grass = Grass(
  93. config=self.config,
  94. simulation=self.simulation,
  95. position=position,
  96. density=20,
  97. )
  98. self.simulation.subjects.append(new_grass)
  99. events.append(GrassSpawn(
  100. new_grass.id,
  101. new_grass.position,
  102. new_grass.density,
  103. ))
  104. return events
  105. class GrassEatableDirectProximityMechanism(ProximitySubjectMechanism):
  106. distance = 1.41 # distance when on angle
  107. feel_collections = [COLLECTION_GRASS]
  108. def acceptable_subject(self, subject: 'Grass') -> bool:
  109. return subject.density >= self.config.simulation.eat_grass_required_density
  110. class PreyEatableDirectProximityMechanism(ProximitySubjectMechanism):
  111. distance = 1.41 # distance when on angle
  112. feel_collections = [COLLECTION_CELL]
  113. def acceptable_subject(self, subject: 'PreyCell') -> bool:
  114. # TODO: N'attaquer que des "herbivores" ?
  115. from sandbox.engulf.subject import PreyCell # cyclic
  116. return isinstance(subject, PreyCell)
  117. class MoveTo(Event):
  118. def __init__(self, subject_id: int, position: tuple, *args, **kwargs):
  119. super().__init__(*args, **kwargs)
  120. self.subject_id = subject_id
  121. self.position = position
  122. def repr_debug(self) -> str:
  123. return '{}: subject_id:{}, position:{}'.format(
  124. type(self).__name__,
  125. self.subject_id,
  126. self.position,
  127. )
  128. class EatEvent(Event):
  129. def __init__(self, eaten_id: int, eater_id: int, eaten_new_density: float, *args, **kwargs):
  130. super().__init__(*args, **kwargs)
  131. self.eaten_id = eaten_id
  132. self.eater_id = eater_id
  133. self.eaten_new_density = eaten_new_density
  134. class EatenEvent(Event):
  135. def __init__(self, eaten_id: int, *args, **kwargs):
  136. super().__init__(*args, **kwargs)
  137. self.eaten_id = eaten_id
  138. class AttackEvent(Event):
  139. def __init__(self, attacker_id: int, attacked_id: int, *args, **kwargs):
  140. super().__init__(*args, **kwargs)
  141. self.attacker_id = attacker_id
  142. self.attacked_id = attacked_id
  143. class SearchFood(SubjectBehaviour):
  144. mechanism_data_class = NotImplemented
  145. def get_required_appetite(self) -> float:
  146. raise NotImplementedError()
  147. def run(self, data):
  148. if self.subject.appetite < self.get_required_appetite():
  149. return False
  150. if not data[self.mechanism_data_class]:
  151. return False
  152. direction_degrees = [d['direction'] for d in data[self.mechanism_data_class]]
  153. return get_direction_from_north_degree(choice(direction_degrees))
  154. def action(self, data) -> [Event]:
  155. direction = data
  156. position = get_position_for_direction(self.subject.position, direction)
  157. self.subject.position = position
  158. self.subject.previous_direction = direction
  159. return [MoveTo(self.subject.id, position)]
  160. class SearchGrass(SearchFood):
  161. """
  162. Si une nourriture a une case de distance et cellule non rassasié, move dans sa direction.
  163. """
  164. use = [GrassEatableDirectProximityMechanism]
  165. mechanism_data_class = use[0]
  166. def get_required_appetite(self) -> float:
  167. return self.config.simulation.search_food_appetite_required
  168. class SearchPrey(SearchFood):
  169. """
  170. Si une nourriture a une case de distance et cellule non rassasié, move dans sa direction.
  171. """
  172. use = [PreyEatableDirectProximityMechanism]
  173. mechanism_data_class = use[0]
  174. def get_required_appetite(self) -> float:
  175. return self.config.simulation.search_and_attack_prey_apetite_required
  176. class EatGrass(SubjectBehaviour):
  177. def run(self, data):
  178. if self.subject.appetite < self.config.simulation.eat_grass_required_density:
  179. return False
  180. for grass in self.simulation.collections.get(COLLECTION_GRASS, []):
  181. # TODO: Use simulation/xyz pre calculated indexes
  182. if grass.position == self.subject.position:
  183. return grass.id
  184. def action(self, data) -> [Event]:
  185. subject_id = data
  186. grass = self.simulation.subjects.index[subject_id] # TODO: cas ou grass disparu ?
  187. grass.density -= self.config.simulation.eat_grass_density_reduction
  188. self.subject.appetite -= self.config.simulation.eat_grass_appetite_reduction
  189. # TODO: Comment mettre des logs (ne peuvent pas être passé aux subprocess)?
  190. return [EatEvent(
  191. eater_id=self.subject.id,
  192. eaten_id=subject_id,
  193. eaten_new_density=grass.density,
  194. )]
  195. class EatPrey(SubjectBehaviour):
  196. def run(self, data):
  197. if self.subject.appetite < self.config.simulation.search_and_attack_prey_apetite_required:
  198. return False
  199. for prey in self.simulation.collections.get(COLLECTION_PREY, []):
  200. # TODO: Use simulation/xyz pre calculated indexes
  201. if prey.position == self.subject.position:
  202. return prey.id
  203. def action(self, data) -> [Event]:
  204. subject_id = data
  205. # Cell already eaten ?
  206. if subject_id not in self.simulation.subjects.index:
  207. return []
  208. prey = self.simulation.subjects.index[subject_id]
  209. self.simulation.subjects.remove(prey)
  210. self.subject.appetite -= self.config.simulation.eat_prey_required_density
  211. return [EatenEvent(
  212. eaten_id=prey.id,
  213. )]
  214. class Explore(SubjectBehaviour):
  215. """
  216. Produit un mouvement au hasard (ou un immobilisme)
  217. """
  218. use = []
  219. def run(self, data):
  220. # TODO: Il faut pouvoir dire tel behaviour concerne que tel collections
  221. if COLLECTION_ALIVE not in self.subject.collections:
  222. return False
  223. return True
  224. def action(self, data) -> [Event]:
  225. try:
  226. position, direction = self.get_random_position_and_direction()
  227. self.subject.position = position
  228. self.subject.previous_direction = direction
  229. return [MoveTo(self.subject.id, position)]
  230. except NotFoundWhereToGo:
  231. return []
  232. def get_random_position_and_direction(self) -> tuple:
  233. attempts = 0
  234. while attempts <= 5:
  235. attempts += 1
  236. direction = self.get_random_direction()
  237. position = get_position_for_direction(self.subject.position, direction)
  238. if self.simulation.is_possible_subject_position(self.subject, position):
  239. return position, direction
  240. # If blocked, permit any direction (not slightly)
  241. if attempts >= 3:
  242. self.subject.previous_direction = None
  243. raise NotFoundWhereToGo()
  244. def get_random_direction(self):
  245. if not self.subject.previous_direction:
  246. return choice(DIRECTIONS)
  247. return choice(DIRECTION_SLIGHTLY[self.subject.previous_direction])
  248. class Hungry(SubjectBehaviour):
  249. def run(self, data):
  250. return True
  251. def action(self, data) -> [Event]:
  252. self.subject.appetite += self.config.simulation.hungry_reduction
  253. return []
  254. def get_random_direction(self):
  255. if not self.subject.previous_direction:
  256. return choice(DIRECTIONS)
  257. return choice(DIRECTION_SLIGHTLY[self.subject.previous_direction])
  258. class Attack(SubjectBehaviour):
  259. use = [PreyEatableDirectProximityMechanism]
  260. def run(self, data):
  261. if self.subject.appetite < self.config.simulation.search_and_attack_prey_apetite_required:
  262. return False
  263. eatable_datas = data[PreyEatableDirectProximityMechanism]
  264. attackable_datas = []
  265. for eatable_data in eatable_datas:
  266. if COLLECTION_ALIVE in eatable_data['subject'].collections:
  267. attackable_datas.append(eatable_data)
  268. if attackable_datas:
  269. return choice(attackable_datas)
  270. return False
  271. def action(self, data) -> [Event]:
  272. attacked = self.simulation.subjects.index[data['subject'].id]
  273. try:
  274. # TODO il faut automatiser/Refactoriser le fait de retirer/ajouter un collection
  275. self.simulation.collections[COLLECTION_ALIVE].remove(attacked)
  276. attacked.collections.remove(COLLECTION_ALIVE)
  277. except ValueError:
  278. pass # On considere qu'il a ete tué par une autre, TODO: en être sur ?
  279. return [AttackEvent(attacker_id=self.subject.id, attacked_id=data['subject'].id)]
  280. class CellBehaviourSelector(SubjectBehaviourSelector):
  281. # If behaviour in sublist, only one be kept in sublist
  282. behaviour_hierarchy = ( # TODO: refact it
  283. (
  284. EatPrey,
  285. EatGrass, # TODO: Introduce priority with appetite
  286. Attack,
  287. SearchPrey,
  288. SearchGrass, # TODO: Introduce priority with appetite
  289. Explore,
  290. ),
  291. )
  292. def reduce_behaviours(
  293. self,
  294. behaviours: typing.Dict[typing.Type[SubjectBehaviour], object],
  295. ) -> typing.Dict[typing.Type[SubjectBehaviour], object]:
  296. reduced_behaviours = {} # type: typing.Dict[typing.Type[SubjectBehaviour], object]
  297. for behaviour_class, behaviour_data in behaviours.items():
  298. if not self.behaviour_class_in_sublist(behaviour_class):
  299. reduced_behaviours[behaviour_class] = behaviour_data
  300. elif self.behaviour_class_is_prior(behaviour_class, behaviours):
  301. reduced_behaviours[behaviour_class] = behaviour_data
  302. return reduced_behaviours
  303. def behaviour_class_in_sublist(self, behaviour_class: typing.Type[SubjectBehaviour]) -> bool:
  304. for sublist in self.behaviour_hierarchy:
  305. if behaviour_class in sublist:
  306. return True
  307. return False
  308. def behaviour_class_is_prior(
  309. self,
  310. behaviour_class: typing.Type[SubjectBehaviour],
  311. behaviours: typing.Dict[typing.Type[SubjectBehaviour], object],
  312. ) -> bool:
  313. for sublist in self.behaviour_hierarchy:
  314. if behaviour_class in sublist:
  315. behaviour_position = sublist.index(behaviour_class)
  316. other_behaviour_top_position = self.get_other_behaviour_top_position(
  317. behaviour_class,
  318. behaviours,
  319. sublist,
  320. )
  321. if other_behaviour_top_position is not None and behaviour_position > other_behaviour_top_position:
  322. return False
  323. return True
  324. def get_other_behaviour_top_position(
  325. self,
  326. exclude_behaviour_class,
  327. behaviours,
  328. sublist,
  329. ) -> typing.Union[None, int]:
  330. position = None
  331. for behaviour_class in behaviours.keys():
  332. if behaviour_class != exclude_behaviour_class:
  333. try:
  334. behaviour_position = sublist.index(behaviour_class)
  335. if position is None or behaviour_position < position:
  336. position = behaviour_position
  337. except ValueError:
  338. pass
  339. return position