diff --git a/src/ninjabees/Animator.py b/src/ninjabees/Animator.py index dca77dd..36b4bbc 100644 --- a/src/ninjabees/Animator.py +++ b/src/ninjabees/Animator.py @@ -3,18 +3,34 @@ class Animator: + """ + Class to handle the animation of the world map. + """ + def __init__(self): pass @staticmethod def clear_terminal(): + """ + Clear the terminal. + :return: + """ os.system('clear' if os.name == 'posix' else 'cls') @staticmethod - def print_world_status(world_map, found, total): + def print_world_status(world_map, found, total, food_at_hive): + """ + Print the status of the world map. + :param world_map: + :param found: + :param total: + :return: + """ Animator.clear_terminal() for row in world_map: print(''.join(row)) print(f'Found Food Sources: {found} of {total}') + print(f'Food at Hive: {food_at_hive}') time.sleep(0.09) diff --git a/src/ninjabees/Bee.py b/src/ninjabees/Bee.py index b73afb6..4601b09 100644 --- a/src/ninjabees/Bee.py +++ b/src/ninjabees/Bee.py @@ -5,119 +5,262 @@ class BeeJob(Enum): + """ + This class represents the job of a bee. + """ Scout = 0 Forager = 1 class Bee(Entity): - def __init__(self, name, hive, world, exploration_radius=1, move_range=5): + """ + This class represents a bee. + """ + + ENERGY = 600 + + def __init__(self, name, hive, world, exploration_radius=0): super().__init__(hive.get_x(), hive.get_y(), EntityType.Bee) self.name = name self.hive = hive self.world = world + self.__energy = Bee.ENERGY + # Initially no food source is known self.__job = BeeJob.Scout self.__food_goal = None self.__exploration_radius = exploration_radius - self.__move_range = move_range self.__found_food = False self.__found_food_source = None + self.debug_path = [] + self.debug_pos = [(hive.get_x(), hive.get_y())] + + self.__home_path = [] + self.__home_path_index = 0 + self.__flying_path = [] + self.__flying_index = 0 + self.__is_path_set_to_hive = False + + self.wait_for_instructions = False def has_found_food(self): + """ + Check if the bee has found food. + :return: + """ return self.__found_food def get_food_goal(self): + """ + Get the food goal for the bee. + :return: + """ return self.__food_goal def get_found_food_source(self): + """ + Get the found food source. + :return: + """ return self.__found_food_source def get_job(self): + """ + Get the job of the bee. + :return: + """ return self.__job def set_job(self, job): + """ + Set the job for the bee. + :param job: + :return: + """ self.__job = job - def set_food_goal(self, food_goal): + def get_flying_path(self): + """ + Get the flying path. + :return: + """ + return self.__flying_path + + def get_flying_index(self): + """ + Get the flying index. + :return: + """ + return self.__flying_index + + def set_flying_path(self, path): + """ + Set the flying path. + :param path: + :return: + """ + self.__flying_path = path + + def set_food_goal(self, food_goal, path_to_food): + """ + Set the food goal for the bee. + :param food_goal: + :param path_to_food: + :return: + """ self.__food_goal = food_goal + self.__flying_path = path_to_food def explore(self): - if self.__food_goal and self.__job != BeeJob.Scout: - self.move_towards_exploration_goal() + """ + Explore the environment to find food sources. + :return: + """ + if self.__energy == 0 or self.__found_food: + self.return_home() return - nearby_sources = [source for source in self.world.get_unclaimed_food_sources() if self.is_within_radius(source)] - if nearby_sources: - selected_source = random.choice(nearby_sources) - self.__found_food = True - self.__found_food_source = selected_source - else: - self.move() + self.__energy -= 1 + + if self.__job == BeeJob.Scout: + food_source = self.world.get_unclaimed_food_source_at(self.get_x(), self.get_y()) + if food_source: + self.__found_food = True + self.__found_food_source = food_source + else: + self.move() + return + + self.move_towards_exploration_goal() + return def is_within_radius(self, food_source): - distance = ((self.get_x() - food_source.get_x()) ** 2 + (self.get_y() - food_source.get_y()) ** 2) ** 0.5 - return distance <= self.__exploration_radius + """ + Check if the bee is within the exploration radius of a food source. + :param food_source: + :return: + """ + return self.get_x() == food_source.get_x() and self.get_y() == food_source.get_y() def return_home(self): + """ + Move the bee back to the hive. + :return: + """ x = self.get_x() y = self.get_y() if x == self.hive.get_x() and y == self.hive.get_y(): - if self.__found_food_source not in self.hive.food_sources: - self.hive.add_found_food_source(self.__found_food_source) - self.__found_food = False - self.__found_food_source = None - self.__food_goal = None - else: - if x != self.hive.get_x(): - if (x - self.hive.get_x()) > 0: - self.set_x(x - 1) - else: - self.set_x(x + 1) - if y != self.hive.get_y(): - if (y - self.hive.get_y()) > 0: - self.set_y(y - 1) - else: - self.set_y(y + 1) + if self.__found_food: + self.__is_path_set_to_hive = False + self.hive.add_found_food_source(self.__found_food_source, list(self.__flying_path)) + self.wait_for_instructions = True + return + + if not self.__is_path_set_to_hive: + self.__is_path_set_to_hive = True + self.reverse_flying_path() + + move = self.__home_path[self.__home_path_index] + self.__home_path_index += 1 + x += move[0] + y += move[1] + self.set_x(x) + self.set_y(y) def move_towards_exploration_goal(self): + """ + Move the bee towards the food goal. + :return: + """ x = self.get_x() y = self.get_y() if x == self.__food_goal.get_x() and y == self.__food_goal.get_y(): - self.__found_food = True self.__found_food_source = self.__food_goal + self.__found_food = True self.__food_goal = None - else: - if x != self.__food_goal.get_x(): - if (x - self.__food_goal.get_x()) > 0: - self.set_x(x - 1) - else: - self.set_x(x + 1) - if y != self.__food_goal.get_y(): - if (y - self.__food_goal.get_y()) > 0: - self.set_y(y - 1) - else: - self.set_y(y + 1) + self.__flying_index = 0 + return + + move = self.__flying_path[self.__flying_index] + self.debug_path.append((move[0], move[1])) + self.__flying_index += 1 + x += move[0] + y += move[1] + self.set_x(x) + self.set_y(y) + self.debug_pos.append((self.get_x(), self.get_y())) def move(self): + """ + Move the bee in a random direction within a certain range. + :return: + """ x = self.get_x() y = self.get_y() - x += int(random.uniform(-self.__move_range, self.__move_range)) - y += int(random.uniform(-self.__move_range, self.__move_range)) + move_x = random.randint(-1, 1) + move_y = random.randint(-1, 1) - self.set_x(x) - self.set_y(y) + x += move_x + y += move_y if x < 0: - self.set_x(0) + x = 0 + move_x = 0 if y < 0: - self.set_y(0) - if x > 89: - self.set_x(89) - if y > 199: - self.set_y(199) + y = 0 + move_y = 0 + if x > 199: + x = 199 + move_x = 0 + if y > 89: + y = 89 + move_y = 0 + + if move_x == 0 and move_y == 0: + self.move() + return + + self.set_x(x) + self.set_y(y) + + self.__flying_path.append((move_x, move_y)) + # self.__flying_path.add_step(move_x, move_y) + + def reset(self): + """ + Reset the bee. + :return: + """ + self.set_x(self.hive.get_x()) + self.set_y(self.hive.get_y()) + + self.__found_food = False + self.__found_food_source = None + self.__food_goal = None + self.__is_path_set_to_hive = False + self.__energy = Bee.ENERGY + + self.__home_path = [] + self.__home_path_index = 0 + self.__flying_path = [] + self.__flying_index = 0 + + self.debug_path = [] + self.debug_pos = [(self.hive.get_x(), self.hive.get_y())] + + def reverse_flying_path(self): + """ + Reverse the flying path. + :return: + """ + reverse_path = [] + for move in self.__flying_path: + reverse_path.append((-move[0], -move[1])) + reverse_path.reverse() + self.__home_path = list(reverse_path) diff --git a/src/ninjabees/FoodSource.py b/src/ninjabees/FoodSource.py index 2ca54fb..87ca615 100644 --- a/src/ninjabees/FoodSource.py +++ b/src/ninjabees/FoodSource.py @@ -2,10 +2,18 @@ class FoodSource(Entity): + """ + This class represents a food source. + """ + def __init__(self, name, nutritional_val, x, y): super().__init__(x, y, EntityType.Food) self.nutritional_val = nutritional_val self.amount = 100 def get_amount(self): + """ + Get the amount of food in the food source. + :return: + """ return self.amount diff --git a/src/ninjabees/Hive.py b/src/ninjabees/Hive.py index 29d9013..0546832 100644 --- a/src/ninjabees/Hive.py +++ b/src/ninjabees/Hive.py @@ -5,62 +5,136 @@ class Hive(Entity): - def __init__(self, name, num_onlooker_bees, max_cnt_foraging_bees=100, x=0, y=0, world=None): + """ + This class represents a hive. + """ + + def __init__(self, name, num_onlooker_bees, max_cnt_foraging_bees=300, x=0, y=0, world=None): super().__init__(x, y, EntityType.Hive) self.name = name self.num_onlooker_bees = num_onlooker_bees self.world = world + self.food_at_hive = 0 self.food_sources = [] - self.found_food_sources = [] + self.found_food_sources = {} self.__current_foraging = 0 self.max_cnt_foraging_bees = max_cnt_foraging_bees - self.bee_population = [Bee("Bee", self, world) for _ in range(200)] + self.bee_population = [Bee("Bee", self, world) for _ in range(600)] + + def add_found_food_source(self, food_source, path_to_food): + """ + Add a found food source. + :param food_source: + :param path_to_food: + :return: + """ + index = 0 + x, y = int(self.get_x()), int(self.get_y()) + for step in path_to_food: + move = path_to_food[index] + x += move[0] + y += move[1] + index += 1 + if x != food_source.get_x() or y != food_source.get_y(): # not a correct path + raise Exception("Invalid path to food source") + while True: + pass + + if food_source not in self.found_food_sources: + self.found_food_sources[food_source] = list(path_to_food) + if len(path_to_food) < len(self.found_food_sources[food_source]): + self.found_food_sources[food_source] = list(path_to_food) - def add_found_food_source(self, food_source): - self.found_food_sources.append(food_source) def calculate_food_source_quality(self, food_source): + """ + Calculate the quality of a food source. + :param food_source: + :return: + """ # Calculate the quality of a food source based on its distance from the hive and the nutritional_val of the food distance = ((self.get_x() - food_source.get_x()) ** 2 + (self.get_x() - food_source.get_y()) ** 2) ** 0.5 return food_source.nutritional_val / distance def forage(self): + """ + Forage for food. + :return: + """ self.employed_bees_phase() self.onlooker_bees_phase() - for food_source in self.found_food_sources: - if food_source not in self.food_sources: - self.world.add_entity(food_source) - self.food_sources.append(food_source) + list_food_sources = list(self.found_food_sources) + for found_food_source in list_food_sources: + if found_food_source not in self.food_sources: + self.world.add_entity(found_food_source) + self.food_sources.append(found_food_source) def employed_bees_phase(self): + """ + Employed bees phase. + :return: + """ for bee in self.bee_population: - if bee.has_found_food(): - bee.return_home() - else: - bee.explore() + try: + if bee.has_found_food(): + bee.return_home() + else: + bee.explore() + except Exception as e: + print(e) def onlooker_bees_phase(self): - if len(self.food_sources) == 0: + """ + Onlooker bees phase. + :return: + """ + if len(self.found_food_sources) == 0: + for bee in self.bee_population: + if bee.wait_for_instructions: + bee.wait_for_instructions = False + bee.reset() return for bee in self.bee_population: - if self.__current_foraging < self.max_cnt_foraging_bees: - # For every bee in population which is currently a scout bee and has not found food, - # set its food goal random where the probability of - # selecting a food source is proportional to its quality. - if bee.get_job() == BeeJob.Scout and not bee.has_found_food(): - bee.set_food_goal(random.choices(self.found_food_sources, - weights=[self.calculate_food_source_quality(source) for source in - self.found_food_sources], - k=self.num_onlooker_bees)[0]) + # For every bee in population which is currently a scout bee and has not found food, + # set its food goal random where the probability of + # selecting a food source is proportional to its quality. + if bee.get_job() == BeeJob.Scout and bee.get_x() == self.get_x() and bee.get_y() == self.get_y(): + if bee.has_found_food(): + self.food_at_hive += 1 + if bee.get_found_food_source() not in self.found_food_sources: + self.add_found_food_source(bee.get_found_food_source(), bee.get_flying_path()) + if len(bee.get_flying_path()) < len(self.found_food_sources[bee.get_found_food_source()]): + self.add_found_food_source(bee.get_found_food_source(), bee.get_flying_path()) + bee.reset() + if self.__current_foraging < self.max_cnt_foraging_bees: + food_source_qualities = [self.calculate_food_source_quality(source) for source in + self.found_food_sources] + list_food_sources = list(self.found_food_sources) + food_goal = random.choices(list_food_sources, + weights=food_source_qualities, k=self.num_onlooker_bees)[0] + path_to_goal = self.found_food_sources[food_goal] + + bee.set_food_goal(food_goal, list(path_to_goal)) bee.set_job(BeeJob.Forager) self.__current_foraging += 1 + continue + is_bee_home = bee.get_x() == self.get_x() and bee.get_y() == self.get_y() + if bee.get_job() == BeeJob.Forager and is_bee_home and bee.wait_for_instructions: + bee.wait_for_instructions = False + if bee.has_found_food(): + self.food_at_hive += 1 + bee.reset() + + food_source_qualities = [self.calculate_food_source_quality(source) for source in + self.found_food_sources] + list_food_sources = list(self.found_food_sources) + food_goal = random.choices(list_food_sources, + weights=food_source_qualities, k=self.num_onlooker_bees)[0] + path_to_goal = self.found_food_sources[food_goal] - if bee.get_job() == BeeJob.Forager and bee.get_food_goal() is None: - bee.set_food_goal(random.choices(self.found_food_sources, - weights=[self.calculate_food_source_quality(source) for source in - self.found_food_sources], - k=self.num_onlooker_bees)[0]) + bee.set_food_goal(food_goal, list(path_to_goal)) + continue diff --git a/src/ninjabees/environment/Entity.py b/src/ninjabees/environment/Entity.py index f03e446..9989af0 100644 --- a/src/ninjabees/environment/Entity.py +++ b/src/ninjabees/environment/Entity.py @@ -8,6 +8,9 @@ class EntityType(Enum): class Entity: + """ + This class represents an entity in the world. + """ def __init__(self, x, y, type): self.__x = x @@ -15,16 +18,38 @@ def __init__(self, x, y, type): self.__type = type def get_x(self): - return self.__x + """ + Get the x coordinate of the entity. + :return: + """ + return int(self.__x) def get_y(self): - return self.__y + """ + Get the y coordinate of the entity. + :return: + """ + return int(self.__y) def get_type(self): + """ + Get the type of the entity. + :return: + """ return self.__type def set_x(self, x): + """ + Set the x coordinate of the entity. + :param x: + :return: + """ self.__x = x def set_y(self, y): + """ + Set the y coordinate of the entity. + :param y: + :return: + """ self.__y = y diff --git a/src/ninjabees/environment/World.py b/src/ninjabees/environment/World.py index 37772dc..52cb410 100644 --- a/src/ninjabees/environment/World.py +++ b/src/ninjabees/environment/World.py @@ -4,6 +4,10 @@ class World: + """ + This class represents the world of the bee simulation. + """ + def __init__(self, width, height): self.__width = width self.__height = height @@ -13,18 +17,35 @@ def __init__(self, width, height): self.__food_sources = [] self.__unclaimed_food_sources = [] - self.__n_fod_sources = 0 + self.__n_food_sources = 0 self.__world_map = [['-' for _ in range(width)] for _ in range(height)] def add_food_source(self, food_source): + """ + Add a food source to the world. + :param food_source: + :return: + """ self.__unclaimed_food_sources.append(food_source) - self.__n_fod_sources += 1 + self.__n_food_sources += 1 - def get_unclaimed_food_sources(self): - return self.__unclaimed_food_sources + def get_unclaimed_food_source_at(self, x, y): + """ + Get the unclaimed food sources. + :return: + """ + for food_source in self.__unclaimed_food_sources: + if food_source.get_x() == x and food_source.get_y() == y: + return food_source + return None def add_entity(self, entity): + """ + Add an entity to the world. + :param entity: + :return: + """ self.__entities.append(entity) if entity.get_type() == EntityType.Hive: self.__hive = entity @@ -33,38 +54,54 @@ def add_entity(self, entity): self.__food_sources.append(entity) def is_position_blocked(self, x, y): + """ + Check if the position is blocked by an entity. + :param x: + :param y: + :return: + """ for entity in self.__entities: if entity.x == x and entity.y == y: return True return False def update_world_map(self): + """ + Update the world map with the current entities. + :return: + """ + for food in self.__unclaimed_food_sources: + self.__world_map[food.get_y()][food.get_x()] = 'u' for entity in self.__entities: entity_type = entity.get_type() entity_x = entity.get_x() entity_y = entity.get_y() if entity_type == EntityType.Bee: if entity.has_found_food(): - self.__world_map[entity_x][entity_y] = 'S' if entity.get_job() == BeeJob.Scout else 'B' + self.__world_map[entity_y][entity_x] = 'S' if entity.get_job() == BeeJob.Scout else 'B' else: - self.__world_map[entity_x][entity_y] = 's' if entity.get_job() == BeeJob.Scout else 'b' + self.__world_map[entity_y][entity_x] = 's' if entity.get_job() == BeeJob.Scout else 'b' elif entity_type == EntityType.Food: - self.__world_map[entity_x][entity_y] = 'F' + self.__world_map[entity_y][entity_x] = 'F' elif entity_type == EntityType.Hive: - self.__world_map[entity_x][entity_y] = 'H' + self.__world_map[entity_y][entity_x] = 'H' else: raise ValueError("Unknown entity type") def run(self, max_iterations): + """ + Run the simulation for a given number of iterations. + :param max_iterations: + :return: + """ for iteration in range(max_iterations): self.__hive.forage() self.update_world_map() Animator.print_world_status(world_map=self.__world_map, found=len(self.__hive.found_food_sources), - total=self.__n_fod_sources) + total=self.__n_food_sources, food_at_hive=self.__hive.food_at_hive) self.__world_map = [['-' for _ in range(self.__width)] for _ in range(self.__height)] - if len(self.__hive.found_food_sources) == self.__n_fod_sources: + if len(self.__hive.found_food_sources) == self.__n_food_sources: print(f'All food sources found! In {iteration} iterations') - return diff --git a/src/ninjabees/main.py b/src/ninjabees/main.py index f09ff9f..1bb3734 100644 --- a/src/ninjabees/main.py +++ b/src/ninjabees/main.py @@ -36,23 +36,15 @@ def parse_args(args): def main(args): args = parse_args(args) - food1 = FoodSource("Flower", 80, int(random.uniform(0, 89)), int(random.uniform(0, 199))) - food2 = FoodSource("Tree", 10, int(random.uniform(0, 89)), int(random.uniform(0, 199))) - food3 = FoodSource("Garden", 100, int(random.uniform(0, 89)), int(random.uniform(0, 199))) - food4 = FoodSource("Flower 2", 88, int(random.uniform(0, 89)), int(random.uniform(0, 199))) - food5 = FoodSource("Tree 2", 77, int(random.uniform(0, 89)), int(random.uniform(0, 199))) - food6 = FoodSource("Garden 2", 300, int(random.uniform(0, 89)), int(random.uniform(0, 199))) + foods = [FoodSource("Food", int(random.uniform(0, 300)), int(random.uniform(0, 199)), + int(random.uniform(0, 89))) for _ in range(30)] world = World(200, 90) - world.add_food_source(food1) - world.add_food_source(food2) - world.add_food_source(food3) - world.add_food_source(food4) - world.add_food_source(food5) - world.add_food_source(food6) + for food in foods: + world.add_food_source(food) - hive = Hive("MyHive", num_onlooker_bees=1, x=int(random.uniform(0, 89)), y=int(random.uniform(0, 199)), world=world) + hive = Hive("MyHive", num_onlooker_bees=1, x=int(random.uniform(0, 199)), y=int(random.uniform(0, 89)), world=world) world.add_entity(hive) for bee in hive.bee_population: world.add_entity(bee)