In this part 3 of the StarCraft II AI in python series, we are going to be collecting another much-needed resource: Gas, as well as expanding to other resource areas.
Eventually, we saturate this one area. Remember that you only need 3 workers per mineral patch. Eventually, we need to expand out. Expanding out shouldn't be taken too lightly, however. When you expand, you're spreading your defenses thinner. That said, we're saturating our one area, so we should consider expanding. Luckily, this is another low-hanging fruit for us, since some expansion logic is already done for us. Let's add await self.expand()
to our on_step
method:
async def on_step(self, iteration): # what to do every step 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()
Now our expand method:
async def expand(self): if self.units(NEXUS).amount < 2 and self.can_afford(NEXUS): await self.expand_now()
In the above case, we're limiting our expansions to just 2 locations. You can find the code for expand_now
in sc2/bot_ai.py
.
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 class SentdeBot(sc2.BotAI): async def on_step(self, iteration): # what to do every step 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() 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() run_game(maps.get("AbyssalReefLE"), [ Bot(Race.Protoss, SentdeBot()), Computer(Race.Terran, Difficulty.Easy) ], realtime=False)
Now we're cooking! We have 2 command center areas and a fast-growing resource collection. Next, we need to focus on what's required to build our army, which is what we will be talking about in the next tutorial.
As we approach our goal of amassing an army and attacking our enemy, we find that we're going to need gas. Gas is harvested from the Vespene Geysers located around the map. For the Protoss
race, the building we need to harvest this gas is called an Assimilator
, which we need to build right on top of these geysers. We want to import this building to call it to be built, so let's first add that to our constants import:
from sc2.constants import NEXUS, PROBE, PYLON, ASSIMILATOR
Now let's add the following to our on_step
method: await self.build_assimilator()
, making that method:
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
Now we need to actually build that method. As you might imagine, finding the right geyser, and then building right on top of it might be a considerable challenge to code. Luckily for us, a lot of this work has been done for us. Via our API wrapper, we can find available geysers close to our Nexus, for example. We have a nexus per major resource area, so this is how we're going to find them. Next, we also need to select one of our workers who are close to this so we don't waste much time. That too is covered for us. Finally, we can build things by referencing the worker unit, and then just doing a .build(THING,WHERE)
. Let's see:
First we want to iterate through all of our nexuses, then all vespenes will be in a range of ~25 units:
async def build_assimilator(self): for nexus in self.units(NEXUS).ready: vaspenes = self.state.vespene_geyser.closer_than(25.0, nexus)
Great, we have found the vaspenes. Now, we want to build. As usual, if we can't afford them, we can't build!
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
Otherwise, we need to select a worker that is available:
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 we have a location, the funds, and a worker, let's build ... as long as there already isn't one!
if not self.units(ASSIMILATOR).closer_than(1.0, vaspene).exists: await self.do(worker.build(ASSIMILATOR, vaspene))
Full method 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))
Full code now::
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)
Running this, we can make sure that we are now building assimilators:
Awesome! Now let's build an army! ... in the next tutorial!