behaviour.py 11KB

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