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)
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
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
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))
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)
def get_rating_vs_planets(self) -> float: return combine_ratings(x.get_rating_vs_planets() for x in self._ship_stats)
def test_idempotency(): assert combine_ratings(1, 1, 2) == combine_ratings( 2, 1, 1) == combine_ratings(1, 2, 1)
def test_merge_3_ratings_as_collection(iterable): assert combine_ratings(iterable) == pytest.approx(11.65, rel=1e-2)
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())
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))
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
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
def test_two_rating_are_merged(rating1, raring2, combined): assert combine_ratings(rating1, raring2) == combined
def test_merge_3_ratings(): assert combine_ratings(1, 1, 2) == pytest.approx(11.65, rel=1e-2)
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
def test_merge_3_ratings_as_collection_and_args(): assert combine_ratings([1, 1], 2) == pytest.approx(11.65, rel=1e-2)
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 []
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
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