def __init__(self, ai: sc2.BotAI, knowledge: 'Knowledge', x: int, y: int, x2: int, y2: int): self.ai = ai self.knowledge = knowledge self.cache: UnitCacheManager = self.knowledge.unit_cache self.bottom_left_x = x self.bottom_left_y = y self.top_right_x = x2 self.top_right_y = y2 self.center = Point2(((x + x2) / 2.0, (y + y2) / 2.0)) self._x, self._y = self.center.rounded self.zone: Optional['Zone'] = None self.heat: float = 0 self.stealth_heat: float = 0 self.last_enemy_power = ExtendedPower(knowledge.unit_values) d2 = 15 for zone in knowledge.expansion_zones: if zone.center_location.distance_to(self.center) < d2: if ai.get_terrain_height( zone.center_location) == ai.get_terrain_height( self.center): # if zone == self.knowledge.own_main_zone: # print("MAIN ZONE") self.zone = zone
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) 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
async def start(self, knowledge: "Knowledge"): await super().start(knowledge) self.cache: UnitCacheManager = self.knowledge.unit_cache self.pather: PathingManager = self.knowledge.pathing_manager self.tags: List[int] = [] self.unit_micros: Dict[UnitTypeId, MicroStep] = dict() self.all_enemy_power = ExtendedPower(self.unit_values) # Micro controllers / handlers self.unit_micros[UnitTypeId.DRONE] = MicroWorkers(knowledge) self.unit_micros[UnitTypeId.PROBE] = MicroWorkers(knowledge) self.unit_micros[UnitTypeId.SCV] = MicroWorkers(knowledge) # Protoss self.unit_micros[UnitTypeId.ARCHON] = NoMicro(knowledge) self.unit_micros[UnitTypeId.ADEPT] = MicroAdepts(knowledge) self.unit_micros[UnitTypeId.CARRIER] = MicroCarriers(knowledge) self.unit_micros[UnitTypeId.COLOSSUS] = MicroColossi(knowledge) self.unit_micros[UnitTypeId.DARKTEMPLAR] = MicroZerglings(knowledge) self.unit_micros[UnitTypeId.DISRUPTOR] = MicroDisruptor(knowledge) self.unit_micros[UnitTypeId.DISRUPTORPHASED] = MicroPurificationNova( knowledge) self.unit_micros[UnitTypeId.HIGHTEMPLAR] = MicroHighTemplars(knowledge) self.unit_micros[UnitTypeId.OBSERVER] = MicroObservers(knowledge) self.unit_micros[UnitTypeId.ORACLE] = MicroOracles(knowledge) self.unit_micros[UnitTypeId.PHOENIX] = MicroPhoenixes(knowledge) self.unit_micros[UnitTypeId.SENTRY] = MicroSentries(knowledge) self.unit_micros[UnitTypeId.STALKER] = MicroStalkers(knowledge) self.unit_micros[UnitTypeId.WARPPRISM] = MicroWarpPrism(knowledge) self.unit_micros[UnitTypeId.VOIDRAY] = MicroVoidrays(knowledge) self.unit_micros[UnitTypeId.ZEALOT] = MicroZealots(knowledge) # Zerg self.unit_micros[UnitTypeId.ZERGLING] = MicroZerglings(knowledge) self.unit_micros[UnitTypeId.ULTRALISK] = NoMicro(knowledge) self.unit_micros[UnitTypeId.OVERSEER] = MicroOverseers(knowledge) self.unit_micros[UnitTypeId.QUEEN] = MicroQueens(knowledge) self.unit_micros[UnitTypeId.RAVAGER] = MicroRavagers(knowledge) self.unit_micros[UnitTypeId.LURKERMP] = MicroLurkers(knowledge) self.unit_micros[UnitTypeId.INFESTOR] = MicroInfestors(knowledge) self.unit_micros[UnitTypeId.SWARMHOSTMP] = MicroSwarmHosts(knowledge) self.unit_micros[UnitTypeId.LOCUSTMP] = NoMicro(knowledge) self.unit_micros[UnitTypeId.LOCUSTMPFLYING] = NoMicro(knowledge) self.unit_micros[UnitTypeId.VIPER] = MicroVipers(knowledge) # Terran self.unit_micros[UnitTypeId.HELLIONTANK] = NoMicro(knowledge) self.unit_micros[UnitTypeId.SIEGETANK] = MicroTanks(knowledge) self.unit_micros[UnitTypeId.VIKINGFIGHTER] = MicroVikings(knowledge) self.unit_micros[UnitTypeId.MARINE] = MicroBio(knowledge) self.unit_micros[UnitTypeId.MARAUDER] = MicroBio(knowledge) self.unit_micros[UnitTypeId.BATTLECRUISER] = MicroBattleCruisers( knowledge) self.unit_micros[UnitTypeId.RAVEN] = MicroRavens(knowledge) self.unit_micros[UnitTypeId.MEDIVAC] = MicroMedivacs(knowledge) self.generic_micro = GenericMicro(knowledge) self.regroup_threshold = 0.75
async def start(self, knowledge: "Knowledge"): await super().start(knowledge) self.enemy_predicter: EnemyArmyPredicter = knowledge.enemy_army_predicter self.our_power = ExtendedPower(self.unit_values) self.enemy_power: ExtendedPower = ExtendedPower(self.unit_values) self.enemy_predict_power: ExtendedPower = ExtendedPower(self.unit_values) self.resource_updater = IntervalFunc(self.ai, self.save_resources_status, 1) self.resource_updater.execute()
async def start(self, knowledge: "Knowledge"): await super().start(knowledge) self.cache: UnitCacheManager = self.knowledge.unit_cache self.pather: PathingManager = self.knowledge.pathing_manager self.tags: List[int] = [] self.all_enemy_power = ExtendedPower(self.unit_values) await self.default_rules.start(knowledge)
def calc_needs_evacuation(self): """ Checks if the zone needs evacuation for the workers mining there. This is a method because it is quite CPU heavy. """ enemies: Units = self.cache.enemy_in_range(self.mineral_line_center, 10) power = ExtendedPower(self.unit_values) power.add_units(enemies) if power.ground_power > 3 and enemies.exclude_type(self.unit_values.worker_types): self.needs_evacuation = True else: self.needs_evacuation = False
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_values) for type_id in self._known_enemy_units_dict: if self.unit_values.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
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)
class HeatArea: def __init__(self, ai: sc2.BotAI, knowledge: 'Knowledge', x: int, y: int, x2: int, y2: int): self.ai = ai self.knowledge = knowledge self.cache: UnitCacheManager = self.knowledge.unit_cache self.bottom_left_x = x self.bottom_left_y = y self.top_right_x = x2 self.top_right_y = y2 self.center = Point2(((x + x2) / 2.0, (y + y2) / 2.0)) self._x, self._y = self.center.rounded self.zone: Optional['Zone'] = None self.heat: float = 0 self.stealth_heat: float = 0 self.last_enemy_power = ExtendedPower(knowledge.unit_values) d2 = 15 for zone in knowledge.expansion_zones: if zone.center_location.distance_to(self.center) < d2: if ai.get_terrain_height( zone.center_location) == ai.get_terrain_height( self.center): # if zone == self.knowledge.own_main_zone: # print("MAIN ZONE") self.zone = zone def update(self, time_change: float): if self.stealth_heat > 0: self.stealth_heat = min( 2, max(0, (self.stealth_heat - time_change) * (1 - time_change * 0.5))) if self.heat > 0: if self.is_visible(): self.heat = max(0, (self.heat - time_change * 0.02) * (1 - time_change * 0.5)) else: self.heat = max(0, (self.heat - time_change * 0.01) * (1 - time_change * 0.25)) self.heat += self.last_enemy_power.power * time_change self.last_enemy_power.clear() def is_visible(self) -> bool: # TODO: Check all corners? return self.ai.state.visibility.data_numpy[self._y, self._x] == 2
def _should_attack(self, power: ExtendedPower) -> bool: if self.attack_on_advantage: if ((self.game_analyzer.our_army_predict in at_least_clear_advantage and self.game_analyzer.our_income_advantage in at_least_small_disadvantage) or (self.game_analyzer.our_army_predict in at_least_small_advantage and self.game_analyzer.our_income_advantage in at_least_clear_disadvantage)): # Our army is bigger but economy is weaker, attack! return True if ((self.game_analyzer.our_army_predict in at_least_small_disadvantage and self.game_analyzer.our_income_advantage in at_least_clear_advantage) or (self.game_analyzer.our_army_predict in at_least_clear_disadvantage and self.game_analyzer.our_income_advantage in at_least_small_advantage)): # Our army is smaller but economy is better, focus on defence! return False enemy_total_power: ExtendedPower = self.knowledge.enemy_units_manager.enemy_total_power enemy_total_power.multiply(ENEMY_TOTAL_POWER_MULTIPLIER) multiplier = ENEMY_TOTAL_POWER_MULTIPLIER zone_count = 0 for zone in self.knowledge.expansion_zones: # type: Zone if zone.is_enemys: zone_count += 1 enemy_main: Zone = self.knowledge.expansion_zones[-1] enemy_natural: Zone = self.knowledge.expansion_zones[-2] if zone_count == 1 and enemy_main.is_enemys: # We should seriously consider whether we want to crash and burn against a one base defense enemy_total_power.add_units(enemy_main.enemy_static_defenses) #multiplier *= 2 elif zone_count == 2 and enemy_natural.is_enemys: enemy_total_power.add_units(enemy_natural.enemy_static_defenses) # if (self.knowledge.enemy_race == Race.Terran # and self.knowledge.enemy_units_manager.unit_count(UnitTypeId.SIEGETANK) > 1): # multiplier = 1.6 enemy_total_power.power = max(self.start_attack_power, enemy_total_power.power) if power.is_enough_for(enemy_total_power, 1 / multiplier): self.print( f"Power {power.power:.2f} is larger than required attack power {enemy_total_power.power:.2f} -> attack!" ) return True if self.ai.supply_used > 190: self.print(f"Supply is {self.ai.supply_used} -> attack!") return True return False
def get_defenders(self, power: ExtendedPower, position: Point2) -> Units: units = Units([], self.ai) current_power = ExtendedPower(self.unit_values) self._defenders_from(UnitTask.Idle, current_power, position, power, units) self._defenders_from(UnitTask.Moving, current_power, position, power, units) self._defenders_from(UnitTask.Fighting, current_power, position, power, units) self._defenders_from(UnitTask.Attacking, current_power, position, power, units) return units
def calc_total_power(self, units: Units) -> ExtendedPower: """Calculates total power for the given units (either own or enemy).""" total_power = ExtendedPower(self) if not units.exists: return total_power first_owner_id = None for unit in units: if first_owner_id and unit.owner_id and not unit.owner_id == first_owner_id: logging.warning( f"Unit owner id does not match. tag: {unit.tag} type: {unit.type_id} " + f"owner id: {unit.type_id} (expected {first_owner_id}") continue if unit.can_be_attacked: first_owner_id = unit.owner_id total_power.add_unit(unit) return total_power
def __init__(self, units: Units, knowledge: "Knowledge"): self.knowledge = knowledge self.unit_values = knowledge.unit_values self.units = units self.center: Point2 = sc2math.unit_geometric_median(units) self.ground_units = self.units.not_flying if self.ground_units: self.center: Point2 = self.ground_units.closest_to( (self.center)).position self.power = ExtendedPower(self.unit_values) self.power.add_units(self.units) self.debug_index = 0 self._total_distance: Optional[float] = None self._area_by_circles: float = 0 self.average_speed = 0 for unit in self.units: self.average_speed += knowledge.unit_values.real_speed(unit) if len(self.units) > 1: self.average_speed /= len(self.units)
def __init__(self, knowledge): self.knowledge: "Knowledge" = knowledge self.ai: sc2.BotAI = knowledge.ai self.unit_values: UnitValue = knowledge.unit_values self.cd_manager: CooldownManager = knowledge.cooldown_manager self.pather: PathingManager = knowledge.pathing_manager self.cache: UnitCacheManager = knowledge.unit_cache self.delay_to_shoot = self.ai._client.game_step + 1.5 self.enemy_groups: List[CombatUnits] = [] self.ready_to_attack_ratio: float = 0.0 self.center: Point2 = Point2((0, 0)) self.group: CombatUnits self.engage_ratio = 0 self.can_engage_ratio = 0 self.closest_group: CombatUnits self.engaged: Dict[int, List[int]] = dict() self.engaged_power = ExtendedPower(knowledge.unit_values) self.our_power = ExtendedPower(knowledge.unit_values) self.closest_units: Dict[int, Optional[Unit]] = dict() self.move_type = MoveType.Assault self.attack_range = 0 self.enemy_attack_range = 0 self.focus_fired: Dict[int, float] = dict()
class GameAnalyzer(ManagerBase): def __init__(self): super().__init__() self._enemy_air_percentage = 0 self._our_income_advantage = 0 self._our_predicted_army_advantage = 0 self._our_predicted_tech_advantage = 0 self.enemy_gas_income = 0 self.enemy_mineral_income = 0 self.our_zones = 0 self.enemy_zones = 0 self.our_power: ExtendedPower = None self.enemy_power: ExtendedPower = None self.enemy_predict_power: ExtendedPower = None self.predicted_defeat_time = 0.0 self.minerals_left: List[int] = [] self.vespene_left: List[int] = [] self.resource_updater: IntervalFunc = None self._last_income: Advantage = Advantage.Even self._last_army: Advantage = Advantage.Even self._last_predict: Advantage = Advantage.Even async def start(self, knowledge: "Knowledge"): await super().start(knowledge) self.enemy_predicter: EnemyArmyPredicter = knowledge.enemy_army_predicter self.our_power = ExtendedPower(self.unit_values) self.enemy_power: ExtendedPower = ExtendedPower(self.unit_values) self.enemy_predict_power: ExtendedPower = ExtendedPower(self.unit_values) self.resource_updater = IntervalFunc(self.ai, self.save_resources_status, 1) self.resource_updater.execute() def save_resources_status(self): self.minerals_left.append(self.ai.minerals) self.vespene_left.append(self.ai.vespene) async def update(self): self.resource_updater.execute() self.our_power.clear() self.our_zones = 0 self.enemy_zones = 0 our_income = self.knowledge.income_calculator.mineral_income + self.knowledge.income_calculator.gas_income if self.knowledge.my_worker_type is None: # random and we haven't seen enemy race yat enemy_workers = 12 else: enemy_workers = self.knowledge.enemy_units_manager.enemy_worker_count if not self.knowledge.enemy_main_zone.is_scouted_at_least_once: enemy_workers += 12 mineral_fields = 0 for zone in self.knowledge.zone_manager.expansion_zones: # type: Zone if zone.is_enemys: self.enemy_zones += 1 mineral_fields += len(zone.mineral_fields) if zone.is_ours: self.our_zones += 1 built_vespene = len(self.cache.enemy(self.unit_values.gas_miners)) self._enemy_gas_income = min(enemy_workers, built_vespene * 3) * GAS_MINE_RATE workers_on_minerals = min(mineral_fields * 2, enemy_workers - built_vespene * 3) workers_on_minerals = max(0, workers_on_minerals) self.enemy_mineral_income = workers_on_minerals enemy_income = self.enemy_mineral_income + self._enemy_gas_income self._our_income_advantage = our_income - enemy_income self.our_power.add_units( self.ai.units.filter(lambda u: u.is_ready and u.type_id != self.knowledge.my_worker_type) ) self.enemy_predict_power = self.enemy_predicter.predicted_enemy_power self.enemy_power = self.enemy_predicter.enemy_power self._enemy_air_percentage = 0 if self.enemy_predict_power.air_presence > 0: self._enemy_air_percentage = self.enemy_predict_power.air_power / self.enemy_predict_power.power being_defeated = self.predicting_defeat if being_defeated and self.predicted_defeat_time == 0.0: self.predicted_defeat_time = self.ai.time elif not being_defeated and self.predicted_defeat_time != 0.0: self.predicted_defeat_time = 0 income = self._calc_our_income_advantage() army = self._calc_our_army_advantage() predict = self._calc_our_army_predict() if self._last_income != income: self.print(f"Income advantage is now {income.name}") if self._last_army != army: self.print(f"Known army advantage is now {army.name}") if self._last_predict != predict: self.print(f"Predicted army advantage is now {predict.name}") self._last_income = income self._last_army = army self._last_predict = predict async def post_update(self): if self.debug: msg = f"Our income: {self.knowledge.income_calculator.mineral_income} / {round(self.knowledge.income_calculator.gas_income)}" msg += f"\nEnemy income: {self.enemy_mineral_income} / {round(self.enemy_gas_income)}" msg += ( f"\nResources: {round(self._our_income_advantage)}+{self.our_zones - self.enemy_zones}" f" ({self.our_income_advantage.name})" ) msg += ( f"\nArmy: {round(self.our_power.power)} vs" f" {round(self.enemy_power.power)} ({self.our_army_advantage.name})" ) msg += ( f"\nArmy predict: {round(self.our_power.power)} vs" f" {round(self.enemy_predict_power.power)} ({self.our_army_predict.name})" ) msg += f"\nEnemy air: {self.enemy_air.name}" self.client.debug_text_2d(msg, Point2((0.4, 0.15)), None, 14) @property def our_income_advantage(self) -> Advantage: return self._last_income def _calc_our_income_advantage(self) -> Advantage: number = self._our_income_advantage + (self.our_zones - self.enemy_zones) * 10 if number > 40: return Advantage.OverwhelmingAdvantage if number < -40: return Advantage.OverwhelmingDisadvantage if number > 20: return Advantage.ClearAdvantage if number < -20: return Advantage.ClearDisadvantage if number > 10: return Advantage.SmallAdvantage if number < -10: return Advantage.SmallDisadvantage if number > 5: return Advantage.SlightAdvantage if number < -5: return Advantage.SlightDisadvantage return Advantage.Even @property def army_at_least_clear_disadvantage(self) -> bool: return self.our_army_predict in at_least_clear_disadvantage @property def army_at_least_small_disadvantage(self) -> bool: return self.our_army_predict in at_least_small_disadvantage @property def army_at_least_clear_advantage(self) -> bool: return self.our_army_predict in at_least_clear_advantage @property def army_at_least_small_advantage(self) -> bool: return self.our_army_predict in at_least_small_advantage @property def army_at_least_advantage(self) -> bool: return self.our_army_predict in at_least_advantage @property def army_can_survive(self) -> bool: return self.our_army_predict not in at_least_small_disadvantage @property def predicting_victory(self) -> bool: return ( self.our_army_predict == Advantage.OverwhelmingAdvantage and self.our_income_advantage == Advantage.OverwhelmingAdvantage ) @property def been_predicting_defeat_for(self) -> float: if self.predicted_defeat_time == 0: return 0 return self.ai.time - self.predicted_defeat_time @property def predicting_defeat(self) -> bool: return self.our_army_predict == Advantage.OverwhelmingDisadvantage and ( self.ai.supply_workers < 5 or self.our_income_advantage == Advantage.OverwhelmingDisadvantage ) @property def our_army_predict(self) -> Advantage: return self._last_predict def _calc_our_army_predict(self) -> Advantage: if self.our_power.is_enough_for(self.enemy_predict_power, our_percentage=1 / 1.1): if self.our_power.power > 20 and self.our_power.is_enough_for( self.enemy_predict_power, our_percentage=1 / 3 ): return Advantage.OverwhelmingAdvantage if self.our_power.power > 10 and self.our_power.is_enough_for( self.enemy_predict_power, our_percentage=1 / 2 ): return Advantage.ClearAdvantage if self.our_power.power > 5 and self.our_power.is_enough_for( self.enemy_predict_power, our_percentage=1 / 1.4 ): return Advantage.SmallAdvantage return Advantage.SlightAdvantage if self.enemy_predict_power.is_enough_for(self.our_power, our_percentage=1 / 1.1): if self.enemy_predict_power.power > 20 and self.enemy_predict_power.is_enough_for( self.our_power, our_percentage=1 / 3 ): return Advantage.OverwhelmingDisadvantage if self.enemy_predict_power.power > 10 and self.enemy_predict_power.is_enough_for( self.our_power, our_percentage=1 / 2 ): return Advantage.ClearDisadvantage if self.enemy_predict_power.power > 5 and self.enemy_predict_power.is_enough_for( self.our_power, our_percentage=1 / 1.4 ): return Advantage.SmallDisadvantage return Advantage.SlightDisadvantage return Advantage.Even @property def our_army_advantage(self) -> Advantage: return self._last_army def _calc_our_army_advantage(self) -> Advantage: if self.our_power.is_enough_for(self.enemy_power, our_percentage=1 / 1.1): if self.our_power.power > 20 and self.our_power.is_enough_for(self.enemy_power, our_percentage=1 / 3): return Advantage.OverwhelmingAdvantage if self.our_power.power > 10 and self.our_power.is_enough_for(self.enemy_power, our_percentage=1 / 2): return Advantage.ClearAdvantage if self.our_power.power > 5 and self.our_power.is_enough_for(self.enemy_power, our_percentage=1 / 1.4): return Advantage.SmallAdvantage return Advantage.SlightAdvantage if self.enemy_power.is_enough_for(self.our_power, our_percentage=1 / 1.1): if self.enemy_power.power > 20 and self.enemy_power.is_enough_for(self.our_power, our_percentage=1 / 3): return Advantage.OverwhelmingDisadvantage if self.enemy_power.power > 10 and self.enemy_power.is_enough_for(self.our_power, our_percentage=1 / 2): return Advantage.ClearDisadvantage if self.enemy_power.power > 5 and self.enemy_power.is_enough_for(self.our_power, our_percentage=1 / 1.4): return Advantage.SmallDisadvantage return Advantage.SlightDisadvantage return Advantage.Even @property def enemy_air(self) -> AirArmy: if self._enemy_air_percentage > 0.90: return AirArmy.AllAir if self._enemy_air_percentage > 0.65: return AirArmy.AlmostAllAir if self._enemy_air_percentage > 0.35: return AirArmy.Mixed if self._enemy_air_percentage > 0: return AirArmy.SomeAir return AirArmy.NoAir async def on_end(self, game_result: Result): own_types: List[UnitTypeId] = [] own_types_left: Dict[UnitTypeId, int] = {} enemy_types: List[UnitTypeId] = [] enemy_types_left: Dict[UnitTypeId, int] = {} lost_data = self.knowledge.lost_units_manager.get_own_enemy_lost_units() own_lost: Dict[UnitTypeId, List[Unit]] = lost_data[0] enemy_lost: Dict[UnitTypeId, List[Unit]] = lost_data[1] for unit_type, units in self.cache.own_unit_cache.items(): # type: (UnitTypeId, Units) type_id = self.unit_values.real_type(unit_type) if type_id not in own_types: own_types.append(type_id) val = own_types_left.get(type_id, 0) own_types_left[type_id] = val + units.amount for unit_count in self.knowledge.enemy_units_manager.enemy_composition: # type: UnitCount unit_type = unit_count.enemy_type if unit_type not in enemy_types: enemy_types.append(unit_type) val = enemy_types_left.get(unit_type, 0) enemy_types_left[unit_type] = val + unit_count.count for unit_type, units in own_lost.items(): # type: (UnitTypeId, List[Unit]) if unit_type not in own_types: own_types.append(unit_type) for unit_type, units in enemy_lost.items(): # type: (UnitTypeId, List[Unit]) if unit_type not in enemy_types: enemy_types.append(unit_type) self.print_end("Own units:") self._print_by_type(own_types, own_lost, own_types_left) self.print_end("Enemy units:") self._print_by_type(enemy_types, enemy_lost, enemy_types_left) maxed_minerals = max(self.minerals_left) avg_minerals = sum(self.minerals_left) / len(self.minerals_left) maxed_gas = max(self.vespene_left) avg_gas = sum(self.vespene_left) / len(self.vespene_left) self.print_end(f"Minerals max {maxed_minerals} Average {round(avg_minerals)}") self.print_end(f"Vespene max {maxed_gas} Average {round(avg_gas)}") def _print_by_type( self, types: List[UnitTypeId], lost_units: Dict[UnitTypeId, List[Unit]], left_units: Dict[UnitTypeId, int] ): def get_counts(unit_type: UnitTypeId) -> tuple: dead = len(lost_units.get(unit_type, [])) alive = left_units.get(unit_type, 0) total = dead + alive return total, alive, dead # Sort types by total count types = sorted(types, key=lambda t: get_counts(t)[0], reverse=True) for unit_type in types: counts = get_counts(unit_type) self.print_end( f"{str(unit_type.name).ljust(17)} " f"total: {str(counts[0]).rjust(3)} " f"alive: {str(counts[1]).rjust(3)} " f"dead: {str(counts[2]).rjust(3)} " ) def print_end(self, msg: str): self.knowledge.print(msg, "GameAnalyzerEnd", stats=False)
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
def enemy_static_power(self) -> ExtendedPower: """Returns power of enemy static defenses.""" power = ExtendedPower(self.unit_values) power.add_units(self.enemy_static_defenses) return power
class MicroStep(ABC, Component): engaged_power: ExtendedPower our_power: ExtendedPower delay_to_shoot: float enemies_near_by: Units closest_group: CombatUnits closest_group_distance: float def __init__(self): self.enemy_groups: List[CombatUnits] = [] self.ready_to_attack_ratio: float = 0.0 self.center: Point2 = Point2((0, 0)) self.group: CombatUnits self.engage_ratio = 0 self.can_engage_ratio = 0 self.closest_group: CombatUnits self.engaged: Dict[int, List[int]] = dict() self.closest_units: Dict[int, Optional[Unit]] = dict() self.move_type = MoveType.Assault self.attack_range = 0 self.enemy_attack_range = 0 self.focus_fired: Dict[int, float] = dict() async def start(self, knowledge: "Knowledge"): await super().start(knowledge) self.engaged_power = ExtendedPower(knowledge.unit_values) self.our_power = ExtendedPower(knowledge.unit_values) def init_group( self, rules: "MicroRules", group: CombatUnits, units: Units, enemy_groups: List[CombatUnits], move_type: MoveType, ): self.rules = rules self.focus_fired.clear() self.group = group self.move_type = move_type self.our_power = group.power self.closest_units.clear() self.engaged_power.clear() self.rules.init_group_func(self, group, units, enemy_groups, move_type) def ready_to_shoot(self, unit: Unit) -> bool: return self.rules.ready_to_shoot_func(self, unit) def group_solve_combat(self, units: Units, current_command: Action) -> Action: return self.rules.group_solve_combat_func(self, units, current_command) def unit_solve_combat(self, unit: Unit, current_command: Action) -> Action: return self.rules.unit_solve_combat_func(self, unit, current_command) def focus_fire(self, unit: Unit, current_command: Action, prio: Optional[Dict[UnitTypeId, int]]) -> Action: return self.rules.focus_fire_func(self, unit, current_command, prio) def melee_focus_fire( self, unit: Unit, current_command: Action, prio: Optional[Dict[UnitTypeId, int]] = None) -> Action: return self.rules.melee_focus_fire_func(self, unit, current_command, prio) def last_targeted(self, unit: Unit) -> Optional[int]: if unit.orders: # action: UnitCommand # current_action: UnitOrder current_action = unit.orders[0] # targeting unit if isinstance(current_action.target, int): # tag found return current_action.target return None def is_locked_on(self, unit: Unit) -> bool: if unit.has_buff(BuffId.LOCKON): return True return False def is_target(self, unit: Unit) -> bool: return not unit.is_memory and unit.can_be_attacked and not unit.is_hallucination and not unit.is_snapshot
def unit_solve_combat(self, unit: Unit, current_command: Action) -> Action: prism = unit warpgates: Units = self.knowledge.unit_cache.own(UnitTypeId.WARPGATE) count = len(warpgates) ready = 0 for gate in warpgates: # type: Unit if gate.is_transforming or self.cd_manager.is_ready( gate.tag, AbilityId.WARPGATETRAIN_ZEALOT): ready += 1 if (ready > 2 and ready >= count and unit.type_id == UnitTypeId.WARPPRISM and self.ai.supply_left > 3 and self.cd_manager.is_ready( prism.tag, AbilityId.MORPH_WARPPRISMTRANSPORTMODE, 6)): # TODO: Is it safe to warp in? self.cd_manager.used_ability(prism.tag, AbilityId.MORPH_WARPPRISMPHASINGMODE) return Action(None, False, AbilityId.MORPH_WARPPRISMPHASINGMODE) elif unit.type_id == UnitTypeId.WARPPRISMPHASING: not_ready = self.knowledge.unit_cache.own( self.unit_values.gate_types).not_ready if self.cd_manager.is_ready( prism.tag, AbilityId.MORPH_WARPPRISMPHASINGMODE, 2.5) and (len(not_ready) < 1 or not_ready.closest_distance_to(prism) > 4): self.cd_manager.used_ability( prism.tag, AbilityId.MORPH_WARPPRISMTRANSPORTMODE) return Action(None, False, AbilityId.MORPH_WARPPRISMTRANSPORTMODE) if prism.cargo_used: for passenger in prism.passengers: # type: Unit if self.release_tags.get(passenger.tag, 0) < self.ai.time: if not self.ai.in_pathing_grid(prism): break stop_drop = False for enemy in self.knowledge.unit_cache.enemy_in_range( prism.position, 4): # type: Unit if enemy.radius + 1 > prism.distance_to(enemy): stop_drop = True break if stop_drop: break # return CombatAction(prism, passenger, False, AbilityId.UNLOADALLAT_WARPPRISM) return Action(prism, False, AbilityId.UNLOADALLAT_WARPPRISM) power = ExtendedPower(self.unit_values) power.add_units( self.knowledge.unit_cache.enemy_in_range(prism.position, 12)) if power.power < 3: return self.find_safe_position(current_command) if prism.cargo_left and prism.shield > 0 and prism.shield + prism.health > 50: best_score = 0 best_unit = None for own_unit in self.group.units: # type: Unit if own_unit.cargo_size > prism.cargo_left: continue if own_unit.shield: continue if own_unit.weapon_cooldown < 2: continue if own_unit.distance_to(prism) > 12: continue score = (self.unit_values.ground_range(own_unit) * (1.1 - own_unit.health_percentage) * self.unit_values.power(own_unit) - 1) if score > best_score: best_score = score best_unit = own_unit if best_unit is not None: self.release_tags[ best_unit. tag] = self.ai.time + best_unit.weapon_cooldown / 22.4 return Action(best_unit, False, AbilityId.SMART) return self.find_safe_position(current_command)
class GroupCombatManager(ManagerBase): def __init__(self): # How much distance must be between units to consider them to be in different groups self.own_group_threshold = 7 super().__init__() async def start(self, knowledge: "Knowledge"): await super().start(knowledge) self.cache: UnitCacheManager = self.knowledge.unit_cache self.pather: PathingManager = self.knowledge.pathing_manager self.tags: List[int] = [] self.unit_micros: Dict[UnitTypeId, MicroStep] = dict() self.all_enemy_power = ExtendedPower(self.unit_values) # Micro controllers / handlers self.unit_micros[UnitTypeId.DRONE] = MicroWorkers(knowledge) self.unit_micros[UnitTypeId.PROBE] = MicroWorkers(knowledge) self.unit_micros[UnitTypeId.SCV] = MicroWorkers(knowledge) # Protoss self.unit_micros[UnitTypeId.ARCHON] = NoMicro(knowledge) self.unit_micros[UnitTypeId.ADEPT] = MicroAdepts(knowledge) self.unit_micros[UnitTypeId.CARRIER] = MicroCarriers(knowledge) self.unit_micros[UnitTypeId.COLOSSUS] = MicroColossi(knowledge) self.unit_micros[UnitTypeId.DARKTEMPLAR] = MicroZerglings(knowledge) self.unit_micros[UnitTypeId.DISRUPTOR] = MicroDisruptor(knowledge) self.unit_micros[UnitTypeId.DISRUPTORPHASED] = MicroPurificationNova(knowledge) self.unit_micros[UnitTypeId.HIGHTEMPLAR] = MicroHighTemplars(knowledge) self.unit_micros[UnitTypeId.OBSERVER] = MicroObservers(knowledge) self.unit_micros[UnitTypeId.ORACLE] = MicroOracles(knowledge) self.unit_micros[UnitTypeId.PHOENIX] = MicroPhoenixes(knowledge) self.unit_micros[UnitTypeId.SENTRY] = MicroSentries(knowledge) self.unit_micros[UnitTypeId.STALKER] = MicroStalkers(knowledge) self.unit_micros[UnitTypeId.WARPPRISM] = MicroWarpPrism(knowledge) self.unit_micros[UnitTypeId.VOIDRAY] = MicroVoidrays(knowledge) self.unit_micros[UnitTypeId.ZEALOT] = MicroZealots(knowledge) # Zerg self.unit_micros[UnitTypeId.ZERGLING] = MicroZerglings(knowledge) self.unit_micros[UnitTypeId.ULTRALISK] = NoMicro(knowledge) self.unit_micros[UnitTypeId.OVERSEER] = MicroOverseers(knowledge) self.unit_micros[UnitTypeId.QUEEN] = MicroQueens(knowledge) self.unit_micros[UnitTypeId.RAVAGER] = MicroRavagers(knowledge) self.unit_micros[UnitTypeId.ROACH] = MicroRoaches(knowledge) self.unit_micros[UnitTypeId.LURKERMP] = MicroLurkers(knowledge) self.unit_micros[UnitTypeId.INFESTOR] = MicroInfestors(knowledge) self.unit_micros[UnitTypeId.SWARMHOSTMP] = MicroSwarmHosts(knowledge) self.unit_micros[UnitTypeId.LOCUSTMP] = NoMicro(knowledge) self.unit_micros[UnitTypeId.LOCUSTMPFLYING] = NoMicro(knowledge) self.unit_micros[UnitTypeId.VIPER] = MicroVipers(knowledge) # Terran self.unit_micros[UnitTypeId.HELLIONTANK] = NoMicro(knowledge) self.unit_micros[UnitTypeId.SIEGETANK] = MicroTanks(knowledge) self.unit_micros[UnitTypeId.VIKINGFIGHTER] = MicroVikings(knowledge) self.unit_micros[UnitTypeId.MARINE] = MicroBio(knowledge) self.unit_micros[UnitTypeId.MARAUDER] = MicroBio(knowledge) self.unit_micros[UnitTypeId.BATTLECRUISER] = MicroBattleCruisers(knowledge) self.unit_micros[UnitTypeId.RAVEN] = MicroRavens(knowledge) self.unit_micros[UnitTypeId.MEDIVAC] = MicroMedivacs(knowledge) self.generic_micro = GenericMicro(knowledge) self.regroup_threshold = 0.75 async def update(self): self.enemy_groups: List[CombatUnits] = self.group_enemy_units() self.all_enemy_power.clear() for group in self.enemy_groups: # type: CombatUnits self.all_enemy_power.add_units(group.units) async def post_update(self): pass @property def debug(self): return self._debug and self.knowledge.debug def add_unit(self, unit: Unit): if unit.type_id in ignored: # Just no return self.tags.append(unit.tag) def add_units(self, units: Units): for unit in units: self.add_unit(unit) def get_all_units(self) -> Units: units = Units([], self.ai) for tag in self.tags: unit = self.cache.by_tag(tag) if unit: units.append(unit) return units def execute(self, target: Point2, move_type=MoveType.Assault): our_units = self.get_all_units() if len(our_units) < 1: return self.own_groups: List[CombatUnits] = self.group_own_units(our_units) total_power = ExtendedPower(self.unit_values) for group in self.own_groups: total_power.add_power(group.power) if self.debug: fn = lambda group: group.center.distance_to(self.ai.start_location) sorted_list = sorted(self.own_groups, key=fn) for i in range(0, len(sorted_list)): sorted_list[i].debug_index = i for group in self.own_groups: center = group.center closest_enemies = group.closest_target_group(self.enemy_groups) own_closest_group = self.closest_group(center, self.own_groups) if closest_enemies is None: if move_type == MoveType.PanicRetreat: self.move_to(group, target, move_type) else: self.attack_to(group, target, move_type) else: power = group.power enemy_power = ExtendedPower(closest_enemies) is_in_combat = group.is_in_combat(closest_enemies) if move_type == MoveType.DefensiveRetreat or move_type == MoveType.PanicRetreat: self.move_to(group, target, move_type) break if power.power > self.regroup_threshold * total_power.power: # Most of the army is here if group.is_too_spread_out() and not is_in_combat: self.regroup(group, group.center) else: self.attack_to(group, target, move_type) elif is_in_combat: if not power.is_enough_for(enemy_power, 0.75): # Regroup if possible if own_closest_group: self.move_to(group, own_closest_group.center, MoveType.ReGroup) else: # fight to bitter end self.attack_to(group, closest_enemies.center, move_type) else: self.attack_to(group, closest_enemies.center, move_type) else: # if self.faster_group_should_regroup(group, own_closest_group): # self.move_to(group, own_closest_group.center, MoveType.ReGroup) if group.power.is_enough_for(self.all_enemy_power, 0.85): # We have enough units here to crush everything the enemy has self.attack_to(group, closest_enemies.center, move_type) else: # Regroup if possible if move_type == MoveType.Assault: # Group towards attack target own_closest_group = self.closest_group(target, self.own_groups) else: # Group up with closest group own_closest_group = self.closest_group(center, self.own_groups) if own_closest_group: self.move_to(group, own_closest_group.center, MoveType.ReGroup) else: # fight to bitter end self.attack_to(group, closest_enemies.center, move_type) self.tags.clear() def faster_group_should_regroup(self, group1: CombatUnits, group2: Optional[CombatUnits]) -> bool: if not group2: return False if group1.average_speed < group2.average_speed + 0.1: return False # Our group is faster, it's a good idea to regroup return True def regroup(self, group: CombatUnits, target: Union[Unit, Point2]): if isinstance(target, Unit): target = self.pather.find_path(group.center, target.position, 1) else: target = self.pather.find_path(group.center, target, 3) self.move_to(group, target, MoveType.Push) def move_to(self, group: CombatUnits, target, move_type: MoveType): self.action_to(group, target, move_type, False) def attack_to(self, group: CombatUnits, target, move_type: MoveType): self.action_to(group, target, move_type, True) 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: real_type = self.unit_values.real_type(unit.type_id) units = own_unit_cache.get(real_type, Units([], self.ai)) if units.amount == 0: 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: final_action = micro.unit_solve_combat(unit, group_action) order = final_action.to_commmand(unit) if order: self.ai.do(order) if self.debug: if final_action.debug_comment: status = final_action.debug_comment elif final_action.ability: status = final_action.ability.name elif final_action.is_attack: status = "Attack" else: status = "Move" if final_action.target is not None: if isinstance(final_action.target, Unit): status += f": {final_action.target.type_id.name}" else: status += f": {final_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 closest_group(self, start: Point2, combat_groups: List[CombatUnits]) -> Optional[CombatUnits]: group = None best_distance = 50 # doesn't find enemy groups closer than this for combat_group in combat_groups: center = combat_group.center if center == start: continue # it's the same group! distance = start.distance_to(center) if distance < best_distance: best_distance = distance group = combat_group return group def group_own_units(self, our_units: Units) -> List[CombatUnits]: lookup_distance = self.own_group_threshold groups: List[Units] = [] assigned: Dict[int, int] = dict() for unit in our_units: if unit.tag in assigned: continue units = Units([unit], self.ai) index = len(groups) assigned[unit.tag] = index groups.append(units) self.include_own_units(unit, units, lookup_distance, index, assigned) return [CombatUnits(u, self.knowledge) for u in groups] def include_own_units(self, unit: Unit, units: Units, lookup_distance: float, index: int, assigned: Dict[int, int]): units_close_by = self.cache.own_in_range(unit.position, lookup_distance) for unit_close in units_close_by: if unit_close.tag in assigned or unit_close.tag not in self.tags: continue assigned[unit_close.tag] = index units.append(unit_close) self.include_own_units(unit_close, units, lookup_distance, index, assigned) def group_enemy_units(self) -> List[CombatUnits]: groups: List[Units] = [] assigned: Dict[int, int] = dict() lookup_distance = 7 for unit in self.knowledge.known_enemy_units_mobile: if unit.tag in assigned or unit.type_id in self.unit_values.combat_ignore or not unit.can_be_attacked: continue units = Units([unit], self.ai) index = len(groups) assigned[unit.tag] = index groups.append(units) self.include_enemy_units(unit, units, lookup_distance, index, assigned) return [CombatUnits(u, self.knowledge) for u in groups] def include_enemy_units( self, unit: Unit, units: Units, lookup_distance: float, index: int, assigned: Dict[int, int] ): units_close_by = self.cache.enemy_in_range(unit.position, lookup_distance) for unit_close in units_close_by: if unit_close.tag in assigned or unit_close.tag not in self.tags or not unit.can_be_attacked: continue assigned[unit_close.tag] = index units.append(unit_close) self.include_enemy_units(unit_close, units, lookup_distance, index, assigned)
def execute(self, target: Point2, move_type=MoveType.Assault): our_units = self.get_all_units() if len(our_units) < 1: return self.own_groups: List[CombatUnits] = self.group_own_units(our_units) total_power = ExtendedPower(self.unit_values) for group in self.own_groups: total_power.add_power(group.power) if self.debug: fn = lambda group: group.center.distance_to(self.ai.start_location) sorted_list = sorted(self.own_groups, key=fn) for i in range(0, len(sorted_list)): sorted_list[i].debug_index = i for group in self.own_groups: center = group.center closest_enemies = group.closest_target_group(self.enemy_groups) own_closest_group = self.closest_group(center, self.own_groups) if closest_enemies is None: if move_type == MoveType.PanicRetreat: self.move_to(group, target, move_type) else: self.attack_to(group, target, move_type) else: power = group.power enemy_power = ExtendedPower(closest_enemies) is_in_combat = group.is_in_combat(closest_enemies) if move_type == MoveType.DefensiveRetreat or move_type == MoveType.PanicRetreat: self.move_to(group, target, move_type) break if power.power > self.regroup_threshold * total_power.power: # Most of the army is here if group.is_too_spread_out() and not is_in_combat: self.regroup(group, group.center) else: self.attack_to(group, target, move_type) elif is_in_combat: if not power.is_enough_for(enemy_power, 0.75): # Regroup if possible if own_closest_group: self.move_to(group, own_closest_group.center, MoveType.ReGroup) else: # fight to bitter end self.attack_to(group, closest_enemies.center, move_type) else: self.attack_to(group, closest_enemies.center, move_type) else: # if self.faster_group_should_regroup(group, own_closest_group): # self.move_to(group, own_closest_group.center, MoveType.ReGroup) if group.power.is_enough_for(self.all_enemy_power, 0.85): # We have enough units here to crush everything the enemy has self.attack_to(group, closest_enemies.center, move_type) else: # Regroup if possible if move_type == MoveType.Assault: # Group towards attack target own_closest_group = self.closest_group(target, self.own_groups) else: # Group up with closest group own_closest_group = self.closest_group(center, self.own_groups) if own_closest_group: self.move_to(group, own_closest_group.center, MoveType.ReGroup) else: # fight to bitter end self.attack_to(group, closest_enemies.center, move_type) self.tags.clear()
def _should_attack(self, power: ExtendedPower) -> bool: if self.attack_on_advantage and self.ai.supply_used < 190: if (self.game_analyzer.our_army_predict in at_least_clear_advantage and self.game_analyzer.our_income_advantage in at_least_small_disadvantage) or ( self.game_analyzer.our_army_predict in at_least_small_advantage and self.game_analyzer.our_income_advantage in at_least_clear_disadvantage): # Our army is bigger but economy is weaker, attack! return True if (self.game_analyzer.our_army_predict in at_least_small_advantage and self.game_analyzer.our_income_advantage in at_least_small_advantage): # Our army is bigger and economy is better, attack! return True if (self.game_analyzer.our_army_predict in at_least_small_disadvantage and self.game_analyzer.our_income_advantage in at_least_clear_advantage) or ( self.game_analyzer.our_army_predict in at_least_clear_disadvantage and self.game_analyzer.our_income_advantage in at_least_small_advantage): # Our army is smaller but economy is better, focus on defence! return False enemy_total_power: ExtendedPower = self.knowledge.enemy_units_manager.enemy_total_power enemy_total_power.multiply(ENEMY_TOTAL_POWER_MULTIPLIER) multiplier = ENEMY_TOTAL_POWER_MULTIPLIER zone_count = 0 for zone in self.knowledge.expansion_zones: # type: Zone if zone.is_enemys: zone_count += 1 enemy_main: Zone = self.knowledge.expansion_zones[-1] enemy_natural: Zone = self.knowledge.expansion_zones[-2] if zone_count < self.cache.own_townhalls.amount and self.knowledge.enemy_race == Race.Terran: multiplier *= 1.1 if zone_count > 2 and self.cache.own_townhalls.amount > 2: multiplier = 1.1 if zone_count == 1 and enemy_main.is_enemys: # We should seriously consider whether we want to crash and burn against a one base defense enemy_total_power.add_units(enemy_main.enemy_static_defenses.ready) if self.ai.time <= 6 * 60 + 20 and self.knowledge.enemy_race != Race.Zerg: snap_shot_num = self.cache.enemy( [UnitTypeId.SIEGETANKSIEGED, UnitTypeId.CYCLONE, UnitTypeId.BUNKER])\ .filter(lambda u: u.is_snapshot).amount if self.knowledge.enemy_units_manager.unit_count(UnitTypeId.SIEGETANK) + \ self.knowledge.enemy_units_manager.unit_count(UnitTypeId.SIEGETANKSIEGED) + \ self.knowledge.enemy_units_manager.unit_count(UnitTypeId.CYCLONE) + \ self.knowledge.enemy_units_manager.unit_count(UnitTypeId.LIBERATORAG) + \ self.knowledge.enemy_units_manager.unit_count(UnitTypeId.BUNKER) + \ snap_shot_num >= 1: multiplier *= 4.2 return False else: multiplier *= 3.8 return False if self.cache.own_townhalls.amount == 2: multiplier *= 1.8 return False elif zone_count == 2 and enemy_natural.is_enemys: enemy_total_power.add_units(enemy_natural.enemy_static_defenses) if self.cache.own_townhalls.amount == 1 and self.knowledge.enemy_race != Race.Zerg: multiplier *= 0.4 if self.cache.own_townhalls.amount == 2 and self.knowledge.enemy_race != Race.Zerg: multiplier *= 0.7 if self.cache.own_townhalls.amount == 2 and self.knowledge.enemy_race == Race.Terran: multiplier *= 1.1 return False if self.cache.own_townhalls.amount == 2 and self.knowledge.enemy_race == Race.Protoss: multiplier *= 1.1 if self.cache.own_townhalls.ready.amount == 3 and self.knowledge.enemy_race == Race.Terran: multiplier *= 2.4 if self.cache.own_townhalls.ready.amount == 3 and self.knowledge.enemy_race == Race.Protoss: multiplier *= 1.4 if (self.knowledge.enemy_race == Race.Terran and (self.knowledge.enemy_units_manager.unit_count( UnitTypeId.PLANETARYFORTRESS) >= 1 or self.knowledge.enemy_units_manager.unit_count( UnitTypeId.BUNKER) >= 1)): multiplier *= 2.2 elif (self.knowledge.enemy_race == Race.Terran and (self.knowledge.enemy_units_manager.unit_count( UnitTypeId.SIEGETANK) >= 1 or self.knowledge.enemy_units_manager.unit_count( UnitTypeId.WIDOWMINE) >= 1)): multiplier *= 2 elif zone_count == 3 and enemy_natural.is_enemys: if self.cache.own_townhalls.amount == 2: multiplier *= 0.5 if self.cache.own_townhalls.amount == 3 and self.knowledge.enemy_race == Race.Terran: multiplier *= 0.6 if (self.knowledge.enemy_race == Race.Terran and (self.knowledge.enemy_units_manager.unit_count( UnitTypeId.SIEGETANK) >= 1 or self.knowledge.enemy_units_manager.unit_count( UnitTypeId.WIDOWMINE) >= 1)): multiplier *= 1.2 if zone_count - self.cache.own_townhalls.amount >= 2 and self.knowledge.enemy_race == Race.Terran: multiplier = 0.8 if zone_count - self.cache.own_townhalls.amount > 1 and self.knowledge.enemy_race == Race.Zerg: multiplier *= 0.7 if self.ai.time >= 4 * 60 + 45 and self.knowledge.enemy_race == Race.Zerg: multiplier *= 0.3 if self.ai.time < 4 * 60 + 45 and self.knowledge.enemy_race == Race.Zerg: multiplier *= 20 if self.ai.time >= 8 * 60 and self.knowledge.enemy_race != Race.Zerg: multiplier = 1.1 enemy_total_power.power = max(self.start_attack_power, enemy_total_power.power) if power.is_enough_for(enemy_total_power, 1 / multiplier): self.print( f"Power {power.power:.2f} is larger than required attack power {enemy_total_power.power:.2f} -> attack!" f"--------------------------------------------------------------------{1 / multiplier:.2f}" ) return True if self.ai.supply_used >= 188: self.print(f"Supply is {self.ai.supply_used} -> attack!") return True return False
class CombatUnits: def __init__(self, units: Units, knowledge: "Knowledge"): self.knowledge = knowledge self.unit_values = knowledge.unit_values self.units = units self.center: Point2 = sc2math.unit_geometric_median(units) self.ground_units = self.units.not_flying if self.ground_units: self.center: Point2 = self.ground_units.closest_to( (self.center)).position self.power = ExtendedPower(self.unit_values) self.power.add_units(self.units) self.debug_index = 0 self._total_distance: Optional[float] = None self._area_by_circles: float = 0 self.average_speed = 0 for unit in self.units: self.average_speed += knowledge.unit_values.real_speed(unit) if len(self.units) > 1: self.average_speed /= len(self.units) def is_too_spread_out(self) -> bool: if self._total_distance is None: self._total_distance = 0 self._area_by_circles = 5 for unit in self.units: d = unit.distance_to(self.center) if unit.energy_percentage >= 0.4: d *= 2 self._total_distance += d self._area_by_circles += unit.radius**2 # self.knowledge.print(f"spread: {self._total_distance} d to {self._total_radius} r") return (self._total_distance / len(self.units))**2 > self._area_by_circles * 2 def is_in_combat(self, closest_enemies: "CombatUnits") -> bool: if closest_enemies is None: return False distance = self.center.distance_to_point2(closest_enemies.center) if distance > 17: return False if distance < 10 or self.knowledge.unit_cache.enemy_in_range( self.center, 10).exclude_type(self.unit_values.combat_ignore): return True engaged_power = 0 total_power = 0 for unit in self.units: # type: Unit power = self.unit_values.power(unit) total_power += power for enemy_near in closest_enemies.units: d = enemy_near.distance_to(unit) if d < self.unit_values.real_range(unit, enemy_near): engaged_power += power break return engaged_power > total_power * 0.15 def closest_target_group( self, combat_groups: List["CombatUnits"]) -> Optional["CombatUnits"]: group = None start = self.center best_distance = 50 # doesn't find enemy groups closer than this shoots_air = self.power.air_power > 0 shoots_ground = self.power.ground_power > 0 for combat_group in combat_groups: if not combat_group.ground_units and not shoots_air: continue # We can't shoot the targets here if combat_group.power.air_presence == 0 and combat_group.power.ground_presence > 0 and not shoots_ground: continue # We can't shoot the targets here if combat_group.power.air_presence > 0 and combat_group.power.ground_presence == 0 and not shoots_air: continue # We can't shoot the targets here center = combat_group.center distance = start.distance_to(center) if distance < best_distance: best_distance = distance group = combat_group return group
async def start(self, knowledge: "Knowledge"): await super().start(knowledge) self.engaged_power = ExtendedPower(knowledge.unit_values) self.our_power = ExtendedPower(knowledge.unit_values)
def handle_groups(combat: "GroupCombatManager", target: Point2, move_type=MoveType.Assault): total_power = ExtendedPower(combat.unit_values) for group in combat.own_groups: total_power.add_power(group.power) for group in combat.own_groups: if not combat.rules.regroup or combat.regroup_threshold <= 0: # Skip all regroup logic if move_type == MoveType.PanicRetreat: combat.move_to(group, target, move_type) else: combat.attack_to(group, target, move_type) continue center = group.center closest_enemies = group.closest_target_group(combat.enemy_groups) own_closest_group = combat.closest_group(center, combat.own_groups) if closest_enemies is None: if move_type == MoveType.PanicRetreat: combat.move_to(group, target, move_type) else: combat.attack_to(group, target, move_type) else: power = group.power enemy_power = ExtendedPower(closest_enemies) is_in_combat = group.is_in_combat(closest_enemies) if move_type == MoveType.DefensiveRetreat or move_type == MoveType.PanicRetreat: combat.move_to(group, target, move_type) break if power.power > combat.regroup_threshold * total_power.power: # Most of the army is here if group.is_too_spread_out( ) and not is_in_combat and enemy_power.power > 5: combat.regroup(group, group.center) else: combat.attack_to(group, target, move_type) elif is_in_combat: if not power.is_enough_for(enemy_power, 0.75): # Regroup if possible if own_closest_group: combat.move_to(group, own_closest_group.center, MoveType.ReGroup) else: # fight to bitter end combat.attack_to(group, closest_enemies.center, move_type) else: combat.attack_to(group, closest_enemies.center, move_type) else: # if self.faster_group_should_regroup(group, own_closest_group): # self.move_to(group, own_closest_group.center, MoveType.ReGroup) if group.power.is_enough_for(combat.all_enemy_power, 0.85): # We have enough units here to crush everything the enemy has combat.attack_to(group, closest_enemies.center, move_type) else: # Regroup if possible if move_type == MoveType.Assault: # Group towards attack target own_closest_group = combat.closest_group( target, combat.own_groups) else: # Group up with closest group own_closest_group = combat.closest_group( center, combat.own_groups) if own_closest_group: combat.move_to(group, own_closest_group.center, MoveType.ReGroup) else: # fight to bitter end combat.attack_to(group, closest_enemies.center, move_type)
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): positions = map(lambda u: u.position, 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 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])
async def execute(self) -> bool: unit: Unit all_defenders = self.knowledge.roles.all_from_task(UnitTask.Defending) for index in range(0, len(self.knowledge.expansion_zones)): zone: "Zone" = self.knowledge.expansion_zones[index] zone_tags = self.defender_tags[index] 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 not 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[index] + 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[index] = 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) if enemies.exists: # is main force if enemies.amount >= 7: 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) # or (len(enemies) == 1 and enemies[0].type_id not in self.unit_values.worker_types) if len(enemies) >= 1: # 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.roles.refresh_tags(self.combat.tags) self.combat.execute(enemy_center, MoveType.SearchAndDestroy) return True # never block
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_dict.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, 3) 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)
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)