1from logging import debug 2 3import freeOrionAIInterface as fo # pylint: disable=import-error 4import AIstate 5import CombatRatingsAI 6import EspionageAI 7import FleetUtilsAI 8import InvasionAI 9import PlanetUtilsAI 10import PriorityAI 11import ProductionAI 12from AIDependencies import INVALID_ID 13from aistate_interface import get_aistate 14from CombatRatingsAI import combine_ratings, combine_ratings_list, rating_difference 15from EnumsAI import MissionType 16from freeorion_tools import cache_by_turn_persistent 17from target import TargetSystem 18from turn_state import state 19 20MinThreat = 10 # the minimum threat level that will be ascribed to an unknown threat capable of killing scouts 21_military_allocations = [] 22_verbose_mil_reporting = False 23_best_ship_rating_cache = {} # indexed by turn, value is rating of that turn 24 25 26def cur_best_mil_ship_rating(include_designs=False): 27 """Find the best military ship we have available in this turn and return its rating. 28 29 :param include_designs: toggles if available designs are considered or only existing ships 30 :return: float: rating of the best ship 31 """ 32 current_turn = fo.currentTurn() 33 if current_turn in _best_ship_rating_cache: 34 best_rating = _best_ship_rating_cache[current_turn] 35 if include_designs: 36 best_design_rating = ProductionAI.cur_best_military_design_rating() 37 best_rating = max(best_rating, best_design_rating) 38 return best_rating 39 best_rating = 0.001 40 universe = fo.getUniverse() 41 aistate = get_aistate() 42 for fleet_id in FleetUtilsAI.get_empire_fleet_ids_by_role(MissionType.MILITARY): 43 fleet = universe.getFleet(fleet_id) 44 for ship_id in fleet.shipIDs: 45 ship_rating = CombatRatingsAI.ShipCombatStats(ship_id).get_rating(enemy_stats=aistate.get_standard_enemy()) 46 best_rating = max(best_rating, ship_rating) 47 _best_ship_rating_cache[current_turn] = best_rating 48 if include_designs: 49 best_design_rating = ProductionAI.cur_best_military_design_rating() 50 best_rating = max(best_rating, best_design_rating) 51 return max(best_rating, 0.001) 52 53 54def get_preferred_max_military_portion_for_single_battle(): 55 """ 56 Determine and return the preferred max portion of military to be allocated to a single battle. 57 58 May be used to downgrade various possible actions requiring military support if they would require an excessive 59 allocation of military forces. At the beginning of the game this max portion starts as 1.0, then is slightly 60 reduced to account for desire to reserve some defenses for other locations, and then in mid to late game, as the 61 size of the the military grows, this portion is further reduced to promote pursuit of multiple battlefronts in 62 parallel as opposed to single battlefronts against heavily defended positions. 63 64 :return: a number in range (0:1] for preferred max portion of military to be allocated to a single battle 65 :rtype: float 66 """ 67 # TODO: this is a roughcut first pass, needs plenty of refinement 68 if fo.currentTurn() < 40: 69 return 1.0 70 best_ship_equivalents = (get_concentrated_tot_mil_rating() / cur_best_mil_ship_rating())**0.5 71 _MAX_SHIPS_BEFORE_PREFERRING_LESS_THAN_FULL_ENGAGEMENT = 3 72 if best_ship_equivalents <= _MAX_SHIPS_BEFORE_PREFERRING_LESS_THAN_FULL_ENGAGEMENT: 73 return 1.0 74 # the below ratio_exponent is still very much a work in progress. It should probably be somewhere in the range of 75 # 0.2 to 0.5. Values at the larger end will create a smaller expected battle size threshold that would 76 # cause the respective opportunity (invasion, colonization) scores to be discounted, thereby more quickly creating 77 # pressure for the AI to pursue multiple small/medium resistance fronts rather than pursuing a smaller number fronts 78 # facing larger resistance. The AI will start facing some scoring pressure to not need to throw 100% of its 79 # military at a target as soon as its max military rating surpasses the equvalent of 80 # _MAX_SHIPS_BEFORE_PREFERRING_LESS_THAN_FULL_ENGAGEMENT of its best ships. That starts simply as some scoring 81 # pressure to be able to hold back some small portion of its ships from the engagement, in order to be able to use 82 # them for defense or for other targets. With an exponent value of 0.25, this would start creating substantial 83 # pressure against devoting more than half the military to a single target once the total military is somewhere 84 # above 18 best-ship equivalents, and pressure against deovting more than a third once the total is above about 80 85 # best-ship equivalents. With an exponent value of 0.5, those thresholds would be 6 ships and 11 ships. With the 86 # initial value of 0.35, those thresholds are about 10 ships and 25 ships. Depending on how this return value is 87 # used, it should not prevent the more heavily fortified targets (and therefore discounted) from being taken 88 # if there are no more remaining easier targets available. 89 ratio_exponent = 0.35 90 return 1.0 / (best_ship_equivalents + 1 - _MAX_SHIPS_BEFORE_PREFERRING_LESS_THAN_FULL_ENGAGEMENT)**ratio_exponent 91 92 93def try_again(mil_fleet_ids, try_reset=False, thisround=""): 94 """Clear targets and orders for all specified fleets then call get_military_fleets again.""" 95 aistate = get_aistate() 96 for fid in mil_fleet_ids: 97 mission = aistate.get_fleet_mission(fid) 98 mission.clear_fleet_orders() 99 mission.clear_target() 100 get_military_fleets(try_reset=try_reset, thisround=thisround) 101 102 103def avail_mil_needing_repair(mil_fleet_ids, split_ships=False, on_mission=False, repair_limit=0.70): 104 """Returns tuple of lists: (ids_needing_repair, ids_not).""" 105 fleet_buckets = [[], []] 106 universe = fo.getUniverse() 107 cutoff = [repair_limit, 0.25][on_mission] 108 aistate = get_aistate() 109 for fleet_id in mil_fleet_ids: 110 fleet = universe.getFleet(fleet_id) 111 ship_buckets = [[], []] 112 ships_cur_health = [0, 0] 113 ships_max_health = [0, 0] 114 for ship_id in fleet.shipIDs: 115 this_ship = universe.getShip(ship_id) 116 cur_struc = this_ship.initialMeterValue(fo.meterType.structure) 117 max_struc = this_ship.initialMeterValue(fo.meterType.maxStructure) 118 ship_ok = cur_struc >= cutoff * max_struc 119 ship_buckets[ship_ok].append(ship_id) 120 ships_cur_health[ship_ok] += cur_struc 121 ships_max_health[ship_ok] += max_struc 122 this_sys_id = fleet.systemID if fleet.nextSystemID == INVALID_ID else fleet.nextSystemID 123 fleet_ok = (sum(ships_cur_health) >= cutoff * sum(ships_max_health)) 124 local_status = aistate.systemStatus.get(this_sys_id, {}) 125 my_local_rating = combine_ratings(local_status.get('mydefenses', {}).get('overall', 0), local_status.get('myFleetRating', 0)) 126 my_local_rating_vs_planets = local_status.get('myFleetRatingVsPlanets', 0) 127 combat_trigger = bool(local_status.get('fleetThreat', 0) or local_status.get('monsterThreat', 0)) 128 if not combat_trigger and local_status.get('planetThreat', 0): 129 universe = fo.getUniverse() 130 system = universe.getSystem(this_sys_id) 131 for planet_id in system.planetIDs: 132 planet = universe.getPlanet(planet_id) 133 if planet.ownedBy(fo.empireID()): # TODO: also exclude at-peace planets 134 continue 135 if planet.unowned and not EspionageAI.colony_detectable_by_empire(planet_id, empire=fo.empireID()): 136 continue 137 if sum([planet.currentMeterValue(meter_type) for meter_type in 138 [fo.meterType.defense, fo.meterType.shield, fo.meterType.construction]]): 139 combat_trigger = True 140 break 141 needed_here = combat_trigger and local_status.get('totalThreat', 0) > 0 # TODO: assess if remaining other forces are sufficient 142 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 143 if not fleet_ok: 144 if safely_needed: 145 debug("Fleet %d at %s needs repair but deemed safely needed to remain for defense" % (fleet_id, universe.getSystem(fleet.systemID))) 146 else: 147 if needed_here: 148 debug("Fleet %d at %s needed present for combat, but is damaged and deemed unsafe to remain." % (fleet_id, universe.getSystem(fleet.systemID))) 149 debug("\t my_local_rating: %.1f ; threat: %.1f" % (my_local_rating, local_status.get('totalThreat', 0))) 150 debug("Selecting fleet %d at %s for repair" % (fleet_id, universe.getSystem(fleet.systemID))) 151 fleet_buckets[fleet_ok or bool(safely_needed)].append(fleet_id) 152 return fleet_buckets 153 154 155# TODO Move relevant initialization code from get_military_fleets into this class 156class AllocationHelper: 157 158 def __init__(self, already_assigned_rating, already_assigned_rating_vs_planets, available_rating, try_reset): 159 """ 160 :param dict already_assigned_rating: 161 :param float available_rating: 162 """ 163 self.try_reset = try_reset 164 self.allocations = [] 165 self.allocation_by_groups = {} 166 167 self.available_rating = available_rating 168 self._remaining_rating = available_rating 169 170 self.threat_bias = 0. 171 self.safety_factor = get_aistate().character.military_safety_factor() 172 173 self.already_assigned_rating = dict(already_assigned_rating) 174 self.already_assigned_rating_vs_planets = dict(already_assigned_rating_vs_planets) 175 # store the number of empires which have supply or have supply within 2 jumps of the system 176 self.enemy_supply = {sys_id: min(2, len(enemies_nearly_supplying_system(sys_id))) 177 for sys_id in fo.getUniverse().systemIDs} 178 179 @property 180 def remaining_rating(self): 181 return self._remaining_rating 182 183 @remaining_rating.setter 184 def remaining_rating(self, value): 185 self._remaining_rating = max(0, value) 186 187 def allocate(self, group, sys_id, min_rating, min_rating_vs_planets, take_any, max_rating): 188 tup = (sys_id, min_rating, min_rating_vs_planets, take_any, max_rating) 189 self.allocations.append(tup) 190 self.allocation_by_groups.setdefault(group, []).append(tup) 191 if self._remaining_rating <= min_rating: 192 self._remaining_rating = 0 193 else: 194 self._remaining_rating = rating_difference(self._remaining_rating, min_rating) 195 196 197class Allocator: 198 """ 199 Base class for Military allocation for a single system. 200 201 The Allocator class and its subclasses are used to allocate 202 military resources for a single system. First, a minimum 203 and a maximum military rating are calculated which are 204 required / desired based on e.g. threat in the system. 205 206 An Allocator class defines if military resources should 207 be allocated even if the minimum requirements are not met 208 or if military resources are only allocated if the threshold 209 is passed. 210 211 The information is then passed to an AllocationHelper 212 instance. Allocating military resources by an Allocator 213 does not necessarily mean that military ships are actually 214 assigned to that system. It should be understood as a request 215 instead. If allocations of higher priority already use all the 216 available military resources, no ships can be sent. 217 218 219 Public methods: 220 :allocate(): Calculate the required/desired military resources 221 for the system and enqueue the allocation info 222 to the AllocationHelper. 223 224 225 Public attributes: 226 :ivar sys_id: ID of the system for which military resources are allocated 227 228 229 Example usage: 230 CapitelDefenseAllocator(capital_sys_id, allocation_helper).allocate() 231 """ 232 _min_alloc_factor = 1. 233 _max_alloc_factor = 2. 234 _potential_threat_factor = 1. 235 _allocation_group = '' 236 _military_reset_ratio = -1 # if ratio of available to needed rating is smaller than this, then reset allocations 237 238 def __init__(self, sys_id, allocation_helper): 239 """ 240 :param int sys_id: System for which military resources are allocated 241 :param AllocationHelper allocation_helper: The allocation helper where the information is to be stored. 242 """ 243 self.sys_id = sys_id 244 self._allocation_helper = allocation_helper 245 246 def allocate(self): 247 """Calculate the desired allocation for this system and enqueue it in the allocation_helper.""" 248 threat = self._calculate_threat() 249 min_alloc = self._minimum_allocation(threat) 250 max_alloc = self._maximum_allocation(threat) 251 alloc_vs_planets = self._allocation_vs_planets() 252 if min_alloc <= 0 and alloc_vs_planets <= 0: 253 # nothing to allocate here... 254 return 255 min_alloc = max(min_alloc, alloc_vs_planets) 256 max_alloc = max(max_alloc, alloc_vs_planets) 257 258 ratio = self._allocation_helper.remaining_rating / float(min_alloc) 259 if self._allocation_helper.remaining_rating > min_alloc or self._take_any(): 260 self._allocation_helper.allocate( 261 group=self._allocation_group, 262 sys_id=self.sys_id, 263 min_rating=min(min_alloc, self._allocation_helper.remaining_rating), 264 min_rating_vs_planets=min(alloc_vs_planets, self._allocation_helper.remaining_rating), 265 take_any=self._take_any(), 266 max_rating=max_alloc, 267 ) 268 if ratio < 1: 269 self._handle_not_enough_resources(ratio) 270 271 def _calculate_threat(self): 272 """ 273 Calculate the required military rating in the system. 274 275 The value calculated does not have to represent a tangible 276 threat / enemy force. It only provides a measurement how much 277 military should be sent to the system. Deriving further conditions 278 for military presence and translating them into an equivalent 279 military rating is strongly encouraged. 280 It is implied however, that the value calculated here should 281 be greater than or at least equal to the actual visible strength 282 of enemy forces within the system so that a subsequent military 283 mission can be successful. 284 285 :return: Equivalent military rating required in the system 286 :rtype: float 287 """ 288 raise NotImplementedError 289 290 def _minimum_allocation(self, threat): 291 """ 292 Calculate the minimum allocation for the system. 293 294 The default minimum allocation is the missing forces 295 to obtain a rating given by the threat weighted with 296 the subclass' *min_alloc_factor*. 297 Existing military missions are considered. 298 299 Subclasses may choose to override this method and 300 implement a different logic. 301 302 :param float threat: threat as calculated by _calculate_threat() 303 :rtype: float 304 """ 305 return CombatRatingsAI.rating_needed( 306 self._min_alloc_factor * threat, 307 self.assigned_rating) 308 309 def _maximum_allocation(self, threat): 310 """ 311 Calculate the maximum allocation for the system. 312 313 The default maximum allocation is the missing forces 314 to obtain a rating given by the threat weighted with 315 the subclass' *max_alloc_factor*. 316 Existing military missions are considered. 317 318 Subclasses may choose to override this method and 319 implement a different logic. 320 321 :param float threat: 322 :rtype: float 323 """ 324 return CombatRatingsAI.rating_needed( 325 self._max_alloc_factor * threat, 326 self.assigned_rating) 327 328 def _allocation_vs_planets(self): 329 return CombatRatingsAI.rating_needed( 330 self.safety_factor * self._planet_threat(), 331 self.assigned_rating_vs_planets) 332 333 def _take_any(self): 334 """ 335 If true, forces smaller than the minimum allocation are accepted. 336 337 :rtype: bool 338 """ 339 raise NotImplementedError 340 341 def _handle_not_enough_resources(self, ratio): 342 """Called if minimum allocation is larget than available resources. 343 344 High priority subclasses are expected to throw a ReleaseMilitaryException 345 which should be caught from the caller of allocate() and trigger the 346 release of all military resources so they can be reassigned to higher 347 priority targets. 348 349 :param float ratio: ratio of available resources to minimum allocation 350 """ 351 if ratio < self._military_reset_ratio and self._allocation_helper.try_reset: 352 raise ReleaseMilitaryException 353 354 @property 355 def nearby_empire_count(self): 356 """The number of enemy empires within at most 2 jumps.""" 357 return self._allocation_helper.enemy_supply.get(self.sys_id, 0) 358 359 @property 360 def threat_bias(self): 361 """A constant threat biases added additively to the calculated threat.""" 362 return self._allocation_helper.threat_bias 363 364 @property 365 def safety_factor(self): 366 """A multiplicative factor for threat calculations""" 367 return self._allocation_helper.safety_factor 368 369 @property 370 def assigned_rating(self): 371 """The combined rating of existing missions assigned to the system.""" 372 return self._allocation_helper.already_assigned_rating.get(self.sys_id, 0) 373 374 @property 375 def assigned_rating_vs_planets(self): 376 return self._allocation_helper.already_assigned_rating_vs_planets.get(self.sys_id, 0) 377 378 def _local_threat(self): 379 """Military rating of enemies present in the system.""" 380 return get_system_local_threat(self.sys_id) 381 382 def _neighbor_threat(self): 383 """Military rating of enemies present in neighboring system.""" 384 return get_system_neighbor_threat(self.sys_id) 385 386 def _jump2_threat(self): 387 """Military rating of enemies present 2 jumps away from the system.""" 388 return get_system_jump2_threat(self.sys_id) 389 390 def _potential_threat(self): 391 """Number of nearby enemies times the average enemy rating weighted by _potential_threat_factor""" 392 return self.nearby_empire_count * enemy_rating() * self._potential_threat_factor 393 394 def _regional_threat(self): 395 """Threat derived from enemy supply lanes.""" 396 return get_system_regional_threat(self.sys_id) 397 398 def _potential_support(self): 399 """Military rating of our forces in neighboring systems.""" 400 return get_system_neighbor_support(self.sys_id) 401 402 def _planet_threat(self): 403 return get_system_planetary_threat(self.sys_id) 404 405 def _enemy_ship_count(self): 406 return get_aistate().systemStatus.get(self.sys_id, {}).get('enemy_ship_count', 0.) 407 408 409class CapitalDefenseAllocator(Allocator): 410 411 _allocation_group = 'capitol' 412 _military_reset_ratio = 0.5 413 414 def _minimum_allocation(self, threat): 415 nearby_forces = CombatRatingsAI.combine_ratings( 416 self.assigned_rating, self._potential_support()) 417 return max( 418 CombatRatingsAI.rating_needed(self._regional_threat(), nearby_forces), 419 CombatRatingsAI.rating_needed(1.4*threat, self.assigned_rating)) 420 421 def _maximum_allocation(self, threat): 422 return max( 423 CombatRatingsAI.rating_needed(1.5*self._regional_threat(), self.assigned_rating), 424 CombatRatingsAI.rating_needed(2*threat, self.assigned_rating)) 425 426 def _calculate_threat(self): 427 potential_threat = max(self._potential_threat() - self._potential_support(), 0) 428 actual_threat = self.safety_factor * ( 429 2*self.threat_bias + 430 + CombatRatingsAI.combine_ratings(self._local_threat(), self._neighbor_threat())) 431 return potential_threat + actual_threat 432 433 def _take_any(self): 434 return True 435 436 437class PlanetDefenseAllocator(Allocator): 438 439 _allocation_group = 'occupied' 440 _min_alloc_factor = 1.1 441 _max_alloc_factor = 1.5 442 _potential_threat_factor = 0.5 443 _military_reset_ratio = 0.8 444 445 def allocate(self): 446 remaining_rating = self._allocation_helper.remaining_rating 447 if remaining_rating > 0: 448 super(PlanetDefenseAllocator, self).allocate() 449 return 450 if self._minimum_allocation(self._calculate_threat()): 451 pass # raise ReleaseMilitaryException TODO 452 453 def _minimum_allocation(self, threat): 454 super_call = super(PlanetDefenseAllocator, self)._minimum_allocation(threat) 455 restriction = 0.5 * self._allocation_helper.available_rating 456 return min(super_call, restriction) 457 458 def _calculate_threat(self): 459 nearby_forces = CombatRatingsAI.combine_ratings( 460 self._potential_support(), self.assigned_rating) 461 return ( 462 self.threat_bias + 463 + self.safety_factor * CombatRatingsAI.combine_ratings(self._local_threat(), 464 self._neighbor_threat()) + 465 + max(0., self._potential_threat() + self._jump2_threat() - nearby_forces)) 466 467 def _take_any(self): 468 return True 469 470 471class TargetAllocator(Allocator): 472 473 _allocation_group = 'otherTargets' 474 _min_alloc_factor = 1.3 475 _max_alloc_factor = 2.5 476 _potential_threat_factor = 0.5 477 478 def _calculate_threat(self): 479 return ( 480 self.threat_bias + 481 + self.safety_factor * CombatRatingsAI.combine_ratings_list([ 482 self._local_threat(), 483 .75 * self._neighbor_threat(), 484 .5 * self._jump2_threat()]) 485 + self._potential_threat()) 486 487 def _take_any(self): 488 return self.assigned_rating > 0 489 490 def _planet_threat_multiplier(self): 491 # to the extent that enemy planetary defenses are bolstered by fleet defenses, a smaller portion of our 492 # attacks will land on the planet and hence we need a greater portion of planet-effective attacks. One 493 # desired characteristic of the following planet_threat_multiplier is that if the entire local threat is 494 # due to planetary threat then we want the multiplier to be unity. Furthermore, the more enemy ships are 495 # present, the smaller proportion of our attacks would be directed against the enemy planet. The following is 496 # just one of many forms of calculation that might work reasonably. 497 # TODO: assess and revamp the planet_threat_multiplier calculation 498 return ((self._enemy_ship_count() + self._local_threat()/self._planet_threat())**0.5 499 if self._planet_threat() > 0 else 1.0) 500 501 def _allocation_vs_planets(self): 502 return CombatRatingsAI.rating_needed( 503 self.safety_factor*self._planet_threat_multiplier()*self._planet_threat(), 504 self.assigned_rating_vs_planets) 505 506 507class TopTargetAllocator(TargetAllocator): 508 _allocation_group = 'topTargets' 509 _max_alloc_factor = 3 510 511 512class OutpostTargetAllocator(TargetAllocator): 513 _max_alloc_factor = 3 514 515 516class BlockadeAllocator(TargetAllocator): 517 _potential_threat_factor = 0.25 518 _max_alloc_factor = 1.5 519 520 def _maximum_allocation(self, threat): 521 return min(self._minimum_allocation(threat), self._allocation_helper.remaining_rating) * self._max_alloc_factor 522 523 524class LocalThreatAllocator(Allocator): 525 _potential_threat_factor = 0 526 _min_alloc_factor = 1.3 527 _max_alloc_factor = 2 528 _allocation_group = 'otherTargets' 529 530 def _calculate_threat(self): 531 532 systems_status = get_aistate().systemStatus.get(self.sys_id, {}) 533 threat = self.safety_factor * CombatRatingsAI.combine_ratings(systems_status.get('fleetThreat', 0), 534 systems_status.get('monsterThreat', 0) + 535 + systems_status.get('planetThreat', 0)) 536 537 return self.threat_bias + threat 538 539 def _take_any(self): 540 return False 541 542 543class InteriorTargetsAllocator(LocalThreatAllocator): 544 _max_alloc_factor = 2.5 545 _min_alloc_factor = 1.3 546 547 def _calculate_threat(self): 548 return self.threat_bias + self.safety_factor * self._local_threat() 549 550 def _maximum_allocation(self, threat): 551 return self._max_alloc_factor * min(self._minimum_allocation(threat), self._allocation_helper.remaining_rating) 552 553 def _take_any(self): 554 return self.assigned_rating > 0 555 556 557class ExplorationTargetAllocator(LocalThreatAllocator): 558 _potential_threat_factor = 0.25 559 _max_alloc_factor = 2.0 560 _allocation_group = 'exploreTargets' 561 562 def _calculate_threat(self): 563 return self.safety_factor * self._local_threat() + self._potential_threat() 564 565 def _take_any(self): 566 return False 567 568 569class BorderSecurityAllocator(LocalThreatAllocator): 570 _min_alloc_factor = 1.2 571 _max_alloc_factor = 2 572 _allocation_group = 'accessibleTargets' 573 574 def __init__(self, sys_id, allocation_helper): 575 super(BorderSecurityAllocator, self).__init__(sys_id, allocation_helper) 576 577 def _maximum_allocation(self, threat): 578 return self._max_alloc_factor * self.safety_factor * max(self._local_threat(), self._neighbor_threat()) 579 580 581class ReleaseMilitaryException(Exception): 582 pass 583 584 585# TODO: May want to move these functions into AIstate class 586def get_system_local_threat(sys_id): 587 return get_aistate().systemStatus.get(sys_id, {}).get('totalThreat', 0.) 588 589 590def get_system_jump2_threat(sys_id): 591 return get_aistate().systemStatus.get(sys_id, {}).get('jump2_threat', 0.) 592 593 594def get_system_neighbor_support(sys_id): 595 return get_aistate().systemStatus.get(sys_id, {}).get('my_neighbor_rating', 0.) 596 597 598def get_system_neighbor_threat(sys_id): 599 return get_aistate().systemStatus.get(sys_id, {}).get('neighborThreat', 0.) 600 601 602def get_system_regional_threat(sys_id): 603 return get_aistate().systemStatus.get(sys_id, {}).get('regional_threat', 0.) 604 605 606def get_system_planetary_threat(sys_id): 607 return get_aistate().systemStatus.get(sys_id, {}).get('planetThreat', 0.) 608 609 610def enemy_rating(): 611 """:rtype: float""" 612 return get_aistate().empire_standard_enemy_rating 613 614 615def get_my_defense_rating_in_system(sys_id): 616 return get_aistate().systemStatus.get(sys_id, {}).get('mydefenses', {}).get('overall') 617 618 619def enemies_nearly_supplying_system(sys_id): 620 return get_aistate().systemStatus.get(sys_id, {}).get('enemies_nearly_supplied', []) 621 622 623def get_military_fleets(mil_fleets_ids=None, try_reset=True, thisround="Main"): 624 """Get armed military fleets.""" 625 global _military_allocations 626 627 universe = fo.getUniverse() 628 empire_id = fo.empireID() 629 home_system_id = PlanetUtilsAI.get_capital_sys_id() 630 631 all_military_fleet_ids = (mil_fleets_ids if mil_fleets_ids is not None 632 else FleetUtilsAI.get_empire_fleet_ids_by_role(MissionType.MILITARY)) 633 634 # Todo: This block had been originally added to address situations where fleet missions were not properly 635 # terminating, leaving fleets stuck in stale deployments. Assess if this block is still needed at all; delete 636 # if not, otherwise restructure the following code so that in event a reset is occurring greater priority is given 637 # to providing military support to locations where a necessary Secure mission might have just been released (i.e., 638 # at invasion and colony/outpost targets where the troopships and colony ships are on their way), or else allow 639 # only a partial reset which does not reset Secure missions. 640 enable_periodic_mission_reset = False 641 if enable_periodic_mission_reset and try_reset and (fo.currentTurn() + empire_id) % 30 == 0 and thisround == "Main": 642 debug("Resetting all Military missions as part of an automatic periodic reset to clear stale missions.") 643 try_again(all_military_fleet_ids, try_reset=False, thisround=thisround + " Reset") 644 return 645 646 mil_fleets_ids = list(FleetUtilsAI.extract_fleet_ids_without_mission_types(all_military_fleet_ids)) 647 mil_needing_repair_ids, mil_fleets_ids = avail_mil_needing_repair(mil_fleets_ids, split_ships=True) 648 avail_mil_rating = combine_ratings_list(CombatRatingsAI.get_fleet_rating(x) for x in mil_fleets_ids) 649 650 if not mil_fleets_ids: 651 if "Main" in thisround: 652 _military_allocations = [] 653 return [] 654 655 # for each system, get total rating of fleets assigned to it 656 already_assigned_rating = {} 657 already_assigned_rating_vs_planets = {} 658 aistate = get_aistate() 659 systems_status = aistate.systemStatus 660 enemy_sup_factor = {} # enemy supply 661 for sys_id in universe.systemIDs: 662 already_assigned_rating[sys_id] = 0 663 already_assigned_rating_vs_planets[sys_id] = 0 664 enemy_sup_factor[sys_id] = min(2, len(systems_status.get(sys_id, {}).get('enemies_nearly_supplied', []))) 665 for fleet_id in [fid for fid in all_military_fleet_ids if fid not in mil_fleets_ids]: 666 ai_fleet_mission = aistate.get_fleet_mission(fleet_id) 667 if not ai_fleet_mission.target: # shouldn't really be possible 668 continue 669 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? 670 this_rating = CombatRatingsAI.get_fleet_rating(fleet_id) 671 this_rating_vs_planets = CombatRatingsAI.get_fleet_rating_against_planets(fleet_id) 672 already_assigned_rating[last_sys] = CombatRatingsAI.combine_ratings( 673 already_assigned_rating.get(last_sys, 0), this_rating) 674 already_assigned_rating_vs_planets[last_sys] = CombatRatingsAI.combine_ratings( 675 already_assigned_rating_vs_planets.get(last_sys, 0), this_rating_vs_planets) 676 for sys_id in universe.systemIDs: 677 my_defense_rating = systems_status.get(sys_id, {}).get('mydefenses', {}).get('overall', 0) 678 already_assigned_rating[sys_id] = CombatRatingsAI.combine_ratings(my_defense_rating, already_assigned_rating[sys_id]) 679 if _verbose_mil_reporting and already_assigned_rating[sys_id]: 680 debug("\t System %s already assigned rating %.1f" % ( 681 universe.getSystem(sys_id), already_assigned_rating[sys_id])) 682 683 # get systems to defend 684 capital_id = PlanetUtilsAI.get_capital() 685 if capital_id is not None: 686 capital_planet = universe.getPlanet(capital_id) 687 else: 688 capital_planet = None 689 # TODO: if no owned planets try to capture one! 690 if capital_planet: 691 capital_sys_id = capital_planet.systemID 692 else: # should be rare, but so as to not break code below, pick a randomish mil-centroid system 693 capital_sys_id = None # unless we can find one to use 694 system_dict = {} 695 for fleet_id in all_military_fleet_ids: 696 status = aistate.fleetStatus.get(fleet_id, None) 697 if status is not None: 698 system_id = status['sysID'] 699 if not list(universe.getSystem(system_id).planetIDs): 700 continue 701 system_dict[system_id] = system_dict.get(system_id, 0) + status.get('rating', 0) 702 ranked_systems = sorted([(val, sys_id) for sys_id, val in system_dict.items()]) 703 if ranked_systems: 704 capital_sys_id = ranked_systems[-1][-1] 705 else: 706 try: 707 capital_sys_id = next(iter(aistate.fleetStatus.items()))[1]['sysID'] 708 except: # noqa: E722 709 pass 710 711 num_targets = max(10, PriorityAI.allotted_outpost_targets) 712 top_target_planets = ([pid for pid, pscore, trp in AIstate.invasionTargets[:PriorityAI.allotted_invasion_targets()] 713 if pscore > InvasionAI.MIN_INVASION_SCORE] + 714 [pid for pid, (pscore, spec) in list(aistate.colonisableOutpostIDs.items())[:num_targets] 715 if pscore > InvasionAI.MIN_INVASION_SCORE] + 716 [pid for pid, (pscore, spec) in list(aistate.colonisablePlanetIDs.items())[:num_targets] 717 if pscore > InvasionAI.MIN_INVASION_SCORE]) 718 top_target_planets.extend(aistate.qualifyingTroopBaseTargets.keys()) 719 720 base_col_target_systems = PlanetUtilsAI.get_systems(top_target_planets) 721 top_target_systems = [] 722 for sys_id in AIstate.invasionTargetedSystemIDs + base_col_target_systems: 723 if sys_id not in top_target_systems: 724 if aistate.systemStatus[sys_id]['totalThreat'] > get_tot_mil_rating(): 725 continue 726 top_target_systems.append(sys_id) # doing this rather than set, to preserve order 727 728 try: 729 # capital defense 730 allocation_helper = AllocationHelper(already_assigned_rating, already_assigned_rating_vs_planets, avail_mil_rating, try_reset) 731 if capital_sys_id is not None: 732 CapitalDefenseAllocator(capital_sys_id, allocation_helper).allocate() 733 734 # defend other planets 735 empire_planet_ids = PlanetUtilsAI.get_owned_planets_by_empire(universe.planetIDs) 736 empire_occupied_system_ids = list(set(PlanetUtilsAI.get_systems(empire_planet_ids)) - {capital_sys_id}) 737 for sys_id in empire_occupied_system_ids: 738 PlanetDefenseAllocator(sys_id, allocation_helper).allocate() 739 740 # attack / protect high priority targets 741 for sys_id in top_target_systems: 742 TopTargetAllocator(sys_id, allocation_helper).allocate() 743 744 # enemy planets 745 other_targeted_system_ids = [sys_id for sys_id in set(PlanetUtilsAI.get_systems(AIstate.opponentPlanetIDs)) if 746 sys_id not in top_target_systems] 747 for sys_id in other_targeted_system_ids: 748 TargetAllocator(sys_id, allocation_helper).allocate() 749 750 # colony / outpost targets 751 other_targeted_system_ids = [sys_id for sys_id in 752 list(set(AIstate.colonyTargetedSystemIDs + AIstate.outpostTargetedSystemIDs)) if 753 sys_id not in top_target_systems] 754 for sys_id in other_targeted_system_ids: 755 OutpostTargetAllocator(sys_id, allocation_helper).allocate() 756 757 # TODO blockade enemy systems 758 759 # interior systems 760 targetable_ids = set(state.get_systems_by_supply_tier(0)) 761 current_mil_systems = [sid for sid, _, _, _, _ in allocation_helper.allocations] 762 interior_targets1 = targetable_ids.difference(current_mil_systems) 763 interior_targets = [sid for sid in interior_targets1 if ( 764 allocation_helper.threat_bias + systems_status.get(sid, {}).get('totalThreat', 0) > 0.8 * allocation_helper.already_assigned_rating[sid])] 765 for sys_id in interior_targets: 766 InteriorTargetsAllocator(sys_id, allocation_helper).allocate() 767 768 # TODO Exploration targets 769 770 # border protections 771 visible_system_ids = aistate.visInteriorSystemIDs | aistate.visBorderSystemIDs 772 accessible_system_ids = ([sys_id for sys_id in visible_system_ids if 773 universe.systemsConnected(sys_id, home_system_id, empire_id)] 774 if home_system_id != INVALID_ID else []) 775 current_mil_systems = [sid for sid, alloc, rvp, take_any, _ in allocation_helper.allocations if alloc > 0] 776 border_targets1 = [sid for sid in accessible_system_ids if sid not in current_mil_systems] 777 border_targets = [sid for sid in border_targets1 if ( 778 allocation_helper.threat_bias + systems_status.get(sid, {}).get('fleetThreat', 0) + systems_status.get(sid, {}).get( 779 'planetThreat', 0) > 0.8 * allocation_helper.already_assigned_rating[sid])] 780 for sys_id in border_targets: 781 BorderSecurityAllocator(sys_id, allocation_helper).allocate() 782 except ReleaseMilitaryException: 783 try_again(all_military_fleet_ids) 784 return 785 786 new_allocations = [] 787 remaining_mil_rating = avail_mil_rating 788 # for top categories assign max_alloc right away as available 789 for cat in ['capitol', 'occupied', 'topTargets']: 790 for sid, alloc, rvp, take_any, max_alloc in allocation_helper.allocation_by_groups.get(cat, []): 791 if remaining_mil_rating <= 0: 792 break 793 this_alloc = min(remaining_mil_rating, max_alloc) 794 new_allocations.append((sid, this_alloc, alloc, rvp, take_any)) 795 remaining_mil_rating = rating_difference(remaining_mil_rating, this_alloc) 796 797 base_allocs = set() 798 # for lower priority categories, first assign base_alloc around to all, then top up as available 799 for cat in ['otherTargets', 'accessibleTargets', 'exploreTargets']: 800 for sid, alloc, rvp, take_any, max_alloc in allocation_helper.allocation_by_groups.get(cat, []): 801 if remaining_mil_rating <= 0: 802 break 803 alloc = min(remaining_mil_rating, alloc) 804 base_allocs.add(sid) 805 remaining_mil_rating = rating_difference(remaining_mil_rating, alloc) 806 for cat in ['otherTargets', 'accessibleTargets', 'exploreTargets']: 807 for sid, alloc, rvp, take_any, max_alloc in allocation_helper.allocation_by_groups.get(cat, []): 808 if sid not in base_allocs: 809 break 810 if remaining_mil_rating <= 0: 811 new_allocations.append((sid, alloc, alloc, rvp, take_any)) 812 else: 813 local_max_avail = combine_ratings(remaining_mil_rating, alloc) 814 new_rating = min(local_max_avail, max_alloc) 815 new_allocations.append((sid, new_rating, alloc, rvp, take_any)) 816 remaining_mil_rating = rating_difference(local_max_avail, new_rating) 817 818 if "Main" in thisround: 819 _military_allocations = new_allocations 820 if _verbose_mil_reporting or "Main" in thisround: 821 debug("------------------------------\nFinal %s Round Military Allocations: %s \n-----------------------" % (thisround, dict([(sid, alloc) for sid, alloc, _, _, _ in new_allocations]))) 822 debug("(Apparently) remaining military rating: %.1f" % remaining_mil_rating) 823 824 return new_allocations 825 826 827def assign_military_fleets_to_systems(use_fleet_id_list=None, allocations=None, round=1): 828 # assign military fleets to military theater systems 829 global _military_allocations 830 universe = fo.getUniverse() 831 if allocations is None: 832 allocations = [] 833 834 doing_main = (use_fleet_id_list is None) 835 aistate = get_aistate() 836 if doing_main: 837 aistate.misc['ReassignedFleetMissions'] = [] 838 base_defense_ids = FleetUtilsAI.get_empire_fleet_ids_by_role(MissionType.ORBITAL_DEFENSE) 839 unassigned_base_defense_ids = FleetUtilsAI.extract_fleet_ids_without_mission_types(base_defense_ids) 840 for fleet_id in unassigned_base_defense_ids: 841 fleet = universe.getFleet(fleet_id) 842 if not fleet: 843 continue 844 sys_id = fleet.systemID 845 target = TargetSystem(sys_id) 846 fleet_mission = aistate.get_fleet_mission(fleet_id) 847 fleet_mission.clear_fleet_orders() 848 fleet_mission.clear_target() 849 mission_type = MissionType.ORBITAL_DEFENSE 850 fleet_mission.set_target(mission_type, target) 851 852 all_military_fleet_ids = FleetUtilsAI.get_empire_fleet_ids_by_role(MissionType.MILITARY) 853 if not all_military_fleet_ids: 854 _military_allocations = [] 855 return 856 avail_mil_fleet_ids = list(FleetUtilsAI.extract_fleet_ids_without_mission_types(all_military_fleet_ids)) 857 mil_needing_repair_ids, avail_mil_fleet_ids = avail_mil_needing_repair(avail_mil_fleet_ids) 858 these_allocations = _military_allocations 859 debug("==================================================") 860 debug("Assigning military fleets") 861 debug("---------------------------------") 862 else: 863 avail_mil_fleet_ids = list(use_fleet_id_list) 864 mil_needing_repair_ids, avail_mil_fleet_ids = avail_mil_needing_repair(avail_mil_fleet_ids) 865 these_allocations = allocations 866 867 # send_for_repair(mil_needing_repair_ids) #currently, let get taken care of by AIFleetMission.generate_fleet_orders() 868 869 # get systems to defend 870 871 avail_mil_fleet_ids = set(avail_mil_fleet_ids) 872 for sys_id, alloc, minalloc, rvp, takeAny in these_allocations: 873 if not doing_main and not avail_mil_fleet_ids: 874 break 875 debug("Allocating for: %s", TargetSystem(sys_id)) 876 found_fleets = [] 877 found_stats = {} 878 ensure_return = sys_id not in set(AIstate.colonyTargetedSystemIDs 879 + AIstate.outpostTargetedSystemIDs 880 + AIstate.invasionTargetedSystemIDs) 881 these_fleets = FleetUtilsAI.get_fleets_for_mission( 882 target_stats={'rating': alloc, 'ratingVsPlanets': rvp, 'target_system': TargetSystem(sys_id)}, 883 min_stats={'rating': minalloc, 'ratingVsPlanets': rvp, 'target_system': TargetSystem(sys_id)}, 884 cur_stats=found_stats, starting_system=sys_id, fleet_pool_set=avail_mil_fleet_ids, 885 fleet_list=found_fleets, ensure_return=ensure_return) 886 if not these_fleets: 887 debug("Could not allocate any fleets.") 888 if not found_fleets or not (FleetUtilsAI.stats_meet_reqs(found_stats, {'rating': minalloc}) or takeAny): 889 if doing_main: 890 if _verbose_mil_reporting: 891 debug("NO available/suitable military allocation for system %d ( %s ) " 892 "-- requested allocation %8d, found available rating %8d in fleets %s" 893 % (sys_id, universe.getSystem(sys_id).name, minalloc, 894 found_stats.get('rating', 0), found_fleets)) 895 avail_mil_fleet_ids.update(found_fleets) 896 continue 897 else: 898 these_fleets = found_fleets 899 else: 900 debug("Assigning fleets %s to target %s", these_fleets, TargetSystem(sys_id)) 901 if doing_main and _verbose_mil_reporting: 902 debug("FULL+ military allocation for system %d ( %s )" 903 " -- requested allocation %8d, got %8d with fleets %s" 904 % (sys_id, universe.getSystem(sys_id).name, alloc, found_stats.get('rating', 0), these_fleets)) 905 target = TargetSystem(sys_id) 906 for fleet_id in these_fleets: 907 fo.issueAggressionOrder(fleet_id, True) 908 fleet_mission = aistate.get_fleet_mission(fleet_id) 909 fleet_mission.clear_fleet_orders() 910 fleet_mission.clear_target() 911 if sys_id in set(AIstate.colonyTargetedSystemIDs + AIstate.outpostTargetedSystemIDs + AIstate.invasionTargetedSystemIDs): 912 mission_type = MissionType.SECURE 913 elif state.get_empire_planets_by_system(sys_id): 914 mission_type = MissionType.PROTECT_REGION 915 else: 916 mission_type = MissionType.MILITARY 917 fleet_mission.set_target(mission_type, target) 918 fleet_mission.generate_fleet_orders() 919 if not doing_main: 920 aistate.misc.setdefault('ReassignedFleetMissions', []).append(fleet_mission) 921 922 if doing_main: 923 debug("---------------------------------") 924 last_round = 3 925 last_round_name = "LastRound" 926 if round <= last_round: 927 # check if any fleets remain unassigned 928 all_military_fleet_ids = FleetUtilsAI.get_empire_fleet_ids_by_role(MissionType.MILITARY) 929 avail_mil_fleet_ids = list(FleetUtilsAI.extract_fleet_ids_without_mission_types(all_military_fleet_ids)) 930 allocations = [] 931 round += 1 932 thisround = "Extras Remaining Round %d" % round if round < last_round else last_round_name 933 if avail_mil_fleet_ids: 934 debug("Round %s - still have available military fleets: %s", thisround, avail_mil_fleet_ids) 935 allocations = get_military_fleets(mil_fleets_ids=avail_mil_fleet_ids, try_reset=False, thisround=thisround) 936 if allocations: 937 assign_military_fleets_to_systems(use_fleet_id_list=avail_mil_fleet_ids, allocations=allocations, round=round) 938 else: 939 # assign remaining fleets to nearest systems to protect. 940 all_military_fleet_ids = FleetUtilsAI.get_empire_fleet_ids_by_role(MissionType.MILITARY) 941 avail_mil_fleet_ids = list(FleetUtilsAI.extract_fleet_ids_without_mission_types(all_military_fleet_ids)) 942 943 def system_score(_fid, _sys_id): 944 """Helper function to rank systems by priority""" 945 jump_distance = universe.jumpDistance(_fid, _sys_id) 946 if get_system_local_threat(_sys_id): 947 weight = 10 948 elif get_system_neighbor_threat(_sys_id): 949 weight = 3 950 elif get_system_jump2_threat(_sys_id): 951 weight = 1 952 else: 953 weight = 1 / max(.5, float(state.get_distance_to_enemy_supply(_sys_id)))**1.25 954 return float(weight) / (jump_distance+1) 955 956 for fid in avail_mil_fleet_ids: 957 fleet = universe.getFleet(fid) 958 FleetUtilsAI.get_fleet_system(fleet) 959 systems = state.get_empire_planets_by_system().keys() 960 if not systems: 961 continue 962 sys_id = max(systems, key=lambda x: system_score(fid, x)) 963 964 debug("Assigning leftover %s to system %d " 965 "- nothing better to do.", fleet, sys_id) 966 967 fleet_mission = aistate.get_fleet_mission(fid) 968 fleet_mission.clear_fleet_orders() 969 target_system = TargetSystem(sys_id) 970 fleet_mission.set_target(MissionType.PROTECT_REGION, target_system) 971 fleet_mission.generate_fleet_orders() 972 973 974@cache_by_turn_persistent 975def get_tot_mil_rating(): 976 """ 977 Give an assessment of total military rating considering all fleets as if distributed to separate systems. 978 979 :return: a military rating value 980 :rtype: float 981 """ 982 return sum(CombatRatingsAI.get_fleet_rating(fleet_id) 983 for fleet_id in FleetUtilsAI.get_empire_fleet_ids_by_role(MissionType.MILITARY)) 984 985 986@cache_by_turn_persistent 987def get_concentrated_tot_mil_rating(): 988 """ 989 Give an assessment of total military rating as if all fleets were merged into a single mega-fleet. 990 991 :return: a military rating value 992 :rtype: float 993 """ 994 return CombatRatingsAI.combine_ratings_list([CombatRatingsAI.get_fleet_rating(fleet_id) for fleet_id in 995 FleetUtilsAI.get_empire_fleet_ids_by_role(MissionType.MILITARY)]) 996 997 998@cache_by_turn_persistent 999def get_num_military_ships(): 1000 fleet_status = get_aistate().fleetStatus 1001 return sum(fleet_status.get(fid, {}).get('nships', 0) 1002 for fid in FleetUtilsAI.get_empire_fleet_ids_by_role(MissionType.MILITARY)) 1003 1004 1005def get_military_fleets_with_target_system(target_system_id): 1006 military_mission_types = [MissionType.MILITARY, MissionType.SECURE] 1007 found_fleets = [] 1008 for fleet_mission in get_aistate().get_fleet_missions_with_any_mission_types(military_mission_types): 1009 if fleet_mission.target and fleet_mission.target.id == target_system_id: 1010 found_fleets.append(fleet_mission.fleet.id) 1011 return found_fleets 1012