Skip to content

Individual Combat Behaviors

CombatManeuver dataclass

Bases: Behavior

Execute behaviors sequentially.

Add behaviors

Example:

from ares import AresBot
from ares.behaviors.combat import CombatManeuver
from ares.behaviors.combat.individual import (
    DropCargo,
    KeepUnitSafe,
    PathUnitToTarget,
    PickUpCargo,
)

class MyBot(AresBot):
    mine_drop_medivac_tag: int

    async def on_step(self, iteration):
        # Left out here, but `self.mine_drop_medivac_tag`
        # bookkeeping is up to the user
        medivac: Optional[Unit] = self.unit_tag_dict.get(
            self.mine_drop_medivac_tag, None
        )
        if not medivac:
            return

        air_grid: np.ndarray = self.mediator.get_air_grid

        # initiate a new CombatManeuver
        mine_drop: CombatManeuver = CombatManeuver()

        # then add behaviors in the order they should be executed
        # first priority is picking up units
        # (will return False if no cargo and move to next behavior)
        mine_drop.add(
            PickUpCargo(
                unit=medivac,
                grid=air_grid,
                pickup_targets=mines_to_pickup
            )
        )

        # if there is cargo, path to target and drop them off
        if medivac.has_cargo:
            # path
            mine_drop.add(
                PathUnitToTarget(
                    unit=medivac,
                    grid=air_grid,
                    target=self.enemy_start_locations[0],
                )
            )
            # drop off the mines
            mine_drop.add(DropCargo(unit=medivac, target=medivac.position))

        # no cargo and no units to pick up, stay safe
        else:
            mine_drop.add(KeepUnitSafe(unit=medivac, grid=air_grid))

        # register the mine_drop behavior
        self.register_behavior(mine_drop)

Attributes:

Name Type Description
micros list[Behavior]

A list of behaviors that should be executed. Defaults to an empty list.

Source code in src/ares/behaviors/combat/combat_maneuver.py
@dataclass
class CombatManeuver(Behavior):
    """Execute behaviors sequentially.

    Add behaviors

    Example:
    ```py
    from ares import AresBot
    from ares.behaviors.combat import CombatManeuver
    from ares.behaviors.combat.individual import (
        DropCargo,
        KeepUnitSafe,
        PathUnitToTarget,
        PickUpCargo,
    )

    class MyBot(AresBot):
        mine_drop_medivac_tag: int

        async def on_step(self, iteration):
            # Left out here, but `self.mine_drop_medivac_tag`
            # bookkeeping is up to the user
            medivac: Optional[Unit] = self.unit_tag_dict.get(
                self.mine_drop_medivac_tag, None
            )
            if not medivac:
                return

            air_grid: np.ndarray = self.mediator.get_air_grid

            # initiate a new CombatManeuver
            mine_drop: CombatManeuver = CombatManeuver()

            # then add behaviors in the order they should be executed
            # first priority is picking up units
            # (will return False if no cargo and move to next behavior)
            mine_drop.add(
                PickUpCargo(
                    unit=medivac,
                    grid=air_grid,
                    pickup_targets=mines_to_pickup
                )
            )

            # if there is cargo, path to target and drop them off
            if medivac.has_cargo:
                # path
                mine_drop.add(
                    PathUnitToTarget(
                        unit=medivac,
                        grid=air_grid,
                        target=self.enemy_start_locations[0],
                    )
                )
                # drop off the mines
                mine_drop.add(DropCargo(unit=medivac, target=medivac.position))

            # no cargo and no units to pick up, stay safe
            else:
                mine_drop.add(KeepUnitSafe(unit=medivac, grid=air_grid))

            # register the mine_drop behavior
            self.register_behavior(mine_drop)
    ```

    Attributes:
        micros: A list of behaviors that should be executed. Defaults to an empty list.

    """

    micros: list[Behavior] = field(default_factory=list)

    def add(
        self,
        behavior: Union[
            "CombatIndividualBehavior", "CombatGroupBehavior", "CombatManeuver"
        ],
    ) -> None:
        """
        Args:
            behavior: Add a new combat behavior to the current maneuver object.

        """
        self.micros.append(behavior)

    def execute(self, ai: "AresBot", config: dict, mediator: ManagerMediator) -> bool:
        for order in self.micros:
            if order.execute(ai, config, mediator):
                # executed an action
                return True
        # none of the combat micros completed, no actions executed
        return False

add(behavior)

Parameters:

Name Type Description Default
behavior Union[CombatIndividualBehavior, CombatGroupBehavior, CombatManeuver]

Add a new combat behavior to the current maneuver object.

required
Source code in src/ares/behaviors/combat/combat_maneuver.py
def add(
    self,
    behavior: Union[
        "CombatIndividualBehavior", "CombatGroupBehavior", "CombatManeuver"
    ],
) -> None:
    """
    Args:
        behavior: Add a new combat behavior to the current maneuver object.

    """
    self.micros.append(behavior)

AMove dataclass

Bases: CombatIndividualBehavior

A-Move a unit to a target.

Example:

from ares.behaviors.combat.individual import AMove

self.register_behavior(AMove(unit, self.game_info.map_center))

Attributes:

Name Type Description
unit Unit

The unit to stay safe.

target Union[Point2, Unit]

Where the unit is going.

Source code in src/ares/behaviors/combat/individual/a_move.py
@dataclass
class AMove(CombatIndividualBehavior):
    """A-Move a unit to a target.

    Example:
    ```py
    from ares.behaviors.combat.individual import AMove

    self.register_behavior(AMove(unit, self.game_info.map_center))
    ```

    Attributes:
        unit: The unit to stay safe.
        target: Where the unit is going.

    """

    unit: Unit
    target: Union[Point2, Unit]

    def execute(self, ai: "AresBot", config: dict, mediator: ManagerMediator) -> bool:
        self.unit.attack(self.target)
        return True

AttackTarget dataclass

Bases: CombatIndividualBehavior

Shoot a target.

Example:

from ares.behaviors.combat.individual import AttackTarget

unit: Unit
target: Unit
self.register_behavior(AttackTarget(unit, target))

Attributes:

Name Type Description
unit Unit

The unit to shoot.

target Unit

The unit we want to shoot at.

Source code in src/ares/behaviors/combat/individual/attack_target.py
@dataclass
class AttackTarget(CombatIndividualBehavior):
    """Shoot a target.

    Example:
    ```py
    from ares.behaviors.combat.individual import AttackTarget

    unit: Unit
    target: Unit
    self.register_behavior(AttackTarget(unit, target))
    ```

    Attributes:
        unit: The unit to shoot.
        target: The unit we want to shoot at.

    """

    unit: Unit
    target: Unit
    extra_range: float = 0.0

    def execute(
        self, ai: "AresBot", config: dict, mediator: ManagerMediator, **kwargs
    ) -> bool:
        self.unit.attack(self.target)
        return True

DropCargo dataclass

Bases: CombatIndividualBehavior

Handle releasing cargo from a container.

Medivacs, WarpPrism, Overlords, Nydus.

Example:

from ares.behaviors.combat import DropCargo

unit: Unit
target: Unit
self.register_behavior(DropCargo(unit, target))

Attributes:

Name Type Description
unit Unit

The container unit.

target Point2

The target position where to drop the cargo.

Source code in src/ares/behaviors/combat/individual/drop_cargo.py
@dataclass
class DropCargo(CombatIndividualBehavior):
    """Handle releasing cargo from a container.

    Medivacs, WarpPrism, Overlords, Nydus.

    Example:
    ```py
    from ares.behaviors.combat import DropCargo

    unit: Unit
    target: Unit
    self.register_behavior(DropCargo(unit, target))
    ```

    Attributes:
        unit: The container unit.
        target: The target position where to drop the cargo.

    """

    unit: Unit
    target: Point2

    def execute(self, ai: "AresBot", config: dict, mediator: ManagerMediator) -> bool:
        # TODO: Expand logic as needed, initial working version.
        # no action executed
        if self.unit.cargo_used == 0 or not ai.in_pathing_grid(self.unit.position):
            return False

        ai.do_unload_container(self.unit.tag)
        return True

KeepUnitSafe dataclass

Bases: CombatIndividualBehavior

Get a unit to safety based on the influence grid passed in.

Example:

from ares.behaviors.combat import KeepUnitSafe

unit: Unit
grid: np.ndarray = self.mediator.get_ground_grid
self.register_behavior(KeepUnitSafe(unit, grid))

Attributes:

Name Type Description
unit Unit

The unit to stay safe.

grid ndarray

2D grid which usually contains enemy influence.

Source code in src/ares/behaviors/combat/individual/keep_unit_safe.py
@dataclass
class KeepUnitSafe(CombatIndividualBehavior):
    """Get a unit to safety based on the influence grid passed in.

    Example:
    ```py
    from ares.behaviors.combat import KeepUnitSafe

    unit: Unit
    grid: np.ndarray = self.mediator.get_ground_grid
    self.register_behavior(KeepUnitSafe(unit, grid))
    ```

    Attributes:
        unit: The unit to stay safe.
        grid: 2D grid which usually contains enemy influence.

    """

    unit: Unit
    grid: np.ndarray

    def execute(self, ai: "AresBot", config: dict, mediator: ManagerMediator) -> bool:
        # no action executed
        if mediator.is_position_safe(grid=self.grid, position=self.unit.position):
            return False
        else:
            safe_spot: Point2 = mediator.find_closest_safe_spot(
                from_pos=self.unit.position, grid=self.grid
            )
            path: Behavior = PathUnitToTarget(
                unit=self.unit,
                grid=self.grid,
                target=safe_spot,
                success_at_distance=0.0,
            )
            return path.execute(ai, config, mediator)

MedivacHeal dataclass

Bases: CombatIndividualBehavior

Given close allied units, heal things up.

Attributes:

Name Type Description
unit Unit

The siege tank unit.

close_allied list[Unit]

All close by allied units we want to heal.

grid ndarray

The path for medivac to heal on

keep_safe bool

Attempt to stay safe, this may result in not always healing units (Default is True)

Source code in src/ares/behaviors/combat/individual/medivac_heal.py
@dataclass
class MedivacHeal(CombatIndividualBehavior):
    """Given close allied units, heal things up.

    Attributes:
        unit: The siege tank unit.
        close_allied: All close by allied units we want to heal.
        grid: The path for medivac to heal on
        keep_safe: Attempt to stay safe, this may result in
            not always healing units (Default is True)
    """

    unit: Unit
    close_allied: list[Unit]
    grid: np.ndarray
    keep_safe: bool = True

    def execute(self, ai: "AresBot", config: dict, mediator: ManagerMediator) -> bool:
        if self.unit.type_id != UnitID.MEDIVAC:
            return False

        if self.keep_safe and KeepUnitSafe(self.unit, self.grid).execute(
            ai, config, mediator
        ):
            return True

        # don't interfere
        if self.unit.is_using_ability(AbilityId.MEDIVACHEAL_HEAL):
            return True

        bio_need_healing: list[Unit] = [
            u
            for u in self.close_allied
            if u.is_biological and u.health_percentage < 1.0
        ]
        # found something to heal
        if len(bio_need_healing) > 0:
            target_unit: Unit = cy_closest_to(self.unit.position, bio_need_healing)
            self.unit(AbilityId.MEDIVACHEAL_HEAL, target_unit)
            return True

        return False

PathUnitToTarget dataclass

Bases: CombatIndividualBehavior

Path a unit to its target destination.

Add attack enemy in range logic / parameter

Not added yet since that may be it's own Behavior

Example:

from ares.behaviors.combat import PathUnitToTarget

unit: Unit
grid: np.ndarray = self.mediator.get_ground_grid
target: Point2 = self.game_info.map_center
self.register_behavior(PathUnitToTarget(unit, grid, target))

Attributes:

Name Type Description
unit Unit

The unit to path.

grid ndarray

2D grid to path on.

target Point2

Target destination.

success_at_distance float

If the unit has gotten this close, consider path behavior complete. Defaults to 0.0.

sensitivity int

Path precision. Defaults to 5.

smoothing bool

Whether to smooth out the path. Defaults to False.

sense_danger bool

Whether to check for dangers. If none are present, the pathing query is skipped. Defaults to True.

danger_distance float

If sense_danger is True, how far to check for dangers. Defaults to 20.0.

danger_threshold float

Influence at which a danger is respected. Defaults to 5.0.

Source code in src/ares/behaviors/combat/individual/path_unit_to_target.py
@dataclass
class PathUnitToTarget(CombatIndividualBehavior):
    """Path a unit to its target destination.

    TODO: Add attack enemy in range logic / parameter
        Not added yet since that may be it's own Behavior

    Example:
    ```py
    from ares.behaviors.combat import PathUnitToTarget

    unit: Unit
    grid: np.ndarray = self.mediator.get_ground_grid
    target: Point2 = self.game_info.map_center
    self.register_behavior(PathUnitToTarget(unit, grid, target))
    ```

    Attributes:
        unit: The unit to path.
        grid: 2D grid to path on.
        target: Target destination.
        success_at_distance: If the unit has gotten this close, consider path
            behavior complete. Defaults to 0.0.
        sensitivity: Path precision. Defaults to 5.
        smoothing: Whether to smooth out the path. Defaults to False.
        sense_danger: Whether to check for dangers. If none are present,
            the pathing query is skipped. Defaults to True.
        danger_distance: If `sense_danger` is True, how far to check for dangers.
            Defaults to 20.0.
        danger_threshold: Influence at which a danger is respected.
            Defaults to 5.0.

    """

    unit: Unit
    grid: np.ndarray
    target: Point2
    success_at_distance: float = 0.0
    sensitivity: int = 5
    smoothing: bool = False
    sense_danger: bool = True
    danger_distance: float = 20.0
    danger_threshold: float = 5.0

    def execute(self, ai: "AresBot", config: dict, mediator: ManagerMediator) -> bool:
        distance_to_target: float = cy_distance_to(self.unit.position, self.target)
        # no action executed
        if distance_to_target < self.success_at_distance:
            return False

        move_to: Point2 = mediator.find_path_next_point(
            start=self.unit.position,
            target=self.target,
            grid=self.grid,
            sensitivity=self.sensitivity,
            smoothing=self.smoothing,
            sense_danger=self.sense_danger,
            danger_distance=self.danger_distance,
            danger_threshold=self.danger_threshold,
        )
        self.unit.move(move_to)
        return True

PickUpCargo dataclass

Bases: CombatIndividualBehavior

Handle loading cargo into a container.

Medivacs, WarpPrism, Overlords, Nydus.

Example:

from ares.behaviors.combat import PickUpCargo

unit: Unit # medivac for example
grid: np.ndarray = self.mediator.get_ground_grid
pickup_targets: Union[Units, list[Unit]] = self.workers
self.register_behavior(PickUpCargo(unit, grid, pickup_targets))

Attributes:

Name Type Description
unit Unit

The container unit.

grid ndarray

Pathing grid for the container unit.

pickup_targets Union[Units, list[Unit]]

Units we want to load into the container.

cargo_switch_to_role Optional[UnitRole]

Sometimes useful to switch cargo to a new role immediately after loading. Defaults to None.

Source code in src/ares/behaviors/combat/individual/pick_up_cargo.py
@dataclass
class PickUpCargo(CombatIndividualBehavior):
    """Handle loading cargo into a container.

    Medivacs, WarpPrism, Overlords, Nydus.

    Example:
    ```py
    from ares.behaviors.combat import PickUpCargo

    unit: Unit # medivac for example
    grid: np.ndarray = self.mediator.get_ground_grid
    pickup_targets: Union[Units, list[Unit]] = self.workers
    self.register_behavior(PickUpCargo(unit, grid, pickup_targets))
    ```

    Attributes:
        unit: The container unit.
        grid: Pathing grid for the container unit.
        pickup_targets: Units we want to load into the container.
        cargo_switch_to_role: Sometimes useful to switch cargo to
            a new role immediately after loading. Defaults to None.

    """

    unit: Unit
    grid: np.ndarray
    pickup_targets: Union[Units, list[Unit]]
    cargo_switch_to_role: Optional[UnitRole] = None

    def execute(self, ai: "AresBot", config: dict, mediator: ManagerMediator) -> bool:
        # no action executed
        if not self.pickup_targets or self.unit.type_id not in PICKUP_RANGE:
            # just ensure tags inside are assigned correctly
            if len(self.unit.passengers_tags) > 0 and self.cargo_switch_to_role:
                for tag in self.unit.passengers_tags:
                    mediator.assign_role(tag=tag, role=self.cargo_switch_to_role)
            return False

        unit_pos: Point2 = self.unit.position
        target: Unit = cy_closest_to(unit_pos, self.pickup_targets)
        distance: float = cy_distance_to(self.unit.position, target.position)

        if distance <= PICKUP_RANGE[self.unit.type_id]:
            self.unit(AbilityId.SMART, target)
        else:
            move_to: Point2 = mediator.find_path_next_point(
                start=unit_pos, target=target.position, grid=self.grid
            )
            self.unit.move(move_to)

        return True

RavenAutoTurret dataclass

Bases: CombatIndividualBehavior

Drop a turret, opinionated and could be improved Create own behavior based on this if needed.

Attributes:

Name Type Description
unit Unit

Unit

all_close_enemy list[Unit]

All close by allied units we want to heal.

Source code in src/ares/behaviors/combat/individual/raven_auto_turret.py
@dataclass
class RavenAutoTurret(CombatIndividualBehavior):
    """Drop a turret, opinionated and could be improved
    Create own behavior based on this if needed.

    Attributes:
        unit: Unit
        all_close_enemy: All close by allied units we want to heal.
    """

    unit: Unit
    all_close_enemy: list[Unit]

    def execute(self, ai: "AresBot", config: dict, mediator: ManagerMediator) -> bool:
        if AbilityId.BUILDAUTOTURRET_AUTOTURRET not in self.unit.abilities:
            return False

        only_enemy_units: list[Unit] = [
            u for u in self.all_close_enemy if u.type_id not in ALL_STRUCTURES
        ]

        if len(only_enemy_units) <= 1:
            return False

        if (
            ai.get_total_supply(only_enemy_units) > 3
            and AbilityId.BUILDAUTOTURRET_AUTOTURRET in self.unit.abilities
        ):
            target: Point2 = cy_closest_to(
                self.unit.position, only_enemy_units
            ).position.towards(self.unit, 3.0)
            if not ai.in_placement_grid(target):
                return False

            self.unit(AbilityId.BUILDAUTOTURRET_AUTOTURRET, target)
            return True

        if self.all_close_enemy and not only_enemy_units:
            target: Point2 = cy_closest_to(
                self.unit.position, self.all_close_enemy
            ).position.towards(self.unit, 3.0)
            if not ai.in_placement_grid(target):
                return False

            self.unit(AbilityId.BUILDAUTOTURRET_AUTOTURRET, target)
            return True

        if self.unit.is_using_ability(AbilityId.BUILDAUTOTURRET_AUTOTURRET):
            return True

        return False

SiegeTankDecision dataclass

Bases: CombatIndividualBehavior

Decide if a tank should either siege or unsiege.

VERY OPINIONATED, recommend to write own version based on this.

Attributes:

Name Type Description
unit Unit

The siege tank unit.

close_enemy list[Unit]

All close by enemies.

target Point2

Intended destination for this tank.

stay_sieged_near_target bool

This is useful for tanks in defensive position. If on offensive, might not be needed Default is False.

remain_sieged bool

Sometimes we might just want to leave the tank sieged up Default is False.

force_unsiege bool

We might want to not ever siege Default is False.

Source code in src/ares/behaviors/combat/individual/siege_tank_decision.py
@dataclass
class SiegeTankDecision(CombatIndividualBehavior):
    """Decide if a tank should either siege or unsiege.

    VERY OPINIONATED, recommend to write own version based on this.

    Attributes:
        unit: The siege tank unit.
        close_enemy: All close by enemies.
        target: Intended destination for this tank.
        stay_sieged_near_target: This is useful for tanks in defensive position.
            If on offensive, might not be needed
            Default is False.
        remain_sieged: Sometimes we might just want to leave the tank sieged up
            Default is False.
        force_unsiege: We might want to not ever siege
            Default is False.
    """

    unit: Unit
    close_enemy: list[Unit]
    target: Point2
    stay_sieged_near_target: bool = False
    remain_sieged: bool = False
    force_unsiege: bool = False

    def execute(self, ai: "AresBot", config: dict, mediator: ManagerMediator) -> bool:
        type_id: UnitID = self.unit.type_id
        if type_id not in TANK_TYPES:
            return False

        enemy_ground: list[Unit] = [
            e
            for e in self.close_enemy
            if not e.is_burrowed
            and e.type_id not in CHANGELING_TYPES
            and not UNIT_DATA[type_id]["flying"]
        ]
        unit_pos: Point2 = self.unit.position

        if type_id == UnitID.SIEGETANK:
            if self.force_unsiege:
                return False

            # enemy are too close, don't siege no matter what
            if (
                len(
                    [
                        e
                        for e in enemy_ground
                        if cy_distance_to_squared(e.position, unit_pos) < 42.25
                    ]
                )
                > 0
            ):
                return False

            # siege up for any enemy static defence
            if (
                len(
                    [
                        e
                        for e in enemy_ground
                        if e.type_id in STATIC_DEFENCE
                        and cy_distance_to_squared(e.position, self.unit.position)
                        < 169.0
                    ]
                )
                > 0
            ):
                self.unit(AbilityId.SIEGEMODE_SIEGEMODE)
                return True
            elif (
                self.stay_sieged_near_target
                and cy_distance_to_squared(unit_pos, self.target) < 25.0
            ):
                self.unit(AbilityId.SIEGEMODE_SIEGEMODE)
                return True

            elif (
                ai.get_total_supply(enemy_ground) >= 4.0 and len(enemy_ground) > 1
            ) or ([t for t in enemy_ground if t.type_id in TANK_TYPES]):
                self.unit(AbilityId.SIEGEMODE_SIEGEMODE)
                return True

        elif type_id == UnitID.SIEGETANKSIEGED:
            if self.force_unsiege:
                self.unit(AbilityId.UNSIEGE_UNSIEGE)
                return True

            # what if we are sieged and ground enemy have got too close
            if self._enemy_too_close(unit_pos, enemy_ground):
                self.unit(AbilityId.UNSIEGE_UNSIEGE)
                return True

            # didn't actually issue an action, but nothing more needs to be done
            if self.remain_sieged or self.stay_sieged_near_target:
                return False

            # sometimes tanks get isolated a bit, which messes with close enemy calcs
            # but if there are for example marines ahead, we should stay sieged
            if self._own_units_between_tank_and_target(mediator):
                return False

            # just a general if nothing around then unsiege
            if (
                len(enemy_ground) == 0
                and cy_distance_to_squared(unit_pos, self.target) > 200.0
            ):
                self.unit(AbilityId.UNSIEGE_UNSIEGE)
                return True

        # return true for sieged up tanks, as no further action needed
        # return False for non sieged tanks
        return type_id == UnitID.SIEGETANKSIEGED

    @staticmethod
    def _enemy_too_close(unit_pos: Point2, near_enemy_ground) -> bool:
        return (
            len(
                [
                    e
                    for e in near_enemy_ground
                    if cy_distance_to_squared(e.position, unit_pos) < 16.0
                ]
            )
            > 0
            and len(
                [
                    e
                    for e in near_enemy_ground
                    if cy_distance_to_squared(e.position, unit_pos) < 144.0
                ]
            )
            < 2
        )

    def _own_units_between_tank_and_target(self, mediator: ManagerMediator):
        tank_pos: Point2 = self.unit.position
        target: Point2 = self.target

        midway_point: Point2 = Point2(
            ((tank_pos.x + target.x) / 2, (tank_pos.y + target.y) / 2)
        )

        own_units_ahead: Units = mediator.get_units_in_range(
            start_points=[midway_point],
            distances=12,
            query_tree=UnitTreeQueryType.AllOwn,
        )[0].filter(lambda u: not u.is_flying)

        if not own_units_ahead or len(own_units_ahead) < 3:
            return False

        # own units close enough?
        closest_to_tank: Unit = cy_closest_to(tank_pos, own_units_ahead)
        return cy_distance_to_squared(closest_to_tank.position, tank_pos) < 72.25

PlacePredictiveAoE dataclass

Bases: CombatIndividualBehavior

Predict an enemy position and fire AoE accordingly.

Warning: Use this at your own risk. Work in progress.

Guess where the enemy is going based on how it's been moving.

Cythonize this.

Attributes:

Name Type Description
unit Unit

The unit to fire the AoE.

path List[Point2]

How we're getting to the target position (the last point in the list).

enemy_center_unit Unit

Enemy unit to calculate positions based on.

aoe_ability AbilityId

AoE ability to use.

ability_delay int

Amount of frames between using the ability and the ability occurring.

Source code in src/ares/behaviors/combat/individual/place_predictive_aoe.py
@dataclass
class PlacePredictiveAoE(CombatIndividualBehavior):
    """Predict an enemy position and fire AoE accordingly.

    Warning: Use this at your own risk. Work in progress.

    TODO: Guess where the enemy is going based on how it's been moving.
        Cythonize this.

    Attributes:
        unit: The unit to fire the AoE.
        path: How we're getting to the target position (the last point in the list).
        enemy_center_unit: Enemy unit to calculate positions based on.
        aoe_ability: AoE ability to use.
        ability_delay: Amount of frames between using the ability and
            the ability occurring.

    """

    unit: Unit
    path: List[Point2]
    enemy_center_unit: Unit
    aoe_ability: AbilityId
    ability_delay: int

    def execute(
        self, ai: "AresBot", config: dict, mediator: ManagerMediator, **kwargs
    ) -> bool:
        if self.aoe_ability in self.unit.abilities:
            # try to fire the ability if we find a position
            if pos := self._calculate_target_position(ai):
                if ai.is_visible(pos):
                    return self.unit(self.aoe_ability, pos)
        # no position found or the ability isn't ready
        return False

    def _calculate_target_position(self, ai: "AresBot") -> Point2:
        """Calculate where we want to put the AoE.

        Returns
        -------
        Point2 :
            Where we want to place the AoE.

        """
        # figure out where our unit is going to be during the chase
        own_unit_path = self._get_unit_real_path(self.path, self.unit.distance_per_step)

        # enemy path, assuming it moves directly towards our unit at all times
        chasing_path, _reached_target = self._get_chasing_unit_path(
            own_unit_path,
            self.enemy_center_unit.position,
            self.enemy_center_unit.distance_per_step,
        )

        # pick the spot along the predicted path that the enemy will have reached when
        # the ability goes off
        delayed_idx = math.ceil(self.ability_delay / ai.client.game_step)

        return chasing_path[min(delayed_idx, len(chasing_path) - 1)]

    @staticmethod
    def _get_unit_next_position(
        current_position: Point2,
        current_target: Point2,
        distance_per_step: float,
        next_target: Optional[Point2] = None,
    ) -> Tuple[Point2, bool]:
        """Calculate where a unit will be on the next step.

        Assumes knowledge of the unit's current position, target position, and that the
        unit will not change direction.

        TODO: handle the case where the unit is fast enough to travel multiple path
            points per game step

        Parameters
        ----------
        current_position: Point2
            Where the unit currently is.
        current_target: Point2
            Where the unit is going.
        distance_per_step: float
            How far the unit moves per game step.
        next_target: Optional[Point2]
            Where the unit should head if it reaches the target point between steps.

        Returns
        -------
        Tuple[Point2, bool] :
            Point2 is where the unit will be
            bool is whether the unit reached `current_target` in this step

        """
        reached_target: bool = False

        # make sure we won't run past the target point
        distance_to_target: float = current_position.distance_to(current_target)
        if distance_to_target < distance_per_step:
            if next_target:
                """
                Overwrite initial values to reflect moving from the current target
                to the next position.
                """
                distance_per_step = (
                    1 - distance_to_target / distance_per_step
                ) * distance_per_step
                current_position = current_target
                current_target = next_target
                reached_target = True
            else:
                # we don't have a next point to go to, so stop at the current target
                return current_target, True

        # offset the current position towards the target position by the amount of
        # distance covered per game step
        return (
            current_position.towards(current_target, distance_per_step),
            reached_target,
        )

    def _get_unit_real_path(
        self, unit_path: List[Point2], unit_speed: float
    ) -> List[Point2]:
        """Find the location of the unit at each game step given its path.

        Parameters
        ----------
        unit_path: List[Point2]
            Where the unit is being told to move.
        unit_speed: float
            How far the unit moves each game step.

        Returns
        -------
        List[Point2] :
            Where the unit will be at each game iteration.

        """
        real_path: List[Point2] = [unit_path[0]]
        curr_target_idx: int = 1
        # 100 should be overkill, but I'm really just trying to avoid a `while` loop
        for step in range(100):
            if curr_target_idx >= len(unit_path):
                # we've made it to the end of the path
                break
            # travel directly towards the next point on the path, updating the target
            # point when the one before it is reached
            next_position, increase_target_idx = self._get_unit_next_position(
                current_position=real_path[-1],
                current_target=unit_path[curr_target_idx],
                distance_per_step=unit_speed,
                next_target=unit_path[curr_target_idx + 1]
                if curr_target_idx != len(unit_path) - 1
                else None,
            )
            real_path.append(next_position)

            if increase_target_idx:
                # we made it to the current target point, get the next one
                curr_target_idx += 1

        return real_path

    def _get_chasing_unit_path(
        self, target_unit_path: List[Point2], start_position: Point2, unit_speed: float
    ) -> Tuple[List[Point2], bool]:
        """Calculate the path the chasing unit will take to catch the target unit.

        Arguments
        ---------
        target_unit_path: List[Point2]
            Where the target unit is going to be at each game iteration.
        start_position: Point2
            Where the chasing unit is starting from.
        unit_speed: float
            How far the chasing unit moves per game step.

        Returns
        -------
        Tuple[List[Point2], bool] :
            List[Point2] is the chasing unit's path
            bool is whether the chasing unit caught the target unit

        """
        reached_target: bool = False

        unit_path: List[Point2] = [start_position]
        target_idx = 0

        for i in range(100):
            next_position, reached_target = self._get_unit_next_position(
                current_position=unit_path[-1],
                current_target=target_unit_path[target_idx],
                distance_per_step=unit_speed,
            )
            unit_path.append(next_position)
            # keep updating the target index because we're chasing a moving target, but
            # stop if the unit we're chasing isn't moving any further
            target_idx = min(len(target_unit_path) - 1, target_idx + 1)

            if reached_target:
                # we caught the unit
                break

        return unit_path, reached_target

ShootTargetInRange dataclass

Bases: CombatIndividualBehavior

Find something to shoot at.

Currently only picks lowest health.

Might want to pick best one shot KO for example

Example:

from ares.behaviors.combat import ShootTargetInRange

unit: Unit
target: Unit
self.register_behavior(ShootTargetInRange(unit, target))

Attributes:

Name Type Description
unit Unit

The unit to shoot.

targets Union[list[Unit], Units]

Units we want to check.

extra_range float

Look outside the unit's weapon range. This might be useful for hunting down low HP units.

Source code in src/ares/behaviors/combat/individual/shoot_target_in_range.py
@dataclass
class ShootTargetInRange(CombatIndividualBehavior):
    """Find something to shoot at.

    TODO: Currently only picks lowest health.
        Might want to pick best one shot KO for example

    Example:
    ```py
    from ares.behaviors.combat import ShootTargetInRange

    unit: Unit
    target: Unit
    self.register_behavior(ShootTargetInRange(unit, target))
    ```

    Attributes:
        unit: The unit to shoot.
        targets: Units we want to check.
        extra_range: Look outside the unit's weapon range.
            This might be useful for hunting down low HP units.

    """

    unit: Unit
    targets: Union[list[Unit], Units]
    extra_range: float = 0.0

    def execute(
        self, ai: "AresBot", config: dict, mediator: ManagerMediator, **kwargs
    ) -> bool:
        if not self.targets:
            return False

        targets = [
            t
            for t in self.targets
            if not t.is_cloaked or t.is_cloaked and t.is_revealed
        ]
        in_attack_range: list[Unit] = cy_in_attack_range(
            self.unit, targets, self.extra_range
        )

        if len(in_attack_range) == 0:
            return False

        # idea here is if our unit already has an order to shoot one of these
        # in attack range enemies then we return True but don't issue a
        # new action
        if (
            self.unit.orders
            and len([u for u in in_attack_range if u.tag == self.unit.order_target])
            and self.unit.weapon_cooldown == 0.0
        ):
            return True

        enemy_target: Unit = cy_pick_enemy_target(in_attack_range)

        if cy_attack_ready(ai, self.unit, enemy_target):
            self.unit.attack(enemy_target)
            return True

        return False

StutterUnitBack dataclass

Bases: CombatIndividualBehavior

Shoot at the target if possible, else move back.

Example:

from ares.behaviors.combat import StutterUnitBack

unit: Unit
target: Unit
self.register_behavior(StutterUnitBack(unit, target))

Attributes:

Name Type Description
unit Unit

The unit to shoot.

target Unit

The unit we want to shoot at.

kite_via_pathing bool

Kite back using pathing? Value for grid must be present.

grid Optional[ndarray]

Pass in if using kite_via_pathing.

Source code in src/ares/behaviors/combat/individual/stutter_unit_back.py
@dataclass
class StutterUnitBack(CombatIndividualBehavior):
    """Shoot at the target if possible, else move back.

    Example:
    ```py
    from ares.behaviors.combat import StutterUnitBack

    unit: Unit
    target: Unit
    self.register_behavior(StutterUnitBack(unit, target))
    ```

    Attributes:
        unit: The unit to shoot.
        target: The unit we want to shoot at.
        kite_via_pathing: Kite back using pathing? Value for `grid` must be present.
        grid: Pass in if using `kite_via_pathing`.

    """

    unit: Unit
    target: Unit
    kite_via_pathing: bool = True
    grid: Optional[np.ndarray] = None

    def execute(
        self, ai: "AresBot", config: dict, mediator: ManagerMediator, **kwargs
    ) -> bool:
        unit = self.unit
        target = self.target
        if self.kite_via_pathing and self.grid is None:
            self.grid = mediator.get_ground_grid

        if cy_attack_ready(ai, unit, target):
            return AttackTarget(unit=unit, target=target).execute(ai, config, mediator)
        elif self.kite_via_pathing and self.grid is not None:
            return KeepUnitSafe(unit=unit, grid=self.grid).execute(ai, config, mediator)
        # TODO: Implement non pathing kite back
        else:
            logger.warning("Stutter back doesn't work for kite_via_pathing=False yet")
            return False

StutterUnitForward dataclass

Bases: CombatIndividualBehavior

Shoot at the target if possible, else move back.

Example:

from ares.behaviors.combat import StutterUnitForward

unit: Unit
target: Unit
self.register_behavior(StutterUnitForward(unit, target))

Attributes:

Name Type Description
unit Unit

The unit to shoot.

target Unit

The unit we want to shoot at.

Source code in src/ares/behaviors/combat/individual/stutter_unit_forward.py
@dataclass
class StutterUnitForward(CombatIndividualBehavior):
    """Shoot at the target if possible, else move back.

    Example:
    ```py
    from ares.behaviors.combat import StutterUnitForward

    unit: Unit
    target: Unit
    self.register_behavior(StutterUnitForward(unit, target))
    ```

    Attributes:
        unit: The unit to shoot.
        target: The unit we want to shoot at.
    """

    unit: Unit
    target: Unit

    def execute(
        self, ai: "AresBot", config: dict, mediator: ManagerMediator, **kwargs
    ) -> bool:
        unit = self.unit
        target = self.target
        if cy_attack_ready(ai, unit, target):
            return AttackTarget(unit=unit, target=target).execute(ai, config, mediator)
        else:
            unit.move(target.position)
            return True

UseAbility dataclass

Bases: CombatIndividualBehavior

A-Move a unit to a target.

Example:

from ares.behaviors.combat import UseAbility
from sc2.ids.ability_id import AbilityId

unit: Unit
target: Union[Unit, Point2]
self.register_behavior(
    UseAbility(
        AbilityId.FUNGALGROWTH_FUNGALGROWTH, unit, target
    )
)

Attributes:

Name Type Description
ability AbilityId

The ability we want to use.

unit Unit

The unit to use the ability.

target Optional[Union[Point2, Unit]]

Target for this ability.

Source code in src/ares/behaviors/combat/individual/use_ability.py
@dataclass
class UseAbility(CombatIndividualBehavior):
    """A-Move a unit to a target.

    Example:
    ```py
    from ares.behaviors.combat import UseAbility
    from sc2.ids.ability_id import AbilityId

    unit: Unit
    target: Union[Unit, Point2]
    self.register_behavior(
        UseAbility(
            AbilityId.FUNGALGROWTH_FUNGALGROWTH, unit, target
        )
    )
    ```

    Attributes:
        ability: The ability we want to use.
        unit: The unit to use the ability.
        target: Target for this ability.

    """

    ability: AbilityId
    unit: Unit
    target: Optional[Union[Point2, Unit]] = None

    def execute(
        self, ai: "AresBot", config: dict, mediator: ManagerMediator, **kwargs
    ) -> bool:
        if self.ability not in self.unit.abilities:
            return False

        if self.target:
            self.unit(self.ability, self.target)
        else:
            self.unit(self.ability)

        return True

UseAOEAbility dataclass

Bases: CombatIndividualBehavior

Attempt to use AOE ability for a unit.

Attributes:

Name Type Description
unit Unit

The unit that potentially has an AOE ability.

ability_id AbilityId

Ability we want to use.

targets list[Unit]

The targets we want to hit.

min_targets int

Minimum targets to hit with spell.

avoid_own_flying bool

Avoid own flying with this spell? Default is False.

avoid_own_ground bool

Avoid own ground with this spell? Default is False.

bonus_tags Optional[set]

Give more emphasize on this unit tags. For example, perhaps a ravager can do corrosive bile Provide enemy tags that are currently fungaled? Default is empty Set

recalculate bool

If unit is already using ability, should we recalculate this behavior? WARNING: could have performance impact Default is False.

stack_same_spell bool

Stack spell in same position? Default is False.

Source code in src/ares/behaviors/combat/individual/use_aoe_ability.py
@dataclass
class UseAOEAbility(CombatIndividualBehavior):
    """Attempt to use AOE ability for a unit.

    Attributes:
        unit: The unit that potentially has an AOE ability.
        ability_id: Ability we want to use.
        targets: The targets we want to hit.
        min_targets: Minimum targets to hit with spell.
        avoid_own_flying: Avoid own flying with this spell?
            Default is False.
        avoid_own_ground: Avoid own ground with this spell?
            Default is False.
        bonus_tags: Give more emphasize on this unit tags.
            For example, perhaps a ravager can do corrosive bile
            Provide enemy tags that are currently fungaled?
            Default is empty `Set`
        recalculate: If unit is already using ability, should
            we recalculate this behavior?
            WARNING: could have performance impact
            Default is False.
        stack_same_spell: Stack spell in same position?
            Default is False.

    """

    unit: Unit
    ability_id: AbilityId
    targets: list[Unit]
    min_targets: int
    avoid_own_flying: bool = False
    avoid_own_ground: bool = False
    bonus_tags: Optional[set] = field(default_factory=set)
    recalculate: bool = False
    stack_same_spell: bool = False

    def execute(self, ai: "AresBot", config: dict, mediator: ManagerMediator) -> bool:
        if self.ability_id not in AOE_ABILITY_SPELLS_INFO:
            logger.warning(
                f"You're trying to use {self.ability_id} with `UseAOEAbility`, "
                f"but this behavior doesn't support it"
            )
            return False

        radius: float = AOE_ABILITY_SPELLS_INFO[self.ability_id]["radius"]
        # prevent computation if unit is going to use ability
        if (
            not self.recalculate
            and self.unit.is_using_ability(self.ability_id)
            and self._can_cast(ai, mediator, self.unit.order_target, radius)
        ):
            return True

        # no targets / ability not ready / or
        # total targets not enough / or not valid ability id
        if (
            not self.targets
            or self.ability_id not in self.unit.abilities
            or len(self.targets) < self.min_targets
        ):
            return False

        position = Point2(
            cy_find_aoe_position(
                radius, self.targets, self.min_targets, self.bonus_tags
            )
        )

        if self._can_cast(ai, mediator, position, radius):
            # need to cast on the actual unit
            if self.ability_id in {
                AbilityId.PARASITICBOMB_PARASITICBOMB,
                AbilityId.EFFECT_ANTIARMORMISSILE,
            }:
                self.unit(self.ability_id, cy_closest_to(position, self.targets))
            else:
                self.unit(self.ability_id, Point2(position))

            return True
        return False

    def _can_cast(
        self, ai: "AresBot", mediator: ManagerMediator, position: Point2, radius: float
    ) -> bool:
        can_cast: bool = (
            self.min_targets >= 1
            or len(
                [
                    u
                    for u in self.targets
                    if cy_distance_to(u.position, position) < radius
                ]
            )
            >= self.min_targets
        )
        # check for friendly splash damage
        if can_cast and self.avoid_own_ground or self.avoid_own_flying:
            own_in_range = mediator.get_units_in_range(
                start_points=[position],
                distances=radius,
                query_tree=UnitTreeQueryType.AllOwn,
            )[0]
            if self.avoid_own_flying and [
                u for u in own_in_range if UNIT_DATA[u.type_id]["flying"]
            ]:
                can_cast = False
            elif self.avoid_own_ground and [
                u for u in own_in_range if not UNIT_DATA[u.type_id]["flying"]
            ]:
                can_cast = False

        # check if spell already active in this area
        if (
            not self.stack_same_spell
            and can_cast
            and (effect_or_buff := AOE_ABILITY_SPELLS_INFO[self.ability_id]["effect"])
        ):
            if isinstance(effect_or_buff, EffectId):
                radius: float = AOE_ABILITY_SPELLS_INFO[self.ability_id]["radius"]
                for eff in ai.state.effects:
                    if eff == effect_or_buff and any(
                        [
                            p
                            for p in eff.positions
                            if cy_distance_to(position, p) < radius
                        ]
                    ):
                        can_cast = False
            elif isinstance(effect_or_buff, BuffId):
                if [u for u in self.targets if u.has_buff(effect_or_buff)]:
                    can_cast = False

        return can_cast

UseTransfuse dataclass

Bases: CombatIndividualBehavior

Queen tries to transfuse something

Attributes:

Name Type Description
unit Unit

The queen that should transfuse.

targets Union[list[Unit], Units]

Our own units to transfuse.

extra_range float

Look a bit further out of transfuse range? Default is 0.0

Source code in src/ares/behaviors/combat/individual/use_transfuse.py
@dataclass
class UseTransfuse(CombatIndividualBehavior):
    """Queen tries to transfuse something

    Attributes:
        unit: The queen that should transfuse.
        targets: Our own units to transfuse.
        extra_range: Look a bit further out of transfuse range?
            Default is 0.0

    """

    unit: Unit
    targets: Union[list[Unit], Units]
    extra_range: float = 0.0

    def execute(
        self, ai: "AresBot", config: dict, mediator: ManagerMediator, **kwargs
    ) -> bool:
        if self.unit.is_using_ability(AbilityId.TRANSFUSION_TRANSFUSION):
            return True

        if AbilityId.TRANSFUSION_TRANSFUSION in self.unit.abilities:
            transfuse_targets: list[Unit] = [
                u
                for u in self.targets
                if u.health_percentage < 0.4
                and cy_distance_to(self.unit.position, u.position)
                < 7.0 + self.unit.radius + u.radius + self.extra_range
                and u.health_max >= 50.0
                and u.tag != self.unit.tag
                and u.tag not in ai.transfused_tags
            ]
            if transfuse_targets:
                ai.transfused_tags.add(transfuse_targets[0].tag)
                return UseAbility(
                    AbilityId.TRANSFUSION_TRANSFUSION,
                    self.unit,
                    transfuse_targets[0],
                ).execute(ai, config, mediator)

        return False

WorkerKiteBack dataclass

Bases: CombatIndividualBehavior

Shoot at the target if possible, else move back.

This is similar to stutter unit back, but takes advantage of mineral walking.

Example:

from ares.behaviors.combat import WorkerKiteBack

unit: Unit
target: Unit
self.register_behavior(
    WorkerKiteBack(
        unit, target
    )
)

Attributes:

Name Type Description
unit Unit

The unit to shoot.

target Unit

The unit we want to shoot at.

Source code in src/ares/behaviors/combat/individual/worker_kite_back.py
@dataclass
class WorkerKiteBack(CombatIndividualBehavior):
    """Shoot at the target if possible, else move back.

    This is similar to stutter unit back, but takes advantage of
    mineral walking.

    Example:
    ```py
    from ares.behaviors.combat import WorkerKiteBack

    unit: Unit
    target: Unit
    self.register_behavior(
        WorkerKiteBack(
            unit, target
        )
    )
    ```

    Attributes:
        unit: The unit to shoot.
        target: The unit we want to shoot at.
    """

    unit: Unit
    target: Unit

    def execute(
        self, ai: "AresBot", config: dict, mediator: ManagerMediator, **kwargs
    ) -> bool:
        unit = self.unit
        target = self.target
        if not target.is_memory and cy_attack_ready(ai, unit, target):
            return AttackTarget(unit=unit, target=target).execute(ai, config, mediator)
        elif mfs := ai.mineral_field:
            unit.gather(cy_closest_to(position=ai.start_location, units=mfs))