1import random
2
3import freeorion as fo
4
5import universe_statistics
6from names import get_name_list, random_name
7from options import (HS_ACCEPTABLE_PLANET_SIZES, HS_ACCEPTABLE_PLANET_TYPES, HS_MAX_JUMP_DISTANCE_LIMIT,
8                     HS_MIN_DISTANCE_PRIORITY_LIMIT, HS_MIN_PLANETS_IN_VICINITY_PER_SYSTEM,
9                     HS_MIN_PLANETS_IN_VICINITY_TOTAL, HS_MIN_SYSTEMS_IN_VICINITY, HS_VICINITY_RANGE)
10from planets import calc_planet_size, calc_planet_type, planet_sizes_real, planet_types_real
11from starsystems import pick_star_type, star_types_real
12from util import report_error, unique_product
13
14
15def get_empire_name_generator():
16    """
17    String generator, return random empire name from string list,
18    if string list is empty generate random name.
19    """
20    empire_names = get_name_list("EMPIRE_NAMES")
21    random.shuffle(empire_names)
22    while True:
23        if empire_names:
24            yield empire_names.pop()
25        else:
26            yield random_name(5)
27
28
29# generate names for empires, use next(empire_name_generator) to get next name.
30empire_name_generator = get_empire_name_generator()
31
32
33def get_starting_species_pool():
34    """
35    Empire species pool generator, return random empire species and ensure somewhat even distribution
36    """
37    # fill the initial pool with two sets of all playable species
38    # this way we have somewhat, but not absolutely strict even distribution of starting species at least when there
39    # is only a few number of players (some species can occur twice at max while others not at all)
40    pool = fo.get_playable_species() * 2
41
42    # randomize order in initial pool so we don't get the same species all the time
43    random.shuffle(pool)
44    # generator loop
45    while True:
46        # if our pool is exhausted (because we have more players than species instances in our initial pool)
47        # refill the pool with one set of all playable species
48        if not pool:
49            pool = fo.get_playable_species()
50            # again, randomize order in refilled pool so we don't get the same species all the time
51            random.shuffle(pool)
52        # pick and return next species, and remove it from our pool
53        yield pool.pop()
54
55
56# generates starting species for empires, use next(starting_species_pool) to get next species
57starting_species_pool = get_starting_species_pool()
58
59
60def count_planets_in_systems(systems, planet_types_filter=HS_ACCEPTABLE_PLANET_TYPES):
61    """
62    Return the total number of planets in the specified group of systems,
63    only count the planet types specified in planet_types_filter.
64    """
65    num_planets = 0
66    for system in systems:
67        num_planets += len([p for p in fo.sys_get_planets(system) if fo.planet_get_type(p) in planet_types_filter])
68    return num_planets
69
70
71def calculate_home_system_merit(system):
72    """Calculate the system's merit as the number of planets within HS_VICINTIY_RANGE."""
73    return count_planets_in_systems(fo.systems_within_jumps_unordered(HS_VICINITY_RANGE, [system]))
74
75
76def min_planets_in_vicinity_limit(num_systems):
77    """
78    Calculates the minimum planet limit for the specified number of systems.
79    This limit is the lower of HS_MIN_PLANETS_IN_VICINITY_TOTAL or HS_MIN_PLANETS_IN_VICINITY_PER_SYSTEM
80    planets per system.
81    """
82    return min(HS_MIN_PLANETS_IN_VICINITY_TOTAL, num_systems * HS_MIN_PLANETS_IN_VICINITY_PER_SYSTEM)
83
84
85class HomeSystemFinder:
86    """Finds a set of home systems with a least ''num_home_systems'' systems."""
87    def __init__(self, _num_home_systems):
88        # cache of sytem merits
89        self.system_merit = {}
90        self.num_home_systems = _num_home_systems
91
92    def find_home_systems_for_min_jump_distance(self, systems_pool, min_jumps):
93        """
94        Return a good list of home systems or an empty list if there are fewer than num_home_systems in the pool.
95
96        A good list of home systems are at least the specified minimum number of jumps apart,
97        with the best minimum system merit of all such lists picked randomly from the ''systems_pool''.
98
99        Algorithm:
100        Make several attempts to find systems that match the condition
101        of being at least min_jumps apart.
102        Use the minimum merit of the best num_home_system systems found
103        to compare the candidate with the current best set of systems.
104        On each attempt use the minimum merit of the current best set of home
105        systems to truncate the pool of candidates.
106        """
107
108        # precalculate the system merits
109        for system in systems_pool:
110            if system not in self.system_merit:
111                self.system_merit[system] = calculate_home_system_merit(system)
112
113        # The list of merits and systems sorted in descending order by merit.
114        all_merit_system = sorted([(self.system_merit[s], s)
115                                   for s in systems_pool], reverse=True)
116
117        current_merit_lower_bound = 0
118        best_candidate = []
119
120        # Cap the number of attempts when the found number of systems is less than the target
121        # num_home_systems because this indicates that the min_jumps is too large and/or the
122        # systems_pool is too small to ever succeed.
123
124        # From experimentation with cluster and 3 arm spiral galaxies, with low, med and high
125        # starlane density and (number of systems, number of home systems) pairs of (9999, 399),
126        # (999, 39) and (199, 19) the following was observered.  The distribution of candidate
127        # length is a normal random variable with standard deviation approximately equal to
128
129        # expected_len_candidate_std = (len(systems) ** (1.0/2.0)) * 0.03
130
131        # which is about 1 for 1000 systems.  It is likely that anylen(candidate) is within 1
132        # standard deviation of the expected len(candidate)
133
134        # If we are within the MISS_THRESHOLD of the target then try up to num_complete_misses more times.
135        MISS_THRESHOLD = 3
136        num_complete_misses_remaining = 4
137
138        # Cap the number of attempts to the smaller of the number of systems in the pool, or 100
139        attempts = min(100, len(systems_pool))
140
141        while attempts and num_complete_misses_remaining:
142            # use a local pool of all candidate systems better than the worst threshold merit
143            all_merit_system = [(m, s) for (m, s) in all_merit_system if m > current_merit_lower_bound]
144            local_pool = {s for (m, s) in all_merit_system}
145
146            if len(local_pool) < self.num_home_systems:
147                if not best_candidate:
148                    print("Failing in find_home_systems_for_min_jump_distance because "
149                          "current_merit_lower_bound = {} trims local pool to {} systems "
150                          "which is less than num_home_systems {}.".format(
151                              current_merit_lower_bound, len(local_pool), self.num_home_systems))
152                break
153
154            attempts = min(attempts - 1, len(local_pool))
155
156            candidate = []
157            while local_pool:
158                member = random.choice(list(local_pool))
159                candidate.append(member)
160
161                # remove all neighbors from the local pool
162                local_pool -= set(fo.systems_within_jumps_unordered(min_jumps, [member]))
163
164            # Count complete misses when number of candidates is not close to the target.
165            if len(candidate) < (self.num_home_systems - MISS_THRESHOLD):
166                num_complete_misses_remaining -= 1
167
168            if len(candidate) < self.num_home_systems:
169                continue
170
171            # Calculate the merit of the current attempt.  If it is the best so far
172            # keep it and update the merit_threshold
173            merit_system = sorted([(self.system_merit[s], s)
174                                   for s in candidate], reverse=True)[:self.num_home_systems]
175
176            (merit, system) = merit_system[-1]
177
178            # If we have a better candidate, set the new lower bound and try for a better candidate.
179            if merit > current_merit_lower_bound:
180                print("Home system set merit lower bound improved from {} to "
181                      "{}".format(current_merit_lower_bound, merit))
182                current_merit_lower_bound = merit
183                best_candidate = [s for (_, s) in merit_system]
184
185                # Quit successfully if the lowest merit system meets the minimum threshold
186                if merit >= min_planets_in_vicinity_limit(
187                        len(fo.systems_within_jumps_unordered(HS_VICINITY_RANGE, [system]))):
188                    break
189
190        return best_candidate
191
192
193def find_home_systems(num_home_systems, pool_list, jump_distance, min_jump_distance):
194    """
195    Tries to find a specified number of home systems which are as far apart from each other as possible.
196    Starts with the specified jump distance and reduces that limit until enough systems can be found or the
197    jump distance drops below the specified minimum jump distance limit (in this case fail).
198    For each jump distance several attempts are made: one for each pool passed in pool_list.
199    This parameter contains a list of tuples, each tuple has a pool of systems as first element and a description
200    of the pool for logging purposes as second element.
201    """
202
203    finder = HomeSystemFinder(num_home_systems)
204    # try to find home systems, decrease the min jumps until enough systems can be found, or the jump distance drops
205    # below the specified minimum jump distance (which means failure)
206    while jump_distance >= min_jump_distance:
207        print("Trying to find", num_home_systems, "home systems that are at least", jump_distance, "jumps apart...")
208
209        # try to pick our home systems by iterating over the pools we got
210        for pool, pool_label in pool_list:
211            print("...use", pool_label)
212
213            # check if the pool has enough systems to pick from
214            if len(pool) <= num_home_systems:
215                # no, the pool has less systems than home systems requested, so just skip trying using that pool
216                print("...pool only has", len(pool), "systems, skip trying to use it")
217                continue
218
219            # try to pick home systems
220            home_systems = finder.find_home_systems_for_min_jump_distance(pool, jump_distance)
221            # check if we got enough
222            if len(home_systems) >= num_home_systems:
223                # yes, we got what we need, return the home systems we found
224                print("...", len(home_systems), "systems found")
225                return home_systems
226            else:
227                # no, try next pool
228                print("...only", len(home_systems), "systems found")
229
230        # we did not find enough home systems with the current jump distance requirement,
231        # so decrease the jump distance and try again
232        jump_distance -= 1
233
234    # all attempts came up with too few systems, return empty list to indicate failure
235    return []
236
237
238def add_planets_to_vicinity(vicinity, num_planets, gsd):
239    """
240    Adds the specified number of planets to the specified systems.
241    """
242    print("Adding", num_planets, "planets to the following systems:", vicinity)
243
244    # first, compile a list containing all the free orbits in the specified systems
245    # begin with adding the free orbits of all systems that have a real star (that is, no neutron star, black hole,
246    # and not no star), if that isn't enough, also one, by one, add the free orbits of neutron star, black hole and
247    # no star systems (in that order) until we have enough free orbits
248
249    # for that, we use this list of tuples
250    # the first tuple contains all real star types, the following tuples the neutron, black hole and no star types,
251    # so we can iterate over this list and only add the free orbits of systems that match the respective star types
252    # each step
253    # this way we can prioritize the systems we want to add planets to by star type
254    acceptable_star_types_list = [
255        star_types_real,
256        (fo.starType.noStar,),
257        (fo.starType.neutron,),
258        (fo.starType.blackHole,)
259    ]
260
261    # store the free orbits as a list of tuples of (system, orbit)
262    free_orbits_map = []
263
264    # now, iterate over the list of acceptable star types
265    for acceptable_star_types in acceptable_star_types_list:
266        # check all systems in the list of systems we got passed into this function
267        for system in vicinity:
268            # if this system has a star type we want to accept in this step, add its free orbits to our list
269            if fo.sys_get_star_type(system) in acceptable_star_types:
270                free_orbits_map.extend([(system, orbit) for orbit in fo.sys_free_orbits(system)])
271        # check if we got enough free orbits after completing this step
272        # we want 4 times as much free orbits as planets we want to add, that means each system shouldn't get more
273        # than 2-3 additional planets on average
274        if len(free_orbits_map) > (num_planets * 4):
275            break
276
277    # if we got less free orbits than planets that should be added, something is wrong
278    # in that case abort and log an error
279    if len(free_orbits_map) < num_planets:
280        report_error("Python add_planets_to_vicinity: less free orbits than planets to add - cancelled")
281
282    print("...free orbits available:", free_orbits_map)
283    # as we will pop the free orbits from this list afterwards, shuffle it to randomize the order of the orbits
284    random.shuffle(free_orbits_map)
285
286    # add the requested number of planets
287    while num_planets > 0:
288        # get the next free orbit from the list we just compiled
289        system, orbit = free_orbits_map.pop()
290
291        # check the star type of the system containing the orbit we got
292        star_type = fo.sys_get_star_type(system)
293        if star_type in [fo.starType.noStar, fo.starType.blackHole]:
294            # if it is a black hole or has no star, change the star type
295            # pick a star type, continue until we get a real star
296            # don't accept neutron, black hole or no star
297            print("...system picked to add a planet has star type", star_type)
298            while star_type not in star_types_real:
299                star_type = pick_star_type(gsd.age)
300            print("...change that to", star_type)
301            fo.sys_set_star_type(system, star_type)
302
303        # pick a planet size, continue until we get a size that matches the HS_ACCEPTABLE_PLANET_SIZES option
304        planet_size = fo.planetSize.unknown
305        while planet_size not in HS_ACCEPTABLE_PLANET_SIZES:
306            planet_size = calc_planet_size(star_type, orbit, fo.galaxySetupOption.high, gsd.shape)
307
308        # pick an according planet type
309        planet_type = calc_planet_type(star_type, orbit, planet_size)
310
311        # finally, create the planet in the system and orbit we got
312        print("...adding", planet_size, planet_type, "planet to system", system)
313        if fo.create_planet(planet_size, planet_type, system, orbit, "") == fo.invalid_object():
314            report_error("Python add_planets_to_vicinity: create planet in system %d failed" % system)
315
316        # continue with next planet
317        num_planets -= 1
318
319
320def compile_home_system_list(num_home_systems, systems, gsd):
321    """
322    Compiles a list with a requested number of home systems.
323    """
324    print("Compile home system list:", num_home_systems, "systems requested")
325
326    # if the list of systems to choose home systems from is empty, report an error and return empty list
327    if not systems:
328        report_error("Python generate_home_system_list: no systems to choose from")
329        return []
330
331    # calculate an initial minimal number of jumps that the home systems should be apart,
332    # based on the total number of systems to choose from and the requested number of home systems
333    # don't let min_jumps be either:
334    # a.) larger than a defined limit, because an unreasonably large number is really not at all needed,
335    #     and with large galaxies an excessive amount of time can be used in failed attempts
336    # b.) lower than the minimum jump distance limit that should be considered high priority (see options.py),
337    #     otherwise no attempt at all would be made to enforce the other requirements for home systems (see below)
338    min_jumps = min(HS_MAX_JUMP_DISTANCE_LIMIT, max(int(len(systems) / (num_home_systems * 2)),
339                                                    HS_MIN_DISTANCE_PRIORITY_LIMIT))
340
341    # home systems must have a certain minimum of systems and planets in their near vicinity
342    # we will try to select our home systems from systems that match this criteria, if that fails, we will select our
343    # home systems from all systems and add the missing number planets to the systems in their vicinity afterwards
344    # the minimum system and planet limit and the jump range that defines the "near vicinity" are controlled by the
345    # HS_* option constants in options.py (see there)
346
347    # we start by building two additional pools of systems: one that contains all systems that match the criteria
348    # completely (meets the min systems and planets limit), and one that contains all systems that match the criteria
349    # at least partially (meets the min systems limit)
350    pool_matching_sys_and_planet_limit = []
351    pool_matching_sys_limit = []
352    for system in systems:
353        systems_in_vicinity = fo.systems_within_jumps_unordered(HS_VICINITY_RANGE, [system])
354        if len(systems_in_vicinity) >= HS_MIN_SYSTEMS_IN_VICINITY:
355            pool_matching_sys_limit.append(system)
356            if count_planets_in_systems(systems_in_vicinity) >= min_planets_in_vicinity_limit(len(systems_in_vicinity)):
357                pool_matching_sys_and_planet_limit.append(system)
358    print(len(pool_matching_sys_and_planet_limit),
359          "systems meet the min systems and planets in the near vicinity limit")
360    print(len(pool_matching_sys_limit), "systems meet the min systems in the near vicinity limit")
361
362    # now try to pick the requested number of home systems
363    # we will do this by calling find_home_systems, which takes a list of tuples defining the pools from which to pick
364    # the home systems; it will use the pools in the order in which they appear in the list, so put better pools first
365
366    # we will make two attempts: the first one with the filtered pools we just created, and tell find_home_systems
367    # to start with the min_jumps jumps distance we calculated above, but not to go lower than
368    # HS_MIN_DISTANCE_PRIORITY_LIMIT
369
370    print("First attempt: trying to pick home systems from the filtered pools of preferred systems")
371    pool_list = [
372        # the better pool is of course the one where all systems meet BOTH the min systems and planets limit
373        (pool_matching_sys_and_planet_limit, "pool of systems that meet both the min systems and planets limit"),
374        # next the less preferred pool where all systems at least meets the min systems limit
375        # specify 0 as number of requested home systems to pick as much systems as possible
376        (pool_matching_sys_limit, "pool of systems that meet at least the min systems limit"),
377    ]
378    home_systems = find_home_systems(num_home_systems, pool_list, min_jumps, HS_MIN_DISTANCE_PRIORITY_LIMIT)
379
380    # check if the first attempt delivered a list with enough home systems
381    # if not, we make our second attempt, this time disregarding the filtered pools and using all systems, starting
382    # again with the min_jumps jump distance limit and specifying 0 as number of required home systems to pick as much
383    # systems as possible
384    if len(home_systems) < num_home_systems:
385        print("Second attempt: trying to pick home systems from all systems")
386        home_systems = find_home_systems(num_home_systems, [(systems, "complete pool")], min_jumps, 1)
387
388    # check if the selection process delivered a list with enough home systems
389    # if not, our galaxy obviously is too crowded, report an error and return an empty list
390    if len(home_systems) < num_home_systems:
391        report_error("Python generate_home_system_list: requested %d homeworlds in a galaxy with %d systems"
392                     % (num_home_systems, len(systems)))
393        return []
394
395    # check if we got more home systems than we requested
396    if len(home_systems) > num_home_systems:
397        # yes: calculate the number of planets in the near vicinity of each system
398        # and store that value with each system in a map
399        hs_planets_in_vicinity_map = {s: calculate_home_system_merit(s) for s in home_systems}
400        # sort the home systems by the number of planets in their near vicinity using the map
401        # now only pick the number of home systems we need, taking those with the highest number of planets
402        home_systems = sorted(home_systems, key=hs_planets_in_vicinity_map.get, reverse=True)[:num_home_systems]
403
404    # make sure all our home systems have a "real" star (that is, a star that is not a neutron star, black hole,
405    # or even no star at all) and at least one planet in it
406    for home_system in home_systems:
407        # if this home system has no "real" star, change star type to a randomly selected "real" star
408        if fo.sys_get_star_type(home_system) not in star_types_real:
409            star_type = random.choice(star_types_real)
410            print("Home system", home_system, "has star type", fo.sys_get_star_type(home_system), ", changing that to",
411                  star_type)
412            fo.sys_set_star_type(home_system, star_type)
413
414        # if this home system has no planets, create one in a random orbit
415        # we take random values for type and size, as these will be set to suitable values later
416        if not fo.sys_get_planets(home_system):
417            print("Home system", home_system, "has no planets, adding one")
418            planet = fo.create_planet(random.choice(planet_sizes_real),
419                                      random.choice(planet_types_real),
420                                      home_system, random.randint(0, fo.sys_get_num_orbits(home_system) - 1), "")
421            # if we couldn't create the planet, report an error and return an empty list
422            if planet == fo.invalid_object():
423                report_error("Python generate_home_system_list: couldn't create planet in home system")
424                return []
425
426    # finally, check again if all home systems meet the criteria of having the required minimum number of planets
427    # within their near vicinity, if not, add the missing number of planets
428    print("Checking if home systems have the required minimum of planets within the near vicinity...")
429    for home_system in home_systems:
430        # calculate the number of missing planets, and add them if this number is > 0
431        systems_in_vicinity = fo.systems_within_jumps_unordered(HS_VICINITY_RANGE, [home_system])
432        num_systems_in_vicinity = len(systems_in_vicinity)
433        num_planets_in_vicinity = count_planets_in_systems(systems_in_vicinity)
434        num_planets_to_add = min_planets_in_vicinity_limit(num_systems_in_vicinity) - num_planets_in_vicinity
435        print("Home system", home_system, "has", num_systems_in_vicinity, "systems and", num_planets_in_vicinity,
436              "planets in the near vicinity, required minimum:", min_planets_in_vicinity_limit(num_systems_in_vicinity))
437        if num_planets_to_add > 0:
438            systems_in_vicinity.remove(home_system)  # don't add planets to the home system, so remove it from the list
439            # sort the systems_in_vicinity before adding, since the C++ engine doesn't guarrantee the same
440            # platform independence as python.
441            add_planets_to_vicinity(sorted(systems_in_vicinity), num_planets_to_add, gsd)
442
443    # as we've sorted the home system list before, lets shuffle it to ensure random order and return
444    random.shuffle(home_systems)
445    return home_systems
446
447
448def setup_empire(empire, empire_name, home_system, starting_species, player_name):
449    """
450    Sets up various aspects of an empire, like empire name, homeworld, etc.
451    """
452
453    # set empire name, if no one is given, pick one randomly
454    if not empire_name:
455        print("No empire name set for player", player_name, ", picking one randomly")
456        empire_name = next(empire_name_generator)
457    fo.empire_set_name(empire, empire_name)
458    print("Empire name for player", player_name, "is", empire_name)
459
460    # check starting species, if no one is given, pick one randomly
461    if starting_species == "RANDOM" or not starting_species:
462        print("Picking random starting species for player", player_name)
463        starting_species = next(starting_species_pool)
464    print("Starting species for player", player_name, "is", starting_species)
465    universe_statistics.empire_species[starting_species] += 1
466
467    # pick a planet from the specified home system as homeworld
468    planet_list = fo.sys_get_planets(home_system)
469    # if the system is empty, report an error and return false, indicating failure
470    if not planet_list:
471        report_error("Python setup_empire: got home system with no planets")
472        return False
473    homeworld = random.choice(planet_list)
474
475    # set selected planet as empire homeworld with selected starting species
476    fo.empire_set_homeworld(empire, homeworld, starting_species)
477
478    # set homeworld focus
479    # check if the preferred focus for the starting species is among
480    # the foci available on the homeworld planet
481    available_foci = fo.planet_available_foci(homeworld)
482    preferred_focus = fo.species_preferred_focus(starting_species)
483    if preferred_focus in available_foci:
484        # if yes, set the homeworld focus to the preferred focus
485        print("Player", player_name, ": setting preferred focus", preferred_focus, "on homeworld")
486        fo.planet_set_focus(homeworld, preferred_focus)
487    elif len(available_foci) > 0:
488        # if no, and there is at least one available focus,
489        # just take the first of the list
490        if preferred_focus == "":
491            print("Player", player_name, ": starting species", starting_species, "has no preferred focus, using",
492                  available_foci[0], "instead")
493        else:
494            print("Player", player_name, ": preferred focus", preferred_focus, "for starting species", starting_species,
495                  "not available on homeworld, using", available_foci[0], "instead")
496        fo.planet_set_focus(homeworld, available_foci[0])
497    else:
498        # if no focus is available on the homeworld, don't set any focus
499        print("Player", player_name, ": no available foci on homeworld for starting species", starting_species)
500
501    # give homeworld starting buildings
502    print("Player", player_name, ": add starting buildings to homeworld")
503    for item in fo.load_starting_buildings():
504        fo.create_building(item.name, homeworld, empire)
505
506    # unlock starting techs, buildings, hulls, ship parts, etc.
507    # use default content file
508    print("Player", player_name, ": add unlocked items")
509    for item in fo.load_unlockable_item_list():
510        fo.empire_unlock_item(empire, item.type, item.name)
511
512    # add premade ship designs to empire
513    print("Player", player_name, ": add premade ship designs")
514    for ship_design in fo.design_get_premade_list():
515        fo.empire_add_ship_design(empire, ship_design)
516
517    # add starting fleets to empire
518    # use default content file
519    print("Player", player_name, ": add starting fleets")
520    fleet_plans = fo.load_fleet_plan_list()
521    for fleet_plan in fleet_plans:
522        # should fleet be aggressive? check if any ships are armed.
523        should_be_aggressive = False
524        for ship_design in fleet_plan.ship_designs():
525            design = fo.getPredefinedShipDesign(ship_design)
526            if design is None:
527                report_error("Looked up null design with name %s", ship_design)
528            elif design.isArmed:
529                should_be_aggressive = True
530                break
531
532        # first, create the fleet
533        fleet = fo.create_fleet(fleet_plan.name(), home_system, empire, should_be_aggressive)
534        # if the fleet couldn't be created, report an error and try to continue with the next fleet plan
535        if fleet == fo.invalid_object():
536            report_error("Python setup empire: couldn't create fleet %s" % fleet_plan.name())
537            continue
538
539        # second, iterate over the list of ship design names in the fleet plan
540        for ship_design in fleet_plan.ship_designs():
541            # create a ship in the fleet
542            # if the ship couldn't be created, report an error and try to continue with the next ship design
543            if fo.create_ship("", ship_design, starting_species, fleet) == fo.invalid_object():
544                report_error("Python setup empire: couldn't create ship of design %s for fleet %s"
545                             % (ship_design, fleet_plan.name()))
546
547    return True
548
549
550def home_system_layout(home_systems, systems):
551    """
552    Home systems layout generation to place teams.
553    Returns map from home system to neighbor home systems.
554    """
555    # for each system found nearest home systems
556    # maybe multiple if home worlds placed on the same jump distnace
557    system_hs = {}
558    for system in systems:
559        nearest_hs = set()
560        nearest_dist = None
561        for hs in home_systems:
562            dist = fo.jump_distance(system, hs)
563            if nearest_dist is None or nearest_dist > dist:
564                nearest_dist = dist
565                nearest_hs = {hs}
566            elif nearest_dist == dist:
567                nearest_hs.add(hs)
568        system_hs[system] = nearest_hs
569
570    # homeworld is connected to the other
571    # if both are nearest for some system
572    # if each of them is nearest for systems on the starline ends
573    home_system_connections = {}
574    for system, connected_home_systems in system_hs.items():
575        if len(connected_home_systems) >= 2:
576            for hs1, hs2 in unique_product(connected_home_systems, connected_home_systems):
577                home_system_connections.setdefault(hs1, set()).add(hs2)
578                home_system_connections.setdefault(hs2, set()).add(hs1)
579
580    for system, connected_home_systems in system_hs.items():
581        adj_systems = fo.systems_within_jumps_unordered(1, [system])
582        for system2 in adj_systems:
583            connected_home_systems2 = system_hs.get(system2, set())
584            for hs1, hs2 in unique_product(connected_home_systems, connected_home_systems2):
585                home_system_connections.setdefault(hs1, set()).add(hs2)
586                home_system_connections.setdefault(hs2, set()).add(hs1)
587    return home_system_connections
588