7.2 - Common Pitfalls- Desyncs and Stale Data
Beyond simple syntax errors, bot development has its own class of insidious bugs. Two of the most common and difficult to diagnose are desyncs and errors caused by stale data. Understanding these pitfalls is essential for building a stable and reliable bot.
Pitfall 1: The Desync Bug
A "desync" (desynchronization) is a critical failure where your bot's perception of the game world no longer matches the reality of the game engine. Your bot is effectively "hallucinating"—seeing units that aren't there or missing units that are.
The Anatomy of a Desync:
+-----------------+ +----------------+
| Your Bot's | (Commands are | |
| Internal State | too frequent | SC2 Game |
| (e.g., sees 10 | or complex) | Engine |
| marines) | -----------------> | (e.g., only |
+-----------------+ | has 5 marines)|
| +----------------+
| (State Mismatch)
v
CRITICAL FAILURE
(Bot issues commands for
units that don't exist)
- Primary Cause: Sending an overwhelming number of commands to the game engine. This is almost always a symptom of inefficient code that spams redundant commands (see Chapter 7.1).
- The Danger Zone: APM consistently in the tens of thousands is a major warning sign.
How to Avoid Desyncs |
---|
1. Follow Efficiency Principles: Write smart, non-redundant command logic. |
2. Command Only What's Needed: Only issue commands to units whose state needs to change (e.g., .idle units). |
3. Trust the Library's Batching: Simply call action methods (e.g., my_unit.attack(target) ). The library automatically collects all commands within a step and sends them as one batch. |
4. Monitor Your APM: Use self.state.score.apm to check your bot's APM. If it's abnormally high, it's a bug. |
Pitfall 2: Stale Data
A "stale data" bug occurs when your bot makes a decision based on outdated information. The #1 cause of this is incorrectly storing a Unit
object in a self
variable between game steps.
The Problem: The Unit
Object is a Temporary Snapshot
The Unit
objects you receive in self.units
on each on_step
are snapshots in time. They are valid only for that single game step. On the next step, the library creates a fresh set of Unit
objects with updated data. A Unit
object from a previous step is now "stale"—its position, health, and other data are wrong.
on_step(iteration=100)
+------------------------------------+
| Unit A (tag=1, health=50, pos=X) | <-- You get this fresh object.
+------------------------------------+
| self.my_favorite_unit = Unit A | <-- DANGER: You store the object itself.
`------------------------------------`
|
v
on_step(iteration=101)
+------------------------------------+
| Unit A (tag=1, health=40, pos=Y) | <-- The library gets a new, updated object.
+------------------------------------+
| self.my_favorite_unit.health | <-- This still returns 50! It's stale.
`------------------------------------`
The Solution: Store Tags, Not Objects
A unit's tag
is a unique integer that is permanent for the entire match. It's the "serial number" for that unit.
The Golden Rule:
- DO NOT store
Unit
orUnits
objects inself
variables. - DO store a unit's
.tag
if you need to track it over time.
A Developer's Checklist for Data Persistence
- Do I need to track a specific unit across multiple steps? (e.g., a scout, a specific spellcaster).
- If yes, get its tag:
self.scout_tag = my_scout.tag
. - In later steps, retrieve the fresh unit object using the tag:
current_scout = self.units.find_by_tag(self.scout_tag)
. - Always handle the case where the unit might be dead:
if current_scout is None:
.
Code Example: The Scout Tracker
This bot demonstrates the correct, professional way to track a specific unit. It designates a scout, stores its tag
, and then correctly retrieves the fresh Unit
object on each step to check its status.
# scout_tracker_bot.py
from typing import Optional
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.unit import Unit
class ScoutTrackerBot(BotAI):
"""Demonstrates the correct way to track a unit using its tag to avoid stale data."""
def __init__(self):
super().__init__()
# This will hold the scout's permanent ID, not a temporary object.
self.scout_tag: Optional[int] = None
async def on_step(self, iteration: int):
# 1. Designate the scout at the start of the game.
if iteration == 0:
# Select a worker to be our scout.
initial_scout_unit = self.workers.random
if initial_scout_unit:
# CORRECT: Store the integer tag.
self.scout_tag = initial_scout_unit.tag
print(f"Designated worker with tag {self.scout_tag} as the scout.")
# Send the scout to the enemy base.
initial_scout_unit.attack(self.enemy_start_locations[0])
# 2. Track the scout on every subsequent step.
if self.scout_tag is not None:
# CORRECT: Retrieve the fresh, up-to-date Unit object using the stored tag.
# This returns the unit object if it's visible, otherwise None.
current_scout: Optional[Unit] = self.units.find_by_tag(self.scout_tag)
if current_scout:
# The scout is alive and visible. We can safely use its current data.
if iteration % 112 == 0: # Throttle the log message.
print(f"Scout {self.scout_tag} is alive at {current_scout.position.rounded}.")
else:
# The scout is dead or has gone out of our vision.
print(f"Scout {self.scout_tag} has been lost. Resetting.")
# Reset the tag so we can assign a new scout if needed.
self.scout_tag = None
if __name__ == "__main__":
run_game(
maps.get("GresvanAIE"),
[
Bot(Race.Terran, ScoutTrackerBot()),
Computer(Race.Zerg, Difficulty.Easy)
],
realtime=True,
)