コード例 #1
0
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)))
コード例 #2
0
    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)),
        )
コード例 #3
0
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
コード例 #4
0
ファイル: retcon.py プロジェクト: randovania/randovania
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
コード例 #5
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."
            )
コード例 #6
0
ファイル: retcon.py プロジェクト: dyceron/randovania
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
コード例 #7
0
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
コード例 #8
0
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)),
    ))
コード例 #9
0
ファイル: retcon.py プロジェクト: dyceron/randovania
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)),
    ))
コード例 #10
0
ファイル: player_state.py プロジェクト: randovania/randovania
    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",
            )
コード例 #11
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

    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