class NoMolecule(MoleculeException):
    pass class Molecule():

    def __init__(self, category, type, distance=None, intensity=0, cycle_age=None):
        self._category = category
        self._type = type
        self._distance = distance
        self._intensity = intensity
        self._cycle_age = cycle_age

    def get_category(self):
        return self._category

    def get_type(self):
        return self._type

    def get_distance(self):
        return self._distance

    def set_distance(self, distance):
        self._distance = distance

    def get_intensity(self):
        return self._intensity

    def get_cycle_age(self):
        return self._cycle_age

    def set_cycle_age(self, cycle_age):
        self._cycle_age = cycle_age

    def increment_intensity(self, increment_value):
        self._intensity += increment_value class MoleculeFlavour():

    @classmethod
    def new_from_raw_data(cls, raw_data):
        flavour = {}
        for category in raw_data:
            molecules_by_category = raw_data[category]
            for type in molecules_by_category:
                distance, intensity, cycle_age = molecules_by_category[type]
                if category not in flavour:
                    flavour[category] = {}
                flavour[category][type] = Molecule(category, type, distance, intensity, cycle_age)
        return cls(flavour)

    def get_raw_data(self):
        raw_data = {}
        for category in self._flavour:
            molecules_by_category = self._flavour[category]
            for type in molecules_by_category:
                molecule = molecules_by_category[type]
                if category not in raw_data:
                    raw_data[category] = {}
                if molecule.get_intensity() >= 0:
                    raw_data[category][type] = (molecule.get_distance(),
                                                molecule.get_intensity(),
                                                molecule.get_cycle_age())
        return raw_data

    def __init__(self, flavour):
        self._flavour = flavour

    def get_molecule(self, category, type):
        types = self.get_types(category)
        if type not in types:
            raise NoTypeInMolecule()
        return types[type]

    def get_molecules(self, category):
        """

        :param category:
        :return: Molecules dict or empty dict of no molecules
        """
        try:
            return self.get_types(category).values()
        except NoCategoryInMolecule:
            return {}

    def get_molecules_types(self, category):
        try:
            return self.get_types(category).keys()
        except NoCategoryInMolecule:
            return {}

    def get_types(self, category):
        if category not in self._flavour:
            raise NoCategoryInMolecule()
        return self._flavour[category]

    def set_molecule(self, molecule):
        category = molecule.get_category()
        type = molecule.get_type()

        if category not in self._flavour:
            self._flavour[category] = {}

        self._flavour[category][type] = molecule

    def remove_molecule(self, molecule):
        category = molecule.get_category()
        type = molecule.get_type()
        del(self._flavour[category][type]) class Base(unittest.TestCase):
    pass class DirectionMolecule():

    WAY_UP = 'u'
    WAY_DOWN = 'd'

    _positions_key = None

    @classmethod
    def appose(cls, context, point, molecule):
        context.molecules().increment_with_molecule(point, molecule, context.get_cycle())
        context.metas.list.add(MOLECULES, MOLECULES, point, assert_not_in=False)

    @classmethod
    def get_direction_for_point(cls, context, point, molecule_type, molecule_way=WAY_UP):
        flavour = context.molecules().get_flavour(point)
        molecule = flavour.get_molecule(category=MOLECULES_DIRECTION, type=molecule_type)

        distance = molecule.get_distance()
        around_molecule_filter = lambda around_molecule: around_molecule.get_distance() <= distance #  TODO <= ?
        if molecule_way == cls.WAY_DOWN:
            around_molecule_filter = lambda around_molecule: around_molecule.get_distance() >= distance
        around_molecules_points = cls._get_around_molecules(context, point, molecule_type,
                                                            molecule_filter=around_molecule_filter)

        if not around_molecules_points \
           or (len(around_molecules_points) == 1 and around_molecules_points[0][0] == point):
            raise NoMolecule()

        shuffle(around_molecules_points)
        around_molecules_sorted = sorted(around_molecules_points, key=lambda x: x[1].get_intensity(), reverse=True)
        max_intensity = around_molecules_sorted[0][1].get_intensity()

        around_molecules_max = []
        for around_molecule_sorted in around_molecules_sorted:
            if around_molecule_sorted[1].get_intensity() == max_intensity:
                around_molecules_max.append(around_molecule_sorted)

        reverse = False
        if molecule_way == cls.WAY_DOWN:
            reverse = True
        around_molecules_sorted_by_distance = sorted(around_molecules_max,
                                                      key=lambda x: x[1].get_distance(),
                                                      reverse=reverse)

        all_same_distance = True
        for mol in around_molecules_sorted_by_distance:
            mol_mol = mol[1]
            if mol_mol.get_distance() != distance:
                all_same_distance = False
                break

        if all_same_distance:
            raise NoMolecule()

        go_to_point = around_molecules_sorted_by_distance[0][0]

        direction_degrees = get_degree_from_north(point, go_to_point)
        direction = get_direction_for_degrees(direction_degrees)

        return direction

    @classmethod
    def _get_around_molecules(cls, context, reference_point, molecule_type,
                              molecule_filter=lambda around_molecule: True):
        around_points = context.get_around_points_of_point(reference_point)
        around_molecules_points = []
        for around_point in around_points:
            flavour = context.molecules().get_flavour(around_point)
            try:
                around_molecule = flavour.get_molecule(category=MOLECULES_DIRECTION, type=molecule_type)
                if molecule_filter(around_molecule):
                    around_molecules_points.append((around_point, around_molecule))
            except NoMolecule:
                pass  # No molecule, ok continue to sniff around

        return around_molecules_points

    @classmethod
    def get_best_molecule_direction_in(cls, context, reference_point, points, molecule_type):
        around_molecules_points = []
        for around_point in points:
            flavour = context.molecules().get_flavour(around_point)
            try:
                around_molecule = flavour.get_molecule(category=MOLECULES_DIRECTION, type=molecule_type)
                around_molecules_points.append((around_point, around_molecule))
            except NoMolecule:
                pass  # Ok, no molecule, continue to sniff around

        if not around_molecules_points \
           or (len(around_molecules_points) == 1 and around_molecules_points[0][0] == reference_point):
            raise NoMolecule()

        shuffle(around_molecules_points)
        around_molecules_sorted = sorted(around_molecules_points, key=lambda x: x[1].get_intensity(), reverse=True)
        go_to_point = around_molecules_sorted[0][0]

        direction_degrees = get_degree_from_north(reference_point, go_to_point)
        direction = get_direction_for_degrees(direction_degrees)

        return direction class Context(XyzContext):

    def __init__(self):
        super().__init__()
        self._molecules = MoleculesManager(self)

    def molecules(self):
        return self._molecules

    def position_is_penetrable(self, position):
        """

        Return True if position is empty or occuped by non physical impenetrable object.

        :param position:
        :return:
        """
        objects_ids_on_this_point = self.metas.list.get(POSITIONS, position, allow_empty=True)
        for object_id_on_this_point in objects_ids_on_this_point:
          if self.metas.states.have(object_id_on_this_point, IMPENETRABLE):
            return False
        return True

    def position_can_be_odorus(self, position):
        """

        Return True if position can smell

        :param position:
        :return:
        """
        return self.position_is_penetrable(position) MOLECULES_DIRECTION = IncrementedNamedInt.get('intelligine.mol.direction') PHEROMON_DIR_EXPLO = IncrementedNamedInt.get('intelligine.phero.direction.explo') PHEROMON_DIR_NONE = IncrementedNamedInt.get('intelligine.phero.direction.none') NORTH = 11 NORTH_EST = 12 EST = 15 SOUTH_EST = 18 SOUTH = 17 SOUTH_WEST = 16 WEST = 13 NORTH_WEST = 10 CENTER = 14 def get_position_with_direction_decal(direction=CENTER, point=(0, 0, 0)):
    z, x, y = point
    directions_modifier = directions_modifiers[direction]
    return z + directions_modifier[0], x + directions_modifier[1], y + directions_modifier[2]
from intelligine.core.exceptions import NoMolecule
from intelligine.simulation.molecule.Molecule import Molecule
from intelligine.simulation.molecule.MoleculeFlavour import MoleculeFlavour
from intelligine.tests.simulation.molecule.Base import Base
from intelligine.simulation.molecule.DirectionMolecule import DirectionMolecule
from intelligine.core.Context import Context
from intelligine.cst import MOLECULES_DIRECTION, PHEROMON_DIR_EXPLO, PHEROMON_DIR_NONE
from intelligine.synergy.event.move.direction import NORTH, NORTH_EST, EST, SOUTH_EST, SOUTH, SOUTH_WEST, WEST, \
    NORTH_WEST, CENTER
from intelligine.synergy.event.move.direction import get_position_with_direction_decal as _p
                _p(CENTER): {MOLECULES_DIRECTION: {PHEROMON_DIR_EXPLO: (10, 1, 0)}},
                _p(NORTH): {MOLECULES_DIRECTION: {PHEROMON_DIR_EXPLO: (9, 1, 0)}}
            },
            NORTH_EST: {
                _p(CENTER): {MOLECULES_DIRECTION: {PHEROMON_DIR_EXPLO: (10, 1, 0)}},
                _p(NORTH_EST): {MOLECULES_DIRECTION: {PHEROMON_DIR_EXPLO: (9, 1, 0)}}
            },
            WEST: {
                _p(CENTER): {MOLECULES_DIRECTION: {PHEROMON_DIR_EXPLO: (10, 1, 0)}},
                _p(WEST): {MOLECULES_DIRECTION: {PHEROMON_DIR_EXPLO: (9, 1, 0)}}
            },
            EST: {
                _p(CENTER): {MOLECULES_DIRECTION: {PHEROMON_DIR_EXPLO: (10, 1, 0)}},
                _p(EST): {MOLECULES_DIRECTION: {PHEROMON_DIR_EXPLO: (9, 1, 0)}}
            },
            SOUTH_WEST: {
                _p(CENTER): {MOLECULES_DIRECTION: {PHEROMON_DIR_EXPLO: (10, 1, 0)}},
                _p(SOUTH_WEST): {MOLECULES_DIRECTION: {PHEROMON_DIR_EXPLO: (9, 1, 0)}}
            },
            SOUTH: {
                _p(CENTER): {MOLECULES_DIRECTION: {PHEROMON_DIR_EXPLO: (10, 1, 0)}},
                _p(SOUTH): {MOLECULES_DIRECTION: {PHEROMON_DIR_EXPLO: (9, 1, 0)}}
            },
            SOUTH_EST: {
                _p(CENTER): {MOLECULES_DIRECTION: {PHEROMON_DIR_EXPLO: (10, 1, 0)}},
                _p(SOUTH_EST): {MOLECULES_DIRECTION: {PHEROMON_DIR_EXPLO: (9, 1, 0)}}
            }
        }

        for direction_wanted in test_data:
            self._test_direction_for_point(test_data[direction_wanted], direction_wanted)

    def test_route_with_multiple_same_intensity(self):
        """
        Test find route in middle of multiple molecules
        :return:
        """
        test_data = {
            NORTH_WEST: {
                _p(CENTER): {MOLECULES_DIRECTION: {PHEROMON_DIR_EXPLO: (10, 1, 0)}},
                _p(NORTH_WEST): {MOLECULES_DIRECTION: {PHEROMON_DIR_EXPLO: (9, 1, 0)}},
                _p(SOUTH_EST): {MOLECULES_DIRECTION: {PHEROMON_DIR_EXPLO: (12, 1, 0)}}
            },
            NORTH_WEST: {
                _p(CENTER): {MOLECULES_DIRECTION: {PHEROMON_DIR_EXPLO: (10, 1, 0)}},
                _p(NORTH_WEST): {MOLECULES_DIRECTION: {PHEROMON_DIR_EXPLO: (9, 1, 0)}},
                _p(SOUTH_EST): {MOLECULES_DIRECTION: {PHEROMON_DIR_EXPLO: (12, 1, 0)}},
                _p(SOUTH): {MOLECULES_DIRECTION: {PHEROMON_DIR_EXPLO: (12, 1, 0)}}
            },
            NORTH_WEST: {
                _p(CENTER): {MOLECULES_DIRECTION: {PHEROMON_DIR_EXPLO: (10, 1, 0)}},
                _p(NORTH_WEST): {MOLECULES_DIRECTION: {PHEROMON_DIR_EXPLO: (9, 1, 0)}},
                _p(SOUTH_EST): {MOLECULES_DIRECTION: {PHEROMON_DIR_EXPLO: (12, 1, 0)}},
                _p(SOUTH): {MOLECULES_DIRECTION: {PHEROMON_DIR_NONE: (8, 1, 0)}}
            },
        }

        for direction_wanted in test_data:
            self._test_direction_for_point(test_data[direction_wanted], direction_wanted)

    def test_route_with_multiple_different_intensity(self):
        """
        Test find route in middle of multiple molecules
        :return:
        """
        test_data = {
            NORTH_WEST: {
                _p(CENTER): {MOLECULES_DIRECTION: {PHEROMON_DIR_EXPLO: (10, 2, 0)}},
                _p(NORTH_WEST): {MOLECULES_DIRECTION: {PHEROMON_DIR_EXPLO: (9, 2, 0)}},
                _p(SOUTH_EST): {MOLECULES_DIRECTION: {PHEROMON_DIR_EXPLO: (8, 1, 0)}}
            },
            NORTH_WEST: {
                _p(CENTER): {MOLECULES_DIRECTION: {PHEROMON_DIR_EXPLO: (10, 2, 0)}},
                _p(NORTH_WEST): {MOLECULES_DIRECTION: {PHEROMON_DIR_EXPLO: (9, 2, 0)}},
                _p(SOUTH_EST): {MOLECULES_DIRECTION: {PHEROMON_DIR_EXPLO: (8, 1, 0)}},
                _p(SOUTH_EST): {MOLECULES_DIRECTION: {PHEROMON_DIR_NONE: (5, 10, 0)}}  # an other molecule type
            }
        }

        for direction_wanted in test_data:
            self._test_direction_for_point(test_data[direction_wanted], direction_wanted)

    def test_direction_direct(self):
        test_data = {
            NORTH: {
                _p(NORTH): {MOLECULES_DIRECTION: {PHEROMON_DIR_EXPLO: (9, 2, 0)}}
            },
            NORTH: {
                _p(NORTH): {MOLECULES_DIRECTION: {PHEROMON_DIR_EXPLO: (9, 2, 0)}},
                _p(NORTH_WEST): {MOLECULES_DIRECTION: {PHEROMON_DIR_NONE: (9, 500, 0)}}  # An other molecule type
            }
        }

        for direction in test_data:
            self._test_direction_for_points(test_data[direction], direction)

    def test_direction_with_multiple_intensity(self):
        test_data = {
            NORTH: {
                _p(NORTH): {MOLECULES_DIRECTION: {PHEROMON_DIR_EXPLO: (9, 5, 0)}},
                _p(SOUTH_EST): {MOLECULES_DIRECTION: {PHEROMON_DIR_EXPLO: (9, 4, 0)}},
                _p(NORTH_WEST): {MOLECULES_DIRECTION: {PHEROMON_DIR_EXPLO: (9, 4, 0)}}
            },
            NORTH: {
                _p(NORTH): {MOLECULES_DIRECTION: {PHEROMON_DIR_EXPLO: (9, 5, 0)}},
                _p(WEST): {MOLECULES_DIRECTION: {PHEROMON_DIR_NONE: (9, 500, 0)}},  # An other molecule_type
                _p(SOUTH_EST): {MOLECULES_DIRECTION: {PHEROMON_DIR_EXPLO: (9, 4, 0)}},
                _p(NORTH_WEST): {MOLECULES_DIRECTION: {PHEROMON_DIR_EXPLO: (9, 4, 0)}}
            }
        }

        for direction in test_data:
            self._test_direction_for_points(test_data[direction], direction)

    def test_no_molecules_around(self):
        # No molecule
        with self.assertRaises(NoMolecule):
            self._test_direction_for_points({}, -1)

        # Wrong molecule type

        with self.assertRaises(NoMolecule):