class SpatialLlama(sc2.BotAI): def __init__(self): self.verbose = False self.visual_debug = False # Llama Stuff self.llama_controller = LlamaControler() # Control Stuff self.want_to_expand = False self.researched_warpgate = False # Attack stuff self.army_manager = ArmyManager(bot=self) self.attack_target = None self.units_available_for_attack = { ZEALOT: 'ZEALOT', STALKER: 'STALKER' } self.minimum_army_size = 15 # Defense stuff self.threat_proximity = 20 self.defending_units = {} self.defend_around = [PYLON, NEXUS] # Threat stuff stuff self.defending_from = {} # Scout stuff self.scouting_units = set() self.number_of_scouting_units = 3 self.scout_interval = 30 # Seconds self.scout_timer = 0 self.map_size = None # Expansion and macro stuff self.auto_expand_after = 300 # 5 Minutes self.auto_expand_mineral_threshold = 22 # Should be 2.5 ~ 3 fully saturated bases self.maximum_workers = 80 self.gateways_per_nexus = 2 # Research stuff self.start_forge_after = 180 # seconds - 4min self.forge_research_priority = ['ground_weapons', 'shield'] self.event_manager = EventManager() self.upgrades = { 'ground_weapons': [ FORGERESEARCH_PROTOSSGROUNDWEAPONSLEVEL1, FORGERESEARCH_PROTOSSGROUNDWEAPONSLEVEL2, FORGERESEARCH_PROTOSSGROUNDWEAPONSLEVEL3 ], 'ground_armor': [ FORGERESEARCH_PROTOSSGROUNDARMORLEVEL1, FORGERESEARCH_PROTOSSGROUNDARMORLEVEL2, FORGERESEARCH_PROTOSSGROUNDARMORLEVEL3 ], 'shield': [ FORGERESEARCH_PROTOSSSHIELDSLEVEL1, FORGERESEARCH_PROTOSSSHIELDSLEVEL2, FORGERESEARCH_PROTOSSSHIELDSLEVEL3 ] } self.upgrade_names = { FORGERESEARCH_PROTOSSGROUNDWEAPONSLEVEL1: 'GROUND WEAPONS 1', FORGERESEARCH_PROTOSSGROUNDWEAPONSLEVEL2: 'GROUND WEAPONS 2', FORGERESEARCH_PROTOSSGROUNDWEAPONSLEVEL3: 'GROUND WEAPONS 2', FORGERESEARCH_PROTOSSGROUNDARMORLEVEL1: 'GROUND ARMOR 2', FORGERESEARCH_PROTOSSGROUNDARMORLEVEL2: 'GROUND ARMOR 2', FORGERESEARCH_PROTOSSGROUNDARMORLEVEL3: 'GROUND ARMOR 2', FORGERESEARCH_PROTOSSSHIELDSLEVEL1: 'SHIELDS 1', FORGERESEARCH_PROTOSSSHIELDSLEVEL2: 'SHIELDS 2', FORGERESEARCH_PROTOSSSHIELDSLEVEL3: 'SHIELDS 3' } def on_start(self): if self.verbose: print('%6.2f Rise and shine' % (0)) self.map_size = self.game_info.map_size self.army_manager.init() # TODO Tweak these values self.event_manager.add_event(self.distribute_workers, 10) self.event_manager.add_event(self.manage_upgrades, 5.3) self.event_manager.add_event(self.build_workers, 2.25) self.event_manager.add_event(self.manage_supply, 1) self.event_manager.add_event(self.build_assimilator, 2.5) self.event_manager.add_event(self.build_structures, 2.4) self.event_manager.add_event(self.build_nexus, 5) self.event_manager.add_event(self.build_army, 0.9) self.event_manager.add_event(self.scout_controller, 7) self.event_manager.add_event(self.army_controller, 1.1) self.event_manager.add_event(self.defend, 2) self.event_manager.add_event(self.attack, 3) self.event_manager.add_event(self.expansion_controller, 5) async def on_step(self, iteration): sys.stdout.flush() if iteration == 0: # Do nothing on the first iteration to avoid # everything being done at the same time await self.chat_send(self.llama_controller.get_random_llama_fact()) return events = self.event_manager.get_current_events(self.time) for event in events: await event() await self.debug() async def expansion_controller(self): if self.time > self.auto_expand_after: number_of_minerals = sum([ self.state.mineral_field.closer_than(10, x).amount for x in self.townhalls ]) if number_of_minerals <= self.auto_expand_mineral_threshold: self.want_to_expand = True async def army_controller(self): await self.army_manager.step() async def scout_controller(self): current_time = self.time if current_time - self.scout_timer > self.scout_interval: self.scout_timer = self.time n_scouting_units_assigned = len(self.scouting_units) missing_scouting_units = self.number_of_scouting_units - n_scouting_units_assigned # Uses the previous assigned scouting units to keep scouting for scouting_unit_tag in list(self.scouting_units): unit = self.units.find_by_tag(scouting_unit_tag) if unit.exists: target = random.sample(list(self.expansion_locations), k=1)[0] await self.do(unit.attack(target)) else: # If a scouting unit isnt found then it is (most likely) dead # and we need another to replace it self.scouting_units.remove(unit) missing_scouting_units += 1 if missing_scouting_units > 0: idle_stalkers = self.units(STALKER).idle if idle_stalkers.exists: if self.verbose: print('%6.2f Scouting' % (self.time)) # If there is no unit assigned to scouting # the the idle unit furthest from the base for i in range(missing_scouting_units): stalker = idle_stalkers.furthest_to( self.units(NEXUS).first) if stalker: target = random.sample(list( self.expansion_locations), k=1)[0] await self.do(stalker.attack(target)) self.scouting_units idle_stalkers = self.units(STALKER).idle if not idle_stalkers.exists: break else: pass #print(' - no units to scout') async def defend(self): # Attacks units that get too close to import units for structure_type in self.defend_around: for structure in self.units(structure_type): threats = self.known_enemy_units.closer_than( self.threat_proximity, structure.position) if threats.exists: target_threat = None new_threat_count = 0 for threat in threats: if threat.tag not in self.defending_from: self.defending_from[threat.tag] = None target_threat = threat new_threat_count += 1 if new_threat_count > 0: if self.verbose: print('%6.2f found %d threats' % (self.time, new_threat_count)) await self.target_enemy_unit(target_threat) break async def target_enemy_unit(self, target): # sends all idle units to attack an enemy unit zealots = self.units(ZEALOT).idle stalkers = self.units(STALKER).idle total_units = zealots.amount + stalkers.amount # Only sends 1 unit to attack a worker is_worker = target.type_id in [PROBE, SCV, DRONE] if self.verbose: print('%6.2f defending with %d units' % (self.time, total_units)) for unit_group in [zealots, stalkers]: for unit in unit_group: if is_worker: await self.do(unit.attack(target)) if self.verbose: print( ' - target is a probe, sending a single unit') return else: await self.do(unit.attack(target.position)) async def attack(self): total_units = 0 for unit_type in self.units_available_for_attack.keys(): total_units += self.units(unit_type).idle.amount if total_units >= self.minimum_army_size: if self.army_manager.army_size() == 0: for unit_type in self.units_available_for_attack.keys(): for unit in self.units(unit_type).idle: self.army_manager.add(unit.tag) await self.army_manager.group_at_map_center( wait_for_n_units=total_units - 1, timeout=30, move_towards_position=self.enemy_start_locations[0]) if self.verbose: print('%6.2f Attacking with %d units' % (self.time, total_units)) else: for unit_type in self.units_available_for_attack.keys(): for unit in self.units(unit_type).idle: self.army_manager.add(unit.tag, options={'reinforcement': True}) if self.verbose: print('%6.2f reinforcing with %d units' % (self.time, total_units)) async def build_army(self): if not self.can('build_army'): return # Iterates over all gateways for gateway in self.units(GATEWAY).ready.noqueue: abilities = await self.get_available_abilities(gateway) # Checks if the gateway can morph into a warpgate if AbilityId.MORPH_WARPGATE in abilities and self.can_afford( AbilityId.MORPH_WARPGATE): await self.do(gateway(MORPH_WARPGATE)) # Else, tries to build a stalker elif AbilityId.GATEWAYTRAIN_STALKER in abilities: if self.can_afford(STALKER) and self.supply_left > 2: await self.do(gateway.train(STALKER)) # Else, tries to build a zealot elif AbilityId.GATEWAYTRAIN_ZEALOT in abilities: if self.can_afford(ZEALOT) and self.supply_left > 2: await self.do(gateway.train(ZEALOT)) # Iterates over all warpgates and warp in stalkers for warpgate in self.units(WARPGATE).ready: abilities = await self.get_available_abilities(warpgate) if AbilityId.WARPGATETRAIN_STALKER in abilities: if self.can_afford(STALKER) and self.supply_left > 2: # Smartly find a good pylon boy to warp in units next to it pylon = self.pylon_with_less_units() #pos = pylon.position.to2.random_on_distance(4) pos = pylon.position.to2 placement = await self.find_placement(PYLON, pos, placement_step=1, max_distance=4) if placement is not None: await self.do(warpgate.warp_in(STALKER, placement)) else: # otherwise just brute force it for _ in range(5): # TODO tweak this pylon = self.units(PYLON).ready.random #pos = pylon.position.to2.random_on_distance(4) pos = pylon.position.to2 placement = await self.find_placement( PYLON, pos, placement_step=1, max_distance=4) if placement is not None: await self.do( warpgate.warp_in(STALKER, placement)) break async def build_structures(self): if not self.can('build_structures'): return # Only start building main structures if there is # at least one pylon if not self.units(PYLON).ready.exists: return else: pylon = self.units(PYLON).ready.random number_of_gateways = self.units(WARPGATE).amount + self.units( GATEWAY).amount # Build the first gateway if self.can_afford(GATEWAY) and number_of_gateways == 0: if self.verbose: print('%6.2f starting first gateway' % (self.time)) await self.build(GATEWAY, near=pylon) # Build the cybernetics core after the first gateway is ready if self.can_afford(CYBERNETICSCORE) and self.units( CYBERNETICSCORE).amount == 0 and self.units(GATEWAY).ready: if self.verbose: print('%6.2f starting cybernetics' % (self.time)) await self.build(CYBERNETICSCORE, near=pylon) self.want_to_expand = True # Build more gateways after the cybernetics core is ready if self.can_afford(GATEWAY) and self.units(CYBERNETICSCORE).ready and ( (number_of_gateways < 4 and self.units(NEXUS).amount <= 2) or (number_of_gateways <= self.units(NEXUS).amount * self.gateways_per_nexus)): if self.verbose: print('%6.2f starting more gateways' % (self.time)) await self.build(GATEWAY, near=pylon) # Build 2 forges if self.time > self.start_forge_after and self.units(FORGE).amount < 2: if self.can_afford(FORGE) and not self.already_pending(FORGE): if self.verbose: print('%6.2f building forge' % (self.time)) await self.build(FORGE, near=pylon) # Build twilight council if self.units(FORGE).ready.amount >= 2 and self.units( TWILIGHTCOUNCIL).amount == 0: if self.can_afford(TWILIGHTCOUNCIL ) and not self.already_pending(TWILIGHTCOUNCIL): if self.verbose: print('%6.2f building twilight council' % (self.time)) await self.build(TWILIGHTCOUNCIL, near=pylon) async def build_nexus(self): if not self.can('expand'): return if not self.already_pending(NEXUS) and self.can_afford(NEXUS) and \ self.bot.units(UnitTypeId.NEXUS).ready.amount >= 1: if self.verbose: print('%6.2f expanding' % (self.time)) await self.expand_now() self.want_to_expand = False async def manage_upgrades(self): await self.manage_cyberbetics_upgrades() await self.manage_forge_upgrades() async def manage_cyberbetics_upgrades(self): if self.units(CYBERNETICSCORE).ready.exists and self.can_afford( RESEARCH_WARPGATE) and not self.researched_warpgate: ccore = self.units(CYBERNETICSCORE).ready.first await self.do(ccore(RESEARCH_WARPGATE)) self.researched_warpgate = True if self.verbose: print('%6.2f researching warpgate' % (self.time)) async def manage_forge_upgrades(self): for forge in self.units(FORGE).ready.noqueue: abilities = await self.get_available_abilities(forge) for upgrade_type in self.forge_research_priority: for upgrade in self.upgrades[upgrade_type]: sys.stdout.flush() if upgrade in abilities and self.can_afford(upgrade): if self.verbose: print('%6.2f researching %s' % (self.time, self.upgrade_names[upgrade])) await self.do(forge(upgrade)) break async def build_workers(self): nexus = self.units(NEXUS).ready.noqueue if nexus and self.units( PROBE ).amount < self.maximum_workers and self.workers.amount < self.units( NEXUS).amount * 22: if self.can_afford(PROBE) and self.supply_left > 2: await self.do(nexus.random.train(PROBE)) async def manage_supply(self): for tries in range(5): # Only tries 5 different placements nexus = self.units(NEXUS).ready if not nexus: return nexus = nexus.random if self.supply_left < 8 and not self.already_pending(PYLON): if self.can_afford(PYLON): pos = await self.find_placement(PYLON, nexus.position, placement_step=2) mineral_fields = self.state.mineral_field.closer_than( 8, nexus).closer_than(4, pos) if mineral_fields: continue else: await self.build(PYLON, near=pos) break async def build_assimilator(self): if not self.can('build_assimilator'): return if self.workers.amount < 16: return for nexus in self.units(NEXUS).ready: vgs = self.state.vespene_geyser.closer_than(20, nexus) for vg in vgs: if not self.can_afford(ASSIMILATOR): break worker = self.select_build_worker(vg.position) if worker is None: break if not self.units(ASSIMILATOR).closer_than(1.0, vg).exists: if self.verbose: print('%6.2f building assimilator' % (self.time)) await self.do(worker.build(ASSIMILATOR, vg)) async def debug(self): if not self.visual_debug: return # Setup and info font_size = 18 total_units = 0 for unit_type in self.units_available_for_attack.keys(): total_units += self.units(unit_type).idle.amount number_of_minerals = sum([ self.state.mineral_field.closer_than(10, x).amount for x in self.townhalls ]) # Text messages = [ ' n_workers: %3d' % self.units(PROBE).amount, ' n_zealots: %3d' % self.units(ZEALOT).amount, ' n_stalkers: %3d' % self.units(STALKER).amount, ' idle_army: %3d' % total_units, ' army_size: %3d' % self.army_manager.army_size(), ' ememy_units: %3d' % self.known_enemy_units.amount, 'ememy_structures: %3d' % self.known_enemy_structures.amount, ' minerals_left: %3d' % number_of_minerals, ] if self.army_manager.leader is not None: messages.append(' leader: %3d' % self.army_manager.leader) y = 0 inc = 0.025 for message in messages: self._client.debug_text_screen(message, pos=(0.001, y), size=font_size) y += inc # Spheres leader_tag = self.army_manager.leader for soldier_tag in self.army_manager.soldiers: soldier_unit = self.units.find_by_tag(soldier_tag) if soldier_unit is not None: if soldier_tag == leader_tag: self._client.debug_sphere_out(soldier_unit, r=1, color=(255, 0, 0)) else: self._client.debug_sphere_out(soldier_unit, r=1, color=(0, 0, 255)) # Lines if self.army_manager.army_size() > 0: leader_tag = self.army_manager.leader leader_unit = self.units.find_by_tag(leader_tag) for soldier_tag in self.army_manager.soldiers: if soldier_tag == leader_tag: continue soldier_unit = self.units.find_by_tag(soldier_tag) if soldier_unit is not None: leader_tag = self.army_manager.leader leader_unit = self.units.find_by_tag(leader_tag) if leader_unit is not None: self._client.debug_line_out(leader_unit, soldier_unit, color=(0, 255, 255)) # pylon pylon = self.pylon_with_less_units() if pylon is not None: pos = pylon.position3d self._client.debug_sphere_out(pos, r=1, color=(0, 255, 0)) self._client.debug_sphere_out(pos, r=2, color=(0, 255, 0)) self._client.debug_sphere_out(pos, r=3, color=(0, 255, 0)) self._client.debug_sphere_out(pos, r=4, color=(0, 255, 0)) # Sens the debug info to the game await self._client.send_debug() def select_target(self, state): if self.known_enemy_structures.exists: return random.choice(self.known_enemy_structures) return self.enemy_start_locations[0] # Finds the pylon with more "space" next to it # Where more space == Less units # TODO consider "warpable" space def pylon_with_less_units(self, distance=4): pylons = self.units(PYLON).ready good_boy_pylon = None units_next_to_good_boy_pylon = float('inf') for pylon in pylons: units_next_to_candidate_pylon = self.units.closer_than( distance, pylon).amount if units_next_to_candidate_pylon < units_next_to_good_boy_pylon: good_boy_pylon = pylon units_next_to_good_boy_pylon = units_next_to_candidate_pylon return good_boy_pylon def can(self, what): if what == 'build_army': return not self.want_to_expand if what == 'build_structures': return not self.want_to_expand if what == 'build_assimilator': return not self.want_to_expand if what == 'expand': return self.want_to_expand self.console() def console(self): from IPython.terminal.embed import InteractiveShellEmbed ipshell = InteractiveShellEmbed.instance() ipshell() def get_unit_info(self, unit, field="food_required"): assert isinstance(unit, (Unit, UnitTypeId)) if isinstance(unit, Unit): unit = unit._type_data._proto else: unit = self._game_data.units[unit.value]._proto if hasattr(unit, field): return getattr(unit, field) else: return None