************* Introduction ************* This is an overview to the BurnySc2/python-sc2 library which can be found here: https://github.com/BurnySc2/python-sc2 Requirements ------------- - Python 3.9 or newer - StarCraft 2 Client installation in the **default installation path** which should be ``C:\Program Files (x86)\StarCraft II`` Installation ------------- Install through pip using ``pip install burnysc2`` if Python is in your environment path, or go into your python installation folder and run through console ``pip install burnysc2``. Alternatively (of if the command above doesn't work) you can install a specific branch directly from github, here the develop branch:: pip install poetry pip install --upgrade --force-reinstall https://github.com/BurnySc2/python-sc2/archive/develop.zip Creating a bot --------------- A basic bot can be made by creating a new file `my_bot.py` and filling it with the following contents:: import sc2 from sc2.bot_ai import BotAI from sc2.player import Bot, Computer class MyBot(BotAI): async def on_step(self, iteration: int): print(f"This is my bot in iteration {iteration}!") sc2.run_game( sc2.maps.get("AcropolisLE"), [Bot(sc2.Race.Zerg, MyBot()), Computer(sc2.Race.Zerg, sc2.Difficulty.Hard)], realtime=False, ) You can now run the file using command ``python my_bot.py`` or double clicking the file. A SC2 window should open and your bot should print the text several times per second to the console. Your bot will not do anything else because no orders are issued to your units. Available information in the game ------------------------------------ Information about your bot:: # Resources and supply self.minerals: int self.vespene: int self.supply_army: int # 0 at game start self.supply_workers: int # 12 at game start self.supply_cap: int # 14 for zerg, 15 for T and P at game start self.supply_used: int # 12 at game start self.supply_left: int # 2 for zerg, 3 for T and P at game start # Units self.warp_gate_count: Units # Your warp gate count (only protoss) self.idle_worker_count: int # Workers that are doing nothing self.army_count: int # Amount of army units self.workers: Units # Your workers self.larva: Units # Your larva (only zerg) self.townhalls: Units # Your townhalls (nexus, hatchery, lair, hive, command center, orbital command, planetary fortress self.gas_buildings: Units # Your gas structures (refinery, extractor, assimilator self.units: Units # Your units (includes larva and workers) self.structures: Units # Your structures (includes townhalls and gas buildings) # Other information about your bot self.race: Race # The race your bot plays. If you chose random, your bot gets assigned a race and the assigned race will be in here (not random) self.player_id: int # Your bot id (can be 1 or 2 in a 2 player game) # Your spawn location (your first townhall location) self.start_location: Point2 # Location of your main base ramp, and has some information on how to wall the main base as terran bot (see GameInfo) self.main_base_ramp: Ramp Information about the enemy player:: # The following contains enemy units and structures inside your units' vision range (including invisible units, but not burrowed units) self.enemy_units: Units self.enemy_structures: Units # Enemy spawn locations as a list of Point2 points self.enemy_start_locations: List[Point2] # Enemy units that are inside your sensor tower range self.blips: Set[Blip] # The enemy race. If the enemy chose random, this will stay at random forever self.enemy_race: Race Other information:: # Neutral units and structures self.mineral_field: Units # All mineral fields on the map self.vespene_geyser: Units # All vespene fields, even those that have a gas building on them self.resources: Units # Both of the above combined self.destructables: Units # All destructable rocks (except the platforms below the main base ramp) self.watchtowers: Units # All watch towers on the map (some maps don't have watch towers) self.all_units: Units # All units combined: yours, enemy's and neutral # Locations of possible expansions self.expansion_locations: Dict[Point2, Units] # Game data about units, abilities and upgrades (see game_data.py) self.game_data: GameData # Information about the map: pathing grid, building placement, terrain height, vision and creep are found here (see game_info.py) self.game_info: GameInfo # Other information that gets updated every step (see game_state.py) self.state: GameState # Extra information self.realtime: bool # Displays if the game was started in realtime or not. In realtime, your bot only has limited time to execute on_step() self.time: float # The current game time in seconds self.time_formatted: str # The current game time properly formatted in 'min:sec' Possible bot actions --------------------- The game has started and now you want to build stuff with your mined resources. I assume you played at least one game of SC2 and know the basics, for example where you build drones (from larva) and SCVs and probes (from command center and nexus respectively). Training a unit ^^^^^^^^^^^^^^^^ Assuming you picked zerg for your bot and want to build a drone. Your larva is available in ``self.larva``. Your bot starts with 3 larva. To choose which of the larva you want to issue the command to train a drone, you need to pick one. The simplest you can do is ``my_larva = self.larva.random``. Now you have to issue a command to the larva: morph to drone. You can issue commands using the __call__ function: ``unit(ability)``. You have to import ability ids before you can use them. ``from sc2.ids.ability_id import AbilityId``. Here, the action can be ``my_larva(AbilityId.LARVATRAIN_DRONE)``. In total, this results in:: from sc2.ids.ability_id import AbilityId my_larva = self.larva.random my_larva(AbilityId.LARVATRAIN_DRONE) Important: The action will be issued after the ``on_step`` function is completed and the bot communicated with the SC2 Client over the API. This can result in unexpected behavior. Your larva count is still at three (``self.larva.amount == 3``), your minerals are still at 50 (``self.minerals == 50``) and your supply did not go up (``self.supply_used == 12``), but expected behavior might be that the larva amount drops to 2, self.minerals should be 0 and self.supply_used should be 13 since the pending drone uses up supply. The last two issues can be fixed by calling it differently, specifically:: self.larva.random(AbilityId.LARVATRAIN_DRONE, subtract_cost=True, subtract_supply=True) The keyword arguments are optional because many actions are move or attack commands, instead of train or build commands, thus making the bot slightly faster if only specific actions are checked if they have a cost associated. There are two more ways to do the same:: from sc2.ids.unit_typeid import UnitTypeId self.larva.random.train(UnitTypeId.DRONE) This converts the UnitTypeId to the AbilityId that is required to train the unit. Another way is to use the train function from the api:: self.train(UnitTypeId.DRONE, amount=1) This tries to figure out where to build the target unit from, and automatically subtracts the cost and supply after the train command was issued. If performance is important to you, you should try to give structures the train command directly from which you know they are idle and that you have enough resources to afford it. So a more performant way to train as many drones as possible is:: for loop_larva in self.larva: if self.can_afford(UnitTypeId.DRONE): loop_larva.train(UnitTypeId.DRONE) # Add break statement here if you only want to train one else: # Can't afford drones anymore break ``self.can_afford`` checks if you have enough resources and enough free supply to train the unit. ``self.do`` then automatically increases supply count and subtracts resource cost. Warning: You need to prevent issuing multiple commands to the same larva in the same frame (or iteration). The ``self.do`` function automatically adds the unit's tag to ``self.unit_tags_received_action``. This is a set with integers and it will be emptied every frame. So the final proper way to do it is:: for loop_larva in self.larva: if loop_larva.tag in self.unit_tags_received_action: continue if self.can_afford(UnitTypeId.DRONE): loop_larva.train(UnitTypeId.DRONE) # Add break statement here if you only want to train one else: # Can't afford drones anymore break Building a structure ^^^^^^^^^^^^^^^^^^^^^ Nearly the same procedure is when you want to build a structure. All that is needed is - Which building type should be built - Can you afford building it - Which worker should be used - Where should the building be placed The building type could be ``UnitTypeId.SPAWNINGPOOL``. To check if you can afford it you do ``if self.can_afford(UnitTypeId.SPAWNINGPOOL):``. Figuring out which worker to use is a bit more difficult. It could be a random worker (``my_worker = self.workers.random``) or a worker closest to the target building placement position (``my_worker = self.workers.closest_to(placement_position)``), but both of these have the issue that they could use a worker that is already busy (scouting, already on the way to build something, defending the base from worker rush). Usually worker that are mining or idle could be chosen to build something (``my_worker = self.workers.filter(lambda worker: worker.is_collecting or worker.is_idle).random``). There is an issue here that if the Units object is empty after filtering, ``.random`` will result in an assertion error. Lastly, figuring out where to place the spawning pool. This can be as easy as:: map_center = self.game_info.map_center placement_position = self.start_location.towards(map_center, distance=5) But then the question is, can you actually place it there? Is there creep, is it not blocked by a structure or enemy units? Building placement can be very difficult, if you don't want to place your buildings in your mineral line or want to leave enough space so that addons fit on the right of the structure (terran problems), or that you always leave 2x2 space between your structures so that your archons won't get stuck (protoss and terran problems). A function that can test which position is valid for a spawning pool is ``self.find_placement``, which finds a position near the given position. This function can be slow:: map_center = self.game_info.map_center position_towards_map_center = self.start_location.towards(map_center, distance=5) placement_position = await self.find_placement(UnitTypeId.SPAWNINGPOOL, near=position_towards_map_center, placement_step=1) # Can return None if no position was found if placement_position: One thing that was not mentioned yet is that you don't want to build more than 1 spawning pool. To prevent this, you can check that the number of pending and completed structures is zero:: if self.already_pending(UnitTypeId.SPAWNINGPOOL) + self.structures.filter(lambda structure: structure.type_id == UnitTypeId.SPAWNINGPOOL and structure.is_ready).amount == 0: # Build spawning pool So in total: To build a spawning pool in direction of the map center, it is recommended to use:: if self.can_afford(UnitTypeId.SPAWNINGPOOL) and self.already_pending(UnitTypeId.SPAWNINGPOOL) + self.structures.filter(lambda structure: structure.type_id == UnitTypeId.SPAWNINGPOOL and structure.is_ready).amount == 0: worker_candidates = self.workers.filter(lambda worker: (worker.is_collecting or worker.is_idle) and worker.tag not in self.unit_tags_received_action) # Worker_candidates can be empty if worker_candidates: map_center = self.game_info.map_center position_towards_map_center = self.start_location.towards(map_center, distance=5) placement_position = await self.find_placement(UnitTypeId.SPAWNINGPOOL, near=position_towards_map_center, placement_step=1) # Placement_position can be None if placement_position: build_worker = worker_candidates.closest_to(placement_position) build_worker.build(UnitTypeId.SPAWNINGPOOL, placement_position) The same can be achieved with the convenience function ``self.build`` which automatically picks a worker and internally uses ``self.find_placement``:: if self.can_afford(UnitTypeId.SPAWNINGPOOL) and self.already_pending(UnitTypeId.SPAWNINGPOOL) + self.structures.filter(lambda structure: structure.type_id == UnitTypeId.SPAWNINGPOOL and structure.is_ready).amount == 0: map_center = self.game_info.map_center position_towards_map_center = self.start_location.towards(map_center, distance=5) await self.build(UnitTypeId.SPAWNINGPOOL, near=position_towards_map_center, placement_step=1)