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.


Brax

Dude in his 30s starting his digital notepad