def __init__(self, center_location, is_start_location, knowledge): self.center_location: Point2 = center_location self.is_start_location: bool = is_start_location self.knowledge = knowledge self.ai: sc2.BotAI = knowledge.ai self.cache: "UnitCacheManager" = knowledge.unit_cache self.unit_values: "UnitValue" = knowledge.unit_values self.needs_evacuation = False self._is_enemys = False self.zone_index: int = 0 self.paths: Dict[int, Path] = dict() # paths to other expansions as it is dictated in the .expansion_zones # Game time seconds when we have last had visibility on this zone. self.last_scouted_center: float = -1 self.last_scouted_mineral_line: float = -1 # Timing on when there could be enemy workers here self.could_have_enemy_workers_in = 0 # All mineral fields on the zone self._original_mineral_fields: Units = self.ai.expansion_locations.get(self.center_location, Units([], self.ai)) self.mineral_fields: Units = Units(self._original_mineral_fields.copy(), self.ai) self.last_minerals: int = 10000000 # Arbitrary value just to ensure a lower value will get updated. # Game time seconds when scout has last circled around the center location of this zone. # All vespene geysers on the zone self.gas_buildings: Units = None self.scout_last_circled: Optional[int] = None self.our_townhall: Optional[Unit] = None self.enemy_townhall: Optional[Unit] = None self.known_enemy_units: Units = Units([], self.ai) self.our_units: Units = Units([], self.ai) self.our_workers: Units = Units([], self.ai) self.enemy_workers: Units = Units([], self.ai) self.known_enemy_power: ExtendedPower = ExtendedPower(self.unit_values) self.our_power: ExtendedPower = ExtendedPower(self.unit_values) # Assaulting enemies can be further away, but zone defense should prepare for at least that amount of defense self.assaulting_enemies: Units = Units([], self.ai) self.assaulting_enemy_power: ExtendedPower = ExtendedPower(self.unit_values) # 3 positions behind minerals self.behind_mineral_positions: List[Point2] = self._init_behind_mineral_positions() self._count_minerals() self._minerals_counter = IntervalFunc(knowledge.ai, self._count_minerals, 0.5) self.gather_point = self.center_location.towards(self.ai.game_info.map_center, 5) self.height = self.ai.get_terrain_height(center_location) # This is ExtendedRamp! self.ramp = self._find_ramp(self.ai) self.radius = Zone.ZONE_RADIUS self.danger_radius = Zone.ZONE_DANGER_RADIUS if self.ramp is not None: self.gather_point = self.ramp.top_center.towards(self.center_location, 4)
async def start(self, knowledge: "Knowledge"): await super().start(knowledge) self.all_own: Units = Units([], self.ai) self.empty_units: Units = Units([], self.ai)
async def start(self, knowledge: "Knowledge"): await super().start(knowledge) self.units = Units([], self.ai)
def free_units(self) -> Units: units: Units = Units(self.roles[UnitTask.Idle.value].units, self.ai) units.extend(self.roles[UnitTask.Moving.value].units) return units
async def on_step(self, iteration): self.units_by_tag = {unit.tag: unit for unit in self.all_own_units} print( list((str(point) + ": " + str(list(len(group.medivac_tags) for group in groups)) for point, groups in self.harass_assignments.items()))) await self.macro() # endgame condition if not self.townhalls.ready: if not self.units: await self.chat_send("(pineapple)") target: Point2 = self.enemy_structures.random_or( self.enemy_start_locations[0]).position for unit in self.units(): unit.attack(target) return # handle waiting marines and medivacs self.waiting_marine_tags = self.waiting_marine_tags & self.units_by_tag.keys( ) self.waiting_medivac_tags = self.waiting_medivac_tags & self.units_by_tag.keys( ) waiting_marines: Units = Units( {self.units_by_tag[m_tag] for m_tag in self.waiting_marine_tags}, self) waiting_medivacs: Units = Units( {self.units_by_tag[m_tag] for m_tag in self.waiting_medivac_tags}, self) chill_spot = self.own_expansions[self.townhalls.amount > 0].towards( self.game_info.map_center, 7) for unit in (waiting_marines + waiting_medivacs ).filter(lambda u: u.distance_to(chill_spot) > 5): unit.attack(chill_spot) # handle attack groups needs_regroup_groups: Set[DropTactics] = set() perished_groups: Set[DropTactics] = set() for group in self.harass_groups: needs_regroup = await group.handle(self.units_by_tag) perished = group.perished(self.units_by_tag) if needs_regroup: needs_regroup_groups.add(group) elif perished: perished_groups.add(group) # handle wounded attack groups for group in needs_regroup_groups | perished_groups: if group in needs_regroup_groups: self.waiting_marine_tags |= group.marine_tags self.waiting_medivac_tags |= group.medivac_tags elif group in perished_groups: self.stranded_marine_tags |= group.marine_tags self.harass_groups.remove(group) self.harass_assignments[group.targets[0]].remove(group) joiners: List[JoinTactics] = self.join_assignments[group] if joiners: new_drop_tactics = DropTactics( marine_tags=joiners[0].marine_tags, medivac_tags=joiners[0].medivac_tags, targets=group.targets, retreat_point=group.retreat_point, bot_object=self, walk=False) if len(joiners) > 1: joiners[1].assignment = new_drop_tactics self.join_assignments[new_drop_tactics] = joiners[1:] self.harass_groups.add(new_drop_tactics) current_groups = self.harass_assignments.get( new_drop_tactics.targets[0]) or [] current_groups.append(new_drop_tactics) self.harass_assignments[ new_drop_tactics.targets[0]] = current_groups del self.join_assignments[group] # assign drop and join groups if self.already_pending_upgrade(UpgradeId.STIMPACK) >= 0.9: while (len(self.waiting_marine_tags) >= 8 and len(self.waiting_medivac_tags) >= 1): print("new group!") new_harass_marine_tags = set( itertools.islice(self.waiting_marine_tags, 8)) new_harass_medivac_tags = set( itertools.islice(self.waiting_medivac_tags, 1)) self.waiting_marine_tags.difference_update( new_harass_marine_tags) self.waiting_medivac_tags.difference_update( new_harass_medivac_tags) heuristic_scores: Dict[Tuple[Point2, int], int] = dict() for enemy_base_i in range(self.enemy_size): enemy_base = self.enemy_expansions[enemy_base_i] for harass_i in range(self.HARASS_SIZE): index = enemy_base_i + self.enemy_size * harass_i enemy_base_squads = self.harass_assignments.get( enemy_base) or [] squad_size = 0 if harass_i < len(enemy_base_squads): current_squad = enemy_base_squads[harass_i] squad_size += len(current_squad.medivac_tags) joiners = self.join_assignments[current_squad] if joiners: squad_size += sum( len(joiner.medivac_tags) for joiner in joiners) heuristic_scores[( enemy_base, harass_i)] = index + squad_size * 1000 best_match: Tuple[Point2, int] = min(heuristic_scores.keys(), key=lambda t: heuristic_scores[t]) print(f"heuristic: {best_match[0]}, {best_match[1]}") if best_match[0] in self.harass_assignments.keys( ) and best_match[1] < len( self.harass_assignments[best_match[0]]): # there is an existing DropTactics. create a JoinTactics group print("adding new join tactics") existing_drop_tactic = self.harass_assignments[ best_match[0]][best_match[1]] existing_join_tactics = self.join_assignments[ existing_drop_tactic] next_assignment = existing_join_tactics[ len(existing_join_tactics) - 1] if existing_join_tactics else existing_drop_tactic new_join_tactics = JoinTactics( marine_tags=new_harass_marine_tags, medivac_tags=new_harass_medivac_tags, bot_object=self, assignment=next_assignment) self.join_assignments[existing_drop_tactic].append( new_join_tactics) else: # there is not an existing DropTactics. create a new DropTactics group print("adding new drop tactics") new_drop_tactics = DropTactics( marine_tags=new_harass_marine_tags, medivac_tags=new_harass_medivac_tags, targets=[best_match[0], self.enemy_start_locations[0]], retreat_point=self.own_expansions[ self.townhalls.amount > 0], bot_object=self, walk=False) current_groups = self.harass_assignments.get( best_match[0]) or [] current_groups.append(new_drop_tactics) self.harass_assignments[best_match[0]] = current_groups self.harass_groups.add(new_drop_tactics) self.join_assignments[new_drop_tactics] = [] # handle join groups for main_drop, join_groups in self.join_assignments.items(): for join_group_i in range(len(join_groups)): join_group: JoinTactics = join_groups[join_group_i] arrived = await join_group.handle(self.units_by_tag) if join_group.assignment.perished(self.units_by_tag): assert type( join_group.assignment ) == JoinTactics, "somehow a drop tactics slipped by" next_assignment = join_group.assignment while next_assignment.perished(self.units_by_tag): join_groups.remove(next_assignment) next_assignment = next_assignment.assignment join_group.assignment = next_assignment elif arrived: join_group.assignment.marine_tags = join_group.assignment.marine_tags | join_group.marine_tags join_group.assignment.medivac_tags = join_group.assignment.medivac_tags | join_group.medivac_tags if join_group_i + 1 < len(join_groups): arrived_joiner = join_groups[join_group_i + 1] arrived_joiner.assignment = join_group.assignment self.join_assignments[main_drop].remove(join_group)
async def execute(self) -> bool: self.defender_tags.clear() if self.ai.supply_army > 3: # Safe if self.was_active: self.free_others() self.was_active = False return True combined_enemies: Units = Units([], self.ai) for zone in self.knowledge.expansion_zones: # type: Zone if not zone.is_ours: continue combined_enemies |= zone.known_enemy_units already_defending: Units = self.knowledge.roles.units(UnitTask.Defending) if not combined_enemies: if self.was_active: self.free_others() self.was_active = False # Safe return True # for unit in already_defending: # # return workers back to base that have wandered too far away # if unit.type_id in self.unit_values.worker_types and unit.distance_to(self.ai.start_location) > 30: # self.knowledge.roles.clear_task(unit) # unit.gather(self.gather_mf) worker_only = combined_enemies.amount == combined_enemies.of_type(self.unit_values.worker_types).amount if combined_enemies.amount == 1 and worker_only: # Single scout worker u: Unit if ( self.ai.workers.filter( lambda u: u.shield_health_percentage < 0.75 and u.distance_to(self.ai.start_location) < 30 ).amount == 0 ): # Safe, let the scout do whatever it wants # TODO: Check expansion / building blocking self.free_others() return False # Blocks normal zone defense, TODO: Remove this? self.attack_target(combined_enemies[0], 2, already_defending) self.free_others() return False # Block other defense methods if worker_only: distance, closest_enemy = self.closest_distance_between_our_theirs(combined_enemies) if closest_enemy is None: closest_enemy = combined_enemies.closest_to(self.ai.start_location) buildings_needs_defending = self.ai.structures.filter(lambda u: self.building_needs_defending(u, 0.6)) if not buildings_needs_defending.exists and ( distance > 4 or (len(already_defending) < 5 and combined_enemies.closest_distance_to(self.ai.start_location) > 9) ): self.free_others() return False # no real danger, go back to mining require_workers = combined_enemies.amount + 2 worker_count = self.ai.supply_workers if require_workers > 5 and worker_count > 5: require_workers = min(worker_count - 2, require_workers) army = self.get_army(closest_enemy, require_workers, already_defending) if not army: return False # No army to fight with, waiting for one. self.knowledge.roles.set_tasks(UnitTask.Defending, army) # my_closest = army.closest_to(closest_enemy.position) # center = army.center buildings_needs_defending = self.ai.structures.filter(lambda u: self.building_needs_defending(u, 0.5)) # own_closest = army.closest_to(closest_enemy) if buildings_needs_defending.exists or distance < 3: for unit in army: # if unit.type_id == UnitTypeId.PROBE and unit.shield <= 5: # await self.regroup_defend(actions, army, combined_enemies, unit) # else: self.knowledge.combat_manager.add_unit(unit) else: for unit in army: await self.regroup_defend(army, combined_enemies, unit) self.knowledge.combat_manager.execute(closest_enemy.position, MoveType.Assault) self.free_others() else: return True # Don't know how to defend against self.was_active = True return False # In Combat
def test_tags_in(self): self.assertEqual( self.marines.tags_in({245346374, 245346375}), Units([self.marine1, self.marine2], self.mock_game_state)) self.assertEqual(self.marines.tags_in({}), self.emptyUnitsGroup)
def _prepare_units(self): # Set of enemy units detected by own sensor tower, as blips have less unit information than normal visible units self.blips: Set[Blip] = set() self.units: Units = Units([], self) self.structures: Units = Units([], self) self.enemy_units: Units = Units([], self) self.enemy_structures: Units = Units([], self) self.mineral_field: Units = Units([], self) self.vespene_geyser: Units = Units([], self) self.resources: Units = Units([], self) self.destructables: Units = Units([], self) self.watchtowers: Units = Units([], self) self.all_units: Units = Units([], self) self.workers: Units = Units([], self) self.townhalls: Units = Units([], self) self.gas_buildings: Units = Units([], self) self.larva: Units = Units([], self) self.techlab_tags: Set[int] = set() self.reactor_tags: Set[int] = set() for unit in self.state.observation_raw.units: if unit.is_blip: self.blips.add(Blip(unit)) else: unit_type: int = unit.unit_type # Convert these units to effects: reaper grenade, parasitic bomb dummy, forcefield unit_obj = Unit(unit, self) self.units.append(unit_obj)
def add_worker(self, new_worker: Unit): self.add_workers(Units([new_worker], self._bot_object))
async def on_step(self, iteration): self.units_by_tag = {unit.tag: unit for unit in self.all_own_units} for expansion_i in range(len(self.enemy_expansions)): self._client.debug_text_world( text=f"{expansion_i} : {self.enemy_expansions[expansion_i]}", pos=self.enemy_expansions[expansion_i], color=(0, 255, 0), size=12, ) if iteration == 1: raxpos: Point2 = await self.find_placement( UnitTypeId.BARRACKS, near=self.start_location.towards(self.game_info.map_center, 5), addon_place=True) await self.client.debug_create_unit([ [UnitTypeId.BARRACKS, 1, raxpos, 1], ]) if iteration > 1: depot = self.structures(UnitTypeId.SUPPLYDEPOT) barracks = self.structures(UnitTypeId.BARRACKS) if not barracks and not depot and self.already_pending( UnitTypeId.SUPPLYDEPOT) == 0: await self.build(UnitTypeId.SUPPLYDEPOT, near=self.start_location.towards( self.game_info.map_center, 5)) if depot and self.already_pending( UnitTypeId.BARRACKS) == 0 and not barracks: pos: Point2 = await self.find_placement( UnitTypeId.BARRACKS, near=self.start_location.towards(self.game_info.map_center, 5), addon_place=True) await self.build(UnitTypeId.BARRACKS, near=pos) for barrack in barracks.ready.filter(lambda b: not b.has_add_on): barrack.build(UnitTypeId.BARRACKSTECHLAB) techlab_rax = barracks.ready.filter( lambda b: b.has_techlab).random_or(None) if techlab_rax and self.already_pending_upgrade( UpgradeId.STIMPACK) == 0: techlab = self.structures.find_by_tag( tag=techlab_rax.add_on_tag) techlab.research(UpgradeId.STIMPACK) if self.already_pending_upgrade(UpgradeId.STIMPACK) > 0: if (len(self.waiting_marine_tags) >= self.HARASS_SIZE * 8 and len(self.waiting_medivac_tags) >= self.HARASS_SIZE): new_harass_marine_tags = set( itertools.islice(self.waiting_marine_tags, self.HARASS_SIZE * 8)) new_harass_medivac_tags = set( itertools.islice(self.waiting_medivac_tags, self.HARASS_SIZE)) self.waiting_marine_tags.difference_update( new_harass_marine_tags) self.waiting_medivac_tags.difference_update( new_harass_medivac_tags) next_targets = list( filter(lambda p: self.harass_assignments[p] is None, self.enemy_expansions)) if len(next_targets) > 0: new_drop_tactics = DropTactics( marine_tags=new_harass_marine_tags, medivac_tags=new_harass_medivac_tags, targets=[next_targets[0]], retreat_point=self.start_location, bot_object=self, walk=False) self.harass_assignments[ next_targets[0]] = new_drop_tactics self.harass_groups.add(new_drop_tactics) for group in self.harass_groups: await group.handle(self.units_by_tag) alive_marine_tags = self.waiting_marine_tags & self.units_by_tag.keys( ) alive_medivac_tags = self.waiting_medivac_tags & self.units_by_tag.keys( ) waiting_marines: Units = Units( {self.units_by_tag[m_tag] for m_tag in alive_marine_tags}, self) waiting_medivacs: Units = Units( {self.units_by_tag[m_tag] for m_tag in alive_medivac_tags}, self) self.waiting_marine_tags = alive_marine_tags self.waiting_medivac_tags = alive_medivac_tags chill_spot = self.own_expansions[ self.townhalls.amount > 0].towards( self.game_info.map_center, 10) for unit in (waiting_marines + waiting_medivacs ).filter(lambda u: u.distance_to(chill_spot) > 5): unit.attack(chill_spot)
def distribute_workers(self): # Only do anything once every 3 seconds if self.time - self.last_distribute < 3: return self.last_distribute = self.time # Kinda hard to gather anything without a base if not self.townhalls.ready.exists: return # mineral patches near one of our bases acceptable_minerals = self.mineral_field.filter(lambda node: any([ nex.position.is_closer_than(15, node.position) for nex in self.townhalls.ready ])) workers_per_gas = 1 + min( 2, int(self.workers.amount / acceptable_minerals.amount)) if self.minerals < 50 and self.vespene > 300: workers_per_gas -= 1 # gas buildings probably at bases that have been destroyed bad_geysers = self.structures( self.shared.gas_structure).filter(lambda a: all( ex.is_further_than(15, a) for ex in self.owned_expansions.keys( )) or a.vespene_contents == 0 or a.assigned_harvesters > workers_per_gas) # gas buildings that don't have enough harvesters needy_geysers = self.structures( self.shared.gas_structure).ready.tags_not_in([ a.tag for a in bad_geysers ]).filter(lambda a: a.assigned_harvesters < workers_per_gas) # tag collections for easy selection and matching acceptable_mineral_tags = [f.tag for f in acceptable_minerals] needy_mineral_tags = [ f.tag for f in acceptable_minerals if self.townhalls.closest_to(f.position).surplus_harvesters < 0 ] # anywhere else is strictly forbidden unacceptable_mineral_tags = [ f.tag for f in self.mineral_field.tags_not_in(acceptable_mineral_tags) ] bad_workers = self.unallocated(self.shared.worker_types).filter( lambda p: # Grab these suckers first p.is_idle or (p.is_gathering and p.orders[0].target in unacceptable_mineral_tags) or (p.is_gathering and p.orders[0].target in bad_geysers) or p.orders[ 0].ability.id in [ AbilityId.ATTACK_ATTACKTOWARDS, AbilityId.ATTACK_ATTACK, AbilityId.ATTACK ]) # up to N workers, where N is the number of surplus harvesters, from each base where there are any # may not grab them all every time (it gets only the ones returning minerals), but it'll get enough excess_workers = Units( list_flatten([ self.workers.filter(lambda w: w.is_carrying_minerals and w. orders and w.orders[0].target == base.tag) [0:base.surplus_harvesters] for base in self.townhalls.filter( lambda base: base.surplus_harvesters > 0) ]), self) # to fill up your first gas building, you'll need these mining_workers = self.workers.filter( lambda p: # if more are needed, this is okay too p.is_gathering and (p.orders[0].target in acceptable_mineral_tags or p.is_carrying_minerals)) - (bad_workers + excess_workers) usable_workers = bad_workers + excess_workers + mining_workers taken_workers = 0 def get_workers(num): nonlocal taken_workers if taken_workers + num > usable_workers.amount: return [] taken_workers += num return usable_workers[taken_workers - num:taken_workers] for needy_geyser in needy_geysers: workers = get_workers(workers_per_gas - needy_geyser.assigned_harvesters) for worker in workers: self.do(worker.gather(needy_geyser)) if taken_workers < bad_workers.amount and acceptable_mineral_tags: remaining_bad_workers = get_workers(bad_workers.amount - taken_workers) for worker in remaining_bad_workers: self.do( worker.gather( self.mineral_field.tags_in( acceptable_mineral_tags).random)) if taken_workers < bad_workers.amount + excess_workers.amount and needy_mineral_tags: remaining_excess_workers = get_workers(bad_workers.amount + excess_workers.amount - taken_workers) for worker in remaining_excess_workers: self.do( worker.gather( self.mineral_field.tags_in(needy_mineral_tags).random))
class Scout(SubActs): units: Units def __init__(self, unit_types: Union[UnitTypeId, Set[UnitTypeId]], unit_count: int, *args: ScoutBaseAction): """ Scout act for all races, loops the given scout actions @param unit_types: Types of units accepted as scouts @param unit_count: Units required to be used in scouting, scouting will only start after all are available @param args: Scout actions, cen be to scout a certain location, or to move around in certain way. Defaults to scouting enemy main """ if isinstance(unit_types, UnitTypeId): self.unit_types = set() self.unit_types.add(unit_types) else: self.unit_types = unit_types self.unit_count = unit_count if len(args) > 0: super().__init__(*args) else: super().__init__(ScoutLocation.scout_main()) self.scout_tags: List[int] = [] self.started = False self.ended = False self.index = 0 async def start(self, knowledge: "Knowledge"): await super().start(knowledge) self.units = Units([], self.ai) async def execute(self) -> bool: if self.ended: return True self.units.clear() if self.find_units(): return True if self.units: self.roles.set_tasks(UnitTask.Scouting, self.units) await self.micro_units() # Ignore if the scouting has finished return True async def micro_units(self) -> bool: """ Micros units @return: True when finished """ count = len(self.orders) self.index = self.index % count for looped in range(0, count): if looped == count: self.ended = True return True # noinspection PyTypeChecker action: ScoutBaseAction = self.orders[self.index] action.set_scouts(self.units) result = await action.execute() if not result: # Not finished return False self.index = (self.index + 1) % count return False def find_units(self) -> bool: if not self.started: if UnitTypeId.OVERLORD in self.unit_types: free_units = self.roles.get_types_from( self.unit_types, UnitTask.Idle, UnitTask.Moving, UnitTask.Gathering, UnitTask.Reserved ) else: free_units = self.roles.get_types_from( self.unit_types, UnitTask.Idle, UnitTask.Moving, UnitTask.Gathering ) if len(free_units) >= self.unit_count: # TODO: Better selection? new_scouts = free_units.random_group_of(self.unit_count) self.units.extend(new_scouts) self.scout_tags = new_scouts.tags self.started = True else: scouts = self.roles.get_types_from(self.unit_types, UnitTask.Scouting) self.units.extend(scouts.tags_in(self.scout_tags)) if not self.units: # Scouts are dead, end the scout act self.ended = True return True
class UnitManager(): def __init__(self, bot: BotAI, scouting_manager: ScoutingManager): self.bot = bot self.scouting_manager = scouting_manager self.unselectable = Units([], self.bot._game_data) self.unselectable_enemy_units = Units([], self.bot._game_data) self.scouting_ttl = 300 self.army_scouting_ttl = 100 self.panic_scout_ttl = 0 self.inject_targets: Dict[Unit, Unit] = {} self.inject_queens: Units = Units([], self.bot._game_data) self.dead_tumors: Units = Units([], self.bot._game_data) self.spread_overlords: Units = Units([], self.bot._game_data) self.chasing_workers: Units = Units([], self.bot._game_data) async def iterate(self, iteration): self.scouting_ttl -= 1 actions: List[UnitCommand] = [] all_army: Units = self.bot.units.exclude_type( {OVERLORD, DRONE, QUEEN, LARVA, EGG}).not_structure.ready observed_enemy_army = self.scouting_manager.observed_enemy_units.filter( lambda u: u.can_attack_ground or u.type_id == UnitTypeId.BUNKER) estimated_enemy_value = self.scouting_manager.estimated_enemy_army_value army_units = all_army for observed_enemy in observed_enemy_army: pos = observed_enemy.position self.bot._client.debug_text_world(f'observed', Point3((pos.x, pos.y, 10)), None, 12) # ASSIGN INJECT QUEENS hatches = self.bot.find_closest_n_from_units( self.bot.start_location, 4, self.bot.units(HATCHERY)).ready.tags_not_in( set(map(lambda h: h.tag, self.inject_targets.keys()))) for hatch in hatches: free_queens: Units = self.bot.units(QUEEN).tags_not_in( self.unselectable.tags).tags_not_in(self.inject_queens.tags) if free_queens.exists: queen = free_queens.random self.inject_targets[hatch] = queen self.inject_queens.append(queen) # INJECT for hatch in self.inject_targets: if self.bot.known_enemy_units.closer_than(15, hatch).exists: continue inject_queen = self.inject_targets[hatch] if inject_queen: try: abilities = await self.bot.get_available_abilities( inject_queen) if abilities and len( abilities ) > 0 and AbilityId.EFFECT_INJECTLARVA in abilities: actions.append( inject_queen(AbilityId.EFFECT_INJECTLARVA, hatch)) else: # move to hatch pass except: print('inject error') else: del self.inject_targets[hatch] # SCOUTING if army_units( LING ).exists and self.scouting_ttl < 0 and self.scouting_manager.enemy_raiders_value == 0: self.scouting_ttl = 300 unit: Unit = army_units(LING).random actions.append(unit.stop()) scouting_order: List[Point2] = [] keys: List[Point2] = list(self.bot.expansion_locations.keys()) for idx in range(len(self.bot.expansion_locations)): furthest = self.bot.enemy_start_locations[0].furthest(keys) scouting_order.append(furthest) keys.remove(furthest) for position in scouting_order: actions.append(unit.move(position, True)) self.unselectable.append(unit) # army scout only if opponent army has not been close for a while if not observed_enemy_army.closer_than( 70, self.bot.own_natural).amount > 2: self.army_scouting_ttl -= 1 else: self.army_scouting_ttl = 60 if self.army_scouting_ttl <= 0 and army_units(LING).exists: self.army_scouting_ttl = 60 unit: Unit = army_units(LING).random actions.append(unit.move(self.bot.enemy_start_locations[0])) self.unselectable.append(unit) # panic scout main if drone difference gets high enough if self.bot.already_pending(DRONE) + self.bot.units( DRONE ).amount > 25 * self.scouting_manager.enemy_townhall_count: if self.panic_scout_ttl <= 0: if self.bot.units(OVERLORD).exists: closest_overlord = self.bot.units(OVERLORD).tags_not_in( self.unselectable.tags).closest_to( self.bot.enemy_start_locations[0]) original_position = closest_overlord.position actions.append(closest_overlord.stop()) actions.append( closest_overlord.move( self.bot.enemy_start_locations[0], True)) actions.append( closest_overlord.move(original_position, True)) self.unselectable.append(closest_overlord) self.panic_scout_ttl = 300 else: self.panic_scout_ttl -= 1 # KILL TERRAN BUILDINGS WITH MUTAS if self.scouting_manager.terran_floating_buildings: mutas: Units = self.bot.units(MUTALISK).tags_not_in( self.unselectable.tags) pos: Point2 = self.bot.enemy_start_locations[ 0] + 15 * self.bot._game_info.map_center.direction_vector( self.bot.enemy_start_locations[0]) corners = [ Point2((0, 0)), Point2((self.bot._game_info.pathing_grid.width - 1, 0)), Point2((self.bot._game_info.pathing_grid.width - 1, self.bot._game_info.pathing_grid.height - 1)), Point2((0, self.bot._game_info.pathing_grid.height - 1)), Point2((0, 0)) ] for muta in mutas: for corner in corners: actions.append(muta.attack(corner, True)) self.unselectable.append(muta) # UPDATE UNSELECTABLE UNITS SNAPSHOTS self.unselectable = self.bot.units.tags_in(self.unselectable.tags) to_remove = [] for unit in self.unselectable: self.bot._client.debug_text_world( f'unselectable', Point3( (unit.position.x, unit.position.y, 10)), None, 12) if unit.is_idle or unit.is_gathering or not unit.is_visible: to_remove.append(unit.tag) self.unselectable = self.unselectable.tags_not_in(set(to_remove)) self.spread_overlords = self.bot.units.tags_in( self.spread_overlords.tags) for overlord in self.spread_overlords: self.bot._client.debug_text_world( f'spread', Point3((overlord.position.x, overlord.position.y, 10)), None, 12) groups_start_time = time.time() # ARMY GROUPS groups: List[Units] = self.group_army( army_units.tags_not_in(self.unselectable.tags)) for group in groups: nearby_enemies = None if observed_enemy_army.exists: closest_enemy = observed_enemy_army.closest_to(group.center) if closest_enemy.distance_to(group.center) < 15: nearby_enemies: Units = observed_enemy_army.closer_than( 15, closest_enemy) enemy_value = self.bot.calculate_combat_value( nearby_enemies.ready) group_value = self.bot.calculate_combat_value(group) if nearby_enemies and nearby_enemies.exists: bias = 1 if nearby_enemies.closer_than( 15, self.bot.own_natural).exists and group_value > 750: bias = 1.2 if self.bot.supply_used > 180: bias = 1.5 should_engage: bool = self.evaluate_engagement( self.bot.units.exclude_type({DRONE, OVERLORD}).closer_than( 20, nearby_enemies.center), nearby_enemies, bias) > 0 if should_engage: # attack enemy group # ling micro microing_back_tags: List[int] = [] if nearby_enemies(LING).exists: for unit in group(LING): local_enemies: Units = nearby_enemies.closer_than( 3, unit.position) local_allies: Units = group.closer_than( 3, unit.position) # TODO: use attack range instead of proximity... (if enemies cant attack they arent a threat) if (self.bot.calculate_combat_value(local_enemies) > self.bot.calculate_combat_value( local_allies)): target = unit.position + 5 * local_enemies.center.direction_vector( group.center) actions.append(unit.move(target)) microing_back_tags.append(unit.tag) self.bot._client.debug_text_world( f'micro point', Point3((target.x, target.y, 10)), None, 12) self.bot._client.debug_text_world( f'microing back', Point3((unit.position.x, unit.position.y, 10)), None, 12) if nearby_enemies.exclude_type({ UnitTypeId.CHANGELINGZERGLING, UnitTypeId.CHANGELING, UnitTypeId.CHANGELINGZERGLINGWINGS }).exists: actions.extend( self.command_group( group.tags_not_in(set(microing_back_tags)), AbilityId.ATTACK, nearby_enemies.center)) else: actions.extend( self.command_group( group, AbilityId.ATTACK, nearby_enemies.closest_to(group.center))) self.bot._client.debug_text_world( f'attacking group', Point3((group.center.x, group.center.y, 10)), None, 12) else: # retreat somewhwere mins = self.bot.get_mineral_fields_for_expansion( self.bot.closest_mining_expansion_location( group.center).position) if mins.exists: move_position = mins.center else: move_position = self.bot.start_location if group.center.distance_to(move_position) < 5: # Last resort attack with everything everything: Units = group if enemy_value > 150: everything = self.bot.units.closer_than( 15, group.center) self.unselectable.extend(everything) everything = everything + self.bot.units(QUEEN) actions.extend( self.command_group(everything, AbilityId.ATTACK, nearby_enemies.center)) self.bot._client.debug_text_world( f'last resort', Point3((group.center.x, group.center.y, 10)), None, 12) else: # TODO: dont retreat if too close to enemy actions.extend( self.command_group(group, AbilityId.MOVE, move_position)) self.bot._client.debug_text_world( f'retreating', Point3((group.center.x, group.center.y, 10)), None, 12) else: if group_value > 1.2 * estimated_enemy_value or self.bot.supply_used >= 180: # attack toward closest enemy buildings attack_position = self.bot.enemy_start_locations[0] observed_structures = self.scouting_manager.observed_enemy_units.structure if observed_structures.exists: attack_position = observed_structures.closest_to( group.center).position if self.scouting_manager.observed_enemy_units.exists: target_enemy_units: Units = self.scouting_manager.observed_enemy_units.filter( lambda u: u.can_attack_ground) if target_enemy_units.exists: attack_position = target_enemy_units.closest_to( group.center).position actions.extend( self.command_group(group, AbilityId.ATTACK, attack_position)) self.bot._client.debug_text_world( f'attacking base', Point3((group.center.x, group.center.y, 10)), None, 12) else: # merge other_units: Units = all_army.tags_not_in( group.tags.union(self.unselectable.tags)) if other_units.exists: closest_other_unit: Unit = other_units.closest_to( group.center) actions.extend( self.command_group(group, AbilityId.MOVE, closest_other_unit.position)) self.bot._client.debug_text_world( f'merging', Point3((group.center.x, group.center.y, 10)), None, 12) else: self.bot._client.debug_text_world( f'idle', Point3((group.center.x, group.center.y, 10)), None, 12) execution_time = (time.time() - groups_start_time) * 1000 print(f'//// Groups: {round(execution_time, 3)}ms') # DRONE DEFENSE for expansion in self.bot.owned_expansions: enemy_raid: Units = observed_enemy_army.closer_than(15, expansion) if enemy_raid.exists: raid_value = self.bot.calculate_combat_value(enemy_raid) defending_army: Units = self.bot.units.closer_than( 15, expansion) if raid_value > self.bot.calculate_combat_value( defending_army.exclude_type({DRONE})): for defender in self.bot.units(DRONE).closer_than( 10, expansion).tags_not_in(self.unselectable.tags): pos = defender.position if expansion != self.bot.start_location: self.bot._client.debug_text_world( f'mineral walking', Point3((pos.x, pos.y, 10)), None, 12) actions.append( defender.gather(self.bot.main_minerals.random)) else: # pull drones vs harass if enemy_raid.closer_than( 5, defender.position ).exists and not enemy_raid.of_type({ DRONE, UnitTypeId.PROBE, UnitTypeId.SCV }).exists: self.bot._client.debug_text_world( f'pull the bois', Point3( (pos.x, pos.y, 10)), None, 12) actions.append( defender.attack(enemy_raid.center)) # counter worker rush elif enemy_raid.of_type( {DRONE, UnitTypeId.SCV, UnitTypeId.PROBE}).exists: if raid_value > 90: self.bot._client.debug_text_world( f'defend worker rush', Point3((pos.x, pos.y, 10)), None, 12) actions.append( defender.attack(enemy_raid.center)) # DEFEND CANNON RUSH AND OTHER STUFF WITH DRONES for expansion in self.bot.owned_expansions: enemy_scouting_workers = self.bot.known_enemy_units( {DRONE, UnitTypeId.PROBE, UnitTypeId.SCV}).closer_than( 20, expansion).tags_not_in(self.unselectable_enemy_units.tags) enemy_proxies = self.bot.known_enemy_structures.closer_than( 20, expansion).tags_not_in(self.unselectable_enemy_units.tags) if enemy_proxies.exists: for proxy in enemy_proxies: if proxy.type_id == UnitTypeId.PHOTONCANNON: for drone in self.bot.units(DRONE).tags_not_in( self.unselectable.tags).take(4, False): actions.append(drone.attack(proxy)) self.unselectable.append(drone) self.unselectable_enemy_units.append(proxy) if enemy_scouting_workers.exists: for enemy_worker in enemy_scouting_workers: own_workers: Units = self.bot.units(DRONE).tags_not_in( self.unselectable.tags) if own_workers.exists: own_worker: Unit = own_workers.closest_to(enemy_worker) actions.append(own_worker.attack(enemy_worker)) self.chasing_workers.append(own_worker) self.unselectable.append(own_worker) self.unselectable_enemy_units.append(enemy_worker) # send back drones that are chasing workers self.chasing_workers = self.bot.units.tags_in( self.chasing_workers.tags) for drone in self.chasing_workers(DRONE): if self.bot.units(HATCHERY).closest_to( drone.position).position.distance_to(drone.position) > 25: if isinstance(drone.order_target, int): self.unselectable_enemy_units = self.unselectable_enemy_units.tags_not_in( {drone.order_target}) self.chasing_workers = self.chasing_workers.tags_not_in( {drone.tag}) actions.append(drone.gather(self.bot.main_minerals.random)) extra_queen_start_time = time.time() # EXTRA QUEEN CONTROL extra_queens = self.bot.units(QUEEN).tags_not_in( self.unselectable.tags) # if there's a fight contribute otherwise make creep tumors if extra_queens.exists: if self.bot.known_enemy_units.exists and self.bot.units.closer_than( 20, extra_queens.center).tags_not_in( extra_queens.tags).filter( lambda u: u.is_attacking ).exists and self.bot.known_enemy_units.closer_than( 20, extra_queens.center).exists: actions.extend( self.command_group( extra_queens, AbilityId.ATTACK, self.bot.known_enemy_units.closest_to( extra_queens.center).position)) self.bot._client.debug_text_world( f'queen attack', Point3((extra_queens.center.x, extra_queens.center.y, 10)), None, 12) else: for queen in extra_queens.tags_not_in(self.inject_queens.tags): if queen.is_idle: abilities = await self.bot.get_available_abilities( queen) position = await self.bot.find_tumor_placement() if AbilityId.BUILD_CREEPTUMOR_QUEEN in abilities and position and self.bot.has_creep( position): actions.append( queen(AbilityId.BUILD_CREEPTUMOR, position)) self.unselectable.append(queen) else: if queen.position.distance_to( extra_queens.center) > 2: # regroup extra queens actions.append(queen.move(extra_queens.center)) execution_time = (time.time() - extra_queen_start_time) * 1000 print(f'//// Extra queens: {round(execution_time, 3)}ms') creep_start_time = time.time() # CREEP TUMORS for tumor in self.bot.units(UnitTypeId.CREEPTUMORBURROWED).tags_not_in( self.dead_tumors.tags): # TODO: direct creep spread to some direction... # Dont overmake creep xd abilities = await self.bot.get_available_abilities(tumor) if AbilityId.BUILD_CREEPTUMOR_TUMOR in abilities: angle = random.randint(0, 360) x = math.cos(angle) y = math.sin(angle) position: Point2 = tumor.position + (9 * Point2((x, y))) if self.bot.has_creep(position) and not self.bot.units( UnitTypeId.CREEPTUMORBURROWED).closer_than( 9, position ).exists and not self.bot.position_blocks_expansion( position): actions.append(tumor(AbilityId.BUILD_CREEPTUMOR, position)) self.dead_tumors.append(tumor) execution_time = (time.time() - creep_start_time) * 1000 print(f'//// Creep: {round(execution_time, 3)}ms') # OVERLORD retreat from enemy structures and anti air stuff for overlord in self.bot.units(OVERLORD).tags_not_in( self.unselectable.tags): threats: Units = self.bot.known_enemy_units.filter( lambda u: u.is_structure or u.can_attack_air).closer_than( 15, overlord) if threats.exists: destination: Point2 = overlord.position + 2 * threats.center.direction_vector( overlord.position) actions.append(overlord.move(destination)) # OVERSEERS overseers: Units = self.bot.units(OVERSEER) if overseers.exists: for overseer in overseers: if self.spread_overlords.find_by_tag(overseer.tag): self.spread_overlords = self.spread_overlords.tags_not_in( {overseer.tag}) abilities = await self.bot.get_available_abilities(overseer) if AbilityId.SPAWNCHANGELING_SPAWNCHANGELING in abilities: actions.append( overseer(AbilityId.SPAWNCHANGELING_SPAWNCHANGELING)) # CHANGELINGS changelings: Units = self.bot.units(CHANGELING).tags_not_in( self.unselectable.tags) if changelings.exists: for changeling in changelings: actions.append(changeling.move(self.bot.enemy_natural)) self.unselectable.append(changeling) return actions def one_of_targets_in_range(self, unit: Unit, targets: Units): for target in targets: if unit.target_in_range(target): return True return False def group_army(self, army: Units) -> List[Units]: groups: List[Units] = [] already_grouped_tags = [] for unit in army: if unit.tag in already_grouped_tags: continue # TODO: fix recursive grouping # neighbors: Units = self.find_neighbors(unit, army.tags_not_in(set(already_grouped_tags))) neighbors: Units = army.closer_than(15, unit.position) groups.append(neighbors) already_grouped_tags.extend(neighbors.tags) return groups def find_neighbors(self, THE_SOURCE: Unit, units: Units) -> Units: neighbors: Units = units.closer_than(3, THE_SOURCE.position) temp: Units = Units([], self.bot._game_data) for individual in neighbors: temp.__or__( self.find_neighbors(individual, units.tags_not_in(neighbors.tags))) output = neighbors.__or__(temp) if output is None: return Units([], self.bot._game_data) return neighbors.__or__(temp) def command_group(self, units: Units, command: UnitCommand, target: Union[Unit, Point2]): commands = [] for unit in units: commands.append(unit(command, target)) return commands async def inject(self): ready_queens = [] actions = [] for queen in self.bot.units(QUEEN).idle: abilities = await self.bot.get_available_abilities(queen) if AbilityId.EFFECT_INJECTLARVA in abilities: ready_queens.append(queen) for queen in ready_queens: actions.append( queen(AbilityId.EFFECT_INJECTLARVA, self.bot.units(HATCHERY).first)) return actions def evaluate_engagement(self, own_units: Units, enemy_units: Units, bias=1): own_ranged: Units = own_units.filter(lambda u: u.ground_range > 3) own_melee: Units = own_units.tags_not_in(own_ranged.tags) enemy_ranged: Units = enemy_units.filter(lambda u: u.ground_range > 3) try: own_ranged_value = bias * self.bot.calculate_combat_value( own_ranged) except: own_ranged_value = 0 try: enemy_ranged_value = self.bot.calculate_combat_value(enemy_ranged) except: enemy_ranged_value = 0 corrected_own_value = bias * self.bot.calculate_combat_value(own_units) if own_ranged_value < enemy_ranged_value and own_units.exists: perimeter = self.get_enemy_perimeter( enemy_units.not_structure, self.bot.known_enemy_structures, own_units.center) if own_melee.exists: own_melee_value = bias * self.bot.calculate_combat_value( Units(own_melee.take(perimeter * 2, require_all=False), self.bot._game_data)) else: own_melee_value = 0 corrected_own_value = own_melee_value + own_ranged_value evaluation = corrected_own_value - self.bot.calculate_combat_value( enemy_units) return evaluation def get_enemy_perimeter(self, enemy_units: Units, enemy_structures: Units, reference_position: Point2): perimeter = 0 pathing_grid: PixelMap = self.bot._game_info.pathing_grid for enemy_unit in enemy_units: enemies_excluding_self: Units = enemy_units.tags_not_in( {enemy_unit.tag}) pos: Point2 = enemy_unit.position positions = [ Point2((pos.x - 1, pos.y + 1)), Point2((pos.x, pos.y + 1)), Point2((pos.x + 1, pos.y + 1)), Point2((pos.x - 1, pos.y)), # [pos.x, pos.y], disregard center point Point2((pos.x + 1, pos.y)), Point2((pos.x - 1, pos.y - 1)), Point2((pos.x, pos.y - 1)), Point2((pos.x + 1, pos.y - 1)), ] if reference_position.distance_to(enemy_unit.position) > 5: positions = remove_n_furthest_points(positions, reference_position, 3) for p in positions: if pathing_grid[ math.floor(p.x), math.floor( p.y )] <= 0 and not enemies_excluding_self.closer_than( 1, p).exists and not enemy_structures.closer_than( 1, p).exists: perimeter += 1 return perimeter
async def start(self, knowledge: 'Knowledge'): await super().start(knowledge) self.empty_units: Units = Units([], self.ai)
def test_tags_not_in(self): self.assertEqual(self.marines.tags_not_in({}), self.marines) self.assertEqual( self.marines.tags_not_in({245346374}), Units([self.marine2, self.marine3], self.mock_game_state))
def test_bot_ai(): bot: BotAI = random_bot_object # Test initial bot attributes at game start # Properties from _prepare_start assert 1 <= bot.player_id <= 2 assert isinstance(bot.race, Race) assert isinstance(bot.enemy_race, Race) # Properties from _prepare_step assert bot.units.amount == bot.workers.amount assert bot.structures.amount == bot.townhalls.amount assert bot.workers.amount == 12 assert bot.townhalls.amount == 1 assert bot.gas_buildings.amount == 0 assert bot.minerals == 50 assert bot.vespene == 0 assert bot.supply_army == 0 assert bot.supply_workers == 12 assert bot.supply_cap == 15 assert bot.supply_used == 12 assert bot.supply_left == 3 assert bot.idle_worker_count == 0 assert bot.army_count == 0 # Test properties updated by "_prepare_units" function assert not bot.blips assert bot.units assert bot.structures assert not bot.enemy_units assert not bot.enemy_structures assert bot.mineral_field assert bot.vespene_geyser assert bot.resources assert len(bot.destructables) >= 0 assert isinstance(bot.destructables, (list, set, dict)) assert len(bot.watchtowers) >= 0 assert bot.all_units assert bot.workers assert bot.townhalls assert not bot.gas_buildings # Test bot_ai functions assert bot.time == 0 assert bot.time_formatted in {"0:00", "00:00"} assert bot.start_location is None # Is populated by main.py bot._game_info.player_start_location = bot.townhalls.random.position assert bot.townhalls.random.position not in bot.enemy_start_locations assert bot.enemy_units == Units([], bot) assert bot.enemy_structures == Units([], bot) bot._game_info.map_ramps, bot._game_info.vision_blockers = bot._game_info._find_ramps_and_vision_blockers( ) assert bot.main_base_ramp # Test if any ramp was found # Clear cache for expansion locations, recalculate and time it if hasattr(bot, "_cache_expansion_locations"): delattr(bot, "_cache_expansion_locations") t0 = time.perf_counter() bot.expansion_locations t1 = time.perf_counter() print(f"Time to calculate expansion locations: {t1-t0} s") # TODO: Cache all expansion positions for a map and check if it is the same assert len( bot.expansion_locations ) >= 10, f"Too few expansions found: {len(bot.expansion_locations)}" # Honorgrounds LE has 24 assert len( bot.expansion_locations ) <= 24, f"Too many expansions found: {len(bot.expansion_locations)}" # On N player maps, it is expected that there are N*X bases because of symmetry, at least for 1vs1 maps assert (len(bot.expansion_locations) % (len(bot.enemy_start_locations) + 1) == 0), f"{set(bot.expansion_locations.keys())}" # Test if bot start location is in expansion locations assert bot.townhalls.random.position in set( bot.expansion_locations.keys() ), f'This error might occur if you are running the tests locally using command "pytest test/", possibly because you are using an outdated cache.py version, but it should not occur when using docker and pipenv.\n{bot.townhalls.random.position}, {set(bot.expansion_locations.keys())}' # Test if enemy start locations are in expansion locations for location in bot.enemy_start_locations: assert location in set(bot.expansion_locations.keys( )), f"{location}, {bot.expansion_locations.keys()}" # The following functions need to be tested by autotest_bot.py because they use API query which isn't available here as this file only uses the pickle files and is not able to interact with the API as SC2 is not running while this test runs assert bot.owned_expansions == { bot.townhalls.first.position: bot.townhalls.first } assert bot.can_feed(UnitTypeId.MARINE) assert bot.can_feed(UnitTypeId.SIEGETANK) assert not bot.can_feed(UnitTypeId.THOR) assert not bot.can_feed(UnitTypeId.BATTLECRUISER) assert not bot.can_feed(UnitTypeId.IMMORTAL) assert bot.can_afford(UnitTypeId.ZERGLING) assert bot.can_afford(UnitTypeId.MARINE) assert bot.can_afford(UnitTypeId.SCV) assert bot.can_afford(UnitTypeId.DRONE) assert bot.can_afford(UnitTypeId.PROBE) assert bot.can_afford(AbilityId.COMMANDCENTERTRAIN_SCV) assert bot.can_afford(UnitTypeId.MARINE) assert not bot.can_afford(UnitTypeId.SIEGETANK) assert not bot.can_afford(UnitTypeId.BATTLECRUISER) assert not bot.can_afford(UnitTypeId.MARAUDER) assert not bot.can_afford(UpgradeId.WARPGATERESEARCH) assert not bot.can_afford(AbilityId.RESEARCH_WARPGATE) # Store old values for minerals, vespene old_values = bot.minerals, bot.vespene, bot.supply_cap, bot.supply_left, bot.supply_used bot.vespene = 50 assert bot.can_afford(UpgradeId.WARPGATERESEARCH) assert bot.can_afford(AbilityId.RESEARCH_WARPGATE) bot.minerals = 150 bot.supply_cap = 15 bot.supply_left = -1 bot.supply_used = 16 # Confirm that units that don't cost supply can be built while at negative supply using can_afford function assert bot.can_afford(UnitTypeId.GATEWAY) assert bot.can_afford(UnitTypeId.PYLON) assert bot.can_afford(UnitTypeId.OVERLORD) assert bot.can_afford(UnitTypeId.BANELING) assert not bot.can_afford(UnitTypeId.ZERGLING) assert not bot.can_afford(UnitTypeId.MARINE) bot.minerals, bot.vespene, bot.supply_cap, bot.supply_left, bot.supply_used = old_values worker = bot.workers.random assert bot.select_build_worker(worker.position) == worker for w in bot.workers: if w == worker: continue assert bot.select_build_worker(w.position) != worker assert bot.already_pending_upgrade(UpgradeId.STIMPACK) == 0 assert bot.already_pending(UpgradeId.STIMPACK) == 0 assert bot.already_pending(UnitTypeId.SCV) == 0 assert 0 < bot.get_terrain_height(worker) assert bot.in_placement_grid(worker) assert bot.in_pathing_grid(worker) # The pickle data was created by a terran bot, so there is no creep under any worker assert not bot.has_creep(worker) # Why did this stop working, not visible on first frame? assert bot.is_visible( worker ), f"Visibility value at worker is {bot.state.visibility[worker.position.rounded]}" # Check price for morphing units and upgrades cost_100 = [ AbilityId.ARMORYRESEARCH_TERRANSHIPWEAPONSLEVEL1, UpgradeId.TERRANSHIPWEAPONSLEVEL1, AbilityId.ARMORYRESEARCH_TERRANVEHICLEPLATINGLEVEL1, UpgradeId.TERRANVEHICLEARMORSLEVEL1, AbilityId.ARMORYRESEARCH_TERRANVEHICLEWEAPONSLEVEL1, UpgradeId.TERRANVEHICLEWEAPONSLEVEL1, AbilityId.ENGINEERINGBAYRESEARCH_TERRANINFANTRYARMORLEVEL1, UpgradeId.TERRANINFANTRYARMORSLEVEL1, AbilityId.ENGINEERINGBAYRESEARCH_TERRANINFANTRYWEAPONSLEVEL1, UpgradeId.TERRANINFANTRYWEAPONSLEVEL1, AbilityId.RESEARCH_ZERGMELEEWEAPONSLEVEL1, UpgradeId.ZERGMELEEWEAPONSLEVEL1, AbilityId.RESEARCH_ZERGMISSILEWEAPONSLEVEL1, UpgradeId.ZERGMISSILEWEAPONSLEVEL1, AbilityId.FORGERESEARCH_PROTOSSGROUNDARMORLEVEL1, UpgradeId.PROTOSSGROUNDARMORSLEVEL1, AbilityId.FORGERESEARCH_PROTOSSGROUNDWEAPONSLEVEL1, UpgradeId.PROTOSSGROUNDWEAPONSLEVEL1, AbilityId.ENGINEERINGBAYRESEARCH_TERRANINFANTRYWEAPONSLEVEL1, UpgradeId.TERRANINFANTRYWEAPONSLEVEL1, AbilityId.ENGINEERINGBAYRESEARCH_TERRANINFANTRYWEAPONSLEVEL1, UpgradeId.TERRANINFANTRYWEAPONSLEVEL1, AbilityId.RESEARCH_ZERGFLYERATTACKLEVEL1, UpgradeId.ZERGFLYERWEAPONSLEVEL1, AbilityId.CYBERNETICSCORERESEARCH_PROTOSSAIRWEAPONSLEVEL1, UpgradeId.PROTOSSAIRWEAPONSLEVEL1, ] cost_175 = [ AbilityId.ARMORYRESEARCH_TERRANSHIPWEAPONSLEVEL2, UpgradeId.TERRANSHIPWEAPONSLEVEL2, AbilityId.ARMORYRESEARCH_TERRANVEHICLEPLATINGLEVEL2, UpgradeId.TERRANVEHICLEARMORSLEVEL2, AbilityId.ARMORYRESEARCH_TERRANVEHICLEWEAPONSLEVEL2, UpgradeId.TERRANVEHICLEWEAPONSLEVEL2, AbilityId.ENGINEERINGBAYRESEARCH_TERRANINFANTRYARMORLEVEL2, UpgradeId.TERRANINFANTRYARMORSLEVEL2, AbilityId.ENGINEERINGBAYRESEARCH_TERRANINFANTRYWEAPONSLEVEL2, UpgradeId.TERRANINFANTRYWEAPONSLEVEL2, AbilityId.ENGINEERINGBAYRESEARCH_TERRANINFANTRYWEAPONSLEVEL2, UpgradeId.TERRANINFANTRYWEAPONSLEVEL2, AbilityId.ENGINEERINGBAYRESEARCH_TERRANINFANTRYWEAPONSLEVEL2, UpgradeId.TERRANINFANTRYWEAPONSLEVEL2, AbilityId.RESEARCH_ZERGFLYERATTACKLEVEL2, UpgradeId.ZERGFLYERWEAPONSLEVEL2, AbilityId.CYBERNETICSCORERESEARCH_PROTOSSAIRWEAPONSLEVEL2, UpgradeId.PROTOSSAIRWEAPONSLEVEL2, ] cost_200 = [ AbilityId.RESEARCH_ZERGMELEEWEAPONSLEVEL3, UpgradeId.ZERGMELEEWEAPONSLEVEL3, AbilityId.RESEARCH_ZERGMISSILEWEAPONSLEVEL3, UpgradeId.ZERGMISSILEWEAPONSLEVEL3, AbilityId.FORGERESEARCH_PROTOSSGROUNDARMORLEVEL3, UpgradeId.PROTOSSGROUNDARMORSLEVEL3, AbilityId.FORGERESEARCH_PROTOSSGROUNDWEAPONSLEVEL3, UpgradeId.PROTOSSGROUNDWEAPONSLEVEL3, ] cost_250 = [ AbilityId.ARMORYRESEARCH_TERRANSHIPWEAPONSLEVEL3, UpgradeId.TERRANSHIPWEAPONSLEVEL3, AbilityId.ARMORYRESEARCH_TERRANVEHICLEPLATINGLEVEL3, UpgradeId.TERRANVEHICLEARMORSLEVEL3, AbilityId.ARMORYRESEARCH_TERRANVEHICLEWEAPONSLEVEL3, UpgradeId.TERRANVEHICLEWEAPONSLEVEL3, AbilityId.ENGINEERINGBAYRESEARCH_TERRANINFANTRYARMORLEVEL3, UpgradeId.TERRANINFANTRYARMORSLEVEL3, AbilityId.ENGINEERINGBAYRESEARCH_TERRANINFANTRYWEAPONSLEVEL3, UpgradeId.TERRANINFANTRYWEAPONSLEVEL3, AbilityId.ENGINEERINGBAYRESEARCH_TERRANINFANTRYWEAPONSLEVEL3, UpgradeId.TERRANINFANTRYWEAPONSLEVEL3, AbilityId.ENGINEERINGBAYRESEARCH_TERRANINFANTRYWEAPONSLEVEL3, UpgradeId.TERRANINFANTRYWEAPONSLEVEL3, AbilityId.RESEARCH_ZERGFLYERATTACKLEVEL3, UpgradeId.ZERGFLYERWEAPONSLEVEL3, AbilityId.CYBERNETICSCORERESEARCH_PROTOSSAIRWEAPONSLEVEL3, UpgradeId.PROTOSSAIRWEAPONSLEVEL3, ] cost_150 = [ AbilityId.RESEARCH_ZERGGROUNDARMORLEVEL1, UpgradeId.ZERGGROUNDARMORSLEVEL1, AbilityId.FORGERESEARCH_PROTOSSSHIELDSLEVEL1, UpgradeId.PROTOSSSHIELDSLEVEL1, AbilityId.RESEARCH_ZERGFLYERARMORLEVEL1, UpgradeId.ZERGFLYERARMORSLEVEL1, AbilityId.FORGERESEARCH_PROTOSSSHIELDSLEVEL1, UpgradeId.PROTOSSSHIELDSLEVEL1, AbilityId.RESEARCH_ZERGMELEEWEAPONSLEVEL2, UpgradeId.ZERGMELEEWEAPONSLEVEL2, AbilityId.RESEARCH_ZERGMISSILEWEAPONSLEVEL2, UpgradeId.ZERGMISSILEWEAPONSLEVEL2, AbilityId.FORGERESEARCH_PROTOSSGROUNDARMORLEVEL2, UpgradeId.PROTOSSGROUNDARMORSLEVEL2, AbilityId.FORGERESEARCH_PROTOSSGROUNDWEAPONSLEVEL2, UpgradeId.PROTOSSGROUNDWEAPONSLEVEL2, ] cost_225 = [ AbilityId.FORGERESEARCH_PROTOSSSHIELDSLEVEL2, UpgradeId.PROTOSSSHIELDSLEVEL2, AbilityId.FORGERESEARCH_PROTOSSSHIELDSLEVEL2, UpgradeId.PROTOSSSHIELDSLEVEL2, AbilityId.RESEARCH_ZERGGROUNDARMORLEVEL2, UpgradeId.ZERGGROUNDARMORSLEVEL2, AbilityId.RESEARCH_ZERGFLYERARMORLEVEL2, UpgradeId.ZERGFLYERARMORSLEVEL2, ] cost_300 = [ AbilityId.RESEARCH_ZERGGROUNDARMORLEVEL3, UpgradeId.ZERGGROUNDARMORSLEVEL3, AbilityId.FORGERESEARCH_PROTOSSSHIELDSLEVEL3, UpgradeId.PROTOSSSHIELDSLEVEL3, AbilityId.RESEARCH_ZERGFLYERARMORLEVEL3, UpgradeId.ZERGFLYERARMORSLEVEL3, AbilityId.FORGERESEARCH_PROTOSSSHIELDSLEVEL3, UpgradeId.PROTOSSSHIELDSLEVEL3, ] cost_list = [100, 175, 200, 250, 150, 225, 300] def calc_cost(item_id) -> Cost: if isinstance(item_id, AbilityId): return bot.game_data.calculate_ability_cost(item_id) elif isinstance(item_id, UpgradeId): return bot.game_data.upgrades[item_id.value].cost elif isinstance(item_id, UnitTypeId): creation_ability: AbilityId = bot.game_data.units[ item_id.value].creation_ability return bot.game_data.calculate_ability_cost(creation_ability) def assert_cost(item_id, real_cost: Cost): assert calc_cost( item_id ) == real_cost, f"Cost of {item_id} should be {real_cost} but is {calc_cost(item_id)}" for items, cost in zip( [cost_100, cost_175, cost_200, cost_250, cost_150, cost_225, cost_300], cost_list): real_cost2: Cost = Cost(cost, cost) for item in items: assert_cost(item, real_cost2) assert ( bot.calculate_cost(item) == real_cost2 ), f"Cost of {item} should be {real_cost2} but is {calc_cost(item)}" # Do not use the generic research abilities in the bot when testing if you can afford it as these are wrong assert_cost(AbilityId.RESEARCH_ZERGFLYERARMOR, Cost(0, 0)) assert_cost(AbilityId.RESEARCH_ZERGFLYERATTACK, Cost(0, 0)) assert_cost(AbilityId.RESEARCH_ZERGGROUNDARMOR, Cost(0, 0)) assert_cost(AbilityId.RESEARCH_ZERGMELEEWEAPONS, Cost(0, 0)) assert_cost(AbilityId.RESEARCH_ZERGMISSILEWEAPONS, Cost(0, 0)) assert_cost(AbilityId.RESEARCH_TERRANINFANTRYARMOR, Cost(0, 0)) assert_cost(AbilityId.RESEARCH_TERRANINFANTRYWEAPONS, Cost(0, 0)) assert_cost(AbilityId.RESEARCH_PROTOSSGROUNDARMOR, Cost(0, 0)) assert_cost(AbilityId.RESEARCH_PROTOSSGROUNDWEAPONS, Cost(0, 0)) assert_cost(AbilityId.RESEARCH_PROTOSSSHIELDS, Cost(0, 0)) assert_cost(AbilityId.RESEARCH_TERRANSHIPWEAPONS, Cost(0, 0)) assert_cost(AbilityId.RESEARCH_TERRANVEHICLEWEAPONS, Cost(0, 0)) # Somehow this is 0, returned by the API assert_cost(AbilityId.BUILD_REACTOR, Cost(0, 0)) # UnitTypeId.REACTOR has no creation ability (None) # assert_cost(UnitTypeId.REACTOR, Cost(50, 50)) assert_cost(AbilityId.BUILD_REACTOR_BARRACKS, Cost(50, 50)) assert_cost(UnitTypeId.BARRACKSREACTOR, Cost(50, 50)) assert_cost(AbilityId.UPGRADETOORBITAL_ORBITALCOMMAND, Cost(150, 0)) assert_cost(UnitTypeId.ORBITALCOMMAND, Cost(150, 0)) assert_cost(AbilityId.UPGRADETOORBITAL_ORBITALCOMMAND, Cost(150, 0)) assert bot.calculate_unit_value(UnitTypeId.ORBITALCOMMAND) == Cost(550, 0) assert bot.calculate_unit_value(UnitTypeId.RAVAGER) == Cost(100, 100) assert bot.calculate_unit_value(UnitTypeId.ARCHON) == Cost(175, 275) assert bot.calculate_unit_value(UnitTypeId.ADEPTPHASESHIFT) == Cost(0, 0) assert bot.calculate_unit_value(UnitTypeId.AUTOTURRET) == Cost(100, 0) assert bot.calculate_unit_value(UnitTypeId.INFESTORTERRAN) == Cost(0, 0) assert bot.calculate_unit_value(UnitTypeId.INFESTORTERRANBURROWED) == Cost( 0, 0) assert bot.calculate_unit_value(UnitTypeId.LARVA) == Cost(0, 0) assert bot.calculate_unit_value(UnitTypeId.EGG) == Cost(0, 0) assert bot.calculate_unit_value(UnitTypeId.LOCUSTMP) == Cost(0, 0) assert bot.calculate_unit_value(UnitTypeId.LOCUSTMPFLYING) == Cost(0, 0) assert bot.calculate_unit_value(UnitTypeId.BROODLING) == Cost(0, 0) # Other and effects assert bot.calculate_unit_value(UnitTypeId.KD8CHARGE) == Cost(0, 0) assert bot.calculate_unit_value( UnitTypeId.RAVAGERCORROSIVEBILEMISSILE) == Cost(0, 0) assert bot.calculate_unit_value(UnitTypeId.VIPERACGLUESCREENDUMMY) == Cost( 0, 0) assert bot.calculate_cost(UnitTypeId.BROODLORD) == Cost(150, 150) assert bot.calculate_cost(UnitTypeId.RAVAGER) == Cost(25, 75) assert bot.calculate_cost(UnitTypeId.BANELING) == Cost(25, 25) assert bot.calculate_cost(UnitTypeId.ORBITALCOMMAND) == Cost(150, 0) assert bot.calculate_cost( AbilityId.UPGRADETOORBITAL_ORBITALCOMMAND) == Cost(150, 0) assert bot.calculate_cost(UnitTypeId.REACTOR) == Cost(50, 50) assert bot.calculate_cost(UnitTypeId.TECHLAB) == Cost(50, 25) assert bot.calculate_cost(UnitTypeId.QUEEN) == Cost(150, 0) assert bot.calculate_cost(UnitTypeId.HATCHERY) == Cost(300, 0) assert bot.calculate_cost(UnitTypeId.LAIR) == Cost(150, 100) assert bot.calculate_cost(UnitTypeId.HIVE) == Cost(200, 150) assert bot.calculate_cost(UnitTypeId.DRONE) == Cost(50, 0) assert bot.calculate_cost(UnitTypeId.SCV) == Cost(50, 0) assert bot.calculate_cost(UnitTypeId.PROBE) == Cost(50, 0) assert bot.calculate_cost(UnitTypeId.SPIRE) == Cost(200, 200) assert bot.calculate_cost(UnitTypeId.ARCHON) == bot.calculate_unit_value( UnitTypeId.ARCHON) # The following are morph abilities that may need a fix assert_cost(AbilityId.MORPHTOBROODLORD_BROODLORD, Cost(300, 250)) assert_cost(UnitTypeId.BROODLORD, Cost(300, 250)) assert_cost(AbilityId.MORPHTORAVAGER_RAVAGER, Cost(100, 100)) assert_cost(AbilityId.MORPHTOBROODLORD_BROODLORD, Cost(300, 250)) assert_cost(AbilityId.MORPHZERGLINGTOBANELING_BANELING, Cost(50, 25)) assert Cost(100, 50) == 2 * Cost(50, 25) assert Cost(100, 50) == Cost(50, 25) * 2 assert bot.calculate_supply_cost(UnitTypeId.BARRACKS) == 0 assert bot.calculate_supply_cost(UnitTypeId.HATCHERY) == 0 assert bot.calculate_supply_cost(UnitTypeId.OVERLORD) == 0 assert bot.calculate_supply_cost(UnitTypeId.ZERGLING) == 1 assert bot.calculate_supply_cost(UnitTypeId.MARINE) == 1 assert bot.calculate_supply_cost(UnitTypeId.BANELING) == 0 assert bot.calculate_supply_cost(UnitTypeId.QUEEN) == 2 assert bot.calculate_supply_cost(UnitTypeId.ROACH) == 2 assert bot.calculate_supply_cost(UnitTypeId.RAVAGER) == 1 assert bot.calculate_supply_cost(UnitTypeId.CORRUPTOR) == 2 assert bot.calculate_supply_cost(UnitTypeId.BROODLORD) == 2 assert bot.calculate_supply_cost(UnitTypeId.HYDRALISK) == 2 assert bot.calculate_supply_cost(UnitTypeId.LURKERMP) == 1
def test_noqueue(self): self.assertEqual(self.marines.noqueue, Units([self.marine3], self.mock_game_state))
def _initialize_variables(self): DistanceCalculation.__init__(self) # Specific opponent bot ID used in sc2ai ladder games http://sc2ai.net/ # The bot ID will stay the same each game so your bot can "adapt" to the opponent self.opponent_id: int = None # This value will be set to True by main.py in self._prepare_start if game is played in realtime (if true, the bot will have limited time per step) self.realtime: bool = False self.all_units: Units = Units([], self) self.units: Units = Units([], self) self.workers: Units = Units([], self) self.townhalls: Units = Units([], self) self.structures: Units = Units([], self) self.gas_buildings: Units = Units([], self) self.enemy_units: Units = Units([], self) self.enemy_structures: Units = Units([], self) self.resources: Units = Units([], self) self.destructables: Units = Units([], self) self.watchtowers: Units = Units([], self) self.mineral_field: Units = Units([], self) self.vespene_geyser: Units = Units([], self) self.larva: Units = Units([], self) self.techlab_tags: Set[int] = set() self.reactor_tags: Set[int] = set() self.minerals: int = None self.vespene: int = None self.supply_army: Union[float, int] = None # Doesn't include workers in production self.supply_workers: Union[float, int] = None self.supply_cap: Union[float, int] = None self.supply_used: Union[float, int] = None self.supply_left: Union[float, int] = None self.idle_worker_count: int = None self.army_count: int = None self.warp_gate_count: int = None self.larva_count: int = None self.actions: List[UnitCommand] = [] self.blips: Set[Blip] = set() self.race: Race = None self.enemy_race: Race = None self._units_created: Counter = Counter() self._unit_tags_seen_this_game: Set[int] = set() self._units_previous_map: Dict[int, Unit] = dict() self._structures_previous_map: Dict[int, Unit] = dict() self._enemy_units_previous_map: Dict[int, Unit] = dict() self._enemy_structures_previous_map: Dict[int, Unit] = dict() self._all_units_previous_map: Dict[int, Unit] = dict() self._previous_upgrades: Set[UpgradeId] = set() self._expansion_positions_list: List[Point2] = [] self._resource_location_to_expansion_position_dict: Dict[Point2, Point2] = {} # Internally used to keep track which units received an action in this frame, so that self.train() function does not give the same larva two orders - cleared every frame self.unit_tags_received_action: Set[int] = set()
def test_idle(self): self.assertEqual(self.marines.idle, Units([self.marine3], self.mock_game_state))
async def update_influence(self): power = ExtendedPower(self.unit_values) self.path_finder_terrain.reset() # Reset self.map.reset() # Reset self.map.enable_colossus_map( self.knowledge.my_race == Race.Protoss and len(self.cache.own(UnitTypeId.COLOSSUS)) > 0) self.map.enable_reaper_map( self.knowledge.my_race == Race.Terran and len(self.cache.own(UnitTypeId.REAPER)) > 0) positions = [] for mf in self.ai.mineral_field: # type: Unit # In 4.8.5+ minerals are no longer visible in pathing grid unless player has vision positions.append(mf.position) self.path_finder_terrain.create_block(positions, (2, 1)) self.map.create_block(positions, (2, 1)) self.set_rocks(self.path_finder_terrain) self.set_rocks(self.map) for building in self.ai.structures + self.knowledge.known_enemy_structures: # type: Unit if building.type_id in buildings_2x2: self.map.create_block(building.position, (2, 2)) elif building.type_id in buildings_3x3: self.map.create_block(building.position, (3, 3)) elif building.type_id in buildings_5x5: self.map.create_block(building.position, (5, 3)) self.map.create_block(building.position, (3, 5)) self.map.normalize_influence(20) for enemy_type in self.cache.enemy_unit_cache: # type: UnitTypeId enemies: Units = self.cache.enemy_unit_cache.get( enemy_type, Units([], self.ai)) if len(enemies) == 0: continue example_enemy: Unit = enemies[0] power.clear() power.add_unit(enemy_type, 100) if self.unit_values.can_shoot_air(example_enemy): positions: List[Point2] = [ unit.position for unit in enemies ] # need to be specified in both places s_range = self.unit_values.air_range(example_enemy) if example_enemy.type_id == UnitTypeId.CYCLONE: s_range = 7 if example_enemy.is_structure: self.map.add_pure_ground_influence(positions, power.air_power, s_range, s_range) else: self.map.add_air_influence(positions, power.air_power, s_range, s_range + 3) if self.unit_values.can_shoot_ground(example_enemy): positions = [unit.position for unit in enemies ] # need to be specified in both places s_range = self.unit_values.ground_range(example_enemy) if example_enemy.type_id == UnitTypeId.CYCLONE: s_range = 7 if example_enemy.type_id == UnitTypeId.SIEGETANKSIEGED: self.map.add_tank_influence(positions, power.ground_power) elif s_range < 2: self.map.add_walk_influence(positions, power.ground_power, 7) elif example_enemy.is_structure: self.map.add_pure_ground_influence(positions, power.ground_power, s_range, s_range) elif s_range < 5: self.map.add_pure_ground_influence(positions, power.ground_power, s_range, 7) else: self.map.add_pure_ground_influence(positions, power.ground_power, s_range, s_range + 3) # influence, radius, points, can it hit air? effect_dict: Dict[EffectId, Tuple[float, float, List[Point2], bool]] = dict() for effect in self.ai.state.effects: values: Tuple[float, float, List[Point2], bool] = None if effect.id == EffectId.RAVAGERCORROSIVEBILECP: values = effect_dict.get(effect.id, (1000, 2.5, [], True)) values[2].append(Point2.center(effect.positions)) elif effect.id == EffectId.BLINDINGCLOUDCP: values = effect_dict.get(effect.id, (400, 3.5, [], False)) values[2].append(Point2.center(effect.positions)) elif effect.id == EffectId.NUKEPERSISTENT: values = effect_dict.get(effect.id, (900, 9, [], True)) values[2].append(Point2.center(effect.positions)) elif effect.id == EffectId.PSISTORMPERSISTENT: values = effect_dict.get(effect.id, (300, 3.5, [], True)) values[2].append(Point2.center(effect.positions)) elif effect.id == EffectId.LIBERATORTARGETMORPHDELAYPERSISTENT: values = effect_dict.get(effect.id, (200, 6, [], False)) values[2].append(Point2.center(effect.positions)) elif effect.id == EffectId.LIBERATORTARGETMORPHPERSISTENT: values = effect_dict.get(effect.id, (300, 6, [], False)) values[2].append(Point2.center(effect.positions)) elif effect.id == EffectId.LURKERMP: # Each lurker spine deals splash damage to a radius of 0.5 values = effect_dict.get(effect.id, (1000, 1, [], False)) values[2].extend(effect.positions) if values is not None and effect.id not in effect_dict: effect_dict[effect.id] = values for effects in effect_dict.values(): if effects[3]: self.map.add_both_influence(effects[2], effects[0], effects[1], effects[1]) else: self.map.add_ground_influence(effects[2], effects[0], effects[1], effects[1])
class TestUnits(unittest.TestCase): # @classmethod # def setUpClass(cls): # pass # @classmethod # def tearDownClass(cls): # pass def setUp(self): mock_proto_data1 = MockProtoData(tag=245346374, pos=Mock(x=-5, y=6, z=50), health=35, health_max=45, orders=[ Mock(ability_id=AbilityId.ATTACK, target_unit_tag=1337, progress=1.0) ]) mock_proto_data2 = MockProtoData( tag=245346375, pos=Mock(x=-2, y=7, z=50), orders=[ Mock(ability_id=AbilityId.MOVE, target_world_space_pos=Point2((0, 0)), progress=1.0) ]) mock_proto_data3 = MockProtoData( tag=245346376, pos=Mock(x=7, y=7, z=50), ) self.mock_game_state = MockGameState() self.marine1 = Unit(mock_proto_data1, self.mock_game_state) self.marine2 = Unit(mock_proto_data2, self.mock_game_state) self.marine3 = Unit(mock_proto_data3, self.mock_game_state) self.marines = Units([self.marine1, self.marine2, self.marine3], self.mock_game_state) self.emptyUnitsGroup = Units([], self.mock_game_state) def tearDown(self): # unnecessary here del self.marine1 del self.marine2 del self.marine3 del self.marines del self.mock_game_state def test_amount(self): self.assertEqual(self.marines.amount, 3) self.assertEqual(self.emptyUnitsGroup.amount, 0) def test_empty(self): self.assertFalse(self.marines.empty) self.assertTrue(self.emptyUnitsGroup.empty) def test_exists(self): self.assertTrue(self.marines.exists) self.assertFalse(self.emptyUnitsGroup.exists) def test_find_by_tag(self): self.assertEqual(self.marines.find_by_tag(245346374), self.marine1) self.assertIsNone(self.marines.find_by_tag(245346)) def test_first(self): self.assertEqual(self.marines.first, self.marine1) def test_random(self): self.assertTrue( self.marines.random in [self.marine1, self.marine2, self.marine3]) def test_closest_distance_to(self): self.assertEqual(self.marines.closest_distance_to(Point2((10, 10))), (3**2 + 3**2)**0.5) def test_closest_to(self): self.assertEqual(self.marines.closest_to(Point2((10, 10))), self.marine3) def test_furthest_to(self): self.assertEqual(self.marines.furthest_to(Point2((10, 10))), self.marine1) def test_closer_than(self): self.assertEqual(self.marines.closer_than(20, Point2((10, 10))), self.marines) self.assertEqual(self.marines.closer_than(6, Point2((10, 10))), Units([self.marine3], self.mock_game_state)) self.assertEqual(self.marines.closer_than(2, Point2((10, 10))), self.emptyUnitsGroup) def test_tags_in(self): self.assertEqual( self.marines.tags_in({245346374, 245346375}), Units([self.marine1, self.marine2], self.mock_game_state)) self.assertEqual(self.marines.tags_in({}), self.emptyUnitsGroup) def test_tags_not_in(self): self.assertEqual(self.marines.tags_not_in({}), self.marines) self.assertEqual( self.marines.tags_not_in({245346374}), Units([self.marine2, self.marine3], self.mock_game_state)) def test_of_type(self): self.assertEqual(self.marines.of_type(UnitTypeId.MARINE), self.marines) self.assertEqual(self.marines.of_type([UnitTypeId.MARINE]), self.marines) def test_exclude_type(self): self.assertEqual(self.marines.exclude_type([UnitTypeId.MARINE]), self.emptyUnitsGroup) def test_tags(self): self.assertSetEqual(self.marines.tags, {u.tag for u in self.marines}) def test_noqueue(self): self.assertEqual(self.marines.noqueue, Units([self.marine3], self.mock_game_state)) def test_idle(self): self.assertEqual(self.marines.idle, Units([self.marine3], self.mock_game_state)) def test_owned(self): self.assertEqual(self.marines.owned, self.marines) def test_enemy(self): self.assertEqual(self.marines.enemy, self.emptyUnitsGroup)
async def distribute_workers(self, performanceHeavy=True, onlySaturateGas=False): mineralTags = [x.tag for x in self.mineral_field] gas_buildingTags = [x.tag for x in self.gas_buildings] workerPool = Units([], self) workerPoolTags = set() # Find all gas_buildings that have surplus or deficit deficit_gas_buildings = {} surplusgas_buildings = {} for g in self.gas_buildings.filter(lambda x: x.vespene_contents > 0): # Only loop over gas_buildings that have still gas in them deficit = g.ideal_harvesters - g.assigned_harvesters if deficit > 0: deficit_gas_buildings[g.tag] = {"unit": g, "deficit": deficit} elif deficit < 0: surplusWorkers = self.workers.closer_than(10, g).filter( lambda w: w not in workerPoolTags and len(w.orders) == 1 and w.orders[0].ability.id in [AbilityId.HARVEST_GATHER] and w.orders[0].target in gas_buildingTags) for i in range(-deficit): if surplusWorkers.amount > 0: w = surplusWorkers.pop() workerPool.append(w) workerPoolTags.add(w.tag) surplusgas_buildings[g.tag] = {"unit": g, "deficit": deficit} # Find all townhalls that have surplus or deficit deficitTownhalls = {} surplusTownhalls = {} if not onlySaturateGas: for th in self.townhalls: deficit = th.ideal_harvesters - th.assigned_harvesters if deficit > 0: deficitTownhalls[th.tag] = {"unit": th, "deficit": deficit} elif deficit < 0: surplusWorkers = self.workers.closer_than( 10, th).filter(lambda w: w.tag not in workerPoolTags and len(w.orders) == 1 and w.orders[0].ability. id in [AbilityId.HARVEST_GATHER] and w. orders[0].target in mineralTags) # workerPool.extend(surplusWorkers) for i in range(-deficit): if surplusWorkers.amount > 0: w = surplusWorkers.pop() workerPool.append(w) workerPoolTags.add(w.tag) surplusTownhalls[th.tag] = {"unit": th, "deficit": deficit} if all([ len(deficit_gas_buildings) == 0, len(surplusgas_buildings) == 0, len(surplusTownhalls) == 0 or deficitTownhalls == 0, ]): # Cancel early if there is nothing to balance return # Check if deficit in gas less or equal than what we have in surplus, else grab some more workers from surplus bases deficitGasCount = sum( gasInfo["deficit"] for gasTag, gasInfo in deficit_gas_buildings.items() if gasInfo["deficit"] > 0) surplusCount = sum(-gasInfo["deficit"] for gasTag, gasInfo in surplusgas_buildings.items() if gasInfo["deficit"] < 0) surplusCount += sum(-thInfo["deficit"] for thTag, thInfo in surplusTownhalls.items() if thInfo["deficit"] < 0) if deficitGasCount - surplusCount > 0: # Grab workers near the gas who are mining minerals for gTag, gInfo in deficit_gas_buildings.items(): if workerPool.amount >= deficitGasCount: break workersNearGas = self.workers.closer_than( 10, gInfo["unit"]).filter( lambda w: w.tag not in workerPoolTags and len( w.orders) == 1 and w.orders[0].ability.id in [ AbilityId.HARVEST_GATHER ] and w.orders[0].target in mineralTags) while workersNearGas.amount > 0 and workerPool.amount < deficitGasCount: w = workersNearGas.pop() workerPool.append(w) workerPoolTags.add(w.tag) # Now we should have enough workers in the pool to saturate all gases, and if there are workers left over, make them mine at townhalls that have mineral workers deficit for gTag, gInfo in deficit_gas_buildings.items(): if performanceHeavy: # Sort furthest away to closest (as the pop() function will take the last element) workerPool.sort(key=lambda x: x.distance_to(gInfo["unit"]), reverse=True) for i in range(gInfo["deficit"]): if workerPool.amount > 0: w = workerPool.pop() if len(w.orders) == 1 and w.orders[0].ability.id in [ AbilityId.HARVEST_RETURN ]: w.gather(gInfo["unit"], queue=True) else: w.gather(gInfo["unit"]) if not onlySaturateGas: # If we now have left over workers, make them mine at bases with deficit in mineral workers for thTag, thInfo in deficitTownhalls.items(): if performanceHeavy: # Sort furthest away to closest (as the pop() function will take the last element) workerPool.sort( key=lambda x: x.distance_to(thInfo["unit"]), reverse=True) for i in range(thInfo["deficit"]): if workerPool.amount > 0: w = workerPool.pop() mf = self.mineral_field.closer_than( 10, thInfo["unit"]).closest_to(w) if len(w.orders) == 1 and w.orders[0].ability.id in [ AbilityId.HARVEST_RETURN ]: w.gather(mf, queue=True) else: w.gather(mf)
def all_from_task(self, task: Union[int, UnitTask]) -> Units: return Units(self.roles[task].units, self.ai)
class WorkerManager: """ Responsible for managing the workers """ GATHER_RANGE = 1.4 MINERAL_POP_RANGE_MAX = 0.2 MINERAL_POP_RANGE_MIN = 0.001 def __init__(self, bot: CheatMoney, minerals): self.bot = bot self.minerals = Units(minerals, self.bot) self.workers = Units([], self.bot) for mineral in self.minerals: mineral.workers_assigned = 0 print(f'PATH_UNTIL_RANGE: {self.GATHER_RANGE}') async def add(self, worker: Unit): self.workers.append(worker) # assign workers to each mineral patch # prioritize the closest minerals. maximum 2 workers per patch for mineral in self.minerals.sorted_by_distance_to(worker): if mineral.workers_assigned < 2: # if there is already a worker assigned to this patch, assign our worker partners if mineral.workers_assigned == 1: for worker_partner in self.workers: if hasattr( worker_partner, 'assigned_mineral' ) and worker_partner.assigned_mineral == mineral: worker.worker_partner = worker_partner worker_partner.worker_partner = worker worker.assigned_mineral = mineral mineral.workers_assigned += 1 break async def on_step(self, iteration): for worker in self.workers: # for some reason the work in our list doesn't get its data updated, so we need to get this one updated_worker = self.bot.workers.find_by_tag(worker.tag) if updated_worker.is_carrying_minerals: # if worker has minerals, return to base # check for mineral popping opportunity if hasattr(worker, 'worker_partner') \ and self.in_mineral_pop_range(worker) \ and self.on_correct_side_of_partner(worker)\ and updated_worker.distance_to(self.bot.hq_location) > 4: self.bot.do(updated_worker.move(self.bot.hq_location)) else: self.bot.do(updated_worker.return_resource()) # if the worker is over a certain distance away, path to mineral patch elif updated_worker.distance_to( worker.assigned_mineral.position) > self.GATHER_RANGE: pos = updated_worker.position - self.bot.hq_location norm = preprocessing.normalize([pos], norm='l1')[0] self.bot.do( updated_worker.move(worker.assigned_mineral.position - Point2((norm[0], norm[1])))) # if the worker is in range to gather, issue a gather command else: self.bot.do(updated_worker.gather(worker.assigned_mineral)) def in_mineral_pop_range(self, worker): # for some reason the work in our list doesn't get its data updated, so we need to get this one updated_worker = self.bot.workers.find_by_tag(worker.tag) updated_worker_partner = self.bot.workers.find_by_tag( worker.worker_partner.tag) pos = updated_worker.position - updated_worker_partner.position range = math.hypot(pos[0], pos[1]) return range < self.MINERAL_POP_RANGE_MAX and range > self.MINERAL_POP_RANGE_MIN def on_correct_side_of_partner(self, worker): # for some reason the work in our list doesn't get its data updated, so we need to get this one updated_worker = self.bot.workers.find_by_tag(worker.tag) updated_worker_partner = self.bot.workers.find_by_tag( worker.worker_partner.tag) return updated_worker_partner.distance_to( worker.assigned_mineral.position) < updated_worker.distance_to( worker.assigned_mineral.position)
def __init__(self, task: UnitTask, cache: UnitCacheManager, ai: BotAI): self.task = task self.units: Units = Units([], ai) self.tags: List[int] = [] self.cache = cache
def free_workers(self) -> Units: """ Free workers, ie. gathering minerals or gas, or idling, and not dedicated to defending or scouting.""" units: Units = Units(self.roles[UnitTask.Idle.value].units, self.ai) units.extend(self.roles[UnitTask.Gathering.value].units) # Mules should not count for workers return units.of_type([UnitTypeId.DRONE, UnitTypeId.PROBE, UnitTypeId.SCV])
async def attack_units(self, iteration, units, limitToAttack): # airUnits = units.filter(lambda unit: unit.can_attack_air) targets = ( self.enemy_units | self.enemy_structures).filter(lambda unit: unit.can_be_attacked) supectsTargets: Units = Units(variaveis.listEnemyTargettargted, self) targetsEnemyCanAttack = targets.filter(lambda unit: unit.can_attack) targetToMove = "" if supectsTargets: targetToMove = supectsTargets.closest_to( self.enemy_start_locations[0]).position for warrior in units: enemyCloserThan20 = targets.closer_than(25, warrior) unitsCloserThan10 = units.closer_than( int(variaveis.warriorsAttack * 0.7), warrior) #Abilitys if targetsEnemyCanAttack.closer_than(10, warrior): # print(warrior.name) if warrior.type_id == UnitTypeId.SENTRY: warrior(AbilityId.GUARDIANSHIELD_GUARDIANSHIELD) if warrior.type_id == UnitTypeId.STALKER: warrior(AbilityId.EFFECT_BLINK_STALKER, targetsEnemyCanAttack.closest_to(warrior).position) if warrior.type_id == UnitTypeId.VOIDRAY: warrior(AbilityId.EFFECT_VOIDRAYPRISMATICALIGNMENT) #Verificando hp e shield if warrior.health_percentage > 0.8 or warrior.shield_percentage == 1 or variaveis.gameState == variaveis.atacarState or warrior.shield_health_percentage < 0.5: #decisão de alvo # full push if variaveis.gameState == variaveis.atacarState: targets = (self.enemy_units | self.enemy_structures ).filter(lambda unit: unit.can_be_attacked) targetsEnemyCanAttack = targets.filter( lambda unit: unit.can_attack) if targetsEnemyCanAttack: warrior.attack( targetsEnemyCanAttack.closest_to(warrior)) elif targets: warrior.attack(targets.closest_to(warrior)) elif variaveis.hunting: warrior.scan_move(targetToMove) else: warrior.attack(self.enemy_start_locations[0]) if warrior.distance_to( self.enemy_start_locations[0]) < 5: variaveis.hunting = True # ataque seguro elif targets: targetsEnemyCanAttack = targets.filter( lambda unit: unit.can_attack) if targetsEnemyCanAttack: warrior.attack( targetsEnemyCanAttack.closest_to(warrior)) elif enemyCloserThan20.closer_than(8, warrior).amount > 0: warrior.attack(targets.closest_to(warrior)) elif enemyCloserThan20.amount == 0 and targets.closer_than( 5, warrior): warrior.attack( self.enemy_structures.closest_to(warrior)) elif enemyCloserThan20.amount * 0.7 <= unitsCloserThan10.amount: warrior.attack(targets.closest_to(warrior)) # print("tem gente atacando") elif enemyCloserThan20.amount * 0.3 > unitsCloserThan10.amount: furthesWarrior = units.furthest_to( targets.closest_to(warrior)) warrior.move(furthesWarrior) elif enemyCloserThan20.amount == 0: warriorsClosestEnenmy = units.closest_to( targets.closest_to(warrior)) warrior.move(warriorsClosestEnenmy) # elif self.enemy_units.closer_than(15,warrior).amount > 0: # warrior.attack(self.enemy_units.closest_to(warrior)) # elif targets.closer_than(15,warrior).amount > 0: # warrior.attack(targets.closest_to(warrior)) elif units.amount >= limitToAttack and unitsCloserThan10.amount >= limitToAttack * 0.5: if targets: if unitsCloserThan10.amount >= targets.closer_than( 10, warrior).amount * 0.6: warrior.attack(targets.closest_to(warrior)) else: furthesWarrior = units.furthest_to( targetsEnemyCanAttack.closest_to(warrior)) warrior.move(furthesWarrior) elif variaveis.hunting and targetToMove: # print("Scann") warrior.scan_move(targetToMove) else: warrior.attack(self.enemy_start_locations[0]) if warrior.distance_to( self.enemy_start_locations[0]) < 5: variaveis.hunting = True else: targetsHome = (self.townhalls | self.structures(UnitTypeId.PYLON)) targettoMove = self.start_location # if self.structures(UnitTypeId.PYLON).amount>0: # targettoMove = self.structures(UnitTypeId.PYLON).closest_to(self.enemy_start_locations) if targetsHome: targettoMove = targetsHome.closest_to( self.enemy_start_locations[0]) if targets: target = targets.closest_to(warrior) targettoMove = targetsHome.closest_to(target) warrior.move(targettoMove) else: warrior.attack(self.enemy_start_locations[0]) else: # if units.furthest_to(targetsEnemyCanAttack.closest_to(warrior)): # safePlace = units.furthest_to(targetsEnemyCanAttack.closest_to(warrior)) if variaveis.forte2 != "": safePlace = variaveis.forte2 elif variaveis.forte1 != "": safePlace = variaveis.forte1 else: safePlace = (self.townhalls | self.structures(UnitTypeId.PYLON)) if warrior.distance_to(safePlace) < 10: warrior.attack(safePlace) else: warrior.move(safePlace) return
def all_from_task(self, task: UnitTask) -> Units: return Units(self.roles[task.value].units, self.ai)
def __init__(self, response_observation): self.response_observation = response_observation self.actions = response_observation.actions # successful actions since last loop self.action_errors = response_observation.action_errors # error actions since last loop # https://github.com/Blizzard/s2client-proto/blob/51662231c0965eba47d5183ed0a6336d5ae6b640/s2clientprotocol/sc2api.proto#L575 # TODO: implement alerts https://github.com/Blizzard/s2client-proto/blob/51662231c0965eba47d5183ed0a6336d5ae6b640/s2clientprotocol/sc2api.proto#L640 self.observation = response_observation.observation self.observation_raw = self.observation.raw_data self.dead_units: Set[ int] = self.observation_raw.event.dead_units # returns set of tags of units that died self.alerts = self.observation.alerts self.player_result = response_observation.player_result self.chat = response_observation.chat self.common: Common = Common(self.observation.player_common) # Area covered by Pylons and Warpprisms self.psionic_matrix: PsionicMatrix = PsionicMatrix.from_proto( self.observation_raw.player.power_sources) self.game_loop: int = self.observation.game_loop # 22.4 per second on faster game speed # https://github.com/Blizzard/s2client-proto/blob/33f0ecf615aa06ca845ffe4739ef3133f37265a9/s2clientprotocol/score.proto#L31 self.score: ScoreDetails = ScoreDetails(self.observation.score) self.abilities = self.observation.abilities # abilities of selected units self._blipUnits = [] self.own_units: Units = Units([]) self.enemy_units: Units = Units([]) self.mineral_field: Units = Units([]) self.vespene_geyser: Units = Units([]) self.resources: Units = Units([]) self.destructables: Units = Units([]) self.watchtowers: Units = Units([]) self.units: Units = Units([]) for unit in self.observation.raw_data.units: if unit.is_blip: self._blipUnits.append(unit) else: unit_obj = Unit(unit) self.units.append(unit_obj) alliance = unit.alliance # Alliance.Neutral.value = 3 if alliance == 3: unit_type = unit.unit_type # XELNAGATOWER = 149 if unit_type == 149: self.watchtowers.append(unit_obj) # mineral field enums elif unit_type in mineral_ids: self.mineral_field.append(unit_obj) self.resources.append(unit_obj) # geyser enums elif unit_type in geyser_ids: self.vespene_geyser.append(unit_obj) self.resources.append(unit_obj) # all destructable rocks else: self.destructables.append(unit_obj) # Alliance.Self.value = 1 elif alliance == 1: self.own_units.append(unit_obj) # Alliance.Enemy.value = 4 elif alliance == 4: self.enemy_units.append(unit_obj) self.upgrades: Set[UpgradeId] = { UpgradeId(upgrade) for upgrade in self.observation_raw.player.upgrade_ids } # Set of unit tags that died this step self.dead_units: Set[int] = { dead_unit_tag for dead_unit_tag in self.observation_raw.event.dead_units } # Set of enemy units detected by own sensor tower, as blips have less unit information than normal visible units self.blips: Set[Blip] = {Blip(unit) for unit in self._blipUnits} # self.visibility[point]: 0=Hidden, 1=Fogged, 2=Visible self.visibility: PixelMap = PixelMap( self.observation_raw.map_state.visibility, mirrored=True) # HSPARK 수정 시작 # self.creep[point]: 0=No creep, 1=creep # self.creep: PixelMap = PixelMap(self.observation_raw.map_state.creep, mirrored=True) self.creep: PixelMap = PixelMap(self.observation_raw.map_state.creep, mirrored=True, in_bits=True) # HSPARK 수정 끝 # Effects like ravager bile shot, lurker attack, everything in effect_id.py self.effects: Set[EffectData] = { EffectData(effect) for effect in self.observation_raw.effects } """ Usage: