Example #1
0
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

    if all_locations_weighted and (current_player.num_random_starting_items_placed
                                   >= current_player.configuration.minimum_random_starting_items):

        index_owner_state, pickup_index = select_element_with_weight(all_locations_weighted, rng)
        index_owner_state.assign_pickup(pickup_index, PickupTarget(action, current_player.index))

        # Place a hint for the new item
        hint_location = _calculate_hint_location_for_action(
            action,
            UncollectedState.from_reach(index_owner_state.reach),
            pickup_index,
            rng,
            index_owner_state.scan_asset_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)

    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
Example #2
0
def add_relative_hint(world_list: WorldList,
                      patches: GamePatches,
                      rng: Random,
                      target: PickupIndex,
                      target_precision: HintItemPrecision,
                      relative_type: HintLocationPrecision,
                      precise_distance: bool,
                      precision: Union[HintItemPrecision, HintRelativeAreaName],
                      max_distance: int,
                      ) -> Optional[Hint]:
    """
    Creates a relative hint.
    :return: Might be None, if no hint could be created.
    """
    target_node = node_search.pickup_index_to_node(world_list, target)
    target_area = world_list.nodes_to_area(target_node)
    distances = node_search.distances_to_node(world_list, target_node, patches=patches, cutoff=max_distance)

    def _major_pickups(area: Area) -> Iterator[PickupIndex]:
        for index in area.pickup_indices:
            t = patches.pickup_assignment.get(index)
            # FIXME: None should be ok, but this must be called after junk has been filled
            if t is not None:
                cat = t.pickup.item_category
                if cat.is_major_category or (cat != ItemCategory.EXPANSION
                                             and target_precision == HintItemPrecision.DETAILED):
                    yield index

    area_choices = {
        area: 1 / max(distance, 2)
        for area, distance in distances.items()
        if (distance > 0 and area.in_dark_aether == target_area.in_dark_aether
            and (relative_type == HintLocationPrecision.RELATIVE_TO_AREA or _not_empty(_major_pickups(area))))
    }
    if not area_choices:
        return None
    area = random_lib.select_element_with_weight(dict(sorted(area_choices.items(),
                                                             key=lambda a: a[0].area_asset_id)), rng)

    distance_offset = 0
    if not precise_distance:
        distance_offset = max_distance - distances[area]

    if relative_type == HintLocationPrecision.RELATIVE_TO_AREA:
        relative = RelativeDataArea(distance_offset, world_list.area_to_area_location(area),
                                    precision)
    elif relative_type == HintLocationPrecision.RELATIVE_TO_INDEX:
        relative = RelativeDataItem(distance_offset, rng.choice(list(_major_pickups(area))), precision)
    else:
        raise ValueError(f"Invalid relative_type: {relative_type}")

    precision_pair = PrecisionPair(relative_type, target_precision, include_owner=False, relative=relative)
    return Hint(HintType.LOCATION, precision_pair, target)
Example #3
0
def _get_next_player(
        rng: Random,
        player_states: List[PlayerState]) -> Optional[PlayerState]:
    """
    Gets the next player a pickup should be placed for.
    :param rng:
    :param player_states:
    :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
    }
    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)
            raise UnableToGenerate(
                f"No players with possible actions after {total_actions} total actions."
            )
Example #4
0
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)
Example #5
0
def retcon_playthrough_filler(
    rng: Random,
    player_states: List[PlayerState],
    status_update: Callable[[str], None],
) -> Tuple[Dict[PlayerState, GamePatches], Tuple[str, ...]]:
    """
    Runs the retcon logic.
    :param rng:
    :param player_states:
    :param status_update:
    :return: A GamePatches for each player and a sequence of placed items.
    """
    debug.debug_print("{}\nRetcon filler started with major items:\n{}".format(
        "*" * 100, "\n".join("Player {}: {}".format(
            player_state.index,
            pprint.pformat({
                item.name: player_state.pickups_left.count(item)
                for item in sorted(set(player_state.pickups_left),
                                   key=lambda item: item.name)
            })) for player_state in player_states)))
    last_message = "Starting."

    def action_report(message: str):
        status_update("{} {}".format(last_message, message))

    for player_state in player_states:
        player_state.update_for_new_state()

    actions_log = []

    while True:
        current_player = _get_next_player(rng, player_states)
        if current_player is None:
            break

        weighted_actions = current_player.weighted_potential_actions(
            action_report)
        try:
            action = select_element_with_weight(weighted_actions, rng=rng)
        except StopIteration:
            # All actions had weight 0. Select one randomly instead.
            # No need to check if potential_actions is empty, _get_next_player only return players with actions
            action = rng.choice(current_player.potential_actions)

        if isinstance(action, PickupEntry):
            log_entry = _assign_pickup_somewhere(action, current_player,
                                                 player_states, rng)
            actions_log.append(log_entry)
            debug.debug_print(f"\n>>>> {log_entry}")

            # TODO: this item is potentially dangerous and we should remove the invalidated paths
            current_player.pickups_left.remove(action)
            current_player.num_actions += 1

            count_pickups_left = sum(
                len(player_state.pickups_left)
                for player_state in player_states)
            last_message = "{} items left.".format(count_pickups_left)
            status_update(last_message)

        else:
            last_message = "Triggered an event out of {} options.".format(
                len(weighted_actions))
            status_update(last_message)
            debug_print_collect_event(action, current_player.game)

            # This action is potentially dangerous. Use `act_on` to remove invalid paths
            current_player.reach.act_on(action)

        current_player.reach = advance_reach_with_possible_unsafe_resources(
            current_player.reach)
        current_player.update_for_new_state()

    all_patches = {
        player_state: player_state.reach.state.patches
        for player_state in player_states
    }
    return all_patches, tuple(actions_log)