diff --git a/BaseClasses.py b/BaseClasses.py index d09ea402..b79af29d 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -17,6 +17,7 @@ from Utils import int16_as_bytes from Tables import normal_offset_table, spiral_offset_table, multiply_lookup, divisor_lookup from RoomData import Room + class World(object): def __init__(self, players, shuffle, doorShuffle, logic, mode, swords, difficulty, difficulty_adjustments, @@ -216,6 +217,11 @@ class World(object): return r_location raise RuntimeError('No such location %s for player %d' % (location, player)) + def get_location_unsafe(self, location, player): + if (location, player) in self._location_cache: + return self._location_cache[(location, player)] + return None + def get_dungeon(self, dungeonname, player): if isinstance(dungeonname, Dungeon): return dungeonname @@ -1462,6 +1468,10 @@ class Dungeon(object): return self.world.get_name_string_for_object(self) if self.world else f'{self.name} (Player {self.player})' +class FillError(RuntimeError): + pass + + @unique class DoorType(Enum): Normal = 1 @@ -1853,6 +1863,8 @@ class Sector(object): self.destination_entrance = False self.equations = None self.item_logic = set() + self.chest_location_set = set() + def region_set(self): if self.r_name_set is None: @@ -2101,6 +2113,7 @@ class Location(object): self.recursion_count = 0 self.staleness_count = 0 self.locked = False + self.real = not crystal self.always_allow = lambda item, state: False self.access_rule = lambda state: True self.item_rule = lambda item: True diff --git a/Bosses.py b/Bosses.py index 90acafcf..7e797b42 100644 --- a/Bosses.py +++ b/Bosses.py @@ -1,8 +1,8 @@ import logging import RaceRandom as random -from BaseClasses import Boss -from Fill import FillError +from BaseClasses import Boss, FillError + def BossFactory(boss, player): if boss is None: diff --git a/DoorShuffle.py b/DoorShuffle.py index 3e4bfac9..bd828756 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -44,10 +44,10 @@ def link_doors(world, player): reset_rooms(world, player) world.get_door("Skull Pinball WS", player).no_exit() world.swamp_patch_required[player] = orig_swamp_patch + link_doors_prep(world, player) -def link_doors_main(world, player): - +def link_doors_prep(world, player): # Drop-down connections & push blocks for exitName, regionName in logical_connections: connect_simple_door(world, exitName, regionName, player) @@ -99,6 +99,7 @@ def link_doors_main(world, player): analyze_portals(world, player) for portal in world.dungeon_portals[player]: connect_portal(portal, world, player) + if not world.doorShuffle[player] == 'vanilla': fix_big_key_doors_with_ugly_smalls(world, player) else: @@ -119,11 +120,14 @@ def link_doors_main(world, player): for ent, ext in default_one_way_connections: connect_one_way(world, ent, ext, player) vanilla_key_logic(world, player) - elif world.doorShuffle[player] == 'basic': + + +def link_doors_main(world, player): + if world.doorShuffle[player] == 'basic': within_dungeon(world, player) elif world.doorShuffle[player] == 'crossed': cross_dungeon(world, player) - else: + elif world.doorShuffle[player] != 'vanilla': logging.getLogger('').error('Invalid door shuffle setting: %s' % world.doorShuffle[player]) raise Exception('Invalid door shuffle setting: %s' % world.doorShuffle[player]) @@ -214,11 +218,16 @@ def vanilla_key_logic(world, player): world.key_logic[player] = {} analyze_dungeon(key_layout, world, player) world.key_logic[player][builder.name] = key_layout.key_logic + world.key_layout[player][builder.name] = key_layout log_key_logic(builder.name, key_layout.key_logic) # if world.shuffle[player] == 'vanilla' and world.accessibility[player] == 'items' and not world.retro[player] and not world.keydropshuffle[player]: # validate_vanilla_key_logic(world, player) +def validate_vanilla_reservation(dungeon, world, player): + return validate_key_layout(world.key_layout[player][dungeon.name], world, player) + + # some useful functions oppositemap = { Direction.South: Direction.North, diff --git a/DungeonGenerator.py b/DungeonGenerator.py index 64fd3513..5be223c9 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -1162,6 +1162,8 @@ class DungeonBuilder(object): self.sectors = [] self.location_cnt = 0 self.key_drop_cnt = 0 + self.dungeon_items = None # during fill how many dungeon items are left + self.free_items = None # during fill how many dungeon items are left self.bk_required = False self.bk_provided = False self.c_switch_required = False @@ -1324,7 +1326,7 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player, polarized_sectors[sector] = None if bow_sectors: assign_bow_sectors(dungeon_map, bow_sectors, global_pole) - assign_location_sectors(dungeon_map, free_location_sectors, global_pole) + assign_location_sectors(dungeon_map, free_location_sectors, global_pole, world, player) leftover = assign_crystal_switch_sectors(dungeon_map, crystal_switches, crystal_barriers, global_pole) ensure_crystal_switches_reachable(dungeon_map, leftover, polarized_sectors, crystal_barriers, global_pole) for sector in leftover: @@ -1475,6 +1477,7 @@ def define_sector_features(sectors): sector.bk_provided = True elif loc.name not in dungeon_events and not loc.forced_item: sector.chest_locations += 1 + sector.chest_location_set.add(loc.name) if '- Big Chest' in loc.name or loc.name in ["Hyrule Castle - Zelda's Chest", "Thieves' Town - Blind's Cell"]: sector.bk_required = True @@ -1555,19 +1558,26 @@ def assign_bow_sectors(dungeon_map, bow_sectors, global_pole): assign_sector(sector_list[i], builder, bow_sectors, global_pole) -def assign_location_sectors(dungeon_map, free_location_sectors, global_pole): +def assign_location_sectors(dungeon_map, free_location_sectors, global_pole, world, player): valid = False choices = None sector_list = list(free_location_sectors) random.shuffle(sector_list) + orig_location_set = build_orig_location_set(dungeon_map) + num_dungeon_items = requested_dungeon_items(world, player) while not valid: choices, d_idx, totals = weighted_random_locations(dungeon_map, sector_list) + location_set = {x: set(y) for x, y in orig_location_set.items()} for i, sector in enumerate(sector_list): - choice = d_idx[choices[i].name] + d_name = choices[i].name + choice = d_idx[d_name] totals[choice] += sector.chest_locations + location_set[d_name].update(sector.chest_location_set) valid = True for d_name, idx in d_idx.items(): - if totals[idx] < 5: # min locations for dungeons is 5 (bk exception) + free_items = count_reserved_locations(world, player, location_set[d_name]) + target = max(free_items, 2) + num_dungeon_items + if totals[idx] < target: valid = False break for i, choice in enumerate(choices): @@ -1598,6 +1608,30 @@ def weighted_random_locations(dungeon_map, free_location_sectors): return choices, d_idx, totals +def build_orig_location_set(dungeon_map): + orig_locations = {} + for name, builder in dungeon_map.items(): + orig_locations[name] = set().union(*(s.chest_location_set for s in builder.sectors)) + return orig_locations + + +def requested_dungeon_items(world, player): + num = 0 + if not world.bigkeyshuffle[player]: + num += 1 + if not world.compassshuffle[player]: + num += 1 + if not world.mapshuffle[player]: + num += 1 + return num + + +def count_reserved_locations(world, player, proposed_set): + if world.item_pool_config: + return len([x for x in proposed_set if x in world.item_pool_config.reserved_locations[player]]) + return 2 + + def assign_crystal_switch_sectors(dungeon_map, crystal_switches, crystal_barriers, global_pole, assign_one=False): population = [] some_c_switches_present = False diff --git a/Dungeons.py b/Dungeons.py index 2fda8be8..08bb25ba 100644 --- a/Dungeons.py +++ b/Dungeons.py @@ -1,8 +1,5 @@ -import RaceRandom as random - from BaseClasses import Dungeon from Bosses import BossFactory -from Fill import fill_restrictive from Items import ItemFactory @@ -36,117 +33,6 @@ def create_dungeons(world, player): world.dungeons += [ES, EP, DP, ToH, AT, PoD, TT, SW, SP, IP, MM, TR, GT] -def fill_dungeons(world): - freebes = ['Ganons Tower - Map Chest', 'Palace of Darkness - Harmless Hellway', 'Palace of Darkness - Big Key Chest', 'Turtle Rock - Big Key Chest'] - - all_state_base = world.get_all_state() - - for player in range(1, world.players + 1): - pinball_room = world.get_location('Skull Woods - Pinball Room', player) - if world.retro[player]: - world.push_item(pinball_room, ItemFactory('Small Key (Universal)', player), False) - else: - world.push_item(pinball_room, ItemFactory('Small Key (Skull Woods)', player), False) - pinball_room.event = True - pinball_room.locked = True - - dungeons = [(list(dungeon.regions), dungeon.big_key, list(dungeon.small_keys), list(dungeon.dungeon_items)) for dungeon in world.dungeons] - - loopcnt = 0 - while dungeons: - loopcnt += 1 - dungeon_regions, big_key, small_keys, dungeon_items = dungeons.pop(0) - # 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 = all_state_base.copy() - - # 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) - bk_location.event = True - bk_location.locked = True - dungeon_locations.remove(bk_location) - big_key = None - - # next place small keys - while small_keys: - small_key = small_keys.pop() - all_state.sweep_for_events() - sk_location = None - for location in dungeon_locations: - if location.name in freebes or (location.can_reach(all_state) and location.item_rule(small_key)): - sk_location = location - break - - if sk_location is None: - # need to retry this later - small_keys.append(small_key) - dungeons.append((dungeon_regions, big_key, small_keys, dungeon_items)) - # infinite regression protection - if loopcnt < (30 * world.players): - break - else: - raise RuntimeError('No suitable location for %s' % small_key) - - world.push_item(sk_location, small_key, False) - sk_location.event = True - sk_location.locked = True - dungeon_locations.remove(sk_location) - - if small_keys: - # key placement not finished, loop again - continue - - # next place dungeon items - for dungeon_item in dungeon_items: - di_location = dungeon_locations.pop() - world.push_item(di_location, dungeon_item, False) - - -def get_dungeon_item_pool(world): - return [item for dungeon in world.dungeons for item in dungeon.all_items] - - -def fill_dungeons_restrictive(world, shuffled_locations): - all_state_base = world.get_all_state() - - # for player in range(1, world.players + 1): - # pinball_room = world.get_location('Skull Woods - Pinball Room', player) - # if world.retro[player]: - # world.push_item(pinball_room, ItemFactory('Small Key (Universal)', player), False) - # else: - # world.push_item(pinball_room, ItemFactory('Small Key (Skull Woods)', player), False) - # pinball_room.event = True - # pinball_room.locked = True - # shuffled_locations.remove(pinball_room) - - # with shuffled dungeon items they are distributed as part of the normal item pool - for item in world.get_items(): - if (item.smallkey and world.keyshuffle[item.player]) or (item.bigkey and world.bigkeyshuffle[item.player]): - item.advancement = True - elif (item.map and world.mapshuffle[item.player]) or (item.compass and world.compassshuffle[item.player]): - item.priority = True - - dungeon_items = [item for item in get_dungeon_item_pool(world) if item.is_inside_dungeon_item(world)] - - # sort in the order Big Key, Small Key, Other before placing dungeon items - sort_order = {"BigKey": 3, "SmallKey": 2} - dungeon_items.sort(key=lambda item: sort_order.get(item.type, 1)) - - fill_restrictive(world, all_state_base, shuffled_locations, dungeon_items, - keys_in_itempool={player: not world.keyshuffle[player] for player in range(1, world.players+1)}, single_player_placement=True) - dungeon_music_addresses = {'Eastern Palace - Prize': [0x1559A], 'Desert Palace - Prize': [0x1559B, 0x1559C, 0x1559D, 0x1559E], diff --git a/Fill.py b/Fill.py index 59a98cea..cea0513a 100644 --- a/Fill.py +++ b/Fill.py @@ -3,173 +3,68 @@ import collections import itertools import logging -from BaseClasses import CollectionState +from BaseClasses import CollectionState, FillError from Items import ItemFactory from Regions import shop_to_location_table, retro_shops +from source.item.BiasedFill import filter_locations, classify_major_items, replace_trash_item, vanilla_fallback -class FillError(RuntimeError): - pass - -def distribute_items_cutoff(world, cutoffrate=0.33): - # get list of locations to fill in - fill_locations = world.get_unfilled_locations() - random.shuffle(fill_locations) - - # get items to distribute - random.shuffle(world.itempool) - itempool = world.itempool - - total_advancement_items = len([item for item in itempool if item.advancement]) - placed_advancement_items = 0 - - progress_done = False - advancement_placed = False - - # sweep once to pick up preplaced items - world.state.sweep_for_events() - - while itempool and fill_locations: - candidate_item_to_place = None - item_to_place = None - for item in itempool: - if advancement_placed or (progress_done and (item.advancement or item.priority)): - item_to_place = item - break - if item.advancement: - candidate_item_to_place = item - if world.unlocks_new_location(item): - item_to_place = item - placed_advancement_items += 1 - break - - if item_to_place is None: - # check if we can reach all locations and that is why we find no new locations to place - if not progress_done and len(world.get_reachable_locations()) == len(world.get_locations()): - progress_done = True - continue - # check if we have now placed all advancement items - if progress_done: - advancement_placed = True - continue - # we might be in a situation where all new locations require multiple items to reach. If that is the case, just place any advancement item we've found and continue trying - if candidate_item_to_place is not None: - item_to_place = candidate_item_to_place - placed_advancement_items += 1 - else: - # we placed all available progress items. Maybe the game can be beaten anyway? - if world.can_beat_game(): - logging.getLogger('').warning('Not all locations reachable. Game beatable anyway.') - progress_done = True - continue - raise FillError('No more progress items left to place.') - - spot_to_fill = None - for location in fill_locations if placed_advancement_items / total_advancement_items < cutoffrate else reversed(fill_locations): - if location.can_fill(world.state, item_to_place): - spot_to_fill = location - break - - if spot_to_fill is None: - # we filled all reachable spots. Maybe the game can be beaten anyway? - if world.can_beat_game(): - logging.getLogger('').warning('Not all items placed. Game beatable anyway.') - break - raise FillError('No more spots to place %s' % item_to_place) - - world.push_item(spot_to_fill, item_to_place, True) - itempool.remove(item_to_place) - fill_locations.remove(spot_to_fill) - unplaced = [item.name for item in itempool] - unfilled = [location.name for location in fill_locations] - if unplaced or unfilled: - logging.warning('Unplaced items: %s - Unfilled Locations: %s', unplaced, unfilled) +def get_dungeon_item_pool(world): + return [item for dungeon in world.dungeons for item in dungeon.all_items] -def distribute_items_staleness(world): - # get list of locations to fill in - fill_locations = world.get_unfilled_locations() - random.shuffle(fill_locations) +def promote_dungeon_items(world): + world.itempool += get_dungeon_item_pool(world) - # get items to distribute - random.shuffle(world.itempool) - itempool = world.itempool + for item in world.get_items(): + if item.smallkey or item.bigkey: + item.advancement = True + elif item.map or item.compass: + item.priority = True + dungeon_tracking(world) - progress_done = False - advancement_placed = False - # sweep once to pick up preplaced items - world.state.sweep_for_events() +def dungeon_tracking(world): + for dungeon in world.dungeons: + layout = world.dungeon_layouts[dungeon.player][dungeon.name] + layout.dungeon_items = len([i for i in dungeon.all_items if i.is_inside_dungeon_item(world)]) + layout.free_items = layout.location_cnt - layout.dungeon_items - while itempool and fill_locations: - candidate_item_to_place = None - item_to_place = None - for item in itempool: - if advancement_placed or (progress_done and (item.advancement or item.priority)): - item_to_place = item - break - if item.advancement: - candidate_item_to_place = item - if world.unlocks_new_location(item): - item_to_place = item - break - if item_to_place is None: - # check if we can reach all locations and that is why we find no new locations to place - if not progress_done and len(world.get_reachable_locations()) == len(world.get_locations()): - progress_done = True - continue - # check if we have now placed all advancement items - if progress_done: - advancement_placed = True - continue - # we might be in a situation where all new locations require multiple items to reach. If that is the case, just place any advancement item we've found and continue trying - if candidate_item_to_place is not None: - item_to_place = candidate_item_to_place - else: - # we placed all available progress items. Maybe the game can be beaten anyway? - if world.can_beat_game(): - logging.getLogger('').warning('Not all locations reachable. Game beatable anyway.') - progress_done = True - continue - raise FillError('No more progress items left to place.') +def fill_dungeons_restrictive(world, shuffled_locations): + dungeon_tracking(world) + all_state_base = world.get_all_state() - spot_to_fill = None - for location in fill_locations: - # increase likelyhood of skipping a location if it has been found stale - if not progress_done and random.randint(0, location.staleness_count) > 2: - continue + # for player in range(1, world.players + 1): + # pinball_room = world.get_location('Skull Woods - Pinball Room', player) + # if world.retro[player]: + # world.push_item(pinball_room, ItemFactory('Small Key (Universal)', player), False) + # else: + # world.push_item(pinball_room, ItemFactory('Small Key (Skull Woods)', player), False) + # pinball_room.event = True + # pinball_room.locked = True + # shuffled_locations.remove(pinball_room) - if location.can_fill(world.state, item_to_place): - spot_to_fill = location - break - else: - location.staleness_count += 1 + # with shuffled dungeon items they are distributed as part of the normal item pool + for item in world.get_items(): + if (item.smallkey and world.keyshuffle[item.player]) or (item.bigkey and world.bigkeyshuffle[item.player]): + item.advancement = True + elif (item.map and world.mapshuffle[item.player]) or (item.compass and world.compassshuffle[item.player]): + item.priority = True - # might have skipped too many locations due to potential staleness. Do not check for staleness now to find a candidate - if spot_to_fill is None: - for location in fill_locations: - if location.can_fill(world.state, item_to_place): - spot_to_fill = location - break + dungeon_items = [item for item in get_dungeon_item_pool(world) if item.is_inside_dungeon_item(world)] - if spot_to_fill is None: - # we filled all reachable spots. Maybe the game can be beaten anyway? - if world.can_beat_game(): - logging.getLogger('').warning('Not all items placed. Game beatable anyway.') - break - raise FillError('No more spots to place %s' % item_to_place) + # sort in the order Big Key, Small Key, Other before placing dungeon items + sort_order = {"BigKey": 3, "SmallKey": 2} + dungeon_items.sort(key=lambda item: sort_order.get(item.type, 1)) - world.push_item(spot_to_fill, item_to_place, True) - itempool.remove(item_to_place) - fill_locations.remove(spot_to_fill) + fill_restrictive(world, all_state_base, shuffled_locations, dungeon_items, + keys_in_itempool={player: not world.keyshuffle[player] for player in range(1, world.players+1)}, + single_player_placement=True) - unplaced = [item.name for item in itempool] - unfilled = [location.name for location in fill_locations] - if unplaced or unfilled: - logging.warning('Unplaced items: %s - Unfilled Locations: %s', unplaced, unfilled) -def fill_restrictive(world, base_state, locations, itempool, keys_in_itempool = None, single_player_placement = False): +def fill_restrictive(world, base_state, locations, itempool, keys_in_itempool=None, single_player_placement=False, + vanilla=False): def sweep_from_pool(): new_state = base_state.copy() for item in itempool: @@ -201,41 +96,58 @@ def fill_restrictive(world, base_state, locations, itempool, keys_in_itempool = spot_to_fill = None - for location in locations: - if item_to_place.smallkey or item_to_place.bigkey: # a better test to see if a key can go there - location.item = item_to_place - test_state = maximum_exploration_state.copy() - test_state.stale[item_to_place.player] = True - else: - test_state = maximum_exploration_state - if (not single_player_placement or location.player == item_to_place.player)\ - and location.can_fill(test_state, item_to_place, perform_access_check)\ - and valid_key_placement(item_to_place, location, itempool if (keys_in_itempool and keys_in_itempool[item_to_place.player]) else world.itempool, world): - spot_to_fill = location + item_locations = filter_locations(item_to_place, locations, world, vanilla) + for location in item_locations: + spot_to_fill = verify_spot_to_fill(location, item_to_place, maximum_exploration_state, + single_player_placement, perform_access_check, itempool, + keys_in_itempool, world) + if spot_to_fill: break - if item_to_place.smallkey or item_to_place.bigkey: - location.item = None - if spot_to_fill is None: - # we filled all reachable spots. Maybe the game can be beaten anyway? - unplaced_items.insert(0, item_to_place) - if world.can_beat_game(): - if world.accessibility[item_to_place.player] != 'none': - logging.getLogger('').warning('Not all items placed. Game beatable anyway. (Could not place %s)' % item_to_place) + if vanilla: + unplaced_items.insert(0, item_to_place) continue - spot_to_fill = last_ditch_placement(item_to_place, locations, world, maximum_exploration_state, - base_state, itempool, keys_in_itempool, single_player_placement) + spot_to_fill = recovery_placement(item_to_place, locations, world, maximum_exploration_state, + base_state, itempool, perform_access_check, item_locations, + keys_in_itempool, single_player_placement) if spot_to_fill is None: + # we filled all reachable spots. Maybe the game can be beaten anyway? + unplaced_items.insert(0, item_to_place) + if world.can_beat_game(): + if world.accessibility[item_to_place.player] != 'none': + logging.getLogger('').warning('Not all items placed. Game beatable anyway.' + f' (Could not place {item_to_place})') + continue raise FillError('No more spots to place %s' % item_to_place) world.push_item(spot_to_fill, item_to_place, False) track_outside_keys(item_to_place, spot_to_fill, world) + track_dungeon_items(item_to_place, spot_to_fill, world) locations.remove(spot_to_fill) spot_to_fill.event = True itempool.extend(unplaced_items) +def verify_spot_to_fill(location, item_to_place, max_exp_state, single_player_placement, perform_access_check, + itempool, keys_in_itempool, world): + if item_to_place.smallkey or item_to_place.bigkey: # a better test to see if a key can go there + location.item = item_to_place + test_state = max_exp_state.copy() + test_state.stale[item_to_place.player] = True + else: + test_state = max_exp_state + if not single_player_placement or location.player == item_to_place.player: + if location.can_fill(test_state, item_to_place, perform_access_check): + test_pool = itempool if (keys_in_itempool and keys_in_itempool[item_to_place.player]) else world.itempool + if valid_key_placement(item_to_place, location, test_pool, world): + if item_to_place.crystal or valid_dungeon_placement(item_to_place, location, world): + return location + if item_to_place.smallkey or item_to_place.bigkey: + location.item = None + return None + + def valid_key_placement(item, location, itempool, world): if not valid_reserved_placement(item, location, world): return False @@ -254,7 +166,7 @@ def valid_key_placement(item, location, itempool, world): cr_count = world.crystals_needed_for_gt[location.player] return key_logic.check_placement(unplaced_keys, location if item.bigkey else None, prize_loc, cr_count) else: - return not item.is_inside_dungeon_item(world) # todo: big deal for ambrosia to fix this + return not item.is_inside_dungeon_item(world) def valid_reserved_placement(item, location, world): @@ -263,6 +175,17 @@ def valid_reserved_placement(item, location, world): return True +def valid_dungeon_placement(item, location, world): + if location.parent_region.dungeon: + layout = world.dungeon_layouts[location.player][location.parent_region.dungeon.name] + if not is_dungeon_item(item, world) or item.player != location.player: + return layout.free_items > 0 + else: + # the second half probably doesn't matter much - should always return true + return item.dungeon == location.parent_region.dungeon.name and layout.dungeon_items > 0 + return not is_dungeon_item(item, world) + + def track_outside_keys(item, location, world): if not item.smallkey: return @@ -274,6 +197,72 @@ def track_outside_keys(item, location, world): world.key_logic[item.player][item_dungeon].outside_keys += 1 +def track_dungeon_items(item, location, world): + if location.parent_region.dungeon and not item.crystal: + layout = world.dungeon_layouts[location.player][location.parent_region.dungeon.name] + if is_dungeon_item(item, world) and item.player == location.player: + layout.dungeon_items -= 1 + else: + layout.free_items -= 1 + + +def is_dungeon_item(item, world): + return ((item.smallkey and not world.keyshuffle[item.player]) + or (item.bigkey and not world.bigkeyshuffle[item.player]) + or (item.compass and not world.compassshuffle[item.player]) + or (item.map and not world.mapshuffle[item.player])) + + +def recovery_placement(item_to_place, locations, world, state, base_state, itempool, perform_access_check, attempted, + keys_in_itempool=None, single_player_placement=False): + logging.getLogger('').debug(f'Could not place {item_to_place} attempting recovery') + if world.algorithm in ['balanced', 'equitable']: + return last_ditch_placement(item_to_place, locations, world, state, base_state, itempool, keys_in_itempool, + single_player_placement) + elif world.algorithm == 'vanilla_fill': + if item_to_place.type == 'Crystal': + possible_swaps = [x for x in state.locations_checked if x.item.type == 'Crystal'] + return try_possible_swaps(possible_swaps, item_to_place, locations, world, base_state, itempool, + keys_in_itempool, single_player_placement) + else: + i, config = 0, world.item_pool_config + tried = set(attempted) + if not item_to_place.is_inside_dungeon_item(world): + while i < len(config.location_groups[item_to_place.player]): + fallback_locations = config.location_groups[item_to_place.player][i].locations + other_locs = [x for x in locations if x.name in fallback_locations] + for location in other_locs: + spot_to_fill = verify_spot_to_fill(location, item_to_place, state, single_player_placement, + perform_access_check, itempool, keys_in_itempool, world) + if spot_to_fill: + return spot_to_fill + i += 1 + tried.update(other_locs) + else: + other_locations = vanilla_fallback(item_to_place, locations, world) + for location in other_locations: + spot_to_fill = verify_spot_to_fill(location, item_to_place, state, single_player_placement, + perform_access_check, itempool, keys_in_itempool, world) + if spot_to_fill: + return spot_to_fill + tried.update(other_locations) + other_locations = [x for x in locations if x not in tried] + for location in other_locations: + spot_to_fill = verify_spot_to_fill(location, item_to_place, state, single_player_placement, + perform_access_check, itempool, keys_in_itempool, world) + if spot_to_fill: + return spot_to_fill + return None + else: + other_locations = [x for x in locations if x not in attempted] + for location in other_locations: + spot_to_fill = verify_spot_to_fill(location, item_to_place, state, single_player_placement, + perform_access_check, itempool, keys_in_itempool, world) + if spot_to_fill: + return spot_to_fill + return None + + def last_ditch_placement(item_to_place, locations, world, state, base_state, itempool, keys_in_itempool=None, single_player_placement=False): def location_preference(loc): @@ -292,7 +281,12 @@ def last_ditch_placement(item_to_place, locations, world, state, base_state, ite possible_swaps = [x for x in state.locations_checked if x.item.type not in ['Event', 'Crystal'] and not x.forced_item] swap_locations = sorted(possible_swaps, key=location_preference) + return try_possible_swaps(swap_locations, item_to_place, locations, world, base_state, itempool, + keys_in_itempool, single_player_placement) + +def try_possible_swaps(swap_locations, item_to_place, locations, world, base_state, itempool, + keys_in_itempool=None, single_player_placement=False): for location in swap_locations: old_item = location.item new_pool = list(itempool) + [old_item] @@ -355,11 +349,15 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None random.shuffle(fill_locations) # get items to distribute + classify_major_items(world) random.shuffle(world.itempool) progitempool = [item for item in world.itempool if item.advancement] prioitempool = [item for item in world.itempool if not item.advancement and item.priority] restitempool = [item for item in world.itempool if not item.advancement and not item.priority] + gftower_trash &= world.algorithm in ['balanced', 'equitable', 'dungeon_only'] + # dungeon only may fill up the dungeon... and push items out into the overworld + # fill in gtower locations with trash first for player in range(1, world.players + 1): if not gftower_trash or not world.ganonstower_vanilla[player] or world.doorShuffle[player] == 'crossed' or world.logic[player] in ['owglitches', 'nologic']: @@ -383,21 +381,51 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None # Make sure the escape small key is placed first in standard with key shuffle to prevent running out of spots # todo: crossed progitempool.sort(key=lambda item: 1 if item.name == 'Small Key (Escape)' and world.keyshuffle[item.player] and world.mode[item.player] == 'standard' else 0) + keys_in_pool = {player: world.keyshuffle[player] or world.algorithm != 'balanced' for player in range(1, world.players + 1)} - fill_restrictive(world, world.state, fill_locations, progitempool, - keys_in_itempool={player: world.keyshuffle[player] for player in range(1, world.players + 1)}) - + # sort maps and compasses to the back -- this may not be viable in equitable & ambrosia + progitempool.sort(key=lambda item: 0 if item.map or item.compass else 1) + if world.algorithm == 'vanilla_fill': + fill_restrictive(world, world.state, fill_locations, progitempool, keys_in_pool, vanilla=True) + fill_restrictive(world, world.state, fill_locations, progitempool, keys_in_pool) random.shuffle(fill_locations) + if world.algorithm == 'balanced': + fast_fill(world, prioitempool, fill_locations) + elif world.algorithm == 'vanilla_fill': + fast_vanilla_fill(world, prioitempool, fill_locations) + elif world.algorithm in ['major_only', 'dungeon_only', 'district']: + filtered_fill(world, prioitempool, fill_locations) + else: # just need to ensure dungeon items still get placed in dungeons + fast_equitable_fill(world, prioitempool, fill_locations) + # placeholder work + if world.algorithm == 'district': + random.shuffle(fill_locations) + placeholder_items = [item for item in world.itempool if item.name == 'Rupee (1)'] + num_ph_items = len(placeholder_items) + if num_ph_items > 0: + placeholder_locations = filter_locations('Placeholder', fill_locations, world) + num_ph_locations = len(placeholder_locations) + if num_ph_items < num_ph_locations < len(fill_locations): + for _ in range(num_ph_locations - num_ph_items): + placeholder_items.append(replace_trash_item(restitempool, 'Rupee (1)')) + assert len(placeholder_items) == len(placeholder_locations) + for i in placeholder_items: + restitempool.remove(i) + for l in placeholder_locations: + fill_locations.remove(l) + filtered_fill(world, placeholder_items, placeholder_locations) - fast_fill(world, prioitempool, fill_locations) - - fast_fill(world, restitempool, fill_locations) + if world.algorithm == 'vanilla_fill': + fast_vanilla_fill(world, restitempool, fill_locations) + else: + fast_fill(world, restitempool, fill_locations) unplaced = [item.name for item in prioitempool + restitempool] unfilled = [location.name for location in fill_locations] if unplaced or unfilled: logging.warning('Unplaced items: %s - Unfilled Locations: %s', unplaced, unfilled) + def fast_fill(world, item_pool, fill_locations): while item_pool and fill_locations: spot_to_fill = fill_locations.pop() @@ -405,77 +433,59 @@ def fast_fill(world, item_pool, fill_locations): world.push_item(spot_to_fill, item_to_place, False) -def flood_items(world): - # get items to distribute - random.shuffle(world.itempool) - itempool = world.itempool - progress_done = False +def filtered_fill(world, item_pool, fill_locations): + while item_pool and fill_locations: + item_to_place = item_pool.pop() + item_locations = filter_locations(item_to_place, fill_locations, world) + spot_to_fill = next(iter(item_locations)) + fill_locations.remove(spot_to_fill) + world.push_item(spot_to_fill, item_to_place, False) # sweep once to pick up preplaced items world.state.sweep_for_events() - # fill world from top of itempool while we can - while not progress_done: - location_list = world.get_unfilled_locations() - random.shuffle(location_list) - spot_to_fill = None - for location in location_list: - if location.can_fill(world.state, itempool[0]): - spot_to_fill = location - break - if spot_to_fill: - item = itempool.pop(0) - world.push_item(spot_to_fill, item, True) - continue +def fast_vanilla_fill(world, item_pool, fill_locations): + next_item_pool = [] + while item_pool and fill_locations: + item_to_place = item_pool.pop() + locations = filter_locations(item_to_place, fill_locations, world, vanilla_skip=True) + if len(locations): + spot_to_fill = locations.pop() + fill_locations.remove(spot_to_fill) + world.push_item(spot_to_fill, item_to_place, False) + else: + next_item_pool.append(item_to_place) + while next_item_pool and fill_locations: + item_to_place = next_item_pool.pop() + spot_to_fill = next(iter(filter_locations(item_to_place, fill_locations, world))) + fill_locations.remove(spot_to_fill) + world.push_item(spot_to_fill, item_to_place, False) - # ran out of spots, check if we need to step in and correct things - if len(world.get_reachable_locations()) == len(world.get_locations()): - progress_done = True - continue - # need to place a progress item instead of an already placed item, find candidate - item_to_place = None - candidate_item_to_place = None - for item in itempool: - if item.advancement: - candidate_item_to_place = item - if world.unlocks_new_location(item): - item_to_place = item - break +def filtered_equitable_fill(world, item_pool, fill_locations): + while item_pool and fill_locations: + item_to_place = item_pool.pop() + item_locations = filter_locations(item_to_place, fill_locations, world) + spot_to_fill = next(l for l in item_locations if valid_dungeon_placement(item_to_place, l, world)) + fill_locations.remove(spot_to_fill) + world.push_item(spot_to_fill, item_to_place, False) + track_dungeon_items(item_to_place, spot_to_fill, world) - # we might be in a situation where all new locations require multiple items to reach. If that is the case, just place any advancement item we've found and continue trying - if item_to_place is None: - if candidate_item_to_place is not None: - item_to_place = candidate_item_to_place - else: - raise FillError('No more progress items left to place.') - # find item to replace with progress item - location_list = world.get_reachable_locations() - random.shuffle(location_list) - for location in location_list: - if location.item is not None and not location.item.advancement and not location.item.priority and not location.item.smallkey and not location.item.bigkey: - # safe to replace - replace_item = location.item - replace_item.location = None - itempool.append(replace_item) - world.push_item(location, item_to_place, True) - itempool.remove(item_to_place) - break +def fast_equitable_fill(world, item_pool, fill_locations): + while item_pool and fill_locations: + item_to_place = item_pool.pop() + spot_to_fill = next(l for l in fill_locations if valid_dungeon_placement(item_to_place, l, world)) + fill_locations.remove(spot_to_fill) + world.push_item(spot_to_fill, item_to_place, False) + track_dungeon_items(item_to_place, spot_to_fill, world) def lock_shop_locations(world, player): for shop, loc_names in shop_to_location_table.items(): for loc in loc_names: - world.get_location(loc, player).event = True world.get_location(loc, player).locked = True - # I don't believe these locations exist in non-shopsanity - # if world.retro[player]: - # for shop, loc_names in retro_shops.items(): - # for loc in loc_names: - # world.get_location(loc, player).event = True - # world.get_location(loc, player).locked = True def sell_potions(world, player): diff --git a/ItemList.py b/ItemList.py index 9e0b0411..3a93f4d8 100644 --- a/ItemList.py +++ b/ItemList.py @@ -4,12 +4,13 @@ import math import RaceRandom as random from BaseClasses import Region, RegionType, Shop, ShopType, Location, CollectionState -from Dungeons import get_dungeon_item_pool from EntranceShuffle import connect_entrance from Regions import shop_to_location_table, retro_shops, shop_table_by_location -from Fill import FillError, fill_restrictive, fast_fill +from Fill import FillError, fill_restrictive, fast_fill, get_dungeon_item_pool from Items import ItemFactory +from source.item.BiasedFill import trash_items + import source.classes.constants as CONST @@ -261,8 +262,12 @@ def generate_itempool(world, player): if player in world.pool_adjustment.keys(): amt = world.pool_adjustment[player] if amt < 0: - for _ in range(amt, 0): - pool.remove(next(iter([x for x in pool if x in ['Rupees (20)', 'Rupees (5)', 'Rupee (1)']]))) + trash_options = [x for x in pool if x in trash_items] + random.shuffle(trash_options) + trash_options = sorted(trash_options, key=lambda x: trash_items[x], reverse=True) + while amt > 0 and len(trash_options) > 0: + pool.remove(trash_options.pop()) + amt -= 1 elif amt > 0: for _ in range(0, amt): pool.append('Rupees (20)') diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index 1c7ea71b..bb4f6835 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -187,6 +187,8 @@ class PlacementRule(object): return True available_keys = outside_keys empty_chests = 0 + # todo: sometimes we need an extra empty chest to accomodate the big key too + # dungeon bias seed 563518200 for example threshold = self.needed_keys_wo_bk if bk_blocked else self.needed_keys_w_bk for loc in check_locations: if not loc.item: diff --git a/Main.py b/Main.py index 447c3682..e00120af 100644 --- a/Main.py +++ b/Main.py @@ -20,16 +20,17 @@ from InvertedRegions import create_inverted_regions, mark_dark_world_regions from EntranceShuffle import link_entrances, link_inverted_entrances from Rom import patch_rom, patch_race_rom, patch_enemizer, apply_rom_settings, LocalRom, JsonRom, get_hash_string from Doors import create_doors -from DoorShuffle import link_doors, connect_portal +from DoorShuffle import link_doors, connect_portal, link_doors_prep from RoomData import create_rooms from Rules import set_rules -from Dungeons import create_dungeons, fill_dungeons, fill_dungeons_restrictive -from Fill import distribute_items_cutoff, distribute_items_staleness, distribute_items_restrictive, flood_items +from Dungeons import create_dungeons +from Fill import distribute_items_restrictive, promote_dungeon_items, fill_dungeons_restrictive from Fill import sell_potions, sell_keys, balance_multiworld_progression, balance_money_progression, lock_shop_locations from ItemList import generate_itempool, difficulties, fill_prizes, customize_shops from Utils import output_path, parse_player_names -from source.item.FillUtil import create_item_pool_config +from source.item.BiasedFill import create_item_pool_config, massage_item_pool, district_item_pool_config + __version__ = '1.0.2.0-v' @@ -152,7 +153,6 @@ def main(args, seed=None, fish=None): create_dungeons(world, player) adjust_locations(world, player) place_bosses(world, player) - create_item_pool_config(world) if any(world.potshuffle.values()): logger.info(world.fish.translate("cli", "cli", "shuffling.pots")) @@ -168,7 +168,13 @@ def main(args, seed=None, fish=None): else: link_inverted_entrances(world, player) - logger.info(world.fish.translate("cli","cli","shuffling.dungeons")) + logger.info(world.fish.translate("cli", "cli", "shuffling.prep")) + for player in range(1, world.players + 1): + link_doors_prep(world, player) + + create_item_pool_config(world) + + logger.info(world.fish.translate("cli", "cli", "shuffling.dungeons")) for player in range(1, world.players + 1): link_doors(world, player) @@ -176,8 +182,7 @@ def main(args, seed=None, fish=None): mark_light_world_regions(world, player) else: mark_dark_world_regions(world, player) - logger.info(world.fish.translate("cli","cli","generating.itempool")) - logger.info(world.fish.translate("cli","cli","generating.itempool")) + logger.info(world.fish.translate("cli", "cli", "generating.itempool")) for player in range(1, world.players + 1): generate_itempool(world, player) @@ -195,8 +200,9 @@ def main(args, seed=None, fish=None): else: lock_shop_locations(world, player) - - logger.info(world.fish.translate("cli","cli","placing.dungeon.prizes")) + district_item_pool_config(world) + massage_item_pool(world) + logger.info(world.fish.translate("cli", "cli", "placing.dungeon.prizes")) fill_prizes(world) @@ -205,14 +211,12 @@ def main(args, seed=None, fish=None): logger.info(world.fish.translate("cli","cli","placing.dungeon.items")) - shuffled_locations = None - if args.algorithm in ['balanced', 'vt26'] or any(list(args.mapshuffle.values()) + list(args.compassshuffle.values()) + - list(args.keyshuffle.values()) + list(args.bigkeyshuffle.values())): + if args.algorithm != 'equitable': shuffled_locations = world.get_unfilled_locations() random.shuffle(shuffled_locations) fill_dungeons_restrictive(world, shuffled_locations) else: - fill_dungeons(world) + promote_dungeon_items(world) for player in range(1, world.players+1): if world.logic[player] != 'nologic': @@ -230,34 +234,22 @@ def main(args, seed=None, fish=None): logger.info(world.fish.translate("cli","cli","fill.world")) - if args.algorithm == 'flood': - flood_items(world) # different algo, biased towards early game progress items - elif args.algorithm == 'vt21': - distribute_items_cutoff(world, 1) - elif args.algorithm == 'vt22': - distribute_items_cutoff(world, 0.66) - elif args.algorithm == 'freshness': - distribute_items_staleness(world) - elif args.algorithm == 'vt25': - distribute_items_restrictive(world, False) - elif args.algorithm == 'vt26': - - distribute_items_restrictive(world, True, shuffled_locations) - elif args.algorithm == 'balanced': - distribute_items_restrictive(world, True) + distribute_items_restrictive(world, True) if world.players > 1: - logger.info(world.fish.translate("cli","cli","balance.multiworld")) - balance_multiworld_progression(world) + logger.info(world.fish.translate("cli", "cli", "balance.multiworld")) + if args.algorithm in ['balanced', 'equitable']: + balance_multiworld_progression(world) # if we only check for beatable, we can do this sanity check first before creating the rom if not world.can_beat_game(log_error=True): - raise RuntimeError(world.fish.translate("cli","cli","cannot.beat.game")) + raise RuntimeError(world.fish.translate("cli", "cli", "cannot.beat.game")) for player in range(1, world.players+1): if world.shopsanity[player]: customize_shops(world, player) - balance_money_progression(world) + if args.algorithm in ['balanced', 'equitable']: + balance_money_progression(world) outfilebase = f'DR_{args.outputname if args.outputname else world.seed}' @@ -409,6 +401,7 @@ def copy_world(world): ret.keydropshuffle = world.keydropshuffle.copy() ret.mixed_travel = world.mixed_travel.copy() ret.standardize_palettes = world.standardize_palettes.copy() + ret.restrict_boss_items = world.restrict_boss_items.copy() ret.exp_cache = world.exp_cache.copy() @@ -583,11 +576,11 @@ def create_playthrough(world): # todo: this is not very efficient, but I'm not sure how else to do it for this backwards logic # world.clear_exp_cache() if world.can_beat_game(state_cache[num]): - # logging.getLogger('').debug(f'{old_item.name} (Player {old_item.player}) is not required') + logging.getLogger('').debug(f'{old_item.name} (Player {old_item.player}) is not required') to_delete.add(location) else: # still required, got to keep it around - # logging.getLogger('').debug(f'{old_item.name} (Player {old_item.player}) is required') + logging.getLogger('').debug(f'{old_item.name} (Player {old_item.player}) is required') location.item = old_item # cull entries in spheres for spoiler walkthrough at end diff --git a/Mystery.py b/Mystery.py index 2ec8e3a9..e5074a57 100644 --- a/Mystery.py +++ b/Mystery.py @@ -73,6 +73,8 @@ def main(): if args.enemizercli: erargs.enemizercli = args.enemizercli + mw_settings = {'algorithm': False} + settings_cache = {k: (roll_settings(v) if args.samesettings else None) for k, v in weights_cache.items()} for player in range(1, args.multi + 1): @@ -81,7 +83,12 @@ def main(): settings = settings_cache[path] if settings_cache[path] else roll_settings(weights_cache[path]) for k, v in vars(settings).items(): if v is not None: - getattr(erargs, k)[player] = v + if k == 'algorithm': # multiworld wide parameters + if not mw_settings[k]: # only use the first roll + setattr(erargs, k, v) + mw_settings[k] = True + else: + getattr(erargs, k)[player] = v else: raise RuntimeError(f'No weights specified for player {player}') @@ -129,6 +136,8 @@ def roll_settings(weights): ret = argparse.Namespace() + ret.algorithm = get_choice('algorithm') + glitches_required = get_choice('glitches_required') if glitches_required is not None: if glitches_required not in ['none', 'no_logic']: diff --git a/RaceRandom.py b/RaceRandom.py index 127d966d..fa882580 100644 --- a/RaceRandom.py +++ b/RaceRandom.py @@ -22,6 +22,7 @@ def _wrap(name): # These are for intellisense purposes only, and will be overwritten below choice = _prng_inst.choice +choices = _prng_inst.choices gauss = _prng_inst.gauss getrandbits = _prng_inst.getrandbits randint = _prng_inst.randint diff --git a/Regions.py b/Regions.py index c21953f6..f4bfbcb7 100644 --- a/Regions.py +++ b/Regions.py @@ -999,6 +999,14 @@ def adjust_locations(world, player): world.get_location(location, player).address = 0x400000 + index # player address? it is in the shop table index += 1 + # unreal events: + for l in ['Ganon', 'Agahnim 1', 'Agahnim 2', 'Dark Blacksmith Ruins', 'Frog', 'Missing Smith', 'Floodgate', + 'Trench 1 Switch', 'Trench 2 Switch', 'Swamp Drain', 'Attic Cracked Floor', 'Suspicious Maiden', + 'Revealing Light', 'Ice Block Drop', 'Zelda Pickup', 'Zelda Drop Off']: + location = world.get_location_unsafe(l, player) + if location: + location.real = False + # (type, room_id, shopkeeper, custom, locked, [items]) diff --git a/Rom.py b/Rom.py index 9a76b1d5..ab759ef4 100644 --- a/Rom.py +++ b/Rom.py @@ -2088,6 +2088,7 @@ def write_strings(rom, world, player, team): else: entrances_to_hint.update({'Pyramid Ledge': 'The pyramid ledge'}) hint_count = 4 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull'] else 0 + hint_count -= 2 if world.algorithm == 'district' and world.shuffle[player] not in ['simple', 'restricted'] else 0 for entrance in all_entrances: if entrance.name in entrances_to_hint: if hint_count > 0: @@ -2179,11 +2180,22 @@ def write_strings(rom, world, player, team): else: tt[hint_locations.pop(0)] = this_hint - # All remaining hint slots are filled with junk hints. It is done this way to ensure the same junk hint isn't selected twice. - junk_hints = junk_texts.copy() - random.shuffle(junk_hints) - for location in hint_locations: - tt[location] = junk_hints.pop(0) + if world.algorithm == 'district': + hint_candidates = [] + for name, district in world.districts[player].items(): + if name not in world.item_pool_config.recorded_choices and not district.sphere_one: + hint_candidates.append(f'{name} is a foolish choice') + random.shuffle(hint_candidates) + foolish_choice_hints = min(len(hint_candidates), len(hint_locations)) + for i in range(0, foolish_choice_hints): + tt[hint_locations.pop(0)] = hint_candidates.pop(0) + if len(hint_locations) > 0: + # All remaining hint slots are filled with junk hints. It is done this way to ensure the same junk hint + # isn't selected twice. + junk_hints = junk_texts.copy() + random.shuffle(junk_hints) + for location in hint_locations: + tt[location] = junk_hints.pop(0) # We still need the older hints of course. Those are done here. @@ -2341,7 +2353,7 @@ def set_inverted_mode(world, player, rom): write_int16(rom, snes_to_pc(0x02E8D5), 0x07C8) write_int16(rom, snes_to_pc(0x02E8F7), 0x01F8) rom.write_byte(snes_to_pc(0x08D40C), 0xD0) # morph proof - rom.write_byte(snes_to_pc(0x1BC428), 0x00) # remove diggable light world portals + rom.write_byte(snes_to_pc(0x1BC428), 0x00) # remove diggable light world portals rom.write_byte(snes_to_pc(0x1BC43A), 0x00) rom.write_byte(snes_to_pc(0x1BC590), 0x00) rom.write_byte(snes_to_pc(0x1BC5A1), 0x00) diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index 867f424c..f2b99fb9 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -101,12 +101,11 @@ "algorithm": { "choices": [ "balanced", - "freshness", - "flood", - "vt21", - "vt22", - "vt25", - "vt26" + "equitable", + "vanilla_fill", + "major_only", + "dungeon_only", + "district" ] }, "shuffle": { diff --git a/resources/app/cli/lang/en.json b/resources/app/cli/lang/en.json index 950c89a3..4bc52e24 100644 --- a/resources/app/cli/lang/en.json +++ b/resources/app/cli/lang/en.json @@ -7,6 +7,7 @@ "seed": "Seed", "player": "Player", "shuffling.world": "Shuffling the World about", + "shuffling.prep": "Dungeon and Item prep", "shuffling.dungeons": "Shuffling dungeons", "shuffling.pots": "Shuffling pots", "basic.traversal": "--Basic Traversal", @@ -153,22 +154,28 @@ "balanced: vt26 derivative that aims to strike a balance between", " the overworld heavy vt25 and the dungeon heavy vt26", " algorithm.", - "vt26: Shuffle items and place them in a random location", - " that it is not impossible to be in. This includes", - " dungeon keys and items.", - "vt25: Shuffle items and place them in a random location", - " that it is not impossible to be in.", - "vt21: Unbiased in its selection, but has tendency to put", - " Ice Rod in Turtle Rock.", - "vt22: Drops off stale locations after 1/3 of progress", - " items were placed to try to circumvent vt21\\'s", - " shortcomings.", - "Freshness: Keep track of stale locations (ones that cannot be", - " reached yet) and decrease likeliness of selecting", - " them the more often they were found unreachable.", - "Flood: Push out items starting from Link\\'s House and", - " slightly biased to placing progression items with", - " less restrictions." + "equitable: does not place dungeon items first allowing new potential", + " but mixed with the normal advancement pool", + "restricted placements: these consider all major items to be special and attempts", + "to place items from fixed to semi-random locations. For purposes of these shuffles, all", + "Y items, A items, swords (unless vanilla swords), mails, shields, heart containers and", + "1/2 magic are considered to be part of a major items pool. Big Keys are added to the pool", + "if shuffled. Same for small keys, compasses, maps, keydrops (if small keys are also shuffled),", + "1 of each capacity upgrade for shopsanity, the quiver item for retro+shopsanity, and", + "triforce pieces for Triforce Hunt. Future modes will add to these as appropriate.", + "vanilla_fill As above, but attempts to place items in their vanilla", + " location first. Major items that cannot be placed that way", + " will attempt to be placed in other failed locations first.", + " Also attempts to place all items in vanilla locations", + "major_only As above, but uses the major items' location preferentially", + " major item location are defined as the group of location where", + " the items are found in the vanilla game.", + "dungeon_only As above, but major items are preferentially placed", + " in dungeons locations first", + "district As above, but groups of locations are chosen randomly", + " from a pool of fixed locations designed to be interesting", + " and give major clues about the location of other", + " advancement items. These fixed groups will be documented." ], "shuffle": [ "Select Entrance Shuffling Algorithm. (default: %(default)s)", diff --git a/resources/app/gui/lang/en.json b/resources/app/gui/lang/en.json index 448c8ac8..d40c34fb 100644 --- a/resources/app/gui/lang/en.json +++ b/resources/app/gui/lang/en.json @@ -283,13 +283,12 @@ "randomizer.item.accessibility.none": "Beatable", "randomizer.item.sortingalgo": "Item Sorting", - "randomizer.item.sortingalgo.freshness": "Freshness", - "randomizer.item.sortingalgo.flood": "Flood", - "randomizer.item.sortingalgo.vt21": "VT8.21", - "randomizer.item.sortingalgo.vt22": "VT8.22", - "randomizer.item.sortingalgo.vt25": "VT8.25", - "randomizer.item.sortingalgo.vt26": "VT8.26", "randomizer.item.sortingalgo.balanced": "Balanced", + "randomizer.item.sortingalgo.equitable": "Equitable", + "randomizer.item.sortingalgo.vanilla_fill": "Vanilla Fill", + "randomizer.item.sortingalgo.major_only": "Major Location Restriction", + "randomizer.item.sortingalgo.dungeon_only": "Dungeon Restriction", + "randomizer.item.sortingalgo.district": "District Restriction", "randomizer.item.restrict_boss_items": "Forbidden Boss Items", "randomizer.item.restrict_boss_items.none": "None", diff --git a/resources/app/gui/randomize/item/widgets.json b/resources/app/gui/randomize/item/widgets.json index 89cacb00..7b76d8fe 100644 --- a/resources/app/gui/randomize/item/widgets.json +++ b/resources/app/gui/randomize/item/widgets.json @@ -116,13 +116,12 @@ "type": "selectbox", "default": "balanced", "options": [ - "freshness", - "flood", - "vt21", - "vt22", - "vt25", - "vt26", - "balanced" + "balanced", + "equitable", + "vanilla_fill", + "major_only", + "dungeon_only", + "district" ] }, "restrict_boss_items": { diff --git a/source/item/District.py b/source/item/District.py new file mode 100644 index 00000000..7b48ede8 --- /dev/null +++ b/source/item/District.py @@ -0,0 +1,169 @@ +from collections import deque + +from BaseClasses import CollectionState, RegionType +from Dungeons import dungeon_table + + +class District(object): + + def __init__(self, name, locations, entrances=None, dungeon=None): + self.name = name + self.dungeon = dungeon + self.locations = locations + self.entrances = entrances if entrances else [] + self.sphere_one = False + + +def create_districts(world): + world.districts = {} + for p in range(1, world.players + 1): + create_district_helper(world, p) + + +def create_district_helper(world, player): + inverted = world.mode[player] == 'inverted' + districts = {} + kak_locations = {'Bottle Merchant', 'Kakariko Tavern', 'Maze Race'} + nw_lw_locations = {'Mushroom', 'Master Sword Pedestal'} + central_lw_locations = {'Sunken Treasure', 'Flute Spot'} + desert_locations = {'Purple Chest', 'Desert Ledge'} + lake_locations = {'Hobo'} + east_lw_locations = {"Zora's Ledge", 'King Zora'} + lw_dm_locations = {'Old Man', 'Spectacle Rock', 'Ether Tablet'} + east_dw_locations = {'Pyramid', 'Catfish'} + south_dw_locations = {'Stumpy', 'Digging Game', 'Bombos Tablet', 'Lake Hylia Island'} + voo_north_locations = {'Bumper Cave Ledge'} + ddm_locations = {'Floating Island'} + + kak_entrances = ['Kakariko Well Cave', 'Bat Cave Cave', 'Elder House (East)', 'Elder House (West)', + 'Two Brothers House (East)', 'Two Brothers House (West)', 'Blinds Hideout', 'Chicken House', + 'Blacksmiths Hut', 'Sick Kids House', 'Snitch Lady (East)', 'Snitch Lady (West)', + 'Bush Covered House', 'Tavern (Front)', 'Light World Bomb Hut', 'Kakariko Shop', 'Library', + 'Kakariko Gamble Game', 'Kakariko Well Drop', 'Bat Cave Drop'] + nw_lw_entrances = ['North Fairy Cave', 'Lost Woods Hideout Stump', 'Lumberjack Tree Cave', 'Sanctuary', + 'Old Man Cave (West)', 'Death Mountain Return Cave (West)', 'Kings Grave', 'Lost Woods Gamble', + 'Fortune Teller (Light)', 'Bonk Rock Cave', 'Lumberjack House', 'North Fairy Cave Drop', + 'Lost Woods Hideout Drop', 'Lumberjack Tree Tree', 'Sanctuary Grave'] + central_lw_entrances = ['Links House', 'Hyrule Castle Entrance (South)', 'Hyrule Castle Entrance (West)', + 'Hyrule Castle Entrance (East)', 'Agahnims Tower', 'Hyrule Castle Secret Entrance Stairs', + 'Dam', 'Bonk Fairy (Light)', 'Light Hype Fairy', 'Cave Shop (Lake Hylia)', + 'Lake Hylia Fortune Teller', 'Hyrule Castle Secret Entrance Drop'] + desert_entrances = ['Desert Palace Entrance (South)', 'Desert Palace Entrance (West)', + 'Desert Palace Entrance (North)', 'Desert Palace Entrance (East)', 'Desert Fairy', + 'Aginahs Cave', '50 Rupee Cave'] + lake_entrances = ['Capacity Upgrade', 'Mini Moldorm Cave', 'Good Bee Cave', '20 Rupee Cave', 'Ice Rod Cave'] + east_lw_entrances = ['Eastern Palace', 'Waterfall of Wishing', 'Lake Hylia Fairy', 'Sahasrahlas Hut', + 'Long Fairy Cave', 'Potion Shop'] + lw_dm_entrances = ['Tower of Hera', 'Old Man Cave (East)', 'Old Man House (Bottom)', 'Old Man House (Top)', + 'Death Mountain Return Cave (East)', 'Spectacle Rock Cave Peak', 'Spectacle Rock Cave', + 'Spectacle Rock Cave (Bottom)', 'Paradox Cave (Bottom)', 'Paradox Cave (Middle)', + 'Paradox Cave (Top)', 'Fairy Ascension Cave (Bottom)', 'Fairy Ascension Cave (Top)', + 'Spiral Cave', 'Spiral Cave (Bottom)', 'Hookshot Fairy'] + east_dw_entrances = ['Palace of Darkness', 'Pyramid Entrance', 'Pyramid Fairy', 'East Dark World Hint', + 'Palace of Darkness Hint', 'Dark Lake Hylia Fairy', 'Dark World Potion Shop', 'Pyramid Hole'] + south_dw_entrances = ['Ice Palace', 'Swamp Palace', 'Dark Lake Hylia Ledge Fairy', + 'Dark Lake Hylia Ledge Spike Cave', 'Dark Lake Hylia Ledge Hint', 'Hype Cave', + 'Bonk Fairy (Dark)', 'Archery Game', 'Big Bomb Shop', 'Dark Lake Hylia Shop', 'Cave 45'] + voo_north_entrances = ['Thieves Town', 'Skull Woods First Section Door', 'Skull Woods Second Section Door (East)', + 'Skull Woods Second Section Door (West)', 'Skull Woods Final Section', + 'Bumper Cave (Bottom)', 'Bumper Cave (Top)', 'Brewery', 'C-Shaped House', 'Chest Game', + 'Dark World Hammer Peg Cave', 'Red Shield Shop', 'Dark Sanctuary Hint', + 'Fortune Teller (Dark)', 'Dark World Shop', 'Dark World Lumberjack Shop', 'Graveyard Cave', + 'Skull Woods First Section Hole (West)', 'Skull Woods First Section Hole (East)', + 'Skull Woods First Section Hole (North)', 'Skull Woods Second Section Hole'] + mire_entrances = ['Misery Mire', 'Mire Shed', 'Dark Desert Hint', 'Dark Desert Fairy', 'Checkerboard Cave'] + ddm_entrances = ['Turtle Rock', 'Dark Death Mountain Ledge (West)', 'Dark Death Mountain Ledge (East)', + 'Turtle Rock Isolated Ledge Entrance', 'Superbunny Cave (Top)', 'Superbunny Cave (Bottom)', + 'Hookshot Cave', 'Hookshot Cave Back Entrance', 'Ganons Tower', 'Spike Cave', + 'Cave Shop (Dark Death Mountain)', 'Dark Death Mountain Fairy', 'Mimic Cave'] + + if inverted: + south_dw_locations.remove('Bombos Tablet') + south_dw_locations.remove('Lake Hylia Island') + voo_north_locations.remove('Bumper Cave Ledge') + ddm_locations.remove('Floating Island') + desert_locations.add('Bombos Tablet') + lake_locations.add('Lake Hylia Island') + nw_lw_locations.add('Bumper Cave Ledge') + lw_dm_locations.add('Floating Island') + + south_dw_entrances.remove('Cave 45') + central_lw_entrances.append('Cave 45') + voo_north_entrances.remove('Graveyard Cave') + nw_lw_entrances.append('Graveyard Cave') + mire_entrances.remove('Checkerboard Cave') + desert_entrances.append('Checkerboard Cave') + ddm_entrances.remove('Mimic Cave') + lw_dm_entrances.append('Mimic Cave') + + south_dw_entrances.remove('Big Bomb Shop') + central_lw_entrances.append('Inverted Big Bomb Shop') + central_lw_entrances.remove('Links House') + south_dw_entrances.append('Inverted Links House') + voo_north_entrances.remove('Dark Sanctuary') + voo_north_entrances.append('Inverted Dark Sanctuary') + ddm_entrances.remove('Ganons Tower') + central_lw_entrances.append('Inverted Ganons Tower') + central_lw_entrances.remove('Agahnims Tower') + ddm_entrances.append('Inverted Agahnims Tower') + east_dw_entrances.remove('Pyramid Entrance') + central_lw_entrances.append('Inverted Pyramid Entrance') + east_dw_entrances.remove('Pyramid Hole') + central_lw_entrances.append('Inverted Pyramid Hole') + + districts['Kakariko'] = District('Kakariko', kak_locations, entrances=kak_entrances) + districts['Northwest Hyrule'] = District('Northwest Hyrule', nw_lw_locations, entrances=nw_lw_entrances) + districts['Central Hyrule'] = District('Central Hyrule', central_lw_locations, entrances=central_lw_entrances) + districts['Desert'] = District('Desert', desert_locations, entrances=desert_entrances) + districts['Lake Hylia'] = District('Lake Hylia', lake_locations, entrances=lake_entrances) + districts['Eastern Hyrule'] = District('Eastern Hyrule', east_lw_locations, entrances=east_lw_entrances) + districts['Death Mountain'] = District('Death Mountain', lw_dm_locations, entrances=lw_dm_entrances) + districts['East Dark World'] = District('East Dark World', east_dw_locations, entrances=east_dw_entrances) + districts['South Dark World'] = District('South Dark World', south_dw_locations, entrances=south_dw_entrances) + districts['Northwest Dark World'] = District('Northwest Dark World', voo_north_locations, + entrances=voo_north_entrances) + districts['The Mire'] = District('The Mire', set(), entrances=mire_entrances) + districts['Dark Death Mountain'] = District('Dark Death Mountain', ddm_locations, entrances=ddm_entrances) + districts.update({x: District(x, set(), dungeon=x) for x in dungeon_table.keys()}) + + world.districts[player] = districts + + +def resolve_districts(world): + create_districts(world) + state = CollectionState(world) + state.sweep_for_events() + for player in range(1, world.players + 1): + check_set = find_reachable_locations(state, player) + used_locations = {l for d in world.districts[player].values() for l in d.locations} + for name, district in world.districts[player].items(): + if district.dungeon: + layout = world.dungeon_layouts[player][district.dungeon] + district.locations.update([l.name for r in layout.master_sector.regions + for l in r.locations if not l.item and l.real]) + else: + for entrance in district.entrances: + ent = world.get_entrance(entrance, player) + queue = deque([ent.connected_region]) + visited = set() + while len(queue) > 0: + region = queue.pop() + visited.add(region) + if region.type == RegionType.Cave: + for location in region.locations: + if location.name not in used_locations and not location.item and location.real: + district.locations.add(location.name) + used_locations.add(location.name) + for ext in region.exits: + if ext.connected_region not in visited: + queue.appendleft(ext.connected_region) + district.sphere_one = len(check_set.intersection(district.locations)) > 0 + + +def find_reachable_locations(state, player): + check_set = set() + for region in state.reachable_regions[player]: + for location in region.locations: + if location.can_reach(state) and not location.forced_item and location.real: + check_set.add(location.name) + return check_set diff --git a/source/item/FillUtil.py b/source/item/FillUtil.py index 0a1e54f7..96e33cfd 100644 --- a/source/item/FillUtil.py +++ b/source/item/FillUtil.py @@ -1,12 +1,48 @@ +import RaceRandom as random +import logging +from math import ceil from collections import defaultdict +from source.item.District import resolve_districts +from DoorShuffle import validate_vanilla_reservation from Dungeons import dungeon_table +from Items import item_table, ItemFactory + class ItemPoolConfig(object): def __init__(self): + self.location_groups = None + self.static_placement = None + self.item_pool = None + self.placeholders = None self.reserved_locations = defaultdict(set) + self.recorded_choices = [] + + +class LocationGroup(object): + def __init__(self, name): + self.name = name + self.locations = [] + + # flags + self.keyshuffle = False + self.keydropshuffle = False + self.shopsanity = False + self.retro = False + + def locs(self, locs): + self.locations = list(locs) + return self + + def flags(self, k, d=False, s=False, r=False): + self.keyshuffle = k + self.keydropshuffle = d + self.shopsanity = s + self.retro = r + return self + def create_item_pool_config(world): world.item_pool_config = config = ItemPoolConfig() @@ -24,3 +60,1000 @@ def create_item_pool_config(world): for item in dungeon.all_items: if item.map or item.compass: item.advancement = True + if world.algorithm == 'vanilla_fill': + config.static_placement = {} + config.location_groups = {} + for player in range(1, world.players + 1): + config.static_placement[player] = vanilla_mapping.copy() + if world.keydropshuffle[player]: + for item, locs in keydrop_vanilla_mapping.items(): + if item in config.static_placement[player]: + config.static_placement[player][item].extend(locs) + else: + config.static_placement[player][item] = list(locs) + # todo: shopsanity... + # todo: retro (universal keys...) + # retro + shops + config.location_groups[player] = [ + LocationGroup('Major').locs(mode_grouping['Overworld Major'] + mode_grouping['Big Chests'] + mode_grouping['Heart Containers']), + LocationGroup('bkhp').locs(mode_grouping['Heart Pieces']), + LocationGroup('bktrash').locs(mode_grouping['Overworld Trash'] + mode_grouping['Dungeon Trash']), + LocationGroup('bkgt').locs(mode_grouping['GT Trash'])] + for loc_name in mode_grouping['Big Chests'] + mode_grouping['Heart Containers']: + config.reserved_locations[player].add(loc_name) + elif world.algorithm == 'major_only': + config.location_groups = [ + LocationGroup('MajorItems'), + LocationGroup('Backup') + ] + config.item_pool = {} + init_set = mode_grouping['Overworld Major'] + mode_grouping['Big Chests'] + mode_grouping['Heart Containers'] + for player in range(1, world.players + 1): + groups = LocationGroup('Major').locs(init_set) + if world.bigkeyshuffle[player]: + groups.locations.extend(mode_grouping['Big Keys']) + if world.keydropshuffle[player]: + groups.locations.append(mode_grouping['Big Key Drops']) + if world.keyshuffle[player]: + groups.locations.extend(mode_grouping['Small Keys']) + if world.keydropshuffle[player]: + groups.locations.extend(mode_grouping['Key Drops']) + if world.compassshuffle[player]: + groups.locations.extend(mode_grouping['Compasses']) + if world.mapshuffle[player]: + groups.locations.extend(mode_grouping['Maps']) + if world.shopsanity[player]: + groups.locations.append('Capacity Upgrade - Left') + groups.locations.append('Capacity Upgrade - Right') + if world.retro[player]: + if world.shopsanity[player]: + pass # todo: 5 locations for single arrow representation? + config.item_pool[player] = determine_major_items(world, player) + config.location_groups[0].locations = set(groups.locations) + config.reserved_locations[player].add(groups.locations) + backup = (mode_grouping['Heart Pieces'] + mode_grouping['Dungeon Trash'] + mode_grouping['Shops'] + + mode_grouping['Overworld Trash'] + mode_grouping['GT Trash'] + mode_grouping['RetroShops']) + config.location_groups[1].locations = set(backup) + elif world.algorithm == 'dungeon_only': + config.location_groups = [ + LocationGroup('Dungeons'), + LocationGroup('Backup') + ] + config.item_pool = {} + dungeon_set = (mode_grouping['Big Chests'] + mode_grouping['Dungeon Trash'] + mode_grouping['Big Keys'] + + mode_grouping['Heart Containers'] + mode_grouping['GT Trash'] + mode_grouping['Small Keys'] + + mode_grouping['Compasses'] + mode_grouping['Maps'] + mode_grouping['Key Drops'] + + mode_grouping['Big Key Drops']) + for player in range(1, world.players + 1): + config.item_pool[player] = determine_major_items(world, player) + config.location_groups[0].locations = set(dungeon_set) + backup = (mode_grouping['Heart Pieces'] + mode_grouping['Overworld Major'] + + mode_grouping['Overworld Trash'] + mode_grouping['Shops'] + mode_grouping['RetroShops']) + config.location_groups[1].locations = set(backup) + elif world.algorithm == 'cluster_bias': + config.location_groups = [ + LocationGroup('Clusters'), + ] + item_cnt = defaultdict(int) + config.item_pool = {} + for player in range(1, world.players + 1): + config.item_pool[player] = determine_major_items(world, player) + item_cnt[player] += count_major_items(world, player) + # set cluster choices + cluster_choices = figure_out_clustered_choices(world) + + chosen_locations = defaultdict(set) + placeholder_cnt = 0 + for player in range(1, world.players + 1): + number_of_clusters = ceil(item_cnt[player] / 13) + location_cnt = 0 + while item_cnt[player] > location_cnt: + chosen_clusters = random.sample(cluster_choices[player], number_of_clusters) + for loc_group in chosen_clusters: + for location in loc_group.locations: + if not location_prefilled(location, world, player): + world.item_pool_config.reserved_locations[player].add(location) + chosen_locations[location].add(player) + location_cnt += 1 + cluster_choices[player] = [x for x in cluster_choices[player] if x not in chosen_clusters] + number_of_clusters = 1 + placeholder_cnt += location_cnt - item_cnt[player] + config.placeholders = placeholder_cnt + config.location_groups[0].locations = chosen_locations + elif world.algorithm == 'entangled' and world.players > 1: + config.location_groups = [ + LocationGroup('Entangled'), + ] + item_cnt = 0 + config.item_pool = {} + limits = {} + for player in range(1, world.players + 1): + config.item_pool[player] = determine_major_items(world, player) + item_cnt += count_major_items(world, player) + limits[player] = calc_dungeon_limits(world, player) + c_set = {} + for location in world.get_locations(): + if location.real and not location.forced_item: + c_set[location.name] = None + # todo: retroshop locations are created later, so count them here? + ttl_locations, candidates = 0, list(c_set.keys()) + chosen_locations = defaultdict(set) + random.shuffle(candidates) + while ttl_locations < item_cnt: + choice = candidates.pop() + dungeon = world.get_location(choice, 1).parent_region.dungeon + if dungeon: + for player in range(1, world.players + 1): + location = world.get_location(choice, player) + if location.real and not location.forced_item: + if isinstance(limits[player], int): + if limits[player] > 0: + config.reserved_locations[player].add(choice) + limits[player] -= 1 + chosen_locations[choice].add(player) + else: + previous = previously_reserved(location, world, player) + if limits[player][dungeon.name] > 0 or previous: + if validate_reservation(location, dungeon, world, player): + if not previous: + limits[player][dungeon.name] -= 1 + chosen_locations[choice].add(player) + else: # not dungeon restricted + for player in range(1, world.players + 1): + location = world.get_location(choice, player) + if location.real and not location.forced_item: + chosen_locations[choice].add(player) + ttl_locations += len(chosen_locations[choice]) + config.placeholders = ttl_locations - item_cnt + config.location_groups[0].locations = chosen_locations + + +def district_item_pool_config(world): + resolve_districts(world) + if world.algorithm == 'district': + config = world.item_pool_config + config.location_groups = [ + LocationGroup('Districts'), + ] + item_cnt = 0 + config.item_pool = {} + for player in range(1, world.players + 1): + config.item_pool[player] = determine_major_items(world, player) + item_cnt += count_major_items(world, player) + # set district choices + district_choices = {} + for p in range(1, world.players + 1): + for name, district in world.districts[p].items(): + adjustment = 0 + if district.dungeon: + adjustment = len([i for i in world.get_dungeon(name, p).all_items + if i.is_inside_dungeon_item(world)]) + dist_len = len(district.locations) - adjustment + if name not in district_choices: + district_choices[name] = (district.sphere_one, dist_len) + else: + so, amt = district_choices[name] + district_choices[name] = (so or district.sphere_one, amt + dist_len) + + chosen_locations = defaultdict(set) + location_cnt = 0 + + # choose a sphere one district + sphere_one_choices = [d for d, info in district_choices.items() if info[0]] + sphere_one = random.choice(sphere_one_choices) + so, amt = district_choices[sphere_one] + location_cnt += amt + for player in range(1, world.players + 1): + for location in world.districts[player][sphere_one].locations: + chosen_locations[location].add(player) + del district_choices[sphere_one] + config.recorded_choices.append(sphere_one) + + scale_factors = defaultdict(int) + scale_total = 0 + for p in range(1, world.players + 1): + ent = 'Inverted Ganons Tower' if world.mode[p] == 'inverted' else 'Ganons Tower' + dungeon = world.get_entrance(ent, p).connected_region.dungeon + if dungeon: + scale = world.crystals_needed_for_gt[p] + scale_total += scale + scale_factors[dungeon.name] += scale + scale_total = max(1, scale_total) + scale_divisors = defaultdict(lambda: 1) + scale_divisors.update(scale_factors) + + while location_cnt < item_cnt: + weights = [scale_total / scale_divisors[d] for d in district_choices.keys()] + choice = random.choices(list(district_choices.keys()), weights=weights, k=1)[0] + so, amt = district_choices[choice] + location_cnt += amt + for player in range(1, world.players + 1): + for location in world.districts[player][choice].locations: + chosen_locations[location].add(player) + del district_choices[choice] + config.recorded_choices.append(choice) + config.placeholders = location_cnt - item_cnt + config.location_groups[0].locations = chosen_locations + + +def location_prefilled(location, world, player): + if world.swords[player] == 'vanilla': + return location in vanilla_swords + if world.goal[player] == 'pedestal': + return location == 'Master Sword Pedestal' + return False + + +def previously_reserved(location, world, player): + if '- Boss' in location.name: + if world.restrict_boss_items[player] == 'mapcompass' and (not world.compassshuffle[player] + or not world.mapshuffle[player]): + return True + if world.restrict_boss_items[player] == 'dungeon' and (not world.compassshuffle[player] + or not world.mapshuffle[player] + or not world.bigkeyshuffle[player] + or not (world.keyshuffle[player] or world.retro[player])): + return True + return False + + +def massage_item_pool(world): + player_pool = defaultdict(list) + for item in world.itempool: + player_pool[item.player].append(item) + for dungeon in world.dungeons: + for item in dungeon.all_items: + if item.is_inside_dungeon_item(world): + player_pool[item.player].append(item) + player_locations = defaultdict(list) + for player in player_pool: + player_locations[player] = [x for x in world.get_unfilled_locations(player) if '- Prize' not in x.name] + discrepancy = len(player_pool[player]) - len(player_locations[player]) + if discrepancy: + trash_options = [x for x in player_pool[player] if x.name in trash_items] + random.shuffle(trash_options) + trash_options = sorted(trash_options, key=lambda x: trash_items[x.name], reverse=True) + while discrepancy > 0 and len(trash_options) > 0: + deleted = trash_options.pop() + world.itempool.remove(deleted) + discrepancy -= 1 + if discrepancy > 0: + logging.getLogger('').warning(f'Too many good items in pool, something will be removed at random') + if world.item_pool_config.placeholders is not None: + removed = 0 + single_rupees = [item for item in world.itempool if item.name == 'Rupee (1)'] + removed += len(single_rupees) + for x in single_rupees: + world.itempool.remove(x) + if removed < world.item_pool_config.placeholders: + trash_options = [x for x in world.itempool if x.name in trash_items] + random.shuffle(trash_options) + trash_options = sorted(trash_options, key=lambda x: trash_items[x.name], reverse=True) + while removed < world.item_pool_config.placeholders: + if len(trash_options) == 0: + logging.getLogger('').warning(f'Too many good items in pool, not enough room for placeholders') + deleted = trash_options.pop() + world.itempool.remove(deleted) + removed += 1 + if world.item_pool_config.placeholders > len(single_rupees): + for _ in range(world.item_pool_config.placeholders-len(single_rupees)): + single_rupees.append(ItemFactory('Rupee (1)', random.randint(1, world.players))) + placeholders = random.sample(single_rupees, world.item_pool_config.placeholders) + world.itempool += placeholders + removed -= len(placeholders) + for _ in range(removed): + world.itempool.append(ItemFactory('Rupees (5)', random.randint(1, world.players))) + + +def replace_trash_item(item_pool, replacement): + trash_options = [x for x in item_pool if x.name in trash_items] + random.shuffle(trash_options) + trash_options = sorted(trash_options, key=lambda x: trash_items[x.name], reverse=True) + if len(trash_options) == 0: + logging.getLogger('').warning(f'Too many good items in pool, not enough room for placeholders') + deleted = trash_options.pop() + item_pool.remove(deleted) + replace_item = ItemFactory(replacement, deleted.player) + item_pool.append(replace_item) + return replace_item + + +def validate_reservation(location, dungeon, world, player): + world.item_pool_config.reserved_locations[player].add(location.name) + if world.doorShuffle[player] != 'vanilla': + return True # we can generate the dungeon somehow most likely + if validate_vanilla_reservation(dungeon, world, player): + return True + world.item_pool_config.reserved_locations[player].remove(location.name) + return False + + +def count_major_items(world, player): + major_item_set = 52 + if world.bigkeyshuffle[player]: + major_item_set += 11 + if world.keydropshuffle[player]: + major_item_set += 1 + if world.doorShuffle[player] == 'crossed': + major_item_set += 1 + if world.keyshuffle[player]: + major_item_set += 29 + if world.keydropshuffle[player]: + major_item_set += 32 + if world.compassshuffle[player]: + major_item_set += 11 + if world.doorShuffle[player] == 'crossed': + major_item_set += 2 + if world.mapshuffle[player]: + major_item_set += 12 + if world.doorShuffle[player] == 'crossed': + major_item_set += 1 + if world.shopsanity[player]: + major_item_set += 2 + if world.retro[player]: + major_item_set += 5 # the single arrow quiver + if world.goal == 'triforcehunt': + major_item_set += world.triforce_pool[player] + if world.bombbag[player]: + major_item_set += world.triforce_pool[player] + if world.swords[player] != "random": + if world.swords[player] == 'assured': + major_item_set -= 1 + if world.swords[player] in ['vanilla', 'swordless']: + major_item_set -= 4 + if world.retro[player]: + if world.shopsanity[player]: + major_item_set -= 1 # sword in old man cave + if world.keyshuffle[player]: + major_item_set -= 29 + # universal keys + major_item_set += 19 if world.difficulty[player] == 'normal' else 14 + if world.mode[player] == 'standard' and world.doorShuffle[player] == 'vanilla': + major_item_set -= 1 # a key in escape + if world.doorShuffle[player] != 'vanilla': + major_item_set += 10 # tries to add up to 10 more universal keys for door rando + # todo: starting equipment? + return major_item_set + + +def calc_dungeon_limits(world, player): + b, s, c, m, k, r, bi = (world.bigkeyshuffle[player], world.keyshuffle[player], world.compassshuffle[player], + world.mapshuffle[player], world.keydropshuffle[player], world.retro[player], + world.restrict_boss_items[player]) + if world.doorShuffle[player] in ['vanilla', 'basic']: + limits = {} + for dungeon, info in dungeon_table.items(): + val = info.free_items + if bi != 'none' and info.prize: + if bi == 'mapcompass' and (not c or not m): + val -= 1 + if bi == 'dungeon' and (not c or not m or not (s or r) or not b): + val -= 1 + if b: + val += 1 if info.bk_present else 0 + if k: + val += 1 if info.bk_drops else 0 + if s or r: + val += info.key_num + if k: + val += info.key_drops + if c: + val += 1 if info.compass_present else 0 + if m: + val += 1 if info.map_present else 0 + limits[dungeon] = val + else: + limits = 60 + if world.bigkeyshuffle[player]: + limits += 11 + if world.keydropshuffle[player]: + limits += 1 + if world.keyshuffle[player] or world.retro[player]: + limits += 29 + if world.keydropshuffle[player]: + limits += 32 + if world.compassshuffle[player]: + limits += 11 + if world.mapshuffle[player]: + limits += 12 + return limits + + +def determine_major_items(world, player): + major_item_set = set(major_items) + if world.progressive == 'off': + pass # now what? + if world.bigkeyshuffle[player]: + major_item_set.update({x for x, y in item_table.items() if y[2] == 'BigKey'}) + if world.keyshuffle[player]: + major_item_set.update({x for x, y in item_table.items() if y[2] == 'SmallKey'}) + if world.compassshuffle[player]: + major_item_set.update({x for x, y in item_table.items() if y[2] == 'Compass'}) + if world.mapshuffle[player]: + major_item_set.update({x for x, y in item_table.items() if y[2] == 'Map'}) + if world.shopsanity[player]: + major_item_set.add('Bomb Upgrade (+5)') + major_item_set.add('Arrow Upgrade (+5)') + if world.retro[player]: + major_item_set.add('Single Arrow') + major_item_set.add('Small Key (Universal)') + if world.goal == 'triforcehunt': + major_item_set.add('Triforce Piece') + if world.bombbag[player]: + major_item_set.add('Bomb Upgrade (+10)') + return major_item_set + + +def classify_major_items(world): + if world.algorithm in ['major_only', 'dungeon_only', 'district']: + config = world.item_pool_config + for item in world.itempool: + if item.name in config.item_pool[item.player]: + if not item.advancement and not item.priority: + if item.smallkey or item.bigkey: + item.advancement = True + else: + item.priority = True + else: + if item.priority: + item.priority = False + + +def figure_out_clustered_choices(world): + cluster_candidates = {} + for player in range(1, world.players + 1): + cluster_candidates[player] = [LocationGroup(x.name).locs(x.locations) for x in clustered_groups] + backups = list(reversed(leftovers)) + if world.bigkeyshuffle[player]: + bk_grp = LocationGroup('BigKeys').locs(mode_grouping['Big Keys']) + if world.keydropshuffle[player]: + bk_grp.locations.append(mode_grouping['Big Key Drops']) + for i in range(13-len(bk_grp.locations)): + bk_grp.locations.append(backups.pop()) + cluster_candidates[player].append(bk_grp) + if world.compassshuffle[player]: + cmp_grp = LocationGroup('Compasses').locs(mode_grouping['Compasses']) + if len(cmp_grp.locations) + len(backups) >= 13: + for i in range(13-len(cmp_grp.locations)): + cmp_grp.locations.append(backups.pop()) + cluster_candidates[player].append(cmp_grp) + else: + backups.extend(reversed(cmp_grp.locations)) + if world.mapshuffle[player]: + mp_grp = LocationGroup('Maps').locs(mode_grouping['Maps']) + if len(mp_grp.locations) + len(backups) >= 13: + for i in range(13-len(mp_grp.locations)): + mp_grp.locations.append(backups.pop()) + cluster_candidates[player].append(mp_grp) + else: + backups.extend(reversed(mp_grp.locations)) + if world.shopsanity[player]: + cluster_candidates[player].append(LocationGroup('Shopsanity1').locs(other_clusters['Shopsanity1'])) + cluster_candidates[player].append(LocationGroup('Shopsanity2').locs(other_clusters['Shopsanity2'])) + extras = list(other_clusters['ShopsanityLeft']) + if world.retro[player]: + extras.extend(mode_grouping['RetroShops']) + if len(extras)+len(backups) >= 13: + for i in range(13-len(extras)): + extras.append(backups.pop()) + cluster_candidates[player].append(LocationGroup('ShopExtra').locs(extras)) + else: + backups.extend(reversed(extras)) + if world.keyshuffle[player] or world.retro[player]: + cluster_candidates[player].append(LocationGroup('SmallKey1').locs(other_clusters['SmallKey1'])) + cluster_candidates[player].append(LocationGroup('SmallKey2').locs(other_clusters['SmallKey2'])) + extras = list(other_clusters['SmallKeyLeft']) + if world.keydropshuffle[player]: + cluster_candidates[player].append(LocationGroup('KeyDrop1').locs(other_clusters['KeyDrop1'])) + cluster_candidates[player].append(LocationGroup('KeyDrop2').locs(other_clusters['KeyDrop2'])) + extras.extend(other_clusters['KeyDropLeft']) + if len(extras)+len(backups) >= 13: + for i in range(13-len(extras)): + extras.append(backups.pop()) + cluster_candidates[player].append(LocationGroup('SmallKeyExtra').locs(extras)) + else: + backups.extend(reversed(extras)) + return cluster_candidates + + +def vanilla_fallback(item_to_place, locations, world): + if item_to_place.is_inside_dungeon_item(world): + return [x for x in locations if x.name in vanilla_fallback_dungeon_set + and x.parent_region.dungeon and x.parent_region.dungeon.name == item_to_place.dungeon] + return [] + + +def filter_locations(item_to_place, locations, world, vanilla_skip=False): + if world.algorithm == 'vanilla_fill': + config, filtered = world.item_pool_config, [] + item_name = 'Bottle' if item_to_place.name.startswith('Bottle') else item_to_place.name + if item_name in config.static_placement[item_to_place.player]: + restricted = config.static_placement[item_to_place.player][item_name] + filtered = [l for l in locations if l.player == item_to_place.player and l.name in restricted] + if vanilla_skip and len(filtered) == 0: + return filtered + i = 0 + while len(filtered) <= 0: + if i >= len(config.location_groups[item_to_place.player]): + return locations + restricted = config.location_groups[item_to_place.player][i].locations + filtered = [l for l in locations if l.player == item_to_place.player and l.name in restricted] + i += 1 + return filtered + if world.algorithm in ['major_only', 'dungeon_only']: + config = world.item_pool_config + if item_to_place.name in config.item_pool[item_to_place.player]: + restricted = config.location_groups[0].locations + filtered = [l for l in locations if l.name in restricted] + if len(filtered) == 0: + restricted = config.location_groups[1].locations + filtered = [l for l in locations if l.name in restricted] + # bias toward certain location in overflow? (thinking about this for major_bias) + return filtered if len(filtered) > 0 else locations + if (world.algorithm == 'entangled' and world.players > 1) or world.algorithm == 'district': + config = world.item_pool_config + if item_to_place == 'Placeholder' or item_to_place.name in config.item_pool[item_to_place.player]: + restricted = config.location_groups[0].locations + filtered = [l for l in locations if l.name in restricted and l.player in restricted[l.name]] + return filtered if len(filtered) > 0 else locations + return locations + + +vanilla_mapping = { + 'Green Pendant': ['Eastern Palace - Prize'], + 'Red Pendant': ['Desert Palace - Prize', 'Tower of Hera - Prize'], + 'Blue Pendant': ['Desert Palace - Prize', 'Tower of Hera - Prize'], + 'Crystal 1': ['Palace of Darkness - Prize', 'Swamp Palace - Prize', 'Thieves\' Town - Prize', + 'Skull Woods - Prize', 'Turtle Rock - Prize'], + 'Crystal 2': ['Palace of Darkness - Prize', 'Swamp Palace - Prize', 'Thieves\' Town - Prize', + 'Skull Woods - Prize', 'Turtle Rock - Prize'], + 'Crystal 3': ['Palace of Darkness - Prize', 'Swamp Palace - Prize', 'Thieves\' Town - Prize', + 'Skull Woods - Prize', 'Turtle Rock - Prize'], + 'Crystal 4': ['Palace of Darkness - Prize', 'Swamp Palace - Prize', 'Thieves\' Town - Prize', + 'Skull Woods - Prize', 'Turtle Rock - Prize'], + 'Crystal 7': ['Palace of Darkness - Prize', 'Swamp Palace - Prize', 'Thieves\' Town - Prize', + 'Skull Woods - Prize', 'Turtle Rock - Prize'], + 'Crystal 5': ['Ice Palace - Prize', 'Misery Mire - Prize'], + 'Crystal 6': ['Ice Palace - Prize', 'Misery Mire - Prize'], + 'Bow': ['Eastern Palace - Big Chest'], + 'Progressive Bow': ['Eastern Palace - Big Chest', 'Pyramid Fairy - Right'], + 'Book of Mudora': ['Library'], + 'Hammer': ['Palace of Darkness - Big Chest'], + 'Hookshot': ['Swamp Palace - Big Chest'], + 'Magic Mirror': ['Old Man'], + 'Ocarina': ['Flute Spot'], + 'Pegasus Boots': ['Sahasrahla'], + 'Power Glove': ['Desert Palace - Big Chest'], + 'Cape': ["King's Tomb"], + 'Mushroom': ['Mushroom'], + 'Shovel': ['Stumpy'], + 'Lamp': ["Link's House"], + 'Magic Powder': ['Potion Shop'], + 'Moon Pearl': ['Tower of Hera - Big Chest'], + 'Cane of Somaria': ['Misery Mire - Big Chest'], + 'Fire Rod': ['Skull Woods - Big Chest'], + 'Flippers': ['King Zora'], + 'Ice Rod': ['Ice Rod Cave'], + 'Titans Mitts': ["Thieves' Town - Big Chest"], + 'Bombos': ['Bombos Tablet'], + 'Ether': ['Ether Tablet'], + 'Quake': ['Catfish'], + 'Bottle': ['Bottle Merchant', 'Kakariko Tavern', 'Purple Chest', 'Hobo'], + 'Master Sword': ['Master Sword Pedestal'], + 'Tempered Sword': ['Blacksmith'], + 'Fighter Sword': ["Link's Uncle"], + 'Golden Sword': ['Pyramid Fairy - Left'], + 'Progressive Sword': ["Link's Uncle", 'Blacksmith', 'Master Sword Pedestal', 'Pyramid Fairy - Left'], + 'Progressive Glove': ['Desert Palace - Big Chest', "Thieves' Town - Big Chest"], + 'Silver Arrows': ['Pyramid Fairy - Right'], + 'Single Arrow': ['Palace of Darkness - Dark Basement - Left'], + 'Arrows (10)': ['Chicken House', 'Mini Moldorm Cave - Far Right', 'Sewers - Secret Room - Right', + 'Paradox Cave Upper - Right', 'Mire Shed - Right', 'Ganons Tower - Hope Room - Left', + 'Ganons Tower - Compass Room - Bottom Right', 'Ganons Tower - DMs Room - Top Right', + 'Ganons Tower - Randomizer Room - Top Left', 'Ganons Tower - Randomizer Room - Top Right', + "Ganons Tower - Bob's Chest", 'Ganons Tower - Big Key Room - Left'], + 'Bombs (3)': ['Floodgate Chest', "Sahasrahla's Hut - Middle", 'Kakariko Well - Bottom', 'Superbunny Cave - Top', + 'Mini Moldorm Cave - Far Left', 'Sewers - Secret Room - Left', 'Paradox Cave Upper - Left', + "Thieves' Town - Attic", 'Ice Palace - Freezor Chest', 'Palace of Darkness - Dark Maze - Top', + 'Ganons Tower - Hope Room - Right', 'Ganons Tower - DMs Room - Top Left', + 'Ganons Tower - Randomizer Room - Bottom Left', 'Ganons Tower - Randomizer Room - Bottom Right', + 'Ganons Tower - Big Key Room - Right', 'Ganons Tower - Mini Helmasaur Room - Left', + 'Ganons Tower - Mini Helmasaur Room - Right'], + 'Blue Mail': ['Ice Palace - Big Chest'], + 'Red Mail': ['Ganons Tower - Big Chest'], + 'Progressive Armor': ['Ice Palace - Big Chest', 'Ganons Tower - Big Chest'], + 'Blue Boomerang': ['Hyrule Castle - Boomerang Chest'], + 'Red Boomerang': ['Waterfall Fairy - Left'], + 'Blue Shield': ['Secret Passage'], + 'Red Shield': ['Waterfall Fairy - Right'], + 'Mirror Shield': ['Turtle Rock - Big Chest'], + 'Progressive Shield': ['Secret Passage', 'Waterfall Fairy - Right', 'Turtle Rock - Big Chest'], + 'Bug Catching Net': ['Sick Kid'], + 'Cane of Byrna': ['Spike Cave'], + 'Boss Heart Container': ['Desert Palace - Boss', 'Eastern Palace - Boss', 'Tower of Hera - Boss', + 'Swamp Palace - Boss', "Thieves' Town - Boss", 'Skull Woods - Boss', 'Ice Palace - Boss', + 'Misery Mire - Boss', 'Turtle Rock - Boss', 'Palace of Darkness - Boss'], + 'Sanctuary Heart Container': ['Sanctuary'], + 'Piece of Heart': ['Sunken Treasure', "Blind's Hideout - Top", "Zora's Ledge", "Aginah's Cave", 'Maze Race', + 'Kakariko Well - Top', 'Lost Woods Hideout', 'Lumberjack Tree', 'Cave 45', 'Graveyard Cave', + 'Checkerboard Cave', 'Bonk Rock Cave', 'Lake Hylia Island', 'Desert Ledge', 'Spectacle Rock', + 'Spectacle Rock Cave', 'Pyramid', 'Digging Game', 'Peg Cave', 'Chest Game', 'Bumper Cave Ledge', + 'Mire Shed - Left', 'Floating Island', 'Mimic Cave'], + 'Rupee (1)': ['Turtle Rock - Eye Bridge - Top Right', 'Ganons Tower - Compass Room - Top Right'], + 'Rupees (5)': ["Hyrule Castle - Zelda's Chest", 'Turtle Rock - Eye Bridge - Top Left', + # 'Palace of Darkness - Harmless Hellway', + 'Palace of Darkness - Dark Maze - Bottom', + 'Ganons Tower - Validation Chest'], + 'Rupees (20)': ["Blind's Hideout - Left", "Blind's Hideout - Right", "Blind's Hideout - Far Left", + "Blind's Hideout - Far Right", 'Kakariko Well - Left', 'Kakariko Well - Middle', + 'Kakariko Well - Right', 'Mini Moldorm Cave - Left', 'Mini Moldorm Cave - Right', + 'Paradox Cave Lower - Far Left', 'Paradox Cave Lower - Left', 'Paradox Cave Lower - Right', + 'Paradox Cave Lower - Far Right', 'Paradox Cave Lower - Middle', 'Hype Cave - Top', + 'Hype Cave - Middle Right', 'Hype Cave - Middle Left', 'Hype Cave - Bottom', + 'Swamp Palace - West Chest', 'Swamp Palace - Flooded Room - Left', 'Swamp Palace - Waterfall Room', + 'Swamp Palace - Flooded Room - Right', "Thieves' Town - Ambush Chest", + 'Turtle Rock - Eye Bridge - Bottom Right', 'Ganons Tower - Compass Room - Bottom Left', + 'Swamp Palace - Flooded Room - Right', "Thieves' Town - Ambush Chest", + 'Ganons Tower - DMs Room - Bottom Left', 'Ganons Tower - DMs Room - Bottom Right'], + 'Rupees (50)': ["Sahasrahla's Hut - Left", "Sahasrahla's Hut - Right", 'Spiral Cave', 'Superbunny Cave - Bottom', + 'Hookshot Cave - Top Right', 'Hookshot Cave - Top Left', 'Hookshot Cave - Bottom Right', + 'Hookshot Cave - Bottom Left'], + 'Rupees (100)': ['Eastern Palace - Cannonball Chest'], + 'Rupees (300)': ['Mini Moldorm Cave - Generous Guy', 'Sewers - Secret Room - Middle', 'Hype Cave - Generous Guy', + 'Brewery', 'C-Shaped House'], + 'Magic Upgrade (1/2)': ['Magic Bat'], + 'Big Key (Eastern Palace)': ['Eastern Palace - Big Key Chest'], + 'Compass (Eastern Palace)': ['Eastern Palace - Compass Chest'], + 'Map (Eastern Palace)': ['Eastern Palace - Map Chest'], + 'Small Key (Desert Palace)': ['Desert Palace - Torch'], + 'Big Key (Desert Palace)': ['Desert Palace - Big Key Chest'], + 'Compass (Desert Palace)': ['Desert Palace - Compass Chest'], + 'Map (Desert Palace)': ['Desert Palace - Map Chest'], + 'Small Key (Tower of Hera)': ['Tower of Hera - Basement Cage'], + 'Big Key (Tower of Hera)': ['Tower of Hera - Big Key Chest'], + 'Compass (Tower of Hera)': ['Tower of Hera - Compass Chest'], + 'Map (Tower of Hera)': ['Tower of Hera - Map Chest'], + 'Small Key (Escape)': ['Sewers - Dark Cross'], + 'Map (Escape)': ['Hyrule Castle - Map Chest'], + 'Small Key (Agahnims Tower)': ['Castle Tower - Room 03', 'Castle Tower - Dark Maze'], + 'Small Key (Palace of Darkness)': ['Palace of Darkness - Shooter Room', 'Palace of Darkness - The Arena - Bridge', + 'Palace of Darkness - Stalfos Basement', + 'Palace of Darkness - The Arena - Ledge', + 'Palace of Darkness - Dark Basement - Right', + 'Palace of Darkness - Harmless Hellway'], + # 'Palace of Darkness - Dark Maze - Bottom'], + 'Big Key (Palace of Darkness)': ['Palace of Darkness - Big Key Chest'], + 'Compass (Palace of Darkness)': ['Palace of Darkness - Compass Chest'], + 'Map (Palace of Darkness)': ['Palace of Darkness - Map Chest'], + 'Small Key (Thieves Town)': ["Thieves' Town - Blind's Cell"], + 'Big Key (Thieves Town)': ["Thieves' Town - Big Key Chest"], + 'Compass (Thieves Town)': ["Thieves' Town - Compass Chest"], + 'Map (Thieves Town)': ["Thieves' Town - Map Chest"], + 'Small Key (Skull Woods)': ['Skull Woods - Pot Prison', 'Skull Woods - Pinball Room', 'Skull Woods - Bridge Room'], + 'Big Key (Skull Woods)': ['Skull Woods - Big Key Chest'], + 'Compass (Skull Woods)': ['Skull Woods - Compass Chest'], + 'Map (Skull Woods)': ['Skull Woods - Map Chest'], + 'Small Key (Swamp Palace)': ['Swamp Palace - Entrance'], + 'Big Key (Swamp Palace)': ['Swamp Palace - Big Key Chest'], + 'Compass (Swamp Palace)': ['Swamp Palace - Compass Chest'], + 'Map (Swamp Palace)': ['Swamp Palace - Map Chest'], + 'Small Key (Ice Palace)': ['Ice Palace - Iced T Room', 'Ice Palace - Spike Room'], + 'Big Key (Ice Palace)': ['Ice Palace - Big Key Chest'], + 'Compass (Ice Palace)': ['Ice Palace - Compass Chest'], + 'Map (Ice Palace)': ['Ice Palace - Map Chest'], + 'Small Key (Misery Mire)': ['Misery Mire - Main Lobby', 'Misery Mire - Bridge Chest', 'Misery Mire - Spike Chest'], + 'Big Key (Misery Mire)': ['Misery Mire - Big Key Chest'], + 'Compass (Misery Mire)': ['Misery Mire - Compass Chest'], + 'Map (Misery Mire)': ['Misery Mire - Map Chest'], + 'Small Key (Turtle Rock)': ['Turtle Rock - Roller Room - Right', 'Turtle Rock - Chain Chomps', + 'Turtle Rock - Crystaroller Room', 'Turtle Rock - Eye Bridge - Bottom Left'], + 'Big Key (Turtle Rock)': ['Turtle Rock - Big Key Chest'], + 'Compass (Turtle Rock)': ['Turtle Rock - Compass Chest'], + 'Map (Turtle Rock)': ['Turtle Rock - Roller Room - Left'], + 'Small Key (Ganons Tower)': ["Ganons Tower - Bob's Torch", 'Ganons Tower - Tile Room', + 'Ganons Tower - Firesnake Room', 'Ganons Tower - Pre-Moldorm Chest'], + 'Big Key (Ganons Tower)': ['Ganons Tower - Big Key Chest'], + 'Compass (Ganons Tower)': ['Ganons Tower - Compass Room - Top Left'], + 'Map (Ganons Tower)': ['Ganons Tower - Map Chest'] +} + + +keydrop_vanilla_mapping = { + 'Small Key (Desert Palace)': ['Desert Palace - Desert Tiles 1 Pot Key', + 'Desert Palace - Beamos Hall Pot Key', 'Desert Palace - Desert Tiles 2 Pot Key'], + 'Small Key (Eastern Palace)': ['Eastern Palace - Dark Square Pot Key', 'Eastern Palace - Dark Eyegore Key Drop'], + 'Small Key (Escape)': ['Hyrule Castle - Map Guard Key Drop', 'Hyrule Castle - Boomerang Guard Key Drop', + 'Hyrule Castle - Key Rat Key Drop'], + 'Big Key (Escape)': ['Hyrule Castle - Big Key Drop'], + 'Small Key (Agahnims Tower)': ['Castle Tower - Dark Archer Key Drop', 'Castle Tower - Circle of Pots Key Drop'], + 'Small Key (Thieves Town)': ["Thieves' Town - Hallway Pot Key", "Thieves' Town - Spike Switch Pot Key"], + 'Small Key (Skull Woods)': ['Skull Woods - West Lobby Pot Key', 'Skull Woods - Spike Corner Key Drop'], + 'Small Key (Swamp Palace)': ['Swamp Palace - Pot Row Pot Key', 'Swamp Palace - Trench 1 Pot Key', + 'Swamp Palace - Hookshot Pot Key', 'Swamp Palace - Trench 2 Pot Key', + 'Swamp Palace - Waterway Pot Key'], + 'Small Key (Ice Palace)': ['Ice Palace - Jelly Key Drop', 'Ice Palace - Conveyor Key Drop', + 'Ice Palace - Hammer Block Key Drop', 'Ice Palace - Many Pots Pot Key'], + 'Small Key (Misery Mire)': ['Misery Mire - Spikes Pot Key', + 'Misery Mire - Fishbone Pot Key', 'Misery Mire - Conveyor Crystal Key Drop'], + 'Small Key (Turtle Rock)': ['Turtle Rock - Pokey 1 Key Drop', 'Turtle Rock - Pokey 2 Key Drop'], + 'Small Key (Ganons Tower)': ['Ganons Tower - Conveyor Cross Pot Key', 'Ganons Tower - Double Switch Pot Key', + 'Ganons Tower - Conveyor Star Pits Pot Key', 'Ganons Tower - Mini Helmasuar Key Drop'], +} + +mode_grouping = { + 'Overworld Major': [ + "Link's Uncle", 'King Zora', "Link's House", 'Sahasrahla', 'Ice Rod Cave', 'Library', + 'Master Sword Pedestal', 'Old Man', 'Ether Tablet', 'Catfish', 'Stumpy', 'Bombos Tablet', 'Mushroom', + 'Bottle Merchant', 'Kakariko Tavern', 'Secret Passage', 'Flute Spot', 'Purple Chest', + 'Waterfall Fairy - Left', 'Waterfall Fairy - Right', 'Blacksmith', 'Magic Bat', 'Sick Kid', 'Hobo', + 'Potion Shop', 'Spike Cave', 'Pyramid Fairy - Left', 'Pyramid Fairy - Right', "King's Tomb", + ], + 'Big Chests': ['Eastern Palace - Big Chest','Desert Palace - Big Chest', 'Tower of Hera - Big Chest', + 'Palace of Darkness - Big Chest', 'Swamp Palace - Big Chest', 'Skull Woods - Big Chest', + "Thieves' Town - Big Chest", 'Misery Mire - Big Chest', 'Hyrule Castle - Boomerang Chest', + 'Ice Palace - Big Chest', 'Turtle Rock - Big Chest', 'Ganons Tower - Big Chest'], + 'Heart Containers': ['Sanctuary', 'Eastern Palace - Boss','Desert Palace - Boss', 'Tower of Hera - Boss', + 'Palace of Darkness - Boss', 'Swamp Palace - Boss', 'Skull Woods - Boss', + "Thieves' Town - Boss", 'Ice Palace - Boss', 'Misery Mire - Boss', 'Turtle Rock - Boss'], + 'Heart Pieces': [ + 'Bumper Cave Ledge', 'Desert Ledge', 'Lake Hylia Island', 'Floating Island', + 'Maze Race', 'Spectacle Rock', 'Pyramid', "Zora's Ledge", 'Lumberjack Tree', + 'Sunken Treasure', 'Spectacle Rock Cave', 'Lost Woods Hideout', 'Checkerboard Cave', 'Peg Cave', 'Cave 45', + 'Graveyard Cave', 'Kakariko Well - Top', "Blind's Hideout - Top", 'Bonk Rock Cave', "Aginah's Cave", + 'Chest Game', 'Digging Game', 'Mire Shed - Right', 'Mimic Cave' + ], + 'Big Keys': [ + 'Eastern Palace - Big Key Chest', 'Ganons Tower - Big Key Chest', + 'Desert Palace - Big Key Chest', 'Tower of Hera - Big Key Chest', 'Palace of Darkness - Big Key Chest', + 'Swamp Palace - Big Key Chest', "Thieves' Town - Big Key Chest", 'Skull Woods - Big Key Chest', + 'Ice Palace - Big Key Chest', 'Misery Mire - Big Key Chest', 'Turtle Rock - Big Key Chest', + ], + 'Compasses': [ + 'Eastern Palace - Compass Chest', 'Desert Palace - Compass Chest', 'Tower of Hera - Compass Chest', + 'Palace of Darkness - Compass Chest', 'Swamp Palace - Compass Chest', 'Skull Woods - Compass Chest', + "Thieves' Town - Compass Chest", 'Ice Palace - Compass Chest', 'Misery Mire - Compass Chest', + 'Turtle Rock - Compass Chest', 'Ganons Tower - Compass Room - Top Left' + ], + 'Maps': [ + 'Hyrule Castle - Map Chest', 'Eastern Palace - Map Chest', 'Desert Palace - Map Chest', + 'Tower of Hera - Map Chest', 'Palace of Darkness - Map Chest', 'Swamp Palace - Map Chest', + 'Skull Woods - Map Chest', "Thieves' Town - Map Chest", 'Ice Palace - Map Chest', 'Misery Mire - Map Chest', + 'Turtle Rock - Roller Room - Left', 'Ganons Tower - Map Chest' + ], + 'Small Keys': [ + 'Sewers - Dark Cross', 'Desert Palace - Torch', 'Tower of Hera - Basement Cage', + 'Castle Tower - Room 03', 'Castle Tower - Dark Maze', + 'Palace of Darkness - Stalfos Basement', 'Palace of Darkness - Dark Basement - Right', + 'Palace of Darkness - Dark Maze - Bottom', 'Palace of Darkness - Shooter Room', + 'Palace of Darkness - The Arena - Bridge', 'Palace of Darkness - The Arena - Ledge', + "Thieves' Town - Blind's Cell", 'Skull Woods - Bridge Room', 'Ice Palace - Spike Room', + 'Skull Woods - Pot Prison', 'Skull Woods - Pinball Room', 'Misery Mire - Spike Chest', + 'Ice Palace - Iced T Room', 'Misery Mire - Main Lobby', 'Misery Mire - Bridge Chest', 'Swamp Palace - Entrance', + 'Turtle Rock - Chain Chomps', 'Turtle Rock - Crystaroller Room', 'Turtle Rock - Roller Room - Right', + 'Turtle Rock - Eye Bridge - Bottom Left', "Ganons Tower - Bob's Torch", 'Ganons Tower - Tile Room', + 'Ganons Tower - Firesnake Room', 'Ganons Tower - Pre-Moldorm Chest' + ], + 'Dungeon Trash': [ + 'Sewers - Secret Room - Right', 'Sewers - Secret Room - Left', 'Sewers - Secret Room - Middle', + "Hyrule Castle - Zelda's Chest", 'Eastern Palace - Cannonball Chest', "Thieves' Town - Ambush Chest", + "Thieves' Town - Attic", 'Ice Palace - Freezor Chest', 'Palace of Darkness - Dark Basement - Left', + 'Palace of Darkness - Dark Maze - Bottom', 'Palace of Darkness - Dark Maze - Top', + 'Swamp Palace - Flooded Room - Left', 'Swamp Palace - Flooded Room - Right', 'Swamp Palace - Waterfall Room', + 'Turtle Rock - Eye Bridge - Bottom Right', 'Turtle Rock - Eye Bridge - Top Left', + 'Turtle Rock - Eye Bridge - Top Right', 'Swamp Palace - West Chest', + ], + 'Overworld Trash': [ + "Blind's Hideout - Left", "Blind's Hideout - Right", "Blind's Hideout - Far Left", + "Blind's Hideout - Far Right", 'Kakariko Well - Left', 'Kakariko Well - Middle', 'Kakariko Well - Right', + 'Kakariko Well - Bottom', 'Chicken House', 'Floodgate Chest', 'Mini Moldorm Cave - Left', + 'Mini Moldorm Cave - Right', 'Mini Moldorm Cave - Generous Guy', 'Mini Moldorm Cave - Far Left', + 'Mini Moldorm Cave - Far Right', "Sahasrahla's Hut - Left", "Sahasrahla's Hut - Right", + "Sahasrahla's Hut - Middle", 'Paradox Cave Lower - Far Left', 'Paradox Cave Lower - Left', + 'Paradox Cave Lower - Right', 'Paradox Cave Lower - Far Right', 'Paradox Cave Lower - Middle', + 'Paradox Cave Upper - Left', 'Paradox Cave Upper - Right', 'Spiral Cave', 'Brewery', 'C-Shaped House', + 'Hype Cave - Top', 'Hype Cave - Middle Right', 'Hype Cave - Middle Left', 'Hype Cave - Bottom', + 'Hype Cave - Generous Guy', 'Superbunny Cave - Bottom', 'Superbunny Cave - Top', 'Hookshot Cave - Top Right', + 'Hookshot Cave - Top Left', 'Hookshot Cave - Bottom Right', 'Hookshot Cave - Bottom Left', 'Mire Shed - Left' + ], + 'GT Trash': [ + 'Ganons Tower - DMs Room - Top Right', 'Ganons Tower - DMs Room - Top Left', + 'Ganons Tower - DMs Room - Bottom Left', 'Ganons Tower - DMs Room - Bottom Right', + 'Ganons Tower - Compass Room - Top Right', 'Ganons Tower - Compass Room - Bottom Right', + 'Ganons Tower - Compass Room - Bottom Left', 'Ganons Tower - Hope Room - Left', + 'Ganons Tower - Hope Room - Right', 'Ganons Tower - Randomizer Room - Top Left', + 'Ganons Tower - Randomizer Room - Top Right', 'Ganons Tower - Randomizer Room - Bottom Right', + 'Ganons Tower - Randomizer Room - Bottom Left', "Ganons Tower - Bob's Chest", + 'Ganons Tower - Big Key Room - Left', 'Ganons Tower - Big Key Room - Right', + 'Ganons Tower - Mini Helmasaur Room - Left', 'Ganons Tower - Mini Helmasaur Room - Right', + 'Ganons Tower - Validation Chest', + ], + 'Key Drops': [ + 'Hyrule Castle - Map Guard Key Drop', 'Hyrule Castle - Boomerang Guard Key Drop', + 'Hyrule Castle - Key Rat Key Drop', 'Eastern Palace - Dark Square Pot Key', + 'Eastern Palace - Dark Eyegore Key Drop', 'Desert Palace - Desert Tiles 1 Pot Key', + 'Desert Palace - Beamos Hall Pot Key', 'Desert Palace - Desert Tiles 2 Pot Key', + 'Castle Tower - Dark Archer Key Drop', 'Castle Tower - Circle of Pots Key Drop', + 'Swamp Palace - Pot Row Pot Key', 'Swamp Palace - Trench 1 Pot Key', 'Swamp Palace - Hookshot Pot Key', + 'Swamp Palace - Trench 2 Pot Key', 'Swamp Palace - Waterway Pot Key', 'Skull Woods - West Lobby Pot Key', + 'Skull Woods - Spike Corner Key Drop', "Thieves' Town - Hallway Pot Key", + "Thieves' Town - Spike Switch Pot Key", 'Ice Palace - Jelly Key Drop', 'Ice Palace - Conveyor Key Drop', + 'Ice Palace - Hammer Block Key Drop', 'Ice Palace - Many Pots Pot Key', 'Misery Mire - Spikes Pot Key', + 'Misery Mire - Fishbone Pot Key', 'Misery Mire - Conveyor Crystal Key Drop', 'Turtle Rock - Pokey 1 Key Drop', + 'Turtle Rock - Pokey 2 Key Drop', 'Ganons Tower - Conveyor Cross Pot Key', + 'Ganons Tower - Double Switch Pot Key', 'Ganons Tower - Conveyor Star Pits Pot Key', + 'Ganons Tower - Mini Helmasuar Key Drop', + ], + 'Big Key Drops': ['Hyrule Castle - Big Key Drop'], + 'Shops': [ + 'Dark Death Mountain Shop - Left', 'Dark Death Mountain Shop - Middle', 'Dark Death Mountain Shop - Right', + 'Red Shield Shop - Left', 'Red Shield Shop - Middle', 'Red Shield Shop - Right', 'Dark Lake Hylia Shop - Left', + 'Dark Lake Hylia Shop - Middle', 'Dark Lake Hylia Shop - Right', 'Dark Lumberjack Shop - Left', + 'Dark Lumberjack Shop - Middle', 'Dark Lumberjack Shop - Right', 'Village of Outcasts Shop - Left', + 'Village of Outcasts Shop - Middle', 'Village of Outcasts Shop - Right', 'Dark Potion Shop - Left', + 'Dark Potion Shop - Middle', 'Dark Potion Shop - Right', 'Paradox Shop - Left', 'Paradox Shop - Middle', + 'Paradox Shop - Right', 'Kakariko Shop - Left', 'Kakariko Shop - Middle', 'Kakariko Shop - Right', + 'Lake Hylia Shop - Left', 'Lake Hylia Shop - Middle', 'Lake Hylia Shop - Right', 'Capacity Upgrade - Left', + 'Capacity Upgrade - Right' + ], + 'RetroShops': [ + 'Old Man Sword Cave Item 1', 'Take-Any #1 Item 1', 'Take-Any #1 Item 2', 'Take-Any #2 Item 1', + 'Take-Any #2 Item 2', 'Take-Any #3 Item 1', 'Take-Any #3 Item 2','Take-Any #4 Item 1', 'Take-Any #4 Item 2' + ] +} + +vanilla_fallback_dungeon_set = set(mode_grouping['Dungeon Trash'] + mode_grouping['Big Keys'] + + mode_grouping['GT Trash'] + mode_grouping['Small Keys'] + + mode_grouping['Compasses'] + mode_grouping['Maps'] + mode_grouping['Key Drops'] + + mode_grouping['Big Key Drops']) + + +major_items = {'Bombos', 'Book of Mudora', 'Cane of Somaria', 'Ether', 'Fire Rod', 'Flippers', 'Ocarina', 'Hammer', + 'Hookshot', 'Ice Rod', 'Lamp', 'Cape', 'Magic Powder', 'Mushroom', 'Pegasus Boots', 'Quake', 'Shovel', + 'Bug Catching Net', 'Cane of Byrna', 'Blue Boomerang', 'Red Boomerang', 'Progressive Glove', + 'Power Glove', 'Titans Mitts', 'Bottle', 'Bottle (Red Potion)', 'Bottle (Green Potion)', 'Magic Mirror', + 'Bottle (Blue Potion)', 'Bottle (Fairy)', 'Bottle (Bee)', 'Bottle (Good Bee)', 'Magic Upgrade (1/2)', + 'Sanctuary Heart Container', 'Boss Heart Container', 'Progressive Shield', + 'Mirror Shield', 'Progressive Armor', 'Blue Mail', 'Red Mail', 'Progressive Sword', 'Fighter Sword', + 'Master Sword', 'Tempered Sword', 'Golden Sword', 'Bow', 'Silver Arrows', 'Triforce Piece', 'Moon Pearl', + 'Progressive Bow', 'Progressive Bow (Alt)'} + + +clustered_groups = [ + LocationGroup("MajorRoute1").locs([ + 'Ice Rod Cave', 'Library', 'Old Man', 'Magic Bat', 'Ether Tablet', 'Hobo', 'Purple Chest', 'Spike Cave', + 'Sahasrahla', 'Superbunny Cave - Bottom', 'Superbunny Cave - Top', + 'Ganons Tower - Randomizer Room - Bottom Left', 'Ganons Tower - Randomizer Room - Bottom Right' + ]), + LocationGroup("MajorRoute2").locs([ + 'Mushroom', 'Secret Passage', 'Bottle Merchant', 'Flute Spot', 'Catfish', 'Stumpy', 'Waterfall Fairy - Left', + 'Waterfall Fairy - Right', 'Master Sword Pedestal', "Thieves' Town - Attic", 'Sewers - Secret Room - Right', + 'Sewers - Secret Room - Left', 'Sewers - Secret Room - Middle' + ]), + LocationGroup("MajorRoute3").locs([ + 'Kakariko Tavern', 'Sick Kid', 'King Zora', 'Potion Shop', 'Bombos Tablet', "King's Tomb", 'Blacksmith', + 'Pyramid Fairy - Left', 'Pyramid Fairy - Right', 'Hookshot Cave - Top Right', 'Hookshot Cave - Top Left', + 'Hookshot Cave - Bottom Right', 'Hookshot Cave - Bottom Left' + ]), + LocationGroup("Dungeon Major").locs([ + 'Eastern Palace - Big Chest', 'Desert Palace - Big Chest', 'Tower of Hera - Big Chest', + 'Palace of Darkness - Big Chest', 'Swamp Palace - Big Chest', 'Skull Woods - Big Chest', + "Thieves' Town - Big Chest", 'Misery Mire - Big Chest', 'Hyrule Castle - Boomerang Chest', + 'Ice Palace - Big Chest', 'Turtle Rock - Big Chest', 'Ganons Tower - Big Chest', "Link's Uncle"]), + LocationGroup("Dungeon Heart").locs([ + 'Sanctuary', 'Eastern Palace - Boss', 'Desert Palace - Boss', 'Tower of Hera - Boss', + 'Palace of Darkness - Boss', 'Swamp Palace - Boss', 'Skull Woods - Boss', "Thieves' Town - Boss", + 'Ice Palace - Boss', 'Misery Mire - Boss', 'Turtle Rock - Boss', "Link's House", + 'Ganons Tower - Validation Chest']), + LocationGroup("HeartPieces1").locs([ + 'Kakariko Well - Top', 'Lost Woods Hideout', 'Maze Race', 'Lumberjack Tree', 'Bonk Rock Cave', 'Graveyard Cave', + 'Checkerboard Cave', "Zora's Ledge", 'Digging Game', 'Desert Ledge', 'Bumper Cave Ledge', 'Floating Island', + 'Swamp Palace - Waterfall Room']), + LocationGroup("HeartPieces2").locs([ + "Blind's Hideout - Top", 'Sunken Treasure', "Aginah's Cave", 'Mimic Cave', 'Spectacle Rock Cave', 'Cave 45', + 'Spectacle Rock', 'Lake Hylia Island', 'Chest Game', 'Mire Shed - Right', 'Pyramid', 'Peg Cave', + 'Eastern Palace - Cannonball Chest']), + LocationGroup("BlindHope").locs([ + "Blind's Hideout - Left", "Blind's Hideout - Right", "Blind's Hideout - Far Left", + "Blind's Hideout - Far Right", 'Floodgate Chest', 'Spiral Cave', 'Palace of Darkness - Dark Maze - Bottom', + 'Palace of Darkness - Dark Maze - Top', 'Swamp Palace - Flooded Room - Left', + 'Swamp Palace - Flooded Room - Right', "Thieves' Town - Ambush Chest", 'Ganons Tower - Hope Room - Left', + 'Ganons Tower - Hope Room - Right']), + LocationGroup('WellHype').locs([ + 'Kakariko Well - Left', 'Kakariko Well - Middle', 'Kakariko Well - Right', 'Kakariko Well - Bottom', + 'Paradox Cave Upper - Left', 'Paradox Cave Upper - Right', 'Hype Cave - Top', 'Hype Cave - Middle Right', + 'Hype Cave - Middle Left', 'Hype Cave - Bottom', 'Hype Cave - Generous Guy', + 'Ganons Tower - DMs Room - Bottom Left', 'Ganons Tower - DMs Room - Bottom Right', + ]), + LocationGroup('MiniMoldormLasers').locs([ + 'Mini Moldorm Cave - Left', 'Mini Moldorm Cave - Right', 'Mini Moldorm Cave - Generous Guy', + 'Mini Moldorm Cave - Far Left', 'Mini Moldorm Cave - Far Right', 'Chicken House', 'Brewery', + 'Palace of Darkness - Dark Basement - Left', 'Ice Palace - Freezor Chest', 'Swamp Palace - West Chest', + 'Turtle Rock - Eye Bridge - Bottom Right', 'Turtle Rock - Eye Bridge - Top Left', + 'Turtle Rock - Eye Bridge - Top Right', + ]), + LocationGroup('ParadoxCloset').locs([ + "Sahasrahla's Hut - Left", "Sahasrahla's Hut - Right", "Sahasrahla's Hut - Middle", + 'Paradox Cave Lower - Far Left', 'Paradox Cave Lower - Left', 'Paradox Cave Lower - Right', + 'Paradox Cave Lower - Far Right', 'Paradox Cave Lower - Middle', "Hyrule Castle - Zelda's Chest", + 'C-Shaped House', 'Mire Shed - Left', 'Ganons Tower - Compass Room - Bottom Right', + 'Ganons Tower - Compass Room - Bottom Left', + ]) +] + +other_clusters = { + 'SmallKey1': [ + 'Sewers - Dark Cross', 'Tower of Hera - Basement Cage', 'Palace of Darkness - Shooter Room', + 'Palace of Darkness - The Arena - Bridge', 'Palace of Darkness - Stalfos Basement', + 'Palace of Darkness - Dark Basement - Right', "Thieves' Town - Blind's Cell", 'Skull Woods - Bridge Room', + 'Ice Palace - Iced T Room', 'Misery Mire - Main Lobby', 'Misery Mire - Bridge Chest', + 'Misery Mire - Spike Chest', "Ganons Tower - Bob's Torch"], + 'SmallKey2': [ + 'Desert Palace - Torch', 'Castle Tower - Room 03', 'Castle Tower - Dark Maze', + 'Palace of Darkness - The Arena - Ledge', 'Palace of Darkness - Harmless Hellway', 'Swamp Palace - Entrance', + 'Skull Woods - Pot Prison', 'Skull Woods - Pinball Room', 'Ice Palace - Spike Room', + 'Turtle Rock - Roller Room - Right', 'Turtle Rock - Chain Chomps', 'Turtle Rock - Crystaroller Room', + 'Turtle Rock - Eye Bridge - Bottom Left'], + 'SmallKeyLeft': [ + 'Ganons Tower - Tile Room', 'Ganons Tower - Firesnake Room', 'Ganons Tower - Pre-Moldorm Chest'], + 'KeyDrop1': [ + 'Hyrule Castle - Map Guard Key Drop', 'Hyrule Castle - Boomerang Guard Key Drop', + 'Hyrule Castle - Key Rat Key Drop', 'Swamp Palace - Hookshot Pot Key', 'Swamp Palace - Trench 2 Pot Key', + 'Swamp Palace - Waterway Pot Key', 'Skull Woods - West Lobby Pot Key', 'Skull Woods - Spike Corner Key Drop', + 'Ice Palace - Jelly Key Drop', 'Ice Palace - Conveyor Key Drop', 'Misery Mire - Spikes Pot Key', + 'Misery Mire - Fishbone Pot Key', 'Misery Mire - Conveyor Crystal Key Drop'], + 'KeyDrop2': [ + 'Eastern Palace - Dark Square Pot Key', 'Eastern Palace - Dark Eyegore Key Drop', + 'Desert Palace - Desert Tiles 1 Pot Key', 'Desert Palace - Beamos Hall Pot Key', + 'Desert Palace - Desert Tiles 2 Pot Key', 'Swamp Palace - Pot Row Pot Key', 'Swamp Palace - Trench 1 Pot Key', + "Thieves' Town - Hallway Pot Key", "Thieves' Town - Spike Switch Pot Key", 'Ice Palace - Hammer Block Key Drop', + 'Ice Palace - Many Pots Pot Key', 'Turtle Rock - Pokey 1 Key Drop', 'Turtle Rock - Pokey 2 Key Drop'], + 'KeyDropLeft': [ + 'Castle Tower - Dark Archer Key Drop', 'Castle Tower - Circle of Pots Key Drop', + 'Ganons Tower - Conveyor Cross Pot Key', 'Ganons Tower - Double Switch Pot Key', + 'Ganons Tower - Conveyor Star Pits Pot Key', 'Ganons Tower - Mini Helmasuar Key Drop'], + 'Shopsanity1': [ + 'Dark Lumberjack Shop - Left', 'Dark Lumberjack Shop - Middle', 'Dark Lumberjack Shop - Right', + 'Dark Lake Hylia Shop - Left', 'Dark Lake Hylia Shop - Middle', 'Dark Lake Hylia Shop - Right', + 'Paradox Shop - Left', 'Paradox Shop - Middle', 'Paradox Shop - Right', + 'Kakariko Shop - Left', 'Kakariko Shop - Middle', 'Kakariko Shop - Right', 'Capacity Upgrade - Left'], + 'Shopsanity2': [ + 'Red Shield Shop - Left', 'Red Shield Shop - Middle', 'Red Shield Shop - Right', + 'Village of Outcasts Shop - Left', 'Village of Outcasts Shop - Middle', 'Village of Outcasts Shop - Right', + 'Dark Potion Shop - Left', 'Dark Potion Shop - Middle', 'Dark Potion Shop - Right', + 'Lake Hylia Shop - Left', 'Lake Hylia Shop - Middle', 'Lake Hylia Shop - Right', 'Capacity Upgrade - Right', + ], + 'ShopsanityLeft': ['Potion Shop - Left', 'Potion Shop - Middle', 'Potion Shop - Right'] +} + +leftovers = [ + 'Ganons Tower - DMs Room - Top Right', 'Ganons Tower - DMs Room - Top Left', + 'Ganons Tower - Compass Room - Top Right', 'Ganons Tower - Randomizer Room - Top Left', + 'Ganons Tower - Randomizer Room - Top Right',"Ganons Tower - Bob's Chest", 'Ganons Tower - Big Key Room - Left', + 'Ganons Tower - Big Key Room - Right', 'Ganons Tower - Mini Helmasaur Room - Left', + 'Ganons Tower - Mini Helmasaur Room - Right', +] + +vanilla_swords = {"Link's Uncle", 'Master Sword Pedestal', 'Blacksmith', 'Pyramid Fairy - Left'} + +trash_items = { + 'Nothing': -1, + 'Bee Trap': 0, + 'Rupee (1)': 1, + 'Rupees (5)': 1, + 'Rupees (20)': 1, + + 'Small Heart': 2, + 'Bee': 2, + + 'Bombs (3)': 3, + 'Arrows (10)': 3, + 'Bombs (10)': 3, + + 'Red Potion': 4, + 'Blue Shield': 4, + 'Rupees (50)': 4, + 'Rupees (100)': 4, + 'Rupees (300)': 5, + + 'Piece of Heart': 17 +}