Beispiel #1
0
 def _calculate_threat(self):
     nearby_forces = combine_ratings(self._potential_support(),
                                     self.assigned_rating)
     return (self.threat_bias + +self.safety_factor * combine_ratings(
         self._local_threat(), self._neighbor_threat()) + +max(
             0.0,
             self._potential_threat() + self._jump2_threat() -
             nearby_forces))
    def _portion_of_fleet_needed_here(self):
        """Calculate the portion of the fleet needed in target system considering enemy forces."""
        # TODO check rating against planets
        if assertion_fails(self.type in COMBAT_MISSION_TYPES, msg=str(self)):
            return 0
        if assertion_fails(self.target and self.target.id != INVALID_ID, msg=str(self)):
            return 0
        system_id = self.target.id
        aistate = get_aistate()
        local_defenses = MilitaryAI.get_my_defense_rating_in_system(system_id)
        potential_threat = combine_ratings(
            MilitaryAI.get_system_local_threat(system_id),
            MilitaryAI.get_system_neighbor_threat(system_id)
        )
        universe = fo.getUniverse()
        system = universe.getSystem(system_id)

        # tally planetary defenses
        total_defense = total_shields = 0
        for planet_id in system.planetIDs:
            planet = universe.getPlanet(planet_id)
            total_defense += planet.currentMeterValue(fo.meterType.defense)
            total_shields += planet.currentMeterValue(fo.meterType.shield)
        planetary_ratings = total_defense * (total_shields + total_defense)
        potential_threat += planetary_ratings  # TODO: rewrite to return min rating vs planets as well

        # consider safety factor just once here rather than everywhere below
        safety_factor = aistate.character.military_safety_factor()
        potential_threat *= safety_factor

        # TODO: Rate against specific threat here
        fleet_rating = CombatRatingsAI.get_fleet_rating(self.fleet.id)
        return CombatRatingsAI.rating_needed(potential_threat, local_defenses) / float(max(fleet_rating, 1.))
    def get_rating(self, enemy_stats: ShipCombatStats = None) -> float:
        """Calculates the rating of the fleet by combining all its ships ratings.

        :param enemy_stats: enemy to be rated against
        :return: Rating of the fleet
        """
        return combine_ratings(x.get_rating(enemy_stats) for x in self._ship_stats)
Beispiel #4
0
 def _calculate_threat(self):
     potential_threat = max(
         self._potential_threat() - self._potential_support(), 0)
     actual_threat = self.safety_factor * (
         2 * self.threat_bias +
         +combine_ratings(self._local_threat(), self._neighbor_threat()))
     return potential_threat + actual_threat
Beispiel #5
0
    def _calculate_threat(self):

        systems_status = get_aistate().systemStatus.get(self.sys_id, {})
        threat = self.safety_factor * combine_ratings(
            systems_status.get('fleetThreat', 0),
            systems_status.get('monsterThreat', 0) +
            +systems_status.get('planetThreat', 0))
        return self.threat_bias + threat
Beispiel #6
0
def get_concentrated_tot_mil_rating() -> float:
    """
    Give an assessment of total military rating as if all fleets were merged into a single mega-fleet.

    :return: a military rating value
    """
    return combine_ratings(
        get_fleet_rating(fleet_id) for fleet_id in
        FleetUtilsAI.get_empire_fleet_ids_by_role(MissionType.MILITARY))
Beispiel #7
0
def merge_fleet_a_into_b(fleet_a_id,
                         fleet_b_id,
                         leave_rating=0,
                         need_rating=0,
                         context=""):
    debug("Merging fleet %s into %s", TargetFleet(fleet_a_id),
          TargetFleet(fleet_b_id))
    universe = fo.getUniverse()
    fleet_a = universe.getFleet(fleet_a_id)
    fleet_b = universe.getFleet(fleet_b_id)
    if not fleet_a or not fleet_b:
        return 0
    system_id = fleet_a.systemID
    if fleet_b.systemID != system_id:
        return 0

    # TODO: Should this rate against specific enemy?
    remaining_rating = get_fleet_rating(fleet_a_id)
    transferred_rating = 0
    for ship_id in fleet_a.shipIDs:
        this_ship = universe.getShip(ship_id)
        if not this_ship:
            continue
        this_rating = get_ship_combat_stats(ship_id).get_rating()
        remaining_rating = rating_needed(remaining_rating, this_rating)
        if remaining_rating < leave_rating:  # merging this would leave old fleet under minimum rating, try other ships.
            continue
        transferred = fo.issueFleetTransferOrder(ship_id, fleet_b_id)
        if transferred:
            transferred_rating = combine_ratings(transferred_rating,
                                                 this_rating)
        else:
            debug(
                "  *** transfer of ship %4d, formerly of fleet %4d, into fleet %4d failed; %s"
                % (ship_id, fleet_a_id, fleet_b_id,
                   (" context is %s" % context) if context else ""))
        if need_rating != 0 and need_rating <= transferred_rating:
            break
    fleet_a = universe.getFleet(fleet_a_id)
    aistate = get_aistate()
    if not fleet_a or fleet_a.empty or fleet_a_id in universe.destroyedObjectIDs(
            fo.empireID()):
        aistate.delete_fleet_info(fleet_a_id)
    aistate.update_fleet_rating(fleet_b_id)
    def issue_fleet_orders(self):
        """issues AIFleetOrders which can be issued in system and moves to next one if is possible"""
        # TODO: priority
        order_completed = True

        debug("\nChecking orders for fleet %s (on turn %d), with mission type %s and target %s",
              self.fleet.get_object(), fo.currentTurn(), self.type or 'No mission', self.target or 'No Target')
        if MissionType.INVASION == self.type:
            self._check_retarget_invasion()
        just_issued_move_order = False
        last_move_target_id = INVALID_ID
        # Note: the following abort check somewhat assumes only one major mission type
        for fleet_order in self.orders:
            if (isinstance(fleet_order, (OrderColonize, OrderOutpost, OrderInvade)) and
                    self._check_abort_mission(fleet_order)):
                return
        aistate = get_aistate()
        for fleet_order in self.orders:
            if just_issued_move_order and self.fleet.get_object().systemID != last_move_target_id:
                # having just issued a move order, we will normally stop issuing orders this turn, except that if there
                # are consecutive move orders we will consider moving through the first destination rather than stopping
                # Without the below noinspection directive, PyCharm is concerned about the 2nd part of the test
                # noinspection PyTypeChecker
                if (not isinstance(fleet_order, OrderMove) or
                        self.need_to_pause_movement(last_move_target_id, fleet_order)):
                    break
            debug("Checking order: %s" % fleet_order)
            self.check_mergers(context=str(fleet_order))
            if fleet_order.can_issue_order(verbose=False):
                # only move if all other orders completed
                if isinstance(fleet_order, OrderMove) and order_completed:
                    debug("Issuing fleet order %s" % fleet_order)
                    fleet_order.issue_order()
                    just_issued_move_order = True
                    last_move_target_id = fleet_order.target.id
                elif not isinstance(fleet_order, OrderMove):
                    debug("Issuing fleet order %s" % fleet_order)
                    fleet_order.issue_order()
                else:
                    debug("NOT issuing (even though can_issue) fleet order %s" % fleet_order)
                status_words = tuple(["not", ""][_s] for _s in [fleet_order.order_issued, fleet_order.executed])
                debug("Order %s issued and %s fully executed." % status_words)
                if not fleet_order.executed:
                    order_completed = False
            else:  # check that we're not held up by a Big Monster
                if fleet_order.order_issued:
                    # A previously issued order that wasn't instantly executed must have had cirumstances change so that
                    # the order can't currently be reissued (or perhaps simply a savegame has been reloaded on the same
                    # turn the order was issued).
                    if not fleet_order.executed:
                        order_completed = False
                    # Go on to the next order.
                    continue
                debug("CAN'T issue fleet order %s because:" % fleet_order)
                fleet_order.can_issue_order(verbose=True)
                if isinstance(fleet_order, OrderMove):
                    this_system_id = fleet_order.target.id
                    this_status = aistate.systemStatus.setdefault(this_system_id, {})
                    threat_threshold = fo.currentTurn() * MilitaryAI.cur_best_mil_ship_rating() / 4.0
                    if this_status.get('monsterThreat', 0) > threat_threshold:
                        # if this move order is not this mil fleet's final destination, and blocked by Big Monster,
                        # release and hope for more effective reassignment
                        if (self.type not in (MissionType.MILITARY, MissionType.SECURE) or
                                fleet_order != self.orders[-1]):
                            debug("Aborting mission due to being blocked by Big Monster at system %d, threat %d" % (
                                this_system_id, aistate.systemStatus[this_system_id]['monsterThreat']))
                            debug("Full set of orders were:")
                            for this_order in self.orders:
                                debug(" - %s" % this_order)
                            self.clear_fleet_orders()
                            self.clear_target()
                            return
                break  # do not order the next order until this one is finished.
        else:  # went through entire order list
            if order_completed:
                debug("Final order is completed")
                orders = self.orders
                last_order = orders[-1] if orders else None
                universe = fo.getUniverse()

                if last_order and isinstance(last_order, OrderColonize):
                    planet = universe.getPlanet(last_order.target.id)
                    sys_partial_vis_turn = get_partial_visibility_turn(planet.systemID)
                    planet_partial_vis_turn = get_partial_visibility_turn(planet.id)
                    if (planet_partial_vis_turn == sys_partial_vis_turn and
                            not planet.initialMeterValue(fo.meterType.population)):
                        warning("Fleet %s has tentatively completed its "
                                "colonize mission but will wait to confirm population.", self.fleet)
                        debug("    Order details are %s" % last_order)
                        debug("    Order is valid: %s; issued: %s; executed: %s" % (
                            last_order.is_valid(), last_order.order_issued, last_order.executed))
                        if not last_order.is_valid():
                            source_target = last_order.fleet
                            target_target = last_order.target
                            debug("        source target validity: %s; target target validity: %s " % (
                                bool(source_target), bool(target_target)))
                        return  # colonize order must not have completed yet
                clear_all = True
                last_sys_target = INVALID_ID
                if last_order and isinstance(last_order, OrderMilitary):
                    last_sys_target = last_order.target.id
                    # not doing this until decide a way to release from a SECURE mission
                    # if (MissionType.SECURE == self.type) or
                    secure_targets = set(AIstate.colonyTargetedSystemIDs +
                                         AIstate.outpostTargetedSystemIDs +
                                         AIstate.invasionTargetedSystemIDs)
                    if last_sys_target in secure_targets:  # consider a secure mission
                        if last_sys_target in AIstate.colonyTargetedSystemIDs:
                            secure_type = "Colony"
                        elif last_sys_target in AIstate.outpostTargetedSystemIDs:
                            secure_type = "Outpost"
                        elif last_sys_target in AIstate.invasionTargetedSystemIDs:
                            secure_type = "Invasion"
                        else:
                            secure_type = "Unidentified"
                        debug("Fleet %d has completed initial stage of its mission "
                              "to secure system %d (targeted for %s), "
                              "may release a portion of ships" % (self.fleet.id, last_sys_target, secure_type))
                        clear_all = False

                # for PROTECT_REGION missions, only release fleet if no more threat
                if self.type == MissionType.PROTECT_REGION:
                    # use military logic code below to determine if can release
                    # any or even all of the ships.
                    clear_all = False
                    last_sys_target = self.target.id
                    debug("Check if PROTECT_REGION mission with target %d is finished.", last_sys_target)

                fleet_id = self.fleet.id
                if clear_all:
                    if orders:
                        debug("Fleet %d has completed its mission; clearing all orders and targets." % self.fleet.id)
                        debug("Full set of orders were:")
                        for this_order in orders:
                            debug("\t\t %s" % this_order)
                        self.clear_fleet_orders()
                        self.clear_target()
                        if aistate.get_fleet_role(fleet_id) in (MissionType.MILITARY, MissionType.SECURE):
                            allocations = MilitaryAI.get_military_fleets(mil_fleets_ids=[fleet_id],
                                                                         try_reset=False,
                                                                         thisround="Fleet %d Reassignment" % fleet_id)
                            if allocations:
                                MilitaryAI.assign_military_fleets_to_systems(use_fleet_id_list=[fleet_id],
                                                                             allocations=allocations)
                    else:  # no orders
                        debug("No Current Orders")
                else:
                    potential_threat = combine_ratings(
                        MilitaryAI.get_system_local_threat(last_sys_target),
                        MilitaryAI.get_system_neighbor_threat(last_sys_target)
                    )
                    threat_present = potential_threat > 0
                    debug("Fleet threat present? %s", threat_present)
                    target_system = universe.getSystem(last_sys_target)
                    if not threat_present and target_system:
                        for pid in target_system.planetIDs:
                            planet = universe.getPlanet(pid)
                            if (planet and
                                    planet.owner != fo.empireID() and
                                    planet.currentMeterValue(fo.meterType.maxDefense) > 0):
                                debug("Found local planetary threat: %s", planet)
                                threat_present = True
                                break
                    if not threat_present:
                        debug("No current threat in target system; releasing a portion of ships.")
                        # at least first stage of current task is done;
                        # release extra ships for potential other deployments
                        new_fleets = FleetUtilsAI.split_fleet(self.fleet.id)
                        if self.type == MissionType.PROTECT_REGION:
                            self.clear_fleet_orders()
                            self.clear_target()
                            new_fleets.append(self.fleet.id)
                    else:
                        debug("Threat remains in target system; Considering to release some ships.")
                        new_fleets = []
                        fleet_portion_to_remain = self._portion_of_fleet_needed_here()
                        if fleet_portion_to_remain >= 1:
                            debug("Can not release fleet yet due to large threat.")
                        elif fleet_portion_to_remain > 0:
                            debug("Not all ships are needed here - considering releasing a few")
                            # TODO: Rate against specific enemy threat cause
                            fleet_remaining_rating = CombatRatingsAI.get_fleet_rating(fleet_id)
                            fleet_min_rating = fleet_portion_to_remain * fleet_remaining_rating
                            debug("Starting rating: %.1f, Target rating: %.1f",
                                  fleet_remaining_rating, fleet_min_rating)
                            allowance = CombatRatingsAI.rating_needed(fleet_remaining_rating, fleet_min_rating)
                            debug("May release ships with total rating of %.1f", allowance)
                            ship_ids = list(self.fleet.get_object().shipIDs)
                            for ship_id in ship_ids:
                                ship_rating = CombatRatingsAI.get_ship_rating(ship_id)
                                debug("Considering to release ship %d with rating %.1f", ship_id, ship_rating)
                                if ship_rating > allowance:
                                    debug("Remaining rating insufficient. Not released.")
                                    continue
                                debug("Splitting from fleet.")
                                new_fleet_id = FleetUtilsAI.split_ship_from_fleet(fleet_id, ship_id)
                                if assertion_fails(new_fleet_id and new_fleet_id != INVALID_ID):
                                    break
                                new_fleets.append(new_fleet_id)
                                fleet_remaining_rating = CombatRatingsAI.rating_difference(
                                    fleet_remaining_rating, ship_rating)
                                allowance = CombatRatingsAI.rating_difference(
                                    fleet_remaining_rating, fleet_min_rating)
                                debug("Remaining fleet rating: %.1f - Allowance: %.1f",
                                      fleet_remaining_rating, allowance)
                            if new_fleets:
                                aistate.get_fleet_role(fleet_id, force_new=True)
                                aistate.update_fleet_rating(fleet_id)
                                aistate.ensure_have_fleet_missions(new_fleets)
                        else:
                            debug("Planetary defenses are deemed sufficient. Release fleet.")
                            new_fleets = FleetUtilsAI.split_fleet(self.fleet.id)

                    new_military_fleets = []
                    for fleet_id in new_fleets:
                        if aistate.get_fleet_role(fleet_id) in COMBAT_MISSION_TYPES:
                            new_military_fleets.append(fleet_id)
                    allocations = []
                    if new_military_fleets:
                        allocations = MilitaryAI.get_military_fleets(
                            mil_fleets_ids=new_military_fleets,
                            try_reset=False,
                            thisround="Fleet Reassignment %s" % new_military_fleets
                        )
                    if allocations:
                        MilitaryAI.assign_military_fleets_to_systems(use_fleet_id_list=new_military_fleets,
                                                                     allocations=allocations)
Beispiel #9
0
 def get_rating_vs_planets(self) -> float:
     return combine_ratings(x.get_rating_vs_planets()
                            for x in self._ship_stats)
Beispiel #10
0
def test_idempotency():
    assert combine_ratings(1, 1, 2) == combine_ratings(
        2, 1, 1) == combine_ratings(1, 2, 1)
Beispiel #11
0
def test_merge_3_ratings_as_collection(iterable):
    assert combine_ratings(iterable) == pytest.approx(11.65, rel=1e-2)
Beispiel #12
0
 def _calculate_threat(self):
     return (self.threat_bias + +self.safety_factor * combine_ratings(
         self._local_threat(), 0.75 * self._neighbor_threat(),
         0.5 * self._jump2_threat()) + self._potential_threat())
Beispiel #13
0
 def _minimum_allocation(self, threat):
     nearby_forces = combine_ratings(self.assigned_rating,
                                     self._potential_support())
     return max(rating_needed(self._regional_threat(), nearby_forces),
                rating_needed(1.4 * threat, self.assigned_rating))
Beispiel #14
0
def avail_mil_needing_repair(mil_fleet_ids,
                             split_ships=False,
                             on_mission=False,
                             repair_limit=0.70):
    """Returns tuple of lists: (ids_needing_repair, ids_not)."""
    fleet_buckets = [[], []]
    universe = fo.getUniverse()
    cutoff = [repair_limit, 0.25][on_mission]
    aistate = get_aistate()
    for fleet_id in mil_fleet_ids:
        fleet = universe.getFleet(fleet_id)
        ship_buckets = [[], []]
        ships_cur_health = [0, 0]
        ships_max_health = [0, 0]
        for ship_id in fleet.shipIDs:
            this_ship = universe.getShip(ship_id)
            cur_struc = this_ship.initialMeterValue(fo.meterType.structure)
            max_struc = this_ship.initialMeterValue(fo.meterType.maxStructure)
            ship_ok = cur_struc >= cutoff * max_struc
            ship_buckets[ship_ok].append(ship_id)
            ships_cur_health[ship_ok] += cur_struc
            ships_max_health[ship_ok] += max_struc
        this_sys_id = fleet.systemID if fleet.nextSystemID == INVALID_ID else fleet.nextSystemID
        fleet_ok = sum(ships_cur_health) >= cutoff * sum(ships_max_health)
        local_status = aistate.systemStatus.get(this_sys_id, {})
        my_local_rating = combine_ratings(
            local_status.get("mydefenses", {}).get("overall", 0),
            local_status.get("myFleetRating", 0))
        my_local_rating_vs_planets = local_status.get("myFleetRatingVsPlanets",
                                                      0)
        combat_trigger = bool(
            local_status.get("fleetThreat", 0)
            or local_status.get("monsterThreat", 0))
        if not combat_trigger and local_status.get("planetThreat", 0):
            universe = fo.getUniverse()
            system = universe.getSystem(this_sys_id)
            for planet_id in system.planetIDs:
                planet = universe.getPlanet(planet_id)
                if planet.ownedBy(
                        fo.empireID()):  # TODO: also exclude at-peace planets
                    continue
                if planet.unowned and not EspionageAI.colony_detectable_by_empire(
                        planet_id, empire=fo.empireID()):
                    continue
                if sum([
                        planet.currentMeterValue(meter_type)
                        for meter_type in [
                            fo.meterType.defense, fo.meterType.shield,
                            fo.meterType.construction
                        ]
                ]):
                    combat_trigger = True
                    break
        needed_here = (
            combat_trigger and local_status.get("totalThreat", 0) > 0
        )  # TODO: assess if remaining other forces are sufficient
        safely_needed = (
            needed_here
            and my_local_rating > local_status.get("totalThreat", 0) and
            my_local_rating_vs_planets > local_status.get("planetThreat", 0)
        )  # TODO: improve both assessment prongs
        if not fleet_ok:
            if safely_needed:
                debug(
                    "Fleet %d at %s needs repair but deemed safely needed to remain for defense"
                    % (fleet_id, universe.getSystem(fleet.systemID)))
            else:
                if needed_here:
                    debug(
                        "Fleet %d at %s needed present for combat, but is damaged and deemed unsafe to remain."
                        % (fleet_id, universe.getSystem(fleet.systemID)))
                    debug(
                        "\t my_local_rating: %.1f ; threat: %.1f" %
                        (my_local_rating, local_status.get("totalThreat", 0)))
                debug("Selecting fleet %d at %s for repair" %
                      (fleet_id, universe.getSystem(fleet.systemID)))
        fleet_buckets[fleet_ok or bool(safely_needed)].append(fleet_id)
    return fleet_buckets
Beispiel #15
0
    def can_issue_order(self, verbose=False):
        if not super(OrderMove, self).can_issue_order(verbose=verbose):
            return False
        # TODO: figure out better way to have invasions (& possibly colonizations)
        #       require visibility on target without needing visibility of all intermediate systems
        # if False and main_mission_type not in [MissionType.ATTACK,  # TODO: consider this later
        #                                        MissionType.MILITARY,
        #                                        MissionType.SECURE,
        #                                        MissionType.HIT_AND_RUN,
        #                                        MissionType.EXPLORATION]:
        #     if not universe.getVisibility(target_id, get_aistate().empireID) >= fo.visibility.partial:
        #         #if not target_id in interior systems
        #         get_aistate().needsEmergencyExploration.append(fleet.systemID)
        #         return False
        system_id = self.fleet.get_system().id
        if system_id == self.target.get_system().id:
            return True  # TODO: already there, but could consider retreating

        aistate = get_aistate()
        main_fleet_mission = aistate.get_fleet_mission(self.fleet.id)

        # TODO: Rate against specific enemies here
        fleet_rating = CombatRatingsAI.get_fleet_rating(self.fleet.id)
        fleet_rating_vs_planets = CombatRatingsAI.get_fleet_rating_against_planets(
            self.fleet.id)
        target_sys_status = aistate.systemStatus.get(self.target.id, {})
        f_threat = target_sys_status.get("fleetThreat", 0)
        m_threat = target_sys_status.get("monsterThreat", 0)
        p_threat = target_sys_status.get("planetThreat", 0)
        threat = f_threat + m_threat + p_threat
        safety_factor = aistate.character.military_safety_factor()
        universe = fo.getUniverse()
        if main_fleet_mission.type == MissionType.INVASION and not trooper_move_reqs_met(
                main_fleet_mission.target, self, verbose):
            return False
        if fleet_rating >= safety_factor * threat and fleet_rating_vs_planets >= p_threat:
            return True
        elif not p_threat and self.target.id in fo.getEmpire(
        ).supplyUnobstructedSystems:
            return True
        else:
            sys1 = universe.getSystem(system_id)
            sys1_name = sys1 and sys1.name or "unknown"
            target_system = self.target.get_system()
            target_system_name = (target_system and
                                  target_system.get_object().name) or "unknown"
            # TODO: adjust calc for any departing fleets
            my_other_fleet_rating = aistate.systemStatus.get(
                self.target.id, {}).get("myFleetRating", 0)
            my_other_fleet_rating_vs_planets = aistate.systemStatus.get(
                self.target.id, {}).get("myFleetRatingVsPlanets", 0)
            is_military = aistate.get_fleet_role(
                self.fleet.id) == MissionType.MILITARY

            total_rating = combine_ratings(my_other_fleet_rating, fleet_rating)
            total_rating_vs_planets = combine_ratings(
                my_other_fleet_rating_vs_planets, fleet_rating_vs_planets)
            if my_other_fleet_rating > 3 * safety_factor * threat or (
                    is_military and total_rating_vs_planets > 2.5 * p_threat
                    and total_rating > safety_factor * threat):
                debug((
                    "\tAdvancing fleet %d (rating %d) at system %d (%s) into system %d (%s) with threat %d"
                    " because of sufficient empire fleet strength already at destination"
                    % (
                        self.fleet.id,
                        fleet_rating,
                        system_id,
                        sys1_name,
                        self.target.id,
                        target_system_name,
                        threat,
                    )))
                return True
            elif (threat == p_threat and not self.fleet.get_object().aggressive
                  and not my_other_fleet_rating
                  and not target_sys_status.get("localEnemyFleetIDs", [-1])):
                if verbose:
                    debug(
                        "\tAdvancing fleet %d (rating %d) at system %d (%s) "
                        "into system %d (%s) with planet threat %d because non aggressive"
                        " and no other fleets present to trigger combat" % (
                            self.fleet.id,
                            fleet_rating,
                            system_id,
                            sys1_name,
                            self.target.id,
                            target_system_name,
                            threat,
                        ))
                return True
            else:
                if verbose:
                    _info = (
                        self.fleet.id,
                        fleet_rating,
                        system_id,
                        sys1_name,
                        self.target.id,
                        target_system_name,
                        threat,
                    )
                    debug(
                        "\tHolding fleet %d (rating %d) at system %d (%s) "
                        "before travelling to system %d (%s) with threat %d" %
                        _info)
                needs_vis = aistate.misc.setdefault("needs_vis", [])
                if self.target.id not in needs_vis:
                    needs_vis.append(self.target.id)
                return False
Beispiel #16
0
def test_two_rating_are_merged(rating1, raring2, combined):
    assert combine_ratings(rating1, raring2) == combined
Beispiel #17
0
def test_merge_3_ratings():
    assert combine_ratings(1, 1, 2) == pytest.approx(11.65, rel=1e-2)
Beispiel #18
0
def get_military_fleets(mil_fleets_ids=None, try_reset=True, thisround="Main"):
    """Get armed military fleets."""
    global _military_allocations

    universe = fo.getUniverse()
    empire_id = fo.empireID()
    home_system_id = PlanetUtilsAI.get_capital_sys_id()

    all_military_fleet_ids = (mil_fleets_ids if mil_fleets_ids is not None else
                              FleetUtilsAI.get_empire_fleet_ids_by_role(
                                  MissionType.MILITARY))

    # Todo: This block had been originally added to address situations where fleet missions were not properly
    #  terminating, leaving fleets stuck in stale deployments. Assess if this block is still needed at all; delete
    #  if not, otherwise restructure the following code so that in event a reset is occurring greater priority is given
    #  to providing military support to locations where a necessary Secure mission might have just been released (i.e.,
    #  at invasion and colony/outpost targets where the troopships and colony ships are on their way), or else allow
    #  only a partial reset which does not reset Secure missions.
    enable_periodic_mission_reset = False
    if enable_periodic_mission_reset and try_reset and (
            fo.currentTurn() + empire_id) % 30 == 0 and thisround == "Main":
        debug(
            "Resetting all Military missions as part of an automatic periodic reset to clear stale missions."
        )
        try_again(all_military_fleet_ids,
                  try_reset=False,
                  thisround=thisround + " Reset")
        return

    mil_fleets_ids = list(
        FleetUtilsAI.extract_fleet_ids_without_mission_types(
            all_military_fleet_ids))
    mil_needing_repair_ids, mil_fleets_ids = avail_mil_needing_repair(
        mil_fleets_ids, split_ships=True)
    avail_mil_rating = combine_ratings(
        get_fleet_rating(x) for x in mil_fleets_ids)

    if not mil_fleets_ids:
        if "Main" in thisround:
            _military_allocations = []
        return []

    # for each system, get total rating of fleets assigned to it
    already_assigned_rating = {}
    already_assigned_rating_vs_planets = {}
    aistate = get_aistate()
    systems_status = aistate.systemStatus
    enemy_sup_factor = {}  # enemy supply
    for sys_id in universe.systemIDs:
        already_assigned_rating[sys_id] = 0
        already_assigned_rating_vs_planets[sys_id] = 0
        enemy_sup_factor[sys_id] = min(
            2,
            len(
                systems_status.get(sys_id, {}).get("enemies_nearly_supplied",
                                                   [])))
    for fleet_id in [
            fid for fid in all_military_fleet_ids if fid not in mil_fleets_ids
    ]:
        ai_fleet_mission = aistate.get_fleet_mission(fleet_id)
        if not ai_fleet_mission.target:  # shouldn't really be possible
            continue
        last_sys = (
            ai_fleet_mission.target.get_system().id
        )  # will count this fleet as assigned to last system in target list  # TODO last_sys or target sys?
        this_rating = get_fleet_rating(fleet_id)
        this_rating_vs_planets = get_fleet_rating_against_planets(fleet_id)
        already_assigned_rating[last_sys] = combine_ratings(
            already_assigned_rating.get(last_sys, 0), this_rating)
        already_assigned_rating_vs_planets[last_sys] = combine_ratings(
            already_assigned_rating_vs_planets.get(last_sys, 0),
            this_rating_vs_planets)
    for sys_id in universe.systemIDs:
        my_defense_rating = systems_status.get(sys_id,
                                               {}).get("mydefenses",
                                                       {}).get("overall", 0)
        already_assigned_rating[sys_id] = combine_ratings(
            my_defense_rating, already_assigned_rating[sys_id])
        if _verbose_mil_reporting and already_assigned_rating[sys_id]:
            debug(
                "\t System %s already assigned rating %.1f" %
                (universe.getSystem(sys_id), already_assigned_rating[sys_id]))

    # get systems to defend
    capital_id = PlanetUtilsAI.get_capital()
    if capital_id is not None:
        capital_planet = universe.getPlanet(capital_id)
    else:
        capital_planet = None
    # TODO: if no owned planets try to capture one!
    if capital_planet:
        capital_sys_id = capital_planet.systemID
    else:  # should be rare, but so as to not break code below, pick a randomish mil-centroid system
        capital_sys_id = None  # unless we can find one to use
        system_dict = {}
        for fleet_id in all_military_fleet_ids:
            status = aistate.fleetStatus.get(fleet_id, None)
            if status is not None:
                system_id = status["sysID"]
                if not list(universe.getSystem(system_id).planetIDs):
                    continue
                system_dict[system_id] = system_dict.get(
                    system_id, 0) + status.get("rating", 0)
        ranked_systems = sorted([(val, sys_id)
                                 for sys_id, val in system_dict.items()])
        if ranked_systems:
            capital_sys_id = ranked_systems[-1][-1]
        else:
            try:
                capital_sys_id = next(iter(
                    aistate.fleetStatus.items()))[1]["sysID"]
            except:  # noqa: E722
                pass

    num_targets = max(10, PriorityAI.allotted_outpost_targets)
    top_target_planets = ([
        pid for pid, pscore, trp in
        AIstate.invasionTargets[:PriorityAI.allotted_invasion_targets()]
        if pscore > InvasionAI.MIN_INVASION_SCORE
    ] + [
        pid
        for pid, (pscore, spec) in list(aistate.colonisableOutpostIDs.items())
        [:num_targets] if pscore > InvasionAI.MIN_INVASION_SCORE
    ] + [
        pid
        for pid, (pscore, spec) in list(aistate.colonisablePlanetIDs.items())
        [:num_targets] if pscore > InvasionAI.MIN_INVASION_SCORE
    ])
    top_target_planets.extend(aistate.qualifyingTroopBaseTargets.keys())

    base_col_target_systems = PlanetUtilsAI.get_systems(top_target_planets)
    top_target_systems = []
    for sys_id in AIstate.invasionTargetedSystemIDs + base_col_target_systems:
        if sys_id not in top_target_systems:
            if aistate.systemStatus[sys_id][
                    "totalThreat"] > get_tot_mil_rating():
                continue
            top_target_systems.append(
                sys_id)  # doing this rather than set, to preserve order

    try:
        # capital defense
        allocation_helper = AllocationHelper(
            already_assigned_rating, already_assigned_rating_vs_planets,
            avail_mil_rating, try_reset)
        if capital_sys_id is not None:
            CapitalDefenseAllocator(capital_sys_id,
                                    allocation_helper).allocate()

        # defend other planets
        empire_planet_ids = PlanetUtilsAI.get_owned_planets_by_empire()
        empire_occupied_system_ids = list(
            set(PlanetUtilsAI.get_systems(empire_planet_ids)) -
            {capital_sys_id})
        for sys_id in empire_occupied_system_ids:
            PlanetDefenseAllocator(sys_id, allocation_helper).allocate()

        # attack / protect high priority targets
        for sys_id in top_target_systems:
            TopTargetAllocator(sys_id, allocation_helper).allocate()

        # enemy planets
        other_targeted_system_ids = [
            sys_id for sys_id in set(
                PlanetUtilsAI.get_systems(AIstate.opponentPlanetIDs))
            if sys_id not in top_target_systems
        ]
        for sys_id in other_targeted_system_ids:
            TargetAllocator(sys_id, allocation_helper).allocate()

        # colony / outpost targets
        other_targeted_system_ids = [
            sys_id for sys_id in list(
                set(AIstate.colonyTargetedSystemIDs +
                    AIstate.outpostTargetedSystemIDs))
            if sys_id not in top_target_systems
        ]
        for sys_id in other_targeted_system_ids:
            OutpostTargetAllocator(sys_id, allocation_helper).allocate()

        # TODO blockade enemy systems

        # interior systems
        targetable_ids = set(get_systems_by_supply_tier(0))
        current_mil_systems = [
            sid for sid, _, _, _, _ in allocation_helper.allocations
        ]
        interior_targets1 = targetable_ids.difference(current_mil_systems)
        interior_targets = [
            sid for sid in interior_targets1
            if (allocation_helper.threat_bias +
                systems_status.get(sid, {}).get("totalThreat", 0) > 0.8 *
                allocation_helper.already_assigned_rating[sid])
        ]
        for sys_id in interior_targets:
            InteriorTargetsAllocator(sys_id, allocation_helper).allocate()

        # TODO Exploration targets

        # border protections
        visible_system_ids = aistate.visInteriorSystemIDs | aistate.visBorderSystemIDs
        accessible_system_ids = ([
            sys_id for sys_id in visible_system_ids
            if systems_connected(sys_id, home_system_id)
        ] if home_system_id != INVALID_ID else [])
        current_mil_systems = [
            sid
            for sid, alloc, rvp, take_any, _ in allocation_helper.allocations
            if alloc > 0
        ]
        border_targets1 = [
            sid for sid in accessible_system_ids
            if sid not in current_mil_systems
        ]
        border_targets = [
            sid for sid in border_targets1
            if (allocation_helper.threat_bias +
                systems_status.get(sid, {}).get("fleetThreat", 0) +
                systems_status.get(sid, {}).get("planetThreat", 0) > 0.8 *
                allocation_helper.already_assigned_rating[sid])
        ]
        for sys_id in border_targets:
            BorderSecurityAllocator(sys_id, allocation_helper).allocate()
    except ReleaseMilitaryException:
        try_again(all_military_fleet_ids)
        return

    new_allocations = []
    remaining_mil_rating = avail_mil_rating
    # for top categories assign max_alloc right away as available
    for cat in ["capitol", "occupied", "topTargets"]:
        for sid, alloc, rvp, take_any, max_alloc in allocation_helper.allocation_by_groups.get(
                cat, []):
            if remaining_mil_rating <= 0:
                break
            this_alloc = min(remaining_mil_rating, max_alloc)
            new_allocations.append((sid, this_alloc, alloc, rvp, take_any))
            remaining_mil_rating = rating_difference(remaining_mil_rating,
                                                     this_alloc)

    base_allocs = set()
    # for lower priority categories, first assign base_alloc around to all, then top up as available
    for cat in ["otherTargets", "accessibleTargets", "exploreTargets"]:
        for sid, alloc, rvp, take_any, max_alloc in allocation_helper.allocation_by_groups.get(
                cat, []):
            if remaining_mil_rating <= 0:
                break
            alloc = min(remaining_mil_rating, alloc)
            base_allocs.add(sid)
            remaining_mil_rating = rating_difference(remaining_mil_rating,
                                                     alloc)
    for cat in ["otherTargets", "accessibleTargets", "exploreTargets"]:
        for sid, alloc, rvp, take_any, max_alloc in allocation_helper.allocation_by_groups.get(
                cat, []):
            if sid not in base_allocs:
                break
            if remaining_mil_rating <= 0:
                new_allocations.append((sid, alloc, alloc, rvp, take_any))
            else:
                local_max_avail = combine_ratings(remaining_mil_rating, alloc)
                new_rating = min(local_max_avail, max_alloc)
                new_allocations.append((sid, new_rating, alloc, rvp, take_any))
                remaining_mil_rating = rating_difference(
                    local_max_avail, new_rating)

    if "Main" in thisround:
        _military_allocations = new_allocations
    if _verbose_mil_reporting or "Main" in thisround:
        debug(
            "------------------------------\nFinal %s Round Military Allocations: %s \n-----------------------"
            % (thisround,
               dict([(sid, alloc)
                     for sid, alloc, _, _, _ in new_allocations])))
        debug("(Apparently) remaining military rating: %.1f" %
              remaining_mil_rating)

    return new_allocations
Beispiel #19
0
def test_merge_3_ratings_as_collection_and_args():
    assert combine_ratings([1, 1], 2) == pytest.approx(11.65, rel=1e-2)
Beispiel #20
0
def get_fleets_for_mission(
    target_stats: dict,
    min_stats: dict,
    cur_stats: dict,
    starting_system: int,
    fleet_pool_set: Set[int],
    fleet_list: List[int],
    species: str = "",
    ensure_return: bool = False,
) -> List[int]:
    """Get fleets for a mission.

    Implements breadth-first search through systems starting at the **starting_sytem**.
    In each system, local fleets are checked if they are in the allowed **fleet_pool_set** and suitable for the mission.
    If so, they are added to the **fleet_list** and **cur_stats** is updated with the currently selected fleet summary.
    The search continues until the requirements defined in **target_stats** are met or there are no more systems/fleets.
    In that case, if the **min_stats** are covered, the **fleet_list** is returned anyway.
    Otherwise, an empty list is returned by the function, in which case the caller can make an evaluation of
    an emergency use of the found fleets in fleet_list; if not to be used they should be added back to the main pool.

    :param target_stats: stats the fleet should ideally meet
    :param min_stats: minimum stats the final fleet must meet to be accepted
    :param cur_stats: (**mutated**) stat summary of selected fleets
    :param starting_system: system_id where breadth-first-search is centered
    :param fleet_pool_set: (**mutated**) fleets allowed to be selected. Split fleed_ids are added, used ones removed.
    :param fleet_list: (**mutated**) fleets that are selected for the mission. Gets filled during the call.
    :param species: species for colonization mission
    :param bool ensure_return: If true, fleet must have sufficient fuel to return into supply after mission
    :return: List of selected fleet_ids or empty list if couldn't meet minimum requirements.
    """
    universe = fo.getUniverse()
    colonization_roles = (ShipRoleType.CIVILIAN_COLONISATION,
                          ShipRoleType.BASE_COLONISATION)
    systems_enqueued = [starting_system]
    systems_visited = []
    # loop over systems in a breadth-first-search trying to find nearby suitable ships in fleet_pool_set
    aistate = get_aistate()
    while systems_enqueued and fleet_pool_set:
        this_system_id = systems_enqueued.pop(0)
        this_system_obj = TargetSystem(this_system_id)
        systems_visited.append(this_system_id)
        accessible_fleets = aistate.systemStatus.get(this_system_id, {}).get(
            "myFleetsAccessible", [])
        fleets_here = [
            fid for fid in accessible_fleets if fid in fleet_pool_set
        ]
        # loop over all fleets in the system, split them if possible and select suitable ships
        while fleets_here:
            fleet_id = fleets_here.pop(0)
            fleet = universe.getFleet(fleet_id)
            if not fleet:  # TODO should be checked before passed to the function
                fleet_pool_set.remove(fleet_id)
                continue
            # try splitting fleet
            if fleet.numShips > 1:
                debug("Splitting candidate fleet to get ships for mission.")
                new_fleets = split_fleet(fleet_id)
                fleet_pool_set.update(new_fleets)
                fleets_here.extend(new_fleets)

            if "target_system" in target_stats and not MoveUtilsAI.can_travel_to_system(
                    fleet_id,
                    this_system_obj,
                    target_stats["target_system"],
                    ensure_return=ensure_return):
                continue

            # check species for colonization missions
            if species:
                for ship_id in fleet.shipIDs:
                    ship = universe.getShip(ship_id)
                    if (ship and aistate.get_ship_role(
                            ship.design.id) in colonization_roles
                            and species == ship.speciesName):
                        break
                else:  # no suitable species found
                    continue
            # check troop capacity for invasion missions
            troop_capacity = 0
            if "troopCapacity" in target_stats:
                troop_capacity = count_troops_in_fleet(fleet_id)
                if troop_capacity <= 0:
                    continue

            # check if we need additional rating vs planets
            this_rating_vs_planets = 0
            if "ratingVsPlanets" in target_stats:
                this_rating_vs_planets = aistate.get_rating(
                    fleet_id, against_planets=True)
                if this_rating_vs_planets <= 0 and cur_stats.get(
                        "rating", 0) >= target_stats.get("rating", 0):
                    # we already have enough general rating, so do not add any more warships useless against planets
                    continue

            # all checks passed, add ship to selected fleets and update the stats
            try:
                fleet_pool_set.remove(fleet_id)
            except KeyError:
                error(
                    "After having split a fleet, the original fleet apparently no longer exists.",
                    exc_info=True)
                continue
            fleet_list.append(fleet_id)

            this_rating = aistate.get_rating(fleet_id)
            cur_stats["rating"] = combine_ratings(cur_stats.get("rating", 0),
                                                  this_rating)
            if "ratingVsPlanets" in target_stats:
                cur_stats["ratingVsPlanets"] = combine_ratings(
                    cur_stats.get("ratingVsPlanets", 0),
                    this_rating_vs_planets)
            if "troopCapacity" in target_stats:
                cur_stats["troopCapacity"] = cur_stats.get("troopCapacity",
                                                           0) + troop_capacity
            # if we already meet the requirements, we can stop looking for more ships
            if (sum(len(universe.getFleet(fid).shipIDs)
                    for fid in fleet_list) >= 1) and stats_meet_reqs(
                        cur_stats, target_stats):
                return fleet_list

        # finished system without meeting requirements. Add neighboring systems to search queue.
        for neighbor_id in get_neighbors(this_system_id):
            if all((
                    neighbor_id not in systems_visited,
                    neighbor_id not in systems_enqueued,
                    neighbor_id in aistate.exploredSystemIDs,
            )):
                systems_enqueued.append(neighbor_id)
    # we ran out of systems or fleets to check but did not meet requirements yet.
    if stats_meet_reqs(cur_stats, min_stats) and any(
            universe.getFleet(fid).shipIDs for fid in fleet_list):
        return fleet_list
    else:
        return []
Beispiel #21
0
def assess_protection_focus(pinfo, priority):
    """Return True if planet should use Protection Focus."""
    this_planet = pinfo.planet
    # this is unrelated to military threats
    stability_bonus = (pinfo.current_focus == PROTECTION
                       ) * fo.getNamedValue("PROTECION_FOCUS_STABILITY_BONUS")
    # industry and research produce nothing below 0
    threshold = -1 * (pinfo.current_focus not in (INDUSTRY, RESEARCH))
    # Negative IP lowers stability. Trying to counter this by setting planets to Protection just makes it worse!
    ip = fo.getEmpire().resourceAvailable(fo.resourceType.influence)
    if ip >= 0 and this_planet.currentMeterValue(
            fo.meterType.targetHappiness) < threshold + stability_bonus:
        debug("Advising Protection Focus at %s to avoid rebellion",
              this_planet)
        return True
    aistate = get_aistate()
    sys_status = aistate.systemStatus.get(this_planet.systemID, {})
    threat_from_supply = (
        0.25 * aistate.empire_standard_enemy_rating *
        min(2, len(sys_status.get("enemies_nearly_supplied", []))))
    debug("%s has regional+supply threat of %.1f", this_planet,
          threat_from_supply)
    regional_threat = sys_status.get("regional_threat", 0) + threat_from_supply
    if not regional_threat:  # no need for protection
        if pinfo.current_focus == PROTECTION:
            debug(
                "Advising dropping Protection Focus at %s due to no regional threat",
                this_planet)
        return False
    cur_prod_val = weighted_sum_output((pinfo.current_output, priority))
    target_prod_val = max(
        map(
            weighted_sum_output,
            [
                (pinfo.possible_output[INDUSTRY], priority),
                (pinfo.possible_output[RESEARCH], priority),
                (pinfo.possible_output[INFLUENCE], priority),
            ],
        ))
    prot_prod_val = weighted_sum_output(
        (pinfo.possible_output[PROTECTION], priority))
    local_production_diff = 0.5 * cur_prod_val + 0.5 * target_prod_val - prot_prod_val
    fleet_threat = sys_status.get("fleetThreat", 0)
    # TODO: relax the below rejection once the overall determination of PFocus is better tuned
    # priorities have a magnitude of 50
    if not fleet_threat and local_production_diff > 200:
        if pinfo.current_focus == PROTECTION:
            debug(
                "Advising dropping Protection Focus at %s due to excessive productivity loss",
                this_planet)
        return False
    local_p_defenses = sys_status.get("mydefenses", {}).get("overall", 0)
    # TODO have adjusted_p_defenses take other in-system planets into account
    adjusted_p_defenses = local_p_defenses * (
        1.0 if pinfo.current_focus != PROTECTION else 0.5)
    local_fleet_rating = sys_status.get("myFleetRating", 0)
    combined_local_defenses = sys_status.get("all_local_defenses", 0)
    my_neighbor_rating = sys_status.get("my_neighbor_rating", 0)
    neighbor_threat = sys_status.get("neighborThreat", 0)
    safety_factor = 1.2 if pinfo.current_focus == PROTECTION else 0.5
    cur_shield = this_planet.initialMeterValue(fo.meterType.shield)
    max_shield = this_planet.initialMeterValue(fo.meterType.maxShield)
    cur_troops = this_planet.initialMeterValue(fo.meterType.troops)
    max_troops = this_planet.initialMeterValue(fo.meterType.maxTroops)
    cur_defense = this_planet.initialMeterValue(fo.meterType.defense)
    max_defense = this_planet.initialMeterValue(fo.meterType.maxDefense)
    def_meter_pairs = [(cur_troops, max_troops), (cur_shield, max_shield),
                       (cur_defense, max_defense)]
    use_protection = True
    reason = ""
    if fleet_threat and (  # i.e., an enemy is sitting on us
            pinfo.current_focus != PROTECTION
            or  # too late to start protection TODO: but maybe regen worth it
            # protection focus only useful here if it maintains an elevated level
            all([
                AIDependencies.PROT_FOCUS_MULTIPLIER * a <= b
                for a, b in def_meter_pairs
            ])):
        use_protection = False
        reason = "A"
    elif ((pinfo.current_focus != PROTECTION and cur_shield < max_shield - 2
           and not tech_is_complete(AIDependencies.PLANET_BARRIER_I_TECH))
          and (cur_defense < max_defense - 2
               and not tech_is_complete(AIDependencies.DEFENSE_REGEN_1_TECH))
          and (cur_troops < max_troops - 2)):
        use_protection = False
        reason = "B1"
    elif (
        (pinfo.current_focus == PROTECTION
         and cur_shield * AIDependencies.PROT_FOCUS_MULTIPLIER < max_shield - 2
         and not tech_is_complete(AIDependencies.PLANET_BARRIER_I_TECH)) and
        (cur_defense * AIDependencies.PROT_FOCUS_MULTIPLIER < max_defense - 2
         and not tech_is_complete(AIDependencies.DEFENSE_REGEN_1_TECH)) and
        (cur_troops * AIDependencies.PROT_FOCUS_MULTIPLIER < max_troops - 2)):
        use_protection = False
        reason = "B2"
    elif max(max_shield, max_troops, max_defense) < 3:
        # joke defenses, don't bother with protection focus
        use_protection = False
        reason = "C"
    elif regional_threat and local_production_diff <= 2.0:
        use_protection = True
        reason = "D"
    elif safety_factor * regional_threat <= local_fleet_rating:
        use_protection = False
        reason = "E"
    elif safety_factor * regional_threat <= combined_local_defenses and (
            pinfo.current_focus != PROTECTION or
        (0.5 * safety_factor * regional_threat <= local_fleet_rating
         and fleet_threat == 0 and neighbor_threat < combined_local_defenses
         and local_production_diff > 5)):
        use_protection = False
        reason = "F"
    elif (regional_threat <= combine_ratings(local_fleet_rating,
                                             adjusted_p_defenses)
          and safety_factor * regional_threat <= combine_ratings(
              my_neighbor_rating, local_fleet_rating, adjusted_p_defenses)
          and local_production_diff > 5):
        use_protection = False
        reason = "G"
    if use_protection or pinfo.current_focus == PROTECTION:
        debug(
            "Advising %sProtection Focus (reason %s) for planet %s, with local_prod_diff of %.1f, comb. local"
            " defenses %.1f, local fleet rating %.1f and regional threat %.1f, threat sources: %s",
            ["dropping ", ""][use_protection],
            reason,
            this_planet,
            local_production_diff,
            combined_local_defenses,
            local_fleet_rating,
            regional_threat,
            sys_status["regional_fleet_threats"],
        )
    return use_protection
Beispiel #22
0
def assess_protection_focus(pinfo):
    """Return True if planet should use Protection Focus."""
    this_planet = pinfo.planet
    aistate = get_aistate()
    sys_status = aistate.systemStatus.get(this_planet.systemID, {})
    threat_from_supply = (
        0.25 * aistate.empire_standard_enemy_rating *
        min(2, len(sys_status.get('enemies_nearly_supplied', []))))
    debug("%s has regional+supply threat of %.1f", this_planet,
          threat_from_supply)
    regional_threat = sys_status.get('regional_threat', 0) + threat_from_supply
    if not regional_threat:  # no need for protection
        if pinfo.current_focus == PROTECTION:
            debug(
                "Advising dropping Protection Focus at %s due to no regional threat",
                this_planet)
        return False
    cur_prod_val = weighted_sum_output(pinfo.current_output)
    target_prod_val = max(
        map(weighted_sum_output,
            [pinfo.possible_output[INDUSTRY], pinfo.possible_output[RESEARCH]
             ]))
    prot_prod_val = weighted_sum_output(pinfo.possible_output[PROTECTION])
    local_production_diff = 0.8 * cur_prod_val + 0.2 * target_prod_val - prot_prod_val
    fleet_threat = sys_status.get('fleetThreat', 0)
    # TODO: relax the below rejection once the overall determination of PFocus is better tuned
    if not fleet_threat and local_production_diff > 8:
        if pinfo.current_focus == PROTECTION:
            debug(
                "Advising dropping Protection Focus at %s due to excessive productivity loss",
                this_planet)
        return False
    local_p_defenses = sys_status.get('mydefenses', {}).get('overall', 0)
    # TODO have adjusted_p_defenses take other in-system planets into account
    adjusted_p_defenses = local_p_defenses * (
        1.0 if pinfo.current_focus != PROTECTION else 0.5)
    local_fleet_rating = sys_status.get('myFleetRating', 0)
    combined_local_defenses = sys_status.get('all_local_defenses', 0)
    my_neighbor_rating = sys_status.get('my_neighbor_rating', 0)
    neighbor_threat = sys_status.get('neighborThreat', 0)
    safety_factor = 1.2 if pinfo.current_focus == PROTECTION else 0.5
    cur_shield = this_planet.initialMeterValue(fo.meterType.shield)
    max_shield = this_planet.initialMeterValue(fo.meterType.maxShield)
    cur_troops = this_planet.initialMeterValue(fo.meterType.troops)
    max_troops = this_planet.initialMeterValue(fo.meterType.maxTroops)
    cur_defense = this_planet.initialMeterValue(fo.meterType.defense)
    max_defense = this_planet.initialMeterValue(fo.meterType.maxDefense)
    def_meter_pairs = [(cur_troops, max_troops), (cur_shield, max_shield),
                       (cur_defense, max_defense)]
    use_protection = True
    reason = ""
    if (fleet_threat and  # i.e., an enemy is sitting on us
        (
            pinfo.current_focus != PROTECTION
            or  # too late to start protection TODO: but maybe regen worth it
            # protection focus only useful here if it maintains an elevated level
            all([
                AIDependencies.PROT_FOCUS_MULTIPLIER * a <= b
                for a, b in def_meter_pairs
            ]))):
        use_protection = False
        reason = "A"
    elif ((pinfo.current_focus != PROTECTION and cur_shield < max_shield - 2
           and not tech_is_complete(AIDependencies.PLANET_BARRIER_I_TECH))
          and (cur_defense < max_defense - 2
               and not tech_is_complete(AIDependencies.DEFENSE_REGEN_1_TECH))
          and (cur_troops < max_troops - 2)):
        use_protection = False
        reason = "B1"
    elif (
        (pinfo.current_focus == PROTECTION
         and cur_shield * AIDependencies.PROT_FOCUS_MULTIPLIER < max_shield - 2
         and not tech_is_complete(AIDependencies.PLANET_BARRIER_I_TECH)) and
        (cur_defense * AIDependencies.PROT_FOCUS_MULTIPLIER < max_defense - 2
         and not tech_is_complete(AIDependencies.DEFENSE_REGEN_1_TECH)) and
        (cur_troops * AIDependencies.PROT_FOCUS_MULTIPLIER < max_troops - 2)):
        use_protection = False
        reason = "B2"
    elif max(max_shield, max_troops, max_defense) < 3:
        # joke defenses, don't bother with protection focus
        use_protection = False
        reason = "C"
    elif regional_threat and local_production_diff <= 2.0:
        use_protection = True
        reason = "D"
    elif safety_factor * regional_threat <= local_fleet_rating:
        use_protection = False
        reason = "E"
    elif (safety_factor * regional_threat <= combined_local_defenses and
          (pinfo.current_focus != PROTECTION or
           (0.5 * safety_factor * regional_threat <= local_fleet_rating
            and fleet_threat == 0 and neighbor_threat < combined_local_defenses
            and local_production_diff > 5))):
        use_protection = False
        reason = "F"
    elif (regional_threat <= combine_ratings(local_fleet_rating,
                                             adjusted_p_defenses)
          and safety_factor * regional_threat <= combine_ratings(
              my_neighbor_rating, local_fleet_rating, adjusted_p_defenses)
          and local_production_diff > 5):
        use_protection = False
        reason = "G"
    if use_protection or pinfo.current_focus == PROTECTION:
        debug(
            "Advising %sProtection Focus (reason %s) for planet %s, with local_prod_diff of %.1f, comb. local"
            " defenses %.1f, local fleet rating %.1f and regional threat %.1f, threat sources: %s",
            ["dropping ", ""][use_protection], reason, this_planet,
            local_production_diff, combined_local_defenses, local_fleet_rating,
            regional_threat, sys_status['regional_fleet_threats'])
    return use_protection