def unit_solve_combat(self, unit: Unit, current_command: Action) -> Action: if self.move_type == MoveType.DefensiveRetreat: return current_command closest = self.closest_units.get(unit.tag) if not closest or closest.distance_to(unit) > 14: # not in combat, follow the army return current_command time = self.knowledge.ai.time relevant_enemies = self.cache.enemy_in_range(unit.position, 14).not_structure.not_flying if time < self.last_used_any + INTERVAL or len(relevant_enemies) < 4: return self.stay_safe(unit, current_command) center = relevant_enemies.center if self.cd_manager.is_ready(unit.tag, AbilityId.EFFECT_PURIFICATIONNOVA): distance = self.pather.walk_distance(unit.position, center) if distance < 9: self.last_used_any = time return Action(center, False, AbilityId.EFFECT_PURIFICATIONNOVA) else: return Action(center, False) return self.stay_safe(unit, current_command)
def unit_solve_combat(self, unit: Unit, current_command: Action) -> Action: if self.engage_ratio < 0.25 and self.can_engage_ratio < 0.25: return current_command if self.cd_manager.is_ready(unit.tag, AbilityId.EFFECT_BLINK_STALKER): if self.is_locked_on(unit): cyclones = self.enemies_near_by(UnitTypeId.CYCLONE) if cyclones: closest_cyclone = cyclones.closest_to(unit) backstep: Point2 = closest_cyclone.position.towards(unit.position, 15) backstep = self.pather.find_weak_influence_ground(backstep, 4) return Action(backstep, False, AbilityId.EFFECT_BLINK_STALKER) if self.model == CombatModel.StalkerToSiege and \ (self.move_type == MoveType.Assault or self.move_type == MoveType.SearchAndDestroy): siege_units = self.enemies_near_by.of_type(siege) if siege_units: target = siege_units.closest_to(unit) if target.distance_to(unit) > 6: return Action(target.position, False, AbilityId.EFFECT_BLINK_STALKER) if unit.shield_percentage < 0.05: # Blink to safety. target_pos = unit.position if self.closest_group: target_pos = target_pos.towards(self.closest_group.center, -5) target = self.pather.find_weak_influence_ground_blink(target_pos, 6) if target.distance_to(unit) > 3: return Action(target, False, AbilityId.EFFECT_BLINK_STALKER) return super().unit_solve_combat(unit, current_command)
def group_solve_combat(self, units: Units, current_command: Action) -> Action: if self.engage_ratio > 0.5 and self.closest_group: if self.ready_to_attack_ratio > 0.8 or self.closest_group_distance < 2: return Action(self.closest_group.center, True) if self.ready_to_attack_ratio < 0.25: return Action(self.closest_group.center, True) return Action(self.closest_group.center.towards(self.center, -3), False) #if self.engage_percentage == 0 return current_command
def unit_solve_combat(self, unit: Unit, current_command: Action) -> Action: if self.closest_group and self.engaged_power.melee_percentage > 0.9: backstep: Point2 = unit.position.towards(self.closest_group.center, -3) if ((unit.health + unit.shield <= 5 and not self.ready_to_shoot(unit)) or (unit.shield_health_percentage < 0.5 and unit.weapon_cooldown > 9)): backstep = self.pather.find_weak_influence_ground(backstep, 4) if self.cache.own_in_range(unit.position, 1) or self.cache.own_in_range( unit.position, 1): # Mineral walk angle = sc2math.line_angle(unit.position, backstep) best_angle = sc2math.pi / 6 best_mf = None for mf in self.ai.mineral_field: # type: Unit new_angle = sc2math.line_angle(unit.position, mf.position) angle_distance = sc2math.angle_distance( angle, new_angle) if angle_distance < best_angle: best_mf = mf best_angle = angle_distance if best_mf: # Use backstep with gather command to pass through own units return Action(best_mf, False, ability=AbilityId.HARVEST_GATHER) return Action(backstep, False) if self.ready_to_shoot(unit): if self.closest_group: current = Action(self.closest_group.center, True) else: current = Action(current_command.target, True) return self.melee_focus_fire(unit, current) # if unit.health + unit.shield <= 5: # backstep = self.pather.find_weak_influence_ground(backstep, 4) # return Action(backstep, False) # if self.knowledge.enemy_race == Race.Protoss: # if self.engage_percentage < 0.25: # buildings = self.enemies_near_by.sorted_by_distance_to(unit) # if buildings: # if buildings.first.health + buildings.first.shield < 200: # return Action(buildings.first, True) # pylons = buildings(UnitTypeId.PYLON) # if pylons: # return Action(buildings.first, True) return current_command
def group_solve_combat(self, units: Units, current_command: Action) -> Action: if self.move_type == MoveType.DefensiveRetreat or self.move_type == MoveType.PanicRetreat: return current_command if self.engage_ratio > 0.25 and self.closest_group: if self.ready_to_attack_ratio > 0.25 or self.closest_group_distance < 2: return Action(self.closest_group.center, True) return Action(self.closest_group.center.towards(self.center, -3), False) #if self.engage_percentage == 0 return current_command
def unit_solve_combat(self, unit: Unit, current_command: Action) -> Action: if (self.stim_required > 0 and not self.has_stim(unit) and unit.shield_health_percentage > 0.5): if unit.type_id == UnitTypeId.MARAUDER and self.cd_manager.is_ready( unit.tag, AbilityId.EFFECT_STIM_MARAUDER): self.stim_required -= 2 return Action(None, False, AbilityId.EFFECT_STIM_MARAUDER) elif unit.type_id == UnitTypeId.MARINE and self.cd_manager.is_ready( unit.tag, AbilityId.EFFECT_STIM_MARINE): self.stim_required -= 1 return Action(None, False, AbilityId.EFFECT_STIM_MARINE) return super().unit_solve_combat(unit, current_command)
def unit_solve_combat(self, unit: Unit, current_command: Action) -> Action: if isinstance(current_command.target, Unit): target_pos = current_command.target.position else: target_pos = current_command.target if self.move_type == MoveType.PanicRetreat or self.move_type == MoveType.DefensiveRetreat: if unit.has_buff(BuffId.ORACLEWEAPON): return Action(None, False, AbilityId.BEHAVIOR_PULSARBEAMOFF) target = self.pather.find_influence_air_path( unit.position, target_pos) return Action(target, False) if self.move_type == MoveType.Harass: targets = self.cache.enemy(self.knowledge.enemy_worker_type) if targets: close_to_me = targets.closer_than(8, unit.position) close_to_target = targets.closer_than(10, target_pos) if close_to_me: targets = close_to_me elif close_to_target: targets = close_to_target else: targets = self.cache.enemy_in_range( unit.position, 10).filter(lambda u: u.is_light and not u.is_flying) if targets: closest = targets.closest_to(unit) distance = closest.distance_to(unit) if distance > 40 and unit.has_buff(BuffId.ORACLEWEAPON): return Action(None, False, AbilityId.BEHAVIOR_PULSARBEAMOFF) if distance < 5: if not unit.has_buff(BuffId.ORACLEWEAPON): if unit.energy > 40: return Action(None, False, AbilityId.BEHAVIOR_PULSARBEAMON) else: target = self.pather.find_weak_influence_air( unit.position, 10) return Action(target, False) else: return Action(closest, True) target = self.pather.find_weak_influence_air(closest.position, 10) target = self.pather.find_influence_air_path(unit.position, target) return Action(target, False) else: if unit.has_buff(BuffId.ORACLEWEAPON): return Action(None, False, AbilityId.BEHAVIOR_PULSARBEAMOFF) target = self.pather.find_influence_air_path(unit.position, target_pos) return Action(target, False)
def unit_solve_combat(self, unit: Unit, current_command: Action) -> Action: if isinstance(current_command.target, Unit): target_pos = current_command.target.position else: target_pos = current_command.target target = self.pather.find_path(self.group.center, target_pos, 8) # move ahead of group enemies = self.cache.enemy_in_range(target, 12, False) other_observers = self.cache.own(UnitTypeId.OBSERVER).tags_not_in( [unit.tag]) if other_observers: # Try to keep observers separated from each other closest = other_observers.closest_to(unit) if closest.distance_to(unit) < 5: pos: Point2 = closest.position target = unit.position.towards(pos, -6) # for enemy in enemies: # type: Unit # if enemy.detect_range > 0 and enemy.detect_range > target.distance_to(enemy): # break if enemies: target = self.pather.find_weak_influence_air(target, 10) return Action(target, False)
def unit_solve_combat(self, unit: Unit, current_command: Action) -> Action: if self.engage_ratio < 0.25 and self.can_engage_ratio < 0.25: return current_command if self.cd_manager.is_ready(unit.tag, AbilityId.PSISTORM_PSISTORM): stormable_enemies = self.cache.enemy_in_range(unit.position, 10).not_structure if len(stormable_enemies) > 6: center = stormable_enemies.center target = stormable_enemies.closest_to(center) return Action(target.position, False, AbilityId.PSISTORM_PSISTORM) if self.cd_manager.is_ready(unit.tag, AbilityId.FEEDBACK_FEEDBACK): feedback_enemies = self.cache.enemy_in_range(unit.position, 10) \ .filter(lambda u: u.energy > 74 and not u.is_structure) if feedback_enemies: closest = feedback_enemies.closest_to(unit) return Action(closest, False, AbilityId.FEEDBACK_FEEDBACK) return super().unit_solve_combat(unit, current_command)
def final_solve(self, unit: Unit, command: Action) -> Action: is_fighter = unit.type_id == UnitTypeId.VIKINGFIGHTER if not self.enemies_near_by: if is_fighter: return command else: return Action(None, False, AbilityId.MORPH_VIKINGFIGHTERMODE) if is_fighter: if (not self.enemies_near_by(UnitTypeId.COLOSSUS).exists and not self.enemies_near_by.flying.exists and self.enemies_near_by.not_flying.exists): return Action(None, False, AbilityId.MORPH_VIKINGASSAULTMODE) else: if self.engaged_power.air_presence > 0 or self.enemies_near_by( UnitTypeId.COLOSSUS).exists: return Action(None, False, AbilityId.MORPH_VIKINGFIGHTERMODE) return command
def unit_solve_combat(self, unit: Unit, current_command: Action) -> Action: if unit.tag not in self.spawned: self.spawned[unit.tag] = self.ai.time time_percentage = 1 - (self.ai.time - self.spawned.get(unit.tag, self.ai.time)) / NOVA_DURATION relevant_enemies = self.cache.enemy_in_range(unit.position, 9 * time_percentage).not_structure.not_flying if relevant_enemies.exists: target = relevant_enemies.closest_to(relevant_enemies.center) return Action(target, False) else: own_relevant = self.cache.own_in_range(unit.position, 4).not_structure.not_flying if own_relevant: closest_own = own_relevant.closest_to(unit.position) pos = unit.position.towards(closest_own.position, -3) return Action(pos, False) return current_command
def unit_solve_combat(self, unit: Unit, current_command: Action) -> Action: if self.engage_ratio < 0.25 and self.can_engage_ratio < 0.25: return current_command if self.move_type == MoveType.DefensiveRetreat: if self.ready_to_shoot(unit): closest = self.closest_units.get(unit.tag, None) if closest and closest.is_target: unit_range = self.unit_values.real_range( unit, closest, self.knowledge) if unit_range > 0 and unit_range > unit.distance_to( closest): return Action(closest, True) return current_command elif self.move_type == MoveType.PanicRetreat: return current_command if self.ready_to_shoot(unit): if self.closest_group and self.closest_group.ground_units: current_command = Action(self.closest_group.center, True) else: current_command = Action(current_command.target, True) else: closest = self.closest_units[unit.tag] # d = unit.distance_to(closest) unit_range = self.unit_values.real_range(unit, closest, self.knowledge) - 0.5 if unit.is_flying: best_position = self.pather.find_low_inside_air( unit.position, closest.position, unit_range) else: best_position = self.pather.find_low_inside_ground( unit.position, closest.position, unit_range) return Action(best_position, False) if self.ready_to_shoot(unit) and current_command.is_attack: return self.focus_fire(unit, current_command, self.prio_dict) return current_command
def unit_solve_combat(self, unit: Unit, current_command: Action) -> Action: if self.cd_manager.is_ready(unit.tag, AbilityId.TRANSFUSION_TRANSFUSION): own_close = self.cache.own_in_range(unit.position, 7) for own_unit in own_close: # type: Unit if own_unit.health_max - own_unit.health > 70 and not own_unit.has_buff( BuffId.TRANSFUSION): return Action(own_unit, False, AbilityId.TRANSFUSION_TRANSFUSION) return super().unit_solve_combat(unit, current_command)
def action_to(self, group: CombatUnits, target, move_type: MoveType, is_attack: bool): if isinstance(target, Point2) and group.ground_units: if move_type in {MoveType.DefensiveRetreat, MoveType.PanicRetreat}: target = self.pather.find_influence_ground_path( group.center, target, 14) else: target = self.pather.find_path(group.center, target, 14) own_unit_cache: Dict[UnitTypeId, Units] = {} for unit in group.units: units = own_unit_cache.get(unit.type_id, Units([], self.ai)) if units.amount == 0: real_type = self.unit_values.real_type(unit.type_id) own_unit_cache[real_type] = units units.append(unit) for type_id, type_units in own_unit_cache.items(): micro: MicroStep = self.unit_micros.get(type_id, self.generic_micro) micro.init_group(group, type_units, self.enemy_groups, move_type) group_action = micro.group_solve_combat(type_units, Action(target, is_attack)) for unit in type_units: action = micro.unit_solve_combat(unit, group_action) order = action.to_commmand(unit) if order: self.ai.do(order) if self.debug: if action.debug_comment: status = action.debug_comment elif action.ability: status = action.ability.name elif action.is_attack: status = "Attack" else: status = "Move" if action.target is not None: if isinstance(action.target, Unit): status += f": {action.target.type_id.name}" else: status += f": {action.target}" status += f" G: {group.debug_index}" status += f"\n{move_type.name}" pos3d: Point3 = unit.position3d pos3d = Point3((pos3d.x, pos3d.y, pos3d.z + 2)) self.ai._client.debug_text_world(status, pos3d, size=10)
def should_force_field(self, position: Point2) -> Optional[Action]: for ff in self.cache.force_fields: # type: EffectData for position in ff.positions: if position.distance_to(position) < 1.5: return None for ff_pos in self.upcoming_fields: # type: Point2 if ff_pos.distance_to_point2(position) < 1.5: return None self.upcoming_fields.append(position) return Action(position, False, AbilityId.FORCEFIELD_FORCEFIELD)
def unit_solve_combat(self, unit: Unit, current_command: Action) -> Action: if isinstance(current_command.target, Unit): target_pos = current_command.target.position else: target_pos = current_command.target target = self.pather.find_path(self.group.center, target_pos, 8) enemies = self.cache.enemy_in_range(target, 12) if enemies: if self.cd_manager.is_ready( unit.tag, AbilityId.SPAWNCHANGELING_SPAWNCHANGELING): return Action(None, False, AbilityId.SPAWNCHANGELING_SPAWNCHANGELING) for enemy in enemies: # type: Unit if enemy.detect_range > 0 and enemy.detect_range > target.distance_to( enemy): target = self.pather.find_weak_influence_air(target, 10) break return Action(target, False)
def unit_solve_combat(self, unit: Unit, current_command: Action) -> Action: if self.engage_ratio < 0.25 and self.can_engage_ratio < 0.25: return current_command if self.move_type in { MoveType.PanicRetreat, MoveType.DefensiveRetreat }: return current_command if self.cd_manager.is_ready( unit.tag, AbilityId.EFFECT_VOIDRAYPRISMATICALIGNMENT): close_enemies = self.cache.enemy_in_range( unit.position, 7).filter(lambda u: u.is_armored) if close_enemies: return Action(None, False, AbilityId.EFFECT_VOIDRAYPRISMATICALIGNMENT) if not self.should_shoot() and self.should_retreat(unit): pos = self.pather.find_weak_influence_air(unit.position, 4) return Action(pos, False) return self.focus_fire(unit, current_command, None)
def relay_order(self, tank: Unit, order: AbilityId, time: float) -> Optional[Action]: if order is None: self.clear_order() return None if self.requested_status == order: # Do the change when delay matches request delay = self.delay if order == AbilityId.SIEGEMODE_SIEGEMODE: delay = delay * 0.25 if time > self.requested_time + delay: return Action(None, False, self.requested_status) else: self.requested_status = order self.requested_time = time return None
def unit_solve_combat(self, unit: Unit, current_command: Action) -> Action: if unit.has_buff(BuffId.CHARGING): return NoAction() ground_units = self.enemies_near_by.not_flying if not ground_units: current_command.is_attack = False return current_command # if self.knowledge.enemy_race == Race.Protoss: # if self.engage_percentage < 0.25: # buildings = self.enemies_near_by.sorted_by_distance_to(unit) # if buildings: # if buildings.first.health + buildings.first.shield < 200: # return Action(buildings.first, True) # pylons = buildings(UnitTypeId.PYLON) # if pylons: # return Action(buildings.first, True) return current_command
def stay_safe(self, unit: Unit, current_command: Action): # TODO: Backstep pos = self.pather.find_weak_influence_ground(unit.position, 5) return Action(pos, False)
def unit_solve_combat(self, unit: Unit, current_command: Action) -> Action: if self.move_type == MoveType.DefensiveRetreat or self.move_type == MoveType.PanicRetreat: if self.ready_to_shoot(unit): closest = self.closest_units.get(unit.tag, None) if closest: real_range = self.unit_values.real_range(unit, closest, self.knowledge) if 0 < real_range < unit.distance_to(closest): return Action(closest.position, True) return current_command # Phoenixes are generally faster than the rest of the army if self.engage_ratio < 0.25 and self.can_engage_ratio < 0.25: if self.group.ground_units: # Regroup with the ground army return Action(self.group.center, False) has_energy = unit.energy > GRAVITON_BEAM_ENERGY if has_energy and self.allow_lift: best_target: Optional[Unit] = None best_score: float = 0 close_enemies = self.cache.enemy_in_range(unit.position, 14) for enemy in close_enemies: # type: Unit if enemy.is_flying or enemy.is_structure or enemy.has_buff(BuffId.GRAVITONBEAM): continue if self.move_type != MoveType.Harass and enemy.type_id in self.unit_values.worker_types: # If we are not doing any harass, don't lift low priority workers up. # We need to prioritize energy to actual combat units continue pos: Point2 = enemy.position score = self.lift_priority.get(enemy.type_id, -1) + (1 - pos.distance_to(unit) / 10) if score > best_score: best_target = enemy best_score = score if best_target: if best_score > 5 or not close_enemies.flying.exists: self.print(f"Phoenix at {unit.position} lifting {best_target.type_id} at {best_target.position}") if unit.distance_to(best_target) > 8: destination = self.knowledge.pathing_manager.find_influence_air_path(unit.position, best_target.position) return Action(destination, False) return Action(best_target, False, AbilityId.GRAVITONBEAM_GRAVITONBEAM) if self.engage_ratio < 0.25 and self.can_engage_ratio < 0.25: # Not in combat return current_command targets = self.enemies_near_by.flying if targets: closest = targets.closest_to(unit) # d = unit.distance_to(closest) real_range = self.unit_values.real_range(unit, closest, self.knowledge) - 0.5 best_position = self.pather.find_low_inside_air(unit.position, closest.position, real_range) return Action(best_position, False) return current_command
def unit_solve_combat(self, unit: Unit, current_command: Action) -> Action: if self.force_fielding(unit): # Don't do anything if force field is ordered return NoAction() if not self.shield_up and self.should_shield_up and unit.energy >= SHIELD_ENERGY_COST \ and self.last_shield_up + 0.5 < self.ai.time: self.shield_up = True self.last_shield_up = self.ai.time return Action(None, False, AbilityId.GUARDIANSHIELD_GUARDIANSHIELD) if unit.shield_percentage < 0.1: if self.range_power > 5 and unit.energy >= HALLUCINATION_ENERGY_COST: return Action(None, False, AbilityId.HALLUCINATION_ARCHON) if self.melee_power > 5 and unit.energy >= FORCE_FIELD_ENERGY_COST: melee = self.knowledge.unit_cache.enemy(UnitValue.melee) if melee: closest = melee.closest_to(unit) pos = unit.position.towards(closest, 0.6) return Action(pos, False, AbilityId.FORCEFIELD_FORCEFIELD) if self.move_type == MoveType.SearchAndDestroy and unit.energy >= FORCE_FIELD_ENERGY_COST: # Look for defensive force field on ramp or other choke natural: Zone = self.knowledge.expansion_zones[1] main: Zone = self.knowledge.expansion_zones[0] d_natural = unit.distance_to(natural.center_location) d_main = unit.distance_to(main.center_location) if d_natural < 15 and d_natural < d_main and self.closest_group_distance < 10: # Sentry is at the natural zealot_pos: Point2 = self.knowledge.building_solver.zealot_position if self.knowledge.enemy_race == Race.Zerg and natural.our_wall() and zealot_pos: # Protect gate keeper our_keepers = self.cache.own_in_range(zealot_pos, 2).not_structure combined_health = 0 for keeper in our_keepers: # type: Unit combined_health += keeper.health + keeper.shield if combined_health < 70: action = self.should_force_field(zealot_pos.towards(self.closest_group.center, 0.6)) if action: return action if self.model == CombatModel.StalkerToSpeedlings: # Protect buildings buildings = self.cache.own_in_range(unit.position, 8).structure for building in buildings: # type: Unit if building.health + building.shield < 300: action = self.should_force_field(building.position.towards(self.closest_group.center, 1.2)) if action: return action elif not natural.is_ours or natural.power_balance < 0 and d_main < main.radius: # Protect main base ramp not_flying = self.cache.enemy_in_range(self.main_ramp_position, 3).filter(lambda u: not u.is_flying and not u.is_structure) if not_flying: action = self.should_force_field(self.main_ramp_position) if action: return action # and self.model == CombatModel.StalkerToSpeedlings return super().unit_solve_combat(unit, current_command)