behaviour.py 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366
  1. # coding: utf-8
  2. import typing
  3. from random import choice
  4. from sandbox.engulf.const import COLLECTION_GRASS, COLLECTION_CELL
  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]
  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 AttackEvent(Event):
  133. def __init__(self, attacker_id: int, attacked_id: int, *args, **kwargs):
  134. super().__init__(*args, **kwargs)
  135. self.attacker_id = attacker_id
  136. self.attacked_id = attacked_id
  137. class SearchGrass(SubjectBehaviour):
  138. """
  139. Si une nourriture a une case de distance et cellule non rassasié, move dans sa direction.
  140. """
  141. use = [GrassEatableDirectProximityMechanism]
  142. def run(self, data):
  143. if self.subject.appetite < self.config.simulation.search_food_appetite_required:
  144. return False
  145. if not data[GrassEatableDirectProximityMechanism]:
  146. return False
  147. direction_degrees = [d['direction'] for d in data[GrassEatableDirectProximityMechanism]]
  148. return get_direction_from_north_degree(choice(direction_degrees))
  149. def action(self, data) -> [Event]:
  150. direction = data
  151. position = get_position_for_direction(self.subject.position, direction)
  152. self.subject.position = position
  153. self.subject.previous_direction = direction
  154. return [MoveTo(self.subject.id, position)]
  155. class EatGrass(SubjectBehaviour):
  156. """
  157. Prduit un immobilisme si sur une case de nourriture, dans le cas ou la cellule n'est as rassasié.
  158. """
  159. def run(self, data):
  160. if self.subject.appetite < self.config.simulation.eat_grass_required_density:
  161. return False
  162. for grass in self.simulation.collections.get(COLLECTION_GRASS, []):
  163. # TODO: Use simulation/xyz pre calculated indexes
  164. if grass.position == self.subject.position:
  165. return grass.id
  166. def action(self, data) -> [Event]:
  167. subject_id = data
  168. grass = self.simulation.subjects.index[subject_id] # TODO: cas ou grass disparu ?
  169. grass.density -= self.config.simulation.eat_grass_density_reduction
  170. self.subject.appetite -= self.config.simulation.eat_grass_appetite_reduction
  171. # TODO: Comment mettre des logs (ne peuvent pas être passé aux subprocess)?
  172. return [EatEvent(
  173. eater_id=self.subject.id,
  174. eaten_id=subject_id,
  175. eaten_new_density=grass.density,
  176. )]
  177. class Explore(SubjectBehaviour):
  178. """
  179. Produit un mouvement au hasard (ou un immobilisme)
  180. """
  181. use = []
  182. def run(self, data):
  183. return True # for now, want move every time
  184. def action(self, data) -> [Event]:
  185. try:
  186. position, direction = self.get_random_position_and_direction()
  187. self.subject.position = position
  188. self.subject.previous_direction = direction
  189. return [MoveTo(self.subject.id, position)]
  190. except NotFoundWhereToGo:
  191. return []
  192. def get_random_position_and_direction(self) -> tuple:
  193. attempts = 0
  194. while attempts <= 5:
  195. attempts += 1
  196. direction = self.get_random_direction()
  197. position = get_position_for_direction(self.subject.position, direction)
  198. if self.simulation.is_possible_subject_position(self.subject, position):
  199. return position, direction
  200. # If blocked, permit any direction (not slightly)
  201. if attempts >= 3:
  202. self.subject.previous_direction = None
  203. raise NotFoundWhereToGo()
  204. def get_random_direction(self):
  205. if not self.subject.previous_direction:
  206. return choice(DIRECTIONS)
  207. return choice(DIRECTION_SLIGHTLY[self.subject.previous_direction])
  208. class Hungry(SubjectBehaviour):
  209. def run(self, data):
  210. return True
  211. def action(self, data) -> [Event]:
  212. self.subject.appetite += self.config.simulation.hungry_reduction
  213. return []
  214. def get_random_direction(self):
  215. if not self.subject.previous_direction:
  216. return choice(DIRECTIONS)
  217. return choice(DIRECTION_SLIGHTLY[self.subject.previous_direction])
  218. class Attack(SubjectBehaviour):
  219. use = [PreyEatableDirectProximityMechanism]
  220. def run(self, data):
  221. if data[PreyEatableDirectProximityMechanism]:
  222. return choice(data[PreyEatableDirectProximityMechanism])
  223. return False
  224. def action(self, data) -> [Event]:
  225. # TODO: Dommages / mort
  226. return [AttackEvent(attacker_id=self.subject.id, attacked_id=data['subject'].id)]
  227. class CellBehaviourSelector(SubjectBehaviourSelector):
  228. # If behaviour in sublist, only one be kept in sublist
  229. behaviour_hierarchy = ( # TODO: refact it
  230. (
  231. EatGrass, # TODO: Introduce priority with appetite
  232. SearchGrass, # TODO: Introduce priority with appetite
  233. Explore,
  234. ),
  235. )
  236. def reduce_behaviours(
  237. self,
  238. behaviours: typing.Dict[typing.Type[SubjectBehaviour], object],
  239. ) -> typing.Dict[typing.Type[SubjectBehaviour], object]:
  240. reduced_behaviours = {} # type: typing.Dict[typing.Type[SubjectBehaviour], object]
  241. for behaviour_class, behaviour_data in behaviours.items():
  242. if not self.behaviour_class_in_sublist(behaviour_class):
  243. reduced_behaviours[behaviour_class] = behaviour_data
  244. elif self.behaviour_class_is_prior(behaviour_class, behaviours):
  245. reduced_behaviours[behaviour_class] = behaviour_data
  246. return reduced_behaviours
  247. def behaviour_class_in_sublist(self, behaviour_class: typing.Type[SubjectBehaviour]) -> bool:
  248. for sublist in self.behaviour_hierarchy:
  249. if behaviour_class in sublist:
  250. return True
  251. return False
  252. def behaviour_class_is_prior(
  253. self,
  254. behaviour_class: typing.Type[SubjectBehaviour],
  255. behaviours: typing.Dict[typing.Type[SubjectBehaviour], object],
  256. ) -> bool:
  257. for sublist in self.behaviour_hierarchy:
  258. if behaviour_class in sublist:
  259. behaviour_position = sublist.index(behaviour_class)
  260. other_behaviour_top_position = self.get_other_behaviour_top_position(
  261. behaviour_class,
  262. behaviours,
  263. sublist,
  264. )
  265. if other_behaviour_top_position is not None and behaviour_position > other_behaviour_top_position:
  266. return False
  267. return True
  268. def get_other_behaviour_top_position(
  269. self,
  270. exclude_behaviour_class,
  271. behaviours,
  272. sublist,
  273. ) -> typing.Union[None, int]:
  274. position = None
  275. for behaviour_class in behaviours.keys():
  276. if behaviour_class != exclude_behaviour_class:
  277. try:
  278. behaviour_position = sublist.index(behaviour_class)
  279. if position is None or behaviour_position < position:
  280. position = behaviour_position
  281. except ValueError:
  282. pass
  283. return position