Example #1
0
class CombatUnits():
    def __init__(self, units: Units, knowledge: 'Knowledge'):
        self.knowledge = knowledge
        self.unit_values = knowledge.unit_values
        self.units = units
        self.center = sc2math.unit_geometric_median(units)
        self.ground_units = self.units.not_flying
        if self.ground_units:
            self.center = self.ground_units.closest_to((self.center)).position

        self.power = ExtendedPower(self.unit_values)
        self.power.add_units(self.units)
        self.debug_index = 0
        self._total_distance: Optional[float] = None
        self._area_by_circles: float = 0

    def is_too_spread_out(self) -> bool:
        if self._total_distance is None:
            self._total_distance = 0
            self._area_by_circles = 5

            for unit in self.units:
                d = unit.distance_to(self.center)
                self._total_distance += d
                self._area_by_circles += unit.radius ** 2
        # self.knowledge.print(f"spread: {self._total_distance} d to {self._total_radius} r")
        return (self._total_distance / len(self.units)) ** 2 > self._area_by_circles * 2

    def is_in_combat(self, closest_enemies: 'CombatUnits') -> bool:
        if closest_enemies is None:
            return False

        distance = self.center.distance_to_point2(closest_enemies.center)
        if distance > 17:
            return False

        if distance < 10 \
                or self.knowledge.unit_cache.enemy_in_range(self.center, 10).exclude_type(self.unit_values.combat_ignore):
           return True

        engaged_power = 0
        total_power = 0

        for unit in self.units:  # type: Unit
            power = self.unit_values.power(unit)
            total_power += power

            for enemy_near in closest_enemies.units:
                d = enemy_near.distance_to(unit)
                if d < self.unit_values.real_range(unit, enemy_near, self.knowledge):
                    engaged_power += power
                    break

        return engaged_power > total_power * 0.15
Example #2
0
    def calc_needs_evacuation(self):
        """
        Checks if the zone needs evacuation for the workers mining there.
        This is a method because it is quite CPU heavy.
        """

        enemies: Units = self.cache.enemy_in_range(self.mineral_line_center,
                                                   10)
        power = ExtendedPower(self.unit_values)
        power.add_units(enemies)
        if power.ground_power > 3 and enemies.exclude_type(
                self.unit_values.worker_types):
            self.needs_evacuation = True
        else:
            self.needs_evacuation = False
Example #3
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)]
Example #4
0
 def enemy_static_power(self) -> ExtendedPower:
     """Returns power of enemy static defenses."""
     power = ExtendedPower(self.unit_values)
     power.add_units(self.enemy_static_defenses)
     return power
Example #5
0
class GroupCombatManager(ManagerBase):
    def __init__(self):
        super().__init__()

    async def start(self, knowledge: 'Knowledge'):
        await super().start(knowledge)
        self.cache: UnitCacheManager = self.knowledge.unit_cache
        self.pather: PathingManager = self.knowledge.pathing_manager
        self.tags: List[int] = []

        self.unit_micros: Dict[UnitTypeId, MicroStep] = dict()
        self.all_enemy_power = ExtendedPower(self.unit_values)

        # Micro controllers / handlers
        self.unit_micros[UnitTypeId.DRONE] = MicroWorkers(knowledge)
        self.unit_micros[UnitTypeId.PROBE] = MicroWorkers(knowledge)
        self.unit_micros[UnitTypeId.SCV] = MicroWorkers(knowledge)

        # Protoss
        self.unit_micros[UnitTypeId.ARCHON] = NoMicro(knowledge)
        self.unit_micros[UnitTypeId.ADEPT] = MicroAdepts(knowledge)
        self.unit_micros[UnitTypeId.CARRIER] = MicroCarriers(knowledge)
        self.unit_micros[UnitTypeId.COLOSSUS] = MicroColossi(knowledge)
        self.unit_micros[UnitTypeId.DARKTEMPLAR] = MicroZerglings(knowledge)
        self.unit_micros[UnitTypeId.DISRUPTOR] = MicroDisruptor(knowledge)
        self.unit_micros[UnitTypeId.DISRUPTORPHASED] = MicroPurificationNova(
            knowledge)
        self.unit_micros[UnitTypeId.HIGHTEMPLAR] = MicroHighTemplars(knowledge)
        self.unit_micros[UnitTypeId.OBSERVER] = MicroObservers(knowledge)
        self.unit_micros[UnitTypeId.ORACLE] = MicroOracles(knowledge)
        self.unit_micros[UnitTypeId.PHOENIX] = MicroPhoenixes(knowledge)
        self.unit_micros[UnitTypeId.SENTRY] = MicroSentries(knowledge)
        self.unit_micros[UnitTypeId.STALKER] = MicroStalkers(knowledge)
        self.unit_micros[UnitTypeId.WARPPRISM] = MicroWarpPrism(knowledge)
        self.unit_micros[UnitTypeId.VOIDRAY] = MicroVoidrays(knowledge)
        self.unit_micros[UnitTypeId.ZEALOT] = MicroZealots(knowledge)

        # Zerg
        self.unit_micros[UnitTypeId.ZERGLING] = MicroZerglings(knowledge)
        self.unit_micros[UnitTypeId.ULTRALISK] = NoMicro(knowledge)
        self.unit_micros[UnitTypeId.OVERSEER] = MicroOverseers(knowledge)
        self.unit_micros[UnitTypeId.QUEEN] = MicroQueens(knowledge)

        # Terran
        self.unit_micros[UnitTypeId.HELLIONTANK] = NoMicro(knowledge)
        self.unit_micros[UnitTypeId.SIEGETANK] = MicroTanks(knowledge)
        self.unit_micros[UnitTypeId.VIKINGFIGHTER] = MicroVikings(knowledge)
        self.unit_micros[UnitTypeId.MARINE] = MicroBio(knowledge)
        self.unit_micros[UnitTypeId.MARAUDER] = MicroBio(knowledge)

        self.generic_micro = GenericMicro(knowledge)
        self.regroup_threshold = 0.75

    async def update(self):
        self.enemy_groups: List[CombatUnits] = self.group_enemy_units()
        self.all_enemy_power.clear()

        for group in self.enemy_groups:  # type: CombatUnits
            self.all_enemy_power.add_units(group.units)

    async def post_update(self):
        pass

    @property
    def debug(self):
        return self._debug and self.knowledge.debug

    def add_unit(self, unit: Unit):
        if unit.type_id in ignored:  # Just no
            return

        self.tags.append(unit.tag)

    def add_units(self, units: Units):
        for unit in units:
            self.add_unit(unit)

    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

    def execute(self, target: Point2, move_type=MoveType.Assault):
        our_units = self.get_all_units()
        if len(our_units) < 1:
            return

        self.own_groups: List[CombatUnits] = self.group_own_units(
            our_units, 12)

        total_power = ExtendedPower(self.unit_values)

        for group in self.own_groups:
            total_power.add_power(group.power)

        if self.debug:
            fn = lambda group: group.center.distance_to(self.ai.start_location)
            sorted_list = sorted(self.own_groups, key=fn)
            for i in range(0, len(sorted_list)):
                sorted_list[i].debug_index = i

        for group in self.own_groups:
            center = group.center
            closest_enemies = self.closest_group(center, self.enemy_groups)

            if closest_enemies is None:
                if move_type == MoveType.PanicRetreat:
                    self.move_to(group, target, move_type)
                else:
                    self.attack_to(group, target, move_type)
            else:
                power = group.power
                enemy_power = ExtendedPower(closest_enemies)
                enemy_center = closest_enemies.center

                is_in_combat = group.is_in_combat(closest_enemies)
                # pseudocode for attack

                if move_type == MoveType.DefensiveRetreat or move_type == MoveType.PanicRetreat:
                    self.move_to(group, target, move_type)
                    break

                if power.power > self.regroup_threshold * total_power.power:
                    # Most of the army is here
                    if (group.is_too_spread_out() and not is_in_combat):
                        self.regroup(group, group.center)
                    else:
                        self.attack_to(group, target, move_type)

                elif is_in_combat:
                    if not power.is_enough_for(enemy_power, 0.75):
                        # Regroup if possible
                        own_closest_group = self.closest_group(
                            center, self.own_groups)

                        if own_closest_group:
                            self.move_to(group, own_closest_group.center,
                                         MoveType.ReGroup)
                        else:
                            # fight to bitter end
                            self.attack_to(group, closest_enemies.center,
                                           move_type)
                    else:
                        self.attack_to(group, closest_enemies.center,
                                       move_type)
                else:
                    if group.power.is_enough_for(self.all_enemy_power, 0.85):
                        # We have enough units here to crush everything the enemy has
                        self.attack_to(group, closest_enemies.center,
                                       move_type)
                    else:
                        # Regroup if possible
                        if move_type == MoveType.Assault:
                            # Group towards attack target
                            own_closest_group = self.closest_group(
                                target, self.own_groups)
                        else:
                            # Group up with closest group
                            own_closest_group = self.closest_group(
                                center, self.own_groups)

                        if own_closest_group:
                            self.move_to(group, own_closest_group.center,
                                         MoveType.ReGroup)
                        else:
                            # fight to bitter end
                            self.attack_to(group, closest_enemies.center,
                                           move_type)

                # if move_type == MoveType.SearchAndDestroy:
                #     enemy_closest_group = self.closest_group(center, self.enemy_groups)
                #     if enemy_closest_group:
                #         self.attack_to(actions, group, closest_enemies.center, move_type)
                #     else:
                #         self.attack_to(actions, group, target, move_type)
                # elif power.is_enough_for(enemy_power, 0.75):
                #     if move_type == MoveType.PanicRetreat:
                #         self.move_to(actions, group, target, move_type)
                #     else:
                #         self.attack_to(actions, group, closest_enemies.center, move_type)
                #
                # elif distance > 12:
                #     own_closest_group = self.closest_group(center, self.own_groups)
                #
                #     if own_closest_group:
                #         self.move_to(actions, group, own_closest_group.center, MoveType.ReGroup)
                #
                #     elif move_type == MoveType.PanicRetreat:
                #         self.move_to(actions, group, target, move_type)
                #     else:
                #         self.attack_to(actions, group, enemy_center, move_type)
                #
                # elif enemy_power.is_enough_for(power, 0.75):
                #     own_closest_group = self.closest_group(center, self.own_groups)
                #
                #     if own_closest_group:
                #         self.move_to(actions, group, own_closest_group.center, MoveType.ReGroup)
                #
                # elif move_type == MoveType.PanicRetreat:
                #     self.move_to(actions, group, target, move_type)
                # else:
                #     self.attack_to(actions, group, target, move_type)

        self.tags.clear()

    def regroup(self, group: CombatUnits, target: Union[Unit, Point2]):
        if isinstance(target, Unit):
            target = self.pather.find_path(group.center, target.position, 1)
        else:
            target = self.pather.find_path(group.center, target, 3)
        self.move_to(group, target, MoveType.Push)

    def move_to(self, group: CombatUnits, target, move_type: MoveType):
        self.action_to(group, target, move_type, False)

    def attack_to(self, group: CombatUnits, target, move_type: MoveType):
        self.action_to(group, target, move_type, True)

    def action_to(self, group: CombatUnits, target, move_type: MoveType,
                  is_attack: bool):
        if isinstance(target, Point2) and group.ground_units:
            if move_type in {MoveType.DefensiveRetreat, MoveType.PanicRetreat}:
                target = self.pather.find_influence_ground_path(
                    group.center, target, 14)
            else:
                target = self.pather.find_path(group.center, target, 14)

        own_unit_cache: Dict[UnitTypeId, Units] = {}

        for unit in group.units:
            units = own_unit_cache.get(unit.type_id, Units([], self.ai))
            if units.amount == 0:
                real_type = self.unit_values.real_type(unit.type_id)
                own_unit_cache[real_type] = units

            units.append(unit)

        for type_id, type_units in own_unit_cache.items():
            micro: MicroStep = self.unit_micros.get(type_id,
                                                    self.generic_micro)
            micro.init_group(group, type_units, self.enemy_groups, move_type)
            group_action = micro.group_solve_combat(type_units,
                                                    Action(target, is_attack))

            for unit in type_units:
                action = micro.unit_solve_combat(unit, group_action)
                order = action.to_commmand(unit)
                if order:
                    self.ai.do(order)

                if self.debug:
                    if action.debug_comment:
                        status = action.debug_comment
                    elif action.ability:
                        status = action.ability.name
                    elif action.is_attack:
                        status = "Attack"
                    else:
                        status = "Move"
                    if action.target is not None:
                        if isinstance(action.target, Unit):
                            status += f": {action.target.type_id.name}"
                        else:
                            status += f": {action.target}"

                    status += f" G: {group.debug_index}"
                    status += f"\n{move_type.name}"

                    pos3d: Point3 = unit.position3d
                    pos3d = Point3((pos3d.x, pos3d.y, pos3d.z + 2))
                    self.ai._client.debug_text_world(status, pos3d, size=10)

    def closest_group(
            self, start: Point2,
            combat_groups: List[CombatUnits]) -> Optional[CombatUnits]:
        group = None
        best_distance = 50  # doesn't find enemy groups closer than this

        for combat_group in combat_groups:
            center = combat_group.center

            if center == start:
                continue  # it's the same group!

            distance = start.distance_to(center)
            if distance < best_distance:
                best_distance = distance
                group = combat_group

        return group

    def group_own_units(self,
                        our_units: Units,
                        lookup_distance: float = 7) -> List[CombatUnits]:
        groups: List[Units] = []
        assigned: Dict[int, int] = dict()

        for unit in our_units:
            if unit.tag in assigned:
                continue

            units = Units([unit], self.ai)
            index = len(groups)

            assigned[unit.tag] = index

            groups.append(units)
            self.include_own_units(unit, units, lookup_distance, index,
                                   assigned)

        return [CombatUnits(u, self.knowledge) for u in groups]

    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)

    def group_enemy_units(self) -> List[CombatUnits]:
        groups: List[Units] = []
        assigned: Dict[int, int] = dict()
        lookup_distance = 7

        for unit in self.knowledge.known_enemy_units_mobile:
            if unit.tag in assigned or unit.type_id in self.unit_values.combat_ignore or not unit.can_be_attacked:
                continue

            units = Units([unit], self.ai)
            index = len(groups)

            assigned[unit.tag] = index

            groups.append(units)
            self.include_enemy_units(unit, units, lookup_distance, index,
                                     assigned)

        return [CombatUnits(u, self.knowledge) for u in groups]

    def include_enemy_units(self, unit: Unit, units: Units,
                            lookup_distance: float, index: int,
                            assigned: Dict[int, int]):
        units_close_by = self.cache.enemy_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 or not unit.can_be_attacked:
                continue

            assigned[unit_close.tag] = index
            units.append(unit_close)
            self.include_enemy_units(unit_close, units, lookup_distance, index,
                                     assigned)
Example #6
0
class MicroStep(ABC):
    def __init__(self, knowledge):
        self.knowledge: 'Knowledge' = knowledge
        self.ai: sc2.BotAI = knowledge.ai
        self.unit_values: UnitValue = knowledge.unit_values
        self.cd_manager: CooldownManager = knowledge.cooldown_manager
        self.pather: PathingManager = knowledge.pathing_manager
        self.cache: UnitCacheManager = knowledge.unit_cache
        self.delay_to_shoot = self.ai._client.game_step + 1.5
        self.enemy_groups: List[CombatUnits] = []
        self.ready_to_attack_ratio: float = 0.0
        self.center: Point2 = Point2((0, 0))
        self.group: CombatUnits
        self.engage_ratio = 0
        self.can_engage_ratio = 0
        self.closest_group: CombatUnits
        self.engaged: Dict[int, List[int]] = dict()
        self.engaged_power = ExtendedPower(knowledge.unit_values)
        self.our_power = ExtendedPower(knowledge.unit_values)
        self.closest_units: Dict[int, Optional[Unit]] = dict()
        self.move_type = MoveType.Assault

    def init_group(self, group: CombatUnits, units: Units, enemy_groups: List[CombatUnits], move_type: MoveType):
        self.group = group
        self.move_type = move_type
        ready_to_attack = 0

        self.our_power = group.power
        self.closest_units.clear()
        self.engaged_power.clear()

        self.closest_group = None
        self.closest_group_distance = 1000000
        for enemy_group in enemy_groups:
            d = enemy_group.center.distance_to(group.center)
            if d < self.closest_group_distance:
                self.closest_group_distance = d
                self.closest_group = enemy_group

        self.enemy_groups = enemy_groups
        self.center = units.center
        self.enemies_near_by: Units = self.knowledge.unit_cache.enemy_in_range(self.center, 15 + len(group.units) * 0.1)

        self.engaged_power.add_units(self.enemies_near_by)

        engage_count = 0
        can_engage_count = 0
        for unit in units:
            closest_distance = 1000
            if self.ready_to_shoot(unit):
                ready_to_attack += 1

            engage_added = False
            can_engage_added = False
            for enemy_near in self.enemies_near_by:  # type: Unit
                d = enemy_near.distance_to(unit)
                if d < closest_distance:
                    self.closest_units[unit.tag] = enemy_near
                    closest_distance = d

                if not engage_added and d < self.unit_values.real_range(enemy_near, unit, self.knowledge):
                    engage_count += 1
                    engage_added = True

                if not can_engage_added and d < self.unit_values.real_range(unit, enemy_near, self.knowledge):
                    can_engage_count += 1
                    can_engage_added = True

        self.ready_to_attack_ratio = ready_to_attack / len(units)
        self.engage_ratio = engage_count / len(units)
        self.can_engage_ratio = can_engage_count / len(units)

    def ready_to_shoot(self, unit: Unit) -> bool:
        if unit.type_id == UnitTypeId.CYCLONE:
            # if knowledge.cooldown_manager.is_ready(self.unit.tag, AbilityId.LOCKON_LOCKON):
            #     self.ready_to_shoot = True
            #     return
            if self.cd_manager.is_ready(unit.tag, AbilityId.CANCEL_LOCKON):
                return False


        if unit.type_id == UnitTypeId.DISRUPTOR:
            return self.cd_manager.is_ready(unit.tag, AbilityId.EFFECT_PURIFICATIONNOVA)

        if unit.type_id == UnitTypeId.ORACLE:
            tick = self.ai.state.game_loop % 16
            return tick < 8

        if unit.type_id == UnitTypeId.CARRIER:
            tick = self.ai.state.game_loop % 32
            return tick < 8

        return unit.weapon_cooldown <= self.delay_to_shoot

    @abstractmethod
    def group_solve_combat(self, units: Units, current_command: Action) -> Action:
        pass

    @abstractmethod
    def unit_solve_combat(self, unit: Unit, current_command: Action) -> Action:
        pass


    def focus_fire(self, unit: Unit, current_command: Action, prio: Optional[Dict[UnitTypeId, int]]) -> Action:
        shoot_air = self.unit_values.can_shoot_air(unit)
        shoot_ground = self.unit_values.can_shoot_ground(unit, self.knowledge)

        air_range = self.unit_values.air_range(unit)
        ground_range = self.unit_values.ground_range(unit, self.knowledge)
        lookup = min(air_range + 3, ground_range + 3)
        enemies = self.cache.enemy_in_range(unit.position, lookup)

        last_target = self.last_targeted(unit)

        if not enemies:
            # No enemies to shoot at
            return current_command

        value_func: Callable[[Unit],  float] = None
        if prio:
            value_func = lambda u: 1 if u.type_id in changelings else prio.get(u.type_id, -1) \
                  * (1 - u.shield_health_percentage)
        else:
            value_func = lambda u: 1 if u.type_id in changelings else 2 \
                  * self.unit_values.power_by_type(u.type_id, 1 - u.shield_health_percentage)


        best_target: Optional[Unit] = None
        best_score: float = 0
        for enemy in enemies:  # type: Unit
            if not enemy.is_target:
                continue

            if not shoot_air and enemy.is_flying:
                continue

            if not shoot_ground and not enemy.is_flying:
                continue

            pos: Point2 = enemy.position
            score = value_func(enemy) + (1 - pos.distance_to(unit) / lookup)
            if enemy.tag == last_target:
                score += 3

            if score > best_score:
                best_target = enemy
                best_score = score

        if best_target:
            return Action(best_target, True)

        return current_command

    def melee_focus_fire(self, unit: Unit, current_command: Action) -> Action:
        ground_range = self.unit_values.ground_range(unit, self.knowledge)
        lookup = ground_range + 3
        enemies = self.cache.enemy_in_range(unit.position, lookup)

        last_target = self.last_targeted(unit)

        if not enemies:
            # No enemies to shoot at
            return current_command

        def melee_value(u: Unit):
            val = 1 - u.shield_health_percentage
            range = self.unit_values.real_range(unit, u, self.knowledge)
            if unit.distance_to(u) < range:
                val += 1
            return val

        value_func = melee_value
        close_enemies = self.cache.enemy_in_range(unit.position, lookup)

        best_target: Optional[Unit] = None
        best_score: float = 0

        for enemy in close_enemies:  # type: Unit
            if enemy.is_flying:
                continue

            pos: Point2 = enemy.position
            score = value_func(enemy) + (1 - pos.distance_to(unit) / lookup)
            if enemy.tag == last_target:
                score += 3

            if score > best_score:
                best_target = enemy
                best_score = score

        if best_target:
            return Action(best_target, True)

        return current_command

    def last_targeted(self, unit: Unit) -> Optional[int]:
        if unit.orders:
            # action: UnitCommand
            # current_action: UnitOrder
            current_action = unit.orders[0]
            # targeting unit
            if isinstance(current_action.target, int):
                # tag found
                return current_action.target
        return None

    def is_locked_on(self, unit: Unit) -> bool:
        if unit.has_buff(BuffId.LOCKON):
            return True
        return False
class GameAnalyzer(ManagerBase):
    def __init__(self):
        super().__init__()
        self._enemy_air_percentage = 0
        self._our_income_advantage = 0
        self._our_predicted_army_advantage = 0
        self._our_predicted_tech_advantage = 0
        self.enemy_gas_income = 0
        self.enemy_mineral_income = 0
        self.our_zones = 0
        self.enemy_zones = 0
        self.our_power: ExtendedPower = None
        self.enemy_power: ExtendedPower = None
        self.enemy_predict_power: ExtendedPower = None
        self.predicted_defeat_time = 0.0
        self.minerals_left: List[int] = []
        self.vespene_left: List[int] = []
        self.resource_updater: IntervalFunc = None

        self._last_income: Advantage = Advantage.Even
        self._last_army: Advantage = Advantage.Even
        self._last_predict: Advantage = Advantage.Even

    async def start(self, knowledge: 'Knowledge'):
        await super().start(knowledge)
        self.enemy_predicter: EnemyArmyPredicter = knowledge.enemy_army_predicter
        self.our_power = ExtendedPower(self.unit_values)
        self.enemy_power: ExtendedPower = ExtendedPower(self.unit_values)
        self.enemy_predict_power: ExtendedPower = ExtendedPower(
            self.unit_values)
        self.resource_updater = IntervalFunc(self.ai,
                                             self.save_resources_status, 1)
        self.resource_updater.execute()

    def save_resources_status(self):
        self.minerals_left.append(self.ai.minerals)
        self.vespene_left.append(self.ai.vespene)

    async def update(self):
        self.resource_updater.execute()

        self.our_power.clear()
        self.our_zones = 0
        self.enemy_zones = 0
        our_income = self.knowledge.income_calculator.mineral_income + self.knowledge.income_calculator.gas_income

        if self.knowledge.my_worker_type is None:
            # random and we haven't seen enemy race yat
            enemy_workers = 12
        else:
            enemy_workers = self.knowledge.enemy_units_manager.enemy_worker_count
            if not self.knowledge.enemy_main_zone.is_scouted_at_least_once:
                enemy_workers += 12

        mineral_fields = 0
        for zone in self.knowledge.zone_manager.expansion_zones:  # type: Zone
            if zone.is_enemys:
                self.enemy_zones += 1
                mineral_fields += len(zone.mineral_fields)
            if zone.is_ours:
                self.our_zones += 1

        built_vespene = len(self.cache.enemy(self.unit_values.gas_miners))
        self._enemy_gas_income = min(enemy_workers,
                                     built_vespene * 3) * GAS_MINE_RATE
        workers_on_minerals = min(mineral_fields * 2,
                                  enemy_workers - built_vespene * 3)
        workers_on_minerals = max(0, workers_on_minerals)
        self.enemy_mineral_income = workers_on_minerals

        enemy_income = self.enemy_mineral_income + self._enemy_gas_income
        self._our_income_advantage = our_income - enemy_income
        self.our_power.add_units(
            self.ai.units.filter(lambda u: u.is_ready and u.type_id != self.
                                 knowledge.my_worker_type))

        self.enemy_predict_power = self.enemy_predicter.predicted_enemy_power
        self.enemy_power = self.enemy_predicter.enemy_power

        self._enemy_air_percentage = 0
        if self.enemy_predict_power.air_presence > 0:
            self._enemy_air_percentage = self.enemy_predict_power.air_power / self.enemy_predict_power.power

        being_defeated = self.predicting_defeat
        if being_defeated and self.predicted_defeat_time == 0.0:
            self.predicted_defeat_time = self.ai.time
        elif not being_defeated and self.predicted_defeat_time != 0.0:
            self.predicted_defeat_time = 0

        income = self._calc_our_income_advantage()
        army = self._calc_our_army_advantage()
        predict = self._calc_our_army_predict()

        if self._last_income != income:
            self.print(f'Income advantage is now {income.name}')

        if self._last_army != army:
            self.print(f'Known army advantage is now {army.name}')

        if self._last_predict != predict:
            self.print(f'Predicted army advantage is now {predict.name}')

        self._last_income = income
        self._last_army = army
        self._last_predict = predict

    async def post_update(self):
        if self.debug:
            msg = f"Our income: {self.knowledge.income_calculator.mineral_income} / {round(self.knowledge.income_calculator.gas_income)}"
            msg += f"\nEnemy income: {self.enemy_mineral_income} / {round(self.enemy_gas_income)}"
            msg += f"\nResources: {round(self._our_income_advantage)}+{self.our_zones - self.enemy_zones}" \
                f" ({self.our_income_advantage.name})"
            msg += f"\nArmy: {round(self.our_power.power)} vs" \
                f" {round(self.enemy_power.power)} ({self.our_army_advantage.name})"
            msg += f"\nArmy predict: {round(self.our_power.power)} vs" \
                f" {round(self.enemy_predict_power.power)} ({self.our_army_predict.name})"
            msg += f"\nEnemy air: {self.enemy_air.name}"
            self.client.debug_text_2d(msg, Point2((0.4, 0.15)), None, 14)

    @property
    def our_income_advantage(self) -> Advantage:
        return self._last_income

    def _calc_our_income_advantage(self) -> Advantage:
        number = self._our_income_advantage + (self.our_zones -
                                               self.enemy_zones) * 10

        if number > 40:
            return Advantage.OverwhelmingAdvantage
        if number < -40:
            return Advantage.OverwhelmingDisadvantage

        if number > 20:
            return Advantage.ClearAdvantage
        if number < -20:
            return Advantage.ClearDisadvantage

        if number > 10:
            return Advantage.SmallAdvantage
        if number < -10:
            return Advantage.SmallDisadvantage

        if number > 5:
            return Advantage.SlightAdvantage
        if number < -5:
            return Advantage.SlightDisadvantage

        return Advantage.Even

    @property
    def army_at_least_clear_disadvantage(self) -> bool:
        return self.our_army_predict in at_least_clear_disadvantage

    @property
    def army_at_least_small_disadvantage(self) -> bool:
        return self.our_army_predict in at_least_small_disadvantage

    @property
    def army_at_least_clear_advantage(self) -> bool:
        return self.our_army_predict in at_least_clear_advantage

    @property
    def army_at_least_small_advantage(self) -> bool:
        return self.our_army_predict in at_least_small_advantage

    @property
    def army_at_least_advantage(self) -> bool:
        return self.our_army_predict in at_least_advantage

    @property
    def army_can_survive(self) -> bool:
        return self.our_army_predict not in at_least_small_disadvantage

    @property
    def predicting_victory(self) -> bool:
        return (self.our_army_predict == Advantage.OverwhelmingAdvantage and
                self.our_income_advantage == Advantage.OverwhelmingAdvantage)

    @property
    def bean_predicting_defeat_for(self) -> float:
        if self.predicted_defeat_time == 0:
            return 0
        return self.ai.time - self.predicted_defeat_time

    @property
    def predicting_defeat(self) -> bool:
        return (self.our_army_predict == Advantage.OverwhelmingDisadvantage
                and (self.ai.supply_workers < 5 or self.our_income_advantage
                     == Advantage.OverwhelmingDisadvantage))

    @property
    def our_army_predict(self) -> Advantage:
        return self._last_predict

    def _calc_our_army_predict(self) -> Advantage:
        if self.our_power.is_enough_for(self.enemy_predict_power,
                                        our_percentage=1 / 1.1):
            if self.our_power.power > 20 and self.our_power.is_enough_for(
                    self.enemy_predict_power, our_percentage=1 / 3):
                return Advantage.OverwhelmingAdvantage
            if self.our_power.power > 10 and self.our_power.is_enough_for(
                    self.enemy_predict_power, our_percentage=1 / 2):
                return Advantage.ClearAdvantage
            if self.our_power.power > 5 and self.our_power.is_enough_for(
                    self.enemy_predict_power, our_percentage=1 / 1.4):
                return Advantage.SmallAdvantage
            return Advantage.SlightAdvantage

        if self.enemy_predict_power.is_enough_for(self.our_power,
                                                  our_percentage=1 / 1.1):
            if self.enemy_predict_power.power > 20 and self.enemy_predict_power.is_enough_for(
                    self.our_power, our_percentage=1 / 3):
                return Advantage.OverwhelmingDisadvantage
            if self.enemy_predict_power.power > 10 and self.enemy_predict_power.is_enough_for(
                    self.our_power, our_percentage=1 / 2):
                return Advantage.ClearDisadvantage
            if self.enemy_predict_power.power > 5 and self.enemy_predict_power.is_enough_for(
                    self.our_power, our_percentage=1 / 1.4):
                return Advantage.SmallDisadvantage
            return Advantage.SlightDisadvantage
        return Advantage.Even

    @property
    def our_army_advantage(self) -> Advantage:
        return self._last_army

    def _calc_our_army_advantage(self) -> Advantage:
        if self.our_power.is_enough_for(self.enemy_power,
                                        our_percentage=1 / 1.1):
            if self.our_power.power > 20 and self.our_power.is_enough_for(
                    self.enemy_power, our_percentage=1 / 3):
                return Advantage.OverwhelmingAdvantage
            if self.our_power.power > 10 and self.our_power.is_enough_for(
                    self.enemy_power, our_percentage=1 / 2):
                return Advantage.ClearAdvantage
            if self.our_power.power > 5 and self.our_power.is_enough_for(
                    self.enemy_power, our_percentage=1 / 1.4):
                return Advantage.SmallAdvantage
            return Advantage.SlightAdvantage

        if self.enemy_power.is_enough_for(self.our_power,
                                          our_percentage=1 / 1.1):
            if self.enemy_power.power > 20 and self.enemy_power.is_enough_for(
                    self.our_power, our_percentage=1 / 3):
                return Advantage.OverwhelmingDisadvantage
            if self.enemy_power.power > 10 and self.enemy_power.is_enough_for(
                    self.our_power, our_percentage=1 / 2):
                return Advantage.ClearDisadvantage
            if self.enemy_power.power > 5 and self.enemy_power.is_enough_for(
                    self.our_power, our_percentage=1 / 1.4):
                return Advantage.SmallDisadvantage
            return Advantage.SlightDisadvantage
        return Advantage.Even

    @property
    def enemy_air(self) -> AirArmy:
        if self._enemy_air_percentage > 0.90:
            return AirArmy.AllAir
        if self._enemy_air_percentage > 0.65:
            return AirArmy.AlmostAllAir
        if self._enemy_air_percentage > 0.35:
            return AirArmy.Mixed
        if self._enemy_air_percentage > 0:
            return AirArmy.SomeAir
        return AirArmy.NoAir

    async def on_end(self, game_result: Result):
        own_types: List[UnitTypeId] = []
        own_types_left: Dict[UnitTypeId, int] = {}
        enemy_types: List[UnitTypeId] = []
        enemy_types_left: Dict[UnitTypeId, int] = {}

        lost_data = self.knowledge.lost_units_manager.get_own_enemy_lost_units(
        )
        own_lost: Dict[UnitTypeId, List[Unit]] = lost_data[0]
        enemy_lost: Dict[UnitTypeId, List[Unit]] = lost_data[1]

        for unit_type, units in self.cache.own_unit_cache.items(
        ):  # type: (UnitTypeId, Units)
            type_id = self.unit_values.real_type(unit_type)
            if type_id not in own_types:
                own_types.append(type_id)
            val = own_types_left.get(type_id, 0)
            own_types_left[type_id] = val + units.amount

        for unit_count in self.knowledge.enemy_units_manager.enemy_composition:  # type: UnitCount
            unit_type = unit_count.enemy_type
            if unit_type not in enemy_types:
                enemy_types.append(unit_type)
            val = enemy_types_left.get(unit_type, 0)
            enemy_types_left[unit_type] = val + unit_count.count

        for unit_type, units in own_lost.items(
        ):  # type: (UnitTypeId, List[Unit])
            if unit_type not in own_types:
                own_types.append(unit_type)

        for unit_type, units in enemy_lost.items(
        ):  # type: (UnitTypeId, List[Unit])
            if unit_type not in enemy_types:
                enemy_types.append(unit_type)

        self.print_end("Own units:")
        self._print_by_type(own_types, own_lost, own_types_left)
        self.print_end("Enemy units:")
        self._print_by_type(enemy_types, enemy_lost, enemy_types_left)

        maxed_minerals = max(self.minerals_left)
        avg_minerals = sum(self.minerals_left) / len(self.minerals_left)

        maxed_gas = max(self.vespene_left)
        avg_gas = sum(self.vespene_left) / len(self.vespene_left)
        self.print_end(
            f'Minerals max {maxed_minerals} Average {round(avg_minerals)}')
        self.print_end(f'Vespene max {maxed_gas} Average {round(avg_gas)}')

    def _print_by_type(self, types: List[UnitTypeId],
                       lost_units: Dict[UnitTypeId, List[Unit]],
                       left_units: Dict[UnitTypeId, int]):
        def get_counts(unit_type: UnitTypeId) -> tuple:
            dead = len(lost_units.get(unit_type, []))
            alive = left_units.get(unit_type, 0)
            total = dead + alive
            return total, alive, dead

        # Sort types by total count
        types = sorted(types, key=lambda t: get_counts(t)[0], reverse=True)

        for unit_type in types:
            counts = get_counts(unit_type)
            self.print_end(f"{str(unit_type.name).ljust(17)} "
                           f"total: {str(counts[0]).rjust(3)} "
                           f"alive: {str(counts[1]).rjust(3)} "
                           f"dead: {str(counts[2]).rjust(3)} ")

    def print_end(self, msg: str):
        self.knowledge.print(msg, "GameAnalyzerEnd", stats=False)