Skip to main content

3.3 - The Power of Async and Await

The async and await keywords are the engine of python-sc2. They are not optional features; they are the fundamental mechanism that allows your bot to remain responsive and intelligent. Understanding their purpose is non-negotiable for writing a functional bot.

The Core Problem: Your Bot is a Network Client

Every action your bot takes—building a unit, moving a Marine, querying for resources—is not an instant, local function call. It is a network request sent to the StarCraft II game engine, which then processes it and sends a response.

+------------------+         (Commands & Queries)         +---------------+
| | <----------------------------------> | |
| Your Python Bot | | SC2 Game |
| | <----------------------------------> | Engine |
+------------------+ (Game State & Acks) +---------------+
<- This round trip takes time ->

In a traditional, synchronous program, your code would freeze and wait for the game engine's response after every single command. For an RTS bot that needs to make hundreds of decisions per second, this is fatal.

The Solution: The Non-Blocking asyncio Model

asyncio allows your bot to be a "non-blocking" client. It can send a command and, instead of freezing, immediately yield control to work on other tasks. It's the difference between a worker who stops everything to watch a pot boil and one who starts the boil, then goes to chop vegetables, checking back periodically.

Synchronous (Blocking) BotAsynchronous (Non-Blocking) Bot
1. Issues "build" command.1. Issues "build" command.
2. FREEZES. Waits for SC2 to confirm.2. YIELDS. Immediately moves on.
3. (Waits...)3. Issues "attack" command.
4. (Waits...)4. YIELDS. Immediately moves on.
5. Receives "build" confirmation.5. Checks if it can afford an upgrade.
6. Finally issues "attack" command.6. (Receives confirmations in the background).

The asynchronous bot accomplishes far more in the same amount of time, making it an effective RTS player.


The Rules of Implementation: The async/await Contract

To use this model, you must adhere to a simple two-part contract.

.------------------------------------------.
| async def my_method(self): | <-- Marks a function as a "coroutine"
| | that can be paused.
| # Do some logic... |
| |
| await some_game_action() | <-- Pauses the coroutine here until the
| | action is acknowledged by the game.
| # Continue logic after ack... |
`------------------------------------------`

Implementation Checklist:

  • Rule 1: Define with async def. Any method that contains a game action or query (on_step, on_start, your own helper functions) must be defined with async def.
  • Rule 2: Call with await. Any function call that communicates with the game (e.g., self.build, unit.attack, self.expand_now) must be preceded by the await keyword.

The Most Common Mistake: The Forgotten await

Forgetting await is the #1 error for new python-sc2 developers. If you call an async function without await, the action will simply not happen.

Code Example: A Tale of Two Pylons

This bot tries to build two Pylons. One command is issued correctly, the other is not. Observe the difference in both the game and your console output.

from sc2.main import run_game
from sc2 import maps
from sc2.bot_ai import BotAI
from sc2.ids.unit_typeid import UnitTypeId
from sc2.player import Bot, Computer
from sc2.data import Race, Difficulty


class AwaitBot(BotAI):
"""A bot to demonstrate the critical importance of 'await'."""

async def on_step(self, iteration: int):
# We want this logic to run only once, as soon as we can afford the Pylons.
# This checks if we have no Pylons and none are pending construction.
if (
self.structures(UnitTypeId.PYLON).amount
+ self.already_pending(UnitTypeId.PYLON)
== 0
):
# We wait until we can afford Pylon for the demonstration.
if self.can_afford(UnitTypeId.PYLON):
await self._build_pylons_correctly()
elif self.structures(UnitTypeId.PYLON).amount == 1:
if self.can_afford(UnitTypeId.PYLON):
await self._build_pylons_incorrectly()

async def _build_pylons_correctly(self):
"""This function correctly awaits the build command."""
print("ACTION: Issuing CORRECT build command for Pylon 1...")
if self.workers.exists:
# CORRECT: The 'await' keyword ensures the command is sent and processed.
await self.build(UnitTypeId.PYLON, near=self.workers.first)

async def _build_pylons_incorrectly(self):
"""
This function is correctly defined with 'async def' but forgets
to await the build command internally.
"""
print("ACTION: Issuing INCORRECT build command for Pylon 2...")
if self.workers.exists:
# INCORRECT: This creates a coroutine object but never runs it.
# Select a random idle worker
idle_worker = self.workers.idle.random
# It does nothing in-game and will raise a RuntimeWarning.
self.build(UnitTypeId.PYLON, near=idle_worker, build_worker=idle_worker)


if __name__ == "__main__":
run_game(
maps.get("AbyssalReefLE"),
[Bot(Race.Protoss, AwaitBot()), Computer(Race.Zerg, Difficulty.Easy)],
realtime=True,
)