Creating Custom Behaviors
Recommended reading: How to curate Combat Maneuver's using
existing Behavior
's in ares-sc2
.
Before curating custom behaviors, take a look at some of the
core behaviors in ares-sc2, to get an idea
how one should be structured.
Let's start with a simple example, we have a group of marines and tanks we would like to micro.
Let's use the CombatManeuver
functionality in ares-sc2
to orchestrate a simple a-move across the
map.
from ares import AresBot
from ares.behaviors.combat import CombatManeuver
from ares.behaviors.combat.individual import AMove
from sc2.ids.unit_typeid import UnitTypeId
from sc2.units import Units
from sc2.position import Point2
class MyBot(AresBot):
MARINE_TANK_TYPES: set[UnitTypeId] = {
UnitTypeId.MARINE, UnitTypeId.SIEGETANKSIEGED, UnitTypeId.SIEGETANK
}
def __init__(self, game_step_override=None):
"""Initiate custom bot"""
super().__init__(game_step_override)
async def on_step(self, iteration: int) -> None:
await super(MyBot, self).on_step(iteration)
if marine_tank_force := self.units(self.MARINE_TANK_TYPES):
attack_target = self.enemy_start_locations[0]
self._micro_marine_tank(marine_tank_force, attack_target)
def _micro_marine_tank(self, units: Units, target: Point2) -> None:
for unit in units:
# set up a new CombatManeuver for this unit
offensive_attack: CombatManeuver = CombatManeuver()
# add AMove to this maneuver
offensive_attack.add(AMove(unit, target))
# register the maneuver so it gets executed
self.register_behavior(offensive_attack)
This is all working great, how about now we add decision-making to siege or unsiege our tanks?
The problem: there isn't an existing combat behavior in ares-sc2
to do this. This is partly intentional
since sieging/unsieging tanks is a strategic decision personal to your bot.
We propose a solution: create a reusable custom combat behavior that ares-sc2
understands and can be executed.
As long as our behavior class follows the CombatIndividualBehavior
Protocol, we can add it to
our existing offensive_attack
CombatManeuver
.
- CombatIndividualBehavior
Protocol says we should implement an execute
method with the following signature:
def execute(self, ai: "AresBot", config: dict, mediator: ManagerMediator) -> bool:
Notice this method should return a booleon
, this should return True
if this implemented
CombatIndividualBehavior
carried out an action, and False
otherwise.
With this is mind let's implement a SiegeTankDecision
custom behavior, you could save this in a new
siege_tank_decision.py
file:
# siege_tank_decision.py`
from dataclasses import dataclass
from typing import TYPE_CHECKING
from ares.behaviors.combat.individual import CombatIndividualBehavior
from ares.cython_extensions.geometry import cy_distance_to
from ares.managers.manager_mediator import ManagerMediator
from ares.consts import UnitTreeQueryType
from sc2.ids.ability_id import AbilityId
from sc2.ids.unit_typeid import UnitTypeId as UnitID
from sc2.position import Point2
from sc2.unit import Unit
from sc2.units import Units
if TYPE_CHECKING:
from ares import AresBot
@dataclass
class SiegeTankDecision(CombatIndividualBehavior):
"""Decide if a tank should either siege or unsiege.
Attributes
----------
unit : Unit
The siege tank unit.
"""
unit: Unit
def execute(self, ai: "AresBot", config: dict, mediator: ManagerMediator) -> bool:
unit_pos: Point2 = self.unit.position
type_id: UnitID = self.unit.type_id
# get near enemy ground
# ares uses `KDTree` algorithm for faster distance queries
# let's make use of that
near_enemy_ground: Units = mediator.get_units_in_range(
start_points=[self.unit.position],
distances=14,
query_tree=UnitTreeQueryType.EnemyGround,
)[0]
if type_id == UnitID.SIEGETANK:
# if enemies are not too close, and enough ground enemy around then siege
close_to_tank: list[Unit] = [
e for e in near_enemy_ground if cy_distance_to(e.position, unit_pos) < 6.5
]
if len(close_to_tank) == 0 and (
(ai.get_total_supply(near_enemy_ground) >= 4.0 and len(near_enemy_ground) > 3)
):
self.unit(AbilityId.SIEGEMODE_SIEGEMODE)
return True
elif type_id == UnitID.SIEGETANKSIEGED:
# just a general if nothing around then unsiege
if len(near_enemy_ground) == 0:
self.unit(AbilityId.UNSIEGE_UNSIEGE)
return True
# no action was carried out
return False
This is an example, this could be tweaked and extended as required. Meanwhile, this custom Behavior
can be reused in other scenarios!
Let's update our original code to import this Behavior
and include it in our offensive_attack
maneuver:
from ares import AresBot
from ares.behaviors.combat import CombatManeuver
from ares.behaviors.combat.individual import AMove
from sc2.ids.unit_typeid import UnitTypeId
from sc2.units import Units
from sc2.position import Point2
# IMPORT SiegeTankDecision, modify import based on where you saved it
from bot.siege_tank_decision import SiegeTankDecision
class MyBot(AresBot):
MARINE_TANK_TYPES: set[UnitTypeId] = {
UnitTypeId.MARINE, UnitTypeId.SIEGETANKSIEGED, UnitTypeId.SIEGETANK
}
def __init__(self, game_step_override=None):
"""Initiate custom bot"""
super().__init__(game_step_override)
async def on_step(self, iteration: int) -> None:
await super(MyBot, self).on_step(iteration)
if marine_tank_force := self.units(self.MARINE_TANK_TYPES):
attack_target = self.enemy_start_locations[0]
self._micro_marine_tank(marine_tank_force, attack_target)
def _micro_marine_tank(self, units: Units, target: Point2) -> None:
for unit in units:
# set up a new CombatManeuver for this unit
offensive_attack: CombatManeuver = CombatManeuver()
# ADD OUR CUSTOM SIEGE BEHAVIOR HERE
# Maneuvers should be set up so that higher priority tasks are added first.
# If this returns False for a tank, then the
# AMove behavior will try to execute an action instead
offensive_attack.add(SiegeTankDecision(unit))
# add AMove to this maneuver
# AMove always returns True so should typically be added at the end
offensive_attack.add(AMove(unit, target))
# register the maneuver so it gets executed
self.register_behavior(offensive_attack)