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
Exemple #2
0
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
Exemple #3
0
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
Exemple #4
0
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
Exemple #5
0
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]))
Exemple #6
0
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
Exemple #7
0
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
Exemple #8
0
    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
Exemple #9
0
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]))
Exemple #10
0
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