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,
                )
Example #2
0
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
Example #3
0
 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()
Example #4
0
    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
Example #5
0
    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()
Example #7
0
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)
Example #8
0
    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)
Example #9
0
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
Example #10
0
 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)
Example #11
0
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
Example #12
0
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