Welcome to part 4 of the StarCraft II AI in python series, where we will be amassing our army!
Up to this point, we've really just been fairly methodical about things. We need workers, we need to distribute them to be 3 per mineral patch. We need gas, and we need to expand to at least some degree to establish a decent working amount of supplies to wage battle. But now, we have to begin considering strategy. What units will we produce? How many? Units exhaust supplies in terms of minerals/gas, as well as our actual "supply" as in population of units and...again arguably the most important element of all: time.
In the interest of simplicity, I think the best idea is to start off with a single fighting unit that you want to build. Over time, you can make your army a bit more dynamic, and you probably will need to, but, for now, let's just make a single unit. I am no expert, but, after poking around, I have found two units to be decent "general" massing-worthy fighting units: The Stalker and the Void Ray. The stalker is a ground unit, but it can engage ground and air forces. The Void Ray is an air unit that can engage both ground and air. The Void Ray is slightly more complex to build, and takes longer. The easier unit for now will be the Stalker, so we'll go with that.
From here, the game gets exceedingly complex, as does the strategy, so get ready! Revisiting the Protoss units page, I can see the path to the Stalker is: Nexus > Gateway > Cybernetics Core > Stalker. Okay, easy enough. Note that the Cybernetics Core is more about research. You will still produce your fighting units from the Gateway. You only need one Cybernetics Core (and hope it doesn't get destroyed), but you will probably want multiple gateways to create units much faster.
Code up to this point:
import sc2 from sc2 import run_game, maps, Race, Difficulty from sc2.player import Bot, Computer from sc2.constants import NEXUS, PROBE, PYLON, ASSIMILATOR class SentdeBot(sc2.BotAI): async def on_step(self, iteration): await self.distribute_workers() # in sc2/bot_ai.py await self.build_workers() # workers bc obviously await self.build_pylons() # pylons are protoss supply buildings await self.expand() # expand to a new resource area. await self.build_assimilator() # getting gas async def build_workers(self): # nexus = command center for nexus in self.units(NEXUS).ready.noqueue: # we want at least 20 workers, otherwise let's allocate 70% of our supply to workers. # later we should use some sort of regression algo maybe for this? if self.can_afford(PROBE): await self.do(nexus.train(PROBE)) async def build_pylons(self): if self.supply_left < 5 and not self.already_pending(PYLON): nexuses = self.units(NEXUS).ready if nexuses.exists: if self.can_afford(PYLON): await self.build(PYLON, near=nexuses.first) async def expand(self): if self.units(NEXUS).amount < 2 and self.can_afford(NEXUS): await self.expand_now() async def build_assimilator(self): for nexus in self.units(NEXUS).ready: vaspenes = self.state.vespene_geyser.closer_than(25.0, nexus) for vaspene in vaspenes: if not self.can_afford(ASSIMILATOR): break worker = self.select_build_worker(vaspene.position) if worker is None: break if not self.units(ASSIMILATOR).closer_than(1.0, vaspene).exists: await self.do(worker.build(ASSIMILATOR, vaspene)) run_game(maps.get("AbyssalReefLE"), [ Bot(Race.Protoss, SentdeBot()), Computer(Race.Terran, Difficulty.Easy) ], realtime=False)
Now, to begin, we need to build the buildings we need. We need to build any new buildings within the psionic matrix
, which is created by our pylons, so we just need to build our buildings near pylons. So we'll just pick a random pylon. If we have a gateway
already, but not a cyberntetics core
, we want to build one. If we don't have a gateway
, let's build one.
To begin, we need to add the code to our on_step
method:
class SentdeBot(sc2.BotAI): async def on_step(self, iteration): await self.distribute_workers() await self.build_workers() await self.build_pylons() await self.build_assimilators() await self.expand() await self.offensive_force_buildings()
Now we need to build the offensive_force_buildings
method. To start, we look for a pylon to build near:
async def offensive_force_buildings(self): if self.units(PYLON).ready.exists: pylon = self.units(PYLON).ready.random
Next, let's build a cybernetics core if we have a gateway already:
async def offensive_force_buildings(self): if self.units(PYLON).ready.exists: pylon = self.units(PYLON).ready.random if self.units(GATEWAY).ready.exists: if not self.units(CYBERNETICSCORE): if self.can_afford(CYBERNETICSCORE) and not self.already_pending(CYBERNETICSCORE): await self.build(CYBERNETICSCORE, near=pylon)
Done. Now, if we need to otherwise build a gateway:
async def offensive_force_buildings(self): if self.units(PYLON).ready.exists: pylon = self.units(PYLON).ready.random if self.units(GATEWAY).ready.exists: if not self.units(CYBERNETICSCORE): if self.can_afford(CYBERNETICSCORE) and not self.already_pending(CYBERNETICSCORE): await self.build(CYBERNETICSCORE, near=pylon) else: if self.can_afford(GATEWAY) and not self.already_pending(GATEWAY): await self.build(GATEWAY, near=pylon)
Next up, we want to build our army. Let's add a new method to on_step
:
class SentdeBot(sc2.BotAI): async def on_step(self, iteration): await self.distribute_workers() await self.build_workers() await self.build_pylons() await self.build_assimilators() await self.expand() await self.offensive_force_buildings() await self.build_offensive_force()
Now, if we have supply and can afford it, let's build units for our army:
async def build_offensive_force(self): for gw in self.units(GATEWAY).ready.noqueue: if self.can_afford(STALKER) and self.supply_left > 0: await self.do(gw.train(STALKER))
Full code up to this point:
import sc2 from sc2 import run_game, maps, Race, Difficulty from sc2.player import Bot, Computer from sc2.constants import NEXUS, PROBE, PYLON, ASSIMILATOR, GATEWAY, \ CYBERNETICSCORE, STALKER class SentdeBot(sc2.BotAI): async def on_step(self, iteration): await self.distribute_workers() await self.build_workers() await self.build_pylons() await self.build_assimilators() await self.expand() await self.offensive_force_buildings() await self.build_offensive_force() async def build_workers(self): for nexus in self.units(NEXUS).ready.noqueue: if self.can_afford(PROBE): await self.do(nexus.train(PROBE)) async def build_pylons(self): if self.supply_left < 5 and not self.already_pending(PYLON): nexuses = self.units(NEXUS).ready if nexuses.exists: if self.can_afford(PYLON): await self.build(PYLON, near=nexuses.first) async def build_assimilators(self): for nexus in self.units(NEXUS).ready: vaspenes = self.state.vespene_geyser.closer_than(15.0, nexus) for vaspene in vaspenes: if not self.can_afford(ASSIMILATOR): break worker = self.select_build_worker(vaspene.position) if worker is None: break if not self.units(ASSIMILATOR).closer_than(1.0, vaspene).exists: await self.do(worker.build(ASSIMILATOR, vaspene)) async def expand(self): if self.units(NEXUS).amount < 3 and self.can_afford(NEXUS): await self.expand_now() async def offensive_force_buildings(self): if self.units(PYLON).ready.exists: pylon = self.units(PYLON).ready.random if self.units(GATEWAY).ready.exists: if not self.units(CYBERNETICSCORE): if self.can_afford(CYBERNETICSCORE) and not self.already_pending(CYBERNETICSCORE): await self.build(CYBERNETICSCORE, near=pylon) else: if self.can_afford(GATEWAY) and not self.already_pending(GATEWAY): await self.build(GATEWAY, near=pylon) async def build_offensive_force(self): for gw in self.units(GATEWAY).ready.noqueue: if self.can_afford(STALKER) and self.supply_left > 0: await self.do(gw.train(STALKER)) run_game(maps.get("AbyssalReefLE"), [ Bot(Race.Protoss, SentdeBot()), Computer(Race.Terran, Difficulty.Easy) ], realtime=False)
Now we've got an army forming:
At this point, we're ready to begin commanding our army, which is what we'll be doing in the next tutorial.