コード例 #1
0
    def update_for_new_state(self):
        debug.debug_print(f"\n>>> Updating state of {self}")

        self._advance_pickup_index_seen_count()
        self._advance_scan_asset_seen_count()
        self._advance_event_seen_count()
        self._calculate_potential_actions()
コード例 #2
0
    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])
コード例 #3
0
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."
            )
コード例 #4
0
ファイル: runner.py プロジェクト: cdoggers/randovania
def fill_unassigned_hints(
    patches: GamePatches,
    world_list: WorldList,
    rng: Random,
) -> 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)
    }

    # 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()}
    possible_indices -= {
        index
        for index in possible_indices
        if not should_have_hint(patches.pickup_assignment[index].item_category)
    }

    debug.debug_print(
        "fill_unassigned_hints had {} decent indices for {} hint locations".
        format(len(possible_indices), len(potential_hint_locations)))

    # 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 then shuffle
    possible_indices = list(sorted(possible_indices))
    rng.shuffle(possible_indices)

    for logbook in sorted(potential_hint_locations):
        new_hints[logbook] = Hint(HintType.LOCATION, None,
                                  possible_indices.pop())
        debug.debug_print(
            f"Added hint at {logbook} for item at {new_hints[logbook].target}")

    return dataclasses.replace(patches, hints=new_hints)
コード例 #5
0
ファイル: player_state.py プロジェクト: randovania/randovania
    def should_have_hint(self, pickup: PickupEntry,
                         current_uncollected: UncollectedState,
                         all_locations_weighted: WeightedLocations) -> bool:

        if not pickup.item_category.is_major:
            return False

        config = self.configuration
        valid_locations = [
            index for (owner, index), weight in all_locations_weighted.items()
            if (owner == self
                and weight >= config.minimum_location_weight_for_hint_placement
                and index in current_uncollected.indices)
        ]
        can_hint = len(
            valid_locations
        ) >= config.minimum_available_locations_for_hint_placement
        if not can_hint:
            debug.debug_print(
                f"+ Only {len(valid_locations)} qualifying open locations, hint refused."
            )

        return can_hint
コード例 #6
0
ファイル: retcon.py プロジェクト: JaggerTSG/randovania
def retcon_playthrough_filler(rng: Random,
                              player_states: Dict[int, PlayerState],
                              status_update: Callable[[str], None],
                              ) -> Tuple[Dict[int, 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_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_index, player_state in player_states.items()
        )
    ))
    last_message = "Starting."

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

    for player_state in player_states.values():
        player_state.advance_pickup_index_seen_count()
        player_state.advance_scan_asset_seen_count()

    players_to_check = []
    actions_log = []

    while True:
        if not players_to_check:
            players_to_check = [player_index for player_index, player_state in player_states.items()
                                if not player_state.victory_condition_satisfied()]
            if not players_to_check:
                debug.debug_print("Finished because we can win")
                break
            rng.shuffle(players_to_check)
        player_to_check = players_to_check.pop()

        current_player = player_states[player_to_check]
        actions_weights = current_player.calculate_potential_actions(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(current_player.reach.state.patches.pickup_assignment)))

        if isinstance(action, PickupEntry):
            assert action in current_player.pickups_left

            all_weights = _calculate_all_pickup_indices_weight(player_states)

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

                player_index, pickup_index = next(iterate_with_weights(items=iter(all_weights.keys()),
                                                                       item_weights=all_weights,
                                                                       rng=rng))

                index_owner_state = player_states[player_index]
                index_owner_state.reach.state.patches = index_owner_state.reach.state.patches.assign_new_pickups([
                    (pickup_index, PickupTarget(action, player_to_check)),
                ])

                # 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))

                spoiler_entry = pickup_placement_spoiler_entry(player_to_check, action, index_owner_state.game,
                                                               pickup_index, hint_location, player_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 {player_to_check + 1}"

                current_player.reach.advance_to(current_player.reach.state.assign_pickup_to_starting_items(action))

            actions_log.append(spoiler_entry)
            debug.debug_print(f"\n>>>> {spoiler_entry}")

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

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

        else:
            last_message = "Triggered an event out of {} options.".format(len(actions_weights))
            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.advance_pickup_index_seen_count()
        current_player.advance_scan_asset_seen_count()

    return {
        index: player_state.reach.state.patches
        for index, player_state in player_states.items()
    }, tuple(actions_log)
コード例 #7
0
def run_filler(rng: Random,
               player_pools: Dict[int, PlayerPool],
               status_update: Callable[[str], None],
               ) -> FillerResults:
    """
    Runs the filler logic for the given configuration and item pool.
    Returns a GamePatches with progression items and hints assigned, along with all items in the pool
    that weren't assigned.

    :param player_pools:
    :param rng:
    :param status_update:
    :return:
    """

    player_states = []
    player_expansions: Dict[int, List[PickupEntry]] = {}

    for index, pool in player_pools.items():
        status_update(f"Creating state for player {index + 1}")
        major_items, player_expansions[index] = _split_expansions(pool.pickups)
        rng.shuffle(major_items)
        rng.shuffle(player_expansions[index])

        new_game, state = bootstrap.logic_bootstrap(pool.configuration, pool.game, pool.patches)
        new_game.patch_requirements(state.resources, pool.configuration.damage_strictness.value)

        major_configuration = pool.configuration.major_items_configuration
        player_states.append(PlayerState(
            index=index,
            game=new_game,
            initial_state=state,
            pickups_left=major_items,
            configuration=FillerConfiguration(
                randomization_mode=pool.configuration.available_locations.randomization_mode,
                minimum_random_starting_items=major_configuration.minimum_random_starting_items,
                maximum_random_starting_items=major_configuration.maximum_random_starting_items,
                indices_to_exclude=pool.configuration.available_locations.excluded_indices,
            ),
        ))

    try:
        filler_result, actions_log = retcon_playthrough_filler(rng, player_states, status_update=status_update)
    except UnableToGenerate as e:
        message = "{}\n\n{}".format(
            str(e),
            "\n\n".join(
                "#### Player {}\n{}".format(player.index + 1, player.current_state_report())
                for player in player_states
            ),
        )
        debug.debug_print(message)
        raise UnableToGenerate(message) from e

    results = {}

    for player_state, patches in filler_result.items():
        game = player_state.game

        if game.game == RandovaniaGame.PRIME2:
            # Since we haven't added expansions yet, these hints will always be for items added by the filler.
            full_hints_patches = fill_unassigned_hints(patches, game.world_list, rng,
                                                       player_state.scan_asset_initial_pickups)

            if player_pools[player_state.index].configuration.hints.item_hints:
                result = add_hints_precision(player_state, full_hints_patches, rng)
            else:
                result = replace_hints_without_precision_with_jokes(full_hints_patches)
        else:
            result = patches

        results[player_state.index] = FillerPlayerResult(
            game=game,
            patches=result,
            unassigned_pickups=player_state.pickups_left + player_expansions[player_state.index],
        )

    return FillerResults(results, actions_log)
コード例 #8
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)
コード例 #9
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:
        all_locations_weighted = _calculate_all_pickup_indices_weight(
            player_states)
        current_player = _get_next_player(rng, player_states,
                                          all_locations_weighted)
        if current_player is None:
            break

        weighted_actions = weighted_potential_actions(current_player,
                                                      action_report,
                                                      all_locations_weighted)
        action = select_weighted_action(rng, weighted_actions)

        if isinstance(action, tuple):
            new_pickups: list[PickupEntry] = sorted(action)
            rng.shuffle(new_pickups)

            debug.debug_print(f"\n>>> Will place {len(new_pickups)} pickups")
            for new_pickup in new_pickups:
                log_entry = _assign_pickup_somewhere(new_pickup,
                                                     current_player,
                                                     player_states, rng,
                                                     all_locations_weighted)
                actions_log.append(log_entry)
                debug.debug_print(f"* {log_entry}")

                # TODO: this item is potentially dangerous and we should remove the invalidated paths
                current_player.pickups_left.remove(new_pickup)

            current_player.num_actions += 1
        else:
            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)

        last_message = "{} actions performed.".format(
            sum(player.num_actions for player in player_states))
        status_update(last_message)
        current_player.reach = reach_lib.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)
コード例 #10
0
ファイル: retcon.py プロジェクト: ThomasJRyan/randovania
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)
コード例 #11
0
async def run_filler(
    rng: Random,
    player_pools: list[PlayerPool],
    status_update: Callable[[str], None],
) -> FillerResults:
    """
    Runs the filler logic for the given configuration and item pool.
    Returns a GamePatches with progression items and hints assigned, along with all items in the pool
    that weren't assigned.

    :param player_pools:
    :param rng:
    :param status_update:
    :return:
    """

    player_states = []
    player_expansions: dict[int, list[PickupEntry]] = {}

    for index, pool in enumerate(player_pools):
        config = pool.configuration

        status_update(f"Creating state for player {index + 1}")
        if config.multi_pickup_placement:
            major_items, player_expansions[index] = list(pool.pickups), []
        else:
            major_items, player_expansions[index] = _split_expansions(
                pool.pickups)
        rng.shuffle(major_items)
        rng.shuffle(player_expansions[index])

        new_game, state = pool.game_generator.bootstrap.logic_bootstrap(
            config, pool.game, pool.patches)
        major_configuration = config.major_items_configuration
        player_states.append(
            PlayerState(
                index=index,
                game=new_game,
                initial_state=state,
                pickups_left=major_items,
                configuration=FillerConfiguration(
                    randomization_mode=config.available_locations.
                    randomization_mode,
                    minimum_random_starting_items=major_configuration.
                    minimum_random_starting_items,
                    maximum_random_starting_items=major_configuration.
                    maximum_random_starting_items,
                    indices_to_exclude=config.available_locations.
                    excluded_indices,
                    multi_pickup_placement=config.multi_pickup_placement,
                    multi_pickup_new_weighting=config.
                    multi_pickup_new_weighting,
                    logical_resource_action=config.logical_resource_action,
                    first_progression_must_be_local=config.
                    first_progression_must_be_local,
                    minimum_available_locations_for_hint_placement=config.
                    minimum_available_locations_for_hint_placement,
                    minimum_location_weight_for_hint_placement=config.
                    minimum_location_weight_for_hint_placement,
                ),
            ))

    try:
        filler_result, actions_log = retcon_playthrough_filler(
            rng, player_states, status_update=status_update)
    except UnableToGenerate as e:
        message = "{}\n\n{}".format(
            str(e),
            "\n\n".join(
                "#### Player {}\n{}".format(player.index +
                                            1, player.current_state_report())
                for player in player_states),
        )
        debug.debug_print(message)
        raise UnableToGenerate(message) from e

    results = {}
    for player_state, patches in filler_result.items():
        player_pool = player_pools[player_state.index]

        hint_distributor = player_pool.game_generator.hint_distributor
        results[player_state.index] = FillerPlayerResult(
            game=player_state.game,
            patches=await
            hint_distributor.assign_post_filler_hints(patches, rng,
                                                      player_pool,
                                                      player_state),
            unassigned_pickups=player_state.pickups_left +
            player_expansions[player_state.index],
        )

    return FillerResults(results, actions_log)
コード例 #12
0
ファイル: retcon.py プロジェクト: ursineasylum/randovania
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
コード例 #13
0
def retcon_playthrough_filler(
    logic: Logic,
    initial_state: State,
    available_pickups: Tuple[PickupEntry, ...],
    rng: Random,
    status_update: Callable[[str], None],
) -> GamePatches:
    debug.debug_print("Major items: {}".format(
        [item.name for item in available_pickups]))
    last_message = "Starting."

    reach = advance_reach_with_possible_unsafe_resources(
        reach_with_all_safe_resources(logic, initial_state))

    pickup_index_seen_count: Dict[PickupIndex,
                                  int] = collections.defaultdict(int)

    while True:
        current_uncollected = UncollectedState.from_reach(reach)

        pickups_left: Dict[str, PickupEntry] = {
            pickup.name: pickup
            for pickup in available_pickups
            if pickup not in reach.state.patches.pickup_assignment.values()
        }

        if not pickups_left:
            debug.debug_print(
                "Finished because we have nothing else to distribute")
            break

        progression_pickups = _calculate_progression_pickups(
            pickups_left, reach)
        print_retcon_loop_start(current_uncollected, logic, pickups_left,
                                reach)

        for pickup_index in reach.state.collected_pickup_indices:
            pickup_index_seen_count[pickup_index] += 1
        print_new_pickup_indices(logic, reach, pickup_index_seen_count)

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

        actions_weights = _calculate_potential_actions(reach,
                                                       progression_pickups,
                                                       current_uncollected,
                                                       action_report)

        try:
            action = next(
                iterate_with_weights(list(actions_weights.keys()),
                                     actions_weights, rng))
        except StopIteration:
            if actions_weights:
                action = rng.choice(list(actions_weights.keys()))
            else:
                raise RuntimeError(
                    "Unable to generate, no actions found after placing {} items."
                    .format(len(reach.state.patches.pickup_assignment)))

        if isinstance(action, PickupEntry):
            pickup_index_weight = {
                pickup_index:
                1 / (min(pickup_index_seen_count[pickup_index], 10)**2)
                for pickup_index in current_uncollected.indices
            }
            assert pickup_index_weight, "Pickups should only be added to the actions dict " \
                                        "when there are unassigned pickups"

            # print(">>>>>>>>>>>>>")
            # world_list = logic.game.world_list
            # for pickup_index in sorted(current_uncollected.indices, key=lambda x: pickup_index_weight[x]):
            #     print("{1:.6f} {2:5}: {0}".format(
            #         world_list.node_name(find_pickup_node_with_index(pickup_index, world_list.all_nodes)),
            #         pickup_index_weight[pickup_index],
            #         pickup_index_seen_count[pickup_index]))

            pickup_index = next(
                iterate_with_weights(list(current_uncollected.indices),
                                     pickup_index_weight, rng))

            # TODO: this item is potentially dangerous and we should remove the invalidated paths
            next_state = reach.state.assign_pickup_to_index(
                pickup_index, action)

            last_message = "Placed {} items so far, {} left.".format(
                len(next_state.patches.pickup_assignment),
                len(pickups_left) - 1)
            status_update(last_message)
            print_retcon_place_pickup(action, logic, pickup_index)

            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, logic)
            # 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 logic.game.victory_condition.satisfied(
                reach.state.resources, reach.state.resource_database):
            debug.debug_print("Finished because we can win")
            break

    return reach.state.patches
コード例 #14
0
class HintDistributor(ABC):
    @property
    def num_joke_hints(self) -> int:
        return 0

    def get_generic_logbook_nodes(self, prefill: PreFillParams) -> list[NodeIdentifier]:
        return [
            prefill.game.world_list.identifier_for_node(node)
            for node in prefill.game.world_list.iterate_nodes()
            if isinstance(node, LogbookNode) and node.lore_type.holds_generic_hint
        ]

    async def get_specific_pickup_precision_pair_overrides(self, patches: GamePatches, prefill: PreFillParams
                                                           ) -> dict[NodeIdentifier, PrecisionPair]:
        return {}

    async def assign_specific_location_hints(self, patches: GamePatches, prefill: PreFillParams) -> GamePatches:
        specific_location_precisions = await self.get_specific_pickup_precision_pair_overrides(patches, prefill)

        # TODO: this is an Echoes default. Should not have a default and all nodes have one in the DB.
        default_precision = PrecisionPair(HintLocationPrecision.KEYBEARER, HintItemPrecision.BROAD_CATEGORY,
                                          include_owner=True)

        wl = prefill.game.world_list
        for node in wl.iterate_nodes():
            if isinstance(node, LogbookNode) and node.lore_type == LoreType.SPECIFIC_PICKUP:
                identifier = wl.identifier_for_node(node)
                patches = patches.assign_hint(
                    identifier,
                    Hint(HintType.LOCATION,
                         specific_location_precisions.get(identifier, default_precision),
                         PickupIndex(node.hint_index))
                )

        return patches

    async def get_guranteed_hints(self, patches: GamePatches, prefill: PreFillParams) -> list[HintTargetPrecision]:
        return []

    async def assign_guaranteed_indices_hints(self, patches: GamePatches, identifiers: list[NodeIdentifier],
                                              prefill: PreFillParams) -> GamePatches:
        # Specific Pickup/any LogbookNode Hints
        indices_with_hint = await self.get_guranteed_hints(patches, prefill)
        prefill.rng.shuffle(indices_with_hint)

        all_hint_identifiers = [identifier for identifier in identifiers if identifier not in patches.hints]
        prefill.rng.shuffle(all_hint_identifiers)

        for index, precision in indices_with_hint:
            if not all_hint_identifiers:
                break

            identifier = all_hint_identifiers.pop()
            patches = patches.assign_hint(identifier, Hint(HintType.LOCATION, precision, index))
            identifiers.remove(identifier)

        return patches

    async def assign_other_hints(self, patches: GamePatches, identifiers: list[NodeIdentifier],
                                 prefill: PreFillParams) -> GamePatches:
        return patches

    async def assign_joke_hints(self, patches: GamePatches, identifiers: list[NodeIdentifier],
                                prefill: PreFillParams) -> GamePatches:

        all_hint_identifiers = [identifier for identifier in identifiers if identifier not in patches.hints]
        prefill.rng.shuffle(all_hint_identifiers)

        num_joke = self.num_joke_hints

        while num_joke > 0 and all_hint_identifiers:
            identifier = all_hint_identifiers.pop()
            patches = patches.assign_hint(identifier, Hint(HintType.JOKE, None))
            num_joke -= 1
            identifiers.remove(identifier)

        return patches

    async def assign_pre_filler_hints(self, patches: GamePatches, prefill: PreFillParams,
                                      rng_required: bool = True) -> GamePatches:
        patches = await self.assign_specific_location_hints(patches, prefill)
        hint_identifiers = self.get_generic_logbook_nodes(prefill)
        if rng_required or prefill.rng is not None:
            prefill.rng.shuffle(hint_identifiers)
            patches = await self.assign_guaranteed_indices_hints(patches, hint_identifiers, prefill)
            patches = await self.assign_other_hints(patches, hint_identifiers, prefill)
            patches = await self.assign_joke_hints(patches, hint_identifiers, prefill)
        return patches

    async def assign_post_filler_hints(self, patches: GamePatches, rng: Random,
                                       player_pool: PlayerPool, player_state: PlayerState,
                                       ) -> GamePatches:
        # Since we haven't added expansions yet, these hints will always be for items added by the filler.
        full_hints_patches = self.fill_unassigned_hints(
            patches, player_state.game.world_list, rng,
            player_state.hint_initial_pickups,
        )
        return await self.assign_precision_to_hints(full_hints_patches, rng, player_pool, player_state)

    async def assign_precision_to_hints(self, patches: GamePatches, rng: Random,
                                        player_pool: PlayerPool, player_state: PlayerState) -> GamePatches:
        """
        Ensures no hints present in `patches` has no precision.
        :param patches:
        :param rng:
        :param player_pool:
        :param player_state:
        :return:
        """
        raise NotImplementedError()

    def interesting_pickup_to_hint(self, pickup: PickupEntry) -> bool:
        return pickup.item_category.is_major

    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])

        # But if we don't have enough hints, just pick randomly from everything
        while len(possible_indices) < len(potential_hint_locations):
            debug.debug_print(
                f"Still only {len(possible_indices)} indices out of {len(potential_hint_locations)} target."
                f"Desperate pool has {len(all_pickup_indices)} left."
            )
            try:
                possible_indices.add(all_pickup_indices.pop())
            except IndexError:
                raise UnableToGenerate("Not enough PickupNodes in the game to fill all hint locations.")

        # 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()) if num_logbooks else 0
        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 "
                              f"{world_list.node_name(world_list.node_from_pickup_index(new_index))}")

        return dataclasses.replace(patches, hints=new_hints)
コード例 #15
0
ファイル: retcon.py プロジェクト: cdoggers/randovania
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