示例#1
0
    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.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:
                    with open(self.file_name, 'r') as handle:
                        text = handle.read()
                        self.data = jsonpickle.decode(text)

                except:
                    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
示例#2
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()
示例#3
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
示例#4
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
示例#5
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.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, 5)

        self.height = self.ai.get_terrain_height(center_location)

        # This is ExtendedRamp!
        self.ramp = self._find_ramp(self.ai)
        self.radius = Zone.ZONE_RADIUS
        if self.ramp is not None:
            self.gather_point = self.ramp.top_center.towards(
                self.center_location, 4)
示例#6
0
class Zone:
    ZONE_RADIUS = 15
    ZONE_DANGER_RADIUS = 30
    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.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, 5)

        self.height = self.ai.get_terrain_height(center_location)

        # This is ExtendedRamp!
        self.ramp = self._find_ramp(self.ai)
        self.radius = Zone.ZONE_RADIUS
        if self.ramp is not None:
            self.gather_point = self.ramp.top_center.towards(
                self.center_location, 4)

    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:
            mf: Unit = mf
            possible_behind_mineral_positions.append(
                self.center_location.towards(mf.position, 9))

        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:
        return self.behind_mineral_positions[1]

    @property
    def mineral_line_center(self) -> Point2:
        return self.behind_mineral_positions[1].towards(
            self.center_location, 4)

    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:
            mf: Unit = mf
            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()

        self.our_units: Units = self.cache.own_in_range(
            self.center_location, self.radius)
        self.known_enemy_units: Units = self.cache.enemy_in_range(
            self.center_location, self.radius)
        # Only add units that we can fight against
        self.known_enemy_units = self.known_enemy_units.filter(
            lambda x: x.cloak != 2)
        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, Zone.ZONE_DANGER_RADIUS)
            self.assaulting_enemy_power.add_units(self.assaulting_enemies)
        else:
            self.needs_evacuation = False
            self.assaulting_enemies.clear()

    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 = 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.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.power_balance < 0 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).closer_than(
            10, self.center_location)

    @property
    def our_batteries(self) -> Units:
        """Returns shield batteries."""
        return self.our_units(UnitTypeId.SHIELDBATTERY).closer_than(
            10, self.center_location)

    @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):
        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) == 2 and ramp.top_center.distance_to(
                    self.center_location) < Zone.MAIN_ZONE_RAMP_MAX_RADIUS
            ]

            if not ramps:
                ramps: List[Ramp] = [
                    ramp for ramp in self.ai.game_info.map_ramps
                    if len(ramp.upper) <= 4 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:
            map_ramp: Ramp = map_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
示例#7
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)
示例#8
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
示例#9
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 bean_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)
示例#10
0
class DataManager(ManagerBase):
    data: OpponentData
    enabled: bool
    enable_write: bool

    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.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:
                    with open(self.file_name, 'r') as handle:
                        text = handle.read()
                        self.data = jsonpickle.decode(text)

                except:
                    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

    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()

    def set_build(self, build_name: str):
        self.result.build_used = build_name

    def write_defeat(self):
        self.result.result = -1
        self.result.game_duration = self.ai.time
        self.write_results()

    def write_victory(self):
        self.result.result = 1
        self.result.game_duration = self.ai.time
        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:
                with open(self.file_name, 'r') as handle:
                    text = handle.read()
                    self.data = jsonpickle.decode(text)
            except:
                # Don't write if we can't read the current data
                self.knowledge.print("Data read failed on save.")
                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:
            self.knowledge.print("Data write failed.")

    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()