class StigmergyAnt(Actor): COLOUR: Colour = Colour(255, 255, 0) ALT_COLOUR: Colour = Colour(255, 128, 0) ID: str = "sant" DROP_CHANCE: float = 0.5 @staticmethod def create(kinds: [Kind] = []) -> Actor: return StigmergyAnt(kinds) @staticmethod def get_id() -> str: return StigmergyAnt.ID def get_colour(self) -> Colour: return StigmergyAnt.COLOUR if self.carrying is None else StigmergyAnt.ALT_COLOUR def food_interaction(self, loc: Location, world: World, position: Position): if self.carrying is not None and Random().random( ) < StigmergyAnt.DROP_CHANCE: for l in world.get_adjacent_locations(position.x, position.y, 1, False): if len(l.get_objects(Kind.FOOD)) > 0: loc.add_object(self.carrying) self.carrying = None break else: food_items: [Object] = loc.get_objects(Kind.FOOD) if len(food_items) > 0: self.carrying = food_items[0] loc.remove_object(self.carrying) def move(self, frm: Location, to: Location): to.set_actor(self) to.add_object(TestObject()) frm.remove_actor() def tick(self, world: World, elapsed: float, location: Location, position: Position): try: # COMPLETELY RANDOM CHOICES FOR WALKS (no preserving of direction). to: Location = Random().choice( world.get_adjacent_locations(position.x, position.y, 1, True)) self.move(location, to) self.food_interaction(to, world, position) except IndexError: # The ant can't find anywhere to move... location.add_object(TestObject()) def __init__(self, kinds: [Kind]): super().__init__(kinds) self.carrying: Object = None
class Nest(Ground): ID: str = "Nest" IS_PASSABLE: bool = True COLOUR: Colour = Colour(255, 255, 255) @staticmethod def create(kinds: [Kind] = []) -> Ground: return Nest(kinds) @staticmethod def get_id() -> str: return(Nest.ID) def is_passable(self) -> bool: return Nest.IS_PASSABLE def get_kinds(self) -> [Kind]: return self.kinds def add_kind(self, kind: Kind): self.kinds.append(kind) @staticmethod def get_colour() -> Colour: return Nest.COLOUR def __init__(self, kinds: [Kind]): super().__init__(kinds)
class Wall(Ground): IS_PASSABLE: bool = False ID: str = "Wall" COLOUR: Colour = Colour(0, 0, 0) @staticmethod def create(kinds: [Kind] = []) -> Ground: return Wall(kinds) @staticmethod def get_id() -> str: return (Wall.ID) def is_passable(self) -> bool: return Wall.IS_PASSABLE def get_kinds(self) -> [Kind]: return self.kinds def add_kind(self, kind: Kind): self.kinds.append(kind) @staticmethod def get_colour() -> Colour: return Wall.COLOUR def __init__(self, kinds: [Kind]): super().__init__(kinds)
class ModularQueen(ModularAnt): QUEEN_LOGICAL_AGE: float = 25 BROOD_PHEROMONES_PER_TICK: int = 20 HOLD_POSITION_CHANCE: float = 0.7 COLOUR: Colour = Colour(255, 32, 240) ID: str = "mqu" @staticmethod def create(attributes: Attributes = None, kinds: [Kind] = []) -> Actor: return ModularQueen(attributes, kinds) @staticmethod def get_id() -> str: return ModularQueen.ID def get_colour(self) -> Colour: return ModularQueen.COLOUR def get_attributes_string(self) -> str: return repr([("age", self.age), ("bias", self.bias), ("holdness", self.holdness), ("wobble", self.wobble), ("hold_position_chance", self.hold_position_chance), ("brood_position", self.brood_position)]) def tick(self, world: World, elapsed: float, location: Location, position: Position): if self.brood_position is None: self.brood_position = (position.x, position.y) self.current_wander_behaviour.set_brood_position(self.brood_position) # location.add_brood_pheromones(ModularQueen.BROOD_PHEROMONES_PER_TICK) for loc in world.get_adjacent_locations(position.x, position.y, 1, False): loc.add_brood_pheromones(ModularQueen.BROOD_PHEROMONES_PER_TICK) self.current_wander_behaviour.set_age(ModularQueen.QUEEN_LOGICAL_AGE) super().tick(world, elapsed, location, position) def __init__(self, attributes: Union[Attributes, None], kinds: [Kind]): if Kind.QUEEN not in kinds: kinds.append(Kind.QUEEN) super().__init__(attributes, kinds) self.brood_position: Union[Position, None] = None self.hold_position_chance: float = ModularQueen.HOLD_POSITION_CHANCE attributes.set_for(self) self.current_wander_behaviour = QueenWanderPheromoneBehaviour(self.bias, self.holdness, self.wobble) if self.brood_position is not None: self.current_wander_behaviour.set_brood_position(self.brood_position)
class Cleaner(Ant): ID: str = "Cleaner" COLOUR: Colour = Colour(0, 100, 0) @staticmethod def create(kinds: [Kind] = []) -> Actor: return Cleaner(kinds) @staticmethod def get_id() -> str: return Cleaner.ID @staticmethod def get_colour() -> Colour: return Cleaner.COLOUR def __init__(self, kinds: [Kind]): super().__init__(kinds)
class Nurse(Ant): ID: str = "Nurse" COLOUR: Colour = Colour(0, 0, 205) @staticmethod def create(kinds: [Kind] = []) -> Actor: return Nurse(kinds) @staticmethod def get_id() -> str: return Nurse.ID @staticmethod def get_colour() -> Colour: return Nurse.COLOUR def __init__(self, kinds: [Kind]): super().__init__(kinds)
class Queen(Ant): ID: str = "Queen" COLOUR: Colour = Colour(75, 0, 130) @staticmethod def create(kinds: [Kind] = []) -> Actor: return Queen(kinds) @staticmethod def get_id() -> str: return Queen.ID @staticmethod def get_colour() -> Colour: return Queen.COLOUR def __init__(self, kinds: [Kind]): super().__init__(kinds)
class Forager(Ant): ID: str = "Forager" COLOUR: Colour = Colour(255, 128, 0) @staticmethod def create(kinds: [Kind] = []) -> Actor: return Forager(kinds) @staticmethod def get_id() -> str: return Forager.ID @staticmethod def get_colour() -> Colour: return Forager.COLOUR def __init__(self, kinds: [Kind]): super().__init__(kinds)
class BehaviourAnt(Actor): age: float = 0 COLOUR: Colour = Colour(255, 255, 0) ID: str = "bant" DROP_CHANCE: float = 0.5 @staticmethod def create(kinds: [Kind] = []) -> Actor: return BehaviourAnt(kinds) @staticmethod def get_id() -> str: return BehaviourAnt.ID def get_colour(self) -> Colour: if self.age > 200: return Colour(255, 255, 0) else: return Colour(255, self.age, 255 - self.age) def move(self, frm: Location, to: Location): to.set_actor(self) frm.remove_actor() def tick(self, world: World, elapsed: float, location: Location, position: Position): self.current_behaviour: Behaviour = WanderBehaviour( 0, 1 - (self.age / 300), 1 - (self.age / 600)) self.current_behaviour.do(world, elapsed, location, position, self) location.add_object(TestObject()) self.age += 0.1 * elapsed def __init__(self, kinds: [Kind]): super().__init__(kinds) self.carrying: Object = None self.current_behaviour: Behaviour = WanderBehaviour() self.previous_behaviour: Behaviour = WanderBehaviour() self.age = 200 * random.random() self.current_dir = random.choice([[-1, -1], [-1, 0], [0, -1], [1, 1], [1, 0], [0, 1], [-1, 1], [1, -1]])
def get_pheromone_colour(self) -> Colour: return Colour( int(Colour.MAX_VALUE * (float(self.get_foraging_pheromone_count()) / Location.PHEROMONE_COUNT_CAP)), 200, 200)
class Location(ABC): """\ The Location class. Manages a location in a map. """ DEFAULT_OBJECTS_BASE_COLOUR: Colour = Colour(1, 0, 0) MIN_COLOUR_VAL: int = 50 PHEROMONE_DETERIORATION_CHANCE: float = 0.05 COMPLEX_PHEROMONE_DETERIORATION_CHANCE: float = 0.015 PHEROMONE_COUNT_CAP: int = 20 PHEROMONE_COLOUR: Colour = Colour(90, 127, 0) def __init__(self, ground: Ground): self.ground: Ground = ground # 1..1 self.actor: Union[Actor, None] = None # 0..1 self.objects: [Object] = [] # 0..* # General ant pheromones: self.pheromones: int = 0 # More complex pheromones: # For carrying food. self.foraging_pheromones: int = 0 # For staying near the nest. self.brood_pheromones: int = 0 def add_pheromones(self, num: int): if self.pheromones < Location.PHEROMONE_COUNT_CAP and self.get_ground( ).is_passable(): self.pheromones += num def get_pheromone_count(self): return self.pheromones def add_foraging_pheromones(self, num: int): if self.foraging_pheromones < Location.PHEROMONE_COUNT_CAP and self.get_ground( ).is_passable(): self.foraging_pheromones += num def get_foraging_pheromone_count(self): return self.foraging_pheromones def add_brood_pheromones(self, num: int): if self.brood_pheromones < Location.PHEROMONE_COUNT_CAP and self.get_ground( ).is_passable(): self.brood_pheromones += num def get_brood_pheromone_count(self): return self.brood_pheromones def get_pheromone_colour(self) -> Colour: return Colour( int(Colour.MAX_VALUE * (float(self.get_foraging_pheromone_count()) / Location.PHEROMONE_COUNT_CAP)), 200, 200) # return Location.PHEROMONE_COLOUR.get_alt_blue(int(Colour.MAX_VALUE * (float(self.get_pheromone_count()) / # Location.PHEROMONE_COUNT_CAP))).\ # get_alt_red(int(Colour.MAX_VALUE * (float(self.get_foraging_pheromone_count()) / # Location.PHEROMONE_COUNT_CAP))) def tick(self, world: World, elapsed: float, position: Position) -> (Actor, Location, Position): for obj in self.objects: obj.tick(world, elapsed, self, position) if self.get_ground().is_passable(): # TODO: Fix this to be in terms of elapsed time (eg. with binomial distribution for probability). if random.random( ) < Location.PHEROMONE_DETERIORATION_CHANCE and self.pheromones > 0: self.pheromones -= 1 if random.random( ) < Location.COMPLEX_PHEROMONE_DETERIORATION_CHANCE and self.foraging_pheromones > 0: self.foraging_pheromones -= 1 if random.random( ) < Location.COMPLEX_PHEROMONE_DETERIORATION_CHANCE and self.brood_pheromones > 0: self.brood_pheromones -= 1 return self.actor, self, position def get_objects_colour(self, world_state: WorldState) -> Colour: return Location.DEFAULT_OBJECTS_BASE_COLOUR.multiply(( float(len(self.objects))/world_state. get_max_objects_in_location_count() if world_state. get_max_objects_in_location_count() != 0 else 1) * Colour.MAX_VALUE).\ get_alt_blue(Location.MIN_COLOUR_VAL) def get_colour(self, world_state: WorldState, *object_kinds: [Kind]) -> Colour: if self.actor is not None: return self.actor.get_colour() elif len(object_kinds) == 0 and self.get_foraging_pheromone_count( ) > 0: # get_pheromone_count return self.get_pheromone_colour() elif len(self.get_objects(*object_kinds)) > 0: return self.get_objects_colour(world_state) else: return self.ground.get_colour() def set_actor(self, actor: Actor): if self.get_actor() is not None: raise InvalidLocationException() self.actor = actor def add_object(self, object: Object): self.objects.append(object) def remove_object(self, obj: Object): """\ Removes the specified Object from this Location. """ self.objects.remove(obj) def get_object_by_kind(self, kind: Kind): """\ Returns a list of objects in this location which have the specified kind. """ return self.get_objects(kind) # ret_list: [Object] = [] # for obj in self.objects: # if kind in obj.get_kinds(): # ret_list.append(obj) # return ret_list def get_actor(self) -> Union[Actor, None]: """\ Returns the actor at this location. """ return self.actor def remove_actor(self) -> Actor: """\ Removes the actor at this location, and returns it. """ actor: Actor = self.actor self.actor = None return actor def get_ground(self) -> Ground: """\ Returns the ground of this location. """ return self.ground @staticmethod def __any_in(of, lst): for x in of: if x in lst: return True return False def get_objects(self, *with_kinds: [Kind]) -> [Object]: """\ Returns the objects stored at this location. """ if len(with_kinds) == 0: return self.objects else: return list( filter(lambda k: self.__any_in(k.get_kinds(), with_kinds), self.objects)) def is_free(self) -> bool: """\ Return whether or not an actor can enter this location. """ return self.ground.is_passable() and self.actor is None
def get_colour(self) -> Colour: if self.age > 200: return Colour(255, 255, 0) else: return Colour(255, self.age, 255 - self.age)
class ModularAnt(Actor): COLOUR: Colour = Colour(150, 127, 0) AGE_COLOURING_PERIOD: float = 200 AGE_SCALE: float = 0.0001 # ages are being input in days, and each tick represents some seconds, so ~1/10000 scale. ID: str = "mant" HOLD_POSITION_CHANCE: float = 0.2 PHEROMONES_PER_TICK: int = 8 FORAGING_PHEROMONES_PER_TICK: int = 3 FORAGING_AGE: int = 130 FORAGING_PHEROMONE_OVERRIDE_CHANCE: float = 0.3 INITIAL_BIAS_HOLDNESS_WOBBLE: (float, float, float) = 0.2, 10, 0.3 INTERACTION_RADIUS: int = 2 INTERACTIONS_FILE_NAME: str = "interactions.txt" INTERACTING_TICKS_THRESHOLD: int = 3 FOOD_DROP_CHANCE: float = 0.015 ALT_PICKUP_CHANCE: float = 0.05 # Indicates the largest ModularAnt id taken so far (-1 if no id has yet been taken). max_ant_id: int = -1 # Interactions dictionary: interactions = {} @staticmethod def save_interactions(): with open((World.WRITE_OUT_FILE_PATH / ModularAnt.INTERACTIONS_FILE_NAME), "w+") as file: file.write(str(ModularAnt.interactions)) @staticmethod def load_interactions(): with open((World.WRITE_OUT_FILE_PATH / ModularAnt.INTERACTIONS_FILE_NAME), "r") as file: ModularAnt.interactions = eval(file.read()) @staticmethod def create(attributes: Attributes = None, kinds: [Kind] = []) -> Actor: return ModularAnt(attributes, kinds) @staticmethod def get_id() -> str: return ModularAnt.ID @staticmethod def can_interact(a1, a2) -> bool: """\ :type a1: ModularAnt :type a2: ModularAnt """ SIMILARITY_THRESHOLD: float = 1 return (-1 * SIMILARITY_THRESHOLD <= Direction.dif_mag(a1.get_facing(), a2.get_facing().reversed()) <= SIMILARITY_THRESHOLD) @staticmethod def log_interaction(a1, a2): """\ :type a1: ModularAnt :type a2: ModularAnt """ id1: int = a1.get_ant_id() id2: int = a2.get_ant_id() ModularAnt.log_interaction_ids(id1, id2) @staticmethod def log_interaction_ids(id1: int, id2: int): entry: (int, int) = (min(id1, id2), max(id1, id2)) if random.random() > 0.5: if entry in ModularAnt.interactions: ModularAnt.interactions[entry] += 1 else: ModularAnt.interactions[entry] = 1 def get_attributes_string(self) -> str: return repr([("age", self.age), ("bias", self.bias), ("holdness", self.holdness), ("wobble", self.wobble), ("hold_position_chance", self.hold_position_chance), ("carrying_food", self.carrying_food), ("interacting_with_id", self.interacting_with_id), ("interacting_ticks", self.interacting_ticks)]) def get_colour(self) -> Colour: return ModularAnt.COLOUR.get_alt_blue(int((2/math.pi) * Colour.MAX_VALUE * math.atan( self.age/ModularAnt.AGE_COLOURING_PERIOD) )) def move(self, frm: Location, to: Location): to.set_actor(self) frm.remove_actor() def interact(self, other_id: int): self.interacting_ticks = 0 self.interacting_with_id = -1 ModularAnt.log_interaction_ids(self.get_ant_id(), other_id) def tick(self, world: World, elapsed: float, location: Location, position: Position): self.age += elapsed * ModularAnt.AGE_SCALE self.current_wander_behaviour.set_age(self.age) self.current_wander_behaviour.set_seeking_food(not self.carrying_food) if random.random() > self.hold_position_chance * (1 + self.interacting_ticks): self.current_wander_behaviour.do(world, elapsed, location, position, self) if self.age < ModularAnt.FORAGING_AGE or random.random() < ModularAnt.\ FORAGING_PHEROMONE_OVERRIDE_CHANCE: location.add_pheromones(ModularAnt.PHEROMONES_PER_TICK) else: if location.get_brood_pheromone_count() == 0 and location.get_pheromone_count() == 0: location.add_foraging_pheromones(ModularAnt.FORAGING_PHEROMONES_PER_TICK) # If this ant is in the foraging area, it will pick up food readily. if not self.carrying_food: if location.get_ground().get_id() == ForageGrounds.get_id() or \ random.random() < ModularAnt.ALT_PICKUP_CHANCE: foods: [Object] = location.get_objects(Kind.FOOD) if len(foods) > 0: self.carrying_food = True location.remove_object(foods[-1]) elif location.get_ground().get_id() == Nest.get_id(): if random.random() < ModularAnt.FOOD_DROP_CHANCE or sum(map(lambda l: len(l.get_objects(Kind.FOOD)), world.get_adjacent_locations(position.x, position.y, 1, False))) > 0: location.add_object(Food()) self.carrying_food = False # Measure interactions: others: [int] = [] for loc in world.get_adjacent_locations(position.x, position.y, ModularAnt.INTERACTION_RADIUS, False): if loc.get_actor() is None: continue if isinstance(loc.get_actor(), ModularAnt): other: ModularAnt = loc.get_actor() if ModularAnt.can_interact(self, other): others.append(other.get_ant_id()) if self.interacting_with_id in others: self.interacting_ticks += 1 if self.interacting_ticks > ModularAnt.INTERACTING_TICKS_THRESHOLD: self.interact(self.interacting_with_id) elif len(others) > 0: self.interacting_with_id = others[0] self.interacting_ticks = 0 def get_facing(self) -> Direction: return self.current_wander_behaviour.facing def get_ant_id(self) -> int: return self.ant_id def __init__(self, attributes: Union[Attributes, None], kinds: [Kind]): if Kind.ANT not in kinds: kinds.append(Kind.ANT) super().__init__(kinds) # AI attributes. self.bias, self.holdness, self.wobble = ModularAnt.INITIAL_BIAS_HOLDNESS_WOBBLE self.hold_position_chance: float = ModularAnt.HOLD_POSITION_CHANCE self.age: float = 0 self.carrying_food: bool = False self.interacting_with_id: int = -1 self.interacting_ticks: int = 0 if attributes is not None: # Overwrite (potentially all) attributes with input values. attributes.set_for(self) # ID-related attributes, forcibly controlled by the ant. self.ant_id = (ModularAnt.max_ant_id + 1) ModularAnt.max_ant_id += 1 # Create a behaviour for it. self.current_wander_behaviour: WanderPheromoneBehaviour = WanderPheromoneBehaviour(self.bias, self.holdness, self.wobble)
def get_colour(self) -> Colour: return Colour()