async def start(self, knowledge: "Knowledge"): await super().start(knowledge) self.enabled = self.ai.opponent_id is not None self.enable_write = self.knowledge.config["general"].getboolean( "write_data") self.file_name = DATA_FOLDER + os.sep + str( self.ai.opponent_id) + ".json" self.updater = IntervalFunc(self.ai, lambda: self.real_update(), 1) self.result = GameResult() self.result.my_race = knowledge.my_race self.result.enemy_race = knowledge.enemy_race if self.enabled: self.result.game_started = datetime.now().isoformat() my_file = Path(self.file_name) if my_file.is_file(): try: self.read_data() except Exception: self.data = OpponentData() self.data.enemy_id = self.ai.opponent_id self.knowledge.print("Data read failed on game start.") else: self.data = OpponentData() self.data.enemy_id = self.ai.opponent_id if self.data.results: self.last_result = self.data.results[-1] self.last_result_as_current_race = next( (result for result in reversed(self.data.results) if hasattr(result, "my_race") and result.my_race == self.knowledge.my_race), None, )
class WorkerRallyPoint(ActBase): """Handles setting worker rally points""" ability: AbilityId func: IntervalFunc def __init__(self): super().__init__() async def start(self, knowledge: 'Knowledge'): await super().start(knowledge) # set rally point once every 5 seconds self.func = IntervalFunc(self.ai, self.set_rally_point, 5) if self.knowledge.my_race == Race.Terran: self.ability = AbilityId.RALLY_COMMANDCENTER if self.knowledge.my_race == Race.Protoss: self.ability = AbilityId.RALLY_NEXUS if self.knowledge.my_race == Race.Zerg: self.ability = AbilityId.RALLY_HATCHERY_WORKERS def set_rally_point(self): for zone in self.knowledge.our_zones: best_mineral_field = zone.check_best_mineral_field() if zone.our_townhall: if best_mineral_field: self.do(zone.our_townhall(self.ability, best_mineral_field)) else: self.do(zone.our_townhall(self.ability, zone.center_location)) async def execute(self) -> bool: self.func.execute() return True
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) # set rally point once every 5 seconds self.func = IntervalFunc(self.ai, self.set_rally_point, 5) if self.knowledge.my_race == Race.Terran: self.ability = AbilityId.RALLY_COMMANDCENTER if self.knowledge.my_race == Race.Protoss: self.ability = AbilityId.RALLY_NEXUS if self.knowledge.my_race == Race.Zerg: self.ability = AbilityId.RALLY_HATCHERY_WORKERS
def __init__(self, ai: sc2.BotAI, knowledge: "Knowledge"): self.ai = ai self.knowledge = knowledge self.cache: UnitCacheManager = self.knowledge.unit_cache self.unit_values: "UnitValue" = knowledge.unit_values self.updater = IntervalFunc(ai, self.__real_update, 0.5) grid: PixelMap = knowledge.ai._game_info.placement_grid height = grid.height width = grid.width self.slots_w = int(math.ceil(width / SLOT_SIZE)) self.slots_h = int(math.ceil(height / SLOT_SIZE)) self.heat_areas: List[HeatArea] = [] for y in range(0, self.slots_h): for x in range(0, self.slots_w): x2 = min(x * SLOT_SIZE + SLOT_SIZE, width - 1) y2 = min(y * SLOT_SIZE + SLOT_SIZE, height - 1) self.heat_areas.append(HeatArea(ai, knowledge, x * SLOT_SIZE, y * SLOT_SIZE, x2, y2)) self.last_update = 0 self.last_quick_update = 0
class DataManager(ManagerBase): data: OpponentData enabled: bool enable_write: bool last_result: Optional[GameResult] last_result_as_race: Optional[GameResult] def __init__(self): self.last_result = None super().__init__() async def start(self, knowledge: "Knowledge"): await super().start(knowledge) self.enabled = self.ai.opponent_id is not None self.enable_write = self.knowledge.config["general"].getboolean( "write_data") self.file_name = DATA_FOLDER + os.sep + str( self.ai.opponent_id) + ".json" self.updater = IntervalFunc(self.ai, lambda: self.real_update(), 1) self.result = GameResult() self.result.my_race = knowledge.my_race self.result.enemy_race = knowledge.enemy_race if self.enabled: self.result.game_started = datetime.now().isoformat() my_file = Path(self.file_name) if my_file.is_file(): try: self.read_data() except Exception: self.data = OpponentData() self.data.enemy_id = self.ai.opponent_id self.knowledge.print("Data read failed on game start.") else: self.data = OpponentData() self.data.enemy_id = self.ai.opponent_id if self.data.results: self.last_result = self.data.results[-1] self.last_result_as_current_race = next( (result for result in reversed(self.data.results) if hasattr(result, "my_race") and result.my_race == self.knowledge.my_race), None, ) def read_data(self): with open(self.file_name, "r") as handle: text = handle.read() # Compatibility with older versions to prevent crashes text = text.replace("bot.tools", "sharpy.tools") text = text.replace("frozen.tools", "sharpy.tools") self.data = jsonpickle.decode(text) async def update(self): pass async def post_update(self): if self.enabled: self.updater.execute() def real_update(self): if self.result.first_attacked is None: for zone in self.knowledge.expansion_zones: if zone.is_ours and zone.known_enemy_power.power > 10: self.result.first_attacked = self.ai.time # Pre emptive write in case on end does not trigger properly if self.result.result != 1 and self.knowledge.game_analyzer.predicting_victory: self.write_victory() elif self.result.result != -1 and self.knowledge.game_analyzer.predicting_defeat: self.write_defeat() @property def last_enemy_build(self) -> Tuple[EnemyRushBuild, EnemyMacroBuild]: if (not self.last_result or not hasattr(self.last_result, "enemy_macro_build") or not hasattr(self.last_result, "enemy_build")): return EnemyRushBuild.Macro, EnemyMacroBuild.StandardMacro return EnemyRushBuild(self.last_result.enemy_build), EnemyMacroBuild( self.last_result.enemy_macro_build) def set_build(self, build_name: str): self.result.build_used = build_name def write_defeat(self): self.result.result = -1 self.solve_write_data() def write_victory(self): self.result.result = 1 self.solve_write_data() def solve_write_data(self): self.result.enemy_build = int(self.knowledge.build_detector.rush_build) self.result.enemy_macro_build = int( self.knowledge.build_detector.macro_build) self.result.game_duration = self.ai.time self.result.bot_version = get_version() self.write_results() def write_results(self): if not self.enable_write: return my_file = Path(self.file_name) if my_file.is_file(): try: self.read_data() except Exception as e: # Don't write if we can't read the current data self.print(f"Data read failed on save: {e}") return elif not os.path.exists(DATA_FOLDER): os.makedirs(DATA_FOLDER) to_remove = None for result in self.data.results: if result.guid == self.result.guid: to_remove = result break if to_remove: self.data.results.remove(to_remove) self.data.results.append(self.result) frozen = jsonpickle.encode(self.data) try: with open(self.file_name, "w") as handle: handle.write(frozen) # pickle.dump(self.data, handle, protocol=pickle.HIGHEST_PROTOCOL) except Exception as e: self.print(f"Data write failed: {e}") async def on_end(self, game_result: Result): if not self.enabled: return if game_result == Result.Victory: self.result.result = 1 elif game_result == Result.Tie: self.result.result = 0 elif game_result == Result.Defeat: self.result.result = -1 self.result.game_duration = self.ai.time self.write_results()
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 __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 Zone: #ZONE_RADIUS = 18 #ZONE_DANGER_RADIUS = 30 ZONE_RADIUS = 30 ZONE_DANGER_RADIUS = 40 MAIN_ZONE_RAMP_MAX_RADIUS = 26 ZONE_RAMP_MAX_RADIUS = 15 ZONE_RADIUS_SQUARED = ZONE_RADIUS**2 VESPENE_GEYSER_DISTANCE = 10 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) @property def is_island(self) -> bool: """ Pathing is either blocked by non-walkable areas or with minerals @return: True if the zone is an island """ if self.zone_index == 0 or len(self.paths) == 0: return False return self.paths[0].distance <= 0 def _init_behind_mineral_positions(self) -> List[Point2]: positions: List[Point2] = [] possible_behind_mineral_positions: List[Point2] = [] all_mf: Units = self.ai.mineral_field.closer_than( 10, self.center_location) for mf in all_mf: # type: Unit possible_behind_mineral_positions.append( self.center_location.towards(mf.position, 9)) if all_mf: positions.append(self.center_location.towards(all_mf.center, 9)) # Center positions.insert( 0, positions[0].furthest(possible_behind_mineral_positions)) positions.append( positions[0].furthest(possible_behind_mineral_positions)) return positions @property def behind_mineral_position_center(self) -> Point2: if self.behind_mineral_positions: return self.behind_mineral_positions[1] return self.center_location @property def mineral_line_center(self) -> Point2: if self.behind_mineral_positions: return self.behind_mineral_positions[1].towards( self.center_location, 4) return self.center_location 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 _count_minerals(self): total_minerals = 0 nearby_mineral_fields = self.mineral_fields for mf in nearby_mineral_fields: # type: Unit if mf.is_mineral_field: if mf.is_visible: total_minerals += mf.mineral_contents else: # if the last 3 character end in 750, then it's 900 mineral patch, otherwise 1800 if "750" == mf.type_id.name[-3:]: total_minerals += 900 else: total_minerals += 1800 if self.last_minerals > total_minerals: # Set new standard only if less than last time the minerals were seen self.last_minerals = total_minerals @property def resources(self) -> ZoneResources: """Rough amount of mineral resources that are left on the zone.""" if self.last_minerals >= 10000: return ZoneResources.Full elif self.last_minerals >= 5000: return ZoneResources.Plenty elif self.last_minerals >= 1500: return ZoneResources.Limited elif self.last_minerals > 0: return ZoneResources.NearEmpty else: return ZoneResources.Empty def update(self): self.mineral_fields.clear() for mf in self._original_mineral_fields: new_mf = self.cache.mineral_fields.get(mf.position, None) if new_mf: self.mineral_fields.append(new_mf) self.our_power.clear() self.known_enemy_power.clear() self.assaulting_enemy_power.clear() # Own and enemy units are figured out in zone manager update. # Only add units that we can fight against # self.known_enemy_units = self.known_enemy_units.filter(lambda x: x.cloak != 2) self.known_enemy_units = self.known_enemy_units self.enemy_workers = self.known_enemy_units.of_type( self.unit_values.worker_types) self.our_workers: Units = self.our_units.of_type( self.unit_values.worker_types) self._minerals_counter.execute() self._update_gas_buildings() self.update_our_townhall() self.update_enemy_townhall() if self.ai.is_visible(self.center_location): self.last_scouted_center = self.knowledge.ai.time if self.ai.is_visible(self.mineral_line_center): self.last_scouted_mineral_line = self.knowledge.ai.time for unit in self.our_units: # Our unit is inside the zone self.our_power.add_unit(unit) for unit in self.known_enemy_units: # Enemy unit is inside the zone self.known_enemy_power.add_unit(unit) if self.is_ours: self.calc_needs_evacuation() self.assaulting_enemies: Units = self.cache.enemy_in_range( self.center_location, self.danger_radius) self.assaulting_enemy_power.add_units(self.assaulting_enemies) else: self.needs_evacuation = False self.assaulting_enemies.clear() def check_best_mineral_field(self) -> Optional[Unit]: best_score = 0 best_mf: Optional[Unit] = None for mf in self.mineral_fields: # type: Unit score = mf.mineral_contents for worker in self.our_workers: # type: Unit if worker.order_target == mf.tag: score -= 1000 if score > best_score or best_mf is None: best_mf = mf best_score = score return best_mf def update_enemy_worker_status(self): if self.is_ours: self.could_have_enemy_workers_in = self.ai.time + 5 * 60 if self.ai.is_visible( self.behind_mineral_position_center.towards( self.center_location, 3)): if self.is_enemys: if self.enemy_workers: self.could_have_enemy_workers_in = 0 elif self.enemy_townhall: if self.enemy_townhall.is_ready: self.could_have_enemy_workers_in = self.ai.time + 60 else: finish_time = self.unit_values.building_completion_time( self.ai.time, self.enemy_townhall.type_id, self.enemy_townhall.build_progress) self.could_have_enemy_workers_in = finish_time + 60 else: if self.is_scouted_at_least_once: if not self.is_neutral: self.could_have_enemy_workers_in = ( self.last_scouted_center + self.unit_values.build_time(UnitTypeId.NEXUS) + 90) else: self.could_have_enemy_workers_in = 3 * 60 def _update_gas_buildings(self): self.gas_buildings = self.ai.gas_buildings.closer_than( Zone.VESPENE_GEYSER_DISTANCE, self.center_location) def update_our_townhall(self): friendly_townhalls = self.cache.own_townhalls.closer_than( 5, self.center_location) if friendly_townhalls.exists: self.our_townhall = friendly_townhalls.closest_to( self.center_location) else: self.our_townhall = None def update_enemy_townhall(self): enemy_townhalls = self.cache.enemy_townhalls.not_flying.closer_than( 5, self.center_location) if enemy_townhalls.exists: self.enemy_townhall = enemy_townhalls.closest_to( self.center_location) else: self.enemy_townhall = None # We are going to presume that the enemy has a town hall even if we don't see one self._is_enemys = self.enemy_townhall is not None or ( self == self.knowledge.enemy_main_zone and self in self.knowledge.unscouted_zones) @property def should_expand_here(self) -> bool: resources = self.has_minerals or self.resources == ZoneResources.Limited return resources and not self.is_enemys and self.our_townhall is None @property def has_minerals(self) -> bool: return self.resources != ZoneResources.NearEmpty and self.resources != ZoneResources.Empty @property def minerals_running_low(self) -> bool: return not self.has_minerals or self.resources == ZoneResources.Limited @property def is_enemys(self) -> bool: """ Is there an enemy town hall in this zone? """ return self._is_enemys @property def is_neutral(self) -> bool: return not self.is_ours and not self.is_enemys @property def expanding_to(self) -> bool: return self.knowledge.expanding_to == self @property def is_ours(self) -> bool: """ Is there a town hall of ours in this zone or have we walled it off?""" return self.our_townhall is not None or self.our_wall() @property def is_under_attack(self) -> bool: return ((self.is_ours and self.assaulting_enemy_power.power > 5 or self.power_balance < 1) or self.is_enemys and self.power_balance > 0) @property def safe_expand_here(self) -> bool: return (self.is_neutral or self.is_ours) and self.power_balance > -2 @property def is_scouted_at_least_once(self): return self.last_scouted_center and self.last_scouted_center > 0 @property def power_balance(self) -> float: """Returns the power balance on this zone. Positive power balance indicates we have more units than the enemy, and negative indicates enemy has more units.""" return round(self.our_power.power - self.known_enemy_power.power, 1) @property def our_photon_cannons(self) -> Units: """Returns any of our own static defenses on the zone.""" # todo: make this work for Terran and Zerg and rename return self.our_units(UnitTypeId.PHOTONCANNON) @property def our_batteries(self) -> Units: """Returns shield batteries.""" return self.our_units(UnitTypeId.SHIELDBATTERY) @property def enemy_static_defenses(self) -> Units: """Returns all enemy static defenses on the zone. Both ground and air.""" # Use a set so we don't count eg. the same photon cannon twice. defenses = set() defenses.update(self.enemy_static_ground_defenses) defenses.update(self.enemy_static_air_defenses) return Units(defenses, self.ai) # @property # def enemy_static_defenses_power(self) -> ExtendedPower: # """Returns power of enemy static defenses on the zone. Both ground and air.""" # power = ExtendedPower(self.unit_values) # for static_def in self.enemy_static_defenses: # power.add_unit(static_def) # return power @property def enemy_static_ground_defenses(self) -> Units: """Returns all enemy static ground defenses on the zone.""" return self.known_enemy_units.filter( self.unit_values.is_static_ground_defense) @property 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 @property 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 @property def enemy_static_air_defenses(self) -> Units: """Returns all enemy static air defenses on the zone.""" return self.known_enemy_units.filter( self.unit_values.is_static_air_defense) @property 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 go_mine(self, unit: Unit): self.knowledge.roles.clear_task(unit) if len(self.mineral_fields) > 0: # Go to mine in this zone mf = self.mineral_fields[0] self.ai.do(unit.gather(mf)) elif self.ai.townhalls.exists and self.ai.mineral_field.exists: closest_base = self.ai.townhalls.closest_to(self.center_location) # Go to mine in some other base mf = self.ai.mineral_field.closest_to(closest_base) self.ai.do(unit.gather(mf)) def _find_ramp(self, ai) -> Optional[ExtendedRamp]: if not self.ai.game_info.map_ramps: return None if self.center_location in self.ai.enemy_start_locations or self.center_location == self.ai.start_location: ramps: List[Ramp] = [ ramp for ramp in self.ai.game_info.map_ramps if len(ramp.upper) in {2, 5} and ramp.top_center.distance_to( self.center_location) < Zone.MAIN_ZONE_RAMP_MAX_RADIUS ] if not len(ramps): ramps: List[Ramp] = self.ai.game_info.map_ramps ramp: Ramp = min( ramps, key=(lambda r: self.center_location.distance_to(r.top_center))) if ramp.top_center.distance_to( self.center_location) < Zone.MAIN_ZONE_RAMP_MAX_RADIUS: return ExtendedRamp(ramp, self.ai) else: self.knowledge.print("Main zone ramp not found!", "Zone") """ Ramp going closest to center of the map. """ found_ramp: Optional[ExtendedRamp] = None for map_ramp in ai.game_info.map_ramps: # type: Ramp if map_ramp.top_center == map_ramp.bottom_center: continue # Bugged ramp data if (ai.get_terrain_height(map_ramp.top_center) == self.height and map_ramp.top_center.distance_to( self.center_location) < Zone.ZONE_RAMP_MAX_RADIUS): if found_ramp is None: found_ramp = ExtendedRamp(map_ramp, ai) else: if found_ramp.top_center.distance_to( self.gather_point ) > map_ramp.top_center.distance_to(self.gather_point): found_ramp = ExtendedRamp(map_ramp, ai) return found_ramp def our_wall(self): if self != self.knowledge.expansion_zones[ 0] and self != self.knowledge.expansion_zones[1]: return False # Not main base and not natural wall gate_position: Point2 = self.knowledge.gate_keeper_position if gate_position is not None and self.knowledge.base_ramp.top_center.distance_to( gate_position) < 6: # Main base ramp return False if gate_position is not None and gate_position.distance_to( self.center_location) < 20: if self.our_units.of_type({ UnitTypeId.GATEWAY, UnitTypeId.WARPGATE, UnitTypeId.CYBERNETICSCORE }): # Natural wall should be up return True return False
async def start(self, knowledge: Knowledge): await super().start(knowledge) self.zone_manager = knowledge.zone_manager self.position_updater = IntervalFunc(knowledge.ai, self.update_position, 1)
class WorkerScout(ActBase): """ Selects a scout worker and performs basic scout sweep across start and expansion locations. """ def __init__(self): super().__init__() self.position_updater: IntervalFunc = None self.scout: Unit = None self.scout_tag = None self.enemy_ramp_top_scouted: bool = None # This is used for stuck / unreachable detection self.last_locations: List[Point2] = [] # An ordered list of locations to scout. Current target # is first on the list, with descending priority, ie. # least important location is last. self.scout_locations: List[Point2] = [] async def start(self, knowledge: Knowledge): await super().start(knowledge) self.zone_manager = knowledge.zone_manager self.position_updater = IntervalFunc(knowledge.ai, self.update_position, 1) def update_position(self): if self.scout: self.last_locations.append(self.scout.position) async def select_scout(self): workers = self.knowledge.roles.free_workers if not workers.exists: return if self.scout_tag is None: closest_worker = workers.closest_to(self.current_target) self.scout_tag = closest_worker.tag self.knowledge.roles.set_task(UnitTask.Scouting, closest_worker) self.scout = self.cache.by_tag(self.scout_tag) def distance_to_scout(self, location): # Return sys.maxsize so that the sort function does not crash like it does with None if not self.scout: return sys.maxsize if not location: return sys.maxsize return self.scout.distance_to(location) async def scout_locations_upkeep(self): if len(self.scout_locations) > 0: return enemy_base_found = self.knowledge.enemy_start_location_found enemy_base_scouted = ( enemy_base_found and self.knowledge.enemy_main_zone.is_scouted_at_least_once and self.knowledge.enemy_main_zone.scout_last_circled ) enemy_base_blocked = ( enemy_base_found and self.enemy_ramp_top_scouted and await self.target_unreachable(self.knowledge.enemy_main_zone.behind_mineral_position_center) ) if enemy_base_scouted or enemy_base_blocked: # When enemy found and enemy main base scouted, scout nearby expansions self.scout_enemy_expansions() elif ( enemy_base_found and self.enemy_ramp_top_scouted and self.scout.distance_to(self.knowledge.enemy_main_zone.center_location) < 40 ): self.circle_location(self.zone_manager.enemy_main_zone.center_location) self.zone_manager.enemy_main_zone.scout_last_circled = self.knowledge.ai.time else: self.scout_start_locations() def scout_start_locations(self): self.print("Scouting start locations") self.enemy_ramp_top_scouted = False if self.scout: distance_to = self.scout.position else: distance_to = self.ai.start_location closest_distance = sys.maxsize for zone in self.zone_manager.unscouted_enemy_start_zones: distance = zone.center_location.distance_to(distance_to) # Go closest unscouted zone if distance < closest_distance: self.scout_locations.clear() if zone.ramp: # Go ramp first enemy_ramp_top_center = zone.ramp.top_center self.scout_locations.append(enemy_ramp_top_center) # Go center of zone next self.scout_locations.append(zone.center_location) closest_distance = distance self.print(f"Scouting enemy base at locations {self.scout_locations}") def circle_location(self, location: Point2): self.scout_locations.clear() self.scout_locations = points_on_circumference_sorted(location, self.scout.position, 10, 30) self.print(f"Circling location {location}") def scout_enemy_expansions(self): if not self.zone_manager.enemy_start_location_found: return self.scout_locations.clear() self.scout_locations = map_to_point2s_minerals(self.zone_manager.enemy_expansion_zones[0:5]) self.print(f"Scouting {len(self.scout_locations)} expansions from enemy base towards us") @property def current_target(self) -> Optional[Point2]: if len(self.scout_locations) > 0: return self.scout_locations[0] return None @property def current_target_is_enemy_ramp(self) -> bool: for zone in self.knowledge.expansion_zones: # type: Zone if zone.ramp and self.current_target == zone.ramp.top_center: return True return False async def target_unreachable(self, target) -> bool: if target is None: return False start = self.scout if ( len(self.last_locations) < 5 or self.scout.distance_to(self.last_locations[-1]) > 1 or self.scout.distance_to(self.last_locations[-2]) > 1 ): # Worker is still moving, it's not stuck return False end = target result = await self.ai._client.query_pathing(start, end) return result is None def target_location_reached(self): if len(self.scout_locations) > 0: self.scout_locations.pop(0) async def execute(self) -> bool: await self.scout_locations_upkeep() await self.select_scout() if self.scout is None: # No one to scout return True # Non blocking if not len(self.scout_locations): # Nothing to scout return True # Non blocking self.position_updater.execute() dist = self.distance_to_scout(self.current_target) if self.current_target_is_enemy_ramp: if dist < Constants.SCOUT_DISTANCE_RAMP_THRESHOLD: self.print(f"Enemy ramp at {self.current_target} reached") self.target_location_reached() self.enemy_ramp_top_scouted = True else: if dist < Constants.SCOUT_DISTANCE_THRESHOLD: self.print(f"Target at {self.current_target} reached") self.target_location_reached() if await self.target_unreachable(self.current_target): self.print(f"target {self.current_target} unreachable!") self.target_location_reached() if self.scout is not None and self.current_target is not None: self.do(self.scout.move(self.current_target)) return True # Non blocking
class HeatMap: def __init__(self, ai: sc2.BotAI, knowledge: 'Knowledge'): self.ai = ai self.knowledge = knowledge self.cache: UnitCacheManager = self.knowledge.unit_cache self.unit_values: 'UnitValue' = knowledge.unit_values self.updater = IntervalFunc(ai, self.__real_update, 0.5) grid: PixelMap = knowledge.ai._game_info.placement_grid height = grid.height width = grid.width self.slots_w = int(math.ceil(width / SLOT_SIZE)) self.slots_h = int(math.ceil(height / SLOT_SIZE)) self.heat_areas: List[HeatArea] = [] for y in range(0, self.slots_h): for x in range(0, self.slots_w): x2 = min(x * SLOT_SIZE + SLOT_SIZE, width - 1) y2 = min(y * SLOT_SIZE + SLOT_SIZE, height - 1) self.heat_areas.append( HeatArea(ai, knowledge, x * SLOT_SIZE, y * SLOT_SIZE, x2, y2)) self.last_update = 0 self.last_quick_update = 0 def update(self): self.__stealth_update() self.updater.execute() def __stealth_update(self): time_change = self.ai.time - self.last_quick_update for unit in self.knowledge.known_enemy_units: # type: Unit if unit.is_cloaked: own_close = self.cache.own_in_range(unit.position, 12).not_flying area = self.get_zone(unit.position) if own_close: # Only add to stealth heat if we have a ground unit or building nearby # Stealthed units cannot attack air area.stealth_heat += 1 * time_change def get_zone(self, position: Point2) -> HeatArea: x_int = min(self.slots_w, max(0, math.floor(position.x / SLOT_SIZE))) y_int = min(self.slots_h, max(0, math.floor(position.y / SLOT_SIZE))) return self.heat_areas[x_int + y_int * self.slots_w] def __real_update(self): time_change = self.ai.time - self.last_update self.last_update = self.ai.time for unit in self.knowledge.known_enemy_units_mobile: area = self.get_zone(unit.position) area.last_enemy_power.add_unit(unit) for zone in self.heat_areas: zone.update(time_change) def get_stealth_hotspot(self) -> Optional[Tuple[Point2, float]]: top_heat_position: Point2 = None top_value = 0 for heat_area in self.heat_areas: if heat_area.stealth_heat > top_value: top_heat_position = heat_area.center top_value = heat_area.stealth_heat if top_heat_position is None: return None return top_heat_position, top_value def get_zones_hotspot(self, zones: List['Zone']) -> Optional[Point2]: top_heat_area = None top_value = 0 for heat_area in self.heat_areas: value = heat_area.heat if heat_area.zone in zones and value > 0 and ( top_heat_area is None or value > top_value): top_value = value top_heat_area = heat_area return top_heat_area