def find_home_systems_for_min_jump_distance(self, systems_pool, min_jumps): """ Return a good list of home systems or an empty list if there are fewer than num_home_systems in the pool. A good list of home systems are at least the specified minimum number of jumps apart, with the best minimum system merit of all such lists picked randomly from the ''systems_pool''. Algorithm: Make several attempts to find systems that match the condition of being at least min_jumps apart. Use the minimum merit of the best num_home_system systems found to compare the candidate with the current best set of systems. On each attempt use the minimum merit of the current best set of home systems to truncate the pool of candidates. """ # precalculate the system merits for system in systems_pool: if system not in self.system_merit: self.system_merit[system] = calculate_home_system_merit(system) # The list of merits and systems sorted in descending order by merit. all_merit_system = sorted([(self.system_merit[s], s) for s in systems_pool], reverse=True) current_merit_lower_bound = 0 best_candidate = [] # Cap the number of attempts when the found number of systems is less than the target # num_home_systems because this indicates that the min_jumps is too large and/or the # systems_pool is too small to ever succeed. # From experimentation with cluster and 3 arm spiral galaxies, with low, med and high # starlane density and (number of systems, number of home systems) pairs of (9999, 399), # (999, 39) and (199, 19) the following was observered. The distribution of candidate # length is a normal random variable with standard deviation approximately equal to # expected_len_candidate_std = (len(systems) ** (1.0/2.0)) * 0.03 # which is about 1 for 1000 systems. It is likely that anylen(candidate) is within 1 # standard deviation of the expected len(candidate) # If we are within the MISS_THRESHOLD of the target then try up to num_complete_misses more times. MISS_THRESHOLD = 3 num_complete_misses_remaining = 4 # Cap the number of attempts to the smaller of the number of systems in the pool, or 100 attempts = min(100, len(systems_pool)) while attempts and num_complete_misses_remaining: # use a local pool of all candidate systems better than the worst threshold merit all_merit_system = [(m, s) for (m, s) in all_merit_system if m > current_merit_lower_bound] local_pool = {s for (m, s) in all_merit_system} if len(local_pool) < self.num_home_systems: if not best_candidate: print( "Failing in find_home_systems_for_min_jump_distance because " "current_merit_lower_bound = {} trims local pool to {} systems " "which is less than num_home_systems {}.".format( current_merit_lower_bound, len(local_pool), self.num_home_systems)) break attempts = min(attempts - 1, len(local_pool)) candidate = [] while local_pool: member = random.choice(list(local_pool)) candidate.append(member) # remove all neighbors from the local pool local_pool -= set( fo.systems_within_jumps_unordered(min_jumps, [member])) # Count complete misses when number of candidates is not close to the target. if len(candidate) < (self.num_home_systems - MISS_THRESHOLD): num_complete_misses_remaining -= 1 if len(candidate) < self.num_home_systems: continue # Calculate the merit of the current attempt. If it is the best so far # keep it and update the merit_threshold merit_system = sorted([(self.system_merit[s], s) for s in candidate], reverse=True)[:self.num_home_systems] (merit, system) = merit_system[-1] # If we have a better candidate, set the new lower bound and try for a better candidate. if merit > current_merit_lower_bound: print( "Home system set merit lower bound improved from {} to " "{}".format(current_merit_lower_bound, merit)) current_merit_lower_bound = merit best_candidate = [s for (_, s) in merit_system] # Quit sucessfully if the lowest merit system meets the minimum threshold if merit >= min_planets_in_vicinity_limit( fo.systems_within_jumps_unordered( HS_VICINITY_RANGE, [system])): break return best_candidate
def distribute_specials(specials_freq, universe_objects): """ Adds start-of-game specials to universe objects. """ # get basic chance for occurrence of specials from the universe tables base_chance = universe_tables.SPECIALS_FREQUENCY[specials_freq] if base_chance <= 0: return # get a list with all specials that have a spawn rate and limit both > 0 and a location condition defined # (no location condition means a special shouldn't get added at game start) specials = [sp for sp in fo.get_all_specials() if fo.special_spawn_rate(sp) > 0.0 and fo.special_spawn_limit(sp) > 0 and fo.special_has_location(sp)] if not specials: return # dump a list of all specials meeting that conditions and their properties to the log print "Specials available for distribution at game start:" for special in specials: print("... {:30}: spawn rate {:2.3f} / spawn limit {}". format(special, fo.special_spawn_rate(special), fo.special_spawn_limit(special))) objects_needing_specials = [obj for obj in universe_objects if random.random() < base_chance] track_num_placed = {obj: 0 for obj in universe_objects} print("Base chance for specials is {}. Placing specials on {} of {} ({:1.4f})objects" .format(base_chance, len(objects_needing_specials), len(universe_objects), float(len(objects_needing_specials)) / len(universe_objects))) obj_tuple_needing_specials = set(zip(objects_needing_specials, fo.objs_get_systems(objects_needing_specials), calculate_number_of_specials_to_place(objects_needing_specials))) # Equal to the largest distance in WithinStarlaneJumps conditions # GALAXY_DECOUPLING_DISTANCE is used as follows. For any two or more objects # at least GALAXY_DECOUPLING_DISTANCE appart you only need to check # fo.special_locations once and then you can place as many specials as possible, # subject to number restrictions. # # Organize the objects into sets where all objects are spaced GALAXY_DECOUPLING_DISTANCE # appart. Place a special on each one. Repeat until you run out of specials or objects. GALAXY_DECOUPLING_DISTANCE = 6 while obj_tuple_needing_specials: systems_needing_specials = defaultdict(set) for (obj, system, specials_count) in obj_tuple_needing_specials: systems_needing_specials[system].add((obj, system, specials_count)) print " Placing in {} locations remaining.".format(len(systems_needing_specials)) # Find a list of candidates all spaced GALAXY_DECOUPLING_DISTANCE apart candidates = [] while systems_needing_specials: random_sys = random.choice(systems_needing_specials.values()) member = random.choice(list(random_sys)) obj, system, specials_count = member candidates.append(obj) obj_tuple_needing_specials.remove(member) if specials_count > 1: obj_tuple_needing_specials.add((obj, system, specials_count - 1)) # remove all neighbors from the local pool for neighbor in fo.systems_within_jumps_unordered(GALAXY_DECOUPLING_DISTANCE, [system]): if neighbor in systems_needing_specials: systems_needing_specials.pop(neighbor) print("Caching specials_locations() at {} of {} remaining locations.". format(str(len(candidates)), str(len(obj_tuple_needing_specials) + len(candidates)))) # Get the locations at which each special can be placed locations_cache = {} for special in specials: # The fo.special_locations in the following line consumes most of the time in this # function. Decreasing GALAXY_DECOUPLING_DISTANCE will speed up the whole # function by reducing the number of times this needs to be called. locations_cache[special] = set(fo.special_locations(special, candidates)) # Attempt to apply a special to each candidate # by finding a special that can be applied to it and hasn't been added too many times for obj in candidates: # check if the spawn limit for this special has already been reached (that is, if this special # has already been added the maximal allowed number of times) specials = [s for s in specials if universe_statistics.specials_summary[s] < fo.special_spawn_limit(s)] if not specials: break # Find which specials can be placed at this one location local_specials = [sp for sp in specials if obj in locations_cache[sp]] if not local_specials: universe_statistics.specials_repeat_dist[0] += 1 continue # All prerequisites and the test have been met, now add this special to this universe object. track_num_placed[obj] += place_special(local_specials, obj) for num_placed in track_num_placed.values(): universe_statistics.specials_repeat_dist[num_placed] += 1
def distribute_specials(specials_freq, universe_objects): """ Adds start-of-game specials to universe objects. """ # get basic chance for occurrence of specials from the universe tables base_chance = universe_tables.SPECIALS_FREQUENCY[specials_freq] if base_chance <= 0: return # get a list with all specials that have a spawn rate and limit both > 0 and a location condition defined # (no location condition means a special shouldn't get added at game start) specials = [ sp for sp in fo.get_all_specials() if fo.special_spawn_rate(sp) > 0.0 and fo.special_spawn_limit(sp) > 0 and fo.special_has_location(sp) ] if not specials: return # dump a list of all specials meeting that conditions and their properties to the log print("Specials available for distribution at game start:") for special in specials: print("... {:30}: spawn rate {:2.3f} / spawn limit {}".format( special, fo.special_spawn_rate(special), fo.special_spawn_limit(special))) objects_needing_specials = [ obj for obj in universe_objects if random.random() < base_chance ] track_num_placed = {obj: 0 for obj in universe_objects} print( "Base chance for specials is {}. Placing specials on {} of {} ({:1.4f})objects" .format( base_chance, len(objects_needing_specials), len(universe_objects), float(len(objects_needing_specials)) / len(universe_objects), )) obj_tuple_needing_specials = set( zip( objects_needing_specials, fo.objs_get_systems(objects_needing_specials), calculate_number_of_specials_to_place(objects_needing_specials), )) # Equal to the largest distance in WithinStarlaneJumps conditions # GALAXY_DECOUPLING_DISTANCE is used as follows. For any two or more objects # at least GALAXY_DECOUPLING_DISTANCE appart you only need to check # fo.special_locations once and then you can place as many specials as possible, # subject to number restrictions. # # Organize the objects into sets where all objects are spaced GALAXY_DECOUPLING_DISTANCE # appart. Place a special on each one. Repeat until you run out of specials or objects. GALAXY_DECOUPLING_DISTANCE = 6 while obj_tuple_needing_specials: systems_needing_specials = defaultdict(set) for (obj, system, specials_count) in obj_tuple_needing_specials: systems_needing_specials[system].add((obj, system, specials_count)) print(" Placing in {} locations remaining.".format( len(systems_needing_specials))) # Find a list of candidates all spaced GALAXY_DECOUPLING_DISTANCE apart candidates = [] while systems_needing_specials: random_sys = random.choice(list(systems_needing_specials.values())) member = random.choice(list(random_sys)) obj, system, specials_count = member candidates.append(obj) obj_tuple_needing_specials.remove(member) if specials_count > 1: obj_tuple_needing_specials.add( (obj, system, specials_count - 1)) # remove all neighbors from the local pool for neighbor in fo.systems_within_jumps_unordered( GALAXY_DECOUPLING_DISTANCE, [system]): if neighbor in systems_needing_specials: systems_needing_specials.pop(neighbor) print("Caching specials_locations() at {} of {} remaining locations.". format(str(len(candidates)), str(len(obj_tuple_needing_specials) + len(candidates)))) # Get the locations at which each special can be placed locations_cache = {} for special in specials: # The fo.special_locations in the following line consumes most of the time in this # function. Decreasing GALAXY_DECOUPLING_DISTANCE will speed up the whole # function by reducing the number of times this needs to be called. locations_cache[special] = set( fo.special_locations(special, candidates)) # Attempt to apply a special to each candidate # by finding a special that can be applied to it and hasn't been added too many times for obj in candidates: # check if the spawn limit for this special has already been reached (that is, if this special # has already been added the maximal allowed number of times) specials = [ s for s in specials if universe_statistics.specials_summary[s] < fo.special_spawn_limit(s) ] if not specials: break # Find which specials can be placed at this one location local_specials = [ sp for sp in specials if obj in locations_cache[sp] ] if not local_specials: universe_statistics.specials_repeat_dist[0] += 1 continue # All prerequisites and the test have been met, now add this special to this universe object. track_num_placed[obj] += place_special(local_specials, obj) for num_placed in track_num_placed.values(): universe_statistics.specials_repeat_dist[num_placed] += 1
def compile_home_system_list(num_home_systems, systems, gsd): """ Compiles a list with a requested number of home systems. """ print("Compile home system list:", num_home_systems, "systems requested") # if the list of systems to choose home systems from is empty, report an error and return empty list if not systems: report_error( "Python generate_home_system_list: no systems to choose from") return [] # calculate an initial minimal number of jumps that the home systems should be apart, # based on the total number of systems to choose from and the requested number of home systems # don't let min_jumps be either: # a.) larger than a defined limit, because an unreasonably large number is really not at all needed, # and with large galaxies an excessive amount of time can be used in failed attempts # b.) lower than the minimum jump distance limit that should be considered high priority (see options.py), # otherwise no attempt at all would be made to enforce the other requirements for home systems (see below) min_jumps = min( HS_MAX_JUMP_DISTANCE_LIMIT, max(int(len(systems) / (num_home_systems * 2)), HS_MIN_DISTANCE_PRIORITY_LIMIT)) # home systems must have a certain minimum of systems and planets in their near vicinity # we will try to select our home systems from systems that match this criteria, if that fails, we will select our # home systems from all systems and add the missing number planets to the systems in their vicinity afterwards # the minimum system and planet limit and the jump range that defines the "near vicinity" are controlled by the # HS_* option constants in options.py (see there) # we start by building two additional pools of systems: one that contains all systems that match the criteria # completely (meets the min systems and planets limit), and one that contains all systems that match the criteria # at least partially (meets the min systems limit) pool_matching_sys_and_planet_limit = [] pool_matching_sys_limit = [] for system in systems: systems_in_vicinity = fo.systems_within_jumps_unordered( HS_VICINITY_RANGE, [system]) if len(systems_in_vicinity) >= HS_MIN_SYSTEMS_IN_VICINITY: pool_matching_sys_limit.append(system) if count_planets_in_systems( systems_in_vicinity) >= min_planets_in_vicinity_limit( len(systems_in_vicinity)): pool_matching_sys_and_planet_limit.append(system) print( len(pool_matching_sys_and_planet_limit), "systems meet the min systems and planets in the near vicinity limit") print(len(pool_matching_sys_limit), "systems meet the min systems in the near vicinity limit") # now try to pick the requested number of home systems # we will do this by calling find_home_systems, which takes a list of tuples defining the pools from which to pick # the home systems; it will use the pools in the order in which they appear in the list, so put better pools first # we will make two attempts: the first one with the filtered pools we just created, and tell find_home_systems # to start with the min_jumps jumps distance we calculated above, but not to go lower than # HS_MIN_DISTANCE_PRIORITY_LIMIT print( "First attempt: trying to pick home systems from the filtered pools of preferred systems" ) pool_list = [ # the better pool is of course the one where all systems meet BOTH the min systems and planets limit (pool_matching_sys_and_planet_limit, "pool of systems that meet both the min systems and planets limit"), # next the less preferred pool where all systems at least meets the min systems limit # specify 0 as number of requested home systems to pick as much systems as possible (pool_matching_sys_limit, "pool of systems that meet at least the min systems limit"), ] home_systems = find_home_systems(num_home_systems, pool_list, min_jumps, HS_MIN_DISTANCE_PRIORITY_LIMIT) # check if the first attempt delivered a list with enough home systems # if not, we make our second attempt, this time disregarding the filtered pools and using all systems, starting # again with the min_jumps jump distance limit and specifying 0 as number of required home systems to pick as much # systems as possible if len(home_systems) < num_home_systems: print("Second attempt: trying to pick home systems from all systems") home_systems = find_home_systems(num_home_systems, [(systems, "complete pool")], min_jumps, 1) # check if the selection process delivered a list with enough home systems # if not, our galaxy obviously is too crowded, report an error and return an empty list if len(home_systems) < num_home_systems: report_error( "Python generate_home_system_list: requested %d homeworlds in a galaxy with %d systems" % (num_home_systems, len(systems))) return [] # check if we got more home systems than we requested if len(home_systems) > num_home_systems: # yes: calculate the number of planets in the near vicinity of each system # and store that value with each system in a map hs_planets_in_vicinity_map = { s: calculate_home_system_merit(s) for s in home_systems } # sort the home systems by the number of planets in their near vicinity using the map # now only pick the number of home systems we need, taking those with the highest number of planets home_systems = sorted(home_systems, key=hs_planets_in_vicinity_map.get, reverse=True)[:num_home_systems] # make sure all our home systems have a "real" star (that is, a star that is not a neutron star, black hole, # or even no star at all) and at least one planet in it for home_system in home_systems: # if this home system has no "real" star, change star type to a randomly selected "real" star if fo.sys_get_star_type(home_system) not in star_types_real: star_type = random.choice(star_types_real) print("Home system", home_system, "has star type", fo.sys_get_star_type(home_system), ", changing that to", star_type) fo.sys_set_star_type(home_system, star_type) # if this home system has no planets, create one in a random orbit # we take random values for type and size, as these will be set to suitable values later if not fo.sys_get_planets(home_system): print("Home system", home_system, "has no planets, adding one") planet = fo.create_planet( random.choice(planet_sizes_real), random.choice(planet_types_real), home_system, random.randint(0, fo.sys_get_num_orbits(home_system) - 1), "") # if we couldn't create the planet, report an error and return an empty list if planet == fo.invalid_object(): report_error( "Python generate_home_system_list: couldn't create planet in home system" ) return [] # finally, check again if all home systems meet the criteria of having the required minimum number of planets # within their near vicinity, if not, add the missing number of planets print( "Checking if home systems have the required minimum of planets within the near vicinity..." ) for home_system in home_systems: # calculate the number of missing planets, and add them if this number is > 0 systems_in_vicinity = fo.systems_within_jumps_unordered( HS_VICINITY_RANGE, [home_system]) num_systems_in_vicinity = len(systems_in_vicinity) num_planets_in_vicinity = count_planets_in_systems(systems_in_vicinity) num_planets_to_add = min_planets_in_vicinity_limit( num_systems_in_vicinity) - num_planets_in_vicinity print("Home system", home_system, "has", num_systems_in_vicinity, "systems and", num_planets_in_vicinity, "planets in the near vicinity, required minimum:", min_planets_in_vicinity_limit(num_systems_in_vicinity)) if num_planets_to_add > 0: systems_in_vicinity.remove( home_system ) # don't add planets to the home system, so remove it from the list # sort the systems_in_vicinity before adding, since the C++ engine doesn't guarrantee the same # platform independence as python. add_planets_to_vicinity(sorted(systems_in_vicinity), num_planets_to_add, gsd) # as we've sorted the home system list before, lets shuffle it to ensure random order and return random.shuffle(home_systems) return home_systems
def calculate_home_system_merit(system): """Calculate the system's merit as the number of planets within HS_VICINTIY_RANGE.""" return count_planets_in_systems( fo.systems_within_jumps_unordered(HS_VICINITY_RANGE, [system]))
def generate_natives(native_freq, systems, empire_home_systems): """ Adds non-empire-affiliated native populations to planets. """ # first, calculate the chance for natives on a planet based on the native frequency that has been passed # get the corresponding value for the specified natives frequency from the universe tables native_chance = universe_tables.NATIVE_FREQUENCY[native_freq] # a value of 0 means no natives, in this case return immediately if native_chance <= 0: return # compile a list of planets where natives can be placed # select only planets sufficiently far away from player home systems # list of planets safe for natives EMPIRE_TO_NATIVE_MIN_DIST = 2 empire_exclusions = set( itertools.chain.from_iterable( fo.systems_within_jumps_unordered(EMPIRE_TO_NATIVE_MIN_DIST, [e]) for e in empire_home_systems)) native_safe_planets = set( itertools.chain.from_iterable([ fo.sys_get_planets(s) for s in systems if s not in empire_exclusions ])) print( "Number of planets far enough from players for natives to be allowed:", len(native_safe_planets)) # if there are no "native safe" planets at all, we can stop here if not native_safe_planets: return # get all native species native_species = fo.get_native_species() print("Species that can be added as natives:") print("... " + "\n... ".join(native_species)) # create a map with a list for each planet type containing the species # for which this planet type is a good environment # we will need this afterwards when picking natives for a planet natives_for_planet_type.clear() # just to be safe natives_for_planet_type.update( {planet_type: [] for planet_type in planets.planet_types}) planet_types_for_natives.clear() planet_types_for_natives.update( {species: set() for species in native_species}) # iterate over all native species we got for species in native_species: # check the planet environment for all planet types for this species for planet_type in planets.planet_types: # if this planet type is a good environment for the species, add it to the list for this planet type if fo.species_get_planet_environment( species, planet_type) == fo.planetEnvironment.good: natives_for_planet_type[planet_type].append(species) planet_types_for_natives[species].add(planet_type) # randomly add species to planets # iterate over the list of "native safe" planets we compiled earlier for candidate in native_safe_planets: # select a native species to put on this planet planet_type = fo.planet_get_type(candidate) # check if we have any native species that like this planet type if not natives_for_planet_type[planet_type]: # no, continue with next planet continue universe_statistics.potential_native_planet_summary[planet_type] += 1 # make a "roll" against the chance for natives to determine if we shall place natives on this planet if random.random() > native_chance: # no, continue with next planet continue universe_statistics.settled_native_planet_summary[planet_type] += 1 # randomly pick one of the native species available for this planet type natives = random.choice(natives_for_planet_type[planet_type]) # put the selected natives on the planet fo.planet_set_species(candidate, natives) # set planet as homeworld for that species fo.species_add_homeworld(natives, candidate) # set planet focus # check if the preferred focus for the native species is among the foci available on this planet available_foci = fo.planet_available_foci(candidate) preferred_focus = fo.species_preferred_focus(natives) if preferred_focus in available_foci: # if yes, set the planet focus to the preferred focus fo.planet_set_focus(candidate, preferred_focus) elif available_foci: # if no, and there is at least one available focus, just take the first of the list # otherwise don't set any focus fo.planet_set_focus(candidate, available_foci[0]) print("Added native", natives, "to planet", fo.get_name(candidate)) # increase the statistics counter for this native species, so a species summary can be dumped to the log later universe_statistics.species_summary[natives] += 1
def generate_natives(native_freq, systems, empire_home_systems): """ Adds non-empire-affiliated native populations to planets. """ # first, calculate the chance for natives on a planet based on the native frequency that has been passed # get the corresponding value for the specified natives frequency from the universe tables native_chance = universe_tables.NATIVE_FREQUENCY[native_freq] # a value of 0 means no natives, in this case return immediately if native_chance <= 0: return # compile a list of planets where natives can be placed # select only planets sufficiently far away from player home systems # list of planets safe for natives EMPIRE_TO_NATIVE_MIN_DIST = 2 empire_exclusions = set(itertools.chain.from_iterable( fo.systems_within_jumps_unordered(EMPIRE_TO_NATIVE_MIN_DIST, [e]) for e in empire_home_systems)) native_safe_planets = set(itertools.chain.from_iterable( [fo.sys_get_planets(s) for s in systems if s not in empire_exclusions])) print "Number of planets far enough from players for natives to be allowed:", len(native_safe_planets) # if there are no "native safe" planets at all, we can stop here if not native_safe_planets: return # get all native species native_species = fo.get_native_species() print "Species that can be added as natives:" print "... " + "\n... ".join(native_species) # create a map with a list for each planet type containing the species # for which this planet type is a good environment # we will need this afterwards when picking natives for a planet natives_for_planet_type.clear() # just to be safe natives_for_planet_type.update({planet_type: [] for planet_type in planets.planet_types}) planet_types_for_natives.clear() planet_types_for_natives.update({species: set() for species in native_species}) # iterate over all native species we got for species in native_species: # check the planet environment for all planet types for this species for planet_type in planets.planet_types: # if this planet type is a good environment for the species, add it to the list for this planet type if fo.species_get_planet_environment(species, planet_type) == fo.planetEnvironment.good: natives_for_planet_type[planet_type].append(species) planet_types_for_natives[species].add(planet_type) # randomly add species to planets # iterate over the list of "native safe" planets we compiled earlier for candidate in native_safe_planets: # select a native species to put on this planet planet_type = fo.planet_get_type(candidate) # check if we have any native species that like this planet type if not natives_for_planet_type[planet_type]: # no, continue with next planet continue universe_statistics.potential_native_planet_summary[planet_type] += 1 # make a "roll" against the chance for natives to determine if we shall place natives on this planet if random.random() > native_chance: # no, continue with next planet continue universe_statistics.settled_native_planet_summary[planet_type] += 1 # randomly pick one of the native species available for this planet type natives = random.choice(natives_for_planet_type[planet_type]) # put the selected natives on the planet fo.planet_set_species(candidate, natives) # set planet as homeworld for that species fo.species_add_homeworld(natives, candidate) # set planet focus # check if the preferred focus for the native species is among the foci available on this planet available_foci = fo.planet_available_foci(candidate) preferred_focus = fo.species_preferred_focus(natives) if preferred_focus in available_foci: # if yes, set the planet focus to the preferred focus fo.planet_set_focus(candidate, preferred_focus) elif available_foci: # if no, and there is at least one available focus, just take the first of the list # otherwise don't set any focus fo.planet_set_focus(candidate, available_foci[0]) print "Added native", natives, "to planet", fo.get_name(candidate) # increase the statistics counter for this native species, so a species summary can be dumped to the log later universe_statistics.species_summary[natives] += 1
def find_home_systems_for_min_jump_distance(self, systems_pool, min_jumps): """ Return a good list of home systems or an empty list if there are fewer than num_home_systems in the pool. A good list of home systems are at least the specified minimum number of jumps apart, with the best minimum system merit of all such lists picked randomly from the ''systems_pool''. Algorithm: Make several attempts to find systems that match the condition of being at least min_jumps apart. Use the minimum merit of the best num_home_system systems found to compare the candidate with the current best set of systems. On each attempt use the minimum merit of the current best set of home systems to truncate the pool of candidates. """ # precalculate the system merits for system in systems_pool: if system not in self.system_merit: self.system_merit[system] = calculate_home_system_merit(system) # The list of merits and systems sorted in descending order by merit. all_merit_system = sorted([(self.system_merit[s], s) for s in systems_pool], reverse=True) current_merit_lower_bound = 0 best_candidate = [] # Cap the number of attempts when the found number of systems is less than the target # num_home_systems because this indicates that the min_jumps is too large and/or the # systems_pool is too small to ever succeed. # From experimentation with cluster and 3 arm spiral galaxies, with low, med and high # starlane density and (number of systems, number of home systems) pairs of (9999, 399), # (999, 39) and (199, 19) the following was observered. The distribution of candidate # length is a normal random variable with standard deviation approximately equal to # expected_len_candidate_std = (len(systems) ** (1.0/2.0)) * 0.03 # which is about 1 for 1000 systems. It is likely that anylen(candidate) is within 1 # standard deviation of the expected len(candidate) # If we are within the MISS_THRESHOLD of the target then try up to num_complete_misses more times. MISS_THRESHOLD = 3 num_complete_misses_remaining = 4 # Cap the number of attempts to the smaller of the number of systems in the pool, or 100 attempts = min(100, len(systems_pool)) while attempts and num_complete_misses_remaining: # use a local pool of all candidate systems better than the worst threshold merit all_merit_system = [(m, s) for (m, s) in all_merit_system if m > current_merit_lower_bound] local_pool = {s for (m, s) in all_merit_system} if len(local_pool) < self.num_home_systems: if not best_candidate: print ("Failing in find_home_systems_for_min_jump_distance because " "current_merit_lower_bound = {} trims local pool to {} systems " "which is less than num_home_systems {}.".format( current_merit_lower_bound, len(local_pool), self.num_home_systems)) break attempts = min(attempts - 1, len(local_pool)) candidate = [] while local_pool: member = random.choice(list(local_pool)) candidate.append(member) # remove all neighbors from the local pool local_pool -= set(fo.systems_within_jumps_unordered(min_jumps, [member])) # Count complete misses when number of candidates is not close to the target. if len(candidate) < (self.num_home_systems - MISS_THRESHOLD): num_complete_misses_remaining -= 1 if len(candidate) < self.num_home_systems: continue # Calculate the merit of the current attempt. If it is the best so far # keep it and update the merit_threshold merit_system = sorted([(self.system_merit[s], s) for s in candidate], reverse=True)[:self.num_home_systems] (merit, system) = merit_system[-1] # If we have a better candidate, set the new lower bound and try for a better candidate. if merit > current_merit_lower_bound: print ("Home system set merit lower bound improved from {} to " "{}".format(current_merit_lower_bound, merit)) current_merit_lower_bound = merit best_candidate = [s for (_, s) in merit_system] # Quit successfully if the lowest merit system meets the minimum threshold if merit >= min_planets_in_vicinity_limit( fo.systems_within_jumps_unordered(HS_VICINITY_RANGE, [system])): break return best_candidate
def calculate_home_system_merit(system): """Calculate the system's merit as the number of planets within HS_VICINTIY_RANGE.""" return count_planets_in_systems(fo.systems_within_jumps_unordered(HS_VICINITY_RANGE, [system]))
def compile_home_system_list(num_home_systems, systems, gsd): """ Compiles a list with a requested number of home systems. """ print "Compile home system list:", num_home_systems, "systems requested" # if the list of systems to choose home systems from is empty, report an error and return empty list if not systems: report_error("Python generate_home_system_list: no systems to choose from") return [] # calculate an initial minimal number of jumps that the home systems should be apart, # based on the total number of systems to choose from and the requested number of home systems # don't let min_jumps be either: # a.) larger than a defined limit, because an unreasonably large number is really not at all needed, # and with large galaxies an excessive amount of time can be used in failed attempts # b.) lower than the minimum jump distance limit that should be considered high priority (see options.py), # otherwise no attempt at all would be made to enforce the other requirements for home systems (see below) min_jumps = min(HS_MAX_JUMP_DISTANCE_LIMIT, max(int(float(len(systems)) / float(num_home_systems * 2)), HS_MIN_DISTANCE_PRIORITY_LIMIT)) # home systems must have a certain minimum of systems and planets in their near vicinity # we will try to select our home systems from systems that match this criteria, if that fails, we will select our # home systems from all systems and add the missing number planets to the systems in their vicinity afterwards # the minimum system and planet limit and the jump range that defines the "near vicinity" are controlled by the # HS_* option constants in options.py (see there) # we start by building two additional pools of systems: one that contains all systems that match the criteria # completely (meets the min systems and planets limit), and one that contains all systems that match the criteria # at least partially (meets the min systems limit) pool_matching_sys_and_planet_limit = [] pool_matching_sys_limit = [] for system in systems: systems_in_vicinity = fo.systems_within_jumps_unordered(HS_VICINITY_RANGE, [system]) if len(systems_in_vicinity) >= HS_MIN_SYSTEMS_IN_VICINITY: pool_matching_sys_limit.append(system) if count_planets_in_systems(systems_in_vicinity) >= min_planets_in_vicinity_limit(len(systems_in_vicinity)): pool_matching_sys_and_planet_limit.append(system) print (len(pool_matching_sys_and_planet_limit), "systems meet the min systems and planets in the near vicinity limit") print len(pool_matching_sys_limit), "systems meet the min systems in the near vicinity limit" # now try to pick the requested number of home systems # we will do this by calling find_home_systems, which takes a list of tuples defining the pools from which to pick # the home systems; it will use the pools in the order in which they appear in the list, so put better pools first # we will make two attempts: the first one with the filtered pools we just created, and tell find_home_systems # to start with the min_jumps jumps distance we calculated above, but not to go lower than # HS_MIN_DISTANCE_PRIORITY_LIMIT print "First attempt: trying to pick home systems from the filtered pools of preferred systems" pool_list = [ # the better pool is of course the one where all systems meet BOTH the min systems and planets limit (pool_matching_sys_and_planet_limit, "pool of systems that meet both the min systems and planets limit"), # next the less preferred pool where all systems at least meets the min systems limit # specify 0 as number of requested home systems to pick as much systems as possible (pool_matching_sys_limit, "pool of systems that meet at least the min systems limit"), ] home_systems = find_home_systems(num_home_systems, pool_list, min_jumps, HS_MIN_DISTANCE_PRIORITY_LIMIT) # check if the first attempt delivered a list with enough home systems # if not, we make our second attempt, this time disregarding the filtered pools and using all systems, starting # again with the min_jumps jump distance limit and specifying 0 as number of required home systems to pick as much # systems as possible if len(home_systems) < num_home_systems: print "Second attempt: trying to pick home systems from all systems" home_systems = find_home_systems(num_home_systems, [(systems, "complete pool")], min_jumps, 1) # check if the selection process delivered a list with enough home systems # if not, our galaxy obviously is too crowded, report an error and return an empty list if len(home_systems) < num_home_systems: report_error("Python generate_home_system_list: requested %d homeworlds in a galaxy with %d systems" % (num_home_systems, len(systems))) return [] # check if we got more home systems than we requested if len(home_systems) > num_home_systems: # yes: calculate the number of planets in the near vicinity of each system # and store that value with each system in a map hs_planets_in_vicinity_map = {s: calculate_home_system_merit(s) for s in home_systems} # sort the home systems by the number of planets in their near vicinity using the map # now only pick the number of home systems we need, taking those with the highest number of planets home_systems = sorted(home_systems, key=hs_planets_in_vicinity_map.get, reverse=True)[:num_home_systems] # make sure all our home systems have a "real" star (that is, a star that is not a neutron star, black hole, # or even no star at all) and at least one planet in it for home_system in home_systems: # if this home system has no "real" star, change star type to a randomly selected "real" star if fo.sys_get_star_type(home_system) not in star_types_real: star_type = random.choice(star_types_real) print "Home system", home_system, "has star type", fo.sys_get_star_type(home_system),\ ", changing that to", star_type fo.sys_set_star_type(home_system, star_type) # if this home system has no planets, create one in a random orbit # we take random values for type and size, as these will be set to suitable values later if not fo.sys_get_planets(home_system): print "Home system", home_system, "has no planets, adding one" planet = fo.create_planet(random.choice(planet_sizes_real), random.choice(planet_types_real), home_system, random.randint(0, fo.sys_get_num_orbits(home_system) - 1), "") # if we couldn't create the planet, report an error and return an empty list if planet == fo.invalid_object(): report_error("Python generate_home_system_list: couldn't create planet in home system") return [] # finally, check again if all home systems meet the criteria of having the required minimum number of planets # within their near vicinity, if not, add the missing number of planets print "Checking if home systems have the required minimum of planets within the near vicinity..." for home_system in home_systems: # calculate the number of missing planets, and add them if this number is > 0 systems_in_vicinity = fo.systems_within_jumps_unordered(HS_VICINITY_RANGE, [home_system]) num_systems_in_vicinity = len(systems_in_vicinity) num_planets_in_vicinity = count_planets_in_systems(systems_in_vicinity) num_planets_to_add = min_planets_in_vicinity_limit(num_systems_in_vicinity) - num_planets_in_vicinity print "Home system", home_system, "has", num_systems_in_vicinity, "systems and", num_planets_in_vicinity,\ "planets in the near vicinity, required minimum:", min_planets_in_vicinity_limit(num_systems_in_vicinity) if num_planets_to_add > 0: systems_in_vicinity.remove(home_system) # don't add planets to the home system, so remove it from the list # sort the systems_in_vicinity before adding, since the C++ engine doesn't guarrantee the same # platform independence as python. add_planets_to_vicinity(sorted(systems_in_vicinity), num_planets_to_add, gsd) # as we've sorted the home system list before, lets shuffle it to ensure random order and return random.shuffle(home_systems) return home_systems