Пример #1
0
    def run_tests(self, access_pool):
        for location, access, *item_pool in access_pool:
            items = item_pool[0]
            all_except = item_pool[1] if len(item_pool) > 1 else None
            with self.subTest(location=location,
                              access=access,
                              items=items,
                              all_except=all_except):
                if all_except and len(all_except) > 0:
                    items = self.world.itempool[:]
                    items = [
                        item for item in items if item.name not in all_except
                        and not ("Bottle" in item.name
                                 and "AnyBottle" in all_except)
                    ]
                    items.extend(ItemFactory(item_pool[0], 1))
                else:
                    items = ItemFactory(items, 1)
                state = CollectionState(self.world)
                for item in items:
                    item.advancement = True
                    state.collect(item)

                self.assertEqual(
                    self.world.get_location(location, 1).can_reach(state),
                    access)
Пример #2
0
    def run_tests(self, access_pool):
        for exit in self.remove_exits:
            self.world.get_entrance(exit, 1).connected_region = self.world.get_region('Menu', 1)

        for location, access, *item_pool in access_pool:
            items = item_pool[0]
            all_except = item_pool[1] if len(item_pool) > 1 else None
            with self.subTest(location=location, access=access, items=items, all_except=all_except):
                if all_except and len(all_except) > 0:
                    items = self.world.itempool[:]
                    items = [item for item in items if item.name not in all_except and not ("Bottle" in item.name and "AnyBottle" in all_except)]
                    items.extend(ItemFactory(item_pool[0], 1))
                else:
                    items = ItemFactory(items, 1)
                state = CollectionState(self.world)
                state.reachable_regions[1].add(self.world.get_region('Menu', 1))
                for region_name in self.starting_regions:
                    region = self.world.get_region(region_name, 1)
                    state.reachable_regions[1].add(region)
                    for exit in region.exits:
                        if exit.connected_region is not None:
                            state.blocked_connections[1].add(exit)

                for item in items:
                    item.advancement = True
                    state.collect(item)

                self.assertEqual(self.world.get_location(location, 1).can_reach(state), access)
Пример #3
0
 def get_state(self, items):
     if (self.world, tuple(items)) in self._state_cache:
         return self._state_cache[self.world, tuple(items)]
     state = CollectionState(self.world)
     for item in items:
         item.classification = ItemClassification.progression
         state.collect(item)
     state.sweep_for_events()
     self._state_cache[self.world, tuple(items)] = state
     return state
Пример #4
0
 def get_state(self, items):
     if (self.world, tuple(items)) in self._state_cache:
         return self._state_cache[self.world, tuple(items)]
     state = CollectionState(self.world)
     for item in items:
         item.advancement = True
         state.collect(item)
     state.sweep_for_events()
     self._state_cache[self.world, tuple(items)] = state
     return state
Пример #5
0
def create_playthrough(world):
    # create a copy as we will modify it
    world = copy_world(world)

    # get locations containing progress items
    prog_locations = [location for location in world.get_locations() if location.item is not None and location.item.advancement]

    collection_spheres = []
    state = CollectionState(world)
    sphere_candidates = list(prog_locations)
    while sphere_candidates:
        sphere = []
        # build up spheres of collection radius. Everything in each sphere is independent from each other in dependencies and only depends on lower spheres
        for location in sphere_candidates:
            if state.can_reach(location):
                sphere.append(location)

        for location in sphere:
            sphere_candidates.remove(location)
            state.collect(location.item)

        collection_spheres.append(sphere)

    # in the second phase, we cull each sphere such that the game is still beatable, reducing each range of influence to the bare minimum required inside it
    for sphere in reversed(collection_spheres):
        to_delete = []
        for location in sphere:
            # we remove the item at location and check if game is still beatable
            old_item = location.item
            location.item = None
            state.remove(old_item)
            world._item_cache = {}  # need to invalidate
            if world.can_beat_game():
                to_delete.append(location)
            else:
                # still required, got to keep it around
                location.item = old_item

        # cull entries in spheres for spoiler walkthrough at end
        for location in to_delete:
            sphere.remove(location)

    # we are now down to just the required progress items in collection_spheres in a minimum number of spheres. As a cleanup, we right trim empty spheres (can happen if we have multiple triforces)
    collection_spheres = [sphere for sphere in collection_spheres if sphere]

    # we can finally output our playthrough
    return 'Playthrough:\n' + ''.join(['%s: {\n%s}\n' % (i + 1, ''.join(['  %s: %s\n' % (location, location.item) for location in sphere])) for i, sphere in enumerate(collection_spheres)]) + '\n'
Пример #6
0
 def testEmptyStateCanReachSomething(self):
     for game_name, world_type in AutoWorldRegister.world_types.items():
         # Final Fantasy logic is controlled by finalfantasyrandomizer.com
         if game_name != "Archipelago" and game_name != "Final Fantasy":
             with self.subTest("Game", game=game_name):
                 world = setup_default_world(world_type)
                 state = CollectionState(world)
                 locations = set()
                 for location in world.get_locations():
                     if location.can_reach(state):
                         locations.add(location)
                 self.assertGreater(
                     len(locations),
                     0,
                     msg=
                     "Need to be able to reach at least one location to get started."
                 )
Пример #7
0
def create_playthrough(world):
    # create a copy as we will modify it
    old_world = world
    world = copy_world(world)

    # if we only check for beatable, we can do this sanity check first before writing down spheres
    if world.check_beatable_only and not world.can_beat_game():
        raise RuntimeError(
            'Cannot beat game. Something went terribly wrong here!')

    # get locations containing progress items
    prog_locations = [
        location for location in world.get_filled_locations()
        if location.item.advancement
    ]
    state_cache = [None]
    collection_spheres = []
    state = CollectionState(world)
    sphere_candidates = list(prog_locations)
    logging.getLogger('').debug('Building up collection spheres.')
    while sphere_candidates:
        state.sweep_for_events(key_only=True)

        sphere = []
        # build up spheres of collection radius. Everything in each sphere is independent from each other in dependencies and only depends on lower spheres
        for location in sphere_candidates:
            if state.can_reach(location):
                sphere.append(location)

        for location in sphere:
            sphere_candidates.remove(location)
            state.collect(location.item, True, location)

        collection_spheres.append(sphere)

        state_cache.append(state.copy())

        logging.getLogger('').debug(
            'Calculated sphere %i, containing %i of %i progress items.',
            len(collection_spheres), len(sphere), len(prog_locations))
        if not sphere:
            logging.getLogger('').debug(
                'The following items could not be reached: %s', [
                    '%s at %s' % (location.item.name, location.name)
                    for location in sphere_candidates
                ])
            if not world.check_beatable_only:
                raise RuntimeError(
                    'Not all progression items reachable. Something went terribly wrong here.'
                )
            else:
                break

    # in the second phase, we cull each sphere such that the game is still beatable, reducing each range of influence to the bare minimum required inside it
    for num, sphere in reversed(list(enumerate(collection_spheres))):
        to_delete = []
        for location in sphere:
            # we remove the item at location and check if game is still beatable
            logging.getLogger('').debug(
                'Checking if %s is required to beat the game.',
                location.item.name)
            old_item = location.item
            location.item = None
            state.remove(old_item)
            if world.can_beat_game(state_cache[num]):
                to_delete.append(location)
            else:
                # still required, got to keep it around
                location.item = old_item

        # cull entries in spheres for spoiler walkthrough at end
        for location in to_delete:
            sphere.remove(location)

    # we are now down to just the required progress items in collection_spheres. Unfortunately
    # the previous pruning stage could potentially have made certain items dependant on others
    # in the same or later sphere (because the location had 2 ways to access but the item originally
    # used to access it was deemed not required.) So we need to do one final sphere collection pass
    # to build up the correct spheres

    required_locations = [
        item for sphere in collection_spheres for item in sphere
    ]
    state = CollectionState(world)
    collection_spheres = []
    while required_locations:
        state.sweep_for_events(key_only=True)

        sphere = list(filter(state.can_reach, required_locations))

        for location in sphere:
            required_locations.remove(location)
            state.collect(location.item, True, location)

        collection_spheres.append(sphere)

        logging.getLogger('').debug(
            'Calculated final sphere %i, containing %i of %i progress items.',
            len(collection_spheres), len(sphere), len(required_locations))
        if not sphere:
            raise RuntimeError(
                'Not all required items reachable. Something went terribly wrong here.'
            )

    # store the required locations for statistical analysis
    old_world.required_locations = [
        location.name for sphere in collection_spheres for location in sphere
    ]

    def flist_to_iter(node):
        while node:
            value, node = node
            yield value

    def get_path(state, region):
        reversed_path_as_flist = state.path.get(region, (region, None))
        string_path_flat = reversed(
            list(map(str, flist_to_iter(reversed_path_as_flist))))
        # Now we combine the flat string list into (region, exit) pairs
        pathsiter = iter(string_path_flat)
        pathpairs = zip_longest(pathsiter, pathsiter)
        return list(pathpairs)

    old_world.spoiler.paths = {
        location.name: get_path(state, location.parent_region)
        for sphere in collection_spheres for location in sphere
    }

    # we can finally output our playthrough
    old_world.spoiler.playthrough = OrderedDict([
        (str(i + 1),
         {str(location): str(location.item)
          for location in sphere})
        for i, sphere in enumerate(collection_spheres)
    ])
Пример #8
0
def balance_multiworld_progression(world):
    state = CollectionState(world)
    checked_locations = []
    unchecked_locations = world.get_locations().copy()
    random.shuffle(unchecked_locations)

    reachable_locations_count = {}
    for player in range(1, world.players + 1):
        reachable_locations_count[player] = 0

    def get_sphere_locations(sphere_state, locations):
        sphere_state.sweep_for_events(key_only=True, locations=locations)
        return [loc for loc in locations if sphere_state.can_reach(loc)]

    while True:
        sphere_locations = get_sphere_locations(state, unchecked_locations)
        for location in sphere_locations:
            unchecked_locations.remove(location)
            reachable_locations_count[location.player] += 1

        if checked_locations:
            threshold = max(reachable_locations_count.values()) - 20

            balancing_players = [
                player
                for player, reachables in reachable_locations_count.items()
                if reachables < threshold
            ]
            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 = []
                while True:
                    for location in balancing_sphere:
                        if location.event and (
                                world.keyshuffle[location.item.player]
                                or not location.item.smallkey) and (
                                    world.bigkeyshuffle[location.item.player]
                                    or not location.item.bigkey):
                            balancing_state.collect(location.item, True,
                                                    location)
                            if location.item.player in balancing_players and not location.locked:
                                candidate_items.append(location)
                    balancing_sphere = get_sphere_locations(
                        balancing_state, balancing_unchecked_locations)
                    for location in balancing_sphere:
                        balancing_unchecked_locations.remove(location)
                        balancing_reachables[location.player] += 1
                    if world.has_beaten_game(balancing_state) or all([
                            reachables >= threshold
                            for reachables in balancing_reachables.values()
                    ]):
                        break
                    elif not balancing_sphere:
                        raise RuntimeError(
                            'Not all required items reachable. Something went terribly wrong here.'
                        )

                unlocked_locations = [
                    l for l in unchecked_locations
                    if l not in balancing_unchecked_locations
                ]
                items_to_replace = []
                for player in balancing_players:
                    locations_to_test = [
                        l for l in unlocked_locations if l.player == player
                    ]
                    # only replace items that end up in another player's world
                    items_to_test = [
                        l for l in candidate_items
                        if l.item.player == player and l.player != player
                    ]
                    while items_to_test:
                        testing = items_to_test.pop()
                        reducing_state = state.copy()
                        for location in [
                                *[
                                    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)
                            if reachable_locations_count[player] + len(
                                    reduced_sphere) < threshold:
                                items_to_replace.append(testing)

                replaced_items = False
                replacement_locations = [
                    l for l in checked_locations
                    if not l.event and not l.locked
                ]
                while replacement_locations and items_to_replace:
                    new_location = replacement_locations.pop()
                    old_location = items_to_replace.pop()

                    while not new_location.can_fill(
                            state, old_location.item,
                            False) or (new_location.item
                                       and not old_location.can_fill(
                                           state, new_location.item, False)):
                        replacement_locations.insert(0, new_location)
                        new_location = replacement_locations.pop()

                    new_location.item, old_location.item = old_location.item, new_location.item
                    new_location.event, old_location.event = True, False
                    state.collect(new_location.item, True, new_location)
                    replaced_items = True
                if replaced_items:
                    for location in get_sphere_locations(
                            state, [
                                l for l in unlocked_locations
                                if l.player in balancing_players
                            ]):
                        unchecked_locations.remove(location)
                        reachable_locations_count[location.player] += 1
                        sphere_locations.append(location)

        for location in sphere_locations:
            if location.event and (
                    world.keyshuffle[location.item.player]
                    or not location.item.smallkey) and (
                        world.bigkeyshuffle[location.item.player]
                        or not location.item.bigkey):
                state.collect(location.item, True, location)
        checked_locations.extend(sphere_locations)

        if world.has_beaten_game(state):
            break
        elif not sphere_locations:
            raise RuntimeError(
                'Not all required items reachable. Something went terribly wrong here.'
            )
Пример #9
0
def create_playthrough(world):
    # create a copy as we will modify it
    old_world = world
    world = copy_world(world)

    # get locations containing progress items
    prog_locations = [
        location for location in world.get_filled_locations()
        if location.item.advancement
    ]
    state_cache = [None]
    collection_spheres = []
    state = CollectionState(world)
    sphere_candidates = list(prog_locations)
    logging.debug('Building up collection spheres.')
    while sphere_candidates:
        state.sweep_for_events(key_only=True)

        sphere = set()
        # build up spheres of collection radius. Everything in each sphere is independent from each other in dependencies and only depends on lower spheres
        for location in sphere_candidates:
            if state.can_reach(location):
                sphere.add(location)

        for location in sphere:
            sphere_candidates.remove(location)
            state.collect(location.item, True, location)

        collection_spheres.append(sphere)

        state_cache.append(state.copy())

        logging.debug(
            'Calculated sphere %i, containing %i of %i progress items.',
            len(collection_spheres), len(sphere), len(prog_locations))
        if not sphere:
            logging.debug('The following items could not be reached: %s', [
                '%s (Player %d) at %s (Player %d)' %
                (location.item.name, location.item.player, location.name,
                 location.player) for location in sphere_candidates
            ])
            if any([
                    world.accessibility[location.item.player] != 'none'
                    for location in sphere_candidates
            ]):
                raise RuntimeError(
                    f'Not all progression items reachable ({sphere_candidates}). '
                    f'Something went terribly wrong here.')
            else:
                old_world.spoiler.unreachables = sphere_candidates.copy()
                break

    # in the second phase, we cull each sphere such that the game is still beatable, reducing each range of influence to the bare minimum required inside it
    for num, sphere in reversed(tuple(enumerate(collection_spheres))):
        to_delete = set()
        for location in sphere:
            # we remove the item at location and check if game is still beatable
            logging.getLogger('').debug(
                'Checking if %s (Player %d) is required to beat the game.',
                location.item.name, location.item.player)
            old_item = location.item
            location.item = None
            if world.can_beat_game(state_cache[num]):
                to_delete.add(location)
            else:
                # still required, got to keep it around
                location.item = old_item

        # cull entries in spheres for spoiler walkthrough at end
        sphere -= to_delete

    # second phase, sphere 0
    for item in (i for i in world.precollected_items if i.advancement):
        logging.getLogger('').debug(
            'Checking if %s (Player %d) is required to beat the game.',
            item.name, item.player)
        world.precollected_items.remove(item)
        world.state.remove(item)
        if not world.can_beat_game():
            world.push_precollected(item)

    # we are now down to just the required progress items in collection_spheres. Unfortunately
    # the previous pruning stage could potentially have made certain items dependant on others
    # in the same or later sphere (because the location had 2 ways to access but the item originally
    # used to access it was deemed not required.) So we need to do one final sphere collection pass
    # to build up the correct spheres

    required_locations = {
        item
        for sphere in collection_spheres for item in sphere
    }
    state = CollectionState(world)
    collection_spheres = []
    while required_locations:
        state.sweep_for_events(key_only=True)

        sphere = set(filter(state.can_reach, required_locations))

        for location in sphere:
            required_locations.remove(location)
            state.collect(location.item, True, location)

        collection_spheres.append(sphere)

        logging.getLogger('').debug(
            'Calculated final sphere %i, containing %i of %i progress items.',
            len(collection_spheres), len(sphere), len(required_locations))
        if not sphere:
            raise RuntimeError(
                'Not all required items reachable. Something went terribly wrong here.'
            )

    def flist_to_iter(node):
        while node:
            value, node = node
            yield value

    def get_path(state, region):
        reversed_path_as_flist = state.path.get(region, (region, None))
        string_path_flat = reversed(
            list(map(str, flist_to_iter(reversed_path_as_flist))))
        # Now we combine the flat string list into (region, exit) pairs
        pathsiter = iter(string_path_flat)
        pathpairs = zip_longest(pathsiter, pathsiter)
        return list(pathpairs)

    old_world.spoiler.paths = dict()
    for player in range(1, world.players + 1):
        old_world.spoiler.paths.update({
            str(location): get_path(state, location.parent_region)
            for sphere in collection_spheres for location in sphere
            if location.player == player
        })
        for path in dict(old_world.spoiler.paths).values():
            if any(exit == 'Pyramid Fairy' for (_, exit) in path):
                if world.mode[player] != 'inverted':
                    old_world.spoiler.paths[str(
                        world.get_region('Big Bomb Shop', player))] = get_path(
                            state, world.get_region('Big Bomb Shop', player))
                else:
                    old_world.spoiler.paths[str(
                        world.get_region('Inverted Big Bomb Shop',
                                         player))] = get_path(
                                             state,
                                             world.get_region(
                                                 'Inverted Big Bomb Shop',
                                                 player))

    # we can finally output our playthrough
    old_world.spoiler.playthrough = {
        "0":
        sorted([
            str(item) for item in world.precollected_items if item.advancement
        ])
    }

    for i, sphere in enumerate(collection_spheres):
        old_world.spoiler.playthrough[str(i + 1)] = {
            str(location): str(location.item)
            for location in sorted(sphere)
        }
Пример #10
0
def balance_multiworld_progression(world):
    state = CollectionState(world)
    checked_locations = []
    unchecked_locations = world.get_locations().copy()
    random.shuffle(unchecked_locations)

    reachable_locations_count = {}
    for player in range(1, world.players + 1):
        reachable_locations_count[player] = 0

    def get_sphere_locations(sphere_state, locations):
        if not world.keysanity:
            sphere_state.sweep_for_events(key_only=True, locations=locations)
        return [loc for loc in locations if sphere_state.can_reach(loc)]

    while True:
        sphere_locations = get_sphere_locations(state, unchecked_locations)
        for location in sphere_locations:
            unchecked_locations.remove(location)
            reachable_locations_count[location.player] += 1

        if checked_locations:
            average_reachable_locations = sum(
                reachable_locations_count.values()) / world.players
            threshold = ((average_reachable_locations +
                          max(reachable_locations_count.values())) /
                         2) * 0.8  #todo: probably needs some tweaking

            balancing_players = [
                player
                for player, reachables in reachable_locations_count.items()
                if reachables < threshold
            ]
            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 = []
                while True:
                    for location in balancing_sphere:
                        if location.event:
                            balancing_state.collect(location.item, True,
                                                    location)
                            if location.item.player in balancing_players:
                                candidate_items.append(location)
                    balancing_sphere = get_sphere_locations(
                        balancing_state, balancing_unchecked_locations)
                    for location in balancing_sphere:
                        balancing_unchecked_locations.remove(location)
                        balancing_reachables[location.player] += 1
                    if world.has_beaten_game(balancing_state) or all([
                            reachables >= threshold
                            for reachables in balancing_reachables.values()
                    ]):
                        break

                unlocked_locations = [
                    l for l in unchecked_locations
                    if l not in balancing_unchecked_locations
                ]
                items_to_replace = []
                for player in balancing_players:
                    locations_to_test = [
                        l for l in unlocked_locations if l.player == player
                    ]
                    items_to_test = [
                        l for l in candidate_items
                        if l.item.player == player and l.player != player
                    ]
                    while items_to_test:
                        testing = items_to_test.pop()
                        reducing_state = state.copy()
                        for location in [
                                *[
                                    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 testing.locked:
                            continue

                        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)
                            if reachable_locations_count[player] + len(
                                    reduced_sphere) < threshold:
                                items_to_replace.append(testing)

                replaced_items = False
                locations_for_replacing = [
                    l for l in checked_locations
                    if not l.event and not l.locked
                ]
                while locations_for_replacing and items_to_replace:
                    new_location = locations_for_replacing.pop()
                    old_location = items_to_replace.pop()
                    new_location.item, old_location.item = old_location.item, new_location.item
                    new_location.event = True
                    old_location.event = False
                    state.collect(new_location.item, True, new_location)
                    replaced_items = True
                if replaced_items:
                    for location in get_sphere_locations(
                            state, [
                                l for l in unlocked_locations
                                if l.player in balancing_players
                            ]):
                        unchecked_locations.remove(location)
                        reachable_locations_count[location.player] += 1
                        sphere_locations.append(location)

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

        if world.has_beaten_game(state):
            break
Пример #11
0
def create_playthrough(worlds):
    if worlds[0].check_beatable_only and not CollectionState.can_beat_game(
        [world.state for world in worlds]):
        raise RuntimeError('Uncopied is broken too.')
    # create a copy as we will modify it
    old_worlds = worlds
    worlds = [world.copy() for world in worlds]

    # if we only check for beatable, we can do this sanity check first before writing down spheres
    if worlds[0].check_beatable_only and not CollectionState.can_beat_game(
        [world.state for world in worlds]):
        raise RuntimeError(
            'Cannot beat game. Something went terribly wrong here!')

    state_list = [CollectionState(world) for world in worlds]

    # Get all item locations in the worlds
    collection_spheres = []
    item_locations = [
        location for state in state_list
        for location in state.world.get_filled_locations()
        if location.item.advancement
    ]

    # in the first phase, we create the generous spheres. Collecting every item in a sphere will
    # mean that every item in the next sphere is collectable. Will contain every reachable item
    logging.getLogger('').debug('Building up collection spheres.')

    # will loop if there is more items opened up in the previous iteration. Always run once
    reachable_items_locations = True
    while reachable_items_locations:
        # get reachable new items locations
        reachable_items_locations = [
            location for location in item_locations
            if location.name not in state_list[
                location.world.id].collected_locations
            and state_list[location.world.id].can_reach(location)
        ]
        for location in reachable_items_locations:
            # Mark the location collected in the state world it exists in
            state_list[location.world.id].collected_locations.append(
                location.name)
            # Collect the item for the state world it is for
            state_list[location.item.world.id].collect(location.item)
        if reachable_items_locations:
            collection_spheres.append(reachable_items_locations)

    # in the second phase, we cull each sphere such that the game is still beatable, reducing each
    # range of influence to the bare minimum required inside it. Effectively creates a min play
    for num, sphere in reversed(list(enumerate(collection_spheres))):
        to_delete = []
        for location in sphere:
            # we remove the item at location and check if game is still beatable
            logging.getLogger('').debug(
                'Checking if %s is required to beat the game.',
                location.item.name)
            old_item = location.item
            old_state_list = [state.copy() for state in state_list]

            location.item = None
            state_list[old_item.world.id].remove(old_item)
            CollectionState.remove_locations(state_list)
            if CollectionState.can_beat_game(state_list, False):
                to_delete.append(location)
            else:
                # still required, got to keep it around
                state_list = old_state_list
                location.item = old_item

        # cull entries in spheres for spoiler walkthrough at end
        for location in to_delete:
            sphere.remove(location)
    collection_spheres = [sphere for sphere in collection_spheres if sphere]

    # we can finally output our playthrough
    for world in old_worlds:
        world.spoiler.playthrough = OrderedDict([
            (str(i + 1), {location: location.item
                          for location in sphere})
            for i, sphere in enumerate(collection_spheres)
        ])
Пример #12
0
def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = None):
    if not baked_server_options:
        baked_server_options = get_options()["server_options"]
    if args.outputpath:
        os.makedirs(args.outputpath, exist_ok=True)
        output_path.cached_path = args.outputpath

    start = time.perf_counter()
    # initialize the world
    world = MultiWorld(args.multi)

    logger = logging.getLogger()
    world.set_seed(seed, args.race, str(args.outputname if args.outputname else world.seed))

    world.shuffle = args.shuffle.copy()
    world.logic = args.logic.copy()
    world.mode = args.mode.copy()
    world.difficulty = args.difficulty.copy()
    world.item_functionality = args.item_functionality.copy()
    world.timer = args.timer.copy()
    world.goal = args.goal.copy()
    world.open_pyramid = args.open_pyramid.copy()
    world.boss_shuffle = args.shufflebosses.copy()
    world.enemy_health = args.enemy_health.copy()
    world.enemy_damage = args.enemy_damage.copy()
    world.beemizer_total_chance = args.beemizer_total_chance.copy()
    world.beemizer_trap_chance = args.beemizer_trap_chance.copy()
    world.timer = args.timer.copy()
    world.countdown_start_time = args.countdown_start_time.copy()
    world.red_clock_time = args.red_clock_time.copy()
    world.blue_clock_time = args.blue_clock_time.copy()
    world.green_clock_time = args.green_clock_time.copy()
    world.dungeon_counters = args.dungeon_counters.copy()
    world.triforce_pieces_available = args.triforce_pieces_available.copy()
    world.triforce_pieces_required = args.triforce_pieces_required.copy()
    world.shop_shuffle = args.shop_shuffle.copy()
    world.shuffle_prizes = args.shuffle_prizes.copy()
    world.sprite_pool = args.sprite_pool.copy()
    world.dark_room_logic = args.dark_room_logic.copy()
    world.plando_items = args.plando_items.copy()
    world.plando_texts = args.plando_texts.copy()
    world.plando_connections = args.plando_connections.copy()
    world.required_medallions = args.required_medallions.copy()
    world.game = args.game.copy()
    world.player_name = args.name.copy()
    world.enemizer = args.enemizercli
    world.sprite = args.sprite.copy()
    world.glitch_triforce = args.glitch_triforce  # This is enabled/disabled globally, no per player option.

    world.set_options(args)
    world.set_item_links()
    world.state = CollectionState(world)
    logger.info('Archipelago Version %s  -  Seed: %s\n', __version__, world.seed)

    logger.info("Found World Types:")
    longest_name = max(len(text) for text in AutoWorld.AutoWorldRegister.world_types)
    numlength = 8
    for name, cls in AutoWorld.AutoWorldRegister.world_types.items():
        if not cls.hidden:
            logger.info(f"  {name:{longest_name}}: {len(cls.item_names):3} "
                        f"Items (IDs: {min(cls.item_id_to_name):{numlength}} - "
                        f"{max(cls.item_id_to_name):{numlength}}) | "
                        f"{len(cls.location_names):3} "
                        f"Locations (IDs: {min(cls.location_id_to_name):{numlength}} - "
                        f"{max(cls.location_id_to_name):{numlength}})")

    AutoWorld.call_stage(world, "assert_generate")

    AutoWorld.call_all(world, "generate_early")

    logger.info('')

    for player in world.player_ids:
        for item_name, count in world.start_inventory[player].value.items():
            for _ in range(count):
                world.push_precollected(world.create_item(item_name, player))

    for player in world.player_ids:
        if player in world.get_game_players("A Link to the Past"):
            # enforce pre-defined local items.
            if world.goal[player] in ["localtriforcehunt", "localganontriforcehunt"]:
                world.local_items[player].value.add('Triforce Piece')

            # Not possible to place pendants/crystals out side of boss prizes yet.
            world.non_local_items[player].value -= item_name_groups['Pendants']
            world.non_local_items[player].value -= item_name_groups['Crystals']

        # items can't be both local and non-local, prefer local
        world.non_local_items[player].value -= world.local_items[player].value

    logger.info('Creating World.')
    AutoWorld.call_all(world, "create_regions")

    logger.info('Creating Items.')
    AutoWorld.call_all(world, "create_items")

    logger.info('Calculating Access Rules.')
    if world.players > 1:
        for player in world.player_ids:
            locality_rules(world, player)
        group_locality_rules(world)
    else:
        world.non_local_items[1].value = set()
        world.local_items[1].value = set()

    AutoWorld.call_all(world, "set_rules")

    for player in world.player_ids:
        exclusion_rules(world, player, world.exclude_locations[player].value)
        world.priority_locations[player].value -= world.exclude_locations[player].value
        for location_name in world.priority_locations[player].value:
            world.get_location(location_name, player).progress_type = LocationProgressType.PRIORITY

    AutoWorld.call_all(world, "generate_basic")

    # temporary home for item links, should be moved out of Main
    for group_id, group in world.groups.items():
        def find_common_pool(players: Set[int], shared_pool: Set[str]):
            classifications = collections.defaultdict(int)
            counters = {player: {name: 0 for name in shared_pool} for player in players}
            for item in world.itempool:
                if item.player in counters and item.name in shared_pool:
                    counters[item.player][item.name] += 1
                    classifications[item.name] |= item.classification

            for player in players.copy():
                if all([counters[player][item] == 0 for item in shared_pool]):
                    players.remove(player)
                    del(counters[player])

            if not players:
                return None, None

            for item in shared_pool:
                count = min(counters[player][item] for player in players)
                if count:
                    for player in players:
                        counters[player][item] = count
                else:
                    for player in players:
                        del(counters[player][item])
            return counters, classifications

        common_item_count, classifications = find_common_pool(group["players"], group["item_pool"])
        if not common_item_count:
            continue

        new_itempool = []
        for item_name, item_count in next(iter(common_item_count.values())).items():
            for _ in range(item_count):
                new_item = group["world"].create_item(item_name)
                # mangle together all original classification bits
                new_item.classification |= classifications[item_name]
                new_itempool.append(new_item)

        region = Region("Menu", RegionType.Generic, "ItemLink", group_id, world)
        world.regions.append(region)
        locations = region.locations = []
        for item in world.itempool:
            count = common_item_count.get(item.player, {}).get(item.name, 0)
            if count:
                loc = Location(group_id, f"Item Link: {item.name} -> {world.player_name[item.player]} {count}",
                               None, region)
                loc.access_rule = lambda state, item_name = item.name, group_id_ = group_id, count_ = count: \
                    state.has(item_name, group_id_, count_)

                locations.append(loc)
                loc.place_locked_item(item)
                common_item_count[item.player][item.name] -= 1
            else:
                new_itempool.append(item)

        itemcount = len(world.itempool)
        world.itempool = new_itempool

        while itemcount > len(world.itempool):
            items_to_add = []
            for player in group["players"]:
                if group["replacement_items"][player]:
                    items_to_add.append(AutoWorld.call_single(world, "create_item", player,
                                                                group["replacement_items"][player]))
                else:
                    items_to_add.append(AutoWorld.call_single(world, "create_filler", player))
            world.random.shuffle(items_to_add)
            world.itempool.extend(items_to_add[:itemcount - len(world.itempool)])

    if any(world.item_links.values()):
        world._recache()
        world._all_state = None

    logger.info("Running Item Plando")

    for item in world.itempool:
        item.world = world

    distribute_planned(world)

    logger.info('Running Pre Main Fill.')

    AutoWorld.call_all(world, "pre_fill")

    logger.info(f'Filling the world with {len(world.itempool)} items.')

    if world.algorithm == 'flood':
        flood_items(world)  # different algo, biased towards early game progress items
    elif world.algorithm == 'balanced':
        distribute_items_restrictive(world)

    AutoWorld.call_all(world, 'post_fill')

    if world.players > 1:
        balance_multiworld_progression(world)

    logger.info(f'Beginning output...')
    outfilebase = 'AP_' + world.seed_name

    output = tempfile.TemporaryDirectory()
    with output as temp_dir:
        with concurrent.futures.ThreadPoolExecutor(world.players + 2) as pool:
            check_accessibility_task = pool.submit(world.fulfills_accessibility)

            output_file_futures = [pool.submit(AutoWorld.call_stage, world, "generate_output", temp_dir)]
            for player in world.player_ids:
                # skip starting a thread for methods that say "pass".
                if AutoWorld.World.generate_output.__code__ is not world.worlds[player].generate_output.__code__:
                    output_file_futures.append(
                        pool.submit(AutoWorld.call_single, world, "generate_output", player, temp_dir))

            def get_entrance_to_region(region: Region):
                for entrance in region.entrances:
                    if entrance.parent_region.type in (RegionType.DarkWorld, RegionType.LightWorld, RegionType.Generic):
                        return entrance
                for entrance in region.entrances:  # BFS might be better here, trying DFS for now.
                    return get_entrance_to_region(entrance.parent_region)

            # collect ER hint info
            er_hint_data = {player: {} for player in world.get_game_players("A Link to the Past") if
                            world.shuffle[player] != "vanilla" or world.retro_caves[player]}

            for region in world.regions:
                if region.player in er_hint_data and region.locations:
                    main_entrance = get_entrance_to_region(region)
                    for location in region.locations:
                        if type(location.address) == int:  # skips events and crystals
                            if lookup_vanilla_location_to_entrance[location.address] != main_entrance.name:
                                er_hint_data[region.player][location.address] = main_entrance.name

            checks_in_area = {player: {area: list() for area in ordered_areas}
                              for player in range(1, world.players + 1)}

            for player in range(1, world.players + 1):
                checks_in_area[player]["Total"] = 0

            for location in world.get_filled_locations():
                if type(location.address) is int:
                    main_entrance = get_entrance_to_region(location.parent_region)
                    if location.game != "A Link to the Past":
                        checks_in_area[location.player]["Light World"].append(location.address)
                    elif location.parent_region.dungeon:
                        dungeonname = {'Inverted Agahnims Tower': 'Agahnims Tower',
                                       'Inverted Ganons Tower': 'Ganons Tower'} \
                            .get(location.parent_region.dungeon.name, location.parent_region.dungeon.name)
                        checks_in_area[location.player][dungeonname].append(location.address)
                    elif location.parent_region.type == RegionType.LightWorld:
                        checks_in_area[location.player]["Light World"].append(location.address)
                    elif location.parent_region.type == RegionType.DarkWorld:
                        checks_in_area[location.player]["Dark World"].append(location.address)
                    elif main_entrance.parent_region.type == RegionType.LightWorld:
                        checks_in_area[location.player]["Light World"].append(location.address)
                    elif main_entrance.parent_region.type == RegionType.DarkWorld:
                        checks_in_area[location.player]["Dark World"].append(location.address)
                    checks_in_area[location.player]["Total"] += 1

            oldmancaves = []
            takeanyregions = ["Old Man Sword Cave", "Take-Any #1", "Take-Any #2", "Take-Any #3", "Take-Any #4"]
            for index, take_any in enumerate(takeanyregions):
                for region in [world.get_region(take_any, player) for player in
                               world.get_game_players("A Link to the Past") if world.retro_caves[player]]:
                    item = world.create_item(
                        region.shop.inventory[(0 if take_any == "Old Man Sword Cave" else 1)]['item'],
                        region.player)
                    player = region.player
                    location_id = SHOP_ID_START + total_shop_slots + index

                    main_entrance = get_entrance_to_region(region)
                    if main_entrance.parent_region.type == RegionType.LightWorld:
                        checks_in_area[player]["Light World"].append(location_id)
                    else:
                        checks_in_area[player]["Dark World"].append(location_id)
                    checks_in_area[player]["Total"] += 1

                    er_hint_data[player][location_id] = main_entrance.name
                    oldmancaves.append(((location_id, player), (item.code, player)))

            FillDisabledShopSlots(world)

            def write_multidata():
                import NetUtils
                slot_data = {}
                client_versions = {}
                games = {}
                minimum_versions = {"server": AutoWorld.World.required_server_version, "clients": client_versions}
                slot_info = {}
                names = [[name for player, name in sorted(world.player_name.items())]]
                for slot in world.player_ids:
                    player_world: AutoWorld.World = world.worlds[slot]
                    minimum_versions["server"] = max(minimum_versions["server"], player_world.required_server_version)
                    client_versions[slot] = player_world.required_client_version
                    games[slot] = world.game[slot]
                    slot_info[slot] = NetUtils.NetworkSlot(names[0][slot - 1], world.game[slot],
                                                           world.player_types[slot])
                for slot, group in world.groups.items():
                    games[slot] = world.game[slot]
                    slot_info[slot] = NetUtils.NetworkSlot(group["name"], world.game[slot], world.player_types[slot],
                                                           group_members=sorted(group["players"]))
                precollected_items = {player: [item.code for item in world_precollected if type(item.code) == int]
                                      for player, world_precollected in world.precollected_items.items()}
                precollected_hints = {player: set() for player in range(1, world.players + 1 + len(world.groups))}


                for slot in world.player_ids:
                    slot_data[slot] = world.worlds[slot].fill_slot_data()

                def precollect_hint(location):
                    entrance = er_hint_data.get(location.player, {}).get(location.address, "")
                    hint = NetUtils.Hint(location.item.player, location.player, location.address,
                                         location.item.code, False, entrance, location.item.flags)
                    precollected_hints[location.player].add(hint)
                    if location.item.player not in world.groups:
                        precollected_hints[location.item.player].add(hint)
                    else:
                        for player in world.groups[location.item.player]["players"]:
                            precollected_hints[player].add(hint)

                locations_data: Dict[int, Dict[int, Tuple[int, int, int]]] = {player: {} for player in world.player_ids}
                for location in world.get_filled_locations():
                    if type(location.address) == int:
                        assert location.item.code is not None, "item code None should be event, " \
                                                               "location.address should then also be None"
                        locations_data[location.player][location.address] = \
                            location.item.code, location.item.player, location.item.flags
                        if location.name in world.start_location_hints[location.player]:
                            precollect_hint(location)
                        elif location.item.name in world.start_hints[location.item.player]:
                            precollect_hint(location)
                        elif any([location.item.name in world.start_hints[player]
                                  for player in world.groups.get(location.item.player, {}).get("players", [])]):
                            precollect_hint(location)

                multidata = {
                    "slot_data": slot_data,
                    "slot_info": slot_info,
                    "names": names,  # TODO: remove around 0.2.5 in favor of slot_info
                    "games": games,  # TODO: remove around 0.2.5 in favor of slot_info
                    "connect_names": {name: (0, player) for player, name in world.player_name.items()},
                    "remote_items": {player for player in world.player_ids if
                                     world.worlds[player].remote_items},
                    "remote_start_inventory": {player for player in world.player_ids if
                                               world.worlds[player].remote_start_inventory},
                    "locations": locations_data,
                    "checks_in_area": checks_in_area,
                    "server_options": baked_server_options,
                    "er_hint_data": er_hint_data,
                    "precollected_items": precollected_items,
                    "precollected_hints": precollected_hints,
                    "version": tuple(version_tuple),
                    "tags": ["AP"],
                    "minimum_versions": minimum_versions,
                    "seed_name": world.seed_name
                }
                AutoWorld.call_all(world, "modify_multidata", multidata)

                multidata = zlib.compress(pickle.dumps(multidata), 9)

                with open(os.path.join(temp_dir, f'{outfilebase}.archipelago'), 'wb') as f:
                    f.write(bytes([3]))  # version of format
                    f.write(multidata)

            multidata_task = pool.submit(write_multidata)
            if not check_accessibility_task.result():
                if not world.can_beat_game():
                    raise Exception("Game appears as unbeatable. Aborting.")
                else:
                    logger.warning("Location Accessibility requirements not fulfilled.")

            # retrieve exceptions via .result() if they occurred.
            multidata_task.result()
            for i, future in enumerate(concurrent.futures.as_completed(output_file_futures), start=1):
                if i % 10 == 0 or i == len(output_file_futures):
                    logger.info(f'Generating output files ({i}/{len(output_file_futures)}).')
                future.result()

        if args.spoiler > 1:
            logger.info('Calculating playthrough.')
            create_playthrough(world)

        if args.spoiler:
            world.spoiler.to_file(os.path.join(temp_dir, '%s_Spoiler.txt' % outfilebase))

        zipfilename = output_path(f"AP_{world.seed_name}.zip")
        logger.info(f'Creating final archive at {zipfilename}.')
        with zipfile.ZipFile(zipfilename, mode="w", compression=zipfile.ZIP_DEFLATED,
                             compresslevel=9) as zf:
            for file in os.scandir(temp_dir):
                zf.write(file.path, arcname=file.name)

    logger.info('Done. Enjoy. Total Time: %s', time.perf_counter() - start)
    return world
Пример #13
0
def balance_multiworld_progression(world):
    balanceable_players = {
        player
        for player in range(1, world.players + 1)
        if world.progression_balancing[player]
    }
    if not balanceable_players:
        logging.info('Skipping multiworld progression balancing.')
    else:
        logging.info(
            f'Balancing multiworld progression for {len(balanceable_players)} Players.'
        )
        state = CollectionState(world)
        checked_locations = []
        unchecked_locations = world.get_locations().copy()
        world.random.shuffle(unchecked_locations)

        reachable_locations_count = {player: 0 for player in world.player_ids}

        def get_sphere_locations(sphere_state, locations):
            sphere_state.sweep_for_events(key_only=True, locations=locations)
            return [loc for loc in locations if sphere_state.can_reach(loc)]

        while True:
            sphere_locations = get_sphere_locations(state, unchecked_locations)
            for location in sphere_locations:
                unchecked_locations.remove(location)
                reachable_locations_count[location.player] += 1

            if checked_locations:
                threshold = max(reachable_locations_count.values()) - 20
                balancing_players = [
                    player for player, reachables in
                    reachable_locations_count.items()
                    if reachables < threshold and player in balanceable_players
                ]
                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 = collections.defaultdict(list)
                    while True:
                        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 player in balancing_players and location.player != player:
                                    candidate_items[player].append(location)
                        balancing_sphere = get_sphere_locations(
                            balancing_state, balancing_unchecked_locations)
                        for location in balancing_sphere:
                            balancing_unchecked_locations.remove(location)
                            balancing_reachables[location.player] += 1
                        if world.has_beaten_game(balancing_state) or all(
                                reachables >= threshold for reachables in
                                balancing_reachables.values()):
                            break
                        elif not balancing_sphere:
                            raise RuntimeError(
                                'Not all required items reachable. Something went terribly wrong here.'
                            )
                    unlocked_locations = collections.defaultdict(list)
                    for l in unchecked_locations:
                        if l not in balancing_unchecked_locations:
                            unlocked_locations[l.player].append(l)
                    items_to_replace = []
                    for player in balancing_players:
                        locations_to_test = unlocked_locations[player]
                        items_to_test = candidate_items[player]
                        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)
                                if reachable_locations_count[player] + len(
                                        reduced_sphere) < threshold:
                                    items_to_replace.append(testing)

                    replaced_items = False
                    replacement_locations = [
                        l for l in checked_locations
                        if not l.event and not l.locked
                    ]
                    while replacement_locations and items_to_replace:
                        new_location = replacement_locations.pop()
                        old_location = items_to_replace.pop()

                        while not new_location.can_fill(
                                state, old_location.item, False) or (
                                    new_location.item
                                    and not old_location.can_fill(
                                        state, new_location.item, False)):
                            replacement_locations.insert(0, new_location)
                            new_location = replacement_locations.pop()

                        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}"
                        )
                        state.collect(new_location.item, True, new_location)
                        replaced_items = True

                    if replaced_items:
                        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)
                            reachable_locations_count[location.player] += 1
                            sphere_locations.append(location)

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

            if world.has_beaten_game(state):
                break
            elif not sphere_locations:
                raise RuntimeError(
                    'Not all required items reachable. Something went terribly wrong here.'
                )
Пример #14
0
def create_playthrough(world):
    # create a copy as we will modify it
    old_world = world
    world = copy_world(world)

    # in treasure hunt and pedestal goals, ganon is invincible
    if world.goal in ['pedestal', 'starhunt', 'triforcehunt']:
        world.get_location('Ganon').item = None

    # if we only check for beatable, we can do this sanity check first before writing down spheres
    if world.check_beatable_only and not world.can_beat_game():
        raise RuntimeError(
            'Cannot beat game. Something went terribly wrong here!')

    # get locations containing progress items
    prog_locations = [
        location for location in world.get_locations()
        if location.item is not None and location.item.advancement
    ]

    collection_spheres = []
    state = CollectionState(world)
    sphere_candidates = list(prog_locations)
    logging.getLogger('').debug('Building up collection spheres.')
    while sphere_candidates:
        state.sweep_for_events(key_only=True)

        sphere = []
        # build up spheres of collection radius. Everything in each sphere is independent from each other in dependencies and only depends on lower spheres
        for location in sphere_candidates:
            if state.can_reach(location):
                sphere.append(location)

        for location in sphere:
            sphere_candidates.remove(location)
            state.collect(location.item, True)

        collection_spheres.append(sphere)

        logging.getLogger('').debug(
            'Calculated sphere %i, containing %i of %i progress items.' %
            (len(collection_spheres), len(sphere), len(prog_locations)))

        if not sphere:
            logging.getLogger('').debug(
                'The following items could not be reached: %s' % [
                    '%s at %s' % (location.item.name, location.name)
                    for location in sphere_candidates
                ])
            if not world.check_beatable_only:
                raise RuntimeError(
                    'Not all progression items reachable. Something went terribly wrong here.'
                )
            else:
                break

    # in the second phase, we cull each sphere such that the game is still beatable, reducing each range of influence to the bare minimum required inside it
    for sphere in reversed(collection_spheres):
        to_delete = []
        for location in sphere:
            # we remove the item at location and check if game is still beatable
            logging.getLogger('').debug(
                'Checking if %s is required to beat the game.' %
                location.item.name)
            old_item = location.item
            location.item = None
            state.remove(old_item)
            world._item_cache = {}  # need to invalidate
            if world.can_beat_game():
                to_delete.append(location)
            else:
                # still required, got to keep it around
                location.item = old_item

        # cull entries in spheres for spoiler walkthrough at end
        for location in to_delete:
            sphere.remove(location)

    # we are now down to just the required progress items in collection_spheres in a minimum number of spheres. As a cleanup, we right trim empty spheres (can happen if we have multiple triforces)
    collection_spheres = [sphere for sphere in collection_spheres if sphere]

    # store the required locations for statistical analysis
    old_world.required_locations = [
        location.name for sphere in collection_spheres for location in sphere
    ]

    # we can finally output our playthrough
    return 'Playthrough:\n' + ''.join([
        '%s: {\n%s}\n' % (i + 1, ''.join(
            ['  %s: %s\n' % (location, location.item) for location in sphere]))
        for i, sphere in enumerate(collection_spheres)
    ]) + '\n'
Пример #15
0
def fill_dungeons(world):
    ES = (['Hyrule Castle'], None, [ESSmallKey()], [ESMap()])
    EP = (['Eastern Palace'], EPBigKey(), [], [EPMap(), EPCompass()])
    DP = (['Desert Palace Main', 'Desert Palace East', 'Desert Palace North'],
          DPBigKey(), [DPSmallKey()], [DPCompass(), DPMap()])
    ToH = ([
        'Tower of Hera (Bottom)', 'Tower of Hera (Basement)',
        'Tower of Hera (Top)'
    ], THBigKey(), [THSmallKey()], [THCompass(), THMap()])
    AT = (['Agahnims Tower', 'Agahnim 1'], None, [ATSmallKey(),
                                                  ATSmallKey()], [])
    PoD = ([
        'Dark Palace (Entrance)', 'Dark Palace (Center)',
        'Dark Palace (Big Key Chest)', 'Dark Palace (Bonk Section)',
        'Dark Palace (North)', 'Dark Palace (Maze)',
        'Dark Palace (Spike Statue Room)', 'Dark Palace (Final Section)'
    ], PDBigKey(), [
        PDSmallKey(),
        PDSmallKey(),
        PDSmallKey(),
        PDSmallKey(),
        PDSmallKey(),
        PDSmallKey()
    ], [PDCompass(), PDMap()])
    TT = (['Thieves Town (Entrance)', 'Thieves Town (Deep)',
           'Blind Fight'], TTBigKey(), [TTSmallKey()], [TTCompass(),
                                                        TTMap()])
    SW = ([
        'Skull Woods First Section', 'Skull Woods Second Section',
        'Skull Woods Final Section (Entrance)',
        'Skull Woods Final Section (Mothula)'
    ], SWBigKey(), [SWSmallKey(), SWSmallKey()], [SWCompass(),
                                                  SWMap()])
    SP = ([
        'Swamp Palace (Entrance)', 'Swamp Palace (First Room)',
        'Swamp Palace (Starting Area)', 'Swamp Palace (Center)',
        'Swamp Palace (North)'
    ], SPBigKey(), [SPSmallKey()], [SPMap(), SPCompass()])
    IP = ([
        'Ice Palace (Entrance)', 'Ice Palace (Main)', 'Ice Palace (East)',
        'Ice Palace (East Top)', 'Ice Palace (Kholdstare)'
    ], IPBigKey(), [IPSmallKey(), IPSmallKey()], [IPMap(),
                                                  IPCompass()])
    MM = ([
        'Misery Mire (Entrance)', 'Misery Mire (Main)', 'Misery Mire (West)',
        'Misery Mire (Final Area)', 'Misery Mire (Vitreous)'
    ], MMBigKey(), [MMSmallKey(), MMSmallKey(),
                    MMSmallKey()], [MMCompass(), MMMap()])
    TR = ([
        'Turtle Rock (Entrance)', 'Turtle Rock (First Section)',
        'Turtle Rock (Chain Chomp Room)', 'Turtle Rock (Second Section)',
        'Turtle Rock (Big Chest)', 'Turtle Rock (Roller Switch Room)',
        'Turtle Rock (Dark Room)', 'Turtle Rock (Eye Bridge)',
        'Turtle Rock (Trinexx)'
    ], TRBigKey(), [TRSmallKey(),
                    TRSmallKey(),
                    TRSmallKey(),
                    TRSmallKey()], [TRMap(), TRCompass()])
    GT = ([
        'Ganons Tower (Entrance)', 'Ganons Tower (Tile Room)',
        'Ganons Tower (Compass Room)', 'Ganons Tower (Hookshot Room)',
        'Ganons Tower (Map Room)', 'Ganons Tower (Firesnake Room)',
        'Ganons Tower (Teleport Room)', 'Ganons Tower (Bottom)',
        'Ganons Tower (Top)', 'Ganons Tower (Before Moldorm)',
        'Ganons Tower (Moldorm)', 'Agahnim 2'
    ], GTBigKey(), [GTSmallKey(),
                    GTSmallKey(),
                    GTSmallKey(),
                    GTSmallKey()], [GTMap(), GTCompass()])

    freebes = [
        '[dungeon-A2-1F] Ganons Tower - Map Room',
        '[dungeon-D1-1F] Dark Palace - Spike Statue Room',
        '[dungeon-D1-1F] Dark Palace - Big Key Room'
    ]

    # this key is in a fixed location (for now)
    world.push_item(
        world.get_location(
            '[dungeon - D3 - B1] Skull Woods - South of Big Chest'),
        SWSmallKey(), False)

    for dungeon_regions, big_key, small_keys, dungeon_items in [
            TR, ES, EP, DP, ToH, AT, PoD, TT, SW, SP, IP, MM, GT
    ]:
        # this is what we need to fill
        dungeon_locations = [
            location for location in world.get_unfilled_locations()
            if location.parent_region.name in dungeon_regions
        ]
        random.shuffle(dungeon_locations)

        all_state = CollectionState(world, True)

        # first place big key
        if big_key is not None:
            bk_location = None
            for location in dungeon_locations:
                if location.item_rule(big_key):
                    bk_location = location
                    break

            if bk_location is None:
                raise RuntimeError('No suitable location for %s' % big_key)

            world.push_item(bk_location, big_key, False)
            dungeon_locations.remove(bk_location)
            all_state._clear_cache()

        # next place small keys
        for small_key in small_keys:
            sk_location = None
            for location in dungeon_locations:
                if location.name in freebes or location.can_reach(all_state):
                    sk_location = location
                    break

            if sk_location is None:
                raise RuntimeError('No suitable location for %s' % small_key)

            world.push_item(sk_location, small_key, False)
            dungeon_locations.remove(sk_location)
            all_state._clear_cache()

        # next place dungeon items
        if world.place_dungeon_items:
            for dungeon_item in dungeon_items:
                di_location = dungeon_locations.pop()
                world.push_item(di_location, dungeon_item, False)

    world.state._clear_cache()
Пример #16
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