behaviour.py 15KB

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