def test_database_collectable(preset_manager, game_enum, preset_name,
                              ignore_events, ignore_pickups):
    game, initial_state, permalink = run_bootstrap(
        preset_manager.included_preset_with(game_enum,
                                            preset_name).get_preset())
    all_pickups = set(filter_pickup_nodes(game.world_list.all_nodes))
    pool_results = pool_creator.calculate_pool_results(
        permalink.get_preset(0).configuration, game.resource_database)
    add_resources_into_another(initial_state.resources,
                               pool_results.initial_resources)
    for pickup in pool_results.pickups:
        add_pickup_to_state(initial_state, pickup)
    for pickup in pool_results.assignment.values():
        add_pickup_to_state(initial_state, pickup)
    for trick in game.resource_database.trick:
        initial_state.resources[trick] = LayoutTrickLevel.maximum().as_number

    expected_events = [
        event for event in game.resource_database.event
        if event.index not in ignore_events
    ]
    expected_pickups = sorted(it.pickup_index for it in all_pickups
                              if it.pickup_index.index not in ignore_pickups)

    reach = _create_reach_with_unsafe(game, initial_state.heal())
    while list(collectable_resource_nodes(reach.nodes, reach)):
        reach.act_on(next(iter(collectable_resource_nodes(reach.nodes,
                                                          reach))))
        reach = advance_reach_with_possible_unsafe_resources(reach)

    # print("\nCurrent reach:")
    # for world in game.world_list.worlds:
    #     print(f"\n>> {world.name}")
    #     for node in world.all_nodes:
    #         print("[{!s:>5}, {!s:>5}, {!s:>5}] {}".format(
    #             reach.is_reachable_node(node), reach.is_safe_node(node),
    #             reach.state.resources.get(node.resource(), 0) > 0 if isinstance(node, ResourceNode) else "",
    #             game.world_list.node_name(node)))

    collected_indices = {
        resource
        for resource, quantity in reach.state.resources.items()
        if quantity > 0 and isinstance(resource, PickupIndex)
    }
    collected_events = {
        resource
        for resource, quantity in reach.state.resources.items()
        if quantity > 0 and resource.resource_type == ResourceType.EVENT
    }
    assert list(collectable_resource_nodes(reach.nodes, reach)) == []
    assert sorted(collected_indices) == expected_pickups
    assert sorted(collected_events, key=lambda it: it.index) == expected_events
Beispiel #2
0
    def __init__(self, game: GameDescription, initial_state: State,
                 pickups_left: List[PickupEntry],
                 configuration: FillerConfiguration):
        self.game = game
        self.reach = advance_reach_with_possible_unsafe_resources(reach_with_all_safe_resources(game, initial_state))
        self.pickups_left = pickups_left
        self.configuration = configuration

        self.pickup_index_seen_count = collections.defaultdict(int)
        self.scan_asset_seen_count = collections.defaultdict(int)
        self.scan_asset_initial_pickups = {}
        self.num_random_starting_items_placed = 0
        self.indices_groups, self.all_indices = build_available_indices(game.world_list, configuration)
Beispiel #3
0
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)
def _create_reach_with_unsafe(game: GameDescription,
                              state: State) -> GeneratorReach:
    return advance_reach_with_possible_unsafe_resources(
        reach_with_all_safe_resources(game, state))
Beispiel #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)
Beispiel #6
0
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
Beispiel #7
0
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
Beispiel #8
0
def random_assumed_filler(
    logic: Logic,
    initial_state: State,
    patches: GamePatches,
    available_pickups: Tuple[PickupEntry],
    rng: Random,
    status_update: Callable[[str], None],
) -> PickupAssignment:
    pickup_assignment = copy.copy(patches.pickup_assignment)
    print("Major items: {}".format([item.name for item in available_pickups]))
    game = logic.game

    base_reach = advance_reach_with_possible_unsafe_resources(
        reach_with_all_safe_resources(game, initial_state))

    reaches_for_pickup = {}

    previous_reach = base_reach
    for pickup in reversed(available_pickups):
        print("** Preparing reach for {}".format(pickup.name))
        new_reach = copy.deepcopy(previous_reach)
        add_pickup_to_state(new_reach.state, pickup)
        new_reach.state.previous_state = new_reach.state
        new_reach.advance_to(new_reach.state)
        collect_all_safe_resources_in_reach(new_reach)
        previous_reach = advance_reach_with_possible_unsafe_resources(
            new_reach)
        reaches_for_pickup[pickup] = previous_reach

    for i, pickup in enumerate(available_pickups):
        print("\n\n\nWill place {}, have {} pickups left".format(
            pickup,
            len(available_pickups) - i - 1))
        reach = reaches_for_pickup[pickup]
        debug.print_actions_of_reach(reach)
        escape_state = state_with_pickup(reach.state, pickup)

        total_pickup_nodes = list(
            _filter_pickups(filter_reachable(reach.nodes, reach)))
        pickup_nodes = list(
            filter_unassigned_pickup_nodes(total_pickup_nodes,
                                           pickup_assignment))
        num_nodes = len(pickup_nodes)
        actions_weights = {
            node: len(path)
            for node, path in reach.shortest_path_from(
                initial_state.node).items()
        }

        try:
            pickup_node = next(
                pickup_nodes_that_can_reach(
                    iterate_with_weights(pickup_nodes, actions_weights, rng),
                    reach_with_all_safe_resources(game, escape_state),
                    set(reach.safe_nodes)))
            print("Placed {} at {}. Had {} available of {} nodes.".format(
                pickup.name, game.world_list.node_name(pickup_node, True),
                num_nodes, len(total_pickup_nodes)))

        except StopIteration:
            print("\n".join(
                game.world_list.node_name(node, True)
                for node in reach.safe_nodes))
            raise Exception(
                "Couldn't place {}. Had {} available of {} nodes.".format(
                    pickup.name, num_nodes, len(total_pickup_nodes)))

        pickup_assignment[pickup_node.pickup_index] = pickup

    return pickup_assignment