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 current_state_report(self) -> str: state = UncollectedState.from_reach(self.reach) pickups_by_name_and_quantity = collections.defaultdict(int) _KEY_MATCH = re.compile(r"Key (\d+)") for pickup in self.pickups_left: pickups_by_name_and_quantity[_KEY_MATCH.sub("Key", pickup.name)] += 1 to_progress = { _KEY_MATCH.sub("Key", resource.long_name) for resource in interesting_resources_for_reach(self.reach) if resource.resource_type == ResourceType.ITEM } return ( "At {0} after {1} actions and {2} pickups, with {3} collected locations.\n\n" "Pickups still available: {4}\n\nResources to progress: {5}" ).format( self.game.world_list.node_name(self.reach.state.node, with_world=True, distinguish_dark_aether=True), self.num_actions, self.num_assigned_pickups, len(state.indices), ", ".join(name if quantity == 1 else f"{name} x{quantity}" for name, quantity in sorted( pickups_by_name_and_quantity.items())), ", ".join(sorted(to_progress)), )
def _calculate_all_pickup_indices_weight( player_states: list[PlayerState]) -> WeightedLocations: all_weights = {} total_assigned_pickups = sum(player_state.num_assigned_pickups for player_state in player_states) # print("================ WEIGHTS! ==================") for player_state in player_states: delta = (total_assigned_pickups - player_state.num_assigned_pickups) player_weight = 1 + delta # print(f"** Player {player_state.index} -- {player_weight}") pickup_index_weights = _calculate_uncollected_index_weights( player_state.all_indices & UncollectedState.from_reach(player_state.reach).indices, set(player_state.reach.state.patches.pickup_assignment), player_state.pickup_index_seen_count, player_state.indices_groups, ) for pickup_index, weight in pickup_index_weights.items(): all_weights[(player_state, pickup_index)] = weight * player_weight # for (player_state, pickup_index), weight in all_weights.items(): # print(f"> {player_state.index} - {pickup_index}: {weight}") # print("============================================") return all_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: 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 _calculate_weights_for( potential_reach: GeneratorReach, current_uncollected: UncollectedState, ) -> float: if potential_reach.victory_condition_satisfied(): return _VICTORY_WEIGHT potential_uncollected = UncollectedState.from_reach( potential_reach) - current_uncollected return sum(( _EVENTS_WEIGHT_MULTIPLIER * int(bool(potential_uncollected.events)), _INDICES_WEIGHT_MULTIPLIER * int(bool(potential_uncollected.indices)), _LOGBOOKS_WEIGHT_MULTIPLIER * int(bool(potential_uncollected.logbooks)), ))
def _calculate_weights_for( potential_reach: GeneratorReach, current_uncollected: UncollectedState, ) -> float: if potential_reach.game.victory_condition.satisfied( potential_reach.state.resources, potential_reach.state.energy): return _VICTORY_WEIGHT potential_uncollected = UncollectedState.from_reach( potential_reach) - current_uncollected return sum(( _RESOURCES_WEIGHT_MULTIPLIER * int(bool(potential_uncollected.resources)), _INDICES_WEIGHT_MULTIPLIER * int(bool(potential_uncollected.indices)), _LOGBOOKS_WEIGHT_MULTIPLIER * int(bool(potential_uncollected.logbooks)), ))
def current_state_report(self) -> str: state = UncollectedState.from_reach(self.reach) pickups_by_name_and_quantity = collections.defaultdict(int) _KEY_MATCH = re.compile(r"Key (\d+)") for pickup in self.pickups_left: pickups_by_name_and_quantity[_KEY_MATCH.sub("Key", pickup.name)] += 1 to_progress = { _KEY_MATCH.sub("Key", resource.long_name) for resource in interesting_resources_for_reach(self.reach) if resource.resource_type == ResourceType.ITEM } wl = self.reach.game.world_list s = self.reach.state paths_to_be_opened = set() for node, requirement in self.reach.unreachable_nodes_with_requirements( ).items(): for alternative in requirement.alternatives: if any(r.negate or ( r.resource.resource_type != ResourceType.ITEM and not r.satisfied(s.resources, s.energy, self.game.resource_database)) for r in alternative.values()): continue paths_to_be_opened.add("* {}: {}".format( wl.node_name(node, with_world=True), " and ".join( sorted( r.pretty_text for r in alternative.values() if not r.satisfied(s.resources, s.energy, self.game.resource_database))))) teleporters = [] for node in wl.iterate_nodes(): if isinstance( node, TeleporterNode) and self.reach.is_reachable_node(node): other = wl.resolve_teleporter_node(node, s.patches) teleporters.append("* {} to {}".format( elevators.get_elevator_or_area_name( self.game.game, wl, wl.identifier_for_node(node).area_location, True), elevators.get_elevator_or_area_name( self.game.game, wl, wl.identifier_for_node(other).area_location, True) if other is not None else "<Not connected>", )) accessible_nodes = [ wl.node_name(n, with_world=True) for n in self.reach.iterate_nodes if self.reach.is_reachable_node(n) ] return ( "At {0} after {1} actions and {2} pickups, with {3} collected locations, {7} safe nodes.\n\n" "Pickups still available: {4}\n\n" "Resources to progress: {5}\n\n" "Paths to be opened:\n{8}\n\n" "Accessible teleporters:\n{9}\n\n" "Reachable nodes:\n{6}").format( self.game.world_list.node_name(self.reach.state.node, with_world=True, distinguish_dark_aether=True), self.num_actions, self.num_assigned_pickups, len(state.indices), ", ".join(name if quantity == 1 else f"{name} x{quantity}" for name, quantity in sorted( pickups_by_name_and_quantity.items())), ", ".join(sorted(to_progress)), "\n".join(accessible_nodes) if len(accessible_nodes) < 15 else f"{len(accessible_nodes)} nodes total", sum(1 for n in self.reach.iterate_nodes if self.reach.is_safe_node(n)), "\n".join(sorted(paths_to_be_opened)) or "None", "\n".join(teleporters) or "None", )
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