コード例 #1
0
def flood_items(world: MultiWorld) -> None:
    # get items to distribute
    world.random.shuffle(world.itempool)
    itempool = world.itempool
    progress_done = False

    # sweep once to pick up preplaced items
    world.state.sweep_for_events()

    # fill world from top of itempool while we can
    while not progress_done:
        location_list = world.get_unfilled_locations()
        world.random.shuffle(location_list)
        spot_to_fill = None
        for location in location_list:
            if location.can_fill(world.state, itempool[0]):
                spot_to_fill = location
                break

        if spot_to_fill:
            item = itempool.pop(0)
            world.push_item(spot_to_fill, item, True)
            continue

        # ran out of spots, check if we need to step in and correct things
        if len(world.get_reachable_locations()) == len(world.get_locations()):
            progress_done = True
            continue

        # need to place a progress item instead of an already placed item, find candidate
        item_to_place = None
        candidate_item_to_place = None
        for item in itempool:
            if item.advancement:
                candidate_item_to_place = item
                if world.unlocks_new_location(item):
                    item_to_place = item
                    break

        # we might be in a situation where all new locations require multiple items to reach.
        # If that is the case, just place any advancement item we've found and continue trying
        if item_to_place is None:
            if candidate_item_to_place is not None:
                item_to_place = candidate_item_to_place
            else:
                raise FillError('No more progress items left to place.')

        # find item to replace with progress item
        location_list = world.get_reachable_locations()
        world.random.shuffle(location_list)
        for location in location_list:
            if location.item is not None and not location.item.advancement:
                # safe to replace
                replace_item = location.item
                replace_item.location = None
                itempool.append(replace_item)
                world.push_item(location, item_to_place, True)
                itempool.remove(item_to_place)
                break
コード例 #2
0
def balance_multiworld_progression(world: MultiWorld) -> None:
    # A system to reduce situations where players have no checks remaining, popularly known as "BK mode."
    # Overall progression balancing algorithm:
    # Gather up all locations in a sphere.
    # Define a threshold value based on the player with the most available locations.
    # If other players are below the threshold value, swap progression in this sphere into earlier spheres,
    #   which gives more locations available by this sphere.
    balanceable_players: typing.Dict[int, float] = {
        player: world.progression_balancing[player] / 100
        for player in world.player_ids
        if world.progression_balancing[player] > 0
    }
    if not balanceable_players:
        logging.info('Skipping multiworld progression balancing.')
    else:
        logging.info(
            f'Balancing multiworld progression for {len(balanceable_players)} Players.'
        )
        logging.debug(balanceable_players)
        state: CollectionState = CollectionState(world)
        checked_locations: typing.Set[Location] = set()
        unchecked_locations: typing.Set[Location] = set(world.get_locations())

        reachable_locations_count: typing.Dict[int, int] = {
            player: 0
            for player in world.player_ids
            if len(world.get_filled_locations(player)) != 0
        }
        total_locations_count: typing.Counter[int] = Counter(
            location.player for location in world.get_locations()
            if not location.locked)
        balanceable_players = {
            player: balanceable_players[player]
            for player in balanceable_players if total_locations_count[player]
        }
        sphere_num: int = 1
        moved_item_count: int = 0

        def get_sphere_locations(
                sphere_state: CollectionState,
                locations: typing.Set[Location]) -> typing.Set[Location]:
            sphere_state.sweep_for_events(key_only=True, locations=locations)
            return {loc for loc in locations if sphere_state.can_reach(loc)}

        def item_percentage(player: int, num: int) -> float:
            return num / total_locations_count[player]

        while True:
            # Gather non-locked locations.
            # This ensures that only shuffled locations get counted for progression balancing,
            #   i.e. the items the players will be checking.
            sphere_locations = get_sphere_locations(state, unchecked_locations)
            for location in sphere_locations:
                unchecked_locations.remove(location)
                if not location.locked:
                    reachable_locations_count[location.player] += 1

            logging.debug(f"Sphere {sphere_num}")
            logging.debug(f"Reachable locations: {reachable_locations_count}")
            debug_percentages = {
                player: round(item_percentage(player, num), 2)
                for player, num in reachable_locations_count.items()
            }
            logging.debug(f"Reachable percentages: {debug_percentages}\n")
            sphere_num += 1

            if checked_locations:
                max_percentage = max(
                    map(
                        lambda p: item_percentage(p, reachable_locations_count[
                            p]), reachable_locations_count))
                threshold_percentages = {
                    player: max_percentage * balanceable_players[player]
                    for player in balanceable_players
                }
                logging.debug(f"Thresholds: {threshold_percentages}")
                balancing_players = {
                    player
                    for player, reachables in
                    reachable_locations_count.items()
                    if (player in threshold_percentages and item_percentage(
                        player, reachables) < threshold_percentages[player])
                }
                if balancing_players:
                    balancing_state = state.copy()
                    balancing_unchecked_locations = unchecked_locations.copy()
                    balancing_reachables = reachable_locations_count.copy()
                    balancing_sphere = sphere_locations.copy()
                    candidate_items: typing.Dict[
                        int,
                        typing.Set[Location]] = collections.defaultdict(set)
                    while True:
                        # Check locations in the current sphere and gather progression items to swap earlier
                        for location in balancing_sphere:
                            if location.event:
                                balancing_state.collect(
                                    location.item, True, location)
                                player = location.item.player
                                # only replace items that end up in another player's world
                                if (not location.locked and not location.item.
                                        skip_in_prog_balancing
                                        and player in balancing_players
                                        and location.player != player
                                        and location.progress_type !=
                                        LocationProgressType.PRIORITY):
                                    candidate_items[player].add(location)
                                    logging.debug(
                                        f"Candidate item: {location.name}, {location.item.name}"
                                    )
                        balancing_sphere = get_sphere_locations(
                            balancing_state, balancing_unchecked_locations)
                        for location in balancing_sphere:
                            balancing_unchecked_locations.remove(location)
                            if not location.locked:
                                balancing_reachables[location.player] += 1
                        if world.has_beaten_game(balancing_state) or all(
                                item_percentage(player, reachables) >=
                                threshold_percentages[player]
                                for player, reachables in balancing_reachables.
                                items() if player in threshold_percentages):
                            break
                        elif not balancing_sphere:
                            raise RuntimeError(
                                'Not all required items reachable. Something went terribly wrong here.'
                            )
                    # Gather a set of locations which we can swap items into
                    unlocked_locations: typing.Dict[
                        int,
                        typing.Set[Location]] = collections.defaultdict(set)
                    for l in unchecked_locations:
                        if l not in balancing_unchecked_locations:
                            unlocked_locations[l.player].add(l)
                    items_to_replace: typing.List[Location] = []
                    for player in balancing_players:
                        locations_to_test = unlocked_locations[player]
                        items_to_test = list(candidate_items[player])
                        items_to_test.sort()
                        world.random.shuffle(items_to_test)
                        while items_to_test:
                            testing = items_to_test.pop()
                            reducing_state = state.copy()
                            for location in itertools.chain(
                                (l for l in items_to_replace
                                 if l.item.player == player), items_to_test):
                                reducing_state.collect(location.item, True,
                                                       location)

                            reducing_state.sweep_for_events(
                                locations=locations_to_test)

                            if world.has_beaten_game(balancing_state):
                                if not world.has_beaten_game(reducing_state):
                                    items_to_replace.append(testing)
                            else:
                                reduced_sphere = get_sphere_locations(
                                    reducing_state, locations_to_test)
                                p = item_percentage(
                                    player, reachable_locations_count[player] +
                                    len(reduced_sphere))
                                if p < threshold_percentages[player]:
                                    items_to_replace.append(testing)

                    replaced_items = False

                    # sort then shuffle to maintain deterministic behaviour,
                    # while allowing use of set for better algorithm growth behaviour elsewhere
                    replacement_locations = sorted(
                        l for l in checked_locations
                        if not l.event and not l.locked)
                    world.random.shuffle(replacement_locations)
                    items_to_replace.sort()
                    world.random.shuffle(items_to_replace)

                    # Start swapping items. Since we swap into earlier spheres, no need for accessibility checks.
                    while replacement_locations and items_to_replace:
                        old_location = items_to_replace.pop()
                        for new_location in replacement_locations:
                            if new_location.can_fill(state, old_location.item, False) and \
                                    old_location.can_fill(state, new_location.item, False):
                                replacement_locations.remove(new_location)
                                swap_location_item(old_location, new_location)
                                logging.debug(
                                    f"Progression balancing moved {new_location.item} to {new_location}, "
                                    f"displacing {old_location.item} into {old_location}"
                                )
                                moved_item_count += 1
                                state.collect(new_location.item, True,
                                              new_location)
                                replaced_items = True
                                break
                        else:
                            logging.warning(
                                f"Could not Progression Balance {old_location.item}"
                            )

                    if replaced_items:
                        logging.debug(
                            f"Moved {moved_item_count} items so far\n")
                        unlocked = {
                            fresh
                            for player in balancing_players
                            for fresh in unlocked_locations[player]
                        }
                        for location in get_sphere_locations(state, unlocked):
                            unchecked_locations.remove(location)
                            if not location.locked:
                                reachable_locations_count[location.player] += 1
                            sphere_locations.add(location)

            for location in sphere_locations:
                if location.event:
                    state.collect(location.item, True, location)
            checked_locations |= sphere_locations

            if world.has_beaten_game(state):
                break
            elif not sphere_locations:
                logging.warning("Progression Balancing ran out of paths.")
                break
コード例 #3
0
def distribute_items_restrictive(world: MultiWorld) -> None:
    fill_locations = sorted(world.get_unfilled_locations())
    world.random.shuffle(fill_locations)

    # get items to distribute
    itempool = sorted(world.itempool)
    world.random.shuffle(itempool)
    progitempool: typing.List[Item] = []
    nonexcludeditempool: typing.List[Item] = []
    localrestitempool: typing.Dict[int, typing.List[Item]] = {
        player: []
        for player in range(1, world.players + 1)
    }
    nonlocalrestitempool: typing.List[Item] = []
    restitempool: typing.List[Item] = []

    for item in itempool:
        if item.advancement:
            progitempool.append(item)
        elif item.useful:  # this only gets nonprogression items which should not appear in excluded locations
            nonexcludeditempool.append(item)
        elif item.name in world.local_items[item.player].value:
            localrestitempool[item.player].append(item)
        elif item.name in world.non_local_items[item.player].value:
            nonlocalrestitempool.append(item)
        else:
            restitempool.append(item)

    call_all(world, "fill_hook", progitempool, nonexcludeditempool,
             localrestitempool, nonlocalrestitempool, restitempool,
             fill_locations)

    locations: typing.Dict[LocationProgressType, typing.List[Location]] = {
        loc_type: []
        for loc_type in LocationProgressType
    }

    for loc in fill_locations:
        locations[loc.progress_type].append(loc)

    prioritylocations = locations[LocationProgressType.PRIORITY]
    defaultlocations = locations[LocationProgressType.DEFAULT]
    excludedlocations = locations[LocationProgressType.EXCLUDED]

    fill_restrictive(world,
                     world.state,
                     prioritylocations,
                     progitempool,
                     lock=True)
    if prioritylocations:
        defaultlocations = prioritylocations + defaultlocations

    if progitempool:
        fill_restrictive(world, world.state, defaultlocations, progitempool)
        if progitempool:
            raise FillError(
                f'Not enough locations for progress items. There are {len(progitempool)} more items than locations'
            )

    if nonexcludeditempool:
        world.random.shuffle(defaultlocations)
        # needs logical fill to not conflict with local items
        fill_restrictive(world, world.state, defaultlocations,
                         nonexcludeditempool)
        if nonexcludeditempool:
            raise FillError(
                f'Not enough locations for non-excluded items. There are {len(nonexcludeditempool)} more items than locations'
            )

    defaultlocations = defaultlocations + excludedlocations
    world.random.shuffle(defaultlocations)

    if any(localrestitempool.values()
           ):  # we need to make sure some fills are limited to certain worlds
        local_locations: typing.Dict[int, typing.List[Location]] = {
            player: []
            for player in world.player_ids
        }
        for location in defaultlocations:
            local_locations[location.player].append(location)
        for player_locations in local_locations.values():
            world.random.shuffle(player_locations)

        for player, items in localrestitempool.items(
        ):  # items already shuffled
            player_local_locations = local_locations[player]
            for item_to_place in items:
                if not player_local_locations:
                    logging.warning(
                        f"Ran out of local locations for player {player}, "
                        f"cannot place {item_to_place}.")
                    break
                spot_to_fill = player_local_locations.pop()
                world.push_item(spot_to_fill, item_to_place, False)
                defaultlocations.remove(spot_to_fill)

    for item_to_place in nonlocalrestitempool:
        for i, location in enumerate(defaultlocations):
            if location.player != item_to_place.player:
                world.push_item(defaultlocations.pop(i), item_to_place, False)
                break
        else:
            logging.warning(
                f"Could not place non_local_item {item_to_place} among {defaultlocations}, tossing."
            )

    world.random.shuffle(defaultlocations)

    restitempool, defaultlocations = fast_fill(world, restitempool,
                                               defaultlocations)
    unplaced = progitempool + restitempool
    unfilled = defaultlocations

    if unplaced or unfilled:
        logging.warning(
            f'Unplaced items({len(unplaced)}): {unplaced} - Unfilled Locations({len(unfilled)}): {unfilled}'
        )
        items_counter = Counter(location.item.player
                                for location in world.get_locations()
                                if location.item)
        locations_counter = Counter(location.player
                                    for location in world.get_locations())
        items_counter.update(item.player for item in unplaced)
        locations_counter.update(location.player for location in unfilled)
        print_data = {"items": items_counter, "locations": locations_counter}
        logging.info(f'Per-Player counts: {print_data})')