Welcome to part 5 of the StarCraft II tutorial series. Up to this point, we've written the code to build an army, and now we're ready to lead the battle charge!
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 need to add an attacking method 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() await self.build_offensive_force() await self.attack()
A basic attacking method could be something like:
async def attack(self): if self.units(STALKER).amount > 3: if len(self.known_enemy_units) > 0: for s in self.units(STALKER).idle: await self.do(s.attack(random.choice(self.known_enemy_units)))
So here, if we have more than 3 stalkers, we know where some enemy units are, and we have some stalkers doing nothing, let's attack one of the enemy units.
This is enough but attacking, like expanding, stretches your resources thinner, and this is another time when you want to properly strategize. For example, there's a difference between defending your own base/units, versus going to the enemy's base to fight. Let's change our attack method a bit:
async def attack(self): if self.units(STALKER).amount > 15: for s in self.units(STALKER).idle: await self.do(s.attack(self.find_target(self.state))) elif self.units(STALKER).amount > 3: if len(self.known_enemy_units) > 0: for s in self.units(STALKER).idle: await self.do(s.attack(random.choice(self.known_enemy_units)))
Now, if we have more than 15 units, we will find any target. Otherwise, if we have more than 3 units, and we know where some enemy units are, we will go get them. In order for us to know where enemies are, we have to be able to see them. That said, we also do know where the enemy start location is, so, if we have a large enough army ready, we know where to go to find a fight. We do not yet have a find_target
method, however. Let's make that now too.
def find_target(self, state): if len(self.known_enemy_units) > 0: return random.choice(self.known_enemy_units) elif len(self.known_enemy_structures) > 0: return random.choice(self.known_enemy_structures) else: return self.enemy_start_locations[0]
The way we do this is, if we know where any units are, we attack those first. If we don't know where any units are, but we know where their buildings are, we go for those. Finally, if we do not know where any enemy units or buildings are, we go to where we know their starting point was.
It should be noted that, in order to win with StarCraft, you have to just destroy all the enemy's buildings, even if they still have a massive army remaining, so you might prioritize enemy buildings, if enemy units aren't around them
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 import random 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() await self.attack() 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 and not self.units(CYBERNETICSCORE): if self.can_afford(CYBERNETICSCORE) and not self.already_pending(CYBERNETICSCORE): await self.build(CYBERNETICSCORE, near=pylon) elif len(self.units(GATEWAY)) < 3: 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)) def find_target(self, state): if len(self.known_enemy_units) > 0: return random.choice(self.known_enemy_units) elif len(self.known_enemy_structures) > 0: return random.choice(self.known_enemy_structures) else: return self.enemy_start_locations[0] async def attack(self): if self.units(STALKER).amount > 15: for s in self.units(STALKER).idle: await self.do(s.attack(self.find_target(self.state))) elif self.units(STALKER).amount > 3: if len(self.known_enemy_units) > 0: for s in self.units(STALKER).idle: await self.do(s.attack(random.choice(self.known_enemy_units))) run_game(maps.get("AbyssalReefLE"), [ Bot(Race.Protoss, SentdeBot()), Computer(Race.Terran, Difficulty.Easy) ], realtime=False)
This bot is good enough to defeat both the Easy
and Medium
ai consistently:
...but we get destroyed by the Hard AI. From what I can tell, the Hard AI is just plain better at massing a far superior army than we do. So, the next thing for us to focus on is how to best setup our army to defeat the hard AI.