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 generate_monsters(monster_freq, systems): """ Adds space monsters to systems. """ # first, calculate the basic chance for monster generation in a system # based on the monster frequency that has been passed # get the corresponding value for the specified monster frequency from the universe tables basic_chance = universe_tables.MONSTER_FREQUENCY[monster_freq] # a value of 0 means no monsters, in this case return immediately if basic_chance <= 0: return print "Default monster spawn chance:", basic_chance expectation_tally = 0.0 actual_tally = 0 # get all monster fleets that have a spawn rate and limit both > 0 and at least one monster ship design in it # (a monster fleet with no monsters in it is pointless) and store them in a list fleet_plans = fo.load_monster_fleet_plan_list() # create a map where we store a spawn counter for each monster fleet # this counter will be set to the spawn limit initially and decreased every time the monster fleet is spawned # this map (dict) needs to be separate from the list holding the fleet plans because the order in which items # are stored in a dict is undefined (can be different each time), which would result in different distribution # even when using the same seed for the RNG spawn_limits = {fp: fp.spawn_limit() for fp in fleet_plans if fp.spawn_rate() > 0.0 and fp.spawn_limit() > 0 and fp.ship_designs()} # map nests to monsters for ease of reporting nest_name_map = {"KRAKEN_NEST_SPECIAL": "SM_KRAKEN_1", "SNOWFLAKE_NEST_SPECIAL": "SM_SNOWFLAKE_1", "JUGGERNAUT_NEST_SPECIAL": "SM_JUGGERNAUT_1"} tracked_plan_tries = {name: 0 for name in nest_name_map.values()} tracked_plan_counts = {name: 0 for name in nest_name_map.values()} tracked_plan_valid_locations = {fp: 0 for fp in fleet_plans if fp.name() in tracked_plan_counts} if not fleet_plans: return universe = fo.get_universe() # Fleet plans that include ships capable of altering starlanes. # @content_tag{CAN_ALTER_STARLANES} universe_generator special handling # for fleets containing a hull design with this tag. fleet_can_alter_starlanes = {fp for fp in fleet_plans if any([universe.getGenericShipDesign(design).hull_type.hasTag("CAN_ALTER_STARLANES") for design in fp.ship_designs()])} # dump a list of all monster fleets meeting these conditions and their properties to the log print "Monster fleets available for generation at game start:" fp_location_cache = {} for fleet_plan in fleet_plans: print "...", fleet_plan.name(), ": spawn rate", fleet_plan.spawn_rate(), print "/ spawn limit", fleet_plan.spawn_limit(), print "/ effective chance", basic_chance * fleet_plan.spawn_rate(), fp_location_cache[fleet_plan] = set(fleet_plan.locations(systems)) print ("/ can be spawned at", len(fp_location_cache[fleet_plan]), "of", len(systems), "systems") if fleet_plan.name() in nest_name_map.values(): universe_statistics.tracked_monsters_chance[fleet_plan.name()] = basic_chance * fleet_plan.spawn_rate() # initialize a manager for monsters that can alter the map # required to prevent their placement from disjoining the map starlane_altering_monsters = StarlaneAlteringMonsters(systems) # collect info for tracked monster nest valid locations planets = [p for s in systems for p in fo.sys_get_planets(s)] tracked_nest_valid_locations = {nest: len(fo.special_locations(nest, planets)) for nest in nest_name_map} # for each system in the list that has been passed to this function, find a monster fleet that can be spawned at # the system and which hasn't already been added too many times, then attempt to add that monster fleet by # testing the spawn rate chance random.shuffle(systems) for system in systems: # collect info for tracked monster valid locations for fp in tracked_plan_valid_locations: if system in fp_location_cache[fp]: tracked_plan_valid_locations[fp] += 1 # filter out all monster fleets whose location condition allows this system and whose counter hasn't reached 0. suitable_fleet_plans = [fp for fp in fleet_plans if system in fp_location_cache[fp] and spawn_limits.get(fp, 0) and (fp not in fleet_can_alter_starlanes or starlane_altering_monsters.can_place_at(system, fp))] # if there are no suitable monster fleets for this system, continue with the next if not suitable_fleet_plans: continue # randomly select one monster fleet out of the suitable ones and then test if we want to add it to this system # by making a roll against the basic chance multiplied by the spawn rate of this monster fleet expectation_tally += basic_chance * sum([fp.spawn_rate() for fp in suitable_fleet_plans]) / len(suitable_fleet_plans) fleet_plan = random.choice(suitable_fleet_plans) if fleet_plan.name() in tracked_plan_tries: tracked_plan_tries[fleet_plan.name()] += 1 if random.random() > basic_chance * fleet_plan.spawn_rate(): print("\t\t At system %4d rejected monster fleet %s from %d suitable fleets" % (system, fleet_plan.name(), len(suitable_fleet_plans))) # no, test failed, continue with the next system continue actual_tally += 1 if fleet_plan.name() in tracked_plan_counts: tracked_plan_counts[fleet_plan.name()] += 1 # all prerequisites and the test have been met, now spawn this monster fleet in this system # create monster fleet try: if fleet_plan in fleet_can_alter_starlanes: starlane_altering_monsters.place(system, fleet_plan) else: populate_monster_fleet(fleet_plan, system) # decrement counter for this monster fleet spawn_limits[fleet_plan] -= 1 except MapGenerationError as err: report_error(str(err)) continue print "Actual # monster fleets placed: %d; Total Placement Expectation: %.1f" % (actual_tally, expectation_tally) # finally, compile some statistics to be dumped to the log later universe_statistics.monsters_summary = [ (fp.name(), fp.spawn_limit() - counter) for fp, counter in spawn_limits.iteritems() ] universe_statistics.tracked_monsters_tries.update(tracked_plan_tries) universe_statistics.tracked_monsters_summary.update(tracked_plan_counts) universe_statistics.tracked_monsters_location_summary.update( (fp.name(), count) for fp, count in tracked_plan_valid_locations.iteritems()) universe_statistics.tracked_nest_location_summary.update( (nest_name_map[nest], count) for nest, count in tracked_nest_valid_locations.items())
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 generate_monsters(monster_freq, systems): """ Adds space monsters to systems. """ # first, calculate the basic chance for monster generation in a system # based on the monster frequency that has been passed # get the corresponding value for the specified monster frequency from the universe tables basic_chance = universe_tables.MONSTER_FREQUENCY[monster_freq] # a value of 0 means no monsters, in this case return immediately if basic_chance <= 0: return print "Default monster spawn chance:", basic_chance expectation_tally = 0.0 actual_tally = 0 # get all monster fleets that have a spawn rate and limit both > 0 and at least one monster ship design in it # (a monster fleet with no monsters in it is pointless) and store them in a list fleet_plans = fo.load_monster_fleet_plan_list() # create a map where we store a spawn counter for each monster fleet # this counter will be set to the spawn limit initially and decreased every time the monster fleet is spawned # this map (dict) needs to be separate from the list holding the fleet plans because the order in which items # are stored in a dict is undefined (can be different each time), which would result in different distribution # even when using the same seed for the RNG spawn_limits = {fp: fp.spawn_limit() for fp in fleet_plans if fp.spawn_rate() > 0.0 and fp.spawn_limit() > 0 and fp.ship_designs()} # map nests to monsters for ease of reporting nest_name_map = {"KRAKEN_NEST_SPECIAL": "SM_KRAKEN_1", "SNOWFLAKE_NEST_SPECIAL": "SM_SNOWFLAKE_1", "JUGGERNAUT_NEST_SPECIAL": "SM_JUGGERNAUT_1"} tracked_plan_tries = {name: 0 for name in nest_name_map.values()} tracked_plan_counts = {name: 0 for name in nest_name_map.values()} tracked_plan_valid_locations = {fp: 0 for fp in fleet_plans if fp.name() in tracked_plan_counts} if not fleet_plans: return universe = fo.get_universe() # Fleet plans that include ships capable of altering starlanes. # @content_tag{CAN_ALTER_STARLANES} universe_generator special handling for fleets containing a hull design with this tag. fleet_can_alter_starlanes = {fp for fp in fleet_plans if any([universe.getGenericShipDesign(design).hull_type.hasTag("CAN_ALTER_STARLANES") for design in fp.ship_designs()])} # dump a list of all monster fleets meeting these conditions and their properties to the log print "Monster fleets available for generation at game start:" fp_location_cache = {} for fleet_plan in fleet_plans: print "...", fleet_plan.name(), ": spawn rate", fleet_plan.spawn_rate(), print "/ spawn limit", fleet_plan.spawn_limit(), print "/ effective chance", basic_chance * fleet_plan.spawn_rate(), fp_location_cache[fleet_plan] = set(fleet_plan.locations(systems)) print ("/ can be spawned at", len(fp_location_cache[fleet_plan]), "of", len(systems), "systems") if fleet_plan.name() in nest_name_map.values(): universe_statistics.tracked_monsters_chance[fleet_plan.name()] = basic_chance * fleet_plan.spawn_rate() # initialize a manager for monsters that can alter the map # required to prevent their placement from disjoining the map starlane_altering_monsters = StarlaneAlteringMonsters(systems) # collect info for tracked monster nest valid locations planets = [p for s in systems for p in fo.sys_get_planets(s)] tracked_nest_valid_locations = {nest: len(fo.special_locations(nest, planets)) for nest in nest_name_map} # for each system in the list that has been passed to this function, find a monster fleet that can be spawned at # the system and which hasn't already been added too many times, then attempt to add that monster fleet by # testing the spawn rate chance random.shuffle(systems) for system in systems: # collect info for tracked monster valid locations for fp in tracked_plan_valid_locations: if system in fp_location_cache[fp]: tracked_plan_valid_locations[fp] += 1 # filter out all monster fleets whose location condition allows this system and whose counter hasn't reached 0. suitable_fleet_plans = [fp for fp in fleet_plans if system in fp_location_cache[fp] and spawn_limits[fp] and (fp not in fleet_can_alter_starlanes or starlane_altering_monsters.can_place_at(system, fp))] # if there are no suitable monster fleets for this system, continue with the next if not suitable_fleet_plans: continue # randomly select one monster fleet out of the suitable ones and then test if we want to add it to this system # by making a roll against the basic chance multiplied by the spawn rate of this monster fleet expectation_tally += basic_chance * sum([fp.spawn_rate() for fp in suitable_fleet_plans]) / len(suitable_fleet_plans) fleet_plan = random.choice(suitable_fleet_plans) if fleet_plan.name() in tracked_plan_tries: tracked_plan_tries[fleet_plan.name()] += 1 if random.random() > basic_chance * fleet_plan.spawn_rate(): print("\t\t At system %4d rejected monster fleet %s from %d suitable fleets" % (system, fleet_plan.name(), len(suitable_fleet_plans))) # no, test failed, continue with the next system continue actual_tally += 1 if fleet_plan.name() in tracked_plan_counts: tracked_plan_counts[fleet_plan.name()] += 1 # all prerequisites and the test have been met, now spawn this monster fleet in this system # create monster fleet try: if fleet_plan in fleet_can_alter_starlanes: starlane_altering_monsters.place(system, fleet_plan) else: populate_monster_fleet(fleet_plan, system) # decrement counter for this monster fleet spawn_limits[fleet_plan] -= 1 except MapGenerationError as err: report_error(str(err)) continue print "Actual # monster fleets placed: %d; Total Placement Expectation: %.1f" % (actual_tally, expectation_tally) # finally, compile some statistics to be dumped to the log later universe_statistics.monsters_summary = [(fp.name(), fp.spawn_limit() - counter) for fp, counter in spawn_limits.iteritems()] universe_statistics.tracked_monsters_tries.update(tracked_plan_tries) universe_statistics.tracked_monsters_summary.update(tracked_plan_counts) universe_statistics.tracked_monsters_location_summary.update([(fp.name(), count) for fp, count in tracked_plan_valid_locations.iteritems()]) universe_statistics.tracked_nest_location_summary.update([(nest_name_map[nest], count) for nest, count in tracked_nest_valid_locations.items()])