1import math 2from logging import debug, info, warning 3 4import freeOrionAIInterface as fo 5 6from aistate_interface import get_aistate 7import AIDependencies 8import AIstate 9import ColonisationAI 10import CombatRatingsAI 11import EspionageAI 12import FleetUtilsAI 13import MilitaryAI 14import PlanetUtilsAI 15import ProductionAI 16from AIDependencies import INVALID_ID, Tags 17from EnumsAI import MissionType, PriorityType 18from common.print_utils import Table, Text, Float 19from freeorion_tools import tech_is_complete, AITimer, get_partial_visibility_turn, get_species_tag_grade 20from target import TargetPlanet, TargetSystem 21from turn_state import state 22 23MAX_BASE_TROOPERS_GOOD_INVADERS = 20 24MAX_BASE_TROOPERS_POOR_INVADERS = 10 25_TROOPS_SAFETY_MARGIN = 1 # try to send this amount of additional troops to account for uncertainties in calculation 26MIN_INVASION_SCORE = 20 27 28invasion_timer = AITimer('get_invasion_fleets()', write_log=False) 29 30 31def get_invasion_fleets(): 32 invasion_timer.start("gathering initial info") 33 universe = fo.getUniverse() 34 empire = fo.getEmpire() 35 empire_id = fo.empireID() 36 37 home_system_id = PlanetUtilsAI.get_capital_sys_id() 38 aistate = get_aistate() 39 visible_system_ids = list(aistate.visInteriorSystemIDs) + list(aistate.visBorderSystemIDs) 40 41 if home_system_id != INVALID_ID: 42 accessible_system_ids = [sys_id for sys_id in visible_system_ids if 43 (sys_id != INVALID_ID) and universe.systemsConnected(sys_id, home_system_id, 44 empire_id)] 45 else: 46 debug("Empire has no identifiable homeworld; will treat all visible planets as accessible.") 47 # TODO: check if any troop ships owned, use their system as home system 48 accessible_system_ids = visible_system_ids 49 50 acessible_planet_ids = PlanetUtilsAI.get_planets_in__systems_ids(accessible_system_ids) 51 all_owned_planet_ids = PlanetUtilsAI.get_all_owned_planet_ids(acessible_planet_ids) # includes unpopulated outposts 52 all_populated_planets = PlanetUtilsAI.get_populated_planet_ids(acessible_planet_ids) # includes unowned natives 53 empire_owned_planet_ids = PlanetUtilsAI.get_owned_planets_by_empire(universe.planetIDs) 54 invadable_planet_ids = set(all_owned_planet_ids).union(all_populated_planets) - set(empire_owned_planet_ids) 55 56 invasion_targeted_planet_ids = get_invasion_targeted_planet_ids(universe.planetIDs, MissionType.INVASION) 57 invasion_targeted_planet_ids.extend( 58 get_invasion_targeted_planet_ids(universe.planetIDs, MissionType.ORBITAL_INVASION)) 59 all_invasion_targeted_system_ids = set(PlanetUtilsAI.get_systems(invasion_targeted_planet_ids)) 60 61 invasion_fleet_ids = FleetUtilsAI.get_empire_fleet_ids_by_role(MissionType.INVASION) 62 num_invasion_fleets = len(FleetUtilsAI.extract_fleet_ids_without_mission_types(invasion_fleet_ids)) 63 64 debug("Current Invasion Targeted SystemIDs: %s" % PlanetUtilsAI.sys_name_ids(AIstate.invasionTargetedSystemIDs)) 65 debug("Current Invasion Targeted PlanetIDs: %s" % PlanetUtilsAI.planet_string(invasion_targeted_planet_ids)) 66 debug(invasion_fleet_ids and "Invasion Fleet IDs: %s" % invasion_fleet_ids or "Available Invasion Fleets: 0") 67 debug("Invasion Fleets Without Missions: %s" % num_invasion_fleets) 68 69 invasion_timer.start("planning troop base production") 70 reserved_troop_base_targets = [] 71 if aistate.character.may_invade_with_bases(): 72 available_pp = {} 73 for el in empire.planetsWithAvailablePP: # keys are sets of ints; data is doubles 74 avail_pp = el.data() 75 for pid in el.key(): 76 available_pp[pid] = avail_pp 77 # For planning base trooper invasion targets we have a two-pass system. (1) In the first pass we consider all 78 # the invasion targets and figure out which ones appear to be suitable for using base troopers against (i.e., we 79 # already have a populated planet in the same system that could build base troopers) and we have at least a 80 # minimal amount of PP available, and (2) in the second pass we go through the reserved base trooper target list 81 # and check to make sure that there does not appear to be too much military action still needed before the 82 # target is ready to be invaded, we double check that not too many base troopers would be needed, and if things 83 # look clear then we queue up the base troopers on the Production Queue and keep track of where we are building 84 # them, and how many; we may also disqualify and remove previously qualified targets (in case, for example, 85 # we lost our base trooper source planet since it was first added to list). 86 # 87 # For planning and tracking base troopers under construction, we use a dictionary store in 88 # get_aistate().qualifyingTroopBaseTargets, keyed by the invasion target planet ID. We only store values 89 # for invasion targets that appear likely to be suitable for base trooper use, and store a 2-item list. 90 # The first item in this list is the ID of the planet where we expect to build the base troopers, and the second 91 # entry initially is set to INVALID_ID (-1). The presence of this entry in qualifyingTroopBaseTargets 92 # flags this target as being reserved as a base-trooper invasion target. 93 # In the second pass, if/when we actually start construction, then we modify the record, replacing that second 94 # value with the ID of the planet where the troopers are actually being built. (Right now that is always the 95 # same as the source planet originally identified, but we could consider reevaluating that, or use that second 96 # value to instead record how many base troopers have been queued, so that on later turns we can assess if the 97 # process got delayed & perhaps more troopers need to be queued). 98 secure_ai_fleet_missions = aistate.get_fleet_missions_with_any_mission_types([MissionType.SECURE, 99 MissionType.MILITARY]) 100 101 # Pass 1: identify qualifying base troop invasion targets 102 for pid in invadable_planet_ids: # TODO: reorganize 103 if pid in aistate.qualifyingTroopBaseTargets: 104 continue 105 planet = universe.getPlanet(pid) 106 if not planet: 107 continue 108 sys_id = planet.systemID 109 sys_partial_vis_turn = get_partial_visibility_turn(sys_id) 110 planet_partial_vis_turn = get_partial_visibility_turn(pid) 111 if planet_partial_vis_turn < sys_partial_vis_turn: 112 continue 113 best_base_planet = INVALID_ID 114 best_trooper_count = 0 115 for pid2 in state.get_empire_planets_by_system(sys_id, include_outposts=False): 116 if available_pp.get(pid2, 0) < 2: # TODO: improve troop base PP sufficiency determination 117 break 118 planet2 = universe.getPlanet(pid2) 119 if not planet2 or planet2.speciesName not in ColonisationAI.empire_ship_builders: 120 continue 121 best_base_trooper_here = \ 122 ProductionAI.get_best_ship_info(PriorityType.PRODUCTION_ORBITAL_INVASION, pid2)[1] 123 if not best_base_trooper_here: 124 continue 125 troops_per_ship = best_base_trooper_here.troopCapacity 126 if not troops_per_ship: 127 continue 128 species_troop_grade = get_species_tag_grade(planet2.speciesName, Tags.ATTACKTROOPS) 129 troops_per_ship = CombatRatingsAI.weight_attack_troops(troops_per_ship, species_troop_grade) 130 if troops_per_ship > best_trooper_count: 131 best_base_planet = pid2 132 best_trooper_count = troops_per_ship 133 if best_base_planet != INVALID_ID: 134 aistate.qualifyingTroopBaseTargets.setdefault(pid, [best_base_planet, INVALID_ID]) 135 136 # Pass 2: for each target previously identified for base troopers, check that still qualifies and 137 # check how many base troopers would be needed; if reasonable then queue up the troops and record this in 138 # get_aistate().qualifyingTroopBaseTargets 139 for pid in list(aistate.qualifyingTroopBaseTargets.keys()): 140 planet = universe.getPlanet(pid) 141 if planet and planet.owner == empire_id: 142 del aistate.qualifyingTroopBaseTargets[pid] 143 continue 144 if pid in invasion_targeted_planet_ids: # TODO: consider overriding standard invasion mission 145 continue 146 if aistate.qualifyingTroopBaseTargets[pid][1] != -1: 147 reserved_troop_base_targets.append(pid) 148 if planet: 149 all_invasion_targeted_system_ids.add(planet.systemID) 150 # TODO: evaluate changes to situation, any more troops needed, etc. 151 continue # already building for here 152 _, planet_troops = evaluate_invasion_planet(pid, secure_ai_fleet_missions, True) 153 sys_id = planet.systemID 154 this_sys_status = aistate.systemStatus.get(sys_id, {}) 155 troop_tally = 0 156 for _fid in this_sys_status.get('myfleets', []): 157 troop_tally += FleetUtilsAI.count_troops_in_fleet(_fid) 158 if troop_tally > planet_troops: # base troopers appear unneeded 159 del aistate.qualifyingTroopBaseTargets[pid] 160 continue 161 if (planet.currentMeterValue(fo.meterType.shield) > 0 and 162 (this_sys_status.get('myFleetRating', 0) < 0.8 * this_sys_status.get('totalThreat', 0) or 163 this_sys_status.get('myFleetRatingVsPlanets', 0) < this_sys_status.get('planetThreat', 0))): 164 # this system not secured, so ruling out invasion base troops for now 165 # don't immediately delete from qualifyingTroopBaseTargets or it will be opened up for regular troops 166 continue 167 loc = aistate.qualifyingTroopBaseTargets[pid][0] 168 best_base_trooper_here = ProductionAI.get_best_ship_info(PriorityType.PRODUCTION_ORBITAL_INVASION, loc)[1] 169 loc_planet = universe.getPlanet(loc) 170 if best_base_trooper_here is None: # shouldn't be possible at this point, but just to be safe 171 warning("Could not find a suitable orbital invasion design at %s" % loc_planet) 172 continue 173 # TODO: have TroopShipDesigner give the expected number of troops including species effects directly 174 troops_per_ship = best_base_trooper_here.troopCapacity 175 species_troop_grade = get_species_tag_grade(loc_planet.speciesName, Tags.ATTACKTROOPS) 176 troops_per_ship = CombatRatingsAI.weight_attack_troops(troops_per_ship, species_troop_grade) 177 if not troops_per_ship: 178 warning("The best orbital invasion design at %s seems not to have any troop capacity." % loc_planet) 179 continue 180 _, col_design, build_choices = ProductionAI.get_best_ship_info(PriorityType.PRODUCTION_ORBITAL_INVASION, 181 loc) 182 if not col_design: 183 continue 184 if loc not in build_choices: 185 warning('Best troop design %s can not be produced at planet with id: %s' % (col_design, build_choices)) 186 continue 187 n_bases = math.ceil((planet_troops + 1) / troops_per_ship) # TODO: reconsider this +1 safety factor 188 # TODO: evaluate cost and time-to-build of best base trooper here versus cost and time-to-build-and-travel 189 # for best regular trooper elsewhere 190 # For now, we assume what building base troopers is best so long as either (1) we would need no more than 191 # MAX_BASE_TROOPERS_POOR_INVADERS base troop ships, or (2) our base troopers have more than 1 trooper per 192 # ship and we would need no more than MAX_BASE_TROOPERS_GOOD_INVADERS base troop ships 193 if (n_bases > MAX_BASE_TROOPERS_POOR_INVADERS or 194 (troops_per_ship > 1 and n_bases > MAX_BASE_TROOPERS_GOOD_INVADERS)): 195 debug("ruling out base invasion troopers for %s due to high number (%d) required." % (planet, n_bases)) 196 del aistate.qualifyingTroopBaseTargets[pid] 197 continue 198 debug("Invasion base planning, need %d troops at %d per ship, will build %d ships." % ( 199 (planet_troops + 1), troops_per_ship, n_bases)) 200 retval = fo.issueEnqueueShipProductionOrder(col_design.id, loc) 201 debug("Enqueueing %d Troop Bases at %s for %s" % (n_bases, PlanetUtilsAI.planet_string(loc), 202 PlanetUtilsAI.planet_string(pid))) 203 if retval != 0: 204 all_invasion_targeted_system_ids.add(planet.systemID) 205 reserved_troop_base_targets.append(pid) 206 aistate.qualifyingTroopBaseTargets[pid][1] = loc 207 fo.issueChangeProductionQuantityOrder(empire.productionQueue.size - 1, 1, int(n_bases)) 208 fo.issueRequeueProductionOrder(empire.productionQueue.size - 1, 0) 209 210 invasion_timer.start("evaluating target planets") 211 # TODO: check if any invasion_targeted_planet_ids need more troops assigned 212 evaluated_planet_ids = list( 213 set(invadable_planet_ids) - set(invasion_targeted_planet_ids) - set(reserved_troop_base_targets)) 214 evaluated_planets = assign_invasion_values(evaluated_planet_ids) 215 216 sorted_planets = [(pid, pscore % 10000, ptroops) for pid, (pscore, ptroops) in evaluated_planets.items()] 217 sorted_planets.sort(key=lambda x: x[1], reverse=True) 218 sorted_planets = [(pid, pscore % 10000, ptroops) for pid, pscore, ptroops in sorted_planets] 219 220 invasion_table = Table([Text('Planet'), Float('Score'), Text('Species'), Float('Troops')], 221 table_name="Potential Targets for Invasion Turn %d" % fo.currentTurn()) 222 223 for pid, pscore, ptroops in sorted_planets: 224 planet = universe.getPlanet(pid) 225 invasion_table.add_row([ 226 planet, 227 pscore, 228 planet and planet.speciesName or "unknown", 229 ptroops 230 ]) 231 info(invasion_table) 232 233 sorted_planets = [x for x in sorted_planets if x[1] > 0] 234 # export opponent planets for other AI modules 235 AIstate.opponentPlanetIDs = [pid for pid, __, __ in sorted_planets] 236 AIstate.invasionTargets = sorted_planets 237 238 # export invasion targeted systems for other AI modules 239 AIstate.invasionTargetedSystemIDs = list(all_invasion_targeted_system_ids) 240 invasion_timer.stop(section_name="evaluating %d target planets" % (len(evaluated_planet_ids))) 241 invasion_timer.stop_print_and_clear() 242 243 244def get_invasion_targeted_planet_ids(planet_ids, mission_type): 245 invasion_feet_missions = get_aistate().get_fleet_missions_with_any_mission_types([mission_type]) 246 targeted_planets = [] 247 for pid in planet_ids: 248 # add planets that are target of a mission 249 for mission in invasion_feet_missions: 250 target = TargetPlanet(pid) 251 if mission.has_target(mission_type, target): 252 targeted_planets.append(pid) 253 return targeted_planets 254 255 256def retaliation_risk_factor(empire_id): 257 """A multiplicative adjustment to planet scores to account for risk of retaliation from planet owner.""" 258 # TODO implement (in militaryAI) actual military risk assessment of other empires 259 if empire_id == -1: # unowned 260 return 1.5 # since no risk of retaliation, increase score 261 else: 262 return 1.0 263 264 265def assign_invasion_values(planet_ids): 266 """Creates a dictionary that takes planet_ids as key and their invasion score as value.""" 267 empire_id = fo.empireID() 268 planet_values = {} 269 neighbor_values = {} 270 neighbor_val_ratio = .95 271 universe = fo.getUniverse() 272 secure_missions = get_aistate().get_fleet_missions_with_any_mission_types([MissionType.SECURE, 273 MissionType.MILITARY]) 274 for pid in planet_ids: 275 planet_values[pid] = neighbor_values.setdefault(pid, evaluate_invasion_planet(pid, secure_missions)) 276 debug("planet %d, values %s", pid, planet_values[pid]) 277 planet = universe.getPlanet(pid) 278 species_name = (planet and planet.speciesName) or "" 279 species = fo.getSpecies(species_name) 280 if species and species.canProduceShips: 281 system = universe.getSystem(planet.systemID) 282 if not system: 283 continue 284 planet_industries = {} 285 for pid2 in system.planetIDs: 286 planet2 = universe.getPlanet(pid2) 287 species_name2 = (planet2 and planet2.speciesName) or "" 288 species2 = fo.getSpecies(species_name2) 289 if species2 and species2.canProduceShips: 290 # to prevent divide-by-zero 291 planet_industries[pid2] = planet2.initialMeterValue(fo.meterType.industry) + 0.1 292 industry_ratio = planet_industries[pid] / max(planet_industries.values()) 293 for pid2 in system.planetIDs: 294 if pid2 == pid: 295 continue 296 planet2 = universe.getPlanet(pid2) 297 # TODO check for allies 298 if (planet2 and (planet2.owner != empire_id) and 299 ((planet2.owner != -1) or (planet2.initialMeterValue(fo.meterType.population) > 0))): 300 planet_values[pid][0] += ( 301 industry_ratio * 302 neighbor_val_ratio * 303 (neighbor_values.setdefault(pid2, evaluate_invasion_planet(pid2, secure_missions))[0]) 304 ) 305 return planet_values 306 307 308def evaluate_invasion_planet(planet_id, secure_fleet_missions, verbose=True): 309 """Return the invasion value (score, troops) of a planet.""" 310 universe = fo.getUniverse() 311 empire_id = fo.empireID() 312 detail = [] 313 314 planet = universe.getPlanet(planet_id) 315 if planet is None: 316 debug("Invasion AI couldn't access any info for planet id %d" % planet_id) 317 return [0, 0] 318 319 system_id = planet.systemID 320 321 # by using the following instead of simply relying on stealth meter reading, can (sometimes) plan ahead even if 322 # planet is temporarily shrouded by an ion storm 323 predicted_detectable = EspionageAI.colony_detectable_by_empire(planet_id, empire=fo.empireID(), 324 default_result=False) 325 if not predicted_detectable: 326 if get_partial_visibility_turn(planet_id) < fo.currentTurn(): 327 debug("InvasionAI predicts planet id %d to be stealthed" % planet_id) 328 return [0, 0] 329 else: 330 debug("InvasionAI predicts planet id %d to be stealthed" % planet_id + 331 ", but somehow have current visibity anyway, will still consider as target") 332 333 # Check if the target planet was extra-stealthed somehow its system was last viewed 334 # this test below may augment the tests above, but can be thrown off by temporary combat-related sighting 335 system_last_seen = get_partial_visibility_turn(planet_id) 336 planet_last_seen = get_partial_visibility_turn(system_id) 337 if planet_last_seen < system_last_seen: 338 # TODO: track detection strength, order new scouting when it goes up 339 debug("Invasion AI considering planet id %d (stealthed at last view), still proceeding." % planet_id) 340 341 # get a baseline evaluation of the planet as determined by ColonisationAI 342 species_name = planet.speciesName 343 species = fo.getSpecies(species_name) 344 empire_research_list = tuple(element.tech for element in fo.getEmpire().researchQueue) 345 if not species or AIDependencies.TAG_DESTROYED_ON_CONQUEST in species.tags: 346 # this call iterates over this Empire's available species with which it could colonize after an invasion 347 planet_eval = ColonisationAI.assign_colonisation_values([planet_id], MissionType.INVASION, None, detail) 348 colony_base_value = max(0.75 * planet_eval.get(planet_id, [0])[0], 349 ColonisationAI.evaluate_planet( 350 planet_id, MissionType.OUTPOST, None, detail, empire_research_list)) 351 else: 352 colony_base_value = ColonisationAI.evaluate_planet( 353 planet_id, MissionType.INVASION, species_name, detail, empire_research_list) 354 355 # Add extra score for all buildings on the planet 356 building_values = {"BLD_IMPERIAL_PALACE": 1000, 357 "BLD_CULTURE_ARCHIVES": 1000, 358 "BLD_AUTO_HISTORY_ANALYSER": 100, 359 "BLD_SHIPYARD_BASE": 100, 360 "BLD_SHIPYARD_ORG_ORB_INC": 200, 361 "BLD_SHIPYARD_ORG_XENO_FAC": 200, 362 "BLD_SHIPYARD_ORG_CELL_GRO_CHAMB": 200, 363 "BLD_SHIPYARD_CON_NANOROBO": 300, 364 "BLD_SHIPYARD_CON_GEOINT": 400, 365 "BLD_SHIPYARD_CON_ADV_ENGINE": 1000, 366 "BLD_SHIPYARD_AST": 300, 367 "BLD_SHIPYARD_AST_REF": 1000, 368 "BLD_SHIPYARD_ENRG_SOLAR": 1500, 369 "BLD_INDUSTRY_CENTER": 500, 370 "BLD_GAS_GIANT_GEN": 200, 371 "BLD_SOL_ORB_GEN": 800, 372 "BLD_BLACK_HOLE_POW_GEN": 2000, 373 "BLD_ENCLAVE_VOID": 500, 374 "BLD_NEUTRONIUM_EXTRACTOR": 2000, 375 "BLD_NEUTRONIUM_SYNTH": 2000, 376 "BLD_NEUTRONIUM_FORGE": 1000, 377 "BLD_CONC_CAMP": 100, 378 "BLD_BIOTERROR_PROJECTOR": 1000, 379 "BLD_SHIPYARD_ENRG_COMP": 3000, 380 } 381 bld_tally = 0 382 for bldType in [universe.getBuilding(bldg).buildingTypeName for bldg in planet.buildingIDs]: 383 bval = building_values.get(bldType, 50) 384 bld_tally += bval 385 detail.append("%s: %d" % (bldType, bval)) 386 387 # Add extra score for unlocked techs when we conquer the species 388 tech_tally = 0 389 value_per_pp = 4 390 for unlocked_tech in AIDependencies.SPECIES_TECH_UNLOCKS.get(species_name, []): 391 if not tech_is_complete(unlocked_tech): 392 rp_cost = fo.getTech(unlocked_tech).researchCost(empire_id) 393 tech_value = value_per_pp * rp_cost 394 tech_tally += tech_value 395 detail.append("%s: %d" % (unlocked_tech, tech_value)) 396 397 max_jumps = 8 398 capitol_id = PlanetUtilsAI.get_capital() 399 least_jumps_path = [] 400 clear_path = True 401 if capitol_id: 402 homeworld = universe.getPlanet(capitol_id) 403 if homeworld and homeworld.systemID != INVALID_ID and system_id != INVALID_ID: 404 least_jumps_path = list(universe.leastJumpsPath(homeworld.systemID, system_id, empire_id)) 405 max_jumps = len(least_jumps_path) 406 aistate = get_aistate() 407 system_status = aistate.systemStatus.get(system_id, {}) 408 system_fleet_treat = system_status.get('fleetThreat', 1000) 409 system_monster_threat = system_status.get('monsterThreat', 0) 410 sys_total_threat = system_fleet_treat + system_monster_threat + system_status.get('planetThreat', 0) 411 max_path_threat = system_fleet_treat 412 mil_ship_rating = MilitaryAI.cur_best_mil_ship_rating() 413 for path_sys_id in least_jumps_path: 414 path_leg_status = aistate.systemStatus.get(path_sys_id, {}) 415 path_leg_threat = path_leg_status.get('fleetThreat', 1000) + path_leg_status.get('monsterThreat', 0) 416 if path_leg_threat > 0.5 * mil_ship_rating: 417 clear_path = False 418 if path_leg_threat > max_path_threat: 419 max_path_threat = path_leg_threat 420 421 pop = planet.currentMeterValue(fo.meterType.population) 422 target_pop = planet.currentMeterValue(fo.meterType.targetPopulation) 423 troops = planet.currentMeterValue(fo.meterType.troops) 424 troop_regen = planet.currentMeterValue(fo.meterType.troops) - planet.initialMeterValue(fo.meterType.troops) 425 max_troops = planet.currentMeterValue(fo.meterType.maxTroops) 426 # TODO: refactor troop determination into function for use in mid-mission updates and also consider defender techs 427 max_troops += AIDependencies.TROOPS_PER_POP * (target_pop - pop) 428 429 this_system = universe.getSystem(system_id) 430 secure_targets = [system_id] + list(this_system.planetIDs) 431 system_secured = False 432 for mission in secure_fleet_missions: 433 if system_secured: 434 break 435 secure_fleet_id = mission.fleet.id 436 s_fleet = universe.getFleet(secure_fleet_id) 437 if not s_fleet or s_fleet.systemID != system_id: 438 continue 439 if mission.type in [MissionType.SECURE, MissionType.MILITARY]: 440 target_obj = mission.target.get_object() 441 if target_obj is not None and target_obj.id in secure_targets: 442 system_secured = True 443 break 444 system_secured = system_secured and system_status.get('myFleetRating', 0) 445 446 if verbose: 447 debug("Invasion eval of %s\n" 448 " - maxShields: %.1f\n" 449 " - sysFleetThreat: %.1f\n" 450 " - sysMonsterThreat: %.1f", 451 planet, planet.currentMeterValue(fo.meterType.maxShield), system_fleet_treat, system_monster_threat) 452 enemy_val = 0 453 if planet.owner != -1: # value in taking this away from an enemy 454 enemy_val = 20 * (planet.currentMeterValue(fo.meterType.targetIndustry) + 455 2*planet.currentMeterValue(fo.meterType.targetResearch)) 456 457 # devalue invasions that would require too much military force 458 preferred_max_portion = MilitaryAI.get_preferred_max_military_portion_for_single_battle() 459 total_max_mil_rating = MilitaryAI.get_concentrated_tot_mil_rating() 460 threat_exponent = 2 # TODO: make this a character trait; higher aggression with a lower exponent 461 threat_factor = min(1, preferred_max_portion * total_max_mil_rating/(sys_total_threat+0.001))**threat_exponent 462 463 design_id, _, locs = ProductionAI.get_best_ship_info(PriorityType.PRODUCTION_INVASION) 464 if not locs or not universe.getPlanet(locs[0]): 465 # We are in trouble anyway, so just calculate whatever approximation... 466 build_time = 4 467 planned_troops = troops if system_secured else min(troops + troop_regen*(max_jumps + build_time), max_troops) 468 planned_troops += .01 # we must attack with more troops than there are defenders 469 troop_cost = math.ceil((planned_troops+_TROOPS_SAFETY_MARGIN) / 6.0) * 20 * FleetUtilsAI.get_fleet_upkeep() 470 else: 471 loc = locs[0] 472 species_here = universe.getPlanet(loc).speciesName 473 design = fo.getShipDesign(design_id) 474 cost_per_ship = design.productionCost(empire_id, loc) 475 build_time = design.productionTime(empire_id, loc) 476 troops_per_ship = CombatRatingsAI.weight_attack_troops( 477 design.troopCapacity, get_species_tag_grade(species_here, Tags.ATTACKTROOPS)) 478 planned_troops = troops if system_secured else min(troops + troop_regen*(max_jumps + build_time), max_troops) 479 planned_troops += .01 # we must attack with more troops than there are defenders 480 ships_needed = math.ceil((planned_troops+_TROOPS_SAFETY_MARGIN) / float(troops_per_ship)) 481 troop_cost = ships_needed * cost_per_ship # fleet upkeep is already included in query from server 482 483 # apply some bias to expensive operations 484 normalized_cost = float(troop_cost) / max(fo.getEmpire().productionPoints, 1) 485 normalized_cost = max(1., normalized_cost) 486 cost_score = (normalized_cost**2 / 50.0) * troop_cost 487 488 base_score = colony_base_value + bld_tally + tech_tally + enemy_val - cost_score 489 # If the AI does have enough total military to attack this target, and the target is more than minimally valuable, 490 # don't let the threat_factor discount the adjusted value below MIN_INVASION_SCORE +1, so that if there are no 491 # other targets the AI could still pursue this one. Otherwise, scoring pressure from 492 # MilitaryAI.get_preferred_max_military_portion_for_single_battle might prevent the AI from attacking heavily 493 # defended but still defeatable targets even if it has no softer targets available. 494 if total_max_mil_rating > sys_total_threat and base_score > 2 * MIN_INVASION_SCORE: 495 threat_factor = max(threat_factor, (MIN_INVASION_SCORE + 1)/base_score) 496 planet_score = retaliation_risk_factor(planet.owner) * threat_factor * max(0, base_score) 497 if clear_path: 498 planet_score *= 1.5 499 if verbose: 500 debug(' - planet score: %.2f\n' 501 ' - planned troops: %.2f\n' 502 ' - projected troop cost: %.1f\n' 503 ' - threat factor: %s\n' 504 ' - planet detail: %s\n' 505 ' - popval: %.1f\n' 506 ' - bldval: %s\n' 507 ' - enemyval: %s', 508 planet_score, planned_troops, troop_cost, threat_factor, detail, colony_base_value, bld_tally, enemy_val) 509 debug(' - system secured: %s' % system_secured) 510 return [planet_score, planned_troops] 511 512 513def send_invasion_fleets(fleet_ids, evaluated_planets, mission_type): 514 """sends a list of invasion fleets to a list of planet_value_pairs""" 515 if not fleet_ids: 516 return 517 518 universe = fo.getUniverse() 519 invasion_fleet_pool = set(fleet_ids) 520 521 for planet_id, pscore, ptroops in evaluated_planets: 522 if pscore < MIN_INVASION_SCORE: 523 continue 524 planet = universe.getPlanet(planet_id) 525 if not planet: 526 continue 527 sys_id = planet.systemID 528 found_fleets = [] 529 found_stats = {} 530 min_stats = {'rating': 0, 'troopCapacity': ptroops} 531 target_stats = {'rating': 10, 532 'troopCapacity': ptroops + _TROOPS_SAFETY_MARGIN, 533 'target_system': TargetSystem(sys_id)} 534 these_fleets = FleetUtilsAI.get_fleets_for_mission(target_stats, min_stats, found_stats, 535 starting_system=sys_id, fleet_pool_set=invasion_fleet_pool, 536 fleet_list=found_fleets) 537 if not these_fleets: 538 if not FleetUtilsAI.stats_meet_reqs(found_stats, min_stats): 539 debug("Insufficient invasion troop allocation for system %d ( %s ) -- requested %s , found %s" % ( 540 sys_id, universe.getSystem(sys_id).name, min_stats, found_stats)) 541 invasion_fleet_pool.update(found_fleets) 542 continue 543 else: 544 these_fleets = found_fleets 545 target = TargetPlanet(planet_id) 546 debug("assigning invasion fleets %s to target %s" % (these_fleets, target)) 547 aistate = get_aistate() 548 for fleetID in these_fleets: 549 fleet_mission = aistate.get_fleet_mission(fleetID) 550 fleet_mission.clear_fleet_orders() 551 fleet_mission.clear_target() 552 fleet_mission.set_target(mission_type, target) 553 554 555def assign_invasion_bases(): 556 """Assign our troop bases to invasion targets.""" 557 universe = fo.getUniverse() 558 all_troopbase_fleet_ids = FleetUtilsAI.get_empire_fleet_ids_by_role(MissionType.ORBITAL_INVASION) 559 available_troopbase_fleet_ids = set(FleetUtilsAI.extract_fleet_ids_without_mission_types(all_troopbase_fleet_ids)) 560 561 aistate = get_aistate() 562 for fid in list(available_troopbase_fleet_ids): 563 if fid not in available_troopbase_fleet_ids: # entry may have been discarded in previous loop iterations 564 continue 565 fleet = universe.getFleet(fid) 566 if not fleet: 567 continue 568 sys_id = fleet.systemID 569 system = universe.getSystem(sys_id) 570 available_planets = set(system.planetIDs).intersection(set(aistate.qualifyingTroopBaseTargets.keys())) 571 debug("Considering Base Troopers in %s, found planets %s and registered targets %s with status %s" % ( 572 system.name, list(system.planetIDs), available_planets, 573 [(pid, aistate.qualifyingTroopBaseTargets[pid]) for pid in available_planets])) 574 targets = [pid for pid in available_planets if aistate.qualifyingTroopBaseTargets[pid][1] != -1] 575 if not targets: 576 debug("Failure: found no valid target for troop base in system %s" % system) 577 continue 578 status = aistate.systemStatus.get(sys_id, {}) 579 local_base_troops = set(status.get('myfleets', [])).intersection(available_troopbase_fleet_ids) 580 581 target_id = INVALID_ID 582 best_score = -1 583 target_troops = 0 584 for pid, (p_score, p_troops) in assign_invasion_values(targets).items(): 585 if p_score > best_score: 586 best_score = p_score 587 target_id = pid 588 target_troops = p_troops 589 if target_id == INVALID_ID: 590 continue 591 local_base_troops.discard(fid) 592 found_fleets = [] 593 troops_needed = max(0, target_troops - FleetUtilsAI.count_troops_in_fleet(fid)) 594 found_stats = {} 595 min_stats = {'rating': 0, 'troopCapacity': troops_needed} 596 target_stats = {'rating': 10, 'troopCapacity': troops_needed + _TROOPS_SAFETY_MARGIN} 597 598 FleetUtilsAI.get_fleets_for_mission(target_stats, min_stats, found_stats, 599 starting_system=sys_id, fleet_pool_set=local_base_troops, 600 fleet_list=found_fleets) 601 for fid2 in found_fleets: 602 FleetUtilsAI.merge_fleet_a_into_b(fid2, fid) 603 available_troopbase_fleet_ids.discard(fid2) 604 available_troopbase_fleet_ids.discard(fid) 605 aistate.qualifyingTroopBaseTargets[target_id][1] = -1 # TODO: should probably delete 606 target = TargetPlanet(target_id) 607 fleet_mission = aistate.get_fleet_mission(fid) 608 fleet_mission.set_target(MissionType.ORBITAL_INVASION, target) 609 610 611def assign_invasion_fleets_to_invade(): 612 """Assign fleet targets to invadable planets.""" 613 aistate = get_aistate() 614 615 assign_invasion_bases() 616 617 all_invasion_fleet_ids = FleetUtilsAI.get_empire_fleet_ids_by_role(MissionType.INVASION) 618 invasion_fleet_ids = FleetUtilsAI.extract_fleet_ids_without_mission_types(all_invasion_fleet_ids) 619 send_invasion_fleets(invasion_fleet_ids, AIstate.invasionTargets, MissionType.INVASION) 620 all_invasion_fleet_ids = FleetUtilsAI.get_empire_fleet_ids_by_role(MissionType.INVASION) 621 for fid in FleetUtilsAI.extract_fleet_ids_without_mission_types(all_invasion_fleet_ids): 622 this_mission = aistate.get_fleet_mission(fid) 623 this_mission.check_mergers(context="Post-send consolidation of unassigned troops") 624