예제 #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 fill_restrictive(window, worlds, base_state_list, locations, itempool, count=-1):
    unplaced_items = []

    # loop until there are no items or locations
    while itempool and locations:
        # if remaining count is 0, return. Negative means unbounded.
        if count == 0:
            break

        # get and item and remove it from the itempool
        item_to_place = itempool.pop()

        # generate the max states that include every remaining item
        # this will allow us to place this item in a reachable location
        maximum_exploration_state_list = CollectionState.get_states_with_items(base_state_list, itempool + unplaced_items)     

        # perform_access_check checks location reachability
        perform_access_check = True
        if worlds[0].check_beatable_only:
            # if any world can not longer be beatable with the remaining items
            # then we must check for reachability no matter what.
            # This way the reachability test is monotonic. If we were to later
            # stop checking, then we could place an item needed in one world
            # in an unreachable place in another world
            perform_access_check = not CollectionState.can_beat_game(maximum_exploration_state_list)

        # find a location that the item can be places. It must be a valid location
        # in the world we are placing it (possibly checking for reachability)
        spot_to_fill = None
        for location in locations:
            if location.can_fill(maximum_exploration_state_list[location.world.id], item_to_place, perform_access_check):
                spot_to_fill = location
                break

        # if we failed to find a suitable location
        if spot_to_fill is None:
            # if we specify a count, then we only want to place a subset, so a miss might be ok
            if count > 0:
                # don't decrement count, we didn't place anything
                unplaced_items.append(item_to_place)
                continue                
            else:
                # we expect all items to be placed
                raise FillError('Game unbeatable: No more spots to place %s [World %d]' % (item_to_place, item_to_place.world.id))
            
        # Place the item in the world and continue
        spot_to_fill.world.push_item(spot_to_fill, item_to_place)
        locations.remove(spot_to_fill)
        window.fillcount += 1
        window.update_progress(5 + ((window.fillcount / window.locationcount) * 30))

        # decrement count
        count -= 1

    # assert that the specified number of items were placed
    if count > 0:
        raise FillError('Could not place the specified number of item. %d remaining to be placed.' % count)
    # re-add unplaced items that were skipped
    itempool.extend(unplaced_items)        
예제 #4
0
def fill_restrictive(worlds, base_state_list, locations, itempool):
    # loop until there are no items or locations
    while itempool and locations:
        # get and item and remove it from the itempool
        item_to_place = itempool.pop()

        # generate the max states that include every remaining item
        # this will allow us to place this item in a reachable location
        maximum_exploration_state_list = CollectionState.get_states_with_items(
            base_state_list, itempool)

        # perform_access_check checks location reachability
        perform_access_check = True
        if worlds[0].check_beatable_only:
            # if any world can not longer be beatable with the remaining items
            # then we must check for reachability no matter what.
            # This way the reachability test is monotonic. If we were to later
            # stop checking, then we could place an item needed in one world
            # in an unreachable place in another world
            perform_access_check = not CollectionState.can_beat_game(
                maximum_exploration_state_list)

        # find a location that the item can be places. It must be a valid location
        # in the world we are placing it (possibly checking for reachability)
        spot_to_fill = None
        for location in locations:
            if location.can_fill(
                    maximum_exploration_state_list[location.world.id],
                    item_to_place, perform_access_check):
                spot_to_fill = location
                break

        # if we failed to find a suitable location, then stop placing items
        if spot_to_fill is None:
            # Maybe the game can be beaten anyway?
            if not CollectionState.can_beat_game(
                    maximum_exploration_state_list):
                raise FillError(
                    'Game unbeatable: No more spots to place %s [World %d]' %
                    (item_to_place, item_to_place.world.id))

            if not worlds[0].check_beatable_only:
                logging.getLogger('').warning(
                    'Not all items placed. Game beatable anyway.')
            break

        # Place the item in the world and continue
        spot_to_fill.world.push_item(spot_to_fill, item_to_place)
        locations.remove(spot_to_fill)
예제 #5
0
def fill_shops(window, worlds, locations, shoppool, itempool, attempts=15):
    # List of states with all items
    all_state_base_list = CollectionState.get_states_with_items(
        [world.state for world in worlds], itempool)

    while attempts:
        attempts -= 1
        try:
            prizepool = list(shoppool)
            prize_locs = list(locations)
            random.shuffle(prizepool)
            random.shuffle(prize_locs)
            fill_restrictive(window, worlds, all_state_base_list, prize_locs,
                             prizepool)
            logging.getLogger('').info("Shop items placed")
        except FillError as e:
            logging.getLogger('').info(
                "Failed to place shop items. Will retry %s more times",
                attempts)
            for location in locations:
                location.item = None
            logging.getLogger('').info('\t%s' % str(e))
            continue
        break
    else:
        raise FillError('Unable to place shops')
예제 #6
0
def fill_songs(window, worlds, locations, songpool, itempool, attempts=15):
    # get the song locations for each world

    # look for preplaced items
    placed_prizes = [loc.item.name for loc in locations if loc.item is not None]
    unplaced_prizes = [song for song in songpool if song.name not in placed_prizes]
    empty_song_locations = [loc for loc in locations if loc.item is None]

    # List of states with all items
    all_state_base_list = CollectionState.get_states_with_items([world.state for world in worlds], itempool)

    while attempts:
        attempts -= 1
        try:
            prizepool = list(unplaced_prizes)
            prize_locs = list(empty_song_locations)
            random.shuffle(prizepool)
            random.shuffle(prize_locs)
            fill_restrictive(window, worlds, all_state_base_list, prize_locs, prizepool)
            logging.getLogger('').info("Songs placed")
        except FillError as e:
            logging.getLogger('').info("Failed to place songs. Will retry %s more times", attempts)
            for location in empty_song_locations:
                location.item = None
            logging.getLogger('').info('\t%s' % str(e))
            continue
        break
    else:
        raise FillError('Unable to place songs')
예제 #7
0
def sweep_from_pool(
    base_state: CollectionState, itempool: typing.Sequence[Item] = tuple()
) -> CollectionState:
    new_state = base_state.copy()
    for item in itempool:
        new_state.collect(item, True)
    new_state.sweep_for_events()
    return new_state
예제 #8
0
def fill_dungeon_unique_item(window, worlds, fill_locations, itempool):
    # We should make sure that we don't count event items, shop items,
    # token items, or dungeon items as a major item. itempool at this
    # point should only be able to have tokens of those restrictions
    # since the rest are already placed.
    major_items = [item for item in itempool if item.majoritem]
    minor_items = [item for item in itempool if not item.majoritem]

    dungeons = [dungeon for world in worlds for dungeon in world.dungeons]
    double_dungeons = []
    for dungeon in dungeons:
        # we will count spirit temple twice so that it gets 2 items to match vanilla
        if dungeon.name == 'Spirit Temple':
            double_dungeons.append(dungeon)
    dungeons.extend(double_dungeons)

    random.shuffle(dungeons)
    random.shuffle(itempool)

    all_other_item_state = CollectionState.get_states_with_items([world.state for world in worlds], minor_items)
    all_dungeon_locations = []

    # iterate of all the dungeons in a random order, placing the item there
    for dungeon in dungeons:
        dungeon_locations = [location for region in dungeon.regions for location in region.locations if location in fill_locations]
        if dungeon.name == 'Spirit Temple':
            # spirit temple is weird and includes a couple locations outside of the dungeon
            dungeon_locations.extend(filter(lambda location: location in fill_locations, [dungeon.world.get_location(location) for location in ['Mirror Shield Chest', 'Silver Gauntlets Chest']]))

        # cache this list to flag afterwards
        all_dungeon_locations.extend(dungeon_locations)

        # place 1 item into the dungeon
        random.shuffle(dungeon_locations)
        fill_restrictive(window, worlds, all_other_item_state, dungeon_locations, major_items, 1)

        # update the location and item pool, removing any placed items and filled locations
        # the fact that you can remove items from a list you're iterating over is python magic
        for item in itempool:
            if item.location != None:
                fill_locations.remove(item.location)
                itempool.remove(item)

    # flag locations to not place further major items. it's important we do it on the 
    # locations instead of the dungeon because some locations are not in the dungeon
    for location in all_dungeon_locations:
        location.minor_only = True

    logging.getLogger('').info("Unique dungeon items placed")
예제 #9
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
예제 #10
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
예제 #11
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."
                 )
예제 #12
0
def fill_dungeons_restrictive(window, worlds, shuffled_locations, dungeon_items, itempool):
    # List of states with all non-key items
    all_state_base_list = CollectionState.get_states_with_items([world.state for world in worlds], itempool)

    # shuffle this list to avoid placement bias
    random.shuffle(dungeon_items)

    # sort in the order Boss Key, Small Key, Other before placing dungeon items
    # python sort is stable, so the ordering is still random within groups
    sort_order = {"BossKey": 3, "SmallKey": 2}
    dungeon_items.sort(key=lambda item: sort_order.get(item.type, 1))

    # place dungeon items
    fill_restrictive(window, worlds, all_state_base_list, shuffled_locations, dungeon_items)

    for world in worlds:
        world.state.clear_cached_unreachable()
예제 #13
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'
예제 #14
0
def set_shop_rules(ootworld):
    found_bombchus = ootworld.parser.parse_rule('found_bombchus')
    wallet = ootworld.parser.parse_rule('Progressive_Wallet')
    wallet2 = ootworld.parser.parse_rule('(Progressive_Wallet, 2)')

    for location in filter(
            lambda location: location.item and location.item.type == 'Shop',
            ootworld.get_locations()):
        # Add wallet requirements
        if location.item.name in [
                'Buy Arrows (50)', 'Buy Fish', 'Buy Goron Tunic',
                'Buy Bombchu (20)', 'Buy Bombs (30)'
        ]:
            add_rule(location, wallet)
        elif location.item.name in ['Buy Zora Tunic', 'Buy Blue Fire']:
            add_rule(location, wallet2)

        # Add adult only checks
        if location.item.name in ['Buy Goron Tunic', 'Buy Zora Tunic']:
            add_rule(location,
                     ootworld.parser.parse_rule('is_adult', location))

        # Add item prerequisite checks
        if location.item.name in [
                'Buy Blue Fire', 'Buy Blue Potion', 'Buy Bottle Bug',
                'Buy Fish', 'Buy Green Potion', 'Buy Poe',
                'Buy Red Potion [30]', 'Buy Red Potion [40]',
                'Buy Red Potion [50]', 'Buy Fairy\'s Spirit'
        ]:
            add_rule(
                location, lambda state: CollectionState._oot_has_bottle(
                    state, ootworld.player))
        if location.item.name in [
                'Buy Bombchu (10)', 'Buy Bombchu (20)', 'Buy Bombchu (5)'
        ]:
            add_rule(location, found_bombchus)
예제 #15
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.'
            )
예제 #16
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)
        }
예제 #17
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
예제 #18
0
def main(settings):
    start = time.clock()

    # initialize the world

    worlds = []

    if not settings.world_count:
        settings.world_count = 1
    if settings.world_count < 1:
        raise Exception('World Count must be at least 1')
    if settings.player_num > settings.world_count or settings.player_num < 1:
        raise Exception('Player Num must be between 1 and %d' %
                        settings.world_count)

    for i in range(0, settings.world_count):
        worlds.append(World(settings))

    logger = logging.getLogger('')

    random.seed(worlds[0].numeric_seed)

    logger.info('OoT Randomizer Version %s  -  Seed: %s\n\n', __version__,
                worlds[0].seed)

    for id, world in enumerate(worlds):
        world.id = id
        logger.info('Generating World %d.' % id)

        logger.info('Creating Overworld')
        create_regions(world)
        logger.info('Creating Dungeons')
        create_dungeons(world)
        logger.info('Linking Entrances')
        link_entrances(world)
        logger.info('Calculating Access Rules.')
        set_rules(world)
        logger.info('Generating Item Pool.')
        generate_itempool(world)

    logger.info('Fill the world.')
    distribute_items_restrictive(worlds)

    if settings.create_spoiler:
        logger.info('Calculating playthrough.')
        create_playthrough(worlds)
    CollectionState.update_required_items(worlds)

    logger.info('Patching ROM.')

    if settings.world_count > 1:
        outfilebase = 'OoT_%s_%s_W%dP%d' % (
            worlds[0].settings_string, worlds[0].seed, worlds[0].world_count,
            worlds[0].player_num)
    else:
        outfilebase = 'OoT_%s_%s' % (worlds[0].settings_string, worlds[0].seed)

    output_dir = default_output_path(settings.output_dir)

    if not settings.suppress_rom:
        rom = LocalRom(settings)
        patch_rom(worlds[settings.player_num - 1], rom)

        rom_path = os.path.join(output_dir, '%s.z64' % outfilebase)

        rom.write_to_file(rom_path)
        if settings.compress_rom:
            logger.info('Compressing ROM.')
            if platform.system() == 'Windows':
                subprocess.call([
                    "Compress\\Compress.exe", rom_path,
                    os.path.join(output_dir, '%s-comp.z64' % outfilebase)
                ])
            elif platform.system() == 'Linux':
                subprocess.call([
                    "Compress/Compress", rom_path,
                    os.path.join(output_dir, '%s-comp.z64' % outfilebase)
                ])
            elif platform.system() == 'Darwin':
                subprocess.call(
                    ["Compress/Compress.out", ('%s.z64' % outfilebase)])
            else:
                logger.info('OS not supported for compression')

    if settings.create_spoiler:
        worlds[settings.player_num - 1].spoiler.to_file(
            os.path.join(output_dir, '%s_Spoiler.txt' % outfilebase))
    os.remove('hints.txt')
    logger.info('Done. Enjoy.')
    logger.debug('Total Time: %s', time.clock() - start)

    return worlds[settings.player_num - 1]
예제 #19
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)}
예제 #20
0
def fill_dungeon_unique_item(window,
                             worlds,
                             fill_locations,
                             itempool,
                             attempts=15):
    # We should make sure that we don't count event items, shop items,
    # token items, or dungeon items as a major item. itempool at this
    # point should only be able to have tokens of those restrictions
    # since the rest are already placed.
    major_items = [item for item in itempool if item.type != 'Token']
    token_items = [item for item in itempool if item.type == 'Token']

    while attempts:
        attempts -= 1
        try:
            # choose a random set of items and locations
            dungeon_locations = []
            for dungeon in [
                    dungeon for world in worlds for dungeon in world.dungeons
            ]:
                dungeon_locations.append(
                    random.choice([
                        location for region in dungeon.regions
                        for location in region.locations
                        if location in fill_locations
                    ]))
            dungeon_items = random.sample(major_items, len(dungeon_locations))

            new_dungeon_locations = list(dungeon_locations)
            new_dungeon_items = list(dungeon_items)
            non_dungeon_items = [
                item for item in major_items if item not in dungeon_items
            ]
            all_other_item_state = CollectionState.get_states_with_items(
                [world.state for world in worlds],
                token_items + non_dungeon_items)

            # attempt to place the items into the locations
            random.shuffle(new_dungeon_locations)
            random.shuffle(new_dungeon_items)
            fill_restrictive(window, worlds, all_other_item_state,
                             new_dungeon_locations, new_dungeon_items)
            if len(new_dungeon_locations) > 0:
                raise FillError('Not all items were placed successfully')

            logging.getLogger('').info("Unique dungeon items placed")

            # remove the placed items from the fill_location and itempool
            for location in dungeon_locations:
                fill_locations.remove(location)
            for item in dungeon_items:
                itempool.remove(item)

        except FillError as e:
            logging.getLogger('').info(
                "Failed to place unique dungeon items. Will retry %s more times",
                attempts)
            for location in dungeon_locations:
                location.item = None
            for dungeon in [
                    dungeon for world in worlds for dungeon in world.dungeons
            ]:
                dungeon.major_items = 0
            logging.getLogger('').info('\t%s' % str(e))
            continue
        break
    else:
        raise FillError('Unable to place unique dungeon items')
예제 #21
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'
예제 #22
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()
예제 #23
0
def main(settings, window=dummy_window()):

    start = time.clock()

    logger = logging.getLogger('')

    # verify that the settings are valid
    if settings.free_scarecrow:
        verify_scarecrow_song_str(settings.scarecrow_song,
                                  settings.ocarina_songs)

    # initialize the world

    worlds = []
    if settings.compress_rom == 'None':
        settings.create_spoiler = True
        settings.update()

    if not settings.world_count:
        settings.world_count = 1
    if settings.world_count < 1:
        raise Exception('World Count must be at least 1')
    if settings.player_num > settings.world_count or settings.player_num < 1:
        raise Exception('Player Num must be between 1 and %d' %
                        settings.world_count)

    for i in range(0, settings.world_count):
        worlds.append(World(settings))

    random.seed(worlds[0].numeric_seed)

    logger.info('OoT Randomizer Version %s  -  Seed: %s\n\n', __version__,
                worlds[0].seed)

    window.update_status('Creating the Worlds')
    for id, world in enumerate(worlds):
        world.id = id
        logger.info('Generating World %d.' % id)

        window.update_progress(0 + (((id + 1) / settings.world_count) * 1))
        logger.info('Creating Overworld')
        if world.quest == 'master':
            for dungeon in world.dungeon_mq:
                world.dungeon_mq[dungeon] = True
        elif world.quest == 'mixed':
            for dungeon in world.dungeon_mq:
                world.dungeon_mq[dungeon] = random.choice([True, False])
        else:
            for dungeon in world.dungeon_mq:
                world.dungeon_mq[dungeon] = False
        create_regions(world)

        window.update_progress(0 + (((id + 1) / settings.world_count) * 2))
        logger.info('Creating Dungeons')
        create_dungeons(world)

        window.update_progress(0 + (((id + 1) / settings.world_count) * 3))
        logger.info('Linking Entrances')
        link_entrances(world)

        if settings.shopsanity != 'off':
            world.random_shop_prices()

        window.update_progress(0 + (((id + 1) / settings.world_count) * 4))
        logger.info('Calculating Access Rules.')
        set_rules(world)

        window.update_progress(0 + (((id + 1) / settings.world_count) * 5))
        logger.info('Generating Item Pool.')
        generate_itempool(world)

    window.update_status('Placing the Items')
    logger.info('Fill the world.')
    distribute_items_restrictive(window, worlds)
    window.update_progress(35)

    if settings.create_spoiler:
        window.update_status('Calculating Spoiler Data')
        logger.info('Calculating playthrough.')
        create_playthrough(worlds)
        window.update_progress(50)
    if settings.hints != 'none':
        window.update_status('Calculating Hint Data')
        CollectionState.update_required_items(worlds)
        buildGossipHints(worlds[settings.player_num - 1])
        window.update_progress(55)

    logger.info('Patching ROM.')

    if settings.world_count > 1:
        outfilebase = 'OoT_%s_%s_W%dP%d' % (
            worlds[0].settings_string, worlds[0].seed, worlds[0].world_count,
            worlds[0].player_num)
    else:
        outfilebase = 'OoT_%s_%s' % (worlds[0].settings_string, worlds[0].seed)

    output_dir = default_output_path(settings.output_dir)

    if settings.compress_rom != 'None':
        window.update_status('Patching ROM')
        rom = LocalRom(settings)
        patch_rom(worlds[settings.player_num - 1], rom)
        window.update_progress(65)

        rom_path = os.path.join(output_dir, '%s.z64' % outfilebase)

        window.update_status('Saving Uncompressed ROM')
        rom.write_to_file(rom_path)
        if settings.compress_rom == 'True':
            window.update_status('Compressing ROM')
            logger.info('Compressing ROM.')

            compressor_path = ""
            if platform.system() == 'Windows':
                if 8 * struct.calcsize("P") == 64:
                    compressor_path = "Compress\\Compress.exe"
                else:
                    compressor_path = "Compress\\Compress32.exe"
            elif platform.system() == 'Linux':
                compressor_path = "Compress/Compress"
            elif platform.system() == 'Darwin':
                compressor_path = "Compress/Compress.out"
            else:
                logger.info('OS not supported for compression')

            run_process(window, logger, [
                compressor_path, rom_path,
                os.path.join(output_dir, '%s-comp.z64' % outfilebase)
            ])
            os.remove(rom_path)
            window.update_progress(95)

    if settings.create_spoiler:
        window.update_status('Creating Spoiler Log')
        worlds[settings.player_num - 1].spoiler.to_file(
            os.path.join(output_dir, '%s_Spoiler.txt' % outfilebase))

    window.update_progress(100)
    window.update_status('Success: Rom patched successfully')
    logger.info('Done. Enjoy.')
    logger.debug('Total Time: %s', time.clock() - start)

    return worlds[settings.player_num - 1]
예제 #24
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)
    ])
예제 #25
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)
        ])
예제 #26
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
예제 #27
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
예제 #28
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.'
                )
예제 #29
0
 def collect_item(self, state: CollectionState, item: Item, remove=False):
     item_name = item.name
     if item_name.startswith('Progressive '):
         if remove:
             if 'Sword' in item_name:
                 if state.has('Golden Sword', item.player):
                     return 'Golden Sword'
                 elif state.has('Tempered Sword', item.player):
                     return 'Tempered Sword'
                 elif state.has('Master Sword', item.player):
                     return 'Master Sword'
                 elif state.has('Fighter Sword', item.player):
                     return 'Fighter Sword'
                 else:
                     return None
             elif 'Glove' in item.name:
                 if state.has('Titans Mitts', item.player):
                     return 'Titans Mitts'
                 elif state.has('Power Glove', item.player):
                     return 'Power Glove'
                 else:
                     return None
             elif 'Shield' in item_name:
                 if state.has('Mirror Shield', item.player):
                     return 'Mirror Shield'
                 elif state.has('Red Shield', item.player):
                     return 'Red Shield'
                 elif state.has('Blue Shield', item.player):
                     return 'Blue Shield'
                 else:
                     return None
             elif 'Bow' in item_name:
                 if state.has('Silver Bow', item.player):
                     return 'Silver Bow'
                 elif state.has('Bow', item.player):
                     return 'Bow'
                 else:
                     return None
         else:
             if 'Sword' in item_name:
                 if state.has('Golden Sword', item.player):
                     pass
                 elif state.has(
                         'Tempered Sword', item.player
                 ) and self.world.difficulty_requirements[
                         item.player].progressive_sword_limit >= 4:
                     return 'Golden Sword'
                 elif state.has(
                         'Master Sword', item.player
                 ) and self.world.difficulty_requirements[
                         item.player].progressive_sword_limit >= 3:
                     return 'Tempered Sword'
                 elif state.has(
                         'Fighter Sword', item.player
                 ) and self.world.difficulty_requirements[
                         item.player].progressive_sword_limit >= 2:
                     return 'Master Sword'
                 elif self.world.difficulty_requirements[
                         item.player].progressive_sword_limit >= 1:
                     return 'Fighter Sword'
             elif 'Glove' in item_name:
                 if state.has('Titans Mitts', item.player):
                     return
                 elif state.has('Power Glove', item.player):
                     return 'Titans Mitts'
                 else:
                     return 'Power Glove'
             elif 'Shield' in item_name:
                 if state.has('Mirror Shield', item.player):
                     return
                 elif state.has(
                         'Red Shield', item.player
                 ) and self.world.difficulty_requirements[
                         item.player].progressive_shield_limit >= 3:
                     return 'Mirror Shield'
                 elif state.has(
                         'Blue Shield', item.player
                 ) and self.world.difficulty_requirements[
                         item.player].progressive_shield_limit >= 2:
                     return 'Red Shield'
                 elif self.world.difficulty_requirements[
                         item.player].progressive_shield_limit >= 1:
                     return 'Blue Shield'
             elif 'Bow' in item_name:
                 if state.has('Silver Bow', item.player):
                     return
                 elif state.has('Bow', item.player) and (
                         self.world.difficulty_requirements[
                             item.player].progressive_bow_limit >= 2
                         or self.world.logic[item.player] == 'noglitches'
                         or self.world.swordless[item.player]
                 ):  # modes where silver bow is always required for ganon
                     return 'Silver Bow'
                 elif self.world.difficulty_requirements[
                         item.player].progressive_bow_limit >= 1:
                     return 'Bow'
     elif item.advancement:
         return item_name