예제 #1
0
    def __init__(self, id, settings):
        self.id = id
        self.shuffle = 'vanilla'
        self.dungeons = []
        self.regions = []
        self.itempool = []
        self._cached_locations = None
        self._entrance_cache = {}
        self._region_cache = {}
        self._location_cache = {}
        self.required_locations = []
        self.shop_prices = {}
        self.scrub_prices = {}
        self.maximum_wallets = 0
        self.light_arrow_location = None
        self.triforce_count = 0
        self.bingosync_url = None

        self.parser = Rule_AST_Transformer(self)
        self.event_items = set()

        # dump settings directly into world's namespace
        # this gives the world an attribute for every setting listed in Settings.py
        self.settings = settings
        self.__dict__.update(settings.__dict__)
        self.distribution = settings.distribution.world_dists[id]

        # rename a few attributes...
        self.keysanity = self.shuffle_smallkeys in [
            'keysanity', 'remove', 'any_dungeon', 'overworld'
        ]
        self.check_beatable_only = not self.all_reachable

        self.shuffle_special_interior_entrances = self.shuffle_interior_entrances == 'all'
        self.shuffle_interior_entrances = self.shuffle_interior_entrances in [
            'simple', 'all'
        ]

        self.entrance_shuffle = self.shuffle_interior_entrances or self.shuffle_grotto_entrances or self.shuffle_dungeon_entrances or \
                                self.shuffle_overworld_entrances or self.owl_drops or self.warp_songs or self.spawn_positions

        self.ensure_tod_access = self.shuffle_interior_entrances or self.shuffle_overworld_entrances or self.spawn_positions
        self.disable_trade_revert = self.shuffle_interior_entrances or self.shuffle_overworld_entrances

        if self.open_forest == 'closed' and (
                self.shuffle_special_interior_entrances
                or self.shuffle_overworld_entrances or self.warp_songs
                or self.spawn_positions or self.decouple_entrances or
            (self.mix_entrance_pools != 'off')):
            self.open_forest = 'closed_deku'

        self.triforce_goal = self.triforce_goal_per_world * settings.world_count

        if self.triforce_hunt:
            # Pin shuffle_ganon_bosskey to 'triforce' when triforce_hunt is enabled
            # (specifically, for randomize_settings)
            self.shuffle_ganon_bosskey = 'triforce'

        # Determine LACS Condition
        if self.shuffle_ganon_bosskey == 'lacs_medallions':
            self.lacs_condition = 'medallions'
        elif self.shuffle_ganon_bosskey == 'lacs_dungeons':
            self.lacs_condition = 'dungeons'
        elif self.shuffle_ganon_bosskey == 'lacs_stones':
            self.lacs_condition = 'stones'
        elif self.shuffle_ganon_bosskey == 'lacs_tokens':
            self.lacs_condition = 'tokens'
        else:
            self.lacs_condition = 'vanilla'

        # trials that can be skipped will be decided later
        self.skipped_trials = {
            'Forest': False,
            'Fire': False,
            'Water': False,
            'Spirit': False,
            'Shadow': False,
            'Light': False
        }

        # dungeon forms will be decided later
        self.dungeon_mq = {
            'Deku Tree': False,
            'Dodongos Cavern': False,
            'Jabu Jabus Belly': False,
            'Bottom of the Well': False,
            'Ice Cavern': False,
            'Gerudo Training Grounds': False,
            'Forest Temple': False,
            'Fire Temple': False,
            'Water Temple': False,
            'Spirit Temple': False,
            'Shadow Temple': False,
            'Ganons Castle': False
        }

        self.can_take_damage = True

        self.resolve_random_settings()

        if len(settings.hint_dist_user) == 0:
            for d in HintDistFiles():
                dist = read_json(d)
                if dist['name'] == self.hint_dist:
                    self.hint_dist_user = dist
        else:
            self.hint_dist = 'custom'

        # Validate hint distribution format
        # Originally built when I was just adding the type distributions
        # Location/Item Additions and Overrides are not validated
        hint_dist_valid = False
        if all(key in self.hint_dist_user['distribution']
               for key in hint_dist_keys):
            hint_dist_valid = True
            sub_keys = {'order', 'weight', 'fixed', 'copies'}
            for key in self.hint_dist_user['distribution']:
                if not all(sub_key in sub_keys for sub_key in
                           self.hint_dist_user['distribution'][key]):
                    hint_dist_valid = False
        if not hint_dist_valid:
            raise InvalidFileException(
                """Hint distributions require all hint types be present in the distro 
                                          (trial, always, woth, barren, item, song, overworld, dungeon, entrance,
                                          sometimes, random, junk, named-item). If a hint type should not be
                                          shuffled, set its order to 0. Hint type format is \"type\": { 
                                          \"order\": 0, \"weight\": 0.0, \"fixed\": 0, \"copies\": 0 }"""
            )

        self.added_hint_types = {}
        self.item_added_hint_types = {}
        self.hint_exclusions = set()
        if self.skip_child_zelda or settings.skip_child_zelda:
            self.hint_exclusions.add('Song from Impa')
        self.hint_type_overrides = {}
        self.item_hint_type_overrides = {}
        for dist in hint_dist_keys:
            self.added_hint_types[dist] = []
            for loc in self.hint_dist_user['add_locations']:
                if 'types' in loc:
                    if dist in loc['types']:
                        self.added_hint_types[dist].append(loc['location'])
            self.item_added_hint_types[dist] = []
            for i in self.hint_dist_user['add_items']:
                if dist in i['types']:
                    self.item_added_hint_types[dist].append(i['item'])
            self.hint_type_overrides[dist] = []
            for loc in self.hint_dist_user['remove_locations']:
                if dist in loc['types']:
                    self.hint_type_overrides[dist].append(loc['location'])
            self.item_hint_type_overrides[dist] = []
            for i in self.hint_dist_user['remove_items']:
                if dist in i['types']:
                    self.item_hint_type_overrides[dist].append(i['item'])

        self.hint_text_overrides = {}
        for loc in self.hint_dist_user['add_locations']:
            if 'text' in loc:
                # Arbitrarily throw an error at 80 characters to prevent overfilling the text box.
                if len(loc['text']) > 80:
                    raise Exception('Custom hint text too large for %s',
                                    loc['location'])
                self.hint_text_overrides.update({loc['location']: loc['text']})

        self.always_hints = [hint.name for hint in getRequiredHints(self)]

        self.state = State(self)

        # Allows us to cut down on checking whether some items are required
        self.max_progressions = {
            item: value[3].get('progressive', 1) if value[3] else 1
            for item, value in item_table.items()
        }
        max_tokens = 0
        if self.bridge == 'tokens':
            max_tokens = max(max_tokens, self.bridge_tokens)
        if self.lacs_condition == 'tokens':
            max_tokens = max(max_tokens, self.lacs_tokens)
        tokens = [50, 40, 30, 20, 10]
        for t in tokens:
            if f'{t} Gold Skulltula Reward' not in self.disabled_locations:
                max_tokens = max(max_tokens, t)
        self.max_progressions['Gold Skulltula Token'] = max_tokens
        # Additional Ruto's Letter become Bottle, so we may have to collect two.
        self.max_progressions['Rutos Letter'] = 2
예제 #2
0
# items never required:
# refills, maps, compasses, capacity upgrades, masks (not listed in logic)
never_prefix = ['Bombs', 'Arrows', 'Rupee', 'Deku Seeds', 'Map', 'Compass']
never_suffix = ['Capacity']
never = {
    'Bunny Hood',
    'Recovery Heart',
    'Milk',
    'Ice Arrows',
    'Ice Trap',
    'Double Defense',
    'Biggoron Sword',
} | {
    item
    for item, (t, adv, _, special) in item_table.items()
    if adv is False or any(map(item.startswith, never_prefix))
    or any(map(item.endswith, never_suffix))
}

# items required at most once, specifically things with multiple possible names
# (except bottles)
once = {
    'Goron Tunic',
    'Zora Tunic',
}

progressive = {
    item
    for item, (_, _, _, special) in item_table.items()
    if special and 'progressive' in special
예제 #3
0
    def __init__(self, id, settings):
        self.id = id
        self.shuffle = 'vanilla'
        self.dungeons = []
        self.regions = []
        self.itempool = []
        self._cached_locations = None
        self._entrance_cache = {}
        self._region_cache = {}
        self._location_cache = {}
        self.required_locations = []
        self.shop_prices = {}
        self.scrub_prices = {}
        self.maximum_wallets = 0
        self.light_arrow_location = None
        self.triforce_count = 0

        self.parser = Rule_AST_Transformer(self)
        self.event_items = set()

        # dump settings directly into world's namespace
        # this gives the world an attribute for every setting listed in Settings.py
        self.settings = settings
        self.__dict__.update(settings.__dict__)
        self.distribution = settings.distribution.world_dists[id]

        if self.open_forest == 'closed' and self.entrance_shuffle in ['all-indoors', 'all']:
            self.open_forest = 'closed_deku'

        # rename a few attributes...
        self.keysanity = self.shuffle_smallkeys in ['keysanity', 'remove']
        self.check_beatable_only = not self.all_reachable
    
        self.shuffle_dungeon_entrances = self.entrance_shuffle != 'off'
        self.shuffle_grotto_entrances = self.entrance_shuffle in ['simple-indoors', 'all-indoors', 'all']
        self.shuffle_interior_entrances = self.entrance_shuffle in ['simple-indoors', 'all-indoors', 'all']
        self.shuffle_special_indoor_entrances = self.entrance_shuffle in ['all-indoors', 'all']
        self.shuffle_overworld_entrances = self.entrance_shuffle == 'all'

        self.disable_trade_revert = self.shuffle_interior_entrances or self.shuffle_overworld_entrances
        self.ensure_tod_access = self.shuffle_interior_entrances or self.shuffle_overworld_entrances

        self.triforce_goal = self.triforce_goal_per_world * settings.world_count

        if self.triforce_hunt:
            # Pin shuffle_ganon_bosskey to 'triforce' when triforce_hunt is enabled
            # (specifically, for randomize_settings)
            self.shuffle_ganon_bosskey = 'triforce'

        # Determine LACS Condition
        if self.shuffle_ganon_bosskey == 'lacs_medallions':
            self.lacs_condition = 'medallions'
        elif self.shuffle_ganon_bosskey == 'lacs_dungeons':
            self.lacs_condition = 'dungeons'
        elif self.shuffle_ganon_bosskey == 'lacs_stones':
            self.lacs_condition = 'stones'
        else:
            self.lacs_condition = 'vanilla'

        # trials that can be skipped will be decided later
        self.skipped_trials = {
            'Forest': False,
            'Fire': False,
            'Water': False,
            'Spirit': False,
            'Shadow': False,
            'Light': False
        }

        # dungeon forms will be decided later
        self.dungeon_mq = {
            'Deku Tree': False,
            'Dodongos Cavern': False,
            'Jabu Jabus Belly': False,
            'Bottom of the Well': False,
            'Ice Cavern': False,
            'Gerudo Training Grounds': False,
            'Forest Temple': False,
            'Fire Temple': False,
            'Water Temple': False,
            'Spirit Temple': False,
            'Shadow Temple': False,
            'Ganons Castle': False
        }

        self.can_take_damage = True

        self.resolve_random_settings()

        self.always_hints = [hint.name for hint in getRequiredHints(self)]
        self.state = State(self)

        # Allows us to cut down on checking whether some items are required
        self.max_progressions = {
                item: value[3].get('progressive', 1) if value[3] else 1
                for item, value in item_table.items()
        }
        max_tokens = 0
        if self.bridge == 'tokens':
            max_tokens = self.bridge_tokens
        tokens = [50, 40, 30, 20, 10]
        for t in tokens:
            if t > max_tokens and f'{t} Gold Skulltula Reward' not in self.disabled_locations:
                max_tokens = t
        self.max_progressions['Gold Skulltula Token'] = max_tokens
        # Additional Ruto's Letter become Bottle, so we may have to collect two.
        self.max_progressions['Ruto\'s Letter'] = 2
예제 #4
0
from Settings import Settings, get_preset_files

test_dir = os.path.join(os.path.dirname(__file__), 'tests')
output_dir = os.path.join(test_dir, 'Output')
os.makedirs(output_dir, exist_ok=True)

logging.basicConfig(level=logging.INFO, filename=os.path.join(output_dir, 'LAST_TEST_LOG'), filemode='w')

# items never required:
# refills, maps, compasses, capacity upgrades, masks (not listed in logic)
never_prefix = ['Bombs', 'Arrows', 'Rupee', 'Deku Seeds', 'Map', 'Compass']
never_suffix = ['Capacity']
never = {
    'Bunny Hood', 'Recovery Heart', 'Milk', 'Ice Arrows', 'Ice Trap',
    'Double Defense', 'Biggoron Sword', 'Giants Knife',
} | {item for item, (t, adv, _, special) in item_table.items() if adv is False
     or any(map(item.startswith, never_prefix)) or any(map(item.endswith, never_suffix))}

# items required at most once, specifically things with multiple possible names
# (except bottles)
once = {
    'Goron Tunic', 'Zora Tunic',
}

progressive = {
    item for item, (_, _, _, special) in item_table.items()
    if special and 'progressive' in special
}

bottles = {
    item for item, (_, _, _, special) in item_table.items()
예제 #5
0
def distribute_items_restrictive(window, worlds, fill_locations=None):
    song_locations = [
        world.get_location(location) for world in worlds for location in [
            'Song from Composers Grave', 'Song from Impa', 'Song from Malon',
            'Song from Saria', 'Song from Ocarina of Time',
            'Song from Windmill', 'Sheik in Forest', 'Sheik at Temple',
            'Sheik in Crater', 'Sheik in Ice Cavern', 'Sheik in Kakariko',
            'Sheik at Colossus'
        ]
    ]

    shop_locations = [
        location for world in worlds
        for location in world.get_unfilled_locations()
        if location.type == 'Shop' and location.price == None
    ]

    # If not passed in, then get a shuffled list of locations to fill in
    if not fill_locations:
        fill_locations = [location for world in worlds for location in world.get_unfilled_locations() \
            if location not in song_locations and \
               location not in shop_locations and \
               location.type != 'GossipStone']
    world_states = [world.state for world in worlds]

    window.locationcount = len(fill_locations) + len(song_locations) + len(
        shop_locations)
    window.fillcount = 0

    # Generate the itempools
    shopitempool = [
        item for world in worlds for item in world.itempool
        if item.type == 'Shop'
    ]
    songitempool = [
        item for world in worlds for item in world.itempool
        if item.type == 'Song'
    ]
    itempool = [
        item for world in worlds for item in world.itempool
        if item.type != 'Shop' and item.type != 'Song'
    ]

    if worlds[0].shuffle_song_items:
        itempool.extend(songitempool)
        fill_locations.extend(song_locations)
        songitempool = []
        song_locations = []

    # add unrestricted dungeon items to main item pool
    itempool.extend([
        item for world in worlds
        for item in world.get_unrestricted_dungeon_items()
    ])
    dungeon_items = [
        item for world in worlds
        for item in world.get_restricted_dungeon_items()
    ]

    random.shuffle(
        itempool
    )  # randomize item placement order. this ordering can greatly affect the location accessibility bias
    progitempool = [item for item in itempool if item.advancement]
    prioitempool = [
        item for item in itempool if not item.advancement and item.priority
    ]
    restitempool = [
        item for item in itempool if not item.advancement and not item.priority
    ]

    cloakable_locations = shop_locations + song_locations + fill_locations
    all_models = shopitempool + dungeon_items + songitempool + itempool
    worlds[0].settings.distribution.fill(
        window, worlds, [shop_locations, song_locations, fill_locations], [
            shopitempool, dungeon_items, songitempool, progitempool,
            prioitempool, restitempool
        ])
    itempool = progitempool + prioitempool + restitempool

    # set ice traps to have the appearance of other random items in the item pool
    ice_traps = [item for item in itempool if item.name == 'Ice Trap']
    # Extend with ice traps manually placed in plandomizer
    ice_traps.extend(
        location.item for location in cloakable_locations
        if (location.name in location_groups['CanSee']
            and location.item is not None and location.item.name == 'Ice Trap'
            and location.item.looks_like_item is None))
    junk_items = remove_junk_items.copy()
    junk_items.remove('Ice Trap')
    major_items = [
        item for (item, data) in item_table.items()
        if data[0] == 'Item' and data[1] and data[2] is not None
    ]
    fake_items = []
    if worlds[0].settings.ice_trap_appearance == 'major_only':
        model_items = [item for item in itempool if item.majoritem]
        if len(
                model_items
        ) == 0:  # All major items were somehow removed from the pool (can happen in plando)
            model_items = ItemFactory(major_items)
    elif worlds[0].settings.ice_trap_appearance == 'junk_only':
        model_items = [item for item in itempool if item.name in junk_items]
        if len(model_items) == 0:  # All junk was removed
            model_items = ItemFactory(junk_items)
    else:  # world[0].settings.ice_trap_appearance == 'anything':
        model_items = [item for item in itempool if item.name != 'Ice Trap']
        if len(
                model_items
        ) == 0:  # All major items and junk were somehow removed from the pool (can happen in plando)
            model_items = ItemFactory(major_items) + ItemFactory(junk_items)
    while len(ice_traps) > len(fake_items):
        # if there are more ice traps than model items, then double up on model items
        fake_items.extend(model_items)
    for random_item in random.sample(fake_items, len(ice_traps)):
        ice_trap = ice_traps.pop(0)
        ice_trap.looks_like_item = random_item

    # Start a search cache here.
    search = Search([world.state for world in worlds])

    # We place all the shop items first. Like songs, they have a more limited
    # set of locations that they can be placed in, so placing them first will
    # reduce the odds of creating unbeatable seeds. This also avoids needing
    # to create item rules for every location for whether they are a shop item
    # or not. This shouldn't have much affect on item bias.
    if shop_locations:
        logger.info('Placing shop items.')
        fill_ownworld_restrictive(window, worlds, search, shop_locations,
                                  shopitempool,
                                  itempool + songitempool + dungeon_items,
                                  "shop")
    # Update the shop item access rules
    for world in worlds:
        set_shop_rules(world)

    search.collect_locations()

    # If there are dungeon items that are restricted to their original dungeon,
    # we must place them first to make sure that there is always a location to
    # place them. This could probably be replaced for more intelligent item
    # placement, but will leave as is for now
    if dungeon_items:
        logger.info('Placing dungeon items.')
        fill_dungeons_restrictive(window, worlds, search, fill_locations,
                                  dungeon_items, itempool + songitempool)
        search.collect_locations()

    # places the songs into the world
    # Currently places songs only at song locations. if there's an option
    # to allow at other locations then they should be in the main pool.
    # Placing songs on their own since they have a relatively high chance
    # of failing compared to other item type. So this way we only have retry
    # the song locations only.
    if not worlds[0].shuffle_song_items:
        logger.info('Placing song items.')
        fill_ownworld_restrictive(window, worlds, search, song_locations,
                                  songitempool, progitempool, "song")
        search.collect_locations()
        fill_locations += [
            location for location in song_locations if location.item is None
        ]

    # Put one item in every dungeon, needs to be done before other items are
    # placed to ensure there is a spot available for them
    if worlds[0].one_item_per_dungeon:
        logger.info('Placing one major item per dungeon.')
        fill_dungeon_unique_item(window, worlds, search, fill_locations,
                                 progitempool)
        search.collect_locations()

    # Place all progression items. This will include keys in keysanity.
    # Items in this group will check for reachability and will be placed
    # such that the game is guaranteed beatable.
    logger.info('Placing progression items.')
    fill_restrictive(window, worlds, search, fill_locations, progitempool)
    search.collect_locations()

    # Place all priority items.
    # These items are items that only check if the item is allowed to be
    # placed in the location, not checking reachability. This is important
    # for things like Ice Traps that can't be found at some locations
    logger.info('Placing priority items.')
    fill_restrictive_fast(window, worlds, fill_locations, prioitempool)

    # Place the rest of the items.
    # No restrictions at all. Places them completely randomly. Since they
    # cannot affect the beatability, we don't need to check them
    logger.info('Placing the rest of the items.')
    fast_fill(window, fill_locations, restitempool)

    # Log unplaced item/location warnings
    for item in progitempool + prioitempool + restitempool:
        logger.error('Unplaced Items: %s [World %d]' %
                     (item.name, item.world.id))
    for location in fill_locations:
        logger.error('Unfilled Locations: %s [World %d]' %
                     (location.name, location.world.id))

    if progitempool + prioitempool + restitempool:
        raise FillError('Not all items are placed.')

    if fill_locations:
        raise FillError('Not all locations have an item.')

    if not search.can_beat_game():
        raise FillError('Cannot beat game!')

    worlds[0].settings.distribution.cloak(worlds, [cloakable_locations],
                                          [all_models])

    for world in worlds:
        for location in world.get_filled_locations():
            # Get the maximum amount of wallets required to purchase an advancement item.
            if world.maximum_wallets < 3 and location.price and location.item.advancement:
                if location.price > 500:
                    world.maximum_wallets = 3
                elif world.maximum_wallets < 2 and location.price > 200:
                    world.maximum_wallets = 2
                elif world.maximum_wallets < 1 and location.price > 99:
                    world.maximum_wallets = 1

            # Get Light Arrow location for later usage.
            if location.item and location.item.name == 'Light Arrows':
                location.item.world.light_arrow_location = location
예제 #6
0
    'Ice Trap',
]


item_groups = {
    'Junk': remove_junk_items,
    'JunkSong': ('Prelude of Light', 'Serenade of Water'),
    'AdultTrade': tradeitems,
    'Bottle': normal_bottles,
    'Spell': ('Dins Fire', 'Farores Wind', 'Nayrus Love'),
    'Shield': ('Deku Shield', 'Hylian Shield'),
    'Song': songlist,
    'NonWarpSong': songlist[0:6],
    'WarpSong': songlist[6:],
    'HealthUpgrade': ('Heart Container', 'Piece of Heart'),
    'ProgressItem': [name for (name, data) in item_table.items() if data[0] == 'Item' and data[1]],
    'DungeonReward': dungeon_rewards,

    'ForestFireWater': ('Forest Medallion', 'Fire Medallion', 'Water Medallion'),
    'FireWater': ('Fire Medallion', 'Water Medallion'),
}


def get_junk_item(count=1, pool=None, plando_pool=None):
    if count < 1:
        raise ValueError("get_junk_item argument 'count' must be greater than 0.")

    return_pool = []
    if pending_junk_pool:
        pending_count = min(len(pending_junk_pool), count)
        return_pool = [pending_junk_pool.pop() for _ in range(pending_count)]
예제 #7
0
    remove_junk_items,
    'AdultTrade':
    tradeitems,
    'Bottle':
    normal_bottles,
    'Spell': ('Dins Fire', 'Farores Wind', 'Nayrus Love'),
    'Shield': ('Deku Shield', 'Hylian Shield'),
    'Song':
    songlist,
    'NonWarpSong':
    songlist[0:6],
    'WarpSong':
    songlist[6:],
    'HealthUpgrade': ('Heart Container', 'Piece of Heart'),
    'ProgressItem': [
        name for (name, data) in item_table.items()
        if data[0] == 'Item' and data[1]
    ],
    'ForestFireWater':
    ('Forest Medallion', 'Fire Medallion', 'Water Medallion'),
    'FireWater': ('Fire Medallion', 'Water Medallion'),
}


def get_junk_item(count=1):
    if count < 1:
        raise ValueError(
            "get_junk_item argument 'count' must be greater than 0.")

    return_pool = []
    if pending_junk_pool: