Python SC2 – Rule Based Bot
Alright, So I was very excited about this small project. I have been playing starcraft for over 15 years and was always quite pationate about the game and still watch it from time time.
Following the hype early 2019 concerning Alpha star :
I had in my head to try to dig a little bit into that. And finally decided to give it a try this year.
I am using the python-sc2 library to code my bots who will then use Starcraft API to communicate with the game and pass instructions. To get started :
- Download the game here, it’s free, install it
- Then do a pip install sc2 to get the python-sc2 library
- Finally get the maps here (password for the archive : iagreetotheeula)
Edit: This version of the pythonsc2 lib works but is outdated. You can build buildings, create units fight and do pretty much everything. But you cannot do upgrades or use abilities + some other incompatibilities with the current version of starcraft (October 2020). I will therefore do this post on the old version and then continue on the new version maintained by BurnySc2.
That being said, copy the maps that you downloaded to the maps folder in the starcraft installation folder:
And from there we are ready to go and can actually start to interact with the game, so open up your favorite IDE and get to it.
Let’s start nice and easy and just launch 2 easy bots : one that will do a “worker rush”, sending all your collectors to attack your ennemy directly and another one that will start collecting minerals
BOT Collecting Minerals :
import sc2 from sc2 import run_game, maps, Race, Difficulty from sc2.player import Bot, Computer class BraxBot(sc2.BotAI): async def on_step(self, iteration): await self.distribute_workers() run_game(maps.get("Abyssal Reef LE"), [ Bot(Race.Zerg, BraxBot()), Computer(Race.Protoss, Difficulty.Medium) ], realtime=True)
We proceed to import the right tools and then create our Bot (BraxBot). The bot will inherit from the sc2.BotAI
class, and therefore have access to all the built-in functions and tools. You can check them out by looking here : sc2/bot_ai.py
.
We then create an asynchronous method “on_step”. The async
indicates that it is asynchronous, that it will not wait for each method to complete to be doing other stuff, everything is running in parallel. Our method will simply call distribute_workers() on our bot which will just send our drones on the minerals patchs. Simple as that.
We then use the run_game to start the game itself, we pass the map that we want to use, in that case Abyssal Reef LE. Our Bot will be a Zerg and we will fight a Computer difficulty Medium. when you run that piece of code, a window will appear and your bot will be playing, the workers will directly go and pick minerals starting to build up your economy :
BOT Worker rush :
import sc2 from sc2 import run_game, maps, Race, Difficulty from sc2.player import Bot, Computer class BraxBot(sc2.BotAI): async def on_step(self, iteration): if iteration == 0: for worker in self.workers: await self.do(worker.attack(self.enemy_start_locations[0])) run_game(maps.get("Abyssal Reef LE"), [ Bot(Race.Zerg, BraxBot()), Computer(Race.Protoss, Difficulty.Medium) ], realtime=True)
This time, instead of sending all the workers to the mineral patches we will iterate through the workers and then send them with an attack order towards the enemy start location.
The workers are attacking :
And successfully went to the enemy base to attack !
Advanced BOT 1 :
Now we want a basic bot that will win versus an easy or medium AI. To win a game you have to either kill all the buildings of your opponent or make him concede. Our bot needs to be able tobuild a base, expand, build fighting units and then attack.
import sc2 from sc2 import run_game, maps, Race, Difficulty from sc2.player import Bot, Computer from sc2.constants import * import random #Our bot class BraxBot(sc2.BotAI): async def on_step(self, iteration): #List of available methods that we have created for our bot : #Our bot distributes the workers on the mineral patchs : await self.distribute_workers() #Out bot builds workers : await self.build_probes() #Out bot builds pylons : await self.build_pylons() #Out bot builds Assimilators : await self.build_assimilators() #Out bot expands await self.expand() #Out bot builds production buildings await self.build_production_build() #Out bot builds units await self.build_units() #Out bot fights await self.fight() #Build more probes async def build_probes(self): #For every nexus that is created and that does not train a probe at the moment for nexus in self.units(NEXUS).ready.idle: #If we have enough minerals for a probe and we have less than 70 probes if self.can_afford(PROBE) and self.units(PROBE).amount < 70: #Build a probe await self.do(nexus.train(PROBE)) #Build more pylons async def build_pylons(self): #If supply remaining is less than 5 and there is no pylon already being built, then we build a pylon near the nexus but #We build it towards the center of the map so that the building are not getting in the way of our probes gathering minerals if self.supply_left < 5 and not self.already_pending(PYLON): nexuses = self.units(NEXUS).ready if nexuses.exists: #if we have enough minerals to build a pylon if self.can_afford(PYLON): await self.build(PYLON, near=self.units(NEXUS).first.position.towards(self.game_info.map_center, 5)) #Build gas assimilators async def build_assimilators(self): #We need gas to build stalkers so we first need to build an assimilator. Our distribute workers method will then assign workers to the gas #For every nexus for nexus in self.units(NEXUS).ready: #we get the list of vespene geyser closer than 15 unit to our nexus vespenes = self.state.vespene_geyser.closer_than(15.0, nexus) #for all vespene geysers that we have identified for vespene in vespenes: #if we can't affor an assimilator if not self.can_afford(ASSIMILATOR): #We break break #else we select a probe that is available worker = self.select_build_worker(vespene.position) if worker is None: break #and finally if we can afford it, a location and the worker and no assimilator is being build if not self.units(ASSIMILATOR).closer_than(1.0, vespene).exists: #we build the assimilator ! await self.do(worker.build(ASSIMILATOR, vespene)) #We build an expansation async def expand(self): #if we have less than 2 nexuses and we can afford it we expend, prebuilt method if self.units(NEXUS).amount < 3 and self.can_afford(NEXUS): await self.expand_now() #Build production buildings async def build_production_build(self): #if we have a pylon (production buildings needs to be built near a pylon) if self.units(PYLON).ready.exists: #we select a random pylon that is already built pylon = self.units(PYLON).ready.random #If we have already a gateway but no cyberneticscore if self.units(GATEWAY).ready.exists and not self.units(CYBERNETICSCORE): #if we can afford one and we are currently not already building a cyberneticscore if self.can_afford(CYBERNETICSCORE) and not self.already_pending(CYBERNETICSCORE): #then we build a cybernetics core ! await self.build(CYBERNETICSCORE, near=pylon) #Else, if we have less than 7 gates elif len(self.units(GATEWAY)) < 3: #and if we can afford it if self.can_afford(GATEWAY): #We build a gateway await self.build(GATEWAY, near=pylon) elif len(self.units(GATEWAY)) > 3 and not self.already_pending(GATEWAY): #and if we can afford it if self.can_afford(GATEWAY): #We build a gateway await self.build(GATEWAY, near=pylon) #Build Units - Stalkers async def build_units(self): #for each gw that is not producing units for gw in self.units(GATEWAY).ready.idle: #if we can afford it and if we have enough supply left if self.can_afford(STALKER) and self.supply_left > 2: #we produce a stalker await self.do(gw.train(STALKER)) #find a target to attack def target(self, state): #if we identified an ennemy unit if len(self.known_enemy_units) > 0: #we return one of those units randomly return random.choice(self.known_enemy_units) else: #Else we return the starting location of the enemy return self.enemy_start_locations[0] #Stalker behavior async def fight(self): #If we have more than 15 stalkers if self.units(STALKER).amount > 15: #Every stalker that is not doing anything for s in self.units(STALKER).idle: #Will attack either a known unit or go attack the base of our enemy await self.do(s.attack(self.target(self.state))) #But if we have less than 15 stalkers elif self.units(STALKER).amount > 0 and self.units(STALKER).amount < 15: #if we see enemy units if len(self.known_enemy_units) > 0: #for every idle stalker for s in self.units(STALKER).idle: #attack a random unit that we know await self.do(s.attack(random.choice(self.known_enemy_units))) #Launch the game to fight an easy Zerg bot run_game(maps.get("Abyssal Reef LE"), [ Bot(Race.Protoss, BraxBot()), Computer(Race.Zerg, Difficulty.Easy) ], realtime=False)
So we added quite a few commands here. We are now enabling our bot to :
- #Our bot distributes the workers on the mineral patchs : Already explained
- #Our bot builds workers : In starcraft you need to create workers in order to mine ressources that you will then use to build up your base and train an army. You don’t want to build too much workers, but if you have less than 70 basically you want to build more to saturate all your bases. So let’s create workers until we have 70 !
- #Our bot builds Pylons : We need Pylons to increase supply (your maximum number of units) and to provide energy coverage to build your other protoss structures. So every time we have only 5 supply left, we will build a Pylon. And we don’t want to build it between our nexus and our minerals. If you do so your probes will have more troubles to mine the precious minerals and you will hinder your economy. So we are building towards the center of the map.
- #Our bot builds Assimilators : In order to produce stalkers we need assimilators to extract vespene gas, so we search for geysers, we select a worker and we build an assimilator. Once done our dispatch worker method will send the workers there
- #Our bot expands : We want to build additional bases when we want. Currently our bot does not really respect precise timings or supply limits to do so. But we can do that later. Sofor the moment, eventually, if we float more than 400 minerals our bot will build a nexus
- #Our bot builds production buildings : We want to build gateways to produce units and a cybernetics core to enable the production of stalkers. So basically we kind of want to produce 3 gateways, a cybenerics core, than expand and continue gateway production with units. That’s the rationale behind my conditions
- #Our bot builds units : we produce Stalkers in every gateway if we can afford them and if we have enough supply. We mass stalkers
- #Our bot fights : We have a fight method and a target method. If we have less than 15 stalkers we are looking to defend. so we will attack only the units that we see on the map without attacking the base of the opponent. If we have more than 15 stalkers we will in priority attack the known units and if there are no units that we see we will attack the opponent start location and fight him there. All of this to “mass up” a small army and then attack the opponent with it instead of sending the units 1 by 1
Our bot wins vs easy and sometimes even against medium AI !
And here it is :
There are tons of things that could be enhanced and integrated into our rule bot. And that is what we will do in the next part. We will introduce upgrades, abilities and generally try to make our bot more efficient and more precise. To do so, we will have to jump to a new release that will provide us the tools to do that.