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