Skip to main content

7.1 - Writing Efficient Code - Managing APM

As your bot's intelligence grows, so does the complexity of its code. Inefficient code is not just a matter of elegance; it can lead to critical performance issues, including game-losing lag and desync bugs. Writing efficient code is about making your bot both fast and stable.

A key symptom of inefficient code is abnormally high APM (Actions Per Minute). While high APM is not a goal, an APM in the tens of thousands is a red flag that your bot is spamming redundant commands.

The Performance Landscape

Your bot's performance is a balance between the complexity of its logic and the time it has to execute it.

+------------------------------------+
| Your Bot's Logic (on_step) |
|------------------------------------|
| - Complex calculations | <--- The more you do, the longer it takes.
| - Looping through many units |
| - Issuing many commands |
+------------------------------------+
|
v
+------------------------------------+
| Real-world Time Limit per Step | <--- The game engine will not wait for you.
| (e.g., ~16ms for 60FPS) | If your code is too slow, you miss your turn.
+------------------------------------+

The goal is to fit all necessary logic within this time limit.


Core Principles for Efficient Code

PrincipleThe Problem (Inefficient)The Solution (Efficient)
1. Avoid Redundant CommandsTelling a Marine that is already attacking to attack again. This is the #1 cause of high APM.Only issue commands to units whose state needs to change (e.g., units that are idle).
2. Batch Your ActionsUsing await for each action inside a loop. Each await can force a sequential execution within a single step.Issue commands directly (e.g., unit.attack(...)). The library automatically collects all actions and sends them as a single batch at the end of the game step.
3. Throttle Your LogicChecking for a new expansion or a high-level upgrade 60 times per second. This is unnecessary.Use the iteration counter or self.time to run non-urgent logic less frequently (e.g., once every 5-10 seconds).
4. Cache Expensive CalculationsRe-calculating a complex value (like the perfect wall-off position) on every single game step.Calculate the value once in on_start and store the result in a self variable (e.g., self.wall_position).

Developer's Checklist for Optimization

Before finalizing a new piece of logic, review it with this checklist:

  • Is this command necessary? Am I only telling units to do something if they aren't already doing it? (Check .is_idle).
  • Am I commanding a group? If so, am I just issuing commands directly and letting the library batch them automatically?
  • Does this need to run every step? Could this logic be throttled to run every few seconds instead?
  • Is this calculation complex? If so, can I compute it once and cache the result?

Code Example: The "Optimized" Bot

This bot demonstrates all four principles of efficiency. It lets the library batch actions, commands only idle units, throttles its logging, and caches a key strategic position in on_start.

# optimized_bot.py

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

class OptimizedBot(BotAI):
"""A bot that demonstrates core principles of efficient code."""

def __init__(self):
super().__init__()
# A variable to hold our cached position.
self.rally_point = None

async def on_start(self):
"""Called once at the start. Perfect for caching calculations."""
# PRINCIPLE 4: CACHE EXPENSIVE CALCULATIONS
# Calculate a safe rally point behind our main base once.
self.rally_point = self.start_location.position.towards(
self.game_info.map_center, -5
)
print(f"Rally point cached at: {self.rally_point.rounded}")

async def on_step(self, iteration: int):
# Army management logic.
await self.manage_army()
# Non-urgent logic.
self.manage_logging(iteration)
# Helper to build our army.
await self.build_army()

async def manage_army(self):
# PRINCIPLE 1: AVOID REDUNDANT COMMANDS
# We only select idle marines to give new orders to.
idle_marines = self.units(UnitTypeId.MARINE).idle

if idle_marines.exists:
# PRINCIPLE 2: BATCH ACTIONS
# The library automatically batches actions for us. We just issue commands.
for marine in idle_marines:
marine.attack(self.enemy_start_locations[0])

def manage_logging(self, iteration: int):
# PRINCIPLE 3: THROTTLE LOGIC
# This logging will only run once every 10 game seconds.
# 22.4 is the number of game steps per second on 'Faster' speed.
if iteration % round(22.4 * 10) == 0:
army_count = self.units(UnitTypeId.MARINE).amount
print(f"[{self.time_formatted}] Army Count: {army_count}. APM: {self.state.score.apm}")

async def build_army(self):
"""A simple helper method to produce units for the demo."""
# Build Supply Depots
if self.supply_left < 2 and not self.already_pending(UnitTypeId.SUPPLYDEPOT):
if self.can_afford(UnitTypeId.SUPPLYDEPOT):
await self.build(UnitTypeId.SUPPLYDEPOT, near=self.townhalls.first.position.towards(self.game_info.map_center, 5))

# Build Barracks
if self.structures(UnitTypeId.BARRACKS).amount < 3 and not self.already_pending(UnitTypeId.BARRACKS):
if self.can_afford(UnitTypeId.BARRACKS):
await self.build(UnitTypeId.BARRACKS, near=self.townhalls.first.position.towards(self.game_info.map_center, 5))

# Train Marines
idle_barracks = self.structures(UnitTypeId.BARRACKS).ready.idle
if idle_barracks.exists:
if self.can_afford(UnitTypeId.MARINE) and self.supply_left > 0:
# Command the first idle barracks to train a marine.
idle_barracks.first.train(UnitTypeId.MARINE)


if __name__ == "__main__":
run_game(
maps.get("GresvanAIE"),
[
Bot(Race.Terran, OptimizedBot()),
Computer(Race.Zerg, Difficulty.Medium)
],
realtime=False, # Run without graphics to see performance.
)