def _defenders_from(self, task: UnitTask, current_power: ExtendedPower, position: Point2, power: ExtendedPower, units: Units): """ Get defenders from a task. """ if current_power.is_enough_for(power): return exclude_types = [] exclude_types.append(UnitTypeId.OVERSEER) exclude_types.extend(self.knowledge.unit_values.worker_types) exclude_types.extend(self.peace_unit_types) role_units = self.roles[task.value].units\ .exclude_type(exclude_types) unit: Unit for unit in role_units.sorted_by_distance_to(position): enough_air_power = current_power.air_power >= power.air_presence * 1.1 enough_ground_power = current_power.ground_power >= power.ground_presence * 1.1 if not self.unit_values.can_shoot_air(unit) and not enough_air_power and enough_ground_power: # Don't pull any more units that can't actually shoot the targets continue if not self.unit_values.can_shoot_ground(unit, self.knowledge) and enough_air_power and not enough_ground_power: # Don't pull any more units that can't actually shoot the targets continue current_power.add_unit(unit) units.append(unit) if current_power.is_enough_for(power): return return
def enemy_total_power(self) -> ExtendedPower: """Returns the total power of all enemy units we currently know about. Assumes they are all in full health. Ignores workers and overlords.""" total_power = ExtendedPower(self.unit_value) for type_id in self._known_enemy_units_dict: if self.unit_value.is_worker(type_id): continue if type_id == UnitTypeId.OVERLORD: continue count_for_unit_type = self.unit_count(type_id) total_power.add_unit(type_id, count_for_unit_type) return total_power
class EnemyData: close_enemies: Units enemy_center: Point2 closest: Unit def __init__(self, knowledge: Knowledge, close_enemies: Units, unit: Unit, our_units: Units, our_median: Point2): self.ai = knowledge.ai self.our_units = our_units self.our_median = our_median self.close_enemies = close_enemies self.my_height = self.ai.get_terrain_height(unit) self.enemy_power = ExtendedPower(knowledge.unit_values) self.our_power = ExtendedPower(knowledge.unit_values) for unit in our_units: # type: Unit self.our_power.add_unit(unit) self.worker_only = False if self.close_enemies.exists: self.enemy_center = close_enemies.center # Can be empty! self.powered_enemies = close_enemies.filter(lambda x: knowledge.unit_values.power(x) > 0.1) if self.powered_enemies.exists: self.closest = unit.position.closest(self.powered_enemies) self.worker_only = True for enemy in self.powered_enemies: # type: Unit if not knowledge.unit_values.is_worker(enemy): self.worker_only = False self.enemy_power.add_unit(enemy) else: self.closest = unit.position.closest(self.close_enemies) self.enemy_center_height = self.ai.get_terrain_height(self.enemy_center) self.closest_height = self.ai.get_terrain_height(self.closest) else: self.powered_enemies = Units([], self.ai) # empty list @property def enemies_exist(self) -> bool: return self.close_enemies.exists
def solve_combat(self, goal: CombatGoal, command: CombatAction, enemies: EnemyData) -> List[CombatAction]: oracle = goal.unit if not oracle.has_buff(BuffId.ORACLEWEAPON): goal.ready_to_shoot = False air_shooter_enemies = Units([], self.ai) enemy: Unit power = ExtendedPower(self.unit_values) for enemy in enemies.close_enemies: if self.unit_values.air_range(enemy) < enemy.distance_to(oracle) + 1: air_shooter_enemies.append(enemy) power.add_unit(enemy) if self.unit_values.is_static_air_defense(enemy): power.add(5) # can't beat turrets with oracle enemy_center = enemies.close_enemies.center for air_shooter in air_shooter_enemies: # type: Unit if air_shooter.is_light and not air_shooter.is_flying: power.add_units(air_shooter_enemies) else: power.add_units(air_shooter_enemies * 2) time = self.knowledge.ai.time if goal.move_type == MoveType.PanicRetreat and oracle.has_buff(BuffId.ORACLEWEAPON): return self.disable_beam(oracle) possible_targets = enemies.close_enemies.filter(lambda u: not u.is_flying and not u.is_structure and u.is_light) if possible_targets.exists: if oracle.energy > 50 and possible_targets.closest_distance_to(oracle) < 5 and not oracle.has_buff(BuffId.ORACLEWEAPON): return self.enable_beam(oracle) if power.air_power > 0 and power.air_power <= 3: target = air_shooter_enemies.closest_to(oracle) if target.is_light or target.health_percentage < 0.5: if not oracle.has_buff(BuffId.ORACLEWEAPON): return self.enable_beam(oracle) # Kill the target return [CombatAction(oracle, target, True)] #target_pos = self.knowledge.pathing_manager.find_weak_influence_air(goal.target, 7) #move_step = self.knowledge.pathing_manager.find_influence_air_path(oracle.position, target_pos) return [CombatAction(oracle, target.position, True)] elif goal.ready_to_shoot and possible_targets: return [CombatAction(oracle, possible_targets.closest_to(oracle), True)] elif power.air_power > 12: # Panic retreat to whatever direction if goal.move_type in offensive: new_target: Point2 = self.knowledge.pathing_manager.find_weak_influence_air(goal.target, 7) step = self.knowledge.pathing_manager.find_influence_air_path(oracle.position, new_target) # backstep: Point2 = self.knowledge.pathing_manager.find_weak_influence_air(oracle.position, 7) move_action = CombatAction(oracle, step, False) else: backstep = self.knowledge.pathing_manager.find_influence_air_path(oracle.position, goal.target) move_action = CombatAction(oracle, backstep, False) # Todo disable beam? return [move_action] elif power.air_power > 3: # Try kiting while killing the target target = self.knowledge.pathing_manager.find_weak_influence_air(goal.target, 7) backstep = self.knowledge.pathing_manager.find_influence_air_path(oracle.position, target) if goal.move_type in offensive: move_action = CombatAction(oracle, backstep, False) else: move_action = CombatAction(oracle, backstep, False) if oracle.has_buff(BuffId.ORACLEWEAPON): if possible_targets: closest = possible_targets.closest_to(oracle) if closest.distance_to(oracle) < 5: return [CombatAction(oracle, closest, True)] return [CombatAction(oracle, command.target, True), move_action] else: return [move_action] if possible_targets.exists: return [CombatAction(oracle, command.target, True)] else: return [CombatAction(oracle, command.target, False)]
async def execute(self) -> bool: unit: Unit all_defenders = self.knowledge.roles.all_from_task(UnitTask.Defending) for i in range(0, len(self.knowledge.expansion_zones)): zone: 'Zone' = self.knowledge.expansion_zones[i] zone_tags = self.defender_tags[i] zone_defenders_all = all_defenders.tags_in(zone_tags) zone_worker_defenders = zone_defenders_all(self.worker_type) zone_defenders = zone_defenders_all.exclude_type(self.worker_type) enemies = zone.known_enemy_units # Let's loop zone starting from our main, which is the one we want to defend the most # Check that zone is either in our control or is our start location that has no Nexus if zone_defenders.exists or zone.is_ours or zone == self.knowledge.own_main_zone: if not self.defense_required(enemies): # Delay before removing defenses in case we just lost visibility of the enemies if zone.last_scouted_center == self.knowledge.ai.time \ or self.zone_seen_enemy[i] + PlanZoneDefense.ZONE_CLEAR_TIMEOUT < self.ai.time: self.knowledge.roles.clear_tasks(zone_defenders_all) zone_defenders.clear() zone_tags.clear() continue # Zone is well under control. else: self.zone_seen_enemy[i] = self.ai.time if enemies.exists: # enemy_center = zone.assaulting_enemies.center enemy_center = enemies.closest_to( zone.center_location).position elif zone.assaulting_enemies: enemy_center = zone.assaulting_enemies.closest_to( zone.center_location).position else: enemy_center = zone.gather_point defense_required = ExtendedPower(self.unit_values) defense_required.add_power(zone.assaulting_enemy_power) defense_required.multiply(1.5) defenders = ExtendedPower(self.unit_values) for unit in zone_defenders: self.combat.add_unit(unit) defenders.add_unit(unit) # Add units to defenders that are being warped in. for unit in self.knowledge.roles.units( UnitTask.Idle).not_ready: if unit.distance_to(zone.center_location) < zone.radius: # unit is idle in the zone, add to defenders self.combat.add_unit(unit) self.knowledge.roles.set_task(UnitTask.Defending, unit) zone_tags.append(unit.tag) if not defenders.is_enough_for(defense_required): defense_required.substract_power(defenders) for unit in self.knowledge.roles.get_defenders( defense_required, zone.center_location): if unit.distance_to( zone.center_location) < zone.radius: # Only count units that are close as defenders defenders.add_unit(unit) self.knowledge.roles.set_task(UnitTask.Defending, unit) self.combat.add_unit(unit) zone_tags.append(unit.tag) if len(enemies) > 1 or (len(enemies) == 1 and enemies[0].type_id not in self.unit_values.worker_types): # Pull workers to defend only and only if the enemy isn't one worker scout if defenders.is_enough_for(defense_required): # Workers should return to mining. for unit in zone_worker_defenders: zone.go_mine(unit) if unit.tag in zone_tags: # Just in case, should be in zone tags always. zone_tags.remove(unit.tag) # Zone is well under control without worker defense. else: await self.worker_defence(defenders.power, defense_required, enemy_center, zone, zone_tags, zone_worker_defenders) self.combat.execute(enemy_center, MoveType.SearchAndDestroy) return True # never block
def enemy_static_air_power(self) -> ExtendedPower: """Returns power of enemy static ground defenses on the zone.""" power = ExtendedPower(self.unit_values) for air_def in self.enemy_static_air_defenses: power.add_unit(air_def) return power
def enemy_static_ground_power(self) -> ExtendedPower: """Returns power of enemy static ground defenses.""" power = ExtendedPower(self.unit_values) for ground_def in self.enemy_static_ground_defenses: power.add_unit(ground_def) return power
async def update_influence(self): power = ExtendedPower(self.unit_values) self.path_finder_terrain.reset() # Reset self.path_finder_ground.reset() # Reset positions = [] for mf in self.ai.mineral_field: # type: Unit # In 4.8.5+ minerals are no linger visible in pathing grid positions.append(mf.position) # for mf in self.ai.mineral_walls: # type: Unit # # In 4.8.5+ minerals are no linger visible in pathing grid # positions.append(mf.position) self.path_finder_terrain.create_block(positions, (2, 1)) self.path_finder_ground.create_block(positions, (2, 1)) self.set_rocks(self.path_finder_terrain) self.set_rocks(self.path_finder_ground) for building in self.ai.structures + self.knowledge.known_enemy_structures: # type: Unit if building.type_id in buildings_2x2: self.path_finder_ground.create_block(building.position, (2, 2)) elif building.type_id in buildings_3x3: self.path_finder_ground.create_block(building.position, (3, 3)) elif building.type_id in buildings_5x5: self.path_finder_ground.create_block(building.position, (5, 3)) self.path_finder_ground.create_block(building.position, (3, 5)) self.set_rocks(self.path_finder_ground) self.path_finder_ground.normalize_influence(20) self.path_finder_air.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] = map( lambda u: u.position, 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 self.path_finder_air.add_influence(positions, power.air_power, s_range + 3) if self.unit_values.can_shoot_ground(example_enemy, self.knowledge): positions = map(lambda u: u.position, enemies) # need to be specified in both places s_range = self.unit_values.ground_range( example_enemy, self.knowledge) if example_enemy.type_id == UnitTypeId.CYCLONE: s_range = 7 if s_range < 2: self.path_finder_ground.add_influence_walk( positions, power.ground_power, 7) elif s_range < 5: self.path_finder_ground.add_influence_walk( positions, power.ground_power, 7) else: self.path_finder_ground.add_influence( positions, power.ground_power, 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.path_finder_air.add_influence(effects[2], effects[0], effects[1]) self.path_finder_ground.add_influence(effects[2], effects[0], effects[1])
class EnemyArmyPredicter(ManagerBase): def __init__(self): super().__init__() async def start(self, knowledge: 'Knowledge'): await super().start(knowledge) self.enemy_units_manager: EnemyUnitsManager = self.knowledge.enemy_units_manager self.lost_units_manager: LostUnitsManager = knowledge.lost_units_manager self.unit_values: 'UnitValue' = knowledge.unit_values self.updater = IntervalFuncAsync(self.ai, self._real_update, INTERVAL) self.enemy_base_value_minerals = 400 + 12 * 50 + 50 self.enemy_known_worker_count = 12 self.mineral_dict: Dict['Zone', int] = {} # Last time minerals were updated self.mineral_updated_dict: Dict['Zone', float] = {} self.gas_dict: Dict[Point2, int] = {} for zone in knowledge.expansion_zones: minerals = 0 if zone.last_minerals is not None: minerals = zone.last_minerals self.mineral_dict[zone] = minerals for geyser in self.ai.vespene_geyser: # type: Unit self.gas_dict[geyser.position] = 2250 self.enemy_mined_minerals = 0 self.enemy_mined_minerals_prediction = 0 self.enemy_mined_gas = 0 self.enemy_army_known_minerals = 0 self.enemy_army_known_gas = 0 self.own_army_value_minerals = 0 self.own_army_value_gas = 0 self.predicted_enemy_free_minerals = 0 self.predicted_enemy_free_gas = 0 self.predicted_enemy_army_minerals = 0 self.predicted_enemy_army_gas = 0 self.predicted_enemy_composition: List[UnitCount] = [] self.enemy_power = ExtendedPower(self.unit_values) self.predicted_enemy_power = ExtendedPower(self.unit_values) @property def own_value(self): """ Our exact army value that we know of """ return self.own_army_value_minerals + self.own_army_value_gas @property def enemy_value(self): """ Best estimation on how big value enemy army has """ return self.predicted_enemy_army_minerals + self.predicted_enemy_army_gas async def update(self): await self.updater.execute() async def _real_update(self): await self.update_own_army_value() self.enemy_power.clear() self.predicted_enemy_power.clear() self.predicted_enemy_composition.clear() gas_miners = self.knowledge.known_enemy_structures.of_type([ UnitTypeId.ASSIMILATOR, UnitTypeId.EXTRACTOR, UnitTypeId.REFINERY ]) minerals_used: int = 0 gas_used: int = 0 enemy_composition = self.enemy_units_manager.enemy_composition self.enemy_known_worker_count = 0 self.enemy_army_known_minerals = 0 self.enemy_army_known_gas = 0 self.predicted_enemy_free_minerals = 0 self.predicted_enemy_free_gas = 0 self.predicted_enemy_army_minerals = 0 self.predicted_enemy_army_gas = 0 for unit_count in enemy_composition: if unit_count.count > 0: # TODO: Overlords! if self.unit_values.is_worker(unit_count.enemy_type): self.enemy_known_worker_count += unit_count.count mineral_value = self.unit_values.minerals( unit_count.enemy_type) * unit_count.count gas_value = self.unit_values.gas( unit_count.enemy_type) * unit_count.count minerals_used += mineral_value gas_used += gas_value if not self.unit_values.is_worker(unit_count.enemy_type) \ and self.unit_values.power_by_type(unit_count.enemy_type) > 0.25: self.enemy_power.add_unit(unit_count.enemy_type, unit_count.count) self.predicted_enemy_composition.append(unit_count) # Save values as to what we know to be true self.enemy_army_known_minerals += mineral_value self.enemy_army_known_gas += gas_value mined_minerals: int = 0 mined_minerals_predict: float = 0 worker_count_per_base = 12 # TODO: Just random guess for zone in self.knowledge.enemy_expansion_zones: current_minerals = zone.last_minerals if current_minerals is None: current_minerals = 0 last_minerals = self.mineral_dict.get(zone, 0) if last_minerals > current_minerals: self.mineral_dict[zone] = current_minerals self.mineral_updated_dict[zone] = self.ai.time if zone.is_enemys: mined_minerals += last_minerals - current_minerals elif zone.is_enemys: prediction = last_minerals - ( self.ai.time - self.mineral_updated_dict.get(zone, 0) ) * MINERAL_MINING_SPEED * worker_count_per_base prediction = max(0.0, prediction) mined_minerals_predict += last_minerals - prediction self.enemy_mined_minerals += mined_minerals self.enemy_mined_minerals_prediction = round( self.enemy_mined_minerals + mined_minerals_predict) if gas_miners.exists: for miner in gas_miners: # type: Unit last_gas = self.gas_dict.get(miner.position, 2250) if miner.is_visible: gas = miner.vespene_contents else: gas = max(0.0, last_gas - 169.61 / 60 * INTERVAL) self.gas_dict[miner.position] = gas self.enemy_mined_gas += last_gas - gas lost_tuple: tuple = self.lost_units_manager.calculate_enemy_lost_resources( ) minerals_used += lost_tuple[0] gas_used += lost_tuple[1] self.predicted_enemy_free_minerals = round( self.enemy_base_value_minerals + self.enemy_mined_minerals_prediction - minerals_used) self.predicted_enemy_free_gas = round(self.enemy_mined_gas - gas_used) if self.predicted_enemy_free_minerals < 0: # Possibly hidden base or more workers than we think? self.print( f"Predicting negative free minerals for enemy: {self.predicted_enemy_free_minerals}" ) await self.predict_enemy_composition() for unit_count in self.predicted_enemy_composition: self.predicted_enemy_power.add_unit(unit_count.enemy_type, unit_count.count) mineral_value = self.unit_values.minerals( unit_count.enemy_type) * unit_count.count gas_value = self.unit_values.minerals( unit_count.enemy_type) * unit_count.count self.predicted_enemy_army_minerals += mineral_value self.predicted_enemy_army_gas += gas_value async def update_own_army_value(self): self.own_army_value_minerals = 0 self.own_army_value_gas = 0 for unit in self.ai.units: if not self.unit_values.is_worker(unit.type_id): self.own_army_value_minerals += self.unit_values.minerals( unit.type_id) self.own_army_value_gas += self.unit_values.gas(unit.type_id) async def predict_enemy_composition(self): if self.knowledge.enemy_race == Race.Random: return # let's wait until we know the actual race. guesser = CompositionGuesser(self.knowledge) guesser.left_minerals = self.predicted_enemy_free_minerals guesser.left_gas = self.predicted_enemy_free_gas additional_guess: List[UnitCount] = guesser.predict_enemy_composition() for unit_count in additional_guess: existing = self.find(self.predicted_enemy_composition, unit_count.enemy_type) if existing is None: self.predicted_enemy_composition.append(unit_count) else: existing.count += unit_count.count def find(self, lst: List[UnitCount], enemy_type) -> Optional[UnitCount]: for unit_count in lst: if unit_count.enemy_type == enemy_type: return unit_count return None async def post_update(self): await self.debug_message() async def debug_message(self): if self.knowledge.my_race == Race.Protoss: # my_comp = self.enemy_build.gate_type_values(self.predicted_enemy_composition) # my_comp.extend(self.enemy_build.robo_type_values(self.predicted_enemy_composition)) # my_comp.extend(self.enemy_build.star_type_values(self.predicted_enemy_composition)) enemy_comp = sorted(self.predicted_enemy_composition, key=lambda uc: uc.count, reverse=True) # my_comp = sorted(my_comp, key=lambda c: c.count, reverse=True) if self.debug: client: Client = self.ai._client msg = f"Us vs them: {self.own_value} / {self.enemy_value}\n" msg += f"Known enemy army (M/G): {self.enemy_army_known_minerals} / {self.enemy_army_known_gas}\n" msg += f"Enemy predicted money (M/G): {self.predicted_enemy_free_minerals} / {self.predicted_enemy_free_gas}\n" msg += f"\nComposition:\n" for unit_count in enemy_comp: msg += f" {unit_count.to_short_string()}" # msg += f"\nCounter:\n" # for unit_count in my_comp: # if unit_count.count > 0: # msg += f" {unit_count.to_string()}\n" client.debug_text_2d(msg, Point2((0.1, 0.1)), None, 16)