コード例 #1
0
 def by_tags(self, tags: List[int]) -> Units:
     units = Units([], self.ai)
     for tag in tags:
         unit = self.tag_cache.get(tag, None)
         if unit:
             units.append(unit)
     return units
コード例 #2
0
 def get_all_units(self) -> Units:
     units = Units([], self.ai)
     for tag in self.tags:
         unit = self.cache.by_tag(tag)
         if unit:
             units.append(unit)
     return units
コード例 #3
0
    def handle_counter(self) -> bool:
        attackers = Units([], self.ai)
        for tag in self.tags:
            unit = self.cache.by_tag(tag)
            if unit:
                attackers.append(unit)

        if not attackers.exists:
            self.has_failed = True
            return True

        self.roles.set_tasks(UnitTask.Attacking, attackers)
        attackers_left = attackers.amount

        for attacker in attackers:  # type: Unit
            if attacker.weapon_cooldown > 10 and attacker.shield_health_percentage < 0.5:
                self.do(attacker.gather(self.gather_mf))
            else:
                own = self.cache.own_in_range(attacker.position, 3).amount
                enemies = self.cache.enemy_in_range(attacker.position, 3)
                enemy_count = enemies.amount

                if own >= attackers_left or enemy_count <= own:
                    self.combat.add_unit(attacker)
                else:
                    # Regroup
                    if attacker.distance_to(self.gather_mf) < 5:
                        # On other option but to fight
                        self.combat.add_units(attackers)
                    else:
                        self.do(attacker.gather(self.gather_mf))

        self.combat.execute(self.knowledge.enemy_main_zone.center_location)
        return False
コード例 #4
0
    def _defenders_from(self, task: UnitTask, current_power: ExtendedPower,
                        position: Point2, power: ExtendedPower, units: Units):
        """ Get defenders from a task. """
        if current_power.is_enough_for(power):
            return

        exclude_types = []
        exclude_types.append(UnitTypeId.OVERSEER)
        exclude_types.extend(self.knowledge.unit_values.worker_types)
        exclude_types.extend(self.peace_unit_types)

        role_units = self.roles[task.value].units\
            .exclude_type(exclude_types)

        unit: Unit
        for unit in role_units.sorted_by_distance_to(position):
            enough_air_power = current_power.air_power >= power.air_presence * 1.1
            enough_ground_power = current_power.ground_power >= power.ground_presence * 1.1

            if not self.unit_values.can_shoot_air(
                    unit) and not enough_air_power and enough_ground_power:
                # Don't pull any more units that can't actually shoot the targets
                continue

            if not self.unit_values.can_shoot_ground(
                    unit) and enough_air_power and not enough_ground_power:
                # Don't pull any more units that can't actually shoot the targets
                continue

            current_power.add_unit(unit)
            units.append(unit)
            if current_power.is_enough_for(power):
                return
        return
コード例 #5
0
    def own_in_range(self, position: Point2, range: Union[int, float]) -> Units:
        units = Units([], self.ai)
        if self.own_tree is None:
            return units

        for index in self.own_tree.query_ball_point(np.array([position.x, position.y]), range):
            units.append(self.all_own[index])

        return units
コード例 #6
0
    def include_own_units(self, unit: Unit, units: Units, lookup_distance: float, index: int, assigned: Dict[int, int]):
        units_close_by = self.cache.own_in_range(unit.position, lookup_distance)

        for unit_close in units_close_by:
            if unit_close.tag in assigned or unit_close.tag not in self.tags:
                continue

            assigned[unit_close.tag] = index
            units.append(unit_close)
            self.include_own_units(unit_close, units, lookup_distance, index, assigned)
コード例 #7
0
    def enemy_in_range(self, position: Point2, range: Union[int, float], only_targetable = True) -> Units:
        units = Units([], self.ai)
        if self.enemy_tree is None:
            return units

        for index in self.enemy_tree.query_ball_point(np.array([position.x, position.y]), range):
            units.append(self.knowledge.known_enemy_units[index])

        if only_targetable:
            return units.filter(lambda x: x.can_be_attacked or x.is_snapshot)
        return units
コード例 #8
0
ファイル: memory_manager.py プロジェクト: merfolk/sharpy-sc2
    def ghost_units(self) -> Units:
        """Returns latest snapshot for all units that we know of but which are currently not visible."""
        memory_units = Units([], self.ai)

        for tag in self._memory_units_by_tag:
            if self.is_unit_visible(tag):
                continue

            snap = self.get_latest_snapshot(tag)
            memory_units.append(snap)

        return memory_units
コード例 #9
0
    async def execute(self) -> bool:
        target = self._get_target()

        if target is None:
            # Enemy known bases destroyed.
            self.status = AttackStatus.NotActive
            return True

        unit: Unit
        if self.status == AttackStatus.Attacking:
            self.handle_attack(target)

        elif self.attack_retreat_started is not None:
            attacking_units = self.knowledge.roles.attacking_units
            self.roles.refresh_tasks(attacking_units)

            for unit in attacking_units:
                pos: Point2 = unit.position
                at_gather_point = pos.distance_to(
                    self.knowledge.gather_point
                ) < RETREAT_STOP_DISTANCE_SQUARED
                if at_gather_point:
                    # self.print(f"Unit {unit.type_id} {unit.tag} has reached gather point. Stopping retreat.")
                    self.knowledge.roles.clear_task(unit)
                elif self.status == AttackStatus.Withdraw:
                    self.combat.add_unit(unit)
                else:
                    self.combat.add_unit(unit)

            self.combat.execute(self.knowledge.gather_point,
                                MoveType.DefensiveRetreat)

            if self.attack_retreat_started + RETREAT_TIME < self.ai.time:
                # Stop retreat next turn
                self._stop_retreat()
        else:
            self.knowledge.roles.attack_ended()
            attackers = Units([], self.ai)
            for unit in self.knowledge.roles.free_units:
                if self.knowledge.should_attack(unit):
                    attackers.append(unit)

            own_power = self.unit_values.calc_total_power(attackers)

            if self._should_attack(own_power):
                self._start_attack(own_power, attackers)

        return False  # Blocks!
コード例 #10
0
    def get_army(self, target: Point2, attacker_count: int) -> Units:
        # Clear defenders
        defenders = self.roles.all_from_task(UnitTask.Defending)
        self.roles.clear_tasks(defenders.tags)

        count = 0
        army = Units([], self.ai)
        for unit in self.roles.free_workers.sorted_by_distance_to(target):  # type: Unit

            count += 1
            army.append(unit)
            self.tags.append(unit.tag)
            if count >= attacker_count:
                break

        old_defenders = defenders.tags_not_in(self.tags)
        for unit in old_defenders:
            self.do(unit.stop())
        return army
コード例 #11
0
class BallFormation():
    def __init__(self, knowledge):
        self.ai = knowledge.ai
        self.knowledge: 'Knowledge' = knowledge
        self.unit_values: 'UnitValue' = knowledge.unit_values
        self.our_units: Units
        self.keep_together: List[UnitTypeId] = [
            UnitTypeId.COLOSSUS, UnitTypeId.OBSERVER, UnitTypeId.PHOENIX
        ]
        self.enemy_units_in_combat: Units
        self.units_in_combat: Units
        self.units_to_regroup: Units
        self.minimum_distance = 3.5

    def prepare_solve(self, our_units: Units, goal_position: Point2,
                      combat_data: Dict[int, EnemyData], units_median: Point2):
        self.our_units = our_units

        time = self.knowledge.ai.time
        units_behind_tags = []
        units_behind_tags.clear()
        average_distance2 = 0
        wait_ended = False

        self.enemy_units_in_combat = Units([], self.ai)
        self.units_in_combat = Units([], self.ai)

        unit_count = len(our_units)
        # wait for 15% reinforcements
        wait_count = unit_count * 0.15
        if any(our_units):
            our_units = our_units.sorted_by_distance_to(goal_position)

            self.units_gather = units_median

            for unit in our_units:

                enemy_data = combat_data[unit.tag]
                if enemy_data.powered_enemies.exists:
                    self.enemy_units_in_combat.append(enemy_data.closest)
                    self.units_in_combat.append(unit)
                elif enemy_data.enemies_exist:
                    self.units_in_combat.append(unit)

    def solve_combat(self, goal: CombatGoal,
                     command: CombatAction) -> CombatAction:
        if self.enemy_units_in_combat.exists:
            # Move in to assist closest friendly in combat
            closest_enemy = self.enemy_units_in_combat.closest_to((goal.unit))
            return CombatAction(goal.unit, closest_enemy.position,
                                command.is_attack)

        if goal.unit.distance_to(
                self.units_gather
        ) > self.minimum_distance + len(self.our_units) / 10:
            return CombatAction(goal.unit, self.units_gather, False)
        return command
コード例 #12
0
 def get_all_units(self) -> Units:
     units = Units([], self.ai)
     for cmd in self.unit_goals:
         units.append(cmd.unit)
     return units
コード例 #13
0
    async def distribute_workers(self,
                                 performanceHeavy=True,
                                 onlySaturateGas=False):
        mineralTags = [x.tag for x in self.mineral_field]
        gas_buildingTags = [x.tag for x in self.gas_buildings]

        workerPool = Units([], self)
        workerPoolTags = set()

        # Find all gas_buildings that have surplus or deficit
        deficit_gas_buildings = {}
        surplusgas_buildings = {}
        for g in self.gas_buildings.filter(lambda x: x.vespene_contents > 0):
            # Only loop over gas_buildings that have still gas in them
            deficit = g.ideal_harvesters - g.assigned_harvesters
            if deficit > 0:
                deficit_gas_buildings[g.tag] = {"unit": g, "deficit": deficit}
            elif deficit < 0:
                surplusWorkers = self.workers.closer_than(10, g).filter(
                    lambda w: w not in workerPoolTags and len(w.orders) == 1
                    and w.orders[0].ability.id in [AbilityId.HARVEST_GATHER]
                    and w.orders[0].target in gas_buildingTags)
                for i in range(-deficit):
                    if surplusWorkers.amount > 0:
                        w = surplusWorkers.pop()
                        workerPool.append(w)
                        workerPoolTags.add(w.tag)
                surplusgas_buildings[g.tag] = {"unit": g, "deficit": deficit}

        # Find all townhalls that have surplus or deficit
        deficitTownhalls = {}
        surplusTownhalls = {}
        if not onlySaturateGas:
            for th in self.townhalls:
                deficit = th.ideal_harvesters - th.assigned_harvesters
                if deficit > 0:
                    deficitTownhalls[th.tag] = {"unit": th, "deficit": deficit}
                elif deficit < 0:
                    surplusWorkers = self.workers.closer_than(
                        10,
                        th).filter(lambda w: w.tag not in workerPoolTags and
                                   len(w.orders) == 1 and w.orders[0].ability.
                                   id in [AbilityId.HARVEST_GATHER] and w.
                                   orders[0].target in mineralTags)
                    # workerPool.extend(surplusWorkers)
                    for i in range(-deficit):
                        if surplusWorkers.amount > 0:
                            w = surplusWorkers.pop()
                            workerPool.append(w)
                            workerPoolTags.add(w.tag)
                    surplusTownhalls[th.tag] = {"unit": th, "deficit": deficit}

            if all([
                    len(deficit_gas_buildings) == 0,
                    len(surplusgas_buildings) == 0,
                    len(surplusTownhalls) == 0 or deficitTownhalls == 0,
            ]):
                # Cancel early if there is nothing to balance
                return

        # Check if deficit in gas less or equal than what we have in surplus, else grab some more workers from surplus bases
        deficitGasCount = sum(
            gasInfo["deficit"]
            for gasTag, gasInfo in deficit_gas_buildings.items()
            if gasInfo["deficit"] > 0)
        surplusCount = sum(-gasInfo["deficit"]
                           for gasTag, gasInfo in surplusgas_buildings.items()
                           if gasInfo["deficit"] < 0)
        surplusCount += sum(-thInfo["deficit"]
                            for thTag, thInfo in surplusTownhalls.items()
                            if thInfo["deficit"] < 0)

        if deficitGasCount - surplusCount > 0:
            # Grab workers near the gas who are mining minerals
            for gTag, gInfo in deficit_gas_buildings.items():
                if workerPool.amount >= deficitGasCount:
                    break
                workersNearGas = self.workers.closer_than(
                    10, gInfo["unit"]).filter(
                        lambda w: w.tag not in workerPoolTags and len(
                            w.orders) == 1 and w.orders[0].ability.id in [
                                AbilityId.HARVEST_GATHER
                            ] and w.orders[0].target in mineralTags)
                while workersNearGas.amount > 0 and workerPool.amount < deficitGasCount:
                    w = workersNearGas.pop()
                    workerPool.append(w)
                    workerPoolTags.add(w.tag)

        # Now we should have enough workers in the pool to saturate all gases, and if there are workers left over, make them mine at townhalls that have mineral workers deficit
        for gTag, gInfo in deficit_gas_buildings.items():
            if performanceHeavy:
                # Sort furthest away to closest (as the pop() function will take the last element)
                workerPool.sort(key=lambda x: x.distance_to(gInfo["unit"]),
                                reverse=True)
            for i in range(gInfo["deficit"]):
                if workerPool.amount > 0:
                    w = workerPool.pop()
                    if len(w.orders) == 1 and w.orders[0].ability.id in [
                            AbilityId.HARVEST_RETURN
                    ]:
                        w.gather(gInfo["unit"], queue=True)
                    else:
                        w.gather(gInfo["unit"])

        if not onlySaturateGas:
            # If we now have left over workers, make them mine at bases with deficit in mineral workers
            for thTag, thInfo in deficitTownhalls.items():
                if performanceHeavy:
                    # Sort furthest away to closest (as the pop() function will take the last element)
                    workerPool.sort(
                        key=lambda x: x.distance_to(thInfo["unit"]),
                        reverse=True)
                for i in range(thInfo["deficit"]):
                    if workerPool.amount > 0:
                        w = workerPool.pop()
                        mf = self.mineral_field.closer_than(
                            10, thInfo["unit"]).closest_to(w)
                        if len(w.orders) == 1 and w.orders[0].ability.id in [
                                AbilityId.HARVEST_RETURN
                        ]:
                            w.gather(mf, queue=True)
                        else:
                            w.gather(mf)
コード例 #14
0
    def solve_combat(self, goal: CombatGoal, command: CombatAction, enemies: EnemyData) -> List[CombatAction]:
        oracle = goal.unit

        if not oracle.has_buff(BuffId.ORACLEWEAPON):
            goal.ready_to_shoot = False

        air_shooter_enemies = Units([], self.ai)
        enemy: Unit

        power = ExtendedPower(self.unit_values)

        for enemy in enemies.close_enemies:
            if self.unit_values.air_range(enemy) < enemy.distance_to(oracle) + 1:
                air_shooter_enemies.append(enemy)
                power.add_unit(enemy)
                if self.unit_values.is_static_air_defense(enemy):
                    power.add(5) # can't beat turrets with oracle

        enemy_center = enemies.close_enemies.center

        for air_shooter in air_shooter_enemies:  # type: Unit
            if air_shooter.is_light and not air_shooter.is_flying:
                power.add_units(air_shooter_enemies)
            else:
                power.add_units(air_shooter_enemies * 2)

        time = self.knowledge.ai.time
        if goal.move_type == MoveType.PanicRetreat and oracle.has_buff(BuffId.ORACLEWEAPON):
            return self.disable_beam(oracle)

        possible_targets = enemies.close_enemies.filter(lambda u: not u.is_flying and not u.is_structure and u.is_light)

        if possible_targets.exists:
            if oracle.energy > 50 and possible_targets.closest_distance_to(oracle) < 5 and not oracle.has_buff(BuffId.ORACLEWEAPON):
                return self.enable_beam(oracle)

        if  power.air_power > 0 and power.air_power <= 3:
            target = air_shooter_enemies.closest_to(oracle)
            if target.is_light or target.health_percentage < 0.5:
                if not oracle.has_buff(BuffId.ORACLEWEAPON):
                    return self.enable_beam(oracle)
                # Kill the target
                return [CombatAction(oracle, target, True)]

            #target_pos = self.knowledge.pathing_manager.find_weak_influence_air(goal.target, 7)
            #move_step = self.knowledge.pathing_manager.find_influence_air_path(oracle.position, target_pos)
            return [CombatAction(oracle, target.position, True)]
        elif goal.ready_to_shoot and possible_targets:
            return [CombatAction(oracle, possible_targets.closest_to(oracle), True)]
        elif power.air_power > 12:
            # Panic retreat to whatever direction
            if goal.move_type in offensive:
                new_target: Point2 = self.knowledge.pathing_manager.find_weak_influence_air(goal.target, 7)
                step = self.knowledge.pathing_manager.find_influence_air_path(oracle.position, new_target)
                # backstep: Point2 = self.knowledge.pathing_manager.find_weak_influence_air(oracle.position, 7)
                move_action = CombatAction(oracle, step, False)
            else:
                backstep = self.knowledge.pathing_manager.find_influence_air_path(oracle.position, goal.target)
                move_action = CombatAction(oracle, backstep, False)

            # Todo disable beam?
            return [move_action]

        elif power.air_power > 3:
            # Try kiting while killing the target
            target = self.knowledge.pathing_manager.find_weak_influence_air(goal.target, 7)
            backstep = self.knowledge.pathing_manager.find_influence_air_path(oracle.position, target)

            if goal.move_type in offensive:
                move_action = CombatAction(oracle, backstep, False)
            else:
                move_action = CombatAction(oracle, backstep, False)

            if oracle.has_buff(BuffId.ORACLEWEAPON):
                if possible_targets:
                    closest = possible_targets.closest_to(oracle)
                    if closest.distance_to(oracle) < 5:
                        return [CombatAction(oracle, closest, True)]
                return [CombatAction(oracle, command.target, True), move_action]
            else:
                return [move_action]


        if possible_targets.exists:
            return [CombatAction(oracle, command.target, True)]
        else:
            return [CombatAction(oracle, command.target, False)]
コード例 #15
0
class WorkerManager:
    """ Responsible for managing the workers """

    GATHER_RANGE = 1.4
    MINERAL_POP_RANGE_MAX = 0.2
    MINERAL_POP_RANGE_MIN = 0.001

    def __init__(self, bot: CheatMoney, minerals):
        self.bot = bot
        self.minerals = Units(minerals, self.bot)
        self.workers = Units([], self.bot)

        for mineral in self.minerals:
            mineral.workers_assigned = 0

        print(f'PATH_UNTIL_RANGE: {self.GATHER_RANGE}')

    async def add(self, worker: Unit):
        self.workers.append(worker)

        # assign workers to each mineral patch
        # prioritize the closest minerals. maximum 2 workers per patch
        for mineral in self.minerals.sorted_by_distance_to(worker):
            if mineral.workers_assigned < 2:

                # if there is already a worker assigned to this patch, assign our worker partners
                if mineral.workers_assigned == 1:
                    for worker_partner in self.workers:
                        if hasattr(
                                worker_partner, 'assigned_mineral'
                        ) and worker_partner.assigned_mineral == mineral:
                            worker.worker_partner = worker_partner
                            worker_partner.worker_partner = worker

                worker.assigned_mineral = mineral
                mineral.workers_assigned += 1
                break

    async def on_step(self, iteration):
        for worker in self.workers:
            # for some reason the work in our list doesn't get its data updated, so we need to get this one
            updated_worker = self.bot.workers.find_by_tag(worker.tag)
            if updated_worker.is_carrying_minerals:  # if worker has minerals, return to base
                # check for mineral popping opportunity
                if hasattr(worker, 'worker_partner') \
                    and self.in_mineral_pop_range(worker) \
                    and self.on_correct_side_of_partner(worker)\
                    and updated_worker.distance_to(self.bot.hq_location) > 4:
                    self.bot.do(updated_worker.move(self.bot.hq_location))
                else:
                    self.bot.do(updated_worker.return_resource())
            # if the worker is over a certain distance away, path to mineral patch
            elif updated_worker.distance_to(
                    worker.assigned_mineral.position) > self.GATHER_RANGE:
                pos = updated_worker.position - self.bot.hq_location
                norm = preprocessing.normalize([pos], norm='l1')[0]
                self.bot.do(
                    updated_worker.move(worker.assigned_mineral.position -
                                        Point2((norm[0], norm[1]))))
            # if the worker is in range to gather, issue a gather command
            else:
                self.bot.do(updated_worker.gather(worker.assigned_mineral))

    def in_mineral_pop_range(self, worker):
        # for some reason the work in our list doesn't get its data updated, so we need to get this one
        updated_worker = self.bot.workers.find_by_tag(worker.tag)
        updated_worker_partner = self.bot.workers.find_by_tag(
            worker.worker_partner.tag)
        pos = updated_worker.position - updated_worker_partner.position
        range = math.hypot(pos[0], pos[1])
        return range < self.MINERAL_POP_RANGE_MAX and range > self.MINERAL_POP_RANGE_MIN

    def on_correct_side_of_partner(self, worker):
        # for some reason the work in our list doesn't get its data updated, so we need to get this one
        updated_worker = self.bot.workers.find_by_tag(worker.tag)
        updated_worker_partner = self.bot.workers.find_by_tag(
            worker.worker_partner.tag)
        return updated_worker_partner.distance_to(
            worker.assigned_mineral.position) < updated_worker.distance_to(
                worker.assigned_mineral.position)
コード例 #16
0
ファイル: unit_manager.py プロジェクト: Blodir/SLUDGEMENT
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