1import math 2import random 3from logging import debug, error, info, warning 4from operator import itemgetter 5 6import freeOrionAIInterface as fo # pylint: disable=import-error 7 8import AIDependencies 9import AIstate 10import ColonisationAI 11import CombatRatingsAI 12import FleetUtilsAI 13import MilitaryAI 14import PlanetUtilsAI 15import PriorityAI 16import ShipDesignAI 17from AIDependencies import INVALID_ID 18from EnumsAI import (EmpireProductionTypes, FocusType, MissionType, PriorityType, ShipRoleType, 19 get_priority_production_types, ) 20from aistate_interface import get_aistate 21from character.character_module import Aggression 22from common.print_utils import Sequence, Table, Text 23from freeorion_tools import AITimer, chat_human, ppstring, tech_is_complete 24from turn_state import state 25 26_best_military_design_rating_cache = {} # indexed by turn, values are rating of the military design of the turn 27_design_cost_cache = {0: {(-1, -1): 0}} # outer dict indexed by cur_turn (currently only one turn kept); inner dict indexed by (design_id, pid) 28 29_design_cache = {} # dict of tuples (rating,pid,designID,cost) sorted by rating and indexed by priority type 30 31_CHAT_DEBUG = False 32 33 34def find_best_designs_this_turn(): 35 """Calculate the best designs for each ship class available at this turn.""" 36 design_timer = AITimer('ShipDesigner') 37 design_timer.start('Updating cache for new turn') 38 ShipDesignAI.Cache.update_for_new_turn() 39 _design_cache.clear() 40 41 # TODO Dont use PriorityType but introduce more reasonable Enum 42 designers = [ 43 ('Orbital Invasion', PriorityType.PRODUCTION_ORBITAL_INVASION, ShipDesignAI.OrbitalTroopShipDesigner), 44 ('Invasion', PriorityType.PRODUCTION_INVASION, ShipDesignAI.StandardTroopShipDesigner), 45 ('Orbital Colonization', PriorityType.PRODUCTION_ORBITAL_COLONISATION, ShipDesignAI.OrbitalColonisationShipDesigner), 46 ('Colonization', PriorityType.PRODUCTION_COLONISATION, ShipDesignAI.StandardColonisationShipDesigner), 47 ('Orbital Outposter', PriorityType.PRODUCTION_ORBITAL_OUTPOST, ShipDesignAI.OrbitalOutpostShipDesigner), 48 ('Outposter', PriorityType.PRODUCTION_OUTPOST, ShipDesignAI.StandardOutpostShipDesigner), 49 ('Orbital Defense', PriorityType.PRODUCTION_ORBITAL_DEFENSE, ShipDesignAI.OrbitalDefenseShipDesigner), 50 ('Scouts', PriorityType.PRODUCTION_EXPLORATION, ShipDesignAI.ScoutShipDesigner), 51 ] 52 53 for timer_name, priority_type, designer in designers: 54 design_timer.start(timer_name) 55 _design_cache[priority_type] = designer().optimize_design() 56 best_military_stats = ShipDesignAI.WarShipDesigner().optimize_design() 57 best_carrier_stats = ShipDesignAI.CarrierShipDesigner().optimize_design() 58 best_stats = best_military_stats + best_carrier_stats if random.random() < .8 else best_military_stats 59 best_stats.sort(reverse=True) 60 _design_cache[PriorityType.PRODUCTION_MILITARY] = best_stats 61 design_timer.start('Krill Spawner') 62 ShipDesignAI.KrillSpawnerShipDesigner().optimize_design() # just designing it, building+mission not supported yet 63 if fo.currentTurn() % 10 == 0: 64 design_timer.start('Printing') 65 ShipDesignAI.Cache.print_best_designs() 66 design_timer.stop_print_and_clear() 67 68 69def get_design_cost(design, pid): # TODO: Use new framework 70 """Find and return the design_cost of the specified design on the specified planet. 71 72 :param design: 73 :type design: fo.shipDesign 74 :param pid: planet id 75 :type pid: int 76 :return: cost of the design 77 """ 78 cur_turn = fo.currentTurn() 79 if cur_turn in _design_cost_cache: 80 cost_cache = _design_cost_cache[cur_turn] 81 else: 82 _design_cost_cache.clear() 83 cost_cache = {} 84 _design_cost_cache[cur_turn] = cost_cache 85 loc_invariant = design.costTimeLocationInvariant 86 if loc_invariant: 87 loc = INVALID_ID 88 else: 89 loc = pid 90 return cost_cache.setdefault((design.id, loc), design.productionCost(fo.empireID(), pid)) 91 92 93def cur_best_military_design_rating(): 94 """Find and return the default combat rating of our best military design. 95 96 :return: float: rating of the best military design 97 """ 98 current_turn = fo.currentTurn() 99 if current_turn in _best_military_design_rating_cache: 100 return _best_military_design_rating_cache[current_turn] 101 priority = PriorityType.PRODUCTION_MILITARY 102 if _design_cache.get(priority, None) and _design_cache[priority][0]: 103 # the rating provided by the ShipDesigner does not 104 # reflect the rating used in threat considerations 105 # but takes additional factors (such as cost) into 106 # account. Therefore, we want to calculate the actual 107 # rating of the design as provided by CombatRatingsAI. 108 _, _, _, _, stats = _design_cache[priority][0] 109 # TODO: Should this consider enemy stats? 110 rating = CombatRatingsAI.ShipCombatStats(stats=stats.convert_to_combat_stats()).get_rating() 111 _best_military_design_rating_cache[current_turn] = rating 112 return max(rating, 0.001) 113 return 0.001 114 115 116def get_best_ship_info(priority, loc=None): 117 """ Returns 3 item tuple: designID, design, buildLocList.""" 118 if loc is None: 119 planet_ids = state.get_inhabited_planets() 120 elif isinstance(loc, list): 121 planet_ids = set(loc).intersection(state.get_inhabited_planets()) 122 elif isinstance(loc, int) and loc in state.get_inhabited_planets(): 123 planet_ids = [loc] 124 else: # problem 125 return None, None, None 126 if priority in _design_cache: 127 best_designs = _design_cache[priority] 128 if not best_designs: 129 return None, None, None 130 131 # best_designs are already sorted by rating high to low, so the top rating is the first encountered within 132 # our planet search list 133 for design_stats in best_designs: 134 top_rating, pid, top_id, cost, stats = design_stats 135 if pid in planet_ids: 136 break 137 else: 138 return None, None, None # apparently can't build for this priority within the desired planet group 139 valid_locs = [pid_ for rating, pid_, design_id, _, _ in best_designs if 140 rating == top_rating and design_id == top_id and pid_ in planet_ids] 141 return top_id, fo.getShipDesign(top_id), valid_locs 142 else: 143 return None, None, None # must be missing a Shipyard or other orbital (or missing tech) 144 145 146def get_best_ship_ratings(planet_ids): 147 """ 148 Returns list of [partition, pid, designID, design] sublists, currently only for military ships. 149 150 Since we haven't yet implemented a way to target military ship construction at/near particular locations 151 where they are most in need, and also because our rating system is presumably useful-but-not-perfect, we want to 152 distribute the construction across the Resource Group and across similarly rated designs, preferentially choosing 153 the best rated design/loc combo, but if there are multiple design/loc combos with the same or similar ratings then 154 we want some chance of choosing those alternate designs/locations. 155 156 The approach to this taken below is to treat the ratings akin to an energy to be used in a statistical mechanics 157 type partition function. 'tally' will compute the normalization constant. 158 So first go through and calculate the tally as well as convert each individual contribution to 159 the running total up to that point, to facilitate later sampling. Then those running totals are 160 renormalized by the final tally, so that a later random number selector in the range [0,1) can be 161 used to select the chosen design/loc. 162 163 :param planet_ids: list of planets ids. 164 :type planet_ids: list|set|tuple 165 :rtype: list 166 """ 167 priority = PriorityType.PRODUCTION_MILITARY 168 planet_ids = set(planet_ids).intersection(ColonisationAI.empire_shipyards) 169 170 if priority in _design_cache: 171 build_choices = _design_cache[priority] 172 loc_choices = [[rating, pid, design_id, fo.getShipDesign(design_id)] 173 for (rating, pid, design_id, cost, stats) in build_choices if pid in planet_ids] 174 if not loc_choices: 175 return [] 176 best_rating = loc_choices[0][0] 177 tally = 0 178 ret_val = [] 179 for rating, pid, design_id, design in loc_choices: 180 if rating < 0.7 * best_rating: 181 break 182 p = math.exp(10 * (rating/best_rating - 1)) 183 tally += p 184 ret_val.append([tally, pid, design_id, design]) 185 for item in ret_val: 186 item[0] /= tally 187 return ret_val 188 else: 189 return [] 190 191 192# TODO Move Building names to AIDependencies to avoid typos and for IDE-Support 193def generate_production_orders(): 194 """generate production orders""" 195 # first check ship designs 196 # next check for buildings etc that could be placed on queue regardless of locally available PP 197 # next loop over resource groups, adding buildings & ships 198 _print_production_queue() 199 universe = fo.getUniverse() 200 capital_id = PlanetUtilsAI.get_capital() 201 if capital_id is None or capital_id == INVALID_ID: 202 homeworld = None 203 capital_system_id = None 204 else: 205 homeworld = universe.getPlanet(capital_id) 206 capital_system_id = homeworld.systemID 207 debug("Production Queue Management:") 208 empire = fo.getEmpire() 209 production_queue = empire.productionQueue 210 total_pp = empire.productionPoints 211 # prodResPool = empire.getResourcePool(fo.resourceType.industry) 212 # available_pp = dict_from_map(production_queue.available_pp(prodResPool)) 213 # allocated_pp = dict_from_map(production_queue.allocated_pp) 214 # objectsWithWastedPP = production_queue.objectsWithWastedPP(prodResPool) 215 current_turn = fo.currentTurn() 216 debug('') 217 debug(" Total Available Production Points: %s" % total_pp) 218 219 aistate = get_aistate() 220 claimed_stars = aistate.misc.get('claimedStars', {}) 221 if claimed_stars == {}: 222 for sType in AIstate.empireStars: 223 claimed_stars[sType] = list(AIstate.empireStars[sType]) 224 for sys_id in set(AIstate.colonyTargetedSystemIDs + AIstate.outpostTargetedSystemIDs): 225 t_sys = universe.getSystem(sys_id) 226 if not t_sys: 227 continue 228 claimed_stars.setdefault(t_sys.starType, []).append(sys_id) 229 230 if current_turn == 1 and len(AIstate.opponentPlanetIDs) == 0 and len(production_queue) == 0: 231 init_build_nums = [(PriorityType.PRODUCTION_EXPLORATION, 2)] 232 if list(ColonisationAI.empire_colonizers) == ["SP_SLY"]: 233 init_build_nums.append((PriorityType.PRODUCTION_COLONISATION, 1)) 234 else: 235 init_build_nums.append((PriorityType.PRODUCTION_OUTPOST, 1)) 236 for ship_type, num_ships in init_build_nums: 237 best_design_id, _, build_choices = get_best_ship_info(ship_type) 238 if best_design_id is not None: 239 for _ in range(num_ships): 240 fo.issueEnqueueShipProductionOrder(best_design_id, build_choices[0]) 241 fo.updateProductionQueue() 242 243 building_expense = 0.0 244 building_ratio = aistate.character.preferred_building_ratio([0.4, 0.35, 0.30]) 245 debug("Buildings present on all owned planets:") 246 for pid in state.get_all_empire_planets(): 247 planet = universe.getPlanet(pid) 248 if planet: 249 debug("%30s: %s" % (planet.name, [universe.getBuilding(bldg).name for bldg in planet.buildingIDs])) 250 debug('') 251 252 if not homeworld: 253 debug("if no capital, no place to build, should get around to capturing or colonizing a new one") # TODO 254 else: 255 debug("Empire priority_id %d has current Capital %s:" % (empire.empireID, homeworld.name)) 256 table = Table([ 257 Text('Id', description='Building id'), 258 Text('Name'), 259 Text('Type'), 260 Sequence('Tags'), 261 Sequence('Specials'), 262 Text('Owner Id'), 263 ], table_name='Buildings present at empire Capital in Turn %d' % fo.currentTurn()) 264 265 for building_id in homeworld.buildingIDs: 266 building = universe.getBuilding(building_id) 267 268 table.add_row(( 269 building_id, 270 building.name, 271 "_".join(building.buildingTypeName.split("_")[-2:]), 272 sorted(building.tags), 273 sorted(building.specials), 274 building.owner 275 )) 276 277 info(table) 278 capital_buildings = [universe.getBuilding(bldg).buildingTypeName for bldg in homeworld.buildingIDs] 279 280 possible_building_type_ids = [] 281 for type_id in empire.availableBuildingTypes: 282 try: 283 if fo.getBuildingType(type_id).canBeProduced(empire.empireID, homeworld.id): 284 possible_building_type_ids.append(type_id) 285 except: # noqa: E722 286 if fo.getBuildingType(type_id) is None: 287 debug("For empire %d, 'available Building Type priority_id' %s returns None from fo.getBuildingType(type_id)" % (empire.empireID, type_id)) 288 else: 289 debug("For empire %d, problem getting BuildingTypeID for 'available Building Type priority_id' %s" % (empire.empireID, type_id)) 290 if possible_building_type_ids: 291 debug("Possible building types to build:") 292 for type_id in possible_building_type_ids: 293 building_type = fo.getBuildingType(type_id) 294 debug(" %s cost: %s time: %s" % (building_type.name, 295 building_type.productionCost(empire.empireID, homeworld.id), 296 building_type.productionTime(empire.empireID, homeworld.id))) 297 298 possible_building_types = [fo.getBuildingType(type_id) and fo.getBuildingType(type_id).name for type_id in possible_building_type_ids] # makes sure is not None before getting name 299 300 debug('') 301 debug("Buildings already in Production Queue:") 302 capital_queued_buildings = [] 303 for element in [e for e in production_queue if (e.buildType == EmpireProductionTypes.BT_BUILDING)]: 304 building_expense += element.allocation 305 if element.locationID == homeworld.id: 306 capital_queued_buildings.append(element) 307 for bldg in capital_queued_buildings: 308 debug(" %s turns: %s PP: %s" % (bldg.name, bldg.turnsLeft, bldg.allocation)) 309 if not capital_queued_buildings: 310 debug("No capital queued buildings") 311 debug('') 312 queued_building_names = [bldg.name for bldg in capital_queued_buildings] 313 314 if "BLD_AUTO_HISTORY_ANALYSER" in possible_building_types: 315 for pid in find_automatic_historic_analyzer_candidates(): 316 res = fo.issueEnqueueBuildingProductionOrder("BLD_AUTO_HISTORY_ANALYSER", pid) 317 debug("Enqueueing BLD_AUTO_HISTORY_ANALYSER at planet %s - result %d" % (universe.getPlanet(pid), res)) 318 if res: 319 cost, time = empire.productionCostAndTime(production_queue[production_queue.size - 1]) 320 building_expense += cost / time 321 res = fo.issueRequeueProductionOrder(production_queue.size - 1, 0) # move to front 322 debug("Requeueing %s to front of build queue, with result %d" % ("BLD_AUTO_HISTORY_ANALYSER", res)) 323 324 # TODO: check existence of BLD_INDUSTRY_CENTER (and other buildings) in other locations in case we captured it 325 if (total_pp > 40 or ((current_turn > 40) and (state.population_with_industry_focus() >= 20))) and ("BLD_INDUSTRY_CENTER" in possible_building_types) and ("BLD_INDUSTRY_CENTER" not in (capital_buildings+queued_building_names)) and (building_expense < building_ratio*total_pp): 326 res = fo.issueEnqueueBuildingProductionOrder("BLD_INDUSTRY_CENTER", homeworld.id) 327 debug("Enqueueing BLD_INDUSTRY_CENTER, with result %d" % res) 328 if res: 329 cost, time = empire.productionCostAndTime(production_queue[production_queue.size - 1]) 330 building_expense += cost / time 331 332 if ("BLD_SHIPYARD_BASE" in possible_building_types) and ("BLD_SHIPYARD_BASE" not in (capital_buildings + queued_building_names)): 333 try: 334 res = fo.issueEnqueueBuildingProductionOrder("BLD_SHIPYARD_BASE", homeworld.id) 335 debug("Enqueueing BLD_SHIPYARD_BASE, with result %d" % res) 336 except: # noqa: E722 337 warning("Can't build shipyard at new capital, probably no population; we're hosed") 338 339 for building_name in ["BLD_SHIPYARD_ORG_ORB_INC"]: 340 if (building_name in possible_building_types) and (building_name not in (capital_buildings + queued_building_names)) and (building_expense < building_ratio * total_pp): 341 try: 342 res = fo.issueEnqueueBuildingProductionOrder(building_name, homeworld.id) 343 debug("Enqueueing %s at capital, with result %d" % (building_name, res)) 344 if res: 345 cost, time = empire.productionCostAndTime(production_queue[production_queue.size - 1]) 346 building_expense += cost / time 347 res = fo.issueRequeueProductionOrder(production_queue.size - 1, 0) # move to front 348 debug("Requeueing %s to front of build queue, with result %d" % (building_name, res)) 349 except: # noqa: E722 350 error("Exception triggered and caught: ", exc_info=True) 351 352 if ("BLD_IMPERIAL_PALACE" in possible_building_types) and ("BLD_IMPERIAL_PALACE" not in (capital_buildings + queued_building_names)): 353 res = fo.issueEnqueueBuildingProductionOrder("BLD_IMPERIAL_PALACE", homeworld.id) 354 debug("Enqueueing BLD_IMPERIAL_PALACE at %s, with result %d" % (homeworld.name, res)) 355 if res: 356 res = fo.issueRequeueProductionOrder(production_queue.size - 1, 0) # move to front 357 debug("Requeueing BLD_IMPERIAL_PALACE to front of build queue, with result %d" % res) 358 359 # ok, BLD_NEUTRONIUM_SYNTH is not currently unlockable, but just in case... ;-p 360 if ("BLD_NEUTRONIUM_SYNTH" in possible_building_types) and ("BLD_NEUTRONIUM_SYNTH" not in (capital_buildings + queued_building_names)): 361 res = fo.issueEnqueueBuildingProductionOrder("BLD_NEUTRONIUM_SYNTH", homeworld.id) 362 debug("Enqueueing BLD_NEUTRONIUM_SYNTH, with result %d" % res) 363 if res: 364 res = fo.issueRequeueProductionOrder(production_queue.size - 1, 0) # move to front 365 debug("Requeueing BLD_NEUTRONIUM_SYNTH to front of build queue, with result %d" % res) 366 367 # TODO: add total_pp checks below, so don't overload queue 368 best_pilot_facilities = ColonisationAI.facilities_by_species_grade.get( 369 "WEAPONS_%.1f" % state.best_pilot_rating, {}) 370 371 debug("best_pilot_facilities: \n %s" % best_pilot_facilities) 372 373 max_defense_portion = aistate.character.max_defense_portion() 374 if aistate.character.check_orbital_production(): 375 sys_orbital_defenses = {} 376 queued_defenses = {} 377 defense_allocation = 0.0 378 target_orbitals = aistate.character.target_number_of_orbitals() 379 debug("Orbital Defense Check -- target Defense Orbitals: %s" % target_orbitals) 380 for element in production_queue: 381 if (element.buildType == EmpireProductionTypes.BT_SHIP) and ( 382 aistate.get_ship_role(element.designID) == ShipRoleType.BASE_DEFENSE): 383 planet = universe.getPlanet(element.locationID) 384 if not planet: 385 error("Problem getting Planet for build loc %s" % element.locationID) 386 continue 387 sys_id = planet.systemID 388 queued_defenses[sys_id] = queued_defenses.get(sys_id, 0) + element.blocksize*element.remaining 389 defense_allocation += element.allocation 390 debug("Queued Defenses: %s", ppstring([(str(universe.getSystem(sid)), num) 391 for sid, num in queued_defenses.items()])) 392 for sys_id, pids in state.get_empire_planets_by_system(include_outposts=False).items(): 393 if aistate.systemStatus.get(sys_id, {}).get('fleetThreat', 1) > 0: 394 continue # don't build orbital shields if enemy fleet present 395 if defense_allocation > max_defense_portion * total_pp: 396 break 397 sys_orbital_defenses[sys_id] = 0 398 fleets_here = aistate.systemStatus.get(sys_id, {}).get('myfleets', []) 399 for fid in fleets_here: 400 if aistate.get_fleet_role(fid) == MissionType.ORBITAL_DEFENSE: 401 debug("Found %d existing Orbital Defenses in %s :" % ( 402 aistate.fleetStatus.get(fid, {}).get('nships', 0), universe.getSystem(sys_id))) 403 sys_orbital_defenses[sys_id] += aistate.fleetStatus.get(fid, {}).get('nships', 0) 404 for pid in pids: 405 sys_orbital_defenses[sys_id] += queued_defenses.get(pid, 0) 406 if sys_orbital_defenses[sys_id] < target_orbitals: 407 num_needed = target_orbitals - sys_orbital_defenses[sys_id] 408 for pid in pids: 409 best_design_id, col_design, build_choices = get_best_ship_info(PriorityType.PRODUCTION_ORBITAL_DEFENSE, pid) 410 if not best_design_id: 411 debug("no orbital defenses can be built at %s", PlanetUtilsAI.planet_string(pid)) 412 continue 413 retval = fo.issueEnqueueShipProductionOrder(best_design_id, pid) 414 debug("queueing %d Orbital Defenses at %s" % (num_needed, PlanetUtilsAI.planet_string(pid))) 415 if retval != 0: 416 if num_needed > 1: 417 fo.issueChangeProductionQuantityOrder(production_queue.size - 1, 1, num_needed) 418 cost, time = empire.productionCostAndTime(production_queue[production_queue.size - 1]) 419 defense_allocation += production_queue[production_queue.size - 1].blocksize * cost/time 420 fo.issueRequeueProductionOrder(production_queue.size - 1, 0) # move to front 421 break 422 423 building_type = fo.getBuildingType("BLD_SHIPYARD_BASE") 424 queued_shipyard_locs = [element.locationID for element in production_queue if (element.name == "BLD_SHIPYARD_BASE")] 425 system_colonies = {} 426 colony_systems = {} 427 empire_species = state.get_empire_planets_by_species() 428 systems_with_species = state.get_empire_planets_by_system(include_outposts=False).keys() 429 for spec_name in ColonisationAI.empire_colonizers: 430 if (len(ColonisationAI.empire_colonizers[spec_name]) == 0) and (spec_name in empire_species): # not enough current shipyards for this species#TODO: also allow orbital incubators and/or asteroid ships 431 for pid in state.get_empire_planets_with_species(spec_name): # SP_EXOBOT may not actually have a colony yet but be in empireColonizers 432 if pid in queued_shipyard_locs: 433 break # won't try building more than one shipyard at once, per colonizer 434 else: # no queued shipyards, get planets with target pop >=3, and queue a shipyard on the one with biggest current pop 435 planets = (universe.getPlanet(x) for x in state.get_empire_planets_with_species(spec_name)) 436 pops = sorted( 437 (planet_.initialMeterValue(fo.meterType.population), planet_.id) for planet_ in planets if 438 (planet_ and planet_.initialMeterValue(fo.meterType.targetPopulation) >= 3.0) 439 ) 440 pids = [pid for pop, pid in pops if building_type.canBeProduced(empire.empireID, pid)] 441 if pids: 442 build_loc = pids[-1] 443 res = fo.issueEnqueueBuildingProductionOrder("BLD_SHIPYARD_BASE", build_loc) 444 debug("Enqueueing BLD_SHIPYARD_BASE at planet %d (%s) for colonizer species %s, with result %d" % (build_loc, universe.getPlanet(build_loc).name, spec_name, res)) 445 if res: 446 queued_shipyard_locs.append(build_loc) 447 break # only start at most one new shipyard per species per turn 448 for pid in state.get_empire_planets_with_species(spec_name): 449 planet = universe.getPlanet(pid) 450 if planet: 451 system_colonies.setdefault(planet.systemID, {}).setdefault('pids', []).append(pid) 452 colony_systems[pid] = planet.systemID 453 454 acirema_systems = {} 455 for pid in state.get_empire_planets_with_species("SP_ACIREMA"): 456 acirema_systems.setdefault(universe.getPlanet(pid).systemID, []).append(pid) 457 if (pid in queued_shipyard_locs) or not building_type.canBeProduced(empire.empireID, pid): 458 continue # but not 'break' because we want to build shipyards at *every* Acirema planet 459 res = fo.issueEnqueueBuildingProductionOrder("BLD_SHIPYARD_BASE", pid) 460 debug("Enqueueing BLD_SHIPYARD_BASE at planet %d (%s) for Acirema, with result %d" % (pid, universe.getPlanet(pid).name, res)) 461 if res: 462 queued_shipyard_locs.append(pid) 463 res = fo.issueRequeueProductionOrder(production_queue.size - 1, 0) # move to front 464 debug("Requeueing Acirema BLD_SHIPYARD_BASE to front of build queue, with result %d" % res) 465 466 top_pilot_systems = {} 467 for pid, _ in ColonisationAI.pilot_ratings.items(): 468 if (_ <= state.medium_pilot_rating) and (_ < ColonisationAI.GREAT_PILOT_RATING): 469 continue 470 top_pilot_systems.setdefault(universe.getPlanet(pid).systemID, []).append((pid, _)) 471 if (pid in queued_shipyard_locs) or not building_type.canBeProduced(empire.empireID, pid): 472 continue # but not 'break' because we want to build shipyards all top pilot planets 473 res = fo.issueEnqueueBuildingProductionOrder("BLD_SHIPYARD_BASE", pid) 474 debug("Enqueueing BLD_SHIPYARD_BASE at planet %d (%s) for top pilot, with result %d" % (pid, universe.getPlanet(pid).name, res)) 475 if res: 476 queued_shipyard_locs.append(pid) 477 res = fo.issueRequeueProductionOrder(production_queue.size - 1, 0) # move to front 478 debug("Requeueing BLD_SHIPYARD_BASE to front of build queue, with result %d" % res) 479 480 pop_ctrs = list(state.get_inhabited_planets()) 481 red_popctrs = sorted([(ColonisationAI.pilot_ratings.get(pid, 0), pid) for pid in pop_ctrs 482 if colony_systems.get(pid, INVALID_ID) in AIstate.empireStars.get(fo.starType.red, [])], 483 reverse=True) 484 red_pilots = [pid for _, pid in red_popctrs if _ == state.best_pilot_rating] 485 blue_popctrs = sorted([(ColonisationAI.pilot_ratings.get(pid, 0), pid) for pid in pop_ctrs 486 if colony_systems.get(pid, INVALID_ID) in AIstate.empireStars.get(fo.starType.blue, [])], 487 reverse=True) 488 blue_pilots = [pid for _, pid in blue_popctrs if _ == state.best_pilot_rating] 489 bh_popctrs = sorted([(ColonisationAI.pilot_ratings.get(pid, 0), pid) for pid in pop_ctrs 490 if colony_systems.get(pid, INVALID_ID) in AIstate.empireStars.get(fo.starType.blackHole, [])], 491 reverse=True) 492 bh_pilots = [pid for _, pid in bh_popctrs if _ == state.best_pilot_rating] 493 enrgy_shipyard_locs = {} 494 for building_name in ["BLD_SHIPYARD_ENRG_COMP"]: 495 if empire.buildingTypeAvailable(building_name): 496 queued_building_locs = [element.locationID for element in production_queue if (element.name == building_name)] 497 building_type = fo.getBuildingType(building_name) 498 for pid in bh_pilots + blue_pilots: 499 if len(queued_building_locs) > 1: # build a max of 2 at once 500 break 501 this_planet = universe.getPlanet(pid) 502 if not (this_planet and this_planet.speciesName in ColonisationAI.empire_ship_builders): # TODO: also check that not already one for this spec in this sys 503 continue 504 enrgy_shipyard_locs.setdefault(this_planet.systemID, []).append(pid) 505 if pid not in queued_building_locs and building_type.canBeProduced(empire.empireID, pid): 506 res = fo.issueEnqueueBuildingProductionOrder(building_name, pid) 507 debug("Enqueueing %s at planet %s, with result %d" % (building_name, universe.getPlanet(pid).name, res)) 508 if _CHAT_DEBUG: 509 chat_human("Enqueueing %s at planet %s, with result %d" % (building_name, universe.getPlanet(pid), res)) 510 if res: 511 queued_building_locs.append(pid) 512 cost, time = empire.productionCostAndTime(production_queue[production_queue.size - 1]) 513 building_expense += cost / time # production_queue[production_queue.size -1].blocksize * 514 res = fo.issueRequeueProductionOrder(production_queue.size - 1, 0) # move to front 515 debug("Requeueing %s to front of build queue, with result %d", building_name, res) 516 517 bld_name = "BLD_SHIPYARD_ENRG_SOLAR" 518 queued_bld_locs = [element.locationID for element in production_queue if (element.name == bld_name)] 519 if empire.buildingTypeAvailable(bld_name) and not queued_bld_locs: 520 # TODO: check that production is not frozen at a queued location 521 bld_type = fo.getBuildingType(bld_name) 522 for pid in bh_pilots: 523 this_planet = universe.getPlanet(pid) 524 if not (this_planet and this_planet.speciesName in ColonisationAI.empire_ship_builders): # TODO: also check that not already one for this spec in this sys 525 continue 526 if bld_type.canBeProduced(empire.empireID, pid): 527 res = fo.issueEnqueueBuildingProductionOrder(bld_name, pid) 528 debug("Enqueueing %s at planet %s, with result %d", bld_name, universe.getPlanet(pid), res) 529 if _CHAT_DEBUG: 530 chat_human("Enqueueing %s at planet %s, with result %d" % (bld_name, universe.getPlanet(pid), res)) 531 if res: 532 cost, time = empire.productionCostAndTime(production_queue[production_queue.size - 1]) 533 building_expense += cost / time # production_queue[production_queue.size -1].blocksize * 534 res = fo.issueRequeueProductionOrder(production_queue.size - 1, 0) # move to front 535 debug("Requeueing %s to front of build queue, with result %d", bld_name, res) 536 break 537 538 building_name = "BLD_SHIPYARD_BASE" 539 if empire.buildingTypeAvailable(building_name) and (building_expense < building_ratio * total_pp) and (total_pp > 50 or current_turn > 80): 540 building_type = fo.getBuildingType(building_name) 541 for sys_id in enrgy_shipyard_locs: # Todo ensure only one or 2 per sys 542 for pid in enrgy_shipyard_locs[sys_id][:2]: 543 if pid not in queued_shipyard_locs and building_type.canBeProduced(empire.empireID, pid): # TODO: verify that canBeProduced() checks for prexistence of a barring building 544 res = fo.issueEnqueueBuildingProductionOrder(building_name, pid) 545 debug("Enqueueing %s at planet %d (%s) , with result %d", building_name, pid, universe.getPlanet(pid).name, res) 546 if res: 547 queued_shipyard_locs.append(pid) 548 cost, time = empire.productionCostAndTime(production_queue[production_queue.size - 1]) 549 building_expense += cost / time # production_queue[production_queue.size -1].blocksize * 550 res = fo.issueRequeueProductionOrder(production_queue.size - 1, 0) # move to front 551 debug("Requeueing %s to front of build queue, with result %d", building_name, res) 552 break # only start one per turn 553 554 for bld_name in ["BLD_SHIPYARD_ORG_ORB_INC"]: 555 build_ship_facilities(bld_name, best_pilot_facilities) 556 557 # gating by life cycle manipulation helps delay these until they are closer to being worthwhile 558 if tech_is_complete(AIDependencies.GRO_LIFE_CYCLE) or empire.researchProgress(AIDependencies.GRO_LIFE_CYCLE) > 0: 559 for bld_name in ["BLD_SHIPYARD_ORG_XENO_FAC", "BLD_SHIPYARD_ORG_CELL_GRO_CHAMB"]: 560 build_ship_facilities(bld_name, best_pilot_facilities) 561 562 shipyard_type = fo.getBuildingType("BLD_SHIPYARD_BASE") 563 building_name = "BLD_SHIPYARD_AST" 564 if empire.buildingTypeAvailable(building_name) and aistate.character.may_build_building(building_name): 565 queued_building_locs = [element.locationID for element in production_queue if (element.name == building_name)] 566 if not queued_building_locs: 567 building_type = fo.getBuildingType(building_name) 568 asteroid_systems = {} 569 asteroid_yards = {} 570 shipyard_systems = {} 571 builder_systems = {} 572 for pid in state.get_all_empire_planets(): 573 planet = universe.getPlanet(pid) 574 this_spec = planet.speciesName 575 sys_id = planet.systemID 576 if planet.size == fo.planetSize.asteroids and sys_id in systems_with_species: 577 asteroid_systems.setdefault(sys_id, []).append(pid) 578 if (pid in queued_building_locs) or (building_name in [universe.getBuilding(bldg).buildingTypeName for bldg in planet.buildingIDs]): 579 asteroid_yards[sys_id] = pid # shouldn't ever overwrite another, but ok if it did 580 if this_spec in ColonisationAI.empire_ship_builders: 581 if pid in ColonisationAI.empire_ship_builders[this_spec]: 582 shipyard_systems.setdefault(sys_id, []).append(pid) 583 else: 584 builder_systems.setdefault(sys_id, []).append((planet.speciesName, pid)) 585 # check if we need to build another asteroid processor: 586 # check if local shipyard to go with the asteroid processor 587 yard_locs = [] 588 need_yard = {} 589 top_pilot_locs = [] 590 for sys_id in set(asteroid_systems.keys()).difference(asteroid_yards.keys()): 591 if sys_id in top_pilot_systems: 592 for pid, _ in top_pilot_systems[sys_id]: 593 if pid not in queued_shipyard_locs: # will catch it later if shipyard already present 594 top_pilot_locs.append((_, pid, sys_id)) 595 top_pilot_locs.sort(reverse=True) 596 for _, _, sys_id in top_pilot_locs: 597 if sys_id not in yard_locs: 598 yard_locs.append(sys_id) # prioritize asteroid yards for acirema and/or other top pilots 599 for pid, _ in top_pilot_systems[sys_id]: 600 if pid not in queued_shipyard_locs: # will catch it later if shipyard already present 601 need_yard[sys_id] = pid 602 if (not yard_locs) and len(asteroid_yards.values()) <= int(current_turn // 50): # not yet building & not enough current locs, find a location to build one 603 colonizer_loc_choices = [] 604 builder_loc_choices = [] 605 bld_systems = set(asteroid_systems.keys()).difference(asteroid_yards.keys()) 606 for sys_id in bld_systems.intersection(builder_systems.keys()): 607 for this_spec, pid in builder_systems[sys_id]: 608 if this_spec in ColonisationAI.empire_colonizers: 609 if pid in (ColonisationAI.empire_colonizers[this_spec] + queued_shipyard_locs): 610 colonizer_loc_choices.insert(0, sys_id) 611 else: 612 colonizer_loc_choices.append(sys_id) 613 need_yard[sys_id] = pid 614 else: 615 if pid in (ColonisationAI.empire_ship_builders.get(this_spec, []) + queued_shipyard_locs): 616 builder_loc_choices.insert(0, sys_id) 617 else: 618 builder_loc_choices.append(sys_id) 619 need_yard[sys_id] = pid 620 yard_locs.extend((colonizer_loc_choices+builder_loc_choices)[:1]) # add at most one of these non top pilot locs 621 new_yard_count = len(queued_building_locs) 622 for sys_id in yard_locs: # build at most 2 new asteroid yards at a time 623 if new_yard_count >= 2: 624 break 625 pid = asteroid_systems[sys_id][0] 626 if sys_id in need_yard: 627 pid2 = need_yard[sys_id] 628 if shipyard_type.canBeProduced(empire.empireID, pid2): 629 res = fo.issueEnqueueBuildingProductionOrder("BLD_SHIPYARD_BASE", pid2) 630 debug("Enqueueing %s at planet %d (%s) to go with Asteroid Processor , with result %d", "BLD_SHIPYARD_BASE", pid2, universe.getPlanet(pid2).name, res) 631 if res: 632 queued_shipyard_locs.append(pid2) 633 cost, time = empire.productionCostAndTime(production_queue[production_queue.size - 1]) 634 building_expense += cost / time # production_queue[production_queue.size -1].blocksize * 635 res = fo.issueRequeueProductionOrder(production_queue.size - 1, 0) # move to front 636 debug("Requeueing %s to front of build queue, with result %d", "BLD_SHIPYARD_BASE", res) 637 if pid not in queued_building_locs and building_type.canBeProduced(empire.empireID, pid): 638 res = fo.issueEnqueueBuildingProductionOrder(building_name, pid) 639 debug("Enqueueing %s at planet %d (%s) , with result %d on turn %d", building_name, pid, universe.getPlanet(pid).name, res, current_turn) 640 if res: 641 new_yard_count += 1 642 queued_building_locs.append(pid) 643 cost, time = empire.productionCostAndTime(production_queue[production_queue.size - 1]) 644 building_expense += cost / time # production_queue[production_queue.size -1].blocksize * 645 res = fo.issueRequeueProductionOrder(production_queue.size - 1, 0) # move to front 646 debug("Requeueing %s to front of build queue, with result %d", building_name, res) 647 648 building_name = "BLD_GAS_GIANT_GEN" 649 max_gggs = 1 650 if empire.buildingTypeAvailable(building_name) and aistate.character.may_build_building(building_name): 651 queued_building_locs = [element.locationID for element in production_queue if (element.name == building_name)] 652 building_type = fo.getBuildingType(building_name) 653 for pid in state.get_all_empire_planets(): # TODO: check to ensure that a resource center exists in system, or GGG would be wasted 654 if pid not in queued_building_locs and building_type.canBeProduced(empire.empireID, pid): # TODO: verify that canBeProduced() checks for preexistence of a barring building 655 planet = universe.getPlanet(pid) 656 if planet.systemID in systems_with_species: 657 gg_list = [] 658 can_use_gg = False 659 system = universe.getSystem(planet.systemID) 660 for opid in system.planetIDs: 661 other_planet = universe.getPlanet(opid) 662 if other_planet.size == fo.planetSize.gasGiant: 663 gg_list.append(opid) 664 if other_planet.owner == empire.empireID and (FocusType.FOCUS_INDUSTRY in list(other_planet.availableFoci) + [other_planet.focus]): 665 can_use_gg = True 666 if pid in sorted(gg_list)[:max_gggs] and can_use_gg: 667 res = fo.issueEnqueueBuildingProductionOrder(building_name, pid) 668 if res: 669 queued_building_locs.append(pid) 670 cost, time = empire.productionCostAndTime(production_queue[production_queue.size - 1]) 671 building_expense += cost / time # production_queue[production_queue.size -1].blocksize * 672 res = fo.issueRequeueProductionOrder(production_queue.size - 1, 0) # move to front 673 debug("Requeueing %s to front of build queue, with result %d", building_name, res) 674 debug("Enqueueing %s at planet %d (%s) , with result %d", building_name, pid, universe.getPlanet(pid).name, res) 675 676 building_name = "BLD_SOL_ORB_GEN" 677 if empire.buildingTypeAvailable(building_name) and aistate.character.may_build_building(building_name): 678 already_got_one = 99 679 for pid in state.get_all_empire_planets(): 680 planet = universe.getPlanet(pid) 681 if planet and building_name in [bld.buildingTypeName for bld in map(universe.getBuilding, planet.buildingIDs)]: 682 system = universe.getSystem(planet.systemID) 683 if system and system.starType < already_got_one: 684 already_got_one = system.starType 685 best_type = fo.starType.white 686 best_locs = AIstate.empireStars.get(fo.starType.blue, []) + AIstate.empireStars.get(fo.starType.white, []) 687 if not best_locs: 688 best_type = fo.starType.orange 689 best_locs = AIstate.empireStars.get(fo.starType.yellow, []) + AIstate.empireStars.get(fo.starType.orange, []) 690 if (not best_locs) or (already_got_one < 99 and already_got_one <= best_type): 691 pass # could consider building at a red star if have a lot of PP but somehow no better stars 692 else: 693 use_new_loc = True 694 queued_building_locs = [element.locationID for element in production_queue if (element.name == building_name)] 695 if queued_building_locs: 696 queued_star_types = {} 697 for loc in queued_building_locs: 698 planet = universe.getPlanet(loc) 699 if not planet: 700 continue 701 system = universe.getSystem(planet.systemID) 702 queued_star_types.setdefault(system.starType, []).append(loc) 703 if queued_star_types: 704 best_queued = sorted(queued_star_types.keys())[0] 705 if best_queued > best_type: # i.e., best_queued is yellow, best_type available is blue or white 706 pass # should probably evaluate cancelling the existing one under construction 707 else: 708 use_new_loc = False 709 if use_new_loc: # (of course, may be only loc, not really new) 710 if not homeworld: 711 use_sys = best_locs[0] # as good as any 712 else: 713 distance_map = {} 714 for sys_id in best_locs: # want to build close to capital for defense 715 if sys_id == INVALID_ID: 716 continue 717 try: 718 distance_map[sys_id] = universe.jumpDistance(homeworld.systemID, sys_id) 719 except: # noqa: E722 720 pass 721 use_sys = ([(-1, INVALID_ID)] + sorted([(dist, sys_id) for sys_id, dist in distance_map.items()]))[:2][-1][-1] # kinda messy, but ensures a value 722 if use_sys != INVALID_ID: 723 try: 724 use_loc = state.get_empire_planets_by_system(use_sys)[0] 725 res = fo.issueEnqueueBuildingProductionOrder(building_name, use_loc) 726 debug("Enqueueing %s at planet %d (%s) , with result %d", building_name, use_loc, universe.getPlanet(use_loc).name, res) 727 if res: 728 cost, time = empire.productionCostAndTime(production_queue[production_queue.size - 1]) 729 building_expense += cost / time # production_queue[production_queue.size -1].blocksize * 730 res = fo.issueRequeueProductionOrder(production_queue.size - 1, 0) # move to front 731 debug("Requeueing %s to front of build queue, with result %d", building_name, res) 732 except: # noqa: E722 733 debug("problem queueing BLD_SOL_ORB_GEN at planet %s of system", use_loc, use_sys) 734 pass 735 736 building_name = "BLD_ART_BLACK_HOLE" 737 if ( 738 empire.buildingTypeAvailable(building_name) and 739 aistate.character.may_build_building(building_name) and 740 len(AIstate.empireStars.get(fo.starType.red, [])) > 0 741 ): 742 already_got_one = False 743 for pid in state.get_all_empire_planets(): 744 planet = universe.getPlanet(pid) 745 if planet and building_name in [bld.buildingTypeName for bld in map(universe.getBuilding, planet.buildingIDs)]: 746 already_got_one = True # has been built, needs one turn to activate 747 queued_building_locs = [element.locationID for element in production_queue if (element.name == building_name)] # TODO: check that queued locs or already built one are at red stars 748 if not bh_pilots and len(queued_building_locs) == 0 and (red_pilots or not already_got_one): 749 use_loc = None 750 nominal_home = homeworld or universe.getPlanet( 751 (red_pilots + state.get_empire_planets_by_system(AIstate.empireStars[fo.starType.red][0]))[0]) 752 distance_map = {} 753 for sys_id in AIstate.empireStars.get(fo.starType.red, []): 754 if sys_id == INVALID_ID: 755 continue 756 try: 757 distance_map[sys_id] = universe.jumpDistance(nominal_home.systemID, sys_id) 758 except: # noqa: E722 759 pass 760 red_sys_list = sorted([(dist, sys_id) for sys_id, dist in distance_map.items()]) 761 for dist, sys_id in red_sys_list: 762 for loc in state.get_empire_planets_by_system(sys_id): 763 planet = universe.getPlanet(loc) 764 if planet and planet.speciesName not in ["", None]: 765 species = fo.getSpecies(planet.speciesName) 766 if species and "PHOTOTROPHIC" in list(species.tags): 767 break 768 else: 769 use_loc = list( 770 set(red_pilots).intersection(state.get_empire_planets_by_system(sys_id)) 771 or state.get_empire_planets_by_system(sys_id) 772 )[0] 773 if use_loc is not None: 774 break 775 if use_loc is not None: 776 planet_used = universe.getPlanet(use_loc) 777 try: 778 res = fo.issueEnqueueBuildingProductionOrder(building_name, use_loc) 779 debug("Enqueueing %s at planet %s , with result %d", building_name, planet_used, res) 780 if res: 781 if _CHAT_DEBUG: 782 chat_human("Enqueueing %s at planet %s , with result %d" % (building_name, planet_used, res)) 783 res = fo.issueRequeueProductionOrder(production_queue.size - 1, 0) # move to front 784 debug("Requeueing %s to front of build queue, with result %d", building_name, res) 785 except: # noqa: E722 786 debug("problem queueing %s at planet %s" % (building_name, planet_used)) 787 788 building_name = "BLD_BLACK_HOLE_POW_GEN" 789 if empire.buildingTypeAvailable(building_name) and aistate.character.may_build_building(building_name): 790 already_got_one = False 791 for pid in state.get_all_empire_planets(): 792 planet = universe.getPlanet(pid) 793 if planet and building_name in [bld.buildingTypeName for bld in map(universe.getBuilding, planet.buildingIDs)]: 794 already_got_one = True 795 queued_building_locs = [element.locationID for element in production_queue if (element.name == building_name)] 796 if (len(AIstate.empireStars.get(fo.starType.blackHole, [])) > 0) and len(queued_building_locs) == 0 and not already_got_one: 797 if not homeworld: 798 use_sys = AIstate.empireStars.get(fo.starType.blackHole, [])[0] 799 else: 800 distance_map = {} 801 for sys_id in AIstate.empireStars.get(fo.starType.blackHole, []): 802 if sys_id == INVALID_ID: 803 continue 804 try: 805 distance_map[sys_id] = universe.jumpDistance(homeworld.systemID, sys_id) 806 except: # noqa: E722 807 pass 808 use_sys = ([(-1, INVALID_ID)] + sorted([(dist, sys_id) for sys_id, dist in distance_map.items()]))[:2][-1][-1] # kinda messy, but ensures a value 809 if use_sys != INVALID_ID: 810 try: 811 use_loc = state.get_empire_planets_by_system(use_sys)[0] 812 res = fo.issueEnqueueBuildingProductionOrder(building_name, use_loc) 813 debug("Enqueueing %s at planet %d (%s) , with result %d", building_name, use_loc, universe.getPlanet(use_loc).name, res) 814 if res: 815 res = fo.issueRequeueProductionOrder(production_queue.size - 1, 0) # move to front 816 debug("Requeueing %s to front of build queue, with result %d" % (building_name, res)) 817 except: # noqa: E722 818 warning("problem queueing BLD_BLACK_HOLE_POW_GEN at planet %s of system %s", use_loc, use_sys) 819 pass 820 821 building_name = "BLD_ENCLAVE_VOID" 822 if empire.buildingTypeAvailable(building_name): 823 already_got_one = False 824 for pid in state.get_all_empire_planets(): 825 planet = universe.getPlanet(pid) 826 if planet and building_name in [bld.buildingTypeName for bld in map(universe.getBuilding, planet.buildingIDs)]: 827 already_got_one = True 828 queued_locs = [element.locationID for element in production_queue if (element.name == building_name)] 829 if len(queued_locs) == 0 and homeworld and not already_got_one: 830 try: 831 res = fo.issueEnqueueBuildingProductionOrder(building_name, capital_id) 832 debug("Enqueueing %s at planet %d (%s) , with result %d", building_name, capital_id, universe.getPlanet(capital_id).name, res) 833 if res: 834 res = fo.issueRequeueProductionOrder(production_queue.size - 1, 0) # move to front 835 debug("Requeueing %s to front of build queue, with result %d", building_name, res) 836 except: # noqa: E722 837 pass 838 839 building_name = "BLD_GENOME_BANK" 840 if empire.buildingTypeAvailable(building_name): 841 already_got_one = False 842 for pid in state.get_all_empire_planets(): 843 planet = universe.getPlanet(pid) 844 if planet and building_name in [bld.buildingTypeName for bld in map(universe.getBuilding, planet.buildingIDs)]: 845 already_got_one = True 846 queued_locs = [element.locationID for element in production_queue if (element.name == building_name)] 847 if len(queued_locs) == 0 and homeworld and not already_got_one: 848 try: 849 res = fo.issueEnqueueBuildingProductionOrder(building_name, capital_id) 850 debug("Enqueueing %s at planet %d (%s) , with result %d", building_name, capital_id, universe.getPlanet(capital_id).name, res) 851 if res: 852 res = fo.issueRequeueProductionOrder(production_queue.size - 1, 0) # move to front 853 debug("Requeueing %s to front of build queue, with result %d", building_name, res) 854 except: # noqa: E722 855 pass 856 857 building_name = "BLD_NEUTRONIUM_EXTRACTOR" 858 already_got_extractor = False 859 if ( 860 empire.buildingTypeAvailable(building_name) and 861 [element.locationID for element in production_queue if (element.name == building_name)] == [] and 862 AIstate.empireStars.get(fo.starType.neutron, []) 863 ): 864 # building_type = fo.getBuildingType(building_name) 865 for pid in state.get_all_empire_planets(): 866 planet = universe.getPlanet(pid) 867 if planet: 868 building_names = [bld.buildingTypeName for bld in map(universe.getBuilding, planet.buildingIDs)] 869 if ( 870 (planet.systemID in AIstate.empireStars.get(fo.starType.neutron, []) and building_name in building_names) or 871 "BLD_NEUTRONIUM_SYNTH" in building_names 872 ): 873 already_got_extractor = True 874 if not already_got_extractor: 875 if not homeworld: 876 use_sys = AIstate.empireStars.get(fo.starType.neutron, [])[0] 877 else: 878 distance_map = {} 879 for sys_id in AIstate.empireStars.get(fo.starType.neutron, []): 880 if sys_id == INVALID_ID: 881 continue 882 try: 883 distance_map[sys_id] = universe.jumpDistance(homeworld.systemID, sys_id) 884 except Exception: 885 warning("Could not get jump distance from %d to %d", homeworld.systemID, sys_id, exc_info=True) 886 debug([INVALID_ID] + sorted([(dist, sys_id) for sys_id, dist in distance_map.items()])) 887 use_sys = ([(-1, INVALID_ID)] + sorted([(dist, sys_id) for sys_id, dist in distance_map.items()]))[:2][-1][-1] # kinda messy, but ensures a value 888 if use_sys != INVALID_ID: 889 try: 890 use_loc = state.get_empire_planets_by_system(use_sys)[0] 891 res = fo.issueEnqueueBuildingProductionOrder(building_name, use_loc) 892 debug("Enqueueing %s at planet %d (%s) , with result %d", building_name, use_loc, universe.getPlanet(use_loc).name, res) 893 if res: 894 res = fo.issueRequeueProductionOrder(production_queue.size - 1, 0) # move to front 895 debug("Requeueing %s to front of build queue, with result %d", building_name, res) 896 except: # noqa: E722 897 warning("problem queueing BLD_NEUTRONIUM_EXTRACTOR at planet %s of system %s" % (use_loc, use_sys)) 898 pass 899 900 bld_name = "BLD_SHIPYARD_CON_GEOINT" 901 build_ship_facilities(bld_name, best_pilot_facilities) 902 903 # with current stats the AI considers Titanic Hull superior to Scattered Asteroid, so don't bother building for now 904 # TODO: uncomment once dynamic assessment of prospective designs is enabled & indicates building is worthwhile 905 bld_name = "BLD_SHIPYARD_AST_REF" 906 build_ship_facilities(bld_name, best_pilot_facilities) 907 908 bld_name = "BLD_NEUTRONIUM_FORGE" 909 priority_facilities = ["BLD_SHIPYARD_ENRG_SOLAR", 910 "BLD_SHIPYARD_CON_GEOINT", 911 "BLD_SHIPYARD_AST_REF", 912 "BLD_SHIPYARD_ENRG_COMP"] 913 # not a problem if locs appear multiple times here 914 # TODO: also cover good troopship locations 915 top_locs = list(loc for facil in priority_facilities for loc in best_pilot_facilities.get(facil, [])) 916 build_ship_facilities(bld_name, best_pilot_facilities, top_locs) 917 918 colony_ship_map = {} 919 for fid in FleetUtilsAI.get_empire_fleet_ids_by_role(MissionType.COLONISATION): 920 fleet = universe.getFleet(fid) 921 if not fleet: 922 continue 923 for shipID in fleet.shipIDs: 924 ship = universe.getShip(shipID) 925 if ship and (aistate.get_ship_role(ship.design.id) == ShipRoleType.CIVILIAN_COLONISATION): 926 colony_ship_map.setdefault(ship.speciesName, []).append(1) 927 928 building_name = "BLD_CONC_CAMP" 929 verbose_camp = False 930 building_type = fo.getBuildingType(building_name) 931 for pid in state.get_inhabited_planets(): 932 planet = universe.getPlanet(pid) 933 if not planet: 934 continue 935 can_build_camp = building_type.canBeProduced(empire.empireID, pid) and empire.buildingTypeAvailable(building_name) 936 t_pop = planet.initialMeterValue(fo.meterType.targetPopulation) 937 c_pop = planet.initialMeterValue(fo.meterType.population) 938 t_ind = planet.currentMeterValue(fo.meterType.targetIndustry) 939 c_ind = planet.currentMeterValue(fo.meterType.industry) 940 pop_disqualified = (c_pop <= 32) or (c_pop < 0.9*t_pop) 941 built_camp = False 942 this_spec = planet.speciesName 943 safety_margin_met = ((this_spec in ColonisationAI.empire_colonizers and (len(state.get_empire_planets_with_species(this_spec)) + len(colony_ship_map.get(this_spec, [])) >= 2)) or (c_pop >= 50)) 944 if pop_disqualified or not safety_margin_met: # check even if not aggressive, etc, just in case acquired planet with a ConcCamp on it 945 if can_build_camp: 946 if pop_disqualified: 947 if verbose_camp: 948 debug("Conc Camp disqualified at %s due to low pop: current %.1f target: %.1f", planet.name, c_pop, t_pop) 949 else: 950 if verbose_camp: 951 debug("Conc Camp disqualified at %s due to safety margin; species %s, colonizing planets %s, with %d colony ships", planet.name, planet.speciesName, state.get_empire_planets_with_species(planet.speciesName), len(colony_ship_map.get(planet.speciesName, []))) 952 for bldg in planet.buildingIDs: 953 if universe.getBuilding(bldg).buildingTypeName == building_name: 954 res = fo.issueScrapOrder(bldg) 955 debug("Tried scrapping %s at planet %s, got result %d", building_name, planet.name, res) 956 elif aistate.character.may_build_building(building_name) and can_build_camp and (t_pop >= 36): 957 if (planet.focus == FocusType.FOCUS_GROWTH) or (AIDependencies.COMPUTRONIUM_SPECIAL in planet.specials) or (pid == capital_id): 958 continue 959 # pass # now that focus setting takes these into account, probably works ok to have conc camp, but let's not push it 960 queued_building_locs = [element.locationID for element in production_queue if (element.name == building_name)] 961 if c_pop < 0.95 * t_pop: 962 if verbose_camp: 963 debug("Conc Camp disqualified at %s due to pop: current %.1f target: %.1f", planet.name, c_pop, t_pop) 964 else: 965 if pid not in queued_building_locs: 966 if planet.focus in [FocusType.FOCUS_INDUSTRY]: 967 if c_ind >= t_ind + c_pop: 968 continue 969 else: 970 old_focus = planet.focus 971 fo.issueChangeFocusOrder(pid, FocusType.FOCUS_INDUSTRY) 972 universe.updateMeterEstimates([pid]) 973 t_ind = planet.currentMeterValue(fo.meterType.targetIndustry) 974 if c_ind >= t_ind + c_pop: 975 fo.issueChangeFocusOrder(pid, old_focus) 976 universe.updateMeterEstimates([pid]) 977 continue 978 res = fo.issueEnqueueBuildingProductionOrder(building_name, pid) 979 built_camp = res 980 debug("Enqueueing %s at planet %d (%s) , with result %d", building_name, pid, universe.getPlanet(pid).name, res) 981 if res: 982 queued_building_locs.append(pid) 983 fo.issueRequeueProductionOrder(production_queue.size - 1, 0) # move to front 984 else: 985 # TODO: enable location condition reporting a la mapwnd BuildDesignatorWnd 986 warning("Enqueing Conc Camp at %s despite building_type.canBeProduced(empire.empireID, pid) reporting %s" % (planet, can_build_camp)) 987 if verbose_camp: 988 debug("conc camp status at %s : checkedCamp: %s, built_camp: %s", planet.name, can_build_camp, built_camp) 989 990 building_name = "BLD_SCANNING_FACILITY" 991 if empire.buildingTypeAvailable(building_name): 992 queued_locs = [element.locationID for element in production_queue if (element.name == building_name)] 993 scanner_locs = {} 994 for pid in state.get_all_empire_planets(): 995 planet = universe.getPlanet(pid) 996 if planet: 997 if (pid in queued_locs) or (building_name in [bld.buildingTypeName for bld in map(universe.getBuilding, planet.buildingIDs)]): 998 scanner_locs[planet.systemID] = True 999 max_scanner_builds = max(1, int(empire.productionPoints / 30)) 1000 for sys_id in state.get_empire_planets_by_system().keys(): 1001 if len(queued_locs) >= max_scanner_builds: 1002 break 1003 if sys_id in scanner_locs: 1004 continue 1005 need_scanner = False 1006 for nSys in universe.getImmediateNeighbors(sys_id, empire.empireID): 1007 if universe.getVisibility(nSys, empire.empireID) < fo.visibility.partial: 1008 need_scanner = True 1009 break 1010 if not need_scanner: 1011 continue 1012 build_locs = [] 1013 for pid in state.get_empire_planets_by_system(sys_id): 1014 planet = universe.getPlanet(pid) 1015 if not planet: 1016 continue 1017 build_locs.append((planet.currentMeterValue(fo.meterType.maxTroops), pid)) 1018 if not build_locs: 1019 continue 1020 for troops, loc in sorted(build_locs): 1021 planet = universe.getPlanet(loc) 1022 res = fo.issueEnqueueBuildingProductionOrder(building_name, loc) 1023 debug("Enqueueing %s at planet %d (%s) , with result %d", building_name, loc, planet.name, res) 1024 if res: 1025 res = fo.issueRequeueProductionOrder(production_queue.size - 1, 0) # move to front 1026 debug("Requeueing %s to front of build queue, with result %d", building_name, res) 1027 queued_locs.append(planet.systemID) 1028 break 1029 1030 building_name = "BLD_SHIPYARD_ORBITAL_DRYDOCK" 1031 if empire.buildingTypeAvailable(building_name): 1032 queued_locs = [element.locationID for element in production_queue if (element.name == building_name)] 1033 queued_sys = set() 1034 for pid in queued_locs: 1035 dd_planet = universe.getPlanet(pid) 1036 if dd_planet: 1037 queued_sys.add(dd_planet.systemID) 1038 cur_drydoc_sys = set(state.get_empire_drydocks().keys()).union(queued_sys) 1039 covered_drydoc_locs = set() 1040 for start_set, dest_set in [(cur_drydoc_sys, covered_drydoc_locs), 1041 (covered_drydoc_locs, covered_drydoc_locs)]: # coverage of neighbors up to 2 jumps away from a drydock 1042 for dd_sys_id in start_set.copy(): 1043 dest_set.add(dd_sys_id) 1044 neighbors = universe.getImmediateNeighbors(dd_sys_id, empire.empireID) 1045 dest_set.update(neighbors) 1046 1047 max_dock_builds = int(0.8 + empire.productionPoints/120.0) 1048 debug("Considering building %s, found current and queued systems %s", 1049 building_name, PlanetUtilsAI.sys_name_ids(cur_drydoc_sys.union(queued_sys))) 1050 for sys_id, pids in state.get_empire_planets_by_system(include_outposts=False).items(): # TODO: sort/prioritize in some fashion 1051 local_top_pilots = dict(top_pilot_systems.get(sys_id, [])) 1052 local_drydocks = state.get_empire_drydocks().get(sys_id, []) 1053 if len(queued_locs) >= max_dock_builds: 1054 debug("Drydock enqueing halted with %d of max %d", len(queued_locs), max_dock_builds) 1055 break 1056 if (sys_id in covered_drydoc_locs) and not local_top_pilots: 1057 continue 1058 else: 1059 pass 1060 for _, pid in sorted([(local_top_pilots.get(pid, 0), pid) for pid in pids], reverse=True): 1061 if pid not in ColonisationAI.empire_shipyards: 1062 continue 1063 if pid in local_drydocks or pid in queued_locs: 1064 break 1065 planet = universe.getPlanet(pid) 1066 res = fo.issueEnqueueBuildingProductionOrder(building_name, pid) 1067 debug("Enqueueing %s at planet %d (%s) , with result %d", building_name, pid, planet.name, res) 1068 if res: 1069 queued_locs.append(planet.systemID) 1070 covered_drydoc_locs.add(planet.systemID) 1071 neighboring_systems = universe.getImmediateNeighbors(planet.systemID, empire.empireID) 1072 covered_drydoc_locs.update(neighboring_systems) 1073 if max_dock_builds >= 2: 1074 res = fo.issueRequeueProductionOrder(production_queue.size - 1, 0) # move to front 1075 debug("Requeueing %s to front of build queue, with result %d", building_name, res) 1076 break 1077 else: 1078 warning("Failed enqueueing %s at %s, with result %d" % (building_name, planet, res)) 1079 1080 building_name = "BLD_XENORESURRECTION_LAB" 1081 queued_xeno_lab_locs = [element.locationID for element in production_queue if element.name == building_name] 1082 for pid in state.get_all_empire_planets(): 1083 if pid in queued_xeno_lab_locs or not empire.canBuild(fo.buildType.building, building_name, pid): 1084 continue 1085 res = fo.issueEnqueueBuildingProductionOrder(building_name, pid) 1086 debug("Enqueueing %s at planet %d (%s) , with result %d", building_name, pid, planet.name, res) 1087 if res: 1088 res = fo.issueRequeueProductionOrder(production_queue.size-1, 2) # move to near front 1089 debug("Requeueing %s to front of build queue, with result %d", building_name, res) 1090 break 1091 else: 1092 warning("Failed enqueueing %s at planet %s, got result %d", building_name, planet, res) 1093 1094 # ignore acquired-under-construction colony buildings for which our empire lacks the species 1095 queued_clny_bld_locs = [element.locationID for element in production_queue 1096 if (element.name.startswith('BLD_COL_') and 1097 empire_has_colony_bld_species(element.name))] 1098 colony_bldg_entries = [entry for entry in aistate.colonisablePlanetIDs.items() if 1099 entry[1][0] > 60 and 1100 entry[0] not in queued_clny_bld_locs and 1101 entry[0] in state.get_empire_outposts() and 1102 not already_has_completed_colony_building(entry[0])] 1103 colony_bldg_entries = colony_bldg_entries[:PriorityAI.allottedColonyTargets + 2] 1104 for entry in colony_bldg_entries: 1105 pid = entry[0] 1106 building_name = "BLD_COL_" + entry[1][1][3:] 1107 planet = universe.getPlanet(pid) 1108 building_type = fo.getBuildingType(building_name) 1109 if not (building_type and building_type.canBeEnqueued(empire.empireID, pid)): 1110 continue 1111 res = fo.issueEnqueueBuildingProductionOrder(building_name, pid) 1112 debug("Enqueueing %s at planet %d (%s) , with result %d", building_name, pid, planet.name, res) 1113 if res: 1114 res = fo.issueRequeueProductionOrder(production_queue.size - 1, 2) # move to near front 1115 debug("Requeueing %s to front of build queue, with result %d", building_name, res) 1116 break 1117 else: 1118 warning("Failed enqueueing %s at planet %s, got result %d" % (building_name, planet, res)) 1119 1120 buildings_to_scrap = ("BLD_EVACUATION", "BLD_GATEWAY_VOID") 1121 for pid in state.get_inhabited_planets(): 1122 planet = universe.getPlanet(pid) 1123 if not planet: 1124 continue 1125 for bldg in planet.buildingIDs: 1126 building_name = universe.getBuilding(bldg).buildingTypeName 1127 if building_name in buildings_to_scrap: 1128 res = fo.issueScrapOrder(bldg) 1129 debug("Tried scrapping %s at planet %s, got result %d", building_name, planet.name, res) 1130 1131 total_pp_spent = fo.getEmpire().productionQueue.totalSpent 1132 debug(" Total Production Points Spent: %s", total_pp_spent) 1133 1134 wasted_pp = max(0, total_pp - total_pp_spent) 1135 debug(" Wasted Production Points: %s", wasted_pp) # TODO: add resource group analysis 1136 avail_pp = total_pp - total_pp_spent - 0.0001 1137 1138 debug('') 1139 if False: 1140 debug("Possible ship designs to build:") 1141 if homeworld: 1142 for ship_design_id in empire.availableShipDesigns: 1143 design = fo.getShipDesign(ship_design_id) 1144 debug(" %s cost: %s time: %s", design.name, design.productionCost(empire.empireID, homeworld.id), 1145 design.productionTime(empire.empireID, homeworld.id)) 1146 debug('') 1147 production_queue = empire.productionQueue 1148 queued_colony_ships = {} 1149 queued_outpost_ships = 0 1150 queued_troop_ships = 0 1151 1152 # TODO: blocked items might not need dequeuing, but rather for supply lines to be un-blockaded 1153 dequeue_list = [] 1154 fo.updateProductionQueue() 1155 can_prioritize_troops = False 1156 for queue_index in range(len(production_queue)): 1157 element = production_queue[queue_index] 1158 block_str = "%d x " % element.blocksize # ["a single ", "in blocks of %d "%element.blocksize][element.blocksize>1] 1159 debug(" %s%s requiring %s more turns; alloc: %.2f PP with cum. progress of %.1f being built at %s", 1160 block_str, element.name, element.turnsLeft, element.allocation, 1161 element.progress, universe.getObject(element.locationID).name) 1162 if element.turnsLeft == -1: 1163 if element.locationID not in state.get_all_empire_planets(): 1164 # dequeue_list.append(queue_index) #TODO add assessment of recapture -- invasion target etc. 1165 debug("element %s will never be completed as stands and location %d no longer owned; could consider deleting from queue", element.name, element.locationID) # TODO: 1166 else: 1167 debug("element %s is projected to never be completed as currently stands, but will remain on queue ", element.name) 1168 elif element.buildType == EmpireProductionTypes.BT_SHIP: 1169 this_role = aistate.get_ship_role(element.designID) 1170 if this_role == ShipRoleType.CIVILIAN_COLONISATION: 1171 this_spec = universe.getPlanet(element.locationID).speciesName 1172 queued_colony_ships[this_spec] = queued_colony_ships.get(this_spec, 0) + element.remaining * element.blocksize 1173 elif this_role == ShipRoleType.CIVILIAN_OUTPOST: 1174 queued_outpost_ships += element.remaining * element.blocksize 1175 elif this_role == ShipRoleType.BASE_OUTPOST: 1176 queued_outpost_ships += element.remaining * element.blocksize 1177 elif this_role == ShipRoleType.MILITARY_INVASION: 1178 queued_troop_ships += element.remaining * element.blocksize 1179 elif (this_role == ShipRoleType.CIVILIAN_EXPLORATION) and (queue_index <= 1): 1180 if len(AIstate.opponentPlanetIDs) > 0: 1181 can_prioritize_troops = True 1182 if queued_colony_ships: 1183 debug("\nFound colony ships in build queue: %s", queued_colony_ships) 1184 if queued_outpost_ships: 1185 debug("\nFound outpost ships and bases in build queue: %s", queued_outpost_ships) 1186 1187 for queue_index in dequeue_list[::-1]: 1188 fo.issueDequeueProductionOrder(queue_index) 1189 1190 all_military_fleet_ids = FleetUtilsAI.get_empire_fleet_ids_by_role(MissionType.MILITARY) 1191 total_military_ships = sum([aistate.fleetStatus.get(fid, {}).get('nships', 0) for fid in all_military_fleet_ids]) 1192 all_troop_fleet_ids = FleetUtilsAI.get_empire_fleet_ids_by_role(MissionType.INVASION) 1193 total_troop_ships = sum([aistate.fleetStatus.get(fid, {}).get('nships', 0) for fid in all_troop_fleet_ids]) 1194 avail_troop_fleet_ids = list(FleetUtilsAI.extract_fleet_ids_without_mission_types(all_troop_fleet_ids)) 1195 total_available_troops = sum([aistate.fleetStatus.get(fid, {}).get('nships', 0) for fid in avail_troop_fleet_ids]) 1196 debug("Trooper Status turn %d: %d total, with %d unassigned. %d queued, compared to %d total Military Attack Ships", 1197 current_turn, total_troop_ships, total_available_troops, queued_troop_ships, total_military_ships) 1198 if ( 1199 capital_id is not None and 1200 (current_turn >= 40 or can_prioritize_troops) and 1201 aistate.systemStatus.get(capital_system_id, {}).get('fleetThreat', 0) == 0 and 1202 aistate.systemStatus.get(capital_system_id, {}).get('neighborThreat', 0) == 0 1203 ): 1204 best_design_id, best_design, build_choices = get_best_ship_info(PriorityType.PRODUCTION_INVASION) 1205 if build_choices is not None and len(build_choices) > 0: 1206 loc = random.choice(build_choices) 1207 prod_time = best_design.productionTime(empire.empireID, loc) 1208 prod_cost = best_design.productionCost(empire.empireID, loc) 1209 troopers_needed = max(0, int(min(0.99 + (current_turn/20.0 - total_available_troops)/max(2, prod_time - 1), total_military_ships//3 - total_troop_ships))) 1210 ship_number = troopers_needed 1211 per_turn_cost = (float(prod_cost) / prod_time) 1212 if troopers_needed > 0 and total_pp > 3*per_turn_cost*queued_troop_ships and aistate.character.may_produce_troops(): 1213 retval = fo.issueEnqueueShipProductionOrder(best_design_id, loc) 1214 if retval != 0: 1215 debug("forcing %d new ship(s) to production queue: %s; per turn production cost %.1f\n", ship_number, best_design.name, ship_number*per_turn_cost) 1216 if ship_number > 1: 1217 fo.issueChangeProductionQuantityOrder(production_queue.size - 1, 1, ship_number) 1218 avail_pp -= ship_number * per_turn_cost 1219 fo.issueRequeueProductionOrder(production_queue.size - 1, 0) # move to front 1220 fo.updateProductionQueue() 1221 debug('') 1222 1223 debug('') 1224 # get the highest production priorities 1225 production_priorities = {} 1226 for priority_type in get_priority_production_types(): 1227 production_priorities[priority_type] = int(max(0, (aistate.get_priority(priority_type)) ** 0.5)) 1228 1229 sorted_priorities = sorted(production_priorities.items(), key=itemgetter(1), reverse=True) 1230 1231 top_score = -1 1232 1233 num_colony_fleets = len(FleetUtilsAI.get_empire_fleet_ids_by_role(MissionType.COLONISATION)) # counting existing colony fleets each as one ship 1234 total_colony_fleets = sum(queued_colony_ships.values()) + num_colony_fleets 1235 num_outpost_fleets = len(FleetUtilsAI.get_empire_fleet_ids_by_role(MissionType.OUTPOST)) # counting existing outpost fleets each as one ship 1236 total_outpost_fleets = queued_outpost_ships + num_outpost_fleets 1237 1238 max_colony_fleets = PriorityAI.allottedColonyTargets 1239 max_outpost_fleets = max_colony_fleets 1240 1241 _, _, colony_build_choices = get_best_ship_info(PriorityType.PRODUCTION_COLONISATION) 1242 military_emergency = PriorityAI.unmetThreat > (2.0 * MilitaryAI.get_tot_mil_rating()) 1243 1244 debug("Production Queue Priorities:") 1245 filtered_priorities = {} 1246 for priority_id, score in sorted_priorities: 1247 if military_emergency: 1248 if priority_id == PriorityType.PRODUCTION_EXPLORATION: 1249 score /= 10.0 1250 elif priority_id != PriorityType.PRODUCTION_MILITARY: 1251 score /= 2.0 1252 if top_score < score: 1253 top_score = score # don't really need top_score nor sorting with current handling 1254 debug(" Score: %4d -- %s ", score, priority_id) 1255 if priority_id != PriorityType.PRODUCTION_BUILDINGS: 1256 if (priority_id == PriorityType.PRODUCTION_COLONISATION) and (total_colony_fleets < max_colony_fleets) and (colony_build_choices is not None) and len(colony_build_choices) > 0: 1257 filtered_priorities[priority_id] = score 1258 elif (priority_id == PriorityType.PRODUCTION_OUTPOST) and (total_outpost_fleets < max_outpost_fleets): 1259 filtered_priorities[priority_id] = score 1260 elif priority_id not in [PriorityType.PRODUCTION_OUTPOST, PriorityType.PRODUCTION_COLONISATION]: 1261 filtered_priorities[priority_id] = score 1262 if filtered_priorities == {}: 1263 debug("No non-building-production priorities with nonzero score, setting to default: Military") 1264 filtered_priorities[PriorityType.PRODUCTION_MILITARY] = 1 1265 if top_score <= 100: 1266 scaling_power = 1.0 1267 else: 1268 scaling_power = math.log(100) / math.log(top_score) 1269 for pty in filtered_priorities: 1270 filtered_priorities[pty] **= scaling_power 1271 1272 available_pp = dict([(tuple(el.key()), el.data()) for el in empire.planetsWithAvailablePP]) # keys are sets of ints; data is doubles 1273 allocated_pp = dict([(tuple(el.key()), el.data()) for el in empire.planetsWithAllocatedPP]) # keys are sets of ints; data is doubles 1274 planets_with_wasted_pp = set([tuple(pidset) for pidset in empire.planetsWithWastedPP]) 1275 debug("avail_pp ( <systems> : pp ):") 1276 for planet_set in available_pp: 1277 debug("\t%s\t%.2f", PlanetUtilsAI.sys_name_ids(set(PlanetUtilsAI.get_systems(planet_set))), available_pp[planet_set]) 1278 debug("\nallocated_pp ( <systems> : pp ):") 1279 for planet_set in allocated_pp: 1280 debug("\t%s\t%.2f", PlanetUtilsAI.sys_name_ids(set(PlanetUtilsAI.get_systems(planet_set))), allocated_pp[planet_set]) 1281 1282 debug("\n\nBuilding Ships in system groups with remaining PP:") 1283 for planet_set in planets_with_wasted_pp: 1284 total_pp = available_pp.get(planet_set, 0) 1285 avail_pp = total_pp - allocated_pp.get(planet_set, 0) 1286 if avail_pp <= 0.01: 1287 continue 1288 debug("%.2f PP remaining in system group: %s", avail_pp, PlanetUtilsAI.sys_name_ids(set(PlanetUtilsAI.get_systems(planet_set)))) 1289 debug("\t owned planets in this group are:") 1290 debug("\t %s", PlanetUtilsAI.planet_string(planet_set)) 1291 best_design_id, best_design, build_choices = get_best_ship_info(PriorityType.PRODUCTION_COLONISATION, list(planet_set)) 1292 species_map = {} 1293 for loc in (build_choices or []): 1294 this_spec = universe.getPlanet(loc).speciesName 1295 species_map.setdefault(this_spec, []).append(loc) 1296 colony_build_choices = [] 1297 for pid, (score, this_spec) in aistate.colonisablePlanetIDs.items(): 1298 colony_build_choices.extend(int(math.ceil(score))*[pid_ for pid_ in species_map.get(this_spec, []) if pid_ in planet_set]) 1299 1300 local_priorities = {} 1301 local_priorities.update(filtered_priorities) 1302 best_ships = {} 1303 mil_build_choices = get_best_ship_ratings(planet_set) 1304 for priority in list(local_priorities): 1305 if priority == PriorityType.PRODUCTION_MILITARY: 1306 if not mil_build_choices: 1307 del local_priorities[priority] 1308 continue 1309 _, pid, best_design_id, best_design = mil_build_choices[0] 1310 build_choices = [pid] 1311 # score = ColonisationAI.pilotRatings.get(pid, 0) 1312 # if bestScore < ColonisationAI.curMidPilotRating: 1313 else: 1314 best_design_id, best_design, build_choices = get_best_ship_info(priority, list(planet_set)) 1315 if best_design is None: 1316 del local_priorities[priority] # must be missing a shipyard -- TODO build a shipyard if necessary 1317 continue 1318 best_ships[priority] = [best_design_id, best_design, build_choices] 1319 debug("best_ships[%s] = %s \t locs are %s from %s", priority, best_design.name, build_choices, planet_set) 1320 1321 if len(local_priorities) == 0: 1322 debug("Alert!! need shipyards in systemSet %s", PlanetUtilsAI.sys_name_ids(set(PlanetUtilsAI.get_systems(planet_set)))) 1323 priority_choices = [] 1324 for priority in local_priorities: 1325 priority_choices.extend(int(local_priorities[priority]) * [priority]) 1326 1327 loop_count = 0 1328 while (avail_pp > 0) and (loop_count < max(100, current_turn)) and (priority_choices != []): # make sure don't get stuck in some nonbreaking loop like if all shipyards captured 1329 loop_count += 1 1330 debug("Beginning build enqueue loop %d; %.1f PP available", loop_count, avail_pp) 1331 this_priority = random.choice(priority_choices) 1332 debug("selected priority: %s", this_priority) 1333 making_colony_ship = False 1334 making_outpost_ship = False 1335 if this_priority == PriorityType.PRODUCTION_COLONISATION: 1336 if total_colony_fleets >= max_colony_fleets: 1337 debug("Already sufficient colony ships in queue, trying next priority choice\n") 1338 for i in range(len(priority_choices) - 1, -1, -1): 1339 if priority_choices[i] == PriorityType.PRODUCTION_COLONISATION: 1340 del priority_choices[i] 1341 continue 1342 elif colony_build_choices is None or len(colony_build_choices) == 0: 1343 for i in range(len(priority_choices) - 1, -1, -1): 1344 if priority_choices[i] == PriorityType.PRODUCTION_COLONISATION: 1345 del priority_choices[i] 1346 continue 1347 else: 1348 making_colony_ship = True 1349 if this_priority == PriorityType.PRODUCTION_OUTPOST: 1350 if total_outpost_fleets >= max_outpost_fleets: 1351 debug("Already sufficient outpost ships in queue, trying next priority choice\n") 1352 for i in range(len(priority_choices) - 1, -1, -1): 1353 if priority_choices[i] == PriorityType.PRODUCTION_OUTPOST: 1354 del priority_choices[i] 1355 continue 1356 else: 1357 making_outpost_ship = True 1358 best_design_id, best_design, build_choices = best_ships[this_priority] 1359 if making_colony_ship: 1360 loc = random.choice(colony_build_choices) 1361 best_design_id, best_design, build_choices = get_best_ship_info(PriorityType.PRODUCTION_COLONISATION, loc) 1362 elif this_priority == PriorityType.PRODUCTION_MILITARY: 1363 selector = random.random() 1364 choice = mil_build_choices[0] # mil_build_choices can't be empty due to earlier check 1365 for choice in mil_build_choices: 1366 if choice[0] >= selector: 1367 break 1368 loc, best_design_id, best_design = choice[1:4] 1369 if best_design is None: 1370 warning("problem with mil_build_choices;" 1371 " with selector (%s) chose loc (%s), " 1372 "best_design_id (%s), best_design (None) " 1373 "from mil_build_choices: %s" % (selector, loc, best_design_id, mil_build_choices)) 1374 continue 1375 else: 1376 loc = random.choice(build_choices) 1377 1378 ship_number = 1 1379 per_turn_cost = (float(best_design.productionCost(empire.empireID, loc)) / best_design.productionTime(empire.empireID, loc)) 1380 if this_priority == PriorityType.PRODUCTION_MILITARY: 1381 this_rating = ColonisationAI.pilot_ratings.get(loc, 0) 1382 rating_ratio = float(this_rating) / state.best_pilot_rating 1383 if rating_ratio < 0.1: 1384 loc_planet = universe.getPlanet(loc) 1385 if loc_planet: 1386 pname = loc_planet.name 1387 this_rating = ColonisationAI.rate_planetary_piloting(loc) 1388 rating_ratio = float(this_rating) / state.best_pilot_rating 1389 qualifier = "suboptimal " if rating_ratio < 1.0 else "" 1390 debug("Building mil ship at loc %d (%s) with %spilot Rating: %.1f; ratio to empire best is %.1f", loc, pname, qualifier, this_rating, rating_ratio) 1391 while total_pp > 40 * per_turn_cost: 1392 ship_number *= 2 1393 per_turn_cost *= 2 1394 retval = fo.issueEnqueueShipProductionOrder(best_design_id, loc) 1395 if retval != 0: 1396 prioritized = False 1397 debug("adding %d new ship(s) at location %s to production queue: %s; per turn production cost %.1f\n", 1398 ship_number, PlanetUtilsAI.planet_string(loc), best_design.name, per_turn_cost) 1399 if ship_number > 1: 1400 fo.issueChangeProductionQuantityOrder(production_queue.size - 1, 1, ship_number) 1401 avail_pp -= per_turn_cost 1402 if making_colony_ship: 1403 total_colony_fleets += ship_number 1404 if total_pp > 4 * per_turn_cost: 1405 fo.issueRequeueProductionOrder(production_queue.size - 1, 0) # move to front 1406 continue 1407 if making_outpost_ship: 1408 total_outpost_fleets += ship_number 1409 if total_pp > 4 * per_turn_cost: 1410 fo.issueRequeueProductionOrder(production_queue.size - 1, 0) # move to front 1411 continue 1412 if total_pp > 10 * per_turn_cost: 1413 leading_block_pp = 0 1414 for elem in [production_queue[elemi] for elemi in range(0, min(4, production_queue.size))]: 1415 cost, time = empire.productionCostAndTime(elem) 1416 leading_block_pp += elem.blocksize * cost / time 1417 if leading_block_pp > 0.5 * total_pp or (military_emergency and this_priority == PriorityType.PRODUCTION_MILITARY): 1418 prioritized = True 1419 fo.issueRequeueProductionOrder(production_queue.size - 1, 0) # move to front 1420 if this_priority == PriorityType.PRODUCTION_INVASION: 1421 queued_troop_ships += ship_number 1422 if not prioritized: 1423 fo.issueRequeueProductionOrder(production_queue.size - 1, 0) # move to front 1424 # The AI will normally only consider queuing additional ships (above) the current queue is not using all the 1425 # available PP; this can delay the AI from pursuing easy invasion targets. 1426 # Queue an extra troopship if the following conditions are met: 1427 # i) currently dealing with our Capital ResourceGroup 1428 # ii) the invasion priority for this group is nonzero and the max priority, and 1429 # iii) there are minimal troopships already enqueued 1430 invasion_priority = local_priorities.get(PriorityType.PRODUCTION_INVASION, 0) 1431 if (capital_id in planet_set and 1432 invasion_priority and 1433 invasion_priority == max(local_priorities.values()) and 1434 queued_troop_ships <= 2): # todo get max from character module or otherwise calculate 1435 best_design_id, best_design, build_choices = best_ships[PriorityType.PRODUCTION_INVASION] 1436 loc = random.choice(build_choices) 1437 retval = fo.issueEnqueueShipProductionOrder(best_design_id, loc) 1438 if retval != 0: 1439 per_turn_cost = (float(best_design.productionCost(empire.empireID, loc))/best_design.productionTime( 1440 empire.empireID, loc)) 1441 avail_pp -= per_turn_cost 1442 debug("adding extra trooper at location %s to production queue: %s; per turn production cost %.1f\n", 1443 PlanetUtilsAI.planet_string(loc), best_design.name, per_turn_cost) 1444 1445 debug('') 1446 update_stockpile_use() 1447 fo.updateProductionQueue() 1448 _print_production_queue(after_turn=True) 1449 1450 1451def update_stockpile_use(): 1452 """Decide which elements in the production_queue will be enabled for drawing from the imperial stockpile. This 1453 initial version simply ensures that every resource group with at least one item on the queue has its highest 1454 priority item be stockpile-enabled. 1455 1456 :return: None 1457 """ 1458 # TODO: Do a priority and risk evaluation to decide on enabling stockpile draws 1459 empire = fo.getEmpire() 1460 production_queue = empire.productionQueue 1461 resource_groups = set(tuple(el.key()) for el in empire.planetsWithAvailablePP) 1462 planets_in_stockpile_enabled_group = set() 1463 for queue_index, element in enumerate(production_queue): 1464 if element.locationID in planets_in_stockpile_enabled_group: 1465 # TODO: evaluate possibly disabling stockpile for current element if was previously enabled, perhaps 1466 # only allowing multiple stockpile enabled items in empire-capital-resource-group, or considering some 1467 # priority analysis 1468 continue 1469 group = next((_g for _g in resource_groups if element.locationID in _g), None) 1470 if group is None: 1471 continue # we don't appear to own the location any more 1472 if fo.issueAllowStockpileProductionOrder(queue_index, True): 1473 planets_in_stockpile_enabled_group.update(group) 1474 1475 1476def empire_has_colony_bld_species(building_name): 1477 """Checks if this building is a colony building for which this empire has the required source species available. 1478 :rtype: bool 1479 """ 1480 if not building_name.startswith('BLD_COL_'): 1481 return False 1482 species_name = 'SP_' + building_name.split('BLD_COL_')[1] 1483 return species_name in ColonisationAI.empire_colonizers 1484 1485 1486def already_has_completed_colony_building(planet_id): 1487 """Checks if a planet has an already-completed (but not yet 'hatched') colony building. 1488 :rtype: bool 1489 """ 1490 universe = fo.getUniverse() 1491 planet = universe.getPlanet(planet_id) 1492 return any(universe.getBuilding(bldg).name.startswith('BLD_COL_') for bldg in planet.buildingIDs) 1493 1494 1495def build_ship_facilities(bld_name, best_pilot_facilities, top_locs=None): 1496 if top_locs is None: 1497 top_locs = [] 1498 universe = fo.getUniverse() 1499 empire = fo.getEmpire() 1500 total_pp = empire.productionPoints 1501 __, prereq_bldg, this_cost, time = AIDependencies.SHIP_FACILITIES.get(bld_name, (None, '', -1, -1)) 1502 if not get_aistate().character.may_build_building(bld_name): 1503 return 1504 bld_type = fo.getBuildingType(bld_name) 1505 if not empire.buildingTypeAvailable(bld_name): 1506 return 1507 queued_bld_locs = [element.locationID for element in empire.productionQueue if element.name == bld_name] 1508 if bld_name in AIDependencies.SYSTEM_SHIP_FACILITIES: 1509 current_locs = ColonisationAI.system_facilities.get(bld_name, {}).get('systems', set()) 1510 current_coverage = current_locs.union(universe.getPlanet(planet_id).systemID for planet_id in queued_bld_locs) 1511 open_systems = set(universe.getPlanet(pid).systemID 1512 for pid in best_pilot_facilities.get("BLD_SHIPYARD_BASE", [])).difference(current_coverage) 1513 try_systems = open_systems.intersection(ColonisationAI.system_facilities.get( 1514 prereq_bldg, {}).get('systems', [])) if prereq_bldg else open_systems 1515 try_locs = set(pid for sys_id in try_systems for pid in state.get_empire_planets_by_system(sys_id)) 1516 else: 1517 current_locs = best_pilot_facilities.get(bld_name, []) 1518 try_locs = set(best_pilot_facilities.get(prereq_bldg, [])).difference( 1519 queued_bld_locs, current_locs) 1520 debug("Considering constructing a %s, have %d already built and %d queued", 1521 bld_name, len(current_locs), len(queued_bld_locs)) 1522 max_under_construction = max(1, (time * total_pp) // (5 * this_cost)) 1523 max_total = max(1, (time * total_pp) // (2 * this_cost)) 1524 debug("Allowances: max total: %d, max under construction: %d", max_total, max_under_construction) 1525 if len(current_locs) >= max_total: 1526 return 1527 valid_locs = (list(loc for loc in try_locs.intersection(top_locs) if bld_type.canBeProduced(empire.empireID, loc)) + 1528 list(loc for loc in try_locs.difference(top_locs) if bld_type.canBeProduced(empire.empireID, loc))) 1529 debug("Have %d potential locations: %s", len(valid_locs), [universe.getPlanet(x) for x in valid_locs]) 1530 # TODO: rank by defense ability, etc. 1531 num_queued = len(queued_bld_locs) 1532 already_covered = [] # just those covered on this turn 1533 while valid_locs: 1534 if num_queued >= max_under_construction: 1535 break 1536 pid = valid_locs.pop() 1537 if pid in already_covered: 1538 continue 1539 res = fo.issueEnqueueBuildingProductionOrder(bld_name, pid) 1540 debug("Enqueueing %s at planet %s , with result %d", bld_name, universe.getPlanet(pid), res) 1541 if res: 1542 num_queued += 1 1543 already_covered.extend(state.get_empire_planets_by_system(universe.getPlanet(pid).systemID)) 1544 1545 1546def _print_production_queue(after_turn=False): 1547 """Print production queue content with relevant info in table format.""" 1548 universe = fo.getUniverse() 1549 s = "after" if after_turn else "before" 1550 title = "Production Queue Turn %d %s ProductionAI calls" % (fo.currentTurn(), s) 1551 prod_queue_table = Table( 1552 [Text('Object'), Text('Location'), Text('Quantity'), Text('Progress'), Text('Allocated PP'), Text('Turns left')], 1553 table_name=title 1554 ) 1555 for element in fo.getEmpire().productionQueue: 1556 if element.buildType == EmpireProductionTypes.BT_SHIP: 1557 item = fo.getShipDesign(element.designID) 1558 elif element.buildType == EmpireProductionTypes.BT_BUILDING: 1559 item = fo.getBuildingType(element.name) 1560 else: 1561 continue 1562 cost = item.productionCost(fo.empireID(), element.locationID) 1563 1564 prod_queue_table.add_row([ 1565 element.name, 1566 universe.getPlanet(element.locationID), 1567 "%dx %d" % (element.remaining, element.blocksize), 1568 "%.1f / %.1f" % (element.progress*cost, cost), 1569 "%.1f" % element.allocation, 1570 "%d" % element.turnsLeft, 1571 ]) 1572 info(prod_queue_table) 1573 1574 1575def find_automatic_historic_analyzer_candidates(): 1576 """ 1577 Find possible locations for the BLD_AUTO_HISTORY_ANALYSER building and return a subset of chosen building locations. 1578 1579 :return: Random possible locations up to max queueable amount. Empty if no location found or can't queue another one 1580 :rtype: list 1581 """ 1582 empire = fo.getEmpire() 1583 universe = fo.getUniverse() 1584 total_pp = empire.productionPoints 1585 history_analyser = "BLD_AUTO_HISTORY_ANALYSER" 1586 culture_archives = "BLD_CULTURE_ARCHIVES" 1587 1588 ARB_LARGE_NUMBER = 1e4 1589 conditions = { 1590 # aggression: (min_pp, min_turn, min_pp_to_queue_another_one) 1591 fo.aggression.beginner: (100, 100, ARB_LARGE_NUMBER), 1592 fo.aggression.turtle: (75, 75, ARB_LARGE_NUMBER), 1593 fo.aggression.cautious: (40, 40, ARB_LARGE_NUMBER), 1594 fo.aggression.typical: (20, 20, 50), 1595 fo.aggression.aggressive: (10, 10, 40), 1596 fo.aggression.maniacal: (8, 5, 30) 1597 } 1598 1599 min_pp, turn_trigger, min_pp_per_additional = conditions.get(get_aistate().character.get_trait(Aggression).key, 1600 (ARB_LARGE_NUMBER, ARB_LARGE_NUMBER, ARB_LARGE_NUMBER)) 1601 max_enqueued = 1 if total_pp > min_pp or fo.currentTurn() > turn_trigger else 0 1602 max_enqueued += int(total_pp / min_pp_per_additional) 1603 1604 if max_enqueued <= 0: 1605 return [] 1606 1607 # find possible locations 1608 possible_locations = set() 1609 for pid in state.get_all_empire_planets(): 1610 planet = universe.getPlanet(pid) 1611 if not planet or planet.currentMeterValue(fo.meterType.targetPopulation) < 1: 1612 continue 1613 buildings_here = [bld.buildingTypeName for bld in map(universe.getBuilding, planet.buildingIDs)] 1614 if planet and culture_archives in buildings_here and history_analyser not in buildings_here: 1615 possible_locations.add(pid) 1616 1617 # check existing queued buildings and remove from possible locations 1618 queued_locs = {e.locationID for e in empire.productionQueue if e.buildType == EmpireProductionTypes.BT_BUILDING and 1619 e.name == history_analyser} 1620 1621 possible_locations -= queued_locs 1622 chosen_locations = [] 1623 for i in range(min(max_enqueued, len(possible_locations))): 1624 chosen_locations.append(possible_locations.pop()) 1625 return chosen_locations 1626 1627 1628def get_number_of_queued_outpost_and_colony_ships(): 1629 """Get the total number of queued outpost/colony ships/bases. 1630 1631 :rtype: int 1632 """ 1633 num_ships = 0 1634 considered_ship_roles = (ShipRoleType.CIVILIAN_OUTPOST, ShipRoleType.BASE_OUTPOST, 1635 ShipRoleType.BASE_COLONISATION, ShipRoleType.CIVILIAN_COLONISATION) 1636 for element in fo.getEmpire().productionQueue: 1637 if element.turnsLeft >= 0 and element.buildType == EmpireProductionTypes.BT_SHIP: 1638 if get_aistate().get_ship_role(element.designID) in considered_ship_roles: 1639 num_ships += element.blocksize 1640 return num_ships 1641 1642 1643def get_number_of_existing_outpost_and_colony_ships(): 1644 """Get the total number of existing outpost/colony ships/bases. 1645 1646 :rtype: int 1647 """ 1648 num_colony_fleets = len(FleetUtilsAI.get_empire_fleet_ids_by_role(MissionType.COLONISATION)) 1649 num_outpost_fleets = len(FleetUtilsAI.get_empire_fleet_ids_by_role(MissionType.OUTPOST)) 1650 return num_outpost_fleets + num_colony_fleets 1651