bot_ai.py

This basic bot class contains a few helper functions, basic properties and variables to get a simple bot started.

How to use this information is shown in the bot examples (with comments).

class sc2.bot_ai.BotAI

Base class for bots.

alert(alert_code)

Check if alert is triggered in the current step. Possible alerts are listed here https://github.com/Blizzard/s2client-proto/blob/e38efed74c03bec90f74b330ea1adda9215e655f/s2clientprotocol/sc2api.proto#L679-L702

Example use:

from sc2.data import Alert
if self.alert(Alert.AddOnComplete):
    print("Addon Complete")

Alert codes:

AlertError
AddOnComplete
BuildingComplete
BuildingUnderAttack
LarvaHatched
MergeComplete
MineralsExhausted
MorphComplete
MothershipComplete
MULEExpired
NuclearLaunchDetected
NukeComplete
NydusWormDetected
ResearchComplete
TrainError
TrainUnitComplete
TrainWorkerComplete
TransformationComplete
UnitUnderAttack
UpgradeComplete
VespeneExhausted
WarpInComplete
Parameters:

alert_code (Alert)

Return type:

bool

already_pending(unit_type)

Returns a number of buildings or units already in progress, or if a worker is en route to build it. This also includes queued orders for workers and build queues of buildings.

Example:

amount_of_scv_in_production: int = self.already_pending(UnitTypeId.SCV)
amount_of_CCs_in_queue_and_production: int = self.already_pending(UnitTypeId.COMMANDCENTER)
amount_of_lairs_morphing: int = self.already_pending(UnitTypeId.LAIR)
Parameters:

unit_type (UpgradeId | UnitTypeId)

Return type:

float

already_pending_upgrade(upgrade_type)

Check if an upgrade is being researched

Returns values are:

0 # not started
0 < x < 1 # researching
1 # completed

Example:

stim_completion_percentage = self.already_pending_upgrade(UpgradeId.STIMPACK)
Parameters:

upgrade_type (UpgradeId)

Return type:

float

async build(building, near, max_distance=20, build_worker=None, random_alternative=True, placement_step=2)

Not recommended as this function checks many positions if it “can place” on them until it found a valid position. Also if the given position is not placeable, this function tries to find a nearby position to place the structure. Then orders the worker to start the construction.

Parameters:
  • building (UnitTypeId)

  • near (Unit | Point2)

  • max_distance (int)

  • build_worker (Unit | None)

  • random_alternative (bool)

  • placement_step (int)

Return type:

bool

calculate_cost(item_id)

Calculate the required build, train or morph cost of a unit. It is recommended to use the UnitTypeId instead of the ability to create the unit. The total cost to create a ravager is 100/100, but the actual morph cost from roach to ravager is only 25/75, so this function returns 25/75.

It is adviced to use the UnitTypeId instead of the AbilityId. Instead of:

self.calculate_cost(AbilityId.UPGRADETOORBITAL_ORBITALCOMMAND)

use:

self.calculate_cost(UnitTypeId.ORBITALCOMMAND)

More examples:

from sc2.game_data import Cost

self.calculate_cost(UnitTypeId.BROODLORD) == Cost(150, 150)
self.calculate_cost(UnitTypeId.RAVAGER) == Cost(25, 75)
self.calculate_cost(UnitTypeId.BANELING) == Cost(25, 25)
self.calculate_cost(UnitTypeId.ORBITALCOMMAND) == Cost(150, 0)
self.calculate_cost(UnitTypeId.REACTOR) == Cost(50, 50)
self.calculate_cost(UnitTypeId.TECHLAB) == Cost(50, 25)
self.calculate_cost(UnitTypeId.QUEEN) == Cost(150, 0)
self.calculate_cost(UnitTypeId.HATCHERY) == Cost(300, 0)
self.calculate_cost(UnitTypeId.LAIR) == Cost(150, 100)
self.calculate_cost(UnitTypeId.HIVE) == Cost(200, 150)
Parameters:

item_id (UnitTypeId | UpgradeId | AbilityId)

Return type:

Cost

calculate_supply_cost(unit_type)

This function calculates the required supply to train or morph a unit. The total supply of a baneling is 0.5, but a zergling already uses up 0.5 supply, so the morph supply cost is 0. The total supply of a ravager is 3, but a roach already uses up 2 supply, so the morph supply cost is 1. The required supply to build zerglings is 1 because they pop in pairs, so this function returns 1 because the larva morph command requires 1 free supply.

Example:

roach_supply_cost = self.calculate_supply_cost(UnitTypeId.ROACH) # Is 2
ravager_supply_cost = self.calculate_supply_cost(UnitTypeId.RAVAGER) # Is 1
baneling_supply_cost = self.calculate_supply_cost(UnitTypeId.BANELING) # Is 0
Parameters:

unit_type (UnitTypeId)

Return type:

float

calculate_unit_value(unit_type)

Unlike the function below, this function returns the value of a unit given by the API (e.g. the resources lost value on kill).

Examples:

self.calculate_value(UnitTypeId.ORBITALCOMMAND) == Cost(550, 0)
self.calculate_value(UnitTypeId.RAVAGER) == Cost(100, 100)
self.calculate_value(UnitTypeId.ARCHON) == Cost(175, 275)
Parameters:

unit_type (UnitTypeId)

Return type:

Cost

can_afford(item_id, check_supply_cost=True)

Tests if the player has enough resources to build a unit or structure.

Example:

cc = self.townhalls.idle.random_or(None)
# self.townhalls can be empty or there are no idle townhalls
if cc and self.can_afford(UnitTypeId.SCV):
    cc.train(UnitTypeId.SCV)

Example:

# Current state: we have 150 minerals and one command center and a barracks
can_afford_morph = self.can_afford(UnitTypeId.ORBITALCOMMAND, check_supply_cost=False)
# Will be 'True' although the API reports that an orbital is worth 550 minerals, but the morph cost is only 150 minerals
Parameters:
Return type:

bool

async can_cast(unit, ability_id, target=None, only_check_energy_and_cooldown=False, cached_abilities_of_unit=None)

Tests if a unit has an ability available and enough energy to cast it.

Example:

stalkers = self.units(UnitTypeId.STALKER)
stalkers_that_can_blink = stalkers.filter(lambda unit: unit.type_id == UnitTypeId.STALKER and (await self.can_cast(unit, AbilityId.EFFECT_BLINK_STALKER, only_check_energy_and_cooldown=True)))

See data_pb2.py (line 161) for the numbers 1-5 to make sense

Parameters:
Return type:

bool

can_feed(unit_type)

Checks if you have enough free supply to build the unit

Example:

cc = self.townhalls.idle.random_or(None)
# self.townhalls can be empty or there are no idle townhalls
if cc and self.can_feed(UnitTypeId.SCV):
    cc.train(UnitTypeId.SCV)
Parameters:

unit_type (UnitTypeId)

Return type:

bool

async can_place(building, positions)

Tests if a building can be placed in the given locations.

Example:

barracks_placement_position = self.main_base_ramp.barracks_correct_placement
worker = self.select_build_worker(barracks_placement_position)
# Can return None
if worker and (await self.can_place(UnitTypeId.BARRACKS, [barracks_placement_position])[0]:
    worker.build(UnitTypeId.BARRACKS, barracks_placement_position)
Parameters:
Return type:

list[bool]

async can_place_single(building, position)

Checks the placement for only one position.

Return type:

bool

async chat_send(message, team_only=False)

Send a chat message to the SC2 Client.

Example:

await self.chat_send("Hello, this is a message from my bot!")
Parameters:
  • message (str)

  • team_only (bool)

Return type:

None

async distribute_workers(resource_ratio=2)

Distributes workers across all the bases taken. Keyword resource_ratio takes a float. If the current minerals to gas ratio is bigger than resource_ratio, this function prefer filling gas_buildings first, if it is lower, it will prefer sending workers to minerals first.

NOTE: This function is far from optimal, if you really want to have refined worker control, you should write your own distribution function. For example long distance mining control and moving workers if a base was killed are not being handled.

WARNING: This is quite slow when there are lots of workers or multiple bases.

Parameters:

resource_ratio (float)

Return type:

None

property enemy_start_locations: list[Point2]

Possible start locations for enemies.

async expand_now(building=None, max_distance=10, location=None)

Finds the next possible expansion via ‘self.get_next_expansion()’. If the target expansion is blocked (e.g. an enemy unit), it will misplace the expansion.

Parameters:
Return type:

None

async find_placement(building, near, max_distance=20, random_alternative=True, placement_step=2, addon_place=False)

Finds a placement location for building.

Example:

if self.townhalls:
    cc = self.townhalls[0]
    depot_position = await self.find_placement(UnitTypeId.SUPPLYDEPOT, near=cc)
Parameters:
  • building (UnitTypeId | AbilityId)

  • near (Point2)

  • max_distance (int)

  • random_alternative (bool)

  • placement_step (int)

  • addon_place (bool)

Return type:

Point2 | None

async get_available_abilities(units, ignore_resource_requirements=False)

Returns available abilities of one or more units. Right now only checks cooldown, energy cost, and whether the ability has been researched.

Examples:

units_abilities = await self.get_available_abilities(self.units)

or:

units_abilities = await self.get_available_abilities([self.units.random])
Parameters:
  • units (list[Unit] | Units)

  • ignore_resource_requirements (bool)

Return type:

list[list[AbilityId]]

async get_next_expansion()

Find next expansion location.

Return type:

Point2 | None

get_terrain_height(pos)

Returns terrain height at a position. Caution: terrain height is different from a unit’s z-coordinate.

Parameters:

pos (Point2 | Unit)

Return type:

int

get_terrain_z_height(pos)

Returns terrain z-height at a position.

Parameters:

pos (Point2 | Unit)

Return type:

float

has_creep(pos)

Returns True if there is creep on the grid point.

Parameters:

pos (Point2 | Unit)

Return type:

bool

in_map_bounds(pos)

Tests if a 2 dimensional point is within the map boundaries of the pixelmaps.

Parameters:

pos (Point2 | tuple | list)

Return type:

bool

in_pathing_grid(pos)

Returns True if a ground unit can pass through a grid point.

Parameters:

pos (Point2 | Unit)

Return type:

bool

in_placement_grid(pos)

Returns True if you can place something at a position. Remember, buildings usually use 2x2, 3x3 or 5x5 of these grid points. Caution: some x and y offset might be required, see ramp code in game_info.py

Parameters:

pos (Point2 | Unit)

Return type:

bool

is_visible(pos)

Returns True if you have vision on a grid point.

Parameters:

pos (Point2 | Unit)

Return type:

bool

property main_base_ramp: Ramp

Returns the Ramp instance of the closest main-ramp to start location. Look in game_info.py for more information about the Ramp class

Example: See terran ramp wall bot

async on_before_start()

Override this in your bot class. This function is called before “on_start” and before “prepare_first_step” that calculates expansion locations. Not all data is available yet. This function is useful in realtime=True mode to split your workers or start producing the first worker.

Return type:

None

async on_building_construction_complete(unit)

Override this in your bot class. This function is called when a building construction is completed.

Parameters:

unit (Unit)

Return type:

None

async on_building_construction_started(unit)

Override this in your bot class. This function is called when a building construction has started.

Parameters:

unit (Unit)

Return type:

None

async on_end(game_result)

Override this in your bot class. This function is called at the end of a game. Unsure if this function will be called on the laddermanager client as the bot process may forcefully be terminated.

Parameters:

game_result (Result)

Return type:

None

async on_enemy_unit_entered_vision(unit)

Override this in your bot class. This function is called when an enemy unit (unit or structure) entered vision (which was not visible last frame).

Parameters:

unit (Unit)

Return type:

None

async on_enemy_unit_left_vision(unit_tag)

Override this in your bot class. This function is called when an enemy unit (unit or structure) left vision (which was visible last frame). Same as the self.on_unit_destroyed event, this function is called with the unit’s tag because the unit is no longer visible anymore. If you want to store a snapshot of the unit, use self._enemy_units_previous_map[unit_tag] for units or self._enemy_structures_previous_map[unit_tag] for structures.

Examples:

last_known_unit = self._enemy_units_previous_map.get(unit_tag, None) or self._enemy_structures_previous_map[unit_tag]
print(f"Enemy unit left vision, last known location: {last_known_unit.position}")
Parameters:

unit_tag (int)

Return type:

None

async on_start()

Override this in your bot class. At this point, game_data, game_info and the first iteration of game_state (self.state) are available.

Return type:

None

async on_step(iteration)

You need to implement this function! Override this in your bot class. This function is called on every game step (looped in realtime mode).

Parameters:

iteration (int)

async on_unit_created(unit)

Override this in your bot class. This function is called when a unit is created.

Parameters:

unit (Unit)

Return type:

None

async on_unit_destroyed(unit_tag)

Override this in your bot class. Note that this function uses unit tags and not the unit objects because the unit does not exist any more. This will event will be called when a unit (or structure, friendly or enemy) dies. For enemy units, this only works if the enemy unit was in vision on death.

Parameters:

unit_tag (int)

Return type:

None

async on_unit_took_damage(unit, amount_damage_taken)

Override this in your bot class. This function is called when your own unit (unit or structure) took damage. It will not be called if the unit died this frame.

This may be called frequently for terran structures that are burning down, or zerg buildings that are off creep, or terran bio units that just used stimpack ability. TODO: If there is a demand for it, then I can add a similar event for when enemy units took damage

Examples:

print(f"My unit took damage: {unit} took {amount_damage_taken} damage")
Parameters:
  • unit (Unit)

  • amount_damage_taken (float)

Return type:

None

async on_unit_type_changed(unit, previous_type)

Override this in your bot class. This function is called when a unit type has changed. To get the current UnitTypeId of the unit, use ‘unit.type_id’

This may happen when a larva morphed to an egg, siege tank sieged, a zerg unit burrowed, a hatchery morphed to lair, a corruptor morphed to broodlordcocoon, etc..

Examples:

print(f"My unit changed type: {unit} from {previous_type} to {unit.type_id}")
Parameters:
Return type:

None

async on_upgrade_complete(upgrade)

Override this in your bot class. This function is called with the upgrade id of an upgrade that was not finished last step and is now.

Parameters:

upgrade (UpgradeId)

Return type:

None

research(upgrade_type)

Researches an upgrade from a structure that can research it, if it is idle and powered (protoss). Returns True if the research was started. Return False if the requirement was not met, or the bot did not have enough resources to start the upgrade, or the building to research the upgrade was missing or not idle.

New function. Please report any bugs!

Example:

# Try to research zergling movement speed if we can afford it
# and if at least one pool is at build_progress == 1
# and we are not researching it yet
if self.already_pending_upgrade(UpgradeId.ZERGLINGMOVEMENTSPEED) == 0 and self.can_afford(UpgradeId.ZERGLINGMOVEMENTSPEED):
    spawning_pools_ready = self.structures(UnitTypeId.SPAWNINGPOOL).ready
    if spawning_pools_ready:
        self.research(UpgradeId.ZERGLINGMOVEMENTSPEED)
Parameters:

upgrade_type (UpgradeId)

Return type:

bool

select_build_worker(pos, force=False)

Select a worker to build a building with.

Example:

barracks_placement_position = self.main_base_ramp.barracks_correct_placement
worker = self.select_build_worker(barracks_placement_position)
# Can return None
if worker:
    worker.build(UnitTypeId.BARRACKS, barracks_placement_position)
Parameters:
Return type:

Unit | None

property start_location: Point2

Returns the spawn location of the bot, using the position of the first created townhall. This will be None if the bot is run on an arcade or custom map that does not feature townhalls at game start.

property step_time: tuple[float, float, float, float]

Returns a tuple of step duration in milliseconds. First value is the minimum step duration - the shortest the bot ever took Second value is the average step duration Third value is the maximum step duration - the longest the bot ever took (including on_start()) Fourth value is the step duration the bot took last iteration If called in the first iteration, it returns (inf, 0, 0, 0)

structure_type_build_progress(structure_type)

Returns the build progress of a structure type.

Return range: 0 <= x <= 1 where

0: no such structure exists 0 < x < 1: at least one structure is under construction, returns the progress of the one with the highest progress 1: we have at least one such structure complete

Example:

# Assuming you have one barracks building at 0.5 build progress:
progress = self.structure_type_build_progress(UnitTypeId.BARRACKS)
print(progress)
# This prints out 0.5

# If you want to save up money for mutalisks, you can now save up once the spire is nearly completed:
spire_almost_completed: bool = self.structure_type_build_progress(UnitTypeId.SPIRE) > 0.75

# If you have a Hive completed but no lair, this function returns 1.0 for the following:
self.structure_type_build_progress(UnitTypeId.LAIR)

# Assume you have 2 command centers in production, one has 0.5 build_progress and the other 0.2, the following returns 0.5
highest_progress_of_command_center: float = self.structure_type_build_progress(UnitTypeId.COMMANDCENTER)
Parameters:

structure_type (UnitTypeId | int)

Return type:

float

tech_requirement_progress(structure_type)

Returns the tech requirement progress for a specific building

Example:

# Current state: supply depot is at 50% completion
tech_requirement = self.tech_requirement_progress(UnitTypeId.BARRACKS)
print(tech_requirement) # Prints 0.5 because supply depot is half way done

Example:

# Current state: your bot has one hive, no lair
tech_requirement = self.tech_requirement_progress(UnitTypeId.HYDRALISKDEN)
print(tech_requirement) # Prints 1 because a hive exists even though only a lair is required

Example:

# Current state: One factory is flying and one is half way done
tech_requirement = self.tech_requirement_progress(UnitTypeId.STARPORT)
print(tech_requirement) # Prints 1 because even though the type id of the flying factory is different, it still has build progress of 1 and thus tech requirement is completed
Parameters:

structure_type (UnitTypeId)

Return type:

float

property time: float

Returns time in seconds, assumes the game is played on ‘faster’

property time_formatted: str

Returns time as string in min:sec format

train(unit_type, amount=1, closest_to=None, train_only_idle_buildings=True)

Trains a specified number of units. Trains only one if amount is not specified. Warning: currently has issues with warp gate warp ins

Very generic function. Please use with caution and report any bugs!

Example Zerg:

self.train(UnitTypeId.QUEEN, 5)
# This should queue 5 queens in 5 different townhalls if you have enough townhalls, enough minerals and enough free supply left

Example Terran:

# Assuming you have 2 idle barracks with reactors, one barracks without addon and one with techlab
# It should only queue 4 marines in the 2 idle barracks with reactors
self.train(UnitTypeId.MARINE, 4)

Example distance to:

# If you want to train based on distance to a certain point, you can use "closest_to"
self.train(UnitTypeId.MARINE, 4, closest_to = self.game_info.map_center)
Parameters:
  • unit_type (UnitTypeId)

  • amount (int)

  • closest_to (Point2 | None)

  • train_only_idle_buildings (bool)

Return type:

int

property units_created: Counter[UnitTypeId]

Returns a Counter for all your units and buildings you have created so far.

This may be used for statistics (at the end of the game) or for strategic decision making.

CAUTION: This does not properly work at the moment for morphing units and structures. Please use the ‘on_unit_type_changed’ event to add these morphing unit types manually to ‘self._units_created’. Issues would arrise in e.g. siege tank morphing to sieged tank, and then morphing back (suddenly the counter counts 2 tanks have been created).

Examples:

# Give attack command to enemy base every time 10 marines have been trained
async def on_unit_created(self, unit: Unit):
    if unit.type_id == UnitTypeId.MARINE:
        if self.units_created[MARINE] % 10 == 0:
            for marine in self.units(UnitTypeId.MARINE):
                marine.attack(self.enemy_start_locations[0])
worker_en_route_to_build(unit_type)

This function counts how many workers are on the way to start the construction a building.

Parameters:

unit_type (UnitTypeId)

Return type:

float

class sc2.bot_ai_internal.BotAIInternal

Base class for bots.

final static convert_tuple_to_numpy_array(pos)

Converts a single position to a 2d numpy array with 1 row and 2 columns.

Return type:

ndarray

final do(action, subtract_cost=False, subtract_supply=False, can_afford_check=False, ignore_warning=False)

Adds a unit action to the ‘self.actions’ list which is then executed at the end of the frame.

Training a unit:

# Train an SCV from a random idle command center
cc = self.townhalls.idle.random_or(None)
# self.townhalls can be empty or there are no idle townhalls
if cc and self.can_afford(UnitTypeId.SCV):
    cc.train(UnitTypeId.SCV)

Building a building:

# Building a barracks at the main ramp, requires 150 minerals and a depot
worker = self.workers.random_or(None)
barracks_placement_position = self.main_base_ramp.barracks_correct_placement
if worker and self.can_afford(UnitTypeId.BARRACKS):
    worker.build(UnitTypeId.BARRACKS, barracks_placement_position)

Moving a unit:

# Move a random worker to the center of the map
worker = self.workers.random_or(None)
# worker can be None if all are dead
if worker:
    worker.move(self.game_info.map_center)
Parameters:
  • action (UnitCommand)

  • subtract_cost (bool)

  • subtract_supply (bool)

  • can_afford_check (bool)

Return type:

bool

final async issue_events()

This function will be automatically run from main.py and triggers the following functions: - on_unit_created - on_unit_destroyed - on_building_construction_started - on_building_construction_complete - on_upgrade_complete

Return type:

None

final static prevent_double_actions(action)
Parameters:

action

Return type:

bool

final async synchronous_do(action)

Not recommended. Use self.do instead to reduce lag. This function is only useful for realtime=True in the first frame of the game to instantly produce a worker and split workers on the mineral patches.