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) Bot | Asynchronous (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 withasync 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 theawait
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,
)