Quick Reference
Bookmark this page and come back to it - it’s your cheatsheet.
If you are reading the tutorial for the first time, scroll over basic info below and spend a minute if anything catches your eye.
You will feel overwhelmed if you read this all in one go, so lets rather start with small examples.
After a quick scroll-over, let’s do some tutorials ➡️.
Overview
Even though it’s a reference, this document can be read top to bottom:
- Glossing over constants and common game info, we then look in detail at
- Unit/Group selection, actions, info and events.
- Zooming out, we then review the match lifecycle, callbacks, api observations and communication.
- We round off with map awareness and geometry, followed by static and meta-data.
Unless stated otherwise, in the tables below, methods are from the context of an instance of a Bot. Meaning on_step
you can call common.minerals
.
Constants
There are five types of data identifiers.
-
Unit Types: zergling, marine, zealot, gateway, warpgate, barracks, flying barracks, etc.
-
Abilities: attack, stop, build, morph, lift, land, casting specials, etc.
-
Buffs (incl. nerfs): cloaked, neural parasite, carrying minerals/gas, etc.
-
Effects: scanner sweep, psi storm, corrosive bile, etc.
-
Upgrades: tunneling claws, stimpack, ground weapons level 2, psi storm tech, etc.
A few thousand constants are generated from the game’s stableids.json
for your auto-completion convenience.
class | examples |
---|---|
Api::UnitTypeId | Api::UnitTypeId::ZERGLING, Api::UnitTypeId::BARRACKS |
Api::AbilityId | Api::AbilityId::ATTACK, Api::AbilityId::MORPHZERGLINGTOBANELING_BANELING, Api::AbilityId::BARRACKSTRAIN_MARINE |
Api::BuffId | Api::BuffId::FUNGALGROWTH, Api::BuffId::CARRYMINERALFIELDMINERALS |
Api::EffectId | Api::EffectId::SCANNERSWEEP, Api::EffectId::PSISTORMPERSISTENT, Api::EffectId::GUARDIANSHIELDPERSISTENT |
Api::UpgradeId | Api::UpgradeId::PROTOSSGROUNDWEAPONSLEVEL2, Api::EffectId::PSISTORMTECH |
Bot identity
method | desc | Fast/Med/Slow |
---|---|---|
name | bot name | F |
race | race selected. this is updated if set Random | F |
enemy | Sc2::Player::Enemy | F |
enemy.race | enemy race selected. if Random, set after first seen unit. callback: on_random_race_detected | F |
enemy.name | enemy name | F |
enemy.type | :Participant, :Computer | F |
Common info
method | desc | Fast/Med/Slow |
---|---|---|
game_loop | incrementing int. timekeeping. nr of frames since start | F |
common.minerals | mineral count, ui top right | F |
common.vespene | gas, ui top right | F |
common.food_cap | supply available, ui top-right | F |
common.food_used | supply used, ui top-right | F |
common.food_army | army supply, ui hover supply icon | F |
common.food_workers | worker supply, ui hover supply icon | F |
common.idle_worker_count | idlers, ui bottom left icon | F |
common.army_count | unit count (not supply value) | F |
common.warp_gate_count | Protoss: nr of warp gates | F |
common.larva_count | Zerg: nr of larva | F |
common.player_id | int identifier | F |
Units / Group selection
Terminology
The Api refers to units and structures collectively as Api::Unit
.
We refer to units as “units” and structures as “structures” and collectively as capital Unit.
Unit
Api::Unit
is a protobuf Message object with methods added for ease.
Each unit or structure is of type Api::Unit.
It has attributes, such as unit.health
and unit.pos.x
/ unit.pos.y
to get it’s position.
Unique Id: Each Unit has an integer unique identifier called #tag; you will use unit.tag
to identify units.
UnitGroup
Sc2::UnitGroup is a construct of our own.
Units are contained in a Hash/Array hybrid which can perform actions such as #attack, #build, #train etc.
The 10 in-game “Control Groups” aren’t useful at this scale; UnitGroup is a programmatic alternative.
Sc2::UnitGroup#units
holds Hash<unit.tag, unit>
Basic Unit selection and collection
Primer
Bot#all_units
holds everything, allied/enemy, units or structures.
Bot#units
holds allied group of units only
Bot#structures
holds allied group of structures
UnitGroup
offers filters such as #workers
which returns a new group, containing only workers.
All for now, the full reference table is below these basic examples.
class MyBot < Sc2::Player::Bot
def on_step
# Random example worker from UnitGroup units.workers
specific_worker = units.workers.random
first_worker = units.workers.at(0) # = units.workers.first
# Get a unit tag (unique id)
specific_worker.tag #=> Integer
# Get all unit tags in a group
units.workers. #=> Array<Integer> of all unit tags = unit_group.units.keys
# Look up a specific unit in a UnitGroup by tag
units.workers[specific_worker.tag] #=> returns specific_worker
# New empty unit group
unit_group = Sc2::UnitGroup.new()
# or create from existing group or an Array<Api::Unit>
unit_group = Sc2::UnitGroup.new(other_ug)
unit_group = Sc2::UnitGroup.new(arr_units)
# Add a unit / Remove a unit
unit_group.add(unit)
unit_group.remove(unit)
# Add, subtract as you see fit, i.e. if you have some units assigned as a forward army
home_army = units.army.subtract(forward_army)
home_army = units.army - forward_army
# Hash sub-/supersets sets can be useful (<, <=, =>, >)
# i.e. subset: are we sure all our flying units are present in the forward army?
forward_army > units.army.select_attribute(:Flying) #=> bool: true if all flying units present
end
end
Filters
Every filter returns a new UnitGroup (cached), so you can chain and re-use without additional performance cost.
# Filter with blocks
injured = units.army.filter { |unit| unit.health < 100.0 }
# Filter on type(s)
units.army.select_type(Api::UnitTypeId::BANELING)
units.army.select_type([Api::UnitTypeId::BANELINGBURROWED, Api::UnitTypeId::BANELING])
# Filter on attribute(s)
units.select_attribute(Api::Attribute::Structure) # rather use #structures
units.army.select_attribute([Api::Attribute::Mechanical, Api::Attribute::Armored])
# Filters allow #not, which makes the immediate next filter be the inverse
# Select structures which are not creep tumors and not spine crawlers
structures.not.creep_tumors.not.select_type(Api::UnitTypeId::SPINECRAWLER)
Global data
method | type of Api::Unit’s in UnitGroup | Fast/Med/Slow |
---|---|---|
all_units | full, unfiltered list of units+structures | F |
effects | effects such as psi storm, lurker spikes | F |
neutral.minerals | mineral patches | F |
neutral.geyser | gas geysers | F |
neutral.watchtowers | Xel’Naga watchtowers | F |
neutral.debris | destructible debris | F |
Your Units and Structures
method | type of Api::Unit’s in UnitGroup | Fast/Med/Slow |
---|---|---|
structures | with attribute :Structure | F |
structures.hq | Command Centres, PF, Nexus, Hatch, Lair, Hive | F |
structures.townhalls | Command Centres, PF, Nexus, Hatch, Lair, Hive | F |
structures.bases | Command Centres, PF, Nexus, Hatch, Lair, Hive | F |
structures.gas | gas structures (Refinery, Assimulator, Extractor) | F |
structures.creep_tumors | Zerg creep tumors (any) | F |
structures.creep_tumors_burrowed | Zerg creep tumors burrowed underground | F |
structures.pylons / structures.warpables | Protoss pylons | F |
structures.warpgates | Protoss warp gates | F |
structures.not.creep_tumors | all structures excluding creep tumors | F |
units | not attribute :Structure | F |
units.workers | SCV, Probe, Drone + Burrowed | F |
units.larva | Zerg larva | F |
units.queens | Zerg queen | F |
units.overlords | Zerg overlords | F |
units.army | units without: workers, queens, overlords, larva | F |
units.warpables | Protoss warp prism | F |
Enemy units
Same as Bot’s unit
and structures
template, but prefixed with enemy.
method | type of Api::Unit’s in UnitGroup | Fast/Med/Slow |
---|---|---|
enemy.all_units | enemy only, all units+structures | F |
enemy.structures | with attribute :Structure | F |
enemy.structures.hq | Command Centres, PF, Nexus, Hatch, Lair, Hive | F |
enemy.structures.townhalls | Command Centres, PF, Nexus, Hatch, Lair, Hive | F |
enemy.structures.bases | Command Centres, PF, Nexus, Hatch, Lair, Hive | F |
enemy.structures.gas | gas structures (Refinery, Assimulator, Extractor) | F |
enemy.structures.creep_tumors | Zerg creep tumors (any) | F |
enemy.structures.creep_tumors_burrowed | Zerg creep tumors burrowed underground | F |
enemy.structures.pylons / structures.warpables | Protoss pylons | F |
enemy.structures.warpgates | Protoss warp gates | F |
enemy.units | not attribute :Structure | F |
enemy.units.workers | SCV, Probe, Drone + Burrowed | F |
enemy.units.larva | Zerg larva | F |
enemy.units.queens | Zerg queen | F |
enemy.units.overlords | Zerg overlords | F |
enemy.units.army | units without: workers, queens, overlords, larva | F |
enemy.structures.not.creep_tumors | all structures excluding creep tumors | F |
enemy.units.warpables | Protoss warp prism | F |
Unit Group filters
Common methods you can use on Sc2::UnitGroup
to filter properties.
method | desc | Fast/Med/Slow |
---|---|---|
select_type | select on Api::UnitTypeId | F |
reject_type | rejects on Api::UnitTypeId | F |
select_attribute | select on Api::Attribute | F |
reject_attribute | rejects on Api::Attribute | F |
filter(&block) | true/false from block: ug.filter(&:is_flying?) | F |
nearest_to(pos:, amount:) | nearest unit(s) to position | M |
select_in_circle(point:, radius:) | only units in circle | M |
not | inverses next filter | M |
Creating your own filters
units.workers
is just a helper method executing the filter UnitGroup#select_type
, as is units.army
.
You can add a filter to UnitGroup by re-opening the class and defining your own filter methods.
# in my_unit_group_extensions.rb
module Sc2
class UnitGroup
# The efficient way to use this is sub-filter from `units.army`, because it loops less.
# i.e. units.army.zealots, even though the method also exists for units.zealots and any other unit group
def zealots
select_type(Api::UnitTypeId::ZEALOT)
end
# units.army.roaches
# enemy.units.army.roaches
def roaches
select_type([Api::UnitTypeId::ROACH, Api::UnitTypeId::ROACHBURROWED])
end
end
end
Performing actions / giving orders
We refer to an order or command as an “Action”.
Api::Action
s can be performed from both the context of Bot, Unit or UnitGroup. Sending raw actions from Bot requires that you pass the source Unit(s). The advantage of performing actions on Units/UnitGroups is one less param and better reading code.
An action requires an ability_id
, such as Api::AbilityId::ATTACK
, Api::AbilityId::SMART
, etc. “SMART” is the equivalent of performing a right-click in-game.
Sometimes an action requires a target
which is a position Api::Point2D
or a target Api::Unit
.
Let’s inspect Unit Group first and it’s action helper methods.
UnitGroup actions
method | desc | Fast/Med/Slow |
---|---|---|
action(ability_id:, target: ) | makes a group perform a raw ability, i.e. Api::AbilityId::TERRANBUILD_BARRACKS | F |
build(unit_type_id:, target: …) | units.workers.build(unit_type_id: Api::UnitTypeId::BARRACKS, …) provide the unit type and target, the ability id is calculated for you. |
F |
smart(target: …) | right-click on a point or a unit, i.e. attack a unit, go into a bunker, mine a mineral | F |
attack(target: …) | attacks a Unit or attack-moves to position. units.army.attack(…) | F |
warp(unit_type_id:, target: …) | Protoss: structures.warpgates.warp(unit_type_id: Api::UnitTypeId::STALKER, target: point_2d_target). geo.warp_points is helpful. see example. |
F |
repair(target: …) | terran: units.workers.repair(target: injured_unit_or_structure) | F |
Here’s an example using Protoss warp
for five stalkers. We use the unit group structures.warpgates
as the source for this action.
def on_step
enemy_main = geo.enemy_start_position
if can_afford?(unit_type_id: Api::UnitTypeId::STALKER, quantity: 5)
# Find the nearest energy source, typically a Pylon (Api::Unit)
energy_source = structures.warpables.nearest_to(pos: enemy_main)
# geo.warp_points finds an array of 2D coordinates inside the power field
# it matches the size of the unit_type_id passed, so they don't overlap
points = geo.warp_points(source: energy_source,
unit_type_id: Api::UnitTypeId::STALKER)
# Pick the 5 facing the enemy base's position
points = points.min_by(5) { |p| p.distance_to(enemy_main) }
# For UnitGroup structures.warpgates, tell them all to warp in stalkers at provided point
points.each do |point|
structures.warpgates.warp(unit_type_id: Api::UnitTypeId::STALKER, target: point)
end
end
end
Action queue / scheduling (Shift+Click)
All action methods have a queue_command:
parameter (omitted here for brevity), which queues this action after the others.
This is akin to a Shift+Click in-game.
It always defaults to false
, so when giving multiple instructions to one unit per frame, only the last action is executed.
When queue_command:
is set to true
, the action you are sending will not override the previous command, but perform after it in sequence.
It’s handy for set-and-forget actions like constructing a structure and then queuing the worker to go mine afterwards.
Unit actions
The signatures match UnitGroup, with the exception of warp
which is really meant for a group of warpgates.
attack_with
One addition is #attack_with
. We generally contextualize from our own units, but stylistically you can perform inverse of UnitGroup#attack on an enemy unit.
For instance, you spot a ghost launching a nuke and you need to attack it quick with any 3 army units nearby.
hit_squad = enemy_ghost_unit.nearest(units: units.army, amount: 3)
enemy_ghost_unit.attack_with(units: hit_squad)
# synonymous with:
hit_squad.attack(target: enemy_ghost_unit)
method | desc | Fast/Med/Slow |
---|---|---|
unit.action(ability_id:, target: ) | makes a group perform a raw ability, i.e. Api::AbilityId::TERRANBUILD_BARRACKS | F |
unit.build(unit_type_id:, target: …) | units.workers.build(unit_type_id: Api::UnitTypeId::BARRACKS, …) provide the unit type and location, the ability id is calculated for you. |
F |
unit.smart(target: …) | right-click on a point or a unit, i.e. attack a unit, go into a bunker, mine a mineral | F |
unit.attack(target: …) | attacks a Unit or attack-moves to position. units.army.attack(…) | F |
unit.attack_with(units: …) | attacks self with supplied units:. single_enemy.attack_with(units: units.army) | F |
unit.repair(target: …) | terran: units.workers.repair(target: injured_unit_or_structure) | F |
You are always welcome to perform requests, however raw, directly from Bot#api or use some of the built-in Bot Action helpers.
See the Api Requests section for a breakdown of raw commands and Bot Actions for convenience methods built on-top of them.
Unit Data
We want you to explore the protobuf protocol definitions. It is the best reference for information which comes back from the Api.
Below is an extract from the message Unit
in a table.
For each of these properties you can use dot notation to access the property, i.e. unit.health
.
We also provide unit.previous
, if you need to compare the current state vs the previous state, i.e.
puts "taking shield damage!" if unit.shield < unit.previous.shield
# ... side-note: rather implement callback on_unit_damaged(unit, amount)
attribute | desc | Fast/Med/Slow |
---|---|---|
unit.tag | unique id | F |
unit.unit_type | correspondes to Api::UnitTypeId::NAME | F |
unit.owner | player id | F |
unit.pos | Point - 3d position with #x,#y,#z | F |
unit.facing | facing direction in radians | F |
unit.radius | radius of unit model | F |
unit.build_progress | range [0.0, 1.0] | F |
unit.cloak | See CloakState | F |
unit.buff_ids | array of Api::BuffId::NAME | F |
unit.detect_range | detection range | F |
unit.radar_range | sensor tower range | F |
unit.is_selected | unit selected in game | F |
unit.is_on_screen | unit on screen in game | F |
unit.is_blip | detected by sensor tower | F |
unit.is_powered | protoss: has power | F |
unit.is_active | building is training/researching (i.e. animated) | F |
unit.attack_upgrade_level | int 0,1,2,3 | F |
unit.armor_upgrade_level | int 0,1,2,3 | F |
unit.shield_upgrade_level | int 0,1,2,3 | F |
unit.health | float | F |
unit.health_max | float | F |
unit.shield | float | F |
unit.shield_max | float | F |
unit.energy | float | F |
unit.energy_max | float | F |
unit.mineral_contents | minerals | F |
unit.vespene_contents | geyser / gas building | F |
unit.is_flying | above ground level | F |
unit.is_burrowed | under ground level | F |
unit.is_hallucination | unit is your own or detected as a hallucination | F |
unit.orders | your unit’s orders (UnitOrder) | F |
unit.add_on_tag | terran: the id of the add-on building | F |
unit.passengers | array of PassengerUnit. has PassengerUnit#tag | F |
unit.cargo_space_taken | bunkers, medivacs, warp prism, etc. | F |
unit.cargo_space_max | bunkers, medivacs, warp prism, etc. | F |
unit.assigned_harvesters | for use with base structure | F |
unit.ideal_harvesters | for use with base/harvesting structure | F |
unit.weapon_cooldown | F | |
unit.engaged_target_tag | F | |
unit.buff_duration_remain | how long a buff or unit is still around (eg mule, broodling, chronoboost) | F |
unit.buff_duration_max | F | |
unit.rally_targets | array of RallyTarget. always has #point (Point), could be rallied to unit via #tag. | F |
But that’s NOT all.
See Static Unit Data below for more fixed meta information about Unit Type, such as weapon damage, range, etc.
Unit Events
This library generally offers two ways to access events which occurred, @event_*
attributes or by overriding ano on_*
callback.
The callbacks sometimes provide more context than just a Unit list, i.e. on_unit_damaged(unit, amount)
provides the amount of damage for you.
All attributes below return a Sc2::UnitGroup
containing Api::Unit
‘s involved in the event.
This table provides events and callbacks available, with an example below.
attribute | callback | desc | Fast/Med/Slow |
---|---|---|---|
event_units_created | on_unit_created(unit) | units only. probe or marine trained | F |
event_structures_started | on_structure_started(unit) | structure in physical progress. not completed. | F |
event_structures_completed | on_structure_completed(unit) | structure completes | F |
event_units_type_changed | on_unit_type_changed(unit, previous_unit_type_id) | morphs. baneling morph, viking land, etc. | F |
event_units_damaged | on_unit_damaged(unit, amount) | hp or shields took damage, but unit is not destroyed | F |
event_units_destroyed | on_unit_destroyed(dead_unit) | you heard the news that you’re dead | F |
Example handling of dead units
From your Bot, you can read the UnitGroup @event_units_destroyed
on_step
for all Units which got destroyed in the last frame.
Even though those Units are not present in this frame, their Api::Unit
objects are available, should like like to know, i.e. where something died and of what type.
For example, if an SCV died, you can read last frame’s Api::Unit#orders
(dead_unit.orders
). If it wanted to build an expansion, you might want to retry or expand elsewhere.
You can loop over units in @event_units_destroyed
or receive a callback by overriding (implementing) on_unit_destroyed
.
The callbacks occur after the game has ticked forward, but before on_step
.
Typically the callbacks come in the form of individual units, where the @event_*
properties contain the whole list.
Here’s an example, which attempts to save nearby workers after an orbital command structure is destroyed by the enemy.
class MyBot < Sc2::Player::Bot
# Define the callback in your bot
def on_unit_destroyed(dead_unit)
# Custom logic
if dead_unit.unit_type == Api::UnitTypeId::ORBITALCOMMAND && dead_unit.alliance == :Self
# Dead base. Make SCV's mine a nearby mineral at the nearest base
endangered = units.workers.select_in_circle(point: dead_unit.pos, radius:8)
new_base = structures.bases.nearest_to(pos: dead_unit.pos)
# Go mine nearby using right-click aka "smart" action
endangered.smart(target: neutral.minerals.nearest_to(pos: new_base))
end
end
def on_step
# Alternatively, read the events attribute
@event_units_destroyed.select_type(Api::UnitTypeId::ORBITALCOMMAND).each do |dead_unit|
next unless dead_unit.alliance == :Self
# Dead base. Same logic as above...
end
# Or even case by type
@event_units_destroyed.filter{ |u| u.alliance == :Self }.each do |dead_unit|
case dead_unit.unit_type
when *Sc2::UnitGroup::TYPE_BASES # Covers all bases. CC/PF/Orbital
# Dead base. Same logic as above...
end
end
end
end
The approach you take is stylistic. That covers Unit events, but there are additional callbacks below, once we learn about the match lifecycle.
Lifecycle
Typically handled by Match or the ladder runner, the bot will automatically connect to the host and create/join a server. The first game state observation is made and bot is readied.
From here the lifecycle is as follows:
As a bare minimum you merely need to implement on_step
. Observation and stepping is handled internally.
Callbacks
Having examined the Lifecycle, here’s a list of the callbacks you can implement.
You can override these in your bot for additional features.
method | desc | Fast/Med/Slow |
---|---|---|
on_start | override to perform work before first on_step gets called | - |
on_step | the main game loop. called after stepping + new state observation. | - |
on_parse_observation_unit(unit) | called before step for each Unit after observation. use this to decorate Unit before on_step | - |
on_actions_performed(actions) | called before step with actions you performed since last Observation | - |
on_action_errors(errors) | called before step if errors are present (errors = equivalent to red text) | _ |
on_alerts(alerts) | called before step with alerts. nuke launch, nydus, etc. | _ |
on_upgrades_completed(upgrade_ids) | called before step with completed upgrade ids. | _ |
on_random_race_detected(race) | called before step when Random enemy race discovered first | _ |
on_finish(result) | game over. result of :Victory, :Defeat or :Tie | - |
These act just like the optional unit callbacks from Unit Events above. You define these methods in your bot for them to take effect.
class MyBot < Sc2::Player::Bot
def on_start
# load sqlite or flat files from ./data/* or do map parsing preparations
# check win-rate versus opponent and select your best build
# etc.
end
def on_finish(result)
# Make a note of which strategy you chose for this opponent
# and whether `result` was a :Victory.
end
end
Observation of game state
While there are many niceties, realtime data can also be accessed in it’s raw form.
The bulk of which is in #observation, which we parse to fill #units and #structures, callbacks, etc.
Game info (#game_info) is slightly less important, holds less info and is only called if needed.
Bot#debug_json_observation
gives you a json dump of the #observation
in ./data/debug_observation.json
for casual review.
This might be easier to nagivate than #observation.inspect
, due to it’s size.
Below are some game state and miscellaneous utility methods from Sc2::Player::Bot.
method | desc | Fast/Med/Slow |
---|---|---|
game_loop | incrementing int. timekeeping. nr of frames since start | F |
observation | observation data as per Api::Observation from protobuf | F |
game_info | Api::GameInfo from protobuf. will make api request + cache if stale. | M |
previous | all game state info for previous frame stored, i.e. previous.structures | F |
chats_received | read chat messages | F |
result | :Victory, :Defeat, :Tie. use #on_finish callback instead. | F |
status | :in_game, :launched, etc. | F |
spent_minerals | virtually tracked mineral spend this step | F |
spent_supply | virtually tracked supply spend this step | F |
spent_vespene | virtually tracked vespene spend this step | F |
can_afford?(unit_type_id:, quantity: 1) | internal bookkeeping tracks every spent resource for build/train/morph. checks affordability for a Unit type (mineral/gas/supply). |
F |
debug_json_observation | saves observation to ‘./data/debug_observation.json` | M |
Observation Layers
Before we talk about making Api requests, we should mention that there are several ways to interact with the game.
By default we use the Raw layer which is data communication only.
The Api providers additional layers which can be turned on simultaneously, namely the Render Layer and the Feature Layer.
Raw Layer (always on)
The default - all data is communicated via protobuf in what you can liken to a huge JSON dump of everything you need to know.
If something has a visual queue, like “Cloaked” (hidden), the data tells us this.
Similarly if there is a radar circle on the map, the api tells us this too.
The minimap is provided in bit map format, which we parse into a Numo array for efficiency.
Render Layer (not used)
The Render layer provides a visual view of the game in low quality. You see what the human sees.
Due to the data transfer cost, this layer is not used in competitive AI.
It’s good for hobbyists who want to see what the game sees in ML, but it’s outshone by an even better ML layer, the Feature Layer.
Feature Layer (optional)
Advanced, and not necessary for most botters.
The feature layer provides access to a virtual UI, such as the player sees on the HUD in-game.
You can perform physical clicks on buttons. The center panel (“multi-panel”) is available for you to review.
The Feature Layer is almost exclusively used to fill in one or two missing parts from the Raw Layer, like unloading individual units.
While Raw’s Api::AbilityId::UNLOADALL
works just fine for most cases, you can have fine-grained control by focussing and “clicking” specific buttons.
Enabling the feature layer comes at a few millisecond performance cost as it doubles the network throughput of RequestObservation.
To enable this layer, set @enable_feature_layer in your Bot’s configure method, which is called post-init, but before the server is joined.
class MyBot < Sc2::Player::Bot
def configure
@enable_feature_layer = true
end
end
Feature layer actions
When enabled you can perform requests prefixed with:
-
Bot#action_spatial
* (i.e. action_spatial_unit_selection_rect) -
Bot#action_ui
* (i.e. action_ui_cargo_panel_unload)
See Sc2::Player::Actions.
Api Requests
Requests can be called on your api connection.
Any request will be Slow as it takes a few milliseconds to talk to the client.
You can perform these on Bot#api
, i.e. api.available_maps
will return an array of maps available.
For most methods, there are helper method alternatives at the Bot level. See Bot Actions for convenience methods to these raw api requests below.
If you are interested in sending raw commands, you should be familiar with the protobuf definitions and/or have the .proto
files handy for reference.
method | desc | Fast/Med/Slow |
---|---|---|
api.ping | tests connection. returns client information | S |
api.available_maps | returns directory of maps that can be played on | S |
api.create_game | send to host to initialize game | S |
api.join_game | joins a game | S |
api.step(step_count = 1) | ticks game loop forward by nr of steps | S |
api.restart_game | restarts. single player only | S |
api.leave_game | disconnects from a multiplayer game | S |
api.request_quick_save | saves game to an in-memory bookmark | S |
api.request_quick_load | loads from quick save | S |
api.quit | quits and closes client. does not work on ladder | S |
api.data | data about different gameplay elements. may be different for different games. | S |
api.game_info | static data about the current game and map | S |
api.observation | snapshot of the current game state | S |
api.query | additional methods for inspecting game state. synchronous and must wait on response | S |
api.query_pathings | pathing specific query helper | S |
api.query_abilities | queries one or more ability availability checks | S |
api.query_abilities_for_unit_tags | helper making ability queries using unit tags | S |
api.query_placements | query if locations are placeable | S |
api.action | executes an array of [Api::Action] for a participant. rather use Bot#action* for batching. |
S |
api.debug | display debug information and execute debug actions. rather use Bot#debug* methods for batching. |
S |
api.save_replay | gets replay binary data | S |
api.replay_info | query game information about a replay without watching | S |
api.start_replay | for watching replays | S |
api.observer_action | move camera / follow player actions supported. | S |
api.observer_action_camera_move | observer only, camera move action helper | S |
Bot Actions
See Sc2::Player::Actions.
method | desc | Fast/Med/Slow |
---|---|---|
queue_action | helper for queueing bare-bone Api::Action (any type) for this step | S |
action | Best way to queue an action. most other methods pass through here. does ‘Api::ActionRawUnitCommand` | S |
build | abstracts action, detects correct ability from a passed unit type build(unit_type_id: Api::UnitTypeId::BARRACKS, … |
S |
warp | Protoss: structures.warpgates.warp(…) to warp in units | S |
action_raw_toggle_autocast | enable/disable auto-cast for units | S |
action_raw_camera_move | moves camera | S |
action_raw_unit_command | helper for doing queue_action with an Api::ActionRawUnitCommand | S |
action_chat | send a chat message via channel :Broadcast or :Team. | S |
action_spatial_unit_command | Feature Layer: unit command | S |
action_spatial_camera_move | Feature Layer: move camera | S |
action_spatial_unit_selection_point | Feature Layer: click on unit/minimap | S |
action_spatial_unit_selection_rect | Feature Layer: box units | S |
action_ui_control_group | Feature Layer: set/select control groups | S |
action_ui_select_army | Feature Layer: select army hotkey | S |
action_ui_select_warp_gates | Feature Layer: select warp gate hotkey | S |
action_ui_select_larva | Feature Layer: select larva gate hotkey | S |
action_ui_select_idle_worker | Feature Layer: select idle workers gate hotkey | S |
action_ui_multi_panel | Feature Layer: control the center bottom panel | S |
action_ui_cargo_panel_unload | Feature Layer: control cargo displays in multi panel | S |
action_ui_production_panel_remove_from_queue | Feature Layer: remove unit from production in multi panel | S |
action_ui_toggle_autocast | Feature Layer: enable/disable auto-cast for units | S |
Given these, will have no reason to perform raw Api commands.
Debug Actions
For local, offline play only, you can use the debug methods to draw shapes and modify world behaviour such as instant-build and spawing/killing units.
Debug commands are useful for setting up specific test scenarios.
These commands are stripped at network layer on the competitive aiarena ladder.
method | desc | Fast/Med/Slow |
---|---|---|
debug_create_unit(unit_type_id:, owner:, pos:, quantity: 1) | Spawns a quantity of units under an owner at position given. | S |
debug_draw_box(point:, radius: 0.5, color: nil) | Draws a box around position xy at base of z. | S |
debug_draw_line(p0:, p1:, color: nil) | Draws a line between two Api::Point’s for color. | S |
debug_draw_sphere(point:, radius: 1.0, color: nil) | Debug draws a sphere at position with a radius in color. | S |
debug_end_game(end_result:) | Ends game with a specified result of either Surrender or DeclareVictory. | S |
debug_game_state(command) | Toggles cheat commands on/off (send only once to enable) See: Api::DebugGameState for values, i.e. Api::DebugGameState::Fast_build | S |
debug_kill_unit(unit_tags:) | Kills a target unit or unit tag. | S |
debug_print(text, size: 14) | Prints debug text top left corner. | S |
debug_set_unit_value(unit_tag:, unit_value:, value:) | Sets unit_value Energy, Life or Shields for a specific unit tag to given value. | S |
queue_debug_command(debug_command) | Queues debug command for performing later. | S |
Geometry / Map
Point2D
Targeting on the API is often for a 2D location Api::Point2D
.
Our Api:Unit
positions are 3d, in the form of Api::Point
(via unit.pos
), but there are several position types.
The library provides the uniform Sc2::Position
, to ease geometric interoperability. You can sum a 3D Point with a 2D Point2D and get a resulting Point2D, which ignores the z-axis. Proto objects Api::Point
, Api::Point2D
,Api::PointI
,Api::Size2DI
are additionally type Sc2::Position
which respond to #x and #y.
An array of [x,y] coordinates can be turned into a Api::Point2D
with Array#to_p2d. Here are some frequent conversions you might do to get a target Api::Point2D
.
# Creating directly
Api::Point[x, y] #=> <Api::Point2D: x: ..., y: ...>
Api::Point2D.new(x: some_x, y: some_y) #=> <Api::Point2D: x: ..., y: ...>
# Convert array coordinate pair [x, y]
[1.0, 2.0].to_p2d #=> <Api::Point2D: x: 1.0, y: 2.0>
# Unit.pos (Api::Point) conversion
some_unit.pos.to_2d #=> <Api::Point2D: x: ..., y: ...>
target keyword
Additionally, most abstracted targets which require either a 2D position (Api::Point2D
) or a unit’s tag (Integer
).
The target:
keyword param in methods lets you know it accepts either.
To preserve syntactic flow without the need to type-cast as often, when you see “target”, you can pass any reasonable type.
Maps
Coordinate system
Games generally use XY coordinates with the origin bottom-left, so [0.0, 0.0]
is at the origin, bottom left.
Maps have a maximum 255 width/height [0 to 254, 0 to 254].
The maps are clamped to the dimensions it was created at and dont have to be square, i.e. a “2000AtmoshperesAIE” is 144 x 132 (width by height).
The library returns maps in the form Numo
arrays for speed and processing. If you intend on using raw map info (instead of the helper methods below), we encourage you to learn about Numo which is Ruby’s NumPy equivalent.
The map data comes in the form two arrays, where the outermost holds rows and the inner holds columns. If these were pure Ruby arrays, you’d call them like this parsed_visiblity_grid[y][x]
❌,
but the Numo lookup is one method with two params parsed_visiblity_grid[y, x]
✅.
The 10th column, second from the bottom is therefore parsed_visiblity_grid[1, 9]
.
Through the library the only time where X/Y is swapped is when working with raw map data.
Raw map data
Has Y amount of rows corresponding to map height and X amount of columns corresponding with map width.
method | desc | Fast/Med/Slow |
---|---|---|
geo.parsed_placement_grid | boolean: map tiles marked placeable. static. | F |
geo.parsed_terrain_height | float: z position (height) of tile. static. | F |
geo.parsed_pathing_grid | boolean: pathable tiles. updates to reflect buildings. | S, then cached 2 frames |
geo.parsed_visibility_grid | boolean: minimap visibility. int flag 0,1,2 | S, then cached 2 frames |
geo.parsed_creep | boolean: minimap Zerg’s creep spread | F |
geo.parsed_power_grid | boolean: Protoss’ powered tiles | F |
For a visual aid, you can print the entire parsed_pathing_grid to console as 0’s and 1’s.
# We reverse here (Numo::NArray#flipud) before we print, otherwise the map is vertically flipped.
geo.parsed_pathing_grid.flipud.each_over_axis(0) do |row|
puts row.to_a.join("")
end
Typically, you’d want to use the helper methods such as map_visible?(x:, y:)
or placeable?(x:, y:)
, see table below.
Map, Minimap & Geometric helpers
Bot#geo
gives an instance of Sc2::Player::Geometry
.
From the context of your bot, you can access the following map/geo methods:
method | desc | Fast/Med/Slow |
---|---|---|
geo.map_width | map tile width. Range is 1-255 | F |
geo.map_height | map tile height. Range is 1-255 | F |
geo.map_tile_range_x | range 0..(map_width - 1). useful for x.clamp() to stay in map bounds | F |
geo.map_tile_range_y | range 0..(map_height - 1). useful for y.clamp() to stay in map bounds | F |
geo.powered?(x:, y:) | whether a x/y block is powered | F |
geo.pathable?(x:, y:) | whether a x/y block is pathable as per minimap | F |
geo.terrain_height(x:, y:) | float height (z position) of tile (-16.0 to 16.0) | F |
geo.visibility(x:, y:) | visibility indicator: 0=Hidden,1= Snapshot,2=Visible | F |
geo.map_visible?(x:, y:) | whether the point (tile) is currently in vision | F |
geo.map_seen?(x:, y:) | whether point (tile) has been seen before or currently visible | F |
geo.map_unseen?(x:, y:) | has never been seen/explored before (dark fog) | F |
geo.creep?(x:, y:) | Zerg: whether a tile has creep on it, as per minimap | F |
geo.expansions | Gets expos and surrounding minerals. ‘[Hash<Api::Point2D, UnitGroup>]` Location => UnitGroup of resources (minerals+geysers) |
M |
geo.expansion_points | Returns a list of 2D points for expansion build locations = geo.expansions.keys |
M |
geo.expansions_unoccupied | a slice of #expansions where a base hasn’t been built yet | M |
geo.build_coordinates(length:, on_creep: false, in_power: false) | buildable point grid for squares of size, i.e. 3 = 3x3 placements | M |
geo.build_placement_near(length:, target:, random: 1) | gets a buildable location for a square of length, near target. <br/>can randomly select between ‘random` number of possible points for robustness. | M |
geo.points_nearest_linear(source:, target:, offset: 0.0, increment: 1.0, count: 1) | find points on a straight line. equally spaced from a source to a target. line units in a row. | F |
geo.point_random_near(pos:, offset: 1.0) | a random point near a location with a positive/negative offset magnitude. scatter units. | F |
geo.point_random_on_circle(pos:, radius: 1.0) | a random point on a circle’s circumference. fan units around point. | F |
geo.warp_points(source:, unit_type_id: ) | Protoss: warp locations at a power source, for the width of a unit type. | M |
Position (Point and Vector math)
When you get into “micro”, the micro-management of unit actions for performance or strategic gain, these basic geometric helpers will get you started.
As mentioned above, Api::Unit#pos
returns a 3d Api::Point
which conforms to Sc2::Position
.
When targeting or working with coordinates, you generally work with Api::Point2D
, which also conforms to Sc2::Position
.
You thereby interoperate between the various coordinate types using common ground and without too type conversion.
Let’s review what you can do with unit.pos
using these Sc2::Position
methods.
method | desc | Fast/Med/Slow |
---|---|---|
add(other) ⇒ Api::Point2D (also: #+) | a new point representing the sum of this point and the other point pos.add(other_pos) or: pos + other_pos |
F |
subtract(other) ⇒ Api::Point2D (also: #-) | returns a new point representing the difference between this point and the other point/number. | F |
divide(scalar) ⇒ Api::Point2D (also: #/) | a new point representing this point divided by the scalar. | F |
multiply(scalar) ⇒ Api::Point2D (also: #*) | returns this point multiplied by the scalar | F |
cross_product(other) ⇒ Float | the cross product of this vector and the other vector. | F |
dot(other) ⇒ Float | the dot product of this vector and the other vector. | F |
angle_to(other) ⇒ Float | the angle between this vector and the other vector, in radians. | F |
away_from(other, distance) ⇒ Point2D | moves in direction away from the other point by distance. | F |
distance_squared_to(other) ⇒ Float | the squared distance between this point and the other point. | F |
distance_to(other) ⇒ Float | calculates the distance between self and other Sc2::Position | F |
distance_to_circle(center, radius) ⇒ Float | distance from this point to the circle. returns 0 if point is inside circle | F |
distance_to_coordinate(x:, y:) ⇒ Float | distance between this point and coordinate of x and y | F |
lerp(other, scale) ⇒ Api::Point2D | linear interpolation between this point and another for scale Finds a point on a line between two points at % along the way | F |
magnitude ⇒ Float | for vector returns the magnitude, synonymous with Math.hypot | F |
normalize ⇒ Api::Point2D | a new point representing the normalized version of this vector (unit length) | F |
offset(x, y) ⇒ Api::Point2D | creates a new point with x and y which is offset | F |
offset!(x, y) ⇒ Sc2::Position | changes this point’s x and y by the supplied offset. | F |
random_offset(offset) ⇒ Api::Point2D | randomly adjusts both x and y by a range of: -offset..offset. | F |
random_offset!(offset) ⇒ Sc2::Position | changes this point’s x and y by the supplied offset. | F |
towards(other, distance) ⇒ Point2D | moves in direction towards other point by distance. | F |
This is the most likely area which will gain expansion over time, so review the class Sc2::Position for updates.
Static Data
There is a large amount of structure game data available, which doesn’t pertain to state but rather the built-in game attributes.
What if you wanted to know the type of damage a Weapon does, a unit’s vision range or how much an upgrade costs?
The Bot#data
grants access to information via Sc2::Data
.
Data
See Sc2::Data and the data protocol file at github.com/Blizzard/s2client-proto/blob/master/s2clientprotocol/data.proto.
method | desc | Fast/Med/Slow |
---|---|---|
data.abilities [ability_id] | Hash<Integer, Api::AbilityData> AbilityId => AbilityData | F |
data.upgrades [upgrade_id] | Hash<Integer, Api::UpgradeData> UpgradeId => UpgradeData | F |
data.units [unit_type_id] | Hash<Integer, Api::UnitTypeData> UnitTypeId => UnitTypeData | F |
data.effects | just a list, use ‘Api::EffectId` directly | F |
data.buffs | just a list, use ‘Api::BuffId` directly | F |
Static Unit data
Data as it pertains to a specific unit’s data type can be accessed via the convenience method Api::Unit#unit_data
.
# Consider you select a marine
fighter = units.army.select_type(Api::UnitTypeId::MARINE).random
# Print info about unit type MARINE
pp fighter.unit_data
# Or from the context of your Bot, pass in a marine to get info about type MARINE
pp unit_data(fighter)
# Or by using a UnitTypeId directly
pp unit_data(Api::UnitTypeId::MARINE)
These are all equal.
Real-time data vs static
The Data class brings data from the current game, once before on_start
. It is fixed for the entire game.
some_unit.unit_data
(which calls data.units
) will not change, nor will data.abilities[...]
All actual Api::Unit properties are realtime, for this moment, refreshed on each Observation.
See the protobuf message Unit for a list of such properties or run #inspect on a Unit to observe it’s attributes.
Static Ability data vs Query
As above with unit_data, Bot#ability_data(ability_id)
can provide static information surrounding a specific ability.
Can a unit use the ability?
Conversely, if you need dynamic information about which abilities the unit can perform right now, you can run a Query for that information.
This will show, i.e. if a specific Ghost can Nuke or whether a Gateway can warp a Stalker, etc.
(some_unit.tag, ignore_resource_requirements: true)
Tech tree
Courtesy of the brilliant work of the python crew, Dentosal and BuRny <3.
If you wish to do meta programming based on the game’s tech requirement structure, you can use the tech Api::TechTree.
All calls are statically made to the class.
method | desc | Fast/Med/Slow |
---|---|---|
Api::TechTree.creates_unit_types(unit_type_id:) | what unit types a specific unit type can produce. Barracks->Marine/Marauder/Reaper | F |
Api::TechTree.unit_created_from(unit_type_id:) | returns the unit type which can create this unit. Marine->Barracks | F |
Api::TechTree.unit_type_creation_abilities(source:, target: nil) | which units can be created at source + the ability to trigger it. | F |
Api::TechTree.unit_type_creation_ability_id(source:, target:) | which ability id creates the target, given the source unit/building | F |
Api::TechTree.upgrade_researched_from(upgrade_id:) | what the unit type an upgrade is researched from | F |
Protocol
The library talks to SC2 over Google Protobuf.
The .proto files ship with the gem but their original source is available here for review:
Proto: github.com/Blizzard/s2client-proto/tree/master/s2clientprotocol
Overview: github.com/Blizzard/s2client-proto/blob/master/docs/protocol.md
Namespaces
The code forces you to know at all times whether you’re working with the library or api objects.
Sc2
namespace is for gem internals.
Api
namespace is for protocol data and Message objects.
Functionality might be added to Api object, such as Api::Unit#attack
for programming ease.
hero_marine.attack(target: ...)
is a more desirable usage than working from the bot instead, i.e.
class MyBot < Sc2::Player::Bot
def on_step
marines = units.army.select_type(Api::UnitTypeId::MARINE)
hero_marine = marines.first
# Example raw action command for unit from Bot
api.send_request_for(action: Api::RequestAction.new(
actions: [Api::Action.new(
action_raw: Api::ActionRaw.new(
unit_command: Api::ActionRawUnitCommand.new(
ability_id: Api::AbilityId::ATTACK,
source: hero_marine.tag,
target_unit_tag: #...
)
)
)]
))
# Simplified with helper method action_raw_unit_command
action_raw_unit_command(unit_tags: hero_marine.tag,
ability_id: Api::AbilityId::ATTACK,
target_unit_tag: #...
)
# But from the context of a Unit, using #attack extension,
# this code feels better than both.
hero_marine.attack(target: ) #...
# (unit groups, same syntax as Unit)
# marines.attack(target: #...)
end
end
Therefore, sometimes Api
objects such as Api::Unit
or Api::Point2D
will do Sc2
-type work and be more than just store information or be a target for an action request.
It will be to your own benefit to actually reference the class documentation. If you open up Api::Unit, this will be immediately apparent.
Verbose by design
References are verbose by default and mimics the proto definition, which is good for your immersion.
If you target something from the Api, you have to type “Api” and you learn about the Api. Yay!
However, this means you often triple-bang into constants, i.e. Api::AbilityId::RALLY_BUILDING
.
Once you’ve mastered everything, feel free to reduce verbosity in any way you see fit.
# Option A: Define short names. Safe.
TID = Api::UnitTypeId # i.e. TID:MOTHERSHIP
AID = Api::AbilityId # Api::AbilityId::EFFECT_STIM becomes AID:EFFECT_STIM
# etc. for EffectId, BuffId, UpgradeId
# Optionally: Namespace your bot inside module Sc2 to drop all Sc2::* prefixes. Safe.
module Sc2
class MyBot < Player::Bot
# Option B: Mildly riskier than Option A, but safe for now
include Api # All of Api is yours, directly call UnitTypeId::MOTHERSHIP
end
end
Protobuf enums can be referenced as Symbols.
# enum Result {
# Victory = 1;
# Defeat = 2;
# Tie = 3;
# Undecided = 4;
# }
Api::Result::Victory # can be substituted as...
:Victory
# enum AIBuild {
# RandomBuild = 1;
# Rush = 2;
# Timing = 3;
# Power = 4;
# Macro = 5;
# Air = 6;
# }
Api::AIBuild::Air # can be substituted as ...
:Air
# This is not true for the Api::*Id::* fields, such as AbilityId, because they are generated and not proto definitions.
Keyword arguments
Also, where an id for a specific type is required, keyword params will tell you what it wants. Great!
def ...(unit_type_id: ...)
def ...(ability_id: ...)
and so on.
Descriptive keyword arguments are here to stay, but should you grow weary of the lengthy signatures, you are welcome to define your own wrapping methods for your most frequently typed calls.
New botters should learn easily, but you can create your own joy. ❤️
Here be tutorials.