Python Sc2 – Advanced bot
Now, I had started to build my bot with the regular dentosal release, but it appears that this release is not working anymore with the latest starcraft versions. So I had to delete it and start over with the BurnySc2 that seems to be still maintained.
I was trying to figure out how to research upgrades link blink and weapon + armor upgrages for my stalkers. And found out that it was not possible anymore with dentosal.
A quick :
pip uninstall sc2pip install --upgrade burnysc2
And we are ready to go. The logic to code the bot remains the same but some methods are not available anymore and some are new.
I worked a few hours on my new bot and brought a lot of additional functionalities and enhancements in comparison to my older bot..
- I create now a few adepts early on to defend against gling rushes
- I manage the chrono boost of my nexus (chronobost warpgate research -> blink -> upgrades -> nexus) in order of priority
- I start to build some pylons further away from my base to reinforce easier my stalkers when attacking
- I research and morph my gateways into warpgates
- I warp the units that I create from warpgates
- I build a forge and upgrade my units
- I build a twilight council and upgrade blink
- I build a robotics bay and build obs to scout main and 3 possible expend positions of my ennemy
- I added rules for fighting. My stalkers will now target in priority lower health ennemy units and Drones/Scvs/Probes
- They will move away from enemies when their attack is on cooldown
- Avoid blocking each other
- Blink back towards my base when shield is below 4, if blink is on cooldown, go back by foot to avoid being hit more
Pfiew that’s a lot :
#/*******************************************************#Nom ......... : 04 Starcraft_Enhanced_Stalker_Bot.py#Context ......: Advanced Protoss Stalker Micro Bot#Role .........: Mid game push with upgrades and blink and superior Micro Skills#Auteur ...... : JDO#Version ..... : V1#Date ........ : 07.10.2020#Language : Python#Version : 3.7.8#Currently playing on AiArena#********************************************************/#Add faster robo#To do : Add immortals -> colosses#add observers to scout ALL the expend locations of the map#add thrid nexus#add sentries with guardian shield ?#Increae distance for blink distance#bring down limit of shield for retreate so stalkers come back faster to fight#Micro adept and micro obs#Importsimport sys, ossys.path.append(os.path.join(os.path.dirname(__file__), "../.."))import sc2from sc2 import Race, Difficultyfrom sc2.constants import *from sc2.position import Point2, Point3from sc2.unit import Unitfrom sc2.player import Bot, Computerfrom sc2.player import Humanfrom sc2.ids.unit_typeid import UnitTypeIdfrom sc2.ids.ability_id import AbilityIdfrom sc2.units import Unitsimport random#Our botclass BraxBot(sc2.BotAI):#Init classdef __init__(self):sc2.BotAI.__init__(self)self.proxy_built = False#on step functionasync def on_step(self, iteration):#Async actionsawait self.distribute_workers()await self.build_probes()await self.build_pylons()await self.build_assimilators()await self.expand()await self.build_production_build()await self.build_units()await self.fight()await self.chrono_boost()await self.morph_warpgate()await self.warpgate_research()await self.warp_new_units()await self.forge_research()await self.twilight_research()await self.micro()await self.build_obs()await self.send_obs()#Build more probesasync def build_probes(self):#For every nexus that is ready and that does not train a probe at the moment train a probe#If we have less than 22 probes / Nexusfor nexus in self.townhalls.ready:if self.workers.amount < self.townhalls.amount * 22 and nexus.is_idle:if self.can_afford(UnitTypeId.PROBE):nexus.train(UnitTypeId.PROBE)#Build more pylonsasync def build_pylons(self):#If supply remaining is less than 6 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 mineralsif self.supply_left < 6 and self.already_pending(UnitTypeId.PYLON) == 0 and self.can_afford(UnitTypeId.PYLON) and self.supply_used < 100:for nexus in self.townhalls.ready:#if we have enough minerals to build a pylonif self.can_afford(UnitTypeId.PYLON) and self.already_pending(UnitTypeId.PYLON) == 0:await self.build(UnitTypeId.PYLON, near=nexus.position.towards(self.game_info.map_center, 7))#If we have more than 50 supply we can create 2 pylons at once and we build them toward the center of the mapelif self.supply_used > 50 and self.supply_left < 6:for nexus in self.townhalls.ready:#if we have enough minerals to build a pylonif self.can_afford(UnitTypeId.PYLON) and self.already_pending(UnitTypeId.PYLON) < 2:await self.build(UnitTypeId.PYLON, near=nexus.position.towards(self.game_info.map_center, 30))#Build gas assimilatorsasync 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 gasif self.supply_used > 15:for nexus in self.townhalls.ready:vgs = self.vespene_geyser.closer_than(15, nexus)for vg in vgs:if not self.can_afford(UnitTypeId.ASSIMILATOR):breakworker = self.select_build_worker(vg.position)if worker is None:breakif not self.gas_buildings or not self.gas_buildings.closer_than(1, vg):worker.build(UnitTypeId.ASSIMILATOR, vg)worker.stop(queue=True)#We build an expansation when we can afford it and we build another one when we are above 50 supply and so onasync def expand(self):if self.townhalls.ready.amount + self.already_pending(UnitTypeId.NEXUS) < 2:if self.can_afford(UnitTypeId.NEXUS):await self.expand_now()elif self.townhalls.ready.amount + self.already_pending(UnitTypeId.NEXUS) < 3 and self.supply_used > 50:if self.can_afford(UnitTypeId.NEXUS):await self.expand_now()elif self.townhalls.ready.amount + self.already_pending(UnitTypeId.NEXUS) < 4 and self.supply_used > 80:if self.can_afford(UnitTypeId.NEXUS):await self.expand_now()#Build production buildingsasync def build_production_build(self):if self.structures(UnitTypeId.PYLON).ready:pylon = self.structures(UnitTypeId.PYLON).ready.random#If we have a gate and no CC we build a CCif self.already_pending(UnitTypeId.GATEWAY) == 1 and not self.structures(UnitTypeId.CYBERNETICSCORE) :if self.structures(UnitTypeId.GATEWAY).ready and self.can_afford(UnitTypeId.CYBERNETICSCORE) and self.already_pending(UnitTypeId.CYBERNETICSCORE) == 0:await self.build(UnitTypeId.CYBERNETICSCORE, near=pylon)else:#We build 2 gates when we can afford itif (self.can_afford(UnitTypeId.GATEWAY)and self.structures(UnitTypeId.WARPGATE).amount + self.structures(UnitTypeId.GATEWAY).amount < 3):await self.build(UnitTypeId.GATEWAY, near=pylon)#We build a twilight councul when we have at least 2 gates and an expendelif (self.can_afford(UnitTypeId.TWILIGHTCOUNCIL)and self.structures(UnitTypeId.WARPGATE).amount + self.structures(UnitTypeId.GATEWAY).amount > 2and not self.structures(UnitTypeId.TWILIGHTCOUNCIL)and self.already_pending(UnitTypeId.TWILIGHTCOUNCIL) == 0and self.structures(UnitTypeId.NEXUS).amount > 1):await self.build(UnitTypeId.TWILIGHTCOUNCIL, near=pylon)#We build a forge when we have at least 2 gates and an expendelif (self.can_afford(UnitTypeId.FORGE)and self.structures(UnitTypeId.WARPGATE).amount + self.structures(UnitTypeId.GATEWAY).amount > 2and not self.structures(UnitTypeId.FORGE)and self.structures(UnitTypeId.CYBERNETICSCORE).ready.amount == 1and self.already_pending(UnitTypeId.FORGE) == 0and self.structures(UnitTypeId.NEXUS).amount > 1):await self.build(UnitTypeId.FORGE, near=pylon)#We build to 7 gates when we have an expendelif (self.can_afford(UnitTypeId.GATEWAY)and self.structures(UnitTypeId.WARPGATE).amount + self.structures(UnitTypeId.GATEWAY).amount < 7and self.structures(UnitTypeId.NEXUS).amount > 1):await self.build(UnitTypeId.GATEWAY, near=pylon)#We build a robotics facility when we have an expendelif (self.can_afford(UnitTypeId.ROBOTICSFACILITY)and self.structures(UnitTypeId.ROBOTICSFACILITY).amount < 1and self.structures(UnitTypeId.NEXUS).amount > 1):await self.build(UnitTypeId.ROBOTICSFACILITY, near=pylon)#Build Units - Adeptsasync def build_units(self):#for each gw that is not producing unitsfor gw in self.structures(UnitTypeId.GATEWAY).ready:#if we can afford it and if we have enough supply left and the CC is not yet done, we produce two adeptsif self.can_afford(UnitTypeId.ADEPT) and not self.structures(UnitTypeId.CYBERNETICSCORE).ready and gw.is_idle and self.units(UnitTypeId.ADEPT).amount < 2:gw.train(UnitTypeId.ADEPT)#Build Units - Observersasync def build_obs(self):#for each robotics facility that is not producing unitsfor rf in self.structures(UnitTypeId.ROBOTICSFACILITY).ready:#if we can afford it and if we have enough supply left we produce 2 observersif self.can_afford(UnitTypeId.OBSERVER ) and self.units(UnitTypeId.OBSERVER).amount < 2 and rf.is_idle:#we produce a stalkerrf.train(UnitTypeId.OBSERVER)#Method to warp unitsasync def warp_new_units(self):for warpgate in self.structures(UnitTypeId.WARPGATE).ready:#We take the abilities of the warpgate to retrieves the warping stalker abilityabilities = await self.get_available_abilities(warpgate)#if it is not on cooldownif AbilityId.WARPGATETRAIN_STALKER in abilities:#we sort the pylons by their distance to our warp gateself.ordered_pylons = sorted(self.structures(UnitTypeId.PYLON).ready, key=lambda pylon: pylon.distance_to(warpgate))#we pick the pylon the further away from the warp gate, because why notpos = self.ordered_pylons[-1].position.random_on_distance(4)#we select the placement of the stalker we want to warp as the position of the pylon selectedplacement = await self.find_placement(AbilityId.WARPGATETRAIN_STALKER, pos, placement_step=1)#if no placement available we return errorif placement is None:print("can't place")return#else we warp our stalker !warpgate.warp_in(UnitTypeId.STALKER, placement)#Method for Chrono boostasync def chrono_boost(self):#We get the list of our buildings that we want to chronoboostif self.structures(UnitTypeId.CYBERNETICSCORE).ready:ccore = self.structures(UnitTypeId.CYBERNETICSCORE).ready.firstif self.structures(UnitTypeId.FORGE).ready:forge = self.structures(UnitTypeId.FORGE).ready.firstif self.structures(UnitTypeId.TWILIGHTCOUNCIL).ready:twilight = self.structures(UnitTypeId.TWILIGHTCOUNCIL).ready.first#CC is on the top of our priority list if researches the upgrade for warp gateif self.structures(UnitTypeId.CYBERNETICSCORE).ready and not ccore.is_idle and not ccore.has_buff(BuffId.CHRONOBOOSTENERGYCOST):for nexus in self.townhalls.ready:if nexus.energy >= 50:nexus(AbilityId.EFFECT_CHRONOBOOSTENERGYCOST, ccore)#then blink in the twilight councilelif self.structures(UnitTypeId.TWILIGHTCOUNCIL).ready and not twilight.is_idle and not twilight.has_buff(BuffId.CHRONOBOOSTENERGYCOST):for nexus in self.townhalls.ready:if nexus.energy >= 50:nexus(AbilityId.EFFECT_CHRONOBOOSTENERGYCOST, twilight)#then the forge for the upgradeselif self.structures(UnitTypeId.FORGE).ready and not forge.is_idle and not forge.has_buff(BuffId.CHRONOBOOSTENERGYCOST):for nexus in self.townhalls.ready:if nexus.energy >= 50:nexus(AbilityId.EFFECT_CHRONOBOOSTENERGYCOST, forge)#finally the nexus if we are under 70 probeselif self.workers.amount < 70:for nexus in self.townhalls.ready:if not nexus.has_buff(BuffId.CHRONOBOOSTENERGYCOST) and not nexus.is_idle and self.already_pending(UnitTypeId.CYBERNETICSCORE) < 1 and self.workers.amount < 44:if nexus.energy >= 60:nexus(AbilityId.EFFECT_CHRONOBOOSTENERGYCOST, nexus)#and then the warpgateselse:for nexus in self.townhalls.ready:if nexus.energy >= 50:for warpgate in self.structures(UnitTypeId.WARPGATE).ready:if not warpgate.has_buff(BuffId.CHRONOBOOSTENERGYCOST):if nexus.energy >= 50:nexus(AbilityId.EFFECT_CHRONOBOOSTENERGYCOST, nexus)#method to research warpgate, if CC ready, and can afford and not yet researchedasync def warpgate_research(self):if (self.structures(UnitTypeId.CYBERNETICSCORE).readyand self.can_afford(AbilityId.RESEARCH_WARPGATE)and self.already_pending_upgrade(UpgradeId.WARPGATERESEARCH) == 0):#We research warp gate !ccore = self.structures(UnitTypeId.CYBERNETICSCORE).ready.firstccore.research(UpgradeId.WARPGATERESEARCH)#method to research blink, if TC is ready, if we can affort blink and not yet researchedasync def twilight_research(self):if (self.structures(UnitTypeId.TWILIGHTCOUNCIL).readyand self.can_afford(AbilityId.RESEARCH_BLINK)and self.already_pending_upgrade(UpgradeId.BLINKTECH) == 0):#We research blink !twilight = self.structures(UnitTypeId.TWILIGHTCOUNCIL).ready.firsttwilight.research(UpgradeId.BLINKTECH)#method to research the upgrades at the forgeasync def forge_research(self):if self.structures(UnitTypeId.FORGE).ready:#we get our forge and if we can afford and not already upgrading we cycle through the upgrades until lvl 2forge = self.structures(UnitTypeId.FORGE).ready.firstif (self.can_afford(AbilityId.FORGERESEARCH_PROTOSSGROUNDWEAPONSLEVEL1)and self.already_pending_upgrade(UpgradeId.PROTOSSGROUNDWEAPONSLEVEL1) == 0and forge.is_idle):forge.research(UpgradeId.PROTOSSGROUNDWEAPONSLEVEL1)elif (self.can_afford(AbilityId.FORGERESEARCH_PROTOSSGROUNDARMORLEVEL1)and self.already_pending_upgrade(UpgradeId.PROTOSSGROUNDARMORSLEVEL1) == 0and forge.is_idle):forge.research(UpgradeId.PROTOSSGROUNDARMORSLEVEL1)elif (self.can_afford(AbilityId.FORGERESEARCH_PROTOSSGROUNDWEAPONSLEVEL2)and self.already_pending_upgrade(UpgradeId.PROTOSSGROUNDWEAPONSLEVEL2) == 0and forge.is_idle):forge.research(UpgradeId.PROTOSSGROUNDWEAPONSLEVEL2)elif (self.can_afford(AbilityId.FORGERESEARCH_PROTOSSGROUNDARMORLEVEL2)and self.already_pending_upgrade(UpgradeId.PROTOSSGROUNDARMORSLEVEL2) == 0and forge.is_idle):forge.research(UpgradeId.PROTOSSGROUNDARMORSLEVEL2)#Stalker behaviorasync def fight(self):enemies = self.enemy_units.filter(lambda unit: unit.type_id not in {UnitTypeId.LARVA, UnitTypeId.EGG})enemy_fighters = enemies.filter(lambda u: u.can_attack) + self.enemy_structures({UnitTypeId.BUNKER, UnitTypeId.SPINECRAWLER, UnitTypeId.PHOTONCANNON})if self.units(UnitTypeId.STALKER).amount > 15:for stalker in self.units(UnitTypeId.STALKER).ready.idle:if enemy_fighters:# select enemies in rangein_range_enemies = enemy_fighters.in_attack_range_of(stalker)if in_range_enemies:# prioritize workersworkers = in_range_enemies({UnitTypeId.DRONE, UnitTypeId.SCV, UnitTypeId.PROBE})if workers:in_range_enemies = workers# special micro for ranged unitsif stalker.ground_range > 1:# attack if weapon not on cooldownif stalker.weapon_cooldown == 0:# attack enemy with lowest hp of the ones in rangelowest_hp = min(in_range_enemies, key=lambda e: (e.health + e.shield, e.tag))self.do(stalker.attack(lowest_hp))else:# micro away from closest unit# move further away if too many enemies are nearfriends_in_range = self.units(UnitTypeId.STALKER).in_attack_range_of(stalker)closest_enemy = in_range_enemies.closest_to(stalker)distance = stalker.ground_range + stalker.radius + closest_enemy.radiusif (len(friends_in_range) <= len(in_range_enemies)and closest_enemy.ground_range <= stalker.ground_range):distance += 1else:# if more than 5 units friends are close, use distance one shorter than range# to let other friendly units get close enough as well and not block each otherif len(self.units(UnitTypeId.STALKER).closer_than(7, stalker.position)) >= 5:distance -= -1self.do(stalker.move(closest_enemy.position.towards(stalker, distance)))else:# target fire with melee unitslowest_hp = min(in_range_enemies, key=lambda e: (e.health + e.shield, e.tag))self.do(stalker.attack(lowest_hp))else:# no unit in range, go to closestself.do(stalker.move(enemy_fighters.closest_to(stalker)))# no dangerous enemy at all, attack closest anythingelse:stalker.attack(self.enemy_start_locations[0])elif self.units(UnitTypeId.STALKER).amount > 0 and self.units(UnitTypeId.STALKER).amount < 15:for stalker in self.units(UnitTypeId.STALKER).ready.idle:if enemy_fighters:# select enemies in rangein_range_enemies = enemy_fighters.in_attack_range_of(stalker)if in_range_enemies:# prioritize workersworkers = in_range_enemies({UnitTypeId.DRONE, UnitTypeId.SCV, UnitTypeId.PROBE})if workers:in_range_enemies = workers# special micro for ranged unitsif stalker.ground_range > 1:# attack if weapon not on cooldownif stalker.weapon_cooldown == 0:# attack enemy with lowest hp of the ones in rangelowest_hp = min(in_range_enemies, key=lambda e: (e.health + e.shield, e.tag))self.do(stalker.attack(lowest_hp))else:# micro away from closest unit# move further away if too many enemies are nearfriends_in_range = self.units(UnitTypeId.STALKER).in_attack_range_of(unit)closest_enemy = in_range_enemies.closest_to(stalker)distance = stalker.ground_range + stalker.radius + closest_enemy.radiusif (len(friends_in_range) <= len(in_range_enemies)and closest_enemy.ground_range <= stalker.ground_range):distance += 1else:# if more than 5 units friends are close, use distance one shorter than range# to let other friendly units get close enough as well and not block each otherif len(self.units(UnitTypeId.STALKER).closer_than(7, stalker.position)) >= 5:distance -= -1self.do(stalker.move(closest_enemy.position.towards(stalker, distance)))else:# target fire with melee unitslowest_hp = min(in_range_enemies, key=lambda e: (e.health + e.shield, e.tag))self.do(stalker.attack(lowest_hp))else:# no unit in range, go to closestself.do(stalker.move(enemy_fighters.closest_to(stalker)))# no dangerous enemy at all, attack closest anythingelse:#our defense mechanic with our adeptsfor adept in self.units(UnitTypeId.ADEPT).ready.idle:if enemy_fighters:self.do(adept.attack(random.choice(enemy_fighters)))#Method to morph our gateways into warpgates, if gateway is ready and idle, and we have done the upgrade then morphasync def morph_warpgate(self):for gateway in self.structures(UnitTypeId.GATEWAY).ready.idle:if self.already_pending_upgrade(UpgradeId.WARPGATERESEARCH) == 1:gateway(AbilityId.MORPH_WARPGATE)#Method to send our obs scoutingasync def send_obs(self):#We retrieve the list of possible extensionsself.ordered_expansions = Noneself.ordered_expansions = sorted(self.expansion_locations.keys(), key=lambda expansion: expansion.distance_to(self.enemy_start_locations[0]))#We send the obs in each, initially I limited at 4 but then in some games I could not find a hidden expo..for obs in self.units(UnitTypeId.OBSERVER).ready:if obs.is_idle:self.do(obs.move(random.choice(self.ordered_expansions[0:])))#Our method to micro and to blinkasync def micro(self):home_location = self.start_locationenemies: Units = self.enemy_units | self.enemy_structuresenemies2 = self.enemy_units.filter(lambda unit: unit.type_id not in {UnitTypeId.DRONE,UnitTypeId.SCV})enemies_can_attack: Units = enemies2.filter(lambda unit: unit.can_attack_ground)for stalker in self.units(UnitTypeId.STALKER).ready:escape_location = stalker.position.towards(home_location, 6)enemyThreatsClose: Units = enemies_can_attack.filter(lambda unit: unit.distance_to(stalker) < 15) # Threats that can attack the stalkerif stalker.shield < 4 and enemyThreatsClose:abilities = await self.get_available_abilities(stalker)if AbilityId.EFFECT_BLINK_STALKER in abilities:#await self.order(stalker, EFFECT_BLINK_STALKER, escape_location)stalker(AbilityId.EFFECT_BLINK_STALKER, escape_location)continueelse:retreatPoints: Set[Point2] = self.around8(stalker.position, distance=2) | self.around8(stalker.position, distance=4)# Filter points that are pathableretreatPoints: Set[Point2] = {x for x in retreatPoints if self.in_pathing_grid(x)}if retreatPoints:closestEnemy: Unit = enemyThreatsClose.closest_to(stalker)retreatPoint: Unit = closestEnemy.position.furthest(retreatPoints)stalker.move(retreatPoint)continue # Continue for loop, dont execute any of the following# Stolen and modified from position.pydef around8(self, position, distance=1) -> Set[Point2]:p = positiond = distancereturn self.around4(position, distance) | {Point2((p.x - d, p.y - d)),Point2((p.x - d, p.y + d)),Point2((p.x + d, p.y - d)),Point2((p.x + d, p.y + d)),}# Stolen and modified from position.pydef around4(self, position, distance=1) -> Set[Point2]:p = positiond = distancereturn {Point2((p.x - d, p.y)), Point2((p.x + d, p.y)), Point2((p.x, p.y - d)), Point2((p.x, p.y + d))}def main():sc2.run_game(sc2.maps.get("Abyssal Reef LE"),[Bot(Race.Protoss, BraxBot()), Computer(Race.Zerg, Difficulty.Medium)],realtime=False,save_replay_as="Example.SC2Replay",)#def main():# sc2.run_game(# sc2.maps.get("Abyssal Reef LE"),# [Human(Race.Terran), Bot(Race.Protoss, BraxBot())],# realtime=True,# save_replay_as="Example.SC2Replay",# )if __name__ == "__main__":main()
Now the good thing about BurnySc2 is that we can now also fight hard and VeryHard bots.. In the previous part our bot defeated a medium AI, let’s try directly with the very hard..
def main():sc2.run_game(sc2.maps.get("Abyssal Reef LE"),[Bot(Race.Protoss, BraxBot()), Computer(Race.Zerg, Difficulty.VeryHard)],realtime=False,save_replay_as="Example.SC2Replay",)
Well.. There is room for improvement concerning the build order for sure. But we won without losing a single unit ! Challenge achieved !
Now, it is probably time to try ourselves to play vs the bot. Because yes, this is a possibility. If you would like to fight yourself your bot nothing complicated, just change this at the end of the script :
def main():sc2.run_game(sc2.maps.get("Abyssal Reef LE"),[Human(Race.Terran), Bot(Race.Protoss, BraxBot())],realtime=True,save_replay_as="Example.SC2Replay",)
My bot was suprisingly good, I was master at staracft long time ago and was actually quite impressed by the micro of the blink stalkers ! Playing yourself against your bot is a great way to find out his weakness. But nothing as exciting as real competition between machines.
I found out about :
I decided to put my bot to the test and see what he can do. You can find instructions on how to register your bot for this here : link
Basically you have to zip your bot along a few files :

And then create a profile on both sites and eventually go to their discord to ask to upload your bot. Once it is uploaded you can actually request for random matches with other bots and be part of the ladder. My bot got his ass kicked quite heaviliy but managed to sneak in a few wins :

Here is a video from the fight of one of our wins :
Some of those bots are really, really evolved and our small BraxBot can simply not compete, but it did well, for a rule bot !
If you want to see the score of BraxBot : https://aiarena.net/bots/202/
Next step will be to implement some actual machine learning in our bot and progressively shift from the rule based engine to a decision making autonomous agent.