Exemplo n.º 1
0
 def attacking_units(self) -> Units:
     """Returns all units that are currently attacking."""
     attacking_units = Units(self.roles[UnitTask.Attacking].units.copy(),
                             self.ai)
     moving_units = self.roles[UnitTask.Moving].units.copy()
     # Combine lists
     attacking_units.extend(moving_units)
     return attacking_units
Exemplo n.º 2
0
    def enemy(self, type_id: Union[UnitTypeId, Iterable[UnitTypeId]]):
        """Returns all enemy units of the specified type(s)."""
        if isinstance(type_id, UnitTypeId):
            return self.enemy_unit_cache.get(type_id, self.empty_units)

        units = Units([], self.ai)
        for single_type in type_id: # type: UnitTypeId
            units.extend(self.enemy_unit_cache.get(single_type, self.empty_units))
        return units
Exemplo n.º 3
0
    def safe_active_gas_buildings(self) -> Units:
        """All gas buildings that are on a safe zone and could use more workers."""
        result = Units([], self.ai)

        for zone in self.knowledge.our_zones:  # type: Zone
            if zone.is_under_attack:
                continue

            filtered = filter(lambda g: g.has_vespene, zone.gas_buildings)
            result.extend(filtered)

        return result
Exemplo n.º 4
0
class ScoutBaseAction(ActBase):
    _units: Units

    def __init__(self, only_once: bool) -> None:
        super().__init__()
        self.current_target = Point2((0, 0))
        self.ended = False
        self.only_once = only_once

    async def start(self, knowledge: "Knowledge"):
        await super().start(knowledge)
        self._units = Units([], self.ai)

    def set_scouts(self, scouts: List[Unit]):
        self._units.clear()
        self._units.extend(scouts)
Exemplo n.º 5
0
class Scout(SubActs):
    units: Units

    def __init__(self, unit_types: Union[UnitTypeId, Set[UnitTypeId]], unit_count: int, *args: ScoutBaseAction):
        """
        Scout act for all races, loops the given scout actions
        @param unit_types: Types of units accepted as scouts
        @param unit_count: Units required to be used in scouting, scouting will only start after all are available
        @param args: Scout actions, cen be to scout a certain location, or to move around in certain way. Defaults to scouting enemy main
        """
        if isinstance(unit_types, UnitTypeId):
            self.unit_types = set()
            self.unit_types.add(unit_types)
        else:
            self.unit_types = unit_types
        self.unit_count = unit_count

        if len(args) > 0:
            super().__init__(*args)
        else:
            super().__init__(ScoutLocation.scout_main())

        self.scout_tags: List[int] = []
        self.started = False
        self.ended = False
        self.index = 0

    async def start(self, knowledge: "Knowledge"):
        await super().start(knowledge)
        self.units = Units([], self.ai)

    async def execute(self) -> bool:
        if self.ended:
            return True

        self.units.clear()

        if self.find_units():
            return True

        if self.units:
            self.roles.set_tasks(UnitTask.Scouting, self.units)
            await self.micro_units()  # Ignore if the scouting has finished
        return True

    async def micro_units(self) -> bool:
        """
        Micros units
        @return: True when finished
        """
        count = len(self.orders)
        self.index = self.index % count

        for looped in range(0, count + 1):
            if looped == count:
                self.ended = True
                return True
            # noinspection PyTypeChecker
            action: ScoutBaseAction = self.orders[self.index]
            action.set_scouts(self.units)
            result = await action.execute()
            if not result:
                # Not finished
                return False

            self.index = (self.index + 1) % count
        return False

    def find_units(self) -> bool:
        if not self.started:
            if UnitTypeId.OVERLORD in self.unit_types:
                free_units = self.roles.get_types_from(
                    self.unit_types, UnitTask.Idle, UnitTask.Moving, UnitTask.Gathering, UnitTask.Reserved
                )
            else:
                free_units = self.roles.get_types_from(
                    self.unit_types, UnitTask.Idle, UnitTask.Moving, UnitTask.Gathering
                )
            if len(free_units) >= self.unit_count:
                # TODO: Better selection?
                new_scouts = free_units.random_group_of(self.unit_count)
                self.units.extend(new_scouts)
                self.scout_tags = new_scouts.tags

                self.started = True
        else:
            scouts = self.roles.get_types_from(self.unit_types, UnitTask.Scouting)
            self.units.extend(scouts.tags_in(self.scout_tags))
            if not self.units:
                # Scouts are dead, end the scout act
                self.ended = True
                return True
Exemplo n.º 6
0
class NydusMain(ActBase):
    """Set up Nydus in their main."""

    units: Units

    def __init__(self, unit_type: UnitTypeId, amount: int):
        super().__init__()
        self.micro = MicroRules()
        self.micro.load_default_methods()
        self.micro.unit_micros[UnitTypeId.ZERGLING] = SuicideLingMicro()
        self.started = False
        self.ended = False
        self.unit_type = unit_type
        self.amount = amount
        self.lair_tech = MorphLair()
        self.network = ActBuilding(UnitTypeId.NYDUSNETWORK, to_count=1)
        self.zerg_build = ZergUnit(unit_type)
        self.tags: List[int] = []

    async def start(self, knowledge: "Knowledge"):
        await super(NydusMain, self).start(knowledge)
        await self.lair_tech.start(knowledge)
        await self.network.start(knowledge)
        await self.zerg_build.start(knowledge)
        await self.micro.start(knowledge)
        self.units = Units([], self.ai)

    async def execute(self) -> bool:
        if self.cache.own(UnitTypeId.NYDUSCANAL).ready:
            for canal in self.cache.own(UnitTypeId.NYDUSCANAL).ready:
                self.do(canal(AbilityId.UNLOADALL_NYDUSWORM))

        if self.knowledge.lost_units_manager.own_lost_type(UnitTypeId.NYDUSCANAL):
            self.ended = True

        if self.ended:
            return True

        if self.cache.own(self.unit_type).amount < self.amount:
            self.zerg_build.to_count = self.amount
            await self.zerg_build.execute()

        self.units.clear()

        if not self.get_count(UnitTypeId.LAIR, include_pending=True, include_not_ready=True) + self.get_count(
            UnitTypeId.HIVE
        ):
            await self.lair_tech.execute()
            return True

        if not self.get_count(UnitTypeId.NYDUSNETWORK, include_pending=True, include_not_ready=True):
            await self.network.execute()
            return True

        # build the nydus worm
        if not self.get_count(UnitTypeId.NYDUSCANAL) and self.get_count(
            UnitTypeId.NYDUSNETWORK, include_not_ready=False, include_pending=False
        ):
            closest_overlord = self.cache.own(UnitTypeId.OVERLORD).closest_to(self.knowledge.enemy_start_location)
            nydus_network = self.cache.own(UnitTypeId.NYDUSNETWORK).first
            for i in range(11):
                pos = closest_overlord.position.towards(self.knowledge.enemy_start_location, i)
                if self.ai.get_terrain_z_height(pos) != self.ai.get_terrain_z_height(
                    self.knowledge.enemy_start_location
                ):
                    continue
                if self.ai.is_visible(pos) and await self.ai.can_place(UnitTypeId.NYDUSCANAL, pos):
                    self.do(nydus_network(AbilityId.BUILD_NYDUSWORM, pos))
            if i == 10 and not nydus_network.orders:
                self.do(closest_overlord.move(closest_overlord.position.towards(pos, 1)))

        # put units into the Nydus
        if self.get_count(UnitTypeId.NYDUSCANAL, include_pending=True, include_not_ready=True) and self.get_count(
            UnitTypeId.NYDUSNETWORK, include_pending=False, include_not_ready=False
        ):
            network = self.cache.own(UnitTypeId.NYDUSNETWORK).first
            if not self.started:
                free_units = self.roles.get_types_from(
                    {self.unit_type}, UnitTask.Idle, UnitTask.Moving, UnitTask.Gathering
                )
                if free_units.amount < self.amount:
                    return True
                self.units.extend(free_units.random_group_of(self.amount))
                self.tags = self.units.tags
                self.started = True
            else:
                unit_grouping = self.roles.get_types_from({self.unit_type}, UnitTask.Reserved)
                self.units.extend(unit_grouping.tags_in(self.tags))
                if not self.units:
                    self.ended = True
                    return True

        if self.units:
            network = None
            canal = None
            if self.get_count(UnitTypeId.NYDUSNETWORK):
                network = self.cache.own(UnitTypeId.NYDUSNETWORK).first
            if self.get_count(UnitTypeId.NYDUSCANAL):
                canal = self.cache.own(UnitTypeId.NYDUSCANAL).first
            if not canal or not network:
                return True
            self.roles.set_tasks(UnitTask.Reserved, self.units)
            for unit in self.units:
                if unit.distance_to(canal) <= unit.distance_to(network):
                    self.combat.add_unit(unit)
                else:
                    self.do(network(AbilityId.LOAD_NYDUSNETWORK, unit))
            self.combat.execute(
                self.knowledge.enemy_expansion_zones[0].center_location, MoveType.Assault, rules=self.micro
            )

        return True  # never block
Exemplo n.º 7
0
class UnitManager():
    def __init__(self, bot: BotAI, scouting_manager: ScoutingManager):
        self.bot = bot
        self.scouting_manager = scouting_manager
        self.unselectable = Units([], self.bot._game_data)
        self.unselectable_enemy_units = Units([], self.bot._game_data)
        self.scouting_ttl = 300
        self.army_scouting_ttl = 100
        self.panic_scout_ttl = 0
        self.inject_targets: Dict[Unit, Unit] = {}
        self.inject_queens: Units = Units([], self.bot._game_data)
        self.dead_tumors: Units = Units([], self.bot._game_data)
        self.spread_overlords: Units = Units([], self.bot._game_data)
        self.chasing_workers: Units = Units([], self.bot._game_data)

    async def iterate(self, iteration):
        self.scouting_ttl -= 1

        actions: List[UnitCommand] = []

        all_army: Units = self.bot.units.exclude_type(
            {OVERLORD, DRONE, QUEEN, LARVA, EGG}).not_structure.ready
        observed_enemy_army = self.scouting_manager.observed_enemy_units.filter(
            lambda u: u.can_attack_ground or u.type_id == UnitTypeId.BUNKER)
        estimated_enemy_value = self.scouting_manager.estimated_enemy_army_value

        army_units = all_army

        for observed_enemy in observed_enemy_army:
            pos = observed_enemy.position
            self.bot._client.debug_text_world(f'observed',
                                              Point3((pos.x, pos.y, 10)), None,
                                              12)

        # ASSIGN INJECT QUEENS
        hatches = self.bot.find_closest_n_from_units(
            self.bot.start_location, 4,
            self.bot.units(HATCHERY)).ready.tags_not_in(
                set(map(lambda h: h.tag, self.inject_targets.keys())))
        for hatch in hatches:
            free_queens: Units = self.bot.units(QUEEN).tags_not_in(
                self.unselectable.tags).tags_not_in(self.inject_queens.tags)
            if free_queens.exists:
                queen = free_queens.random
                self.inject_targets[hatch] = queen
                self.inject_queens.append(queen)

        # INJECT
        for hatch in self.inject_targets:
            if self.bot.known_enemy_units.closer_than(15, hatch).exists:
                continue
            inject_queen = self.inject_targets[hatch]
            if inject_queen:
                try:
                    abilities = await self.bot.get_available_abilities(
                        inject_queen)
                    if abilities and len(
                            abilities
                    ) > 0 and AbilityId.EFFECT_INJECTLARVA in abilities:
                        actions.append(
                            inject_queen(AbilityId.EFFECT_INJECTLARVA, hatch))
                    else:
                        # move to hatch
                        pass
                except:
                    print('inject error')
            else:
                del self.inject_targets[hatch]

        # SCOUTING

        if army_units(
                LING
        ).exists and self.scouting_ttl < 0 and self.scouting_manager.enemy_raiders_value == 0:
            self.scouting_ttl = 300
            unit: Unit = army_units(LING).random
            actions.append(unit.stop())
            scouting_order: List[Point2] = []
            keys: List[Point2] = list(self.bot.expansion_locations.keys())
            for idx in range(len(self.bot.expansion_locations)):
                furthest = self.bot.enemy_start_locations[0].furthest(keys)
                scouting_order.append(furthest)
                keys.remove(furthest)
            for position in scouting_order:
                actions.append(unit.move(position, True))
            self.unselectable.append(unit)

        # army scout only if opponent army has not been close for a while
        if not observed_enemy_army.closer_than(
                70, self.bot.own_natural).amount > 2:
            self.army_scouting_ttl -= 1
        else:
            self.army_scouting_ttl = 60

        if self.army_scouting_ttl <= 0 and army_units(LING).exists:
            self.army_scouting_ttl = 60
            unit: Unit = army_units(LING).random
            actions.append(unit.move(self.bot.enemy_start_locations[0]))
            self.unselectable.append(unit)

        # panic scout main if drone difference gets high enough
        if self.bot.already_pending(DRONE) + self.bot.units(
                DRONE
        ).amount > 25 * self.scouting_manager.enemy_townhall_count:
            if self.panic_scout_ttl <= 0:
                if self.bot.units(OVERLORD).exists:
                    closest_overlord = self.bot.units(OVERLORD).tags_not_in(
                        self.unselectable.tags).closest_to(
                            self.bot.enemy_start_locations[0])
                    original_position = closest_overlord.position
                    actions.append(closest_overlord.stop())
                    actions.append(
                        closest_overlord.move(
                            self.bot.enemy_start_locations[0], True))
                    actions.append(
                        closest_overlord.move(original_position, True))
                    self.unselectable.append(closest_overlord)
                    self.panic_scout_ttl = 300
            else:
                self.panic_scout_ttl -= 1

        # KILL TERRAN BUILDINGS WITH MUTAS
        if self.scouting_manager.terran_floating_buildings:
            mutas: Units = self.bot.units(MUTALISK).tags_not_in(
                self.unselectable.tags)
            pos: Point2 = self.bot.enemy_start_locations[
                0] + 15 * self.bot._game_info.map_center.direction_vector(
                    self.bot.enemy_start_locations[0])
            corners = [
                Point2((0, 0)),
                Point2((self.bot._game_info.pathing_grid.width - 1, 0)),
                Point2((self.bot._game_info.pathing_grid.width - 1,
                        self.bot._game_info.pathing_grid.height - 1)),
                Point2((0, self.bot._game_info.pathing_grid.height - 1)),
                Point2((0, 0))
            ]
            for muta in mutas:
                for corner in corners:
                    actions.append(muta.attack(corner, True))
                self.unselectable.append(muta)

        # UPDATE UNSELECTABLE UNITS SNAPSHOTS

        self.unselectable = self.bot.units.tags_in(self.unselectable.tags)

        to_remove = []
        for unit in self.unselectable:
            self.bot._client.debug_text_world(
                f'unselectable', Point3(
                    (unit.position.x, unit.position.y, 10)), None, 12)
            if unit.is_idle or unit.is_gathering or not unit.is_visible:
                to_remove.append(unit.tag)
        self.unselectable = self.unselectable.tags_not_in(set(to_remove))

        self.spread_overlords = self.bot.units.tags_in(
            self.spread_overlords.tags)
        for overlord in self.spread_overlords:
            self.bot._client.debug_text_world(
                f'spread',
                Point3((overlord.position.x, overlord.position.y, 10)), None,
                12)

        groups_start_time = time.time()
        # ARMY GROUPS

        groups: List[Units] = self.group_army(
            army_units.tags_not_in(self.unselectable.tags))

        for group in groups:
            nearby_enemies = None
            if observed_enemy_army.exists:
                closest_enemy = observed_enemy_army.closest_to(group.center)
                if closest_enemy.distance_to(group.center) < 15:
                    nearby_enemies: Units = observed_enemy_army.closer_than(
                        15, closest_enemy)
                    enemy_value = self.bot.calculate_combat_value(
                        nearby_enemies.ready)
            group_value = self.bot.calculate_combat_value(group)

            if nearby_enemies and nearby_enemies.exists:
                bias = 1
                if nearby_enemies.closer_than(
                        15, self.bot.own_natural).exists and group_value > 750:
                    bias = 1.2
                if self.bot.supply_used > 180:
                    bias = 1.5
                should_engage: bool = self.evaluate_engagement(
                    self.bot.units.exclude_type({DRONE, OVERLORD}).closer_than(
                        20, nearby_enemies.center), nearby_enemies, bias) > 0
                if should_engage:
                    # attack enemy group

                    # ling micro
                    microing_back_tags: List[int] = []
                    if nearby_enemies(LING).exists:
                        for unit in group(LING):
                            local_enemies: Units = nearby_enemies.closer_than(
                                3, unit.position)
                            local_allies: Units = group.closer_than(
                                3, unit.position)
                            # TODO: use attack range instead of proximity... (if enemies cant attack they arent a threat)
                            if (self.bot.calculate_combat_value(local_enemies)
                                    > self.bot.calculate_combat_value(
                                        local_allies)):
                                target = unit.position + 5 * local_enemies.center.direction_vector(
                                    group.center)
                                actions.append(unit.move(target))
                                microing_back_tags.append(unit.tag)
                                self.bot._client.debug_text_world(
                                    f'micro point',
                                    Point3((target.x, target.y, 10)), None, 12)
                                self.bot._client.debug_text_world(
                                    f'microing back',
                                    Point3((unit.position.x, unit.position.y,
                                            10)), None, 12)

                    if nearby_enemies.exclude_type({
                            UnitTypeId.CHANGELINGZERGLING,
                            UnitTypeId.CHANGELING,
                            UnitTypeId.CHANGELINGZERGLINGWINGS
                    }).exists:
                        actions.extend(
                            self.command_group(
                                group.tags_not_in(set(microing_back_tags)),
                                AbilityId.ATTACK, nearby_enemies.center))
                    else:
                        actions.extend(
                            self.command_group(
                                group, AbilityId.ATTACK,
                                nearby_enemies.closest_to(group.center)))
                    self.bot._client.debug_text_world(
                        f'attacking group',
                        Point3((group.center.x, group.center.y, 10)), None, 12)
                else:
                    # retreat somewhwere
                    mins = self.bot.get_mineral_fields_for_expansion(
                        self.bot.closest_mining_expansion_location(
                            group.center).position)
                    if mins.exists:
                        move_position = mins.center
                    else:
                        move_position = self.bot.start_location
                    if group.center.distance_to(move_position) < 5:
                        # Last resort attack with everything
                        everything: Units = group
                        if enemy_value > 150:
                            everything = self.bot.units.closer_than(
                                15, group.center)
                            self.unselectable.extend(everything)
                        everything = everything + self.bot.units(QUEEN)
                        actions.extend(
                            self.command_group(everything, AbilityId.ATTACK,
                                               nearby_enemies.center))
                        self.bot._client.debug_text_world(
                            f'last resort',
                            Point3((group.center.x, group.center.y, 10)), None,
                            12)
                    else:
                        # TODO: dont retreat if too close to enemy
                        actions.extend(
                            self.command_group(group, AbilityId.MOVE,
                                               move_position))
                        self.bot._client.debug_text_world(
                            f'retreating',
                            Point3((group.center.x, group.center.y, 10)), None,
                            12)
            else:
                if group_value > 1.2 * estimated_enemy_value or self.bot.supply_used >= 180:
                    # attack toward closest enemy buildings
                    attack_position = self.bot.enemy_start_locations[0]
                    observed_structures = self.scouting_manager.observed_enemy_units.structure
                    if observed_structures.exists:
                        attack_position = observed_structures.closest_to(
                            group.center).position
                    if self.scouting_manager.observed_enemy_units.exists:
                        target_enemy_units: Units = self.scouting_manager.observed_enemy_units.filter(
                            lambda u: u.can_attack_ground)
                        if target_enemy_units.exists:
                            attack_position = target_enemy_units.closest_to(
                                group.center).position
                    actions.extend(
                        self.command_group(group, AbilityId.ATTACK,
                                           attack_position))
                    self.bot._client.debug_text_world(
                        f'attacking base',
                        Point3((group.center.x, group.center.y, 10)), None, 12)
                else:
                    # merge
                    other_units: Units = all_army.tags_not_in(
                        group.tags.union(self.unselectable.tags))
                    if other_units.exists:
                        closest_other_unit: Unit = other_units.closest_to(
                            group.center)
                        actions.extend(
                            self.command_group(group, AbilityId.MOVE,
                                               closest_other_unit.position))
                        self.bot._client.debug_text_world(
                            f'merging',
                            Point3((group.center.x, group.center.y, 10)), None,
                            12)
                    else:
                        self.bot._client.debug_text_world(
                            f'idle',
                            Point3((group.center.x, group.center.y, 10)), None,
                            12)
        execution_time = (time.time() - groups_start_time) * 1000
        print(f'//// Groups: {round(execution_time, 3)}ms')

        # DRONE DEFENSE
        for expansion in self.bot.owned_expansions:
            enemy_raid: Units = observed_enemy_army.closer_than(15, expansion)
            if enemy_raid.exists:
                raid_value = self.bot.calculate_combat_value(enemy_raid)
                defending_army: Units = self.bot.units.closer_than(
                    15, expansion)
                if raid_value > self.bot.calculate_combat_value(
                        defending_army.exclude_type({DRONE})):
                    for defender in self.bot.units(DRONE).closer_than(
                            10, expansion).tags_not_in(self.unselectable.tags):
                        pos = defender.position
                        if expansion != self.bot.start_location:
                            self.bot._client.debug_text_world(
                                f'mineral walking', Point3((pos.x, pos.y, 10)),
                                None, 12)
                            actions.append(
                                defender.gather(self.bot.main_minerals.random))
                        else:
                            # pull drones vs harass
                            if enemy_raid.closer_than(
                                    5, defender.position
                            ).exists and not enemy_raid.of_type({
                                    DRONE, UnitTypeId.PROBE, UnitTypeId.SCV
                            }).exists:
                                self.bot._client.debug_text_world(
                                    f'pull the bois', Point3(
                                        (pos.x, pos.y, 10)), None, 12)
                                actions.append(
                                    defender.attack(enemy_raid.center))
                            # counter worker rush
                            elif enemy_raid.of_type(
                                {DRONE, UnitTypeId.SCV,
                                 UnitTypeId.PROBE}).exists:
                                if raid_value > 90:
                                    self.bot._client.debug_text_world(
                                        f'defend worker rush',
                                        Point3((pos.x, pos.y, 10)), None, 12)
                                    actions.append(
                                        defender.attack(enemy_raid.center))

        # DEFEND CANNON RUSH AND OTHER STUFF WITH DRONES

        for expansion in self.bot.owned_expansions:
            enemy_scouting_workers = self.bot.known_enemy_units(
                {DRONE, UnitTypeId.PROBE, UnitTypeId.SCV}).closer_than(
                    20,
                    expansion).tags_not_in(self.unselectable_enemy_units.tags)
            enemy_proxies = self.bot.known_enemy_structures.closer_than(
                20, expansion).tags_not_in(self.unselectable_enemy_units.tags)
            if enemy_proxies.exists:
                for proxy in enemy_proxies:
                    if proxy.type_id == UnitTypeId.PHOTONCANNON:
                        for drone in self.bot.units(DRONE).tags_not_in(
                                self.unselectable.tags).take(4, False):
                            actions.append(drone.attack(proxy))
                            self.unselectable.append(drone)
                            self.unselectable_enemy_units.append(proxy)
            if enemy_scouting_workers.exists:
                for enemy_worker in enemy_scouting_workers:
                    own_workers: Units = self.bot.units(DRONE).tags_not_in(
                        self.unselectable.tags)
                    if own_workers.exists:
                        own_worker: Unit = own_workers.closest_to(enemy_worker)
                        actions.append(own_worker.attack(enemy_worker))
                        self.chasing_workers.append(own_worker)
                        self.unselectable.append(own_worker)
                        self.unselectable_enemy_units.append(enemy_worker)

        # send back drones that are chasing workers
        self.chasing_workers = self.bot.units.tags_in(
            self.chasing_workers.tags)
        for drone in self.chasing_workers(DRONE):
            if self.bot.units(HATCHERY).closest_to(
                    drone.position).position.distance_to(drone.position) > 25:
                if isinstance(drone.order_target, int):
                    self.unselectable_enemy_units = self.unselectable_enemy_units.tags_not_in(
                        {drone.order_target})
                self.chasing_workers = self.chasing_workers.tags_not_in(
                    {drone.tag})
                actions.append(drone.gather(self.bot.main_minerals.random))

        extra_queen_start_time = time.time()
        # EXTRA QUEEN CONTROL
        extra_queens = self.bot.units(QUEEN).tags_not_in(
            self.unselectable.tags)
        # if there's a fight contribute otherwise make creep tumors
        if extra_queens.exists:
            if self.bot.known_enemy_units.exists and self.bot.units.closer_than(
                    20, extra_queens.center).tags_not_in(
                        extra_queens.tags).filter(
                            lambda u: u.is_attacking
                        ).exists and self.bot.known_enemy_units.closer_than(
                            20, extra_queens.center).exists:
                actions.extend(
                    self.command_group(
                        extra_queens, AbilityId.ATTACK,
                        self.bot.known_enemy_units.closest_to(
                            extra_queens.center).position))
                self.bot._client.debug_text_world(
                    f'queen attack',
                    Point3((extra_queens.center.x, extra_queens.center.y, 10)),
                    None, 12)
            else:
                for queen in extra_queens.tags_not_in(self.inject_queens.tags):
                    if queen.is_idle:
                        abilities = await self.bot.get_available_abilities(
                            queen)
                        position = await self.bot.find_tumor_placement()
                        if AbilityId.BUILD_CREEPTUMOR_QUEEN in abilities and position and self.bot.has_creep(
                                position):
                            actions.append(
                                queen(AbilityId.BUILD_CREEPTUMOR, position))
                            self.unselectable.append(queen)
                        else:
                            if queen.position.distance_to(
                                    extra_queens.center) > 2:
                                # regroup extra queens
                                actions.append(queen.move(extra_queens.center))
        execution_time = (time.time() - extra_queen_start_time) * 1000
        print(f'//// Extra queens: {round(execution_time, 3)}ms')

        creep_start_time = time.time()
        # CREEP TUMORS
        for tumor in self.bot.units(UnitTypeId.CREEPTUMORBURROWED).tags_not_in(
                self.dead_tumors.tags):
            # TODO: direct creep spread to some direction...
            # Dont overmake creep xd
            abilities = await self.bot.get_available_abilities(tumor)
            if AbilityId.BUILD_CREEPTUMOR_TUMOR in abilities:
                angle = random.randint(0, 360)
                x = math.cos(angle)
                y = math.sin(angle)
                position: Point2 = tumor.position + (9 * Point2((x, y)))
                if self.bot.has_creep(position) and not self.bot.units(
                        UnitTypeId.CREEPTUMORBURROWED).closer_than(
                            9, position
                        ).exists and not self.bot.position_blocks_expansion(
                            position):
                    actions.append(tumor(AbilityId.BUILD_CREEPTUMOR, position))
                    self.dead_tumors.append(tumor)
        execution_time = (time.time() - creep_start_time) * 1000
        print(f'//// Creep: {round(execution_time, 3)}ms')

        # OVERLORD retreat from enemy structures and anti air stuff
        for overlord in self.bot.units(OVERLORD).tags_not_in(
                self.unselectable.tags):
            threats: Units = self.bot.known_enemy_units.filter(
                lambda u: u.is_structure or u.can_attack_air).closer_than(
                    15, overlord)
            if threats.exists:
                destination: Point2 = overlord.position + 2 * threats.center.direction_vector(
                    overlord.position)
                actions.append(overlord.move(destination))

        # OVERSEERS
        overseers: Units = self.bot.units(OVERSEER)
        if overseers.exists:
            for overseer in overseers:
                if self.spread_overlords.find_by_tag(overseer.tag):
                    self.spread_overlords = self.spread_overlords.tags_not_in(
                        {overseer.tag})
                abilities = await self.bot.get_available_abilities(overseer)
                if AbilityId.SPAWNCHANGELING_SPAWNCHANGELING in abilities:
                    actions.append(
                        overseer(AbilityId.SPAWNCHANGELING_SPAWNCHANGELING))

        # CHANGELINGS
        changelings: Units = self.bot.units(CHANGELING).tags_not_in(
            self.unselectable.tags)
        if changelings.exists:
            for changeling in changelings:
                actions.append(changeling.move(self.bot.enemy_natural))
                self.unselectable.append(changeling)

        return actions

    def one_of_targets_in_range(self, unit: Unit, targets: Units):
        for target in targets:
            if unit.target_in_range(target):
                return True
        return False

    def group_army(self, army: Units) -> List[Units]:
        groups: List[Units] = []
        already_grouped_tags = []

        for unit in army:
            if unit.tag in already_grouped_tags:
                continue
            # TODO: fix recursive grouping
            # neighbors: Units = self.find_neighbors(unit, army.tags_not_in(set(already_grouped_tags)))
            neighbors: Units = army.closer_than(15, unit.position)
            groups.append(neighbors)
            already_grouped_tags.extend(neighbors.tags)

        return groups

    def find_neighbors(self, THE_SOURCE: Unit, units: Units) -> Units:
        neighbors: Units = units.closer_than(3, THE_SOURCE.position)

        temp: Units = Units([], self.bot._game_data)
        for individual in neighbors:
            temp.__or__(
                self.find_neighbors(individual,
                                    units.tags_not_in(neighbors.tags)))
        output = neighbors.__or__(temp)
        if output is None:
            return Units([], self.bot._game_data)
        return neighbors.__or__(temp)

    def command_group(self, units: Units, command: UnitCommand,
                      target: Union[Unit, Point2]):
        commands = []
        for unit in units:
            commands.append(unit(command, target))
        return commands

    async def inject(self):
        ready_queens = []
        actions = []
        for queen in self.bot.units(QUEEN).idle:
            abilities = await self.bot.get_available_abilities(queen)
            if AbilityId.EFFECT_INJECTLARVA in abilities:
                ready_queens.append(queen)
        for queen in ready_queens:
            actions.append(
                queen(AbilityId.EFFECT_INJECTLARVA,
                      self.bot.units(HATCHERY).first))
        return actions

    def evaluate_engagement(self,
                            own_units: Units,
                            enemy_units: Units,
                            bias=1):
        own_ranged: Units = own_units.filter(lambda u: u.ground_range > 3)
        own_melee: Units = own_units.tags_not_in(own_ranged.tags)
        enemy_ranged: Units = enemy_units.filter(lambda u: u.ground_range > 3)

        try:
            own_ranged_value = bias * self.bot.calculate_combat_value(
                own_ranged)
        except:
            own_ranged_value = 0
        try:
            enemy_ranged_value = self.bot.calculate_combat_value(enemy_ranged)
        except:
            enemy_ranged_value = 0

        corrected_own_value = bias * self.bot.calculate_combat_value(own_units)

        if own_ranged_value < enemy_ranged_value and own_units.exists:
            perimeter = self.get_enemy_perimeter(
                enemy_units.not_structure, self.bot.known_enemy_structures,
                own_units.center)
            if own_melee.exists:
                own_melee_value = bias * self.bot.calculate_combat_value(
                    Units(own_melee.take(perimeter * 2, require_all=False),
                          self.bot._game_data))
            else:
                own_melee_value = 0
            corrected_own_value = own_melee_value + own_ranged_value
        evaluation = corrected_own_value - self.bot.calculate_combat_value(
            enemy_units)
        return evaluation

    def get_enemy_perimeter(self, enemy_units: Units, enemy_structures: Units,
                            reference_position: Point2):
        perimeter = 0
        pathing_grid: PixelMap = self.bot._game_info.pathing_grid
        for enemy_unit in enemy_units:
            enemies_excluding_self: Units = enemy_units.tags_not_in(
                {enemy_unit.tag})
            pos: Point2 = enemy_unit.position
            positions = [
                Point2((pos.x - 1, pos.y + 1)),
                Point2((pos.x, pos.y + 1)),
                Point2((pos.x + 1, pos.y + 1)),
                Point2((pos.x - 1, pos.y)),
                # [pos.x, pos.y], disregard center point
                Point2((pos.x + 1, pos.y)),
                Point2((pos.x - 1, pos.y - 1)),
                Point2((pos.x, pos.y - 1)),
                Point2((pos.x + 1, pos.y - 1)),
            ]
            if reference_position.distance_to(enemy_unit.position) > 5:
                positions = remove_n_furthest_points(positions,
                                                     reference_position, 3)
            for p in positions:
                if pathing_grid[
                        math.floor(p.x), math.floor(
                            p.y
                        )] <= 0 and not enemies_excluding_self.closer_than(
                            1, p).exists and not enemy_structures.closer_than(
                                1, p).exists:
                    perimeter += 1
        return perimeter