Пример #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 print_retcon_loop_start(current_uncollected: UncollectedState,
                            logic: Logic, pickups_left, reach):
    if debug.debug_level() > 0:
        if debug.debug_level() > 1:
            extra = ", pickups_left: {}".format(list(pickups_left.keys()))
        else:
            extra = ""

        print("\n\n===============================")
        print("\n>>> From {}, {} open pickup indices, {} open resources{}".
              format(
                  logic.game.world_list.node_name(reach.state.node,
                                                  with_world=True),
                  len(current_uncollected.indices),
                  len(current_uncollected.resources), extra))
Пример #3
0
def export_layout(
    layout: LayoutDescription,
    options: Options,
):
    """
    Creates a seed log file for the given layout and saves it to the configured path
    :param layout:
    :param options:
    :return:
    """

    output_json = options.output_directory.joinpath("{}.json".format(
        _output_name_for(layout)))

    if debug.debug_level() > 0:
        patcher = options.output_directory.joinpath("{}-patcher.json".format(
            _output_name_for(layout)))
        with patcher.open("w") as out_file:
            json.dump(patcher_file.create_patcher_file(
                layout, options.cosmetic_patches),
                      out_file,
                      indent=4,
                      separators=(',', ': '))

    # Save the layout to a file
    layout.save_to_file(output_json)
Пример #4
0
def generate_layout(permalink: Permalink,
                    status_update: Callable[[str], None],
                    validate_after_generation: bool,
                    timeout_during_generation: bool,
                    ) -> LayoutDescription:
    receiving_pipe, output_pipe = multiprocessing.Pipe(True)

    debug_level = debug.debug_level()
    if not permalink.spoiler:
        debug_level = 0

    def on_done(_):
        output_pipe.send(None)

    with ProcessPoolExecutor(max_workers=1) as executor:
        future = executor.submit(_generate_layout_worker, output_pipe, permalink, validate_after_generation,
                                 timeout_during_generation, debug_level)

        future.add_done_callback(on_done)

        while not future.done():
            message = receiving_pipe.recv()
            if message is not None:
                try:
                    status_update(message)
                except Exception:
                    receiving_pipe.send("close")
                    raise

        return future.result()
Пример #5
0
def get_pickups_that_solves_unreachable(
    pickups_left: List[PickupEntry],
    reach: GeneratorReach,
    uncollected_resource_nodes: List[ResourceNode],
) -> Tuple[Tuple[PickupEntry, ...], ...]:
    """New logic. Given pickup list and a reach, checks the combination of pickups
    that satisfies on unreachable nodes"""
    state = reach.state
    possible_sets = list(reach.unreachable_nodes_with_requirements().values())
    uncollected_resources = [
        node.resource() for node in uncollected_resource_nodes
    ]

    all_lists = _requirement_lists_without_satisfied_resources(
        state, possible_sets, uncollected_resources)

    result = []
    for requirement_list in sorted(all_lists):
        pickups = pickups_to_solve_list(pickups_left, requirement_list, state)
        if pickups is not None and pickups:
            # FIXME: avoid duplicates in result
            result.append(tuple(pickups))

    if debug.debug_level() > 2:
        print(">> All pickup combinations alternatives:")
        for items in sorted(result):
            print("* {}".format(", ".join(p.name for p in items)))

    return tuple(result)
Пример #6
0
def _requirement_lists_without_satisfied_resources(
    state: State,
    possible_sets: List[RequirementSet],
    uncollected_resources: List[ResourceInfo],
) -> Set[RequirementList]:
    seen_lists = set()
    result = set()

    def _add_items(it):
        items_tuple = RequirementList(it)
        if items_tuple not in result:
            result.add(items_tuple)

    for requirements in possible_sets:
        # Maybe should first recreate `requirements` by removing the satisfied items or the ones that can't be
        for alternative in requirements.alternatives:
            if alternative in seen_lists:
                continue
            seen_lists.add(alternative)

            for items in _unsatisfied_item_requirements_in_list(
                    alternative, state, uncollected_resources):
                _add_items(items)

    if debug.debug_level() > 2:
        print(">> All requirement lists:")
        for items in sorted(result):
            print(f"* {items}")

    return result
Пример #7
0
def generate_layout(
    permalink: Permalink,
    status_update: Callable[[str], None],
    validate_after_generation: bool,
    timeout_during_generation: bool,
) -> LayoutDescription:
    receiving_pipe, output_pipe = multiprocessing.Pipe(False)

    debug_level = debug.debug_level()
    if not permalink.spoiler:
        debug_level = 0

    process = multiprocessing.Process(
        target=_generate_layout_worker,
        args=(output_pipe, permalink, validate_after_generation,
              timeout_during_generation, debug_level))
    process.start()
    try:
        result: Union[Exception, LayoutDescription] = None
        while result is None:
            pipe_input = receiving_pipe.recv()
            if isinstance(pipe_input, (LayoutDescription, Exception)):
                result = pipe_input
            else:
                status_update(pipe_input)
    finally:
        process.terminate()

    if isinstance(result, Exception):
        raise result
    else:
        return result
Пример #8
0
def print_new_resources(game: GameDescription,
                        reach: GeneratorReach,
                        seen_count: Dict[ResourceInfo, int],
                        label: str,
                        ):
    world_list = game.world_list
    if debug.debug_level() > 1:
        for index, count in seen_count.items():
            if count == 1:
                node = find_node_with_resource(index, world_list.all_nodes)
                print("-> New {}: {}".format(label, world_list.node_name(node, with_world=True)))

                if debug.debug_level() > 2:
                    paths = reach.shortest_path_from(node)
                    path = paths.get(reach.state.node, [])
                    print([node.name for node in path])
        print("")
Пример #9
0
def print_new_pickup_indices(
    logic: Logic,
    reach: GeneratorReach,
    pickup_index_seen_count: Dict[PickupIndex, int],
):
    world_list = logic.game.world_list
    if debug.debug_level() > 0:
        for index, count in pickup_index_seen_count.items():
            if count == 1:
                node = find_pickup_node_with_index(index, world_list.all_nodes)
                print("-> New Pickup Node: {}".format(
                    world_list.node_name(node, with_world=True)))
                if debug.debug_level() > 1:
                    paths = reach.shortest_path_from(node)
                    path = paths.get(reach.state.node, [])
                    print([node.name for node in path])
        print("")
Пример #10
0
def print_new_pickup_index(player: int, game: GameDescription,
                           reach: GeneratorReach, location: PickupIndex,
                           count: int):
    if debug.debug_level() > 1 and count == 1:
        world_list = game.world_list
        node = world_list.node_from_pickup_index(location)
        print("-> New Pickup Index: Player {}'s {}".format(
            player, world_list.node_name(node, with_world=True)))
Пример #11
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])
Пример #12
0
def print_retcon_place_pickup(action: PickupEntry, logic: Logic,
                              pickup_index: PickupIndex):
    world_list = logic.game.world_list
    if debug.debug_level() > 0:
        print("\n--> Placing {} at {}".format(
            action.name,
            world_list.node_name(find_pickup_node_with_index(
                pickup_index, world_list.all_nodes),
                                 with_world=True)))
Пример #13
0
def print_retcon_loop_start(current_uncollected: UncollectedState,
                            game: GameDescription,
                            pickups_left: Iterator[PickupEntry],
                            reach: GeneratorReach,
                            ):
    if debug.debug_level() > 0:
        if debug.debug_level() > 1:
            extra = ", pickups_left: {}".format([pickup.name for pickup in pickups_left])
        else:
            extra = ""

        print("\n\n===============================")
        print("\n>>> From {}, {} open pickup indices, {} open resources{}".format(
            game.world_list.node_name(reach.state.node, with_world=True),
            len(current_uncollected.indices),
            len(current_uncollected.resources),
            extra
        ))
Пример #14
0
def print_new_resources(game: GameDescription,
                        reach: GeneratorReach,
                        seen_count: Dict[ResourceInfo, int],
                        label: str,
                        ):
    world_list = game.world_list
    if debug.debug_level() > 1:
        for index, count in seen_count.items():
            if count == 1:
                node = find_node_with_resource(index, world_list.all_nodes)
                print("-> New {}: {}".format(label, world_list.node_name(node, with_world=True)))
        print("")
Пример #15
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:
        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
Пример #16
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."
            )
Пример #17
0
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
Пример #18
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
Пример #19
0
def print_retcon_place_pickup(action: PickupEntry, game: GameDescription,
                              pickup_index: PickupIndex,
                              hint: Optional[LogbookAsset]):
    world_list = game.world_list
    if debug.debug_level() > 0:
        if hint is not None:
            hint_string = " with hint at {}".format(
                world_list.node_name(find_node_with_resource(
                    hint, world_list.all_nodes),
                                     with_world=True))
        else:
            hint_string = ""

        print("\n--> Placing {0} at {1}{2}".format(
            action.name,
            world_list.node_name(find_node_with_resource(
                pickup_index, world_list.all_nodes),
                                 with_world=True), hint_string))
Пример #20
0
def _calculate_potential_actions(reach: GeneratorReach,
                                 progression_pickups: Tuple[PickupEntry, ...],
                                 current_uncollected: UncollectedState,
                                 free_starting_items_spots: int,
                                 status_update: Callable[[str], None]):
    actions_weights: Dict[Action, float] = {}
    uncollected_resource_nodes = get_collectable_resource_nodes_of_reach(reach)
    total_options = len(uncollected_resource_nodes)
    options_considered = 0

    def update_for_option():
        nonlocal options_considered
        options_considered += 1
        status_update("Checked {} of {} options.".format(
            options_considered, total_options))

    usable_progression_pickups = [
        progression for progression in progression_pickups
        if _items_for_pickup(progression) <= len(current_uncollected.indices)
        or (not uncollected_resource_nodes
            and _items_for_pickup(progression) <= free_starting_items_spots)
    ]

    total_options += len(usable_progression_pickups)

    for progression in usable_progression_pickups:
        actions_weights[progression] = _calculate_weights_for(
            _calculate_reach_for_progression(reach,
                                             progression), current_uncollected,
            progression.name) + progression.probability_offset
        update_for_option()

    for resource in uncollected_resource_nodes:
        actions_weights[resource] = _calculate_weights_for(
            advance_to_with_reach_copy(reach,
                                       reach.state.act_on_node(resource)),
            current_uncollected, resource.name)
        update_for_option()

    if debug.debug_level() > 1:
        for action, weight in actions_weights.items():
            print("{} - {}".format(action.name, weight))

    return actions_weights
Пример #21
0
    def weighted_potential_actions(
        self,
        status_update: Callable[[str], None],
    ) -> Dict[Action, float]:
        """
        Weights all potential actions based on current criteria.
        :param status_update:
        :return:
        """
        actions_weights: Dict[Action, float] = {}
        current_uncollected = UncollectedState.from_reach(self.reach)

        total_options = len(self.potential_actions)
        options_considered = 0

        def update_for_option():
            nonlocal options_considered
            options_considered += 1
            status_update("Checked {} of {} options.".format(
                options_considered, total_options))

        for action in self.potential_actions:
            if isinstance(action, PickupEntry):
                base_weight = _calculate_weights_for(
                    _calculate_reach_for_progression(self.reach, action),
                    current_uncollected, action.name)
                weight = base_weight * action.probability_multiplier + action.probability_offset

            else:
                weight = _calculate_weights_for(
                    advance_to_with_reach_copy(
                        self.reach, self.reach.state.act_on_node(action)),
                    current_uncollected, action.name)

            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, weight))

        return actions_weights
Пример #22
0
def generate_description(
    parameters: GeneratorParameters,
    status_update: Callable[[str], None],
    validate_after_generation: bool,
    timeout_during_generation: bool,
    attempts: Optional[int],
) -> LayoutDescription:
    receiving_pipe, output_pipe = multiprocessing.Pipe(True)

    debug_level = debug.debug_level()
    if not parameters.spoiler:
        debug_level = 0

    def on_done(_):
        output_pipe.send(None)

    with ProcessPoolExecutor(max_workers=1) as executor:
        extra_args = {
            "generator_params": parameters,
            "validate_after_generation": validate_after_generation,
        }
        if not timeout_during_generation:
            extra_args["timeout"] = None
        if attempts is not None:
            extra_args["attempts"] = attempts

        future = executor.submit(_generate_layout_worker, output_pipe,
                                 debug_level, extra_args)
        future.add_done_callback(on_done)

        while not future.done():
            message = receiving_pipe.recv()
            if message is not None:
                try:
                    status_update(message)
                except Exception:
                    receiving_pipe.send("close")
                    raise

        return future.result()
Пример #23
0
def debug_print_collect_event(action, game):
    if debug.debug_level() > 0:
        print("\n--> Collecting {}".format(game.world_list.node_name(action, with_world=True)))
Пример #24
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)
Пример #25
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
Пример #26
0
def debug_print_collect_event(event: ResourceNode, game: GameDescription):
    if debug.debug_level() > 0:
        print("\n--> Collecting {}".format(game.world_list.node_name(event, with_world=True)))
Пример #27
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
Пример #28
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