def print_retcon_loop_start(game: GameDescription, pickups_left: Iterator[PickupEntry], reach: GeneratorReach, player_index: int, ): if debug.debug_level() > 0: current_uncollected = UncollectedState.from_reach(reach) if debug.debug_level() > 1: extra = ", pickups_left: {}".format(sorted(set(pickup.name for pickup in pickups_left))) else: extra = "" print("\n\n===============================") print("\n>>> Player {}: From {}, {} open pickup indices, {} open resources{}".format( player_index, game.world_list.node_name(reach.state.node, with_world=True), len(current_uncollected.indices), len(current_uncollected.resources), extra )) if debug.debug_level() > 2: print("\nCurrent reach:") for node in reach.nodes: print("[{!s:>5}, {!s:>5}] {}".format(reach.is_reachable_node(node), reach.is_safe_node(node), game.world_list.node_name(node)))
def print_retcon_loop_start(current_uncollected: UncollectedState, logic: Logic, pickups_left, reach): if debug.debug_level() > 0: if debug.debug_level() > 1: extra = ", pickups_left: {}".format(list(pickups_left.keys())) else: extra = "" print("\n\n===============================") print("\n>>> From {}, {} open pickup indices, {} open resources{}". format( logic.game.world_list.node_name(reach.state.node, with_world=True), len(current_uncollected.indices), len(current_uncollected.resources), extra))
def export_layout( layout: LayoutDescription, options: Options, ): """ Creates a seed log file for the given layout and saves it to the configured path :param layout: :param options: :return: """ output_json = options.output_directory.joinpath("{}.json".format( _output_name_for(layout))) if debug.debug_level() > 0: patcher = options.output_directory.joinpath("{}-patcher.json".format( _output_name_for(layout))) with patcher.open("w") as out_file: json.dump(patcher_file.create_patcher_file( layout, options.cosmetic_patches), out_file, indent=4, separators=(',', ': ')) # Save the layout to a file layout.save_to_file(output_json)
def generate_layout(permalink: Permalink, status_update: Callable[[str], None], validate_after_generation: bool, timeout_during_generation: bool, ) -> LayoutDescription: receiving_pipe, output_pipe = multiprocessing.Pipe(True) debug_level = debug.debug_level() if not permalink.spoiler: debug_level = 0 def on_done(_): output_pipe.send(None) with ProcessPoolExecutor(max_workers=1) as executor: future = executor.submit(_generate_layout_worker, output_pipe, permalink, validate_after_generation, timeout_during_generation, debug_level) future.add_done_callback(on_done) while not future.done(): message = receiving_pipe.recv() if message is not None: try: status_update(message) except Exception: receiving_pipe.send("close") raise return future.result()
def get_pickups_that_solves_unreachable( pickups_left: List[PickupEntry], reach: GeneratorReach, uncollected_resource_nodes: List[ResourceNode], ) -> Tuple[Tuple[PickupEntry, ...], ...]: """New logic. Given pickup list and a reach, checks the combination of pickups that satisfies on unreachable nodes""" state = reach.state possible_sets = list(reach.unreachable_nodes_with_requirements().values()) uncollected_resources = [ node.resource() for node in uncollected_resource_nodes ] all_lists = _requirement_lists_without_satisfied_resources( state, possible_sets, uncollected_resources) result = [] for requirement_list in sorted(all_lists): pickups = pickups_to_solve_list(pickups_left, requirement_list, state) if pickups is not None and pickups: # FIXME: avoid duplicates in result result.append(tuple(pickups)) if debug.debug_level() > 2: print(">> All pickup combinations alternatives:") for items in sorted(result): print("* {}".format(", ".join(p.name for p in items))) return tuple(result)
def _requirement_lists_without_satisfied_resources( state: State, possible_sets: List[RequirementSet], uncollected_resources: List[ResourceInfo], ) -> Set[RequirementList]: seen_lists = set() result = set() def _add_items(it): items_tuple = RequirementList(it) if items_tuple not in result: result.add(items_tuple) for requirements in possible_sets: # Maybe should first recreate `requirements` by removing the satisfied items or the ones that can't be for alternative in requirements.alternatives: if alternative in seen_lists: continue seen_lists.add(alternative) for items in _unsatisfied_item_requirements_in_list( alternative, state, uncollected_resources): _add_items(items) if debug.debug_level() > 2: print(">> All requirement lists:") for items in sorted(result): print(f"* {items}") return result
def generate_layout( permalink: Permalink, status_update: Callable[[str], None], validate_after_generation: bool, timeout_during_generation: bool, ) -> LayoutDescription: receiving_pipe, output_pipe = multiprocessing.Pipe(False) debug_level = debug.debug_level() if not permalink.spoiler: debug_level = 0 process = multiprocessing.Process( target=_generate_layout_worker, args=(output_pipe, permalink, validate_after_generation, timeout_during_generation, debug_level)) process.start() try: result: Union[Exception, LayoutDescription] = None while result is None: pipe_input = receiving_pipe.recv() if isinstance(pipe_input, (LayoutDescription, Exception)): result = pipe_input else: status_update(pipe_input) finally: process.terminate() if isinstance(result, Exception): raise result else: return result
def print_new_resources(game: GameDescription, reach: GeneratorReach, seen_count: Dict[ResourceInfo, int], label: str, ): world_list = game.world_list if debug.debug_level() > 1: for index, count in seen_count.items(): if count == 1: node = find_node_with_resource(index, world_list.all_nodes) print("-> New {}: {}".format(label, world_list.node_name(node, with_world=True))) if debug.debug_level() > 2: paths = reach.shortest_path_from(node) path = paths.get(reach.state.node, []) print([node.name for node in path]) print("")
def print_new_pickup_indices( logic: Logic, reach: GeneratorReach, pickup_index_seen_count: Dict[PickupIndex, int], ): world_list = logic.game.world_list if debug.debug_level() > 0: for index, count in pickup_index_seen_count.items(): if count == 1: node = find_pickup_node_with_index(index, world_list.all_nodes) print("-> New Pickup Node: {}".format( world_list.node_name(node, with_world=True))) if debug.debug_level() > 1: paths = reach.shortest_path_from(node) path = paths.get(reach.state.node, []) print([node.name for node in path]) print("")
def print_new_pickup_index(player: int, game: GameDescription, reach: GeneratorReach, location: PickupIndex, count: int): if debug.debug_level() > 1 and count == 1: world_list = game.world_list node = world_list.node_from_pickup_index(location) print("-> New Pickup Index: Player {}'s {}".format( player, world_list.node_name(node, with_world=True)))
def fill_unassigned_hints(self, patches: GamePatches, world_list: WorldList, rng: Random, scan_asset_initial_pickups: dict[NodeIdentifier, frozenset[PickupIndex]], ) -> GamePatches: new_hints = copy.copy(patches.hints) # Get all LogbookAssets from the WorldList potential_hint_locations: set[NodeIdentifier] = { world_list.identifier_for_node(node) for node in world_list.iterate_nodes() if isinstance(node, LogbookNode) } for logbook in potential_hint_locations: if logbook not in scan_asset_initial_pickups: scan_asset_initial_pickups[logbook] = frozenset() # But remove these that already have hints potential_hint_locations -= patches.hints.keys() # We try our best to not hint the same thing twice hinted_indices: set[PickupIndex] = {hint.target for hint in patches.hints.values() if hint.target is not None} # Get interesting items to place hints for possible_indices: set[PickupIndex] = { index for index, target in patches.pickup_assignment.items() if self.interesting_pickup_to_hint(target.pickup) } possible_indices -= hinted_indices debug.debug_print("fill_unassigned_hints had {} decent indices for {} hint locations".format( len(possible_indices), len(potential_hint_locations))) if debug.debug_level() > 1: print(f"> Num pickups per asset:") for asset, pickups in scan_asset_initial_pickups.items(): print(f"* {asset}: {len(pickups)} pickups") print("> Done.") all_pickup_indices = [ node.pickup_index for node in world_list.iterate_nodes() if isinstance(node, PickupNode) ] rng.shuffle(all_pickup_indices) # If there isn't enough indices, use unhinted non-majors placed by generator if (num_indices_needed := len(potential_hint_locations) - len(possible_indices)) > 0: potential_indices = [ index for index in all_pickup_indices if index not in possible_indices and index not in hinted_indices ] debug.debug_print( f"Had only {len(possible_indices)} hintable indices, but needed {len(potential_hint_locations)}." f" Found {len(potential_indices)} less desirable locations.") possible_indices |= set(potential_indices[:num_indices_needed])
def print_retcon_place_pickup(action: PickupEntry, logic: Logic, pickup_index: PickupIndex): world_list = logic.game.world_list if debug.debug_level() > 0: print("\n--> Placing {} at {}".format( action.name, world_list.node_name(find_pickup_node_with_index( pickup_index, world_list.all_nodes), with_world=True)))
def print_retcon_loop_start(current_uncollected: UncollectedState, game: GameDescription, pickups_left: Iterator[PickupEntry], reach: GeneratorReach, ): if debug.debug_level() > 0: if debug.debug_level() > 1: extra = ", pickups_left: {}".format([pickup.name for pickup in pickups_left]) else: extra = "" print("\n\n===============================") print("\n>>> From {}, {} open pickup indices, {} open resources{}".format( game.world_list.node_name(reach.state.node, with_world=True), len(current_uncollected.indices), len(current_uncollected.resources), extra ))
def print_new_resources(game: GameDescription, reach: GeneratorReach, seen_count: Dict[ResourceInfo, int], label: str, ): world_list = game.world_list if debug.debug_level() > 1: for index, count in seen_count.items(): if count == 1: node = find_node_with_resource(index, world_list.all_nodes) print("-> New {}: {}".format(label, world_list.node_name(node, with_world=True))) print("")
def weighted_potential_actions( player_state: PlayerState, status_update: Callable[[str], None], locations_weighted: WeightedLocations) -> dict[Action, float]: """ Weights all potential actions based on current criteria. :param player_state: :param status_update: :param locations_weighted: Which locations are available and their weight. :return: """ actions_weights: dict[Action, float] = {} current_uncollected = UncollectedState.from_reach(player_state.reach) actions = player_state.potential_actions(locations_weighted) options_considered = 0 def update_for_option(): nonlocal options_considered options_considered += 1 status_update("Checked {} of {} options.".format( options_considered, len(actions))) for action in actions: state = player_state.reach.state multiplier = 1 offset = 0 resources, pickups = action.split_pickups() if resources: for resource in resources: state = state.act_on_node(resource) multiplier *= _DANGEROUS_ACTION_MULTIPLIER if pickups: state = state.assign_pickups_resources(pickups) multiplier *= sum(pickup.probability_multiplier for pickup in pickups) / len(pickups) offset += sum(pickup.probability_offset for pickup in pickups) / len(pickups) base_weight = _calculate_weights_for( reach_lib.advance_to_with_reach_copy(player_state.reach, state), current_uncollected) actions_weights[action] = base_weight * multiplier + offset update_for_option() if debug.debug_level() > 1: for action, weight in actions_weights.items(): print("{} - {}".format(action.name, weight)) return actions_weights
def _get_next_player( rng: Random, player_states: list[PlayerState], locations_weighted: WeightedLocations) -> Optional[PlayerState]: """ Gets the next player a pickup should be placed for. :param rng: :param player_states: :param locations_weighted: Which locations are available and their weight. :return: """ all_uncollected: dict[PlayerState, UncollectedState] = { player_state: UncollectedState.from_reach(player_state.reach) for player_state in player_states } max_actions = max(player_state.num_actions for player_state in player_states) max_uncollected = max( len(uncollected.indices) for uncollected in all_uncollected.values()) def _calculate_weight(player: PlayerState) -> float: return 1 + (max_actions - player.num_actions) * ( max_uncollected - len(all_uncollected[player].indices)) weighted_players = { player_state: _calculate_weight(player_state) for player_state in player_states if not player_state.victory_condition_satisfied() and player_state.potential_actions(locations_weighted) } if weighted_players: if debug.debug_level() > 1: print(f">>>>> Player Weights: {weighted_players}") return select_element_with_weight(weighted_players, rng) else: if all(player_state.victory_condition_satisfied() for player_state in player_states): debug.debug_print("Finished because we can win") return None else: total_actions = sum(player_state.num_actions for player_state in player_states) unfinished_players = ", ".join([ str(player_state) for player_state in player_states if not player_state.victory_condition_satisfied() ]) raise UnableToGenerate( f"{unfinished_players} with no possible actions after {total_actions} total actions." )
def weighted_potential_actions( player_state: PlayerState, status_update: Callable[[str], None], num_available_indices: int) -> Dict[Action, float]: """ Weights all potential actions based on current criteria. :param player_state: :param status_update: :param num_available_indices: The number of indices available for placement. :return: """ actions_weights: Dict[Action, float] = {} current_uncollected = UncollectedState.from_reach(player_state.reach) actions = player_state.potential_actions(num_available_indices) options_considered = 0 def update_for_option(): nonlocal options_considered options_considered += 1 status_update("Checked {} of {} options.".format( options_considered, len(actions))) for action in actions: if isinstance(action, tuple): pickups = typing.cast(Tuple[PickupEntry, ...], action) base_weight = _calculate_weights_for( _calculate_reach_for_progression(player_state.reach, pickups), current_uncollected) multiplier = sum(pickup.probability_multiplier for pickup in pickups) / len(pickups) offset = sum(pickup.probability_offset for pickup in pickups) weight = (base_weight * multiplier + offset) / len(pickups) else: weight = _calculate_weights_for( advance_to_with_reach_copy( player_state.reach, player_state.reach.state.act_on_node(action)), current_uncollected) actions_weights[action] = weight update_for_option() if debug.debug_level() > 1: for action, weight in actions_weights.items(): print("({}) {} - {}".format( type(action).__name__, action_name(action), weight)) return actions_weights
def weighted_potential_actions( player_state: PlayerState, status_update: Callable[[str], None], locations_weighted: WeightedLocations) -> dict[Action, float]: """ Weights all potential actions based on current criteria. :param player_state: :param status_update: :param locations_weighted: Which locations are available and their weight. :return: """ actions_weights: dict[Action, float] = {} current_uncollected = UncollectedState.from_reach(player_state.reach) actions = player_state.potential_actions(locations_weighted) options_considered = 0 def update_for_option(): nonlocal options_considered options_considered += 1 status_update("Checked {} of {} options.".format( options_considered, len(actions))) for action in actions: if isinstance(action, tuple): pickups = typing.cast(tuple[PickupEntry, ...], action) base_weight = _calculate_weights_for( _calculate_reach_for_progression(player_state.reach, pickups), current_uncollected) multiplier = sum(pickup.probability_multiplier for pickup in pickups) / len(pickups) offset = sum(pickup.probability_offset for pickup in pickups) weight = (base_weight * multiplier + offset) / len(pickups) else: weight = _calculate_weights_for( reach_lib.advance_to_with_reach_copy( player_state.reach, player_state.reach.state.act_on_node(action)), current_uncollected) * _DANGEROUS_ACTION_MULTIPLIER actions_weights[action] = weight update_for_option() if debug.debug_level() > 1: for action, weight in actions_weights.items(): print("{} - {}".format(action_name(action), weight)) return actions_weights
def print_retcon_place_pickup(action: PickupEntry, game: GameDescription, pickup_index: PickupIndex, hint: Optional[LogbookAsset]): world_list = game.world_list if debug.debug_level() > 0: if hint is not None: hint_string = " with hint at {}".format( world_list.node_name(find_node_with_resource( hint, world_list.all_nodes), with_world=True)) else: hint_string = "" print("\n--> Placing {0} at {1}{2}".format( action.name, world_list.node_name(find_node_with_resource( pickup_index, world_list.all_nodes), with_world=True), hint_string))
def _calculate_potential_actions(reach: GeneratorReach, progression_pickups: Tuple[PickupEntry, ...], current_uncollected: UncollectedState, free_starting_items_spots: int, status_update: Callable[[str], None]): actions_weights: Dict[Action, float] = {} uncollected_resource_nodes = get_collectable_resource_nodes_of_reach(reach) total_options = len(uncollected_resource_nodes) options_considered = 0 def update_for_option(): nonlocal options_considered options_considered += 1 status_update("Checked {} of {} options.".format( options_considered, total_options)) usable_progression_pickups = [ progression for progression in progression_pickups if _items_for_pickup(progression) <= len(current_uncollected.indices) or (not uncollected_resource_nodes and _items_for_pickup(progression) <= free_starting_items_spots) ] total_options += len(usable_progression_pickups) for progression in usable_progression_pickups: actions_weights[progression] = _calculate_weights_for( _calculate_reach_for_progression(reach, progression), current_uncollected, progression.name) + progression.probability_offset update_for_option() for resource in uncollected_resource_nodes: actions_weights[resource] = _calculate_weights_for( advance_to_with_reach_copy(reach, reach.state.act_on_node(resource)), current_uncollected, resource.name) update_for_option() if debug.debug_level() > 1: for action, weight in actions_weights.items(): print("{} - {}".format(action.name, weight)) return actions_weights
def weighted_potential_actions( self, status_update: Callable[[str], None], ) -> Dict[Action, float]: """ Weights all potential actions based on current criteria. :param status_update: :return: """ actions_weights: Dict[Action, float] = {} current_uncollected = UncollectedState.from_reach(self.reach) total_options = len(self.potential_actions) options_considered = 0 def update_for_option(): nonlocal options_considered options_considered += 1 status_update("Checked {} of {} options.".format( options_considered, total_options)) for action in self.potential_actions: if isinstance(action, PickupEntry): base_weight = _calculate_weights_for( _calculate_reach_for_progression(self.reach, action), current_uncollected, action.name) weight = base_weight * action.probability_multiplier + action.probability_offset else: weight = _calculate_weights_for( advance_to_with_reach_copy( self.reach, self.reach.state.act_on_node(action)), current_uncollected, action.name) actions_weights[action] = weight update_for_option() if debug.debug_level() > 1: for action, weight in actions_weights.items(): print("({}) {} - {}".format( type(action).__name__, action.name, weight)) return actions_weights
def generate_description( parameters: GeneratorParameters, status_update: Callable[[str], None], validate_after_generation: bool, timeout_during_generation: bool, attempts: Optional[int], ) -> LayoutDescription: receiving_pipe, output_pipe = multiprocessing.Pipe(True) debug_level = debug.debug_level() if not parameters.spoiler: debug_level = 0 def on_done(_): output_pipe.send(None) with ProcessPoolExecutor(max_workers=1) as executor: extra_args = { "generator_params": parameters, "validate_after_generation": validate_after_generation, } if not timeout_during_generation: extra_args["timeout"] = None if attempts is not None: extra_args["attempts"] = attempts future = executor.submit(_generate_layout_worker, output_pipe, debug_level, extra_args) future.add_done_callback(on_done) while not future.done(): message = receiving_pipe.recv() if message is not None: try: status_update(message) except Exception: receiving_pipe.send("close") raise return future.result()
def debug_print_collect_event(action, game): if debug.debug_level() > 0: print("\n--> Collecting {}".format(game.world_list.node_name(action, with_world=True)))
def fill_unassigned_hints(patches: GamePatches, world_list: WorldList, rng: Random, scan_asset_initial_pickups: Dict[LogbookAsset, FrozenSet[PickupIndex]], ) -> GamePatches: new_hints = copy.copy(patches.hints) # Get all LogbookAssets from the WorldList potential_hint_locations: Set[LogbookAsset] = { node.resource() for node in world_list.all_nodes if isinstance(node, LogbookNode) } for logbook in potential_hint_locations: if logbook not in scan_asset_initial_pickups: scan_asset_initial_pickups[logbook] = frozenset() # But remove these that already have hints potential_hint_locations -= patches.hints.keys() # Get interesting items to place hints for possible_indices = set(patches.pickup_assignment.keys()) possible_indices -= {hint.target for hint in patches.hints.values() if hint.target is not None} possible_indices -= {index for index in possible_indices if not should_have_hint(patches.pickup_assignment[index].pickup.item_category)} debug.debug_print("fill_unassigned_hints had {} decent indices for {} hint locations".format( len(possible_indices), len(potential_hint_locations))) if debug.debug_level() > 1: print(f"> Num pickups per asset:") for asset, pickups in scan_asset_initial_pickups.items(): print(f"* {asset}: {len(pickups)} pickups") print("> Done.") # But if we don't have enough hints, just pick randomly from everything if len(possible_indices) < len(potential_hint_locations): possible_indices = {node.pickup_index for node in world_list.all_nodes if isinstance(node, PickupNode)} # Get an stable order ordered_possible_indices = list(sorted(possible_indices)) ordered_potential_hint_locations = list(sorted(potential_hint_locations)) num_logbooks: Dict[PickupIndex, int] = { index: sum(1 for indices in scan_asset_initial_pickups.values() if index in indices) for index in ordered_possible_indices } max_seen = max(num_logbooks.values()) pickup_indices_weight: Dict[PickupIndex, int] = { index: max_seen - num_logbook for index, num_logbook in num_logbooks.items() } # Ensure all indices are present with at least weight 0 for index in ordered_possible_indices: if index not in pickup_indices_weight: pickup_indices_weight[index] = 0 for logbook in sorted(ordered_potential_hint_locations, key=lambda r: len(scan_asset_initial_pickups[r]), reverse=True): try: new_index = random_lib.select_element_with_weight(pickup_indices_weight, rng) except StopIteration: # If everything has weight 0, then just choose randomly. new_index = random_lib.random_key(pickup_indices_weight, rng) del pickup_indices_weight[new_index] new_hints[logbook] = Hint(HintType.LOCATION, None, new_index) debug.debug_print(f"Added hint at {logbook} for item at {new_index}") return dataclasses.replace(patches, hints=new_hints)
def _assign_pickup_somewhere( action: PickupEntry, current_player: PlayerState, player_states: list[PlayerState], rng: Random, all_locations_weighted: WeightedLocations, ) -> str: """ Assigns a PickupEntry to a free, collected PickupIndex or as a starting item. :param action: :param current_player: :param player_states: :param rng: :return: """ assert action in current_player.pickups_left locations_weighted = current_player.filter_usable_locations( all_locations_weighted) if locations_weighted and ( current_player.num_random_starting_items_placed >= current_player.configuration.minimum_random_starting_items): if debug.debug_level() > 1: debug_print_weighted_locations(all_locations_weighted) index_owner_state, pickup_index = select_element_with_weight( locations_weighted, rng) index_owner_state.assign_pickup( pickup_index, PickupTarget(action, current_player.index)) all_locations_weighted.pop((index_owner_state, pickup_index)) # Place a hint for the new item hint_location = _calculate_hint_location_for_action( action, index_owner_state, all_locations_weighted, UncollectedState.from_reach(index_owner_state.reach), pickup_index, rng, index_owner_state.hint_initial_pickups, ) if hint_location is not None: index_owner_state.reach.state.patches = index_owner_state.reach.state.patches.assign_hint( hint_location, Hint(HintType.LOCATION, None, pickup_index)) if pickup_index in index_owner_state.reach.state.collected_pickup_indices: current_player.reach.advance_to( current_player.reach.state.assign_pickup_resources(action)) else: # FIXME: isn't that condition always true? pass spoiler_entry = pickup_placement_spoiler_entry( current_player.index, action, index_owner_state.game, pickup_index, hint_location, index_owner_state.index, len(player_states) > 1, index_owner_state.reach.node_context()) else: current_player.num_random_starting_items_placed += 1 if (current_player.num_random_starting_items_placed > current_player.configuration.maximum_random_starting_items): raise UnableToGenerate( "Attempting to place more extra starting items than the number allowed." ) spoiler_entry = f"{action.name} as starting item" if len(player_states) > 1: spoiler_entry += f" for Player {current_player.index + 1}" current_player.reach.advance_to( current_player.reach.state.assign_pickup_to_starting_items(action)) return spoiler_entry
def debug_print_collect_event(event: ResourceNode, game: GameDescription): if debug.debug_level() > 0: print("\n--> Collecting {}".format(game.world_list.node_name(event, with_world=True)))
def retcon_playthrough_filler( game: GameDescription, initial_state: State, pickups_left: List[PickupEntry], rng: Random, configuration: FillerConfiguration, status_update: Callable[[str], None], ) -> GamePatches: debug.debug_print("{}\nRetcon filler started with major items:\n{}".format( "*" * 100, pprint.pformat({ item.name: pickups_left.count(item) for item in sorted(set(pickups_left), key=lambda item: item.name) }))) last_message = "Starting." minimum_random_starting_items = configuration.minimum_random_starting_items maximum_random_starting_items = configuration.maximum_random_starting_items reach = advance_reach_with_possible_unsafe_resources( reach_with_all_safe_resources(game, initial_state)) pickup_index_seen_count: DefaultDict[PickupIndex, int] = collections.defaultdict(int) scan_asset_seen_count: DefaultDict[LogbookAsset, int] = collections.defaultdict(int) scan_asset_initial_pickups: Dict[LogbookAsset, FrozenSet[PickupIndex]] = {} num_random_starting_items_placed = 0 indices_groups, all_indices = build_available_indices( game.world_list, configuration) while pickups_left: current_uncollected = UncollectedState.from_reach(reach) progression_pickups = _calculate_progression_pickups( pickups_left, reach) print_retcon_loop_start(current_uncollected, game, pickups_left, reach) for pickup_index in reach.state.collected_pickup_indices: pickup_index_seen_count[pickup_index] += 1 print_new_resources(game, reach, pickup_index_seen_count, "Pickup Index") for scan_asset in reach.state.collected_scan_assets: scan_asset_seen_count[scan_asset] += 1 if scan_asset_seen_count[scan_asset] == 1: scan_asset_initial_pickups[scan_asset] = frozenset( reach.state.collected_pickup_indices) print_new_resources(game, reach, scan_asset_seen_count, "Scan Asset") def action_report(message: str): status_update("{} {}".format(last_message, message)) actions_weights = _calculate_potential_actions( reach, progression_pickups, current_uncollected, maximum_random_starting_items - num_random_starting_items_placed, action_report) try: action = next( iterate_with_weights(items=list(actions_weights.keys()), item_weights=actions_weights, rng=rng)) except StopIteration: if actions_weights: action = rng.choice(list(actions_weights.keys())) else: raise UnableToGenerate( "Unable to generate; no actions found after placing {} items." .format(len(reach.state.patches.pickup_assignment))) if isinstance(action, PickupEntry): assert action in pickups_left uncollected_indices = current_uncollected.indices & all_indices if num_random_starting_items_placed >= minimum_random_starting_items and uncollected_indices: pickup_index_weights = _calculate_uncollected_index_weights( uncollected_indices, set(reach.state.patches.pickup_assignment), pickup_index_seen_count, indices_groups, ) assert pickup_index_weights, "Pickups should only be added to the actions dict " \ "when there are unassigned pickups" pickup_index = next( iterate_with_weights(items=iter(uncollected_indices), item_weights=pickup_index_weights, rng=rng)) next_state = reach.state.assign_pickup_to_index( action, pickup_index) # Place a hint for the new item hint_location = _calculate_hint_location_for_action( action, current_uncollected, pickup_index, rng, scan_asset_initial_pickups) if hint_location is not None: next_state.patches = next_state.patches.assign_hint( hint_location, Hint(HintType.LOCATION, None, pickup_index)) print_retcon_place_pickup(action, game, pickup_index, hint_location) else: num_random_starting_items_placed += 1 if num_random_starting_items_placed > maximum_random_starting_items: raise UnableToGenerate( "Attempting to place more extra starting items than the number allowed." ) if debug.debug_level() > 1: print(f"\n--> Adding {action.name} as a starting item") next_state = reach.state.assign_pickup_to_starting_items( action) # TODO: this item is potentially dangerous and we should remove the invalidated paths pickups_left.remove(action) last_message = "Placed {} items so far, {} left.".format( len(next_state.patches.pickup_assignment), len(pickups_left) - 1) status_update(last_message) reach.advance_to(next_state) else: last_message = "Triggered an event out of {} options.".format( len(actions_weights)) status_update(last_message) debug_print_collect_event(action, game) # This action is potentially dangerous. Use `act_on` to remove invalid paths reach.act_on(action) reach = advance_reach_with_possible_unsafe_resources(reach) if game.victory_condition.satisfied(reach.state.resources, reach.state.energy): debug.debug_print("Finished because we can win") break if not pickups_left: debug.debug_print( "Finished because we have nothing else to distribute") return reach.state.patches
def retcon_playthrough_filler( game: GameDescription, initial_state: State, pickups_left: List[PickupEntry], rng: Random, randomization_mode: RandomizationMode, minimum_random_starting_items: int, maximum_random_starting_items: int, status_update: Callable[[str], None], ) -> GamePatches: debug.debug_print("Major items: {}".format( [item.name for item in pickups_left])) last_message = "Starting." reach = advance_reach_with_possible_unsafe_resources( reach_with_all_safe_resources(game, initial_state)) pickup_index_seen_count: Dict[PickupIndex, int] = collections.defaultdict(int) scan_asset_seen_count: Dict[LogbookAsset, int] = collections.defaultdict(int) num_random_starting_items_placed = 0 while pickups_left: current_uncollected = UncollectedState.from_reach(reach) progression_pickups = _calculate_progression_pickups( pickups_left, reach) print_retcon_loop_start(current_uncollected, game, pickups_left, reach) for pickup_index in reach.state.collected_pickup_indices: pickup_index_seen_count[pickup_index] += 1 print_new_resources(game, reach, pickup_index_seen_count, "Pickup Index") for scan_asset in reach.state.collected_scan_assets: scan_asset_seen_count[scan_asset] += 1 print_new_resources(game, reach, scan_asset_seen_count, "Scan Asset") def action_report(message: str): status_update("{} {}".format(last_message, message)) actions_weights = _calculate_potential_actions( reach, progression_pickups, current_uncollected, maximum_random_starting_items - num_random_starting_items_placed, action_report) try: action = next( iterate_with_weights(items=list(actions_weights.keys()), item_weights=actions_weights, rng=rng)) except StopIteration: if actions_weights: action = rng.choice(list(actions_weights.keys())) else: raise UnableToGenerate( "Unable to generate; no actions found after placing {} items." .format(len(reach.state.patches.pickup_assignment))) if isinstance(action, PickupEntry): assert action in pickups_left if randomization_mode is RandomizationMode.FULL: uncollected_indices = current_uncollected.indices elif randomization_mode is RandomizationMode.MAJOR_MINOR_SPLIT: major_indices = { pickup_node.pickup_index for pickup_node in filter_pickup_nodes( reach.state.collected_resource_nodes) if pickup_node.major_location } uncollected_indices = current_uncollected.indices & major_indices if num_random_starting_items_placed >= minimum_random_starting_items and uncollected_indices: pickup_index_weight = { pickup_index: 1 / (min(pickup_index_seen_count[pickup_index], 10)**2) for pickup_index in uncollected_indices } assert pickup_index_weight, "Pickups should only be added to the actions dict " \ "when there are unassigned pickups" pickup_index = next( iterate_with_weights(items=uncollected_indices, item_weights=pickup_index_weight, rng=rng)) next_state = reach.state.assign_pickup_to_index( action, pickup_index) if current_uncollected.logbooks and _should_have_hint( action.item_category): hint_location: Optional[LogbookAsset] = rng.choice( list(current_uncollected.logbooks)) next_state.patches = next_state.patches.assign_hint( hint_location, Hint(HintType.LOCATION, None, pickup_index)) else: hint_location = None print_retcon_place_pickup(action, game, pickup_index, hint_location) else: num_random_starting_items_placed += 1 if num_random_starting_items_placed > maximum_random_starting_items: raise UnableToGenerate( "Attempting to place more extra starting items than the number allowed." ) if debug.debug_level() > 1: print(f"\n--> Adding {action.name} as a starting item") next_state = reach.state.assign_pickup_to_starting_items( action) # TODO: this item is potentially dangerous and we should remove the invalidated paths pickups_left.remove(action) last_message = "Placed {} items so far, {} left.".format( len(next_state.patches.pickup_assignment), len(pickups_left) - 1) status_update(last_message) reach.advance_to(next_state) else: last_message = "Triggered an event out of {} options.".format( len(actions_weights)) status_update(last_message) debug_print_collect_event(action, game) # This action is potentially dangerous. Use `act_on` to remove invalid paths reach.act_on(action) reach = advance_reach_with_possible_unsafe_resources(reach) if game.victory_condition.satisfied(reach.state.resources, reach.state.energy): debug.debug_print("Finished because we can win") break if not pickups_left: debug.debug_print( "Finished because we have nothing else to distribute") return reach.state.patches