From 824bdef6d4bd2b7a34224f8bbda5870cdf6ff462 Mon Sep 17 00:00:00 2001 From: Reynald Pader Date: Fri, 26 Jan 2018 10:58:44 +0800 Subject: [PATCH 1/6] Add inline condition to prevent converting of 'None' to string for non-Windows platforms --- sc2/sc2process.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sc2/sc2process.py b/sc2/sc2process.py index 8a04960ad..eb9e9c28b 100644 --- a/sc2/sc2process.py +++ b/sc2/sc2process.py @@ -78,7 +78,7 @@ def _launch(self): "-dataDir", str(Paths.BASE), "-tempDir", self._tmp_dir ], - cwd=str(Paths.CWD), + cwd=str(Paths.CWD) if Paths.CWD else None, #, env=run_config.env ) From 4ba7ac62fc4b70826a44ee4a2aea42983811da9c Mon Sep 17 00:00:00 2001 From: Reynald Pader Date: Fri, 26 Jan 2018 23:37:44 +0800 Subject: [PATCH 2/6] Wrap cwd expression in parentheses --- sc2/sc2process.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sc2/sc2process.py b/sc2/sc2process.py index eb9e9c28b..6ce238b3f 100644 --- a/sc2/sc2process.py +++ b/sc2/sc2process.py @@ -78,7 +78,7 @@ def _launch(self): "-dataDir", str(Paths.BASE), "-tempDir", self._tmp_dir ], - cwd=str(Paths.CWD) if Paths.CWD else None, + cwd=(str(Paths.CWD) if Paths.CWD else None), #, env=run_config.env ) From 8be993cc3cc6a08d738fce149e70fd62f6158703 Mon Sep 17 00:00:00 2001 From: Reynald Pader Date: Tue, 30 Jan 2018 19:38:27 +0800 Subject: [PATCH 3/6] Add support for getting the available abilities of a unit This includes checks to cooldown, energy, and research --- examples/threebase_voidray.py | 14 ++++++++++++++ examples/zerg_rush.py | 3 ++- sc2/bot_ai.py | 4 ++++ sc2/client.py | 13 +++++++++++++ sc2/ids/ability_id.py | 2 ++ sc2/ids/buff_id.py | 1 + sc2/unit.py | 6 ++++++ 7 files changed, 42 insertions(+), 1 deletion(-) diff --git a/examples/threebase_voidray.py b/examples/threebase_voidray.py index b6612aaa5..cc2497ba0 100644 --- a/examples/threebase_voidray.py +++ b/examples/threebase_voidray.py @@ -3,6 +3,7 @@ import sc2 from sc2 import Race, Difficulty from sc2.constants import * +from sc2.ids.buff_id import BuffId from sc2.player import Bot, Computer class ThreebaseVoidrayBot(sc2.BotAI): @@ -23,6 +24,19 @@ async def on_step(self, state, iteration): else: nexus = self.units(NEXUS).ready.random + if not nexus.has_buff(BuffId.CHRONOBOOSTENERGYCOST): + abilities = await self.get_available_abilities(nexus) + if AbilityId.CHRONOBOOSTENERGYCOST in abilities: + if nexus.energy > 25: + await self.do(nexus(AbilityId.CHRONOBOOSTENERGYCOST, nexus)) + else: + await self.chat_send("Not enough energy") + else: + await self.chat_send("Can't cast") + print(abilities) + else: + await self.chat_send("Nexus is boosted") + for idle_worker in self.workers.idle: mf = state.mineral_field.closest_to(idle_worker) await self.do(idle_worker.gather(mf)) diff --git a/examples/zerg_rush.py b/examples/zerg_rush.py index 58e9a6348..2afa55b47 100644 --- a/examples/zerg_rush.py +++ b/examples/zerg_rush.py @@ -32,7 +32,8 @@ async def on_step(self, state, iteration): await self.do(zl.attack(target)) for queen in self.units(QUEEN).idle: - if queen.energy >= 25: # Hard coded, since this is not (yet) available + abilities = await self.get_available_abilities(queen) + if AbilityId.INJECTLARVA in abilities: await self.do(queen(INJECTLARVA, hatchery)) if self.vespene >= 100: diff --git a/sc2/bot_ai.py b/sc2/bot_ai.py index 7ad20792b..ae57f2975 100644 --- a/sc2/bot_ai.py +++ b/sc2/bot_ai.py @@ -69,6 +69,10 @@ def expansion_locations(self): # Not always accurate, but good enought for now. return [c.rounded for c in centers] + async def get_available_abilities(self, unit): + # right know only checks cooldown, energy cost, and whether the ability has been researched + return await self._client.query_available_abilities(unit) + async def expand_now(self, building=None, max_distance=10): if not building: building = self.townhalls.first.type_id diff --git a/sc2/client.py b/sc2/client.py index 74a02cab2..e05622420 100644 --- a/sc2/client.py +++ b/sc2/client.py @@ -6,6 +6,9 @@ ) import logging + +from sc2.ids.ability_id import AbilityId + logger = logging.getLogger(__name__) from .cache import method_cache_forever @@ -160,6 +163,16 @@ async def query_building_placement(self, ability, positions, ignore_resources=Tr )) return [ActionResult(p.result) for p in result.query.placements] + async def query_available_abilities(self, unit): + assert isinstance(unit, Unit) + result = await self._execute(query=query_pb.RequestQuery( + abilities=[query_pb.RequestQueryAvailableAbilities( + unit_tag=unit.tag + )] + )) + return [AbilityId(a.ability_id) for a in result.query.abilities[0].abilities] + + async def chat_send(self, message, team_only): ch = ChatChannel.Team if team_only else ChatChannel.Broadcast r = await self._execute(action=sc_pb.RequestAction( diff --git a/sc2/ids/ability_id.py b/sc2/ids/ability_id.py index d273e7e4d..7d01b69d7 100644 --- a/sc2/ids/ability_id.py +++ b/sc2/ids/ability_id.py @@ -4,6 +4,8 @@ import enum class AbilityId(enum.Enum): + CHRONOBOOSTENERGYCOST = 3755 # temporary > please see PR#24 + NEXUSMASSRECALL = 3757 # temporary > please see PR#24 INVALID = 0 SMART = 1 STOP_STOP = 4 diff --git a/sc2/ids/buff_id.py b/sc2/ids/buff_id.py index 0217b6a9b..7b9be8be7 100644 --- a/sc2/ids/buff_id.py +++ b/sc2/ids/buff_id.py @@ -4,6 +4,7 @@ import enum class BuffId(enum.Enum): + CHRONOBOOSTENERGYCOST = 281 # temporary > please see PR#24 INVALID = 0 GRAVITONBEAM = 5 GHOSTCLOAK = 6 diff --git a/sc2/unit.py b/sc2/unit.py index 4af6d11f0..061ed7cec 100644 --- a/sc2/unit.py +++ b/sc2/unit.py @@ -1,4 +1,5 @@ from s2clientprotocol import sc2api_pb2 as sc_pb, raw_pb2 as raw_pb +from sc2.ids.buff_id import BuffId from .position import Point3 from .data import Alliance, Attribute, DisplayType @@ -184,6 +185,11 @@ def train(self, unit, *args, **kwargs): def build(self, unit, *args, **kwargs): return self(self._game_data.units[unit.value].creation_ability.id, *args, **kwargs) + def has_buff(self, buff): + assert isinstance(buff, BuffId) + + return buff.value in self._proto.buff_ids + def attack(self, *args, **kwargs): return self(AbilityId.ATTACK, *args, **kwargs) From 902b877f54347840123f5161297b4b93371d83be Mon Sep 17 00:00:00 2001 From: Reynald Pader Date: Thu, 1 Feb 2018 14:57:56 +0800 Subject: [PATCH 4/6] Update example for chronoboost cast --- examples/threebase_voidray.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/examples/threebase_voidray.py b/examples/threebase_voidray.py index 45015dee6..40de6ce06 100644 --- a/examples/threebase_voidray.py +++ b/examples/threebase_voidray.py @@ -27,15 +27,11 @@ async def on_step(self, iteration): if not nexus.has_buff(BuffId.CHRONOBOOSTENERGYCOST): abilities = await self.get_available_abilities(nexus) if AbilityId.CHRONOBOOSTENERGYCOST in abilities: - if nexus.energy > 25: await self.do(nexus(AbilityId.CHRONOBOOSTENERGYCOST, nexus)) - else: - await self.chat_send("Not enough energy") else: - await self.chat_send("Can't cast") - print(abilities) + await self.chat_send("Can't cast chrono boost") else: - await self.chat_send("Nexus is boosted") + await self.chat_send("Nexus is already boosted") for idle_worker in self.workers.idle: mf = self.state.mineral_field.closest_to(idle_worker) From d3d4ec91ce89948c85cd842743b12b8fe7bf3f61 Mon Sep 17 00:00:00 2001 From: Reynald Pader Date: Thu, 1 Feb 2018 16:33:10 +0800 Subject: [PATCH 5/6] Implement warpgate support using find_placement and available_abilities Fixes #27 --- examples/warpgate_toss.py | 114 ++++++++++++++++++++++++++++++++++++++ sc2/data.py | 20 ++++++- sc2/unit.py | 6 +- 3 files changed, 138 insertions(+), 2 deletions(-) create mode 100644 examples/warpgate_toss.py diff --git a/examples/warpgate_toss.py b/examples/warpgate_toss.py new file mode 100644 index 000000000..15d8ac061 --- /dev/null +++ b/examples/warpgate_toss.py @@ -0,0 +1,114 @@ +import random + +import sc2 +from sc2 import Race, Difficulty +from sc2.constants import * +from sc2.player import Bot, Computer + +class WarpGateBot(sc2.BotAI): + + def __init__(self): + self.warpgate_started = False + + def select_target(self, state): + if self.known_enemy_structures.exists: + return random.choice(self.known_enemy_structures) + + return self.enemy_start_locations[0] + + async def on_step(self, iteration): + if iteration == 0: + await self.chat_send("(glhf)") + + if not self.units(NEXUS).ready.exists: + for worker in self.workers: + await self.do(worker.attack(self.enemy_start_locations[0])) + return + else: + nexus = self.units(NEXUS).ready.random + + for idle_worker in self.workers.idle: + mf = self.state.mineral_field.closest_to(idle_worker) + await self.do(idle_worker.gather(mf)) + + + for a in self.units(ASSIMILATOR): + if a.assigned_harvesters < a.ideal_harvesters: + w = self.workers.closer_than(20, a) + if w.exists: + await self.do(w.random.gather(a)) + + if self.supply_left < 2 and not self.already_pending(PYLON): + if self.can_afford(PYLON): + await self.build(PYLON, near=nexus) + return + + if self.workers.amount < self.units(NEXUS).amount*15 and nexus.noqueue: + if self.can_afford(PROBE): + await self.do(nexus.train(PROBE)) + + elif not self.units(PYLON).amount < 5 and not self.already_pending(PYLON): + if self.can_afford(PYLON): + await self.build(PYLON, near=nexus) + + if self.units(NEXUS).amount < 3 and not self.already_pending(NEXUS): + if self.can_afford(NEXUS): + location = await self.get_next_expansion() + await self.build(NEXUS, near=location) + + if self.units(PYLON).ready.exists: + pylon = self.units(PYLON).ready.random + if self.units(GATEWAY).ready.exists: + if not self.units(CYBERNETICSCORE).exists: + if self.can_afford(CYBERNETICSCORE) and not self.already_pending(CYBERNETICSCORE): + await self.build(CYBERNETICSCORE, near=pylon) + else: + if self.can_afford(GATEWAY) and self.units(GATEWAY).amount < 4: + await self.build(GATEWAY, near=pylon) + + for nexus in self.units(NEXUS).ready: + vgs = self.state.vespene_geyser.closer_than(20.0, nexus) + for vg in vgs: + if not self.can_afford(ASSIMILATOR) or self.units(ASSIMILATOR).ready.exists: + break + + worker = self.select_build_worker(vg.position) + if worker is None: + break + + if not self.units(ASSIMILATOR).closer_than(1.0, vg).exists: + await self.do(worker.build(ASSIMILATOR, vg)) + + if self.units(CYBERNETICSCORE).ready.exists and self.can_afford(RESEARCH_WARPGATE) and not self.warpgate_started: + ccore = self.units(CYBERNETICSCORE).ready.first + await self.do(ccore(RESEARCH_WARPGATE)) + self.warpgate_started = True + + for gateway in self.units(GATEWAY).ready: + abilities = await self.get_available_abilities(gateway) + if AbilityId.MORPH_WARPGATE in abilities and self.can_afford(AbilityId.MORPH_WARPGATE): + await self.do(gateway(MORPH_WARPGATE)) + + for warpgate in self.units(WARPGATE).ready: + abilities = await self.get_available_abilities(warpgate) + if AbilityId.TRAINWARP_ZEALOT in abilities: + placement = await self.find_placement(AbilityId.TRAINWARP_ZEALOT, warpgate.position.to2, placement_step=1) + if placement is None: + #return ActionResult.CantFindPlacementLocation + print("can't place") + break + await self.do(warpgate.warp_in(STALKER, placement)) + + if self.units(STALKER).amount > 10 and iteration % 50 == 0: + for vr in self.units(STALKER).idle: + await self.do(vr.attack(self.select_target(self.state))) + + +def main(): + sc2.run_game(sc2.maps.get("Abyssal Reef LE"), [ + Bot(Race.Protoss, WarpGateBot()), + Computer(Race.Protoss, Difficulty.Easy) + ], realtime=False) + +if __name__ == '__main__': + main() diff --git a/sc2/data.py b/sc2/data.py index 8940f2819..74209e214 100644 --- a/sc2/data.py +++ b/sc2/data.py @@ -11,6 +11,16 @@ from .ids.unit_typeid import COMMANDCENTER, ORBITALCOMMAND, PLANETARYFORTRESS from .ids.unit_typeid import HATCHERY, LAIR, HIVE +from .ids.ability_id import TRAIN_ZEALOT, TRAIN_STALKER, TRAIN_HIGHTEMPLAR, TRAIN_DARKTEMPLAR, TRAIN_SENTRY, \ + TRAIN_ADEPT +from .ids.ability_id import \ + TRAINWARP_ZEALOT, \ + TRAINWARP_STALKER, \ + TRAINWARP_HIGHTEMPLAR, \ + TRAINWARP_DARKTEMPLAR, \ + TRAINWARP_SENTRY, \ + TRAINWARP_ADEPT + PlayerType = enum.Enum("PlayerType", sc_pb.PlayerType.items()) Difficulty = enum.Enum("Difficulty", sc_pb.Difficulty.items()) Status = enum.Enum("Status", sc_pb.Status.items()) @@ -28,7 +38,6 @@ ActionResult = enum.Enum("ActionResult", error_pb.ActionResult.items()) - race_worker = { Race.Protoss: PROBE, Race.Terran: SCV, @@ -40,3 +49,12 @@ Race.Terran: {COMMANDCENTER, ORBITALCOMMAND, PLANETARYFORTRESS}, Race.Zerg: {HATCHERY, LAIR, HIVE} } + +warpgate_abilities = { + TRAIN_ZEALOT: TRAINWARP_ZEALOT, + TRAIN_STALKER: TRAINWARP_STALKER, + TRAIN_HIGHTEMPLAR: TRAINWARP_HIGHTEMPLAR, + TRAIN_DARKTEMPLAR: TRAINWARP_DARKTEMPLAR, + TRAIN_SENTRY: TRAINWARP_SENTRY, + TRAIN_ADEPT: TRAINWARP_ADEPT +} diff --git a/sc2/unit.py b/sc2/unit.py index 061ed7cec..e7a86ccf1 100644 --- a/sc2/unit.py +++ b/sc2/unit.py @@ -2,7 +2,7 @@ from sc2.ids.buff_id import BuffId from .position import Point3 -from .data import Alliance, Attribute, DisplayType +from .data import Alliance, Attribute, DisplayType, warpgate_abilities from .game_data import GameData from .ids.unit_typeid import UnitTypeId from .ids.ability_id import AbilityId @@ -190,6 +190,10 @@ def has_buff(self, buff): return buff.value in self._proto.buff_ids + def warp_in(self, unit, placement, *args, **kwargs): + normal_creation_ability = self._game_data.units[unit.value].creation_ability.id + return self(warpgate_abilities[normal_creation_ability], placement, *args, **kwargs) + def attack(self, *args, **kwargs): return self(AbilityId.ATTACK, *args, **kwargs) From bb20eeb3dc8efa407521abf2483edd287bf189cc Mon Sep 17 00:00:00 2001 From: Reynald Pader Date: Thu, 1 Feb 2018 18:17:11 +0800 Subject: [PATCH 6/6] Fix sample --- examples/warpgate_toss.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/warpgate_toss.py b/examples/warpgate_toss.py index 15d8ac061..487857338 100644 --- a/examples/warpgate_toss.py +++ b/examples/warpgate_toss.py @@ -91,8 +91,9 @@ async def on_step(self, iteration): for warpgate in self.units(WARPGATE).ready: abilities = await self.get_available_abilities(warpgate) + # all the units have the same cooldown anyway so let's just look at ZEALOT if AbilityId.TRAINWARP_ZEALOT in abilities: - placement = await self.find_placement(AbilityId.TRAINWARP_ZEALOT, warpgate.position.to2, placement_step=1) + placement = await self.find_placement(AbilityId.TRAINWARP_STALKER, warpgate.position.to2, placement_step=1) if placement is None: #return ActionResult.CantFindPlacementLocation print("can't place")