Пример #1
0
    def pre_start(self, ai: "KnowledgeBot",
                  additional_managers: Optional[List[ManagerBase]]):
        assert isinstance(ai, sc2.BotAI)
        self.ai: "KnowledgeBot" = ai
        self._set_managers(additional_managers)
        self._all_own: Units = Units([], self.ai)
        self.config: ConfigParser = self.ai.config
        self.logger = sc2.main.logger
        self.is_chat_allowed = self.config["general"].getboolean("chat")
        self._debug = self.config["general"].getboolean("debug")

        self.my_race: Race = self.ai.race
        self.enemy_race: Race = self.ai.enemy_race
        self.enemy_worker_type = self.unit_values.get_worker_type(
            self.enemy_race)

        self.map = MapInfo(self)
        self.close_gates = self.enemy_race == Race.Zerg

        # Cached ai fields:
        self._known_enemy_structures: Units = self.ai.enemy_structures
        self._known_enemy_units: Units = self.ai.enemy_units + self.ai.enemy_structures
        self._known_enemy_units_mobile: Units = self.ai.enemy_units
        self._known_enemy_units_workers: Units = Units([], self.ai)

        self.heat_map = HeatMap(self.ai, self)

        self.my_worker_type = self.unit_values.get_worker_type(self.my_race)
Пример #2
0
    def pre_start(self, ai: sc2.BotAI):
        self.ai: sc2.BotAI = ai
        self._all_own: Units = Units([], self.ai)
        self.config: ConfigParser = self.ai.config
        self.logger = sc2.main.logger
        self.is_chat_allowed = self.config["general"].getboolean("chat")
        self._debug = self.config["general"].getboolean("debug")

        self.my_race: Race = self.ai.race
        self.enemy_race: Race = self.ai.enemy_race
        self.enemy_worker_type = self.unit_values.get_worker_type(
            self.enemy_race)

        self.map = MapInfo(self)
        self.close_gates = self.enemy_race == Race.Zerg

        # Cached ai fields:
        self._known_enemy_structures: Units = self.ai.enemy_structures
        self._known_enemy_units: Units = self.ai.enemy_units + self.ai.enemy_structures
        self._known_enemy_units_mobile: Units = self.ai.enemy_units
        self._known_enemy_units_workers: Units = Units([], self.ai)

        self.heat_map = HeatMap(self.ai, self)

        self.my_worker_type = self.unit_values.get_worker_type(self.my_race)
Пример #3
0
class Knowledge:
    def __init__(self):
        self.ai: sc2.BotAI = None
        self.config: ConfigParser = None
        self._debug: bool = False

        self.iteration = 0

        self.supply_blocked = False

        self.my_worker_type: UnitTypeId = None
        self.enemy_worker_type: Optional[UnitTypeId] = None

        # Information about units in game
        self.unit_values: UnitValue = UnitValue()

        self.rush_distance = 0

        self._all_own: Units = None

        # Base building related
        self.reserved_minerals = 0
        self.reserved_gas = 0
        self.expanding_to: Zone = None

        # Event listeners
        self._on_unit_destroyed_listeners: List[Callable] = list()

        # Managers
        self.unit_cache: UnitCacheManager = UnitCacheManager()
        self.zone_manager: ZoneManager = ZoneManager()
        self.enemy_units_manager: EnemyUnitsManager = EnemyUnitsManager()
        self.cooldown_manager: CooldownManager = CooldownManager()
        self.building_solver = BuildingSolver()
        self.income_calculator = IncomeCalculator()
        self.roles: UnitRoleManager = UnitRoleManager()
        self.build_detector: BuildDetector = BuildDetector()
        self.pathing_manager: PathingManager = PathingManager()
        self.enemy_army_predicter = EnemyArmyPredicter()
        self.lost_units_manager: LostUnitsManager = LostUnitsManager()
        self.game_analyzer: GameAnalyzer = GameAnalyzer()
        self.previous_units_manager: PreviousUnitsManager = PreviousUnitsManager()
        self.data_manager: DataManager = DataManager()
        self.combat_manager: GroupCombatManager = GroupCombatManager()
        self.chat_manager: ChatManager = ChatManager()
        self.memory_manager: MemoryManager = MemoryManager()
        self.action_handler: ActionHandler = ActionHandler()

        self.managers: List[ManagerBase] = [
            self.unit_values,
            self.unit_cache,
            self.action_handler,
            self.pathing_manager,
            self.zone_manager,
            self.enemy_units_manager,
            self.cooldown_manager,
            self.building_solver,
            self.income_calculator,
            self.roles,
            self.build_detector,
            self.enemy_army_predicter,
            self.lost_units_manager,
            self.game_analyzer,
            self.combat_manager,
            self.chat_manager,
            self.previous_units_manager,
            self.data_manager,
            self.memory_manager,
        ]

    # noinspection PyAttributeOutsideInit
    def pre_start(self, ai: sc2.BotAI):
        self.ai: sc2.BotAI = ai
        self._all_own: Units = Units([], self.ai)
        self.config: ConfigParser = self.ai.config
        self.logger = sc2.main.logger
        self.is_chat_allowed = self.config["general"].getboolean("chat")
        self._debug = self.config["general"].getboolean("debug")

        self.my_race: Race = self.ai.race
        self.enemy_race: Race = self.ai.enemy_race
        self.enemy_worker_type = self.unit_values.get_worker_type(self.enemy_race)

        self.map = MapInfo(self)
        self.close_gates = self.enemy_race == Race.Zerg

        # Cached ai fields:
        self._known_enemy_structures: Units = self.ai.enemy_structures
        self._known_enemy_units: Units = self.ai.enemy_units + self.ai.enemy_structures
        self._known_enemy_units_mobile: Units = self.ai.enemy_units
        self._known_enemy_units_workers: Units = Units([], self.ai)

        self.heat_map = HeatMap(self.ai, self)

        self.my_worker_type = self.unit_values.get_worker_type(self.my_race)

    def get_str_setting(self, key: str) -> str:
        """
        Returns a string setting from config.ini matching the key.

        :param key: Key of the setting, eg. "builds.edge_protoss" for "edge_protoss" setting under [builds].
        """
        key = key.split(".")
        return self.config[key[0]].get(key[1])

    def get_int_setting(self, key: str) -> int:
        """
        Returns a boolean setting from config.ini matching the key.

        :param key: Key of the setting, eg. "gameplay.disruptor_max_count" for "disruptor_max_count" setting under [gameplay].
        """
        key = key.split(".")
        return self.config[key[0]].getint(key[1])

    def get_boolean_setting(self, key: str) -> str:
        """
        Returns a boolean setting from config.ini matching the key.

        :param key: Key of the setting, eg. "general.chat" for "chat" setting under [general].
        """
        key = key.split(".")
        return self.config[key[0]].getboolean(key[1])

    @property
    def available_mineral(self) -> int:
        return self.ai.minerals - self.reserved_minerals

    @property
    def available_gas(self) -> int:
        return self.ai.vespene - self.reserved_gas

    @property
    def gate_keeper_position(self) -> Optional[Point2]:
        return self.building_solver.zealot_position

    @property
    def debug(self) -> bool:
        return self._debug

    @property
    def all_own(self) -> Units:
        return self._all_own

    @property
    def known_enemy_structures(self) -> Units:
        return self._known_enemy_structures

    @property
    def known_enemy_units(self) -> Units:
        """Returns all known enemy units and structures."""
        return self._known_enemy_units

    @property
    def known_enemy_units_mobile(self) -> Units:
        return self._known_enemy_units_mobile

    @property
    def known_enemy_workers(self) -> Units:
        return self._known_enemy_units_workers

    @property
    def possible_rush_detected(self) -> bool:
        """ True if scouting indicates that the enemy is preparing an early rush. """
        return self.build_detector.rush_detected

    @property
    def base_ramp(self) -> ExtendedRamp:
        """Own start location ramp. Note that ai.main_base_ramp is incorrect in several maps, so scrap that."""
        return self.expansion_zones[0].ramp

    @property
    def enemy_base_ramp(self) -> ExtendedRamp:
        """Enemy start location ramp, based on our best case of the enemy start location."""
        return self.expansion_zones[-1].ramp

    @property
    def natural_wall(self) -> bool:
        natural = self.zone_manager.expansion_zones[1]
        return natural.is_ours and natural.our_wall()

    async def start(self):
        for manager in self.managers:
            await manager.start(self)

        self.gather_point = self.base_ramp.top_center.towards(self.base_ramp.bottom_center, -4)
        start = self.base_ramp.top_center
        end = self.enemy_base_ramp.top_center
        self.rush_distance = await self.ai._client.query_pathing(start, end)
        self._print(f"rush distance: {self.rush_distance}", stats=False)

    async def update(self, iteration: int):
        if self.close_gates:
            lings = self.enemy_units_manager.unit_count(UnitTypeId.ZERGLING)
            if (
                self.enemy_units_manager.unit_count(UnitTypeId.ROACH) > lings
                or self.enemy_units_manager.unit_count(UnitTypeId.HYDRALISK) > lings
            ):
                self.close_gates = False

        self._all_own: Units = self.ai.units + self.ai.structures
        memory_units = self.memory_manager.ghost_units
        self._known_enemy_structures: Units = self.ai.enemy_structures.filter(
            lambda u: u.is_structure and u.type_id not in self.unit_values.not_really_structure
        )
        self._known_enemy_units: Units = self.ai.enemy_units + self.ai.enemy_structures + memory_units
        self._known_enemy_units_mobile: Units = self.ai.enemy_units + memory_units

        self._known_enemy_units_workers: Units = Units(
            self._known_enemy_units_mobile.of_type(
                [UnitTypeId.SCV, UnitTypeId.PROBE, UnitTypeId.DRONE, UnitTypeId.MULE]
            ),
            self.ai,
        )

        self.iteration = iteration

        for manager in self.managers:
            await manager.update()

        if not self.supply_blocked and self.ai.supply_left == 0:
            self.supply_blocked = True
            self.print(f"Started", "SupplyBlock")
        elif self.supply_blocked and self.ai.supply_left > 0:
            self.supply_blocked = False
            self.print(f"Ended", "SupplyBlock")

        self._find_gather_point()

        # Reserved resources are reseted each iteration
        self.expanding_to = None
        self.reserved_minerals = 0
        self.reserved_gas = 0
        self.heat_map.update()
        self.update_enemy_random()

    def update_enemy_random(self):
        if self.enemy_race == Race.Random:
            if self._known_enemy_units_workers(UnitTypeId.SCV).exists:
                self.enemy_race = Race.Terran
            if self._known_enemy_units_workers(UnitTypeId.DRONE).exists:
                self.enemy_race = Race.Zerg
            if self._known_enemy_units_workers(UnitTypeId.PROBE).exists:
                self.enemy_race = Race.Protoss

            self.enemy_worker_type = self.unit_values.get_worker_type(self.enemy_race)

    def reserve(self, minerals: int, gas: int):
        self.reserved_minerals += minerals
        self.reserved_gas += gas

    def reserve_costs(self, item_id: sc2.Union[UnitTypeId, UpgradeId, AbilityId]):
        if isinstance(item_id, UnitTypeId):
            unit = self.ai._game_data.units[item_id.value]
            cost = self.ai._game_data.calculate_ability_cost(unit.creation_ability)
        elif isinstance(item_id, UpgradeId):
            cost = self.ai._game_data.upgrades[item_id.value].cost
        else:
            cost = self.ai._game_data.calculate_ability_cost(item_id)
        self.reserve(cost.minerals, cost.vespene)

    def can_afford(self, item_id: sc2.Union[UnitTypeId, UpgradeId, AbilityId], check_supply_cost: bool = True) -> bool:
        """Tests if the player has enough resources to build a unit or cast an ability even after reservations."""
        enough_supply = True
        if isinstance(item_id, UnitTypeId):
            unit = self.ai._game_data.units[item_id.value]
            cost = self.ai._game_data.calculate_ability_cost(unit.creation_ability)
            if check_supply_cost:
                enough_supply = self.ai.can_feed(item_id)
        elif isinstance(item_id, UpgradeId):
            cost = self.ai._game_data.upgrades[item_id.value].cost
        else:
            cost = self.ai._game_data.calculate_ability_cost(item_id)
        minerals = self.ai.minerals - self.reserved_minerals
        gas = self.ai.vespene - self.reserved_gas
        return cost.minerals <= minerals and cost.vespene <= max(0, gas) and enough_supply

    def should_attack(self, unit: Unit):
        """Returns boolean whether unit should participate in an attack. Ignores structures, workers and other non attacking types."""
        if unit.type_id in self.unit_values.combat_ignore:
            return False
        if self.my_race == Race.Zerg and unit.type_id == UnitTypeId.QUEEN:
            return False
        if (
            unit.type_id == UnitTypeId.INTERCEPTOR
            or unit.type_id == UnitTypeId.ADEPTPHASESHIFT
            or unit.type_id == UnitTypeId.MULE
        ):
            return False
        return not unit.is_structure and self.my_worker_type != unit.type_id

    def building_going_down(self, building: Unit) -> bool:
        """Returns boolean indicating whether a building is low on health and under attack."""
        if building.tag in self.previous_units_manager.previous_units:
            previous_building = self.previous_units_manager.previous_units[building.tag]
            health = building.health
            compare_health = max(70, building.health_max * 0.09)
            if health < previous_building.health < compare_health:
                return True
        return False

    @property
    def enemy_expansions_dict(self) -> Dict[Point2, Unit]:
        """Dictionary of known expansion locations that have an enemy townhall present."""

        # This is basically copy pasted from BotAI.owned_expansions
        expansions = {}

        for exp_loc in self.ai.expansion_locations:

            def is_near_to_expansion(th: Unit):
                return th.position.distance_to(exp_loc) < sc2.BotAI.EXPANSION_GAP_THRESHOLD

            townhall = next((x for x in self.enemy_townhalls if is_near_to_expansion(x)), None)
            if townhall:
                expansions[exp_loc] = townhall

        return expansions

    def building_started_before(self, type_id: UnitTypeId, start_time_ceiling: int) -> bool:
        """Returns true if a building of type type_id has been started before start_time_ceiling seconds."""
        for unit in self.known_enemy_structures(type_id):  # type: Unit
            # fixme: for completed buildings this will report a time later than the actual start_time.
            # not fatal, but may be misleading.
            start_time = self.unit_values.building_start_time(self.ai.time, unit.type_id, unit.build_progress)
            if start_time is not None and start_time < start_time_ceiling:
                return True

        return False

    @property
    def enemy_townhalls(self):
        """Returns all known enemy townhalls, ie. Command Centers, Nexuses, Hatcheries,
        or one of their upgraded versions."""
        return self.known_enemy_structures.filter(self.unit_values.is_townhall)

    #
    # Zones and enemy start
    #

    @property
    def likely_enemy_start_location(self) -> Optional[Point2]:
        return self.zone_manager.expansion_zones[-1].center_location

    @property
    def enemy_start_location_found(self) -> bool:
        """Returns true if enemy start location has (probably) been found."""
        return self.zone_manager.enemy_start_location_found

    @property
    def unscouted_zones(self) -> List[Zone]:
        """Returns a list of all zones that have not been scouted."""
        unscouted = [z for z in self.zone_manager.all_zones if not z.is_scouted_at_least_once]
        return unscouted

    @property
    def expansion_zones(self) -> List[Zone]:
        return self.zone_manager.expansion_zones

    @property
    def enemy_expansion_zones(self) -> List[Zone]:
        return self.zone_manager.enemy_expansion_zones

    @property
    def our_zones(self) -> List[Zone]:
        """Returns all of our own zones."""
        ours = [z for z in self.zone_manager.all_zones if z.is_ours]
        return ours

    @property
    def our_zones_with_minerals(self) -> List[Zone]:
        """Returns all of our zones that have minerals."""
        filtered = filter(lambda z: z.our_townhall and z.has_minerals, self.our_zones)
        return list(filtered)

    @property
    def own_main_zone(self) -> Zone:
        """Returns our own main zone. If we have lost our base at start location, it will be the
        next safe expansion."""
        return self.zone_manager.own_main_zone

    @property
    def enemy_main_zone(self) -> Zone:
        """ Returns enemy main / start zone."""
        return self.zone_manager.enemy_main_zone

    @property
    def enemy_start_location(self) -> Optional[Point2]:
        """Returns the enemy start location, if found."""
        return self.zone_manager.enemy_start_location

    #
    # BotAI event handlers
    #

    async def on_unit_destroyed(self, unit_tag: int):
        # BotAI._units_previous_map[unit_tag] does not contain enemies. :(
        if unit_tag in self.previous_units_manager.previous_units:
            unit = self.previous_units_manager.previous_units[unit_tag]
        else:
            unit = None
            self._print(f"Unknown unit destroyed: {unit_tag}", log_level=logging.DEBUG)

        self.fire_event(self._on_unit_destroyed_listeners, UnitDestroyedEvent(unit_tag, unit))

    async def on_unit_created(self, unit: Unit):
        # This does not seem to be useful, because the same tag is "created" many many times in a match.
        # It may be a bug with our own bot, because others do not seem to be having the problem.
        # if self.knowledge is not None:
        #     self.knowledge._print(f"{unit.type_id} created, position {unit.position} tag {unit.tag}")
        pass

    async def on_building_construction_started(self, unit: Unit):
        self._print(f"Started {unit.type_id.name} at {unit.position}")

    async def on_building_construction_complete(self, unit: Unit):
        self._print(f"Completed {unit.type_id.name} at {unit.position}")

    async def on_end(self, game_result: Result):
        self._print(f"Result: {game_result.name}", stats=False)
        self._print(f"Duration: {self.ai.time_formatted}", stats=False)

        try:
            step_time_min = round(self.ai.step_time[0])
            self._print(f"Step time min: {step_time_min}", stats=False)
        except OverflowError:
            # step_time_min is infinite at the start and can cause an unnecessary exception.
            pass

        step_time_avg = round(self.ai.step_time[1])
        self._print(f"Step time avg: {step_time_avg}", stats=False)

        step_time_max = round(self.ai.step_time[2])
        self._print(f"Step time max: {step_time_max}", stats=False)

        for manager in self.managers:
            await manager.on_end(game_result)

    # region Knowledge event handlers

    # todo: if this is useful, it should be refactored as a more general solution

    def register_on_unit_destroyed_listener(self, func: Callable[[UnitDestroyedEvent], None]):
        assert isinstance(func, Callable)
        self._on_unit_destroyed_listeners.append(func)

    def unregister_on_unit_destroyed_listener(self, func):
        raise NotImplementedError()

    @staticmethod
    def fire_event(listeners, event):
        for listener in listeners:
            listener(event)

    # endregion

    #
    # Printing
    #

    def _print(self, message: string, stats: bool = True, log_level=logging.INFO):
        """Private print method for Knowledge class."""
        self.print(message, tag=type(self).__name__, stats=stats, log_level=log_level)

    def print(self, message: string, tag: string = None, stats: bool = True, log_level=logging.INFO):
        """
        Prints a message to log.

        :param message: The message to print.
        :param tag: An optional tag, which can be used to indicate the logging component.
        :param stats: When true, stats such as time, minerals, gas, and supply are added to the log message.
        :param log_level: Optional logging level. Default is INFO.
        """
        if tag is not None:
            debug_log = self.config["debug_log"]
            enabled = debug_log.getboolean(tag, fallback=True)
            if not enabled:
                return

        if tag is not None:
            message = f"[{tag}] {message}"

        if stats:
            last_step_time = round(self.ai.step_time[3])

            message = (
                f"{self.ai.time_formatted.rjust(5)} {str(last_step_time).rjust(4)}ms "
                f"{str(self.ai.minerals).rjust(4)}M {str(self.ai.vespene).rjust(4)}G "
                f"{str(self.ai.supply_used).rjust(3)}/{str(self.ai.supply_cap).rjust(3)}U {message}"
            )

        # noinspection PyUnresolvedReferences
        if not self.ai.run_custom or self.ai.player_id == 1 or self.ai.realtime:
            message = f"[EDGE] {message}"
        elif not self.config["general"].getboolean("frozen_log") and tag != "Build":
            return  # No print

        if self.logger.hasHandlers():
            # Write to the competition site log
            self.logger.log(log_level, message)
        else:
            # Write to our own log configured in run_custom.py
            logging.log(log_level, message)

    def _find_gather_point(self):
        self.gather_point = self.base_ramp.top_center.towards(self.base_ramp.bottom_center, -4)
        start = 1
        if self.map.safe_first_expand:
            start = 2

        for i in range(start, len(self.zone_manager.expansion_zones)):
            zone = self.zone_manager.expansion_zones[i]
            if zone.expanding_to:
                self.gather_point = zone.gather_point
            elif zone.is_ours:
                if len(self.zone_manager.gather_points) > i:
                    self.gather_point = self.zone_manager.expansion_zones[
                        self.zone_manager.gather_points[i]
                    ].gather_point

    def get_z(self, point: Point2):
        return self.terrain_to_z_height(self.ai.get_terrain_height(point))

    def terrain_to_z_height(self, h):
        """Gets correct z from versions 4.9.0+"""
        return -16 + 32 * h / 255

    async def post_update(self):
        for manager in self.managers:
            await manager.post_update()