From 746a73933980057af6c15de7a6b860eaed958874 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 26 Aug 2021 15:27:05 -0600 Subject: [PATCH] Main structure for various biased fills Lots of help around correctly reserving locations --- BaseClasses.py | 13 + Bosses.py | 4 +- DoorShuffle.py | 33 +- DungeonGenerator.py | 42 +- Dungeons.py | 114 --- Fill.py | 403 ++++---- ItemList.py | 13 +- KeyDoorShuffle.py | 2 + Main.py | 64 +- Mystery.py | 11 +- Regions.py | 8 + resources/app/cli/args.json | 12 +- resources/app/cli/lang/en.json | 40 +- resources/app/gui/lang/en.json | 11 +- resources/app/gui/randomize/item/widgets.json | 13 +- source/item/BiasedFill.py | 881 ++++++++++++++++++ source/item/FillUtil.py | 20 - 17 files changed, 1239 insertions(+), 445 deletions(-) create mode 100644 source/item/BiasedFill.py delete mode 100644 source/item/FillUtil.py diff --git a/BaseClasses.py b/BaseClasses.py index 67f2f93d..702f5436 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, @@ -213,6 +214,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 @@ -1452,6 +1458,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 @@ -1842,6 +1852,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: @@ -2084,6 +2096,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 2718431e..5c742015 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 ca377c65..dc0f18f5 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -46,8 +46,7 @@ def link_doors(world, player): world.swamp_patch_required[player] = orig_swamp_patch -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 +98,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 +119,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 +217,33 @@ 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) + # if not hasattr(world, 'builder_cache'): + # world.builder_cache = {} + # if (dungeon.name, player) not in world.builder_cache: + # sector = Sector() + # sector.name = dungeon.name + # sector.regions.extend(convert_regions(dungeon.regions, world, player)) + # builder = simple_dungeon_builder(sector.name, [sector]) + # builder.master_sector = sector + # + # origin_list = find_accessible_entrances(world, player, builder) + # start_regions = convert_regions(origin_list, world, player) + # doors = convert_key_doors(default_small_key_doors[builder.name], world, player) + # key_layout = build_key_layout(builder, start_regions, doors, world, player) + # world.builder_cache[(dungeon.name, player)] = key_layout + # else: + # key_layout = world.builder_cache[(dungeon.name, player)] + # return validate_key_layout(key_layout, world, player) + + # some useful functions oppositemap = { Direction.South: Direction.North, diff --git a/DungeonGenerator.py b/DungeonGenerator.py index a2c3df04..1b4b3564 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -1198,6 +1198,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 @@ -1359,7 +1361,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: @@ -1510,6 +1512,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 @@ -1590,19 +1593,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): @@ -1633,6 +1643,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 2edba0d4..ce57e7f8 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 67476fbf..13de2a20 100644 --- a/Fill.py +++ b/Fill.py @@ -3,176 +3,74 @@ 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, split_pool -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(dungeon.all_items) + 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, + reserved_items=None): + if not reserved_items: + reserved_items = [] + def sweep_from_pool(): new_state = base_state.copy() - for item in itempool: + for item in itempool + reserved_items: new_state.collect(item, True) new_state.sweep_for_events() return new_state @@ -201,41 +99,56 @@ 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) + 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) + logging.getLogger('').warning('Not all items placed. Game beatable anyway.' + f' (Could not place {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) + if world.algorithm in ['balanced', 'equitable']: + spot_to_fill = last_ditch_placement(item_to_place, locations, world, maximum_exploration_state, + base_state, itempool, keys_in_itempool, + single_player_placement) if spot_to_fill is None: 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 @@ -259,6 +172,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 @@ -270,6 +194,22 @@ 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 last_ditch_placement(item_to_place, locations, world, state, base_state, itempool, keys_in_itempool=None, single_player_placement=False): def location_preference(loc): @@ -351,6 +291,7 @@ 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] @@ -379,21 +320,53 @@ 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)} + if world.algorithm in ['balanced', 'equitable', 'vanilla_bias', 'dungeon_bias', 'entangled']: + 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_bias': + fast_vanilla_fill(world, prioitempool, fill_locations) + elif world.algorithm in ['dungeon_bias', 'entangled']: + 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 == 'entangled' and world.players > 1: + random.shuffle(fill_locations) + placeholder_locations = filter_locations('Placeholder', fill_locations, world) + placeholder_items = [item for item in world.itempool if item.name == 'Rupee (1)'] + for i in placeholder_items: + restitempool.remove(i) + for l in placeholder_locations: + fill_locations.remove(l) + filtered_fill(world, placeholder_items, placeholder_locations) + else: + primary, secondary = split_pool(progitempool, world) + fill_restrictive(world, world.state, fill_locations, primary, keys_in_pool, False, secondary) + random.shuffle(fill_locations) + tertiary, quaternary = split_pool(prioitempool, world) + prioitempool = [] + filtered_equitable_fill(world, tertiary, fill_locations) + prioitempool += tertiary + random.shuffle(fill_locations) + fill_restrictive(world, world.state, fill_locations, secondary, keys_in_pool) + random.shuffle(fill_locations) + fast_equitable_fill(world, quaternary, fill_locations) + prioitempool += quaternary - fill_restrictive(world, world.state, fill_locations, progitempool, - keys_in_itempool={player: world.keyshuffle[player] for player in range(1, world.players + 1)}) - - random.shuffle(fill_locations) - - fast_fill(world, prioitempool, fill_locations) - - fast_fill(world, restitempool, fill_locations) + if world.algorithm == 'vanilla_bias': + 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() @@ -401,70 +374,48 @@ 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 +def fast_vanilla_fill(world, item_pool, fill_locations): + while item_pool and fill_locations: + item_to_place = 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) - if spot_to_fill: - item = itempool.pop(0) - world.push_item(spot_to_fill, item, True) - continue - # ran out of spots, check if we need to step in and correct things - if len(world.get_reachable_locations()) == len(world.get_locations()): - progress_done = True - continue +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) - # need to place a progress item instead of an already placed item, find candidate - item_to_place = None - candidate_item_to_place = None - for item in itempool: - if item.advancement: - candidate_item_to_place = item - if world.unlocks_new_location(item): - item_to_place = item - break - # we might be in a situation where all new locations require multiple items to reach. If that is the case, just place any advancement item we've found and continue trying - if item_to_place is None: - if candidate_item_to_place is not None: - item_to_place = candidate_item_to_place - else: - raise FillError('No more progress items left to place.') - - # find item to replace with progress item - location_list = world.get_reachable_locations() - 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).event = True world.get_location(loc, player).locked = True # I don't believe these locations exist in non-shopsanity # if world.retro[player]: diff --git a/ItemList.py b/ItemList.py index 5f5b5e7e..5721aadc 100644 --- a/ItemList.py +++ b/ItemList.py @@ -5,12 +5,13 @@ import RaceRandom as random from BaseClasses import Region, RegionType, Shop, ShopType, Location, CollectionState from Bosses import place_bosses -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 @@ -262,8 +263,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 5c65dbe6..67654c8c 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -177,6 +177,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 f6a94ce9..912ca72f 100644 --- a/Main.py +++ b/Main.py @@ -19,16 +19,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 + __version__ = '0.5.1.0-u' @@ -149,7 +150,6 @@ def main(args, seed=None, fish=None): create_rooms(world, player) create_dungeons(world, player) adjust_locations(world, player) - create_item_pool_config(world) if any(world.potshuffle.values()): logger.info(world.fish.translate("cli", "cli", "shuffling.pots")) @@ -165,7 +165,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) @@ -173,8 +179,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) @@ -192,8 +197,8 @@ def main(args, seed=None, fish=None): else: lock_shop_locations(world, player) - - logger.info(world.fish.translate("cli","cli","placing.dungeon.prizes")) + massage_item_pool(world) + logger.info(world.fish.translate("cli", "cli", "placing.dungeon.prizes")) fill_prizes(world) @@ -202,14 +207,14 @@ 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 in ['balanced', 'dungeon_bias', 'entangled']: shuffled_locations = world.get_unfilled_locations() random.shuffle(shuffled_locations) fill_dungeons_restrictive(world, shuffled_locations) + elif args.algorithm == 'equitable': + promote_dungeon_items(world) else: - fill_dungeons(world) + promote_dungeon_items(world) for player in range(1, world.players+1): if world.logic[player] != 'nologic': @@ -227,34 +232,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}' @@ -405,6 +398,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() @@ -579,11 +573,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 a53fb514..bf5585b5 100644 --- a/Mystery.py +++ b/Mystery.py @@ -71,6 +71,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): @@ -79,7 +81,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}') @@ -116,6 +123,8 @@ def roll_settings(weights): ret = argparse.Namespace() + ret.algorithm = get_choice('algorithm') + glitches_required = get_choice('glitches_required') if glitches_required not in ['none', 'no_logic']: print("Only NMG and No Logic supported") diff --git a/Regions.py b/Regions.py index 35a7eda3..f26bbdb4 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/resources/app/cli/args.json b/resources/app/cli/args.json index da496104..99ddec22 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -101,12 +101,12 @@ "algorithm": { "choices": [ "balanced", - "freshness", - "flood", - "vt21", - "vt22", - "vt25", - "vt26" + "equitable", + "vanilla_bias", + "major_bias", + "dungeon_bias", + "cluster_bias", + "entangled" ] }, "shuffle": { diff --git a/resources/app/cli/lang/en.json b/resources/app/cli/lang/en.json index 13c99b2a..7b7bb4f7 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,29 @@ "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", + "biased 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_bias Same 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 junk items in vanilla locations", + "major_bias same 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. Backup locations for items", + " not in the vanilla game will be in the documentation", + "dungeon_bias same as above, but major items are preferentially placed", + " in dungeons locations first", + "cluster_bias same 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 ae26b0dd..4cfeaafb 100644 --- a/resources/app/gui/lang/en.json +++ b/resources/app/gui/lang/en.json @@ -279,13 +279,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_bias": "Biased: Vanilla", + "randomizer.item.sortingalgo.major_bias": "Biased: Major Items", + "randomizer.item.sortingalgo.dungeon_bias": "Biased: Dungeons", + "randomizer.item.sortingalgo.cluster_bias": "Biased: Clustered", "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..7f524a33 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_bias", + "major_bias", + "dungeon_bias", + "cluster_bias" ] }, "restrict_boss_items": { diff --git a/source/item/BiasedFill.py b/source/item/BiasedFill.py new file mode 100644 index 00000000..45301093 --- /dev/null +++ b/source/item/BiasedFill.py @@ -0,0 +1,881 @@ +import RaceRandom as random +import logging +from collections import defaultdict + +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) + + +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 = 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() + player_set = set() + for player in range(1, world.players+1): + if world.restrict_boss_items[player] != 'none': + player_set.add(player) + if world.restrict_boss_items[player] == 'dungeon': + for dungeon, info in dungeon_table.items(): + if info.prize: + d_name = "Thieves' Town" if dungeon.startswith('Thieves') else dungeon + config.reserved_locations[player].add(f'{d_name} - Boss') + for dungeon in world.dungeons: + for item in dungeon.all_items: + if item.map or item.compass: + item.advancement = True + if world.algorithm == 'vanilla_bias': + 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('bkhp').locs(mode_grouping['Heart Pieces']), + LocationGroup('bktrash').locs(mode_grouping['Overworld Trash'] + mode_grouping['Dungeon Trash']), + LocationGroup('bkgt').locs(mode_grouping['GT Trash'])] + elif world.algorithm == 'major_bias': + 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) + 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_bias': + 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 == '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 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 not in player_pool[item.player]: # filters out maps, compasses, etc + 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 + 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 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] + # todo: vanilla, assured, swordless? + # 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 + # 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.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_bias', 'dungeon_bias', 'cluster_bias'] or (world.algorithm == 'entangled' + and world.players > 1): + config = world.item_pool_config + for item in world.itempool: + if item.name in config.item_pool[item.player]: + if not item.advancement or not item.priority: + if item.smallkey or item.bigkey: + item.advancement = True + else: + item.priority = True + + +def split_pool(pool, world): + # bias or entangled + config = world.item_pool_config + priority, secondary = [], [] + for item in pool: + if item.name in config.item_pool[item.player]: + priority.append(item) + else: + secondary.append(item) + return priority, secondary + + +def filter_locations(item_to_place, locations, world): + if world.algorithm == 'vanilla_bias': + 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] + 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_bias', 'dungeon_bias']: + 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: + 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 - Left'], + '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 - Right'], + 'Progressive Sword': ["Link's Uncle", 'Blacksmith', 'Master Sword Pedestal', 'Pyramid Fairy - Right'], + 'Progressive Glove': ['Desert Palace - Big Chest', "Thieves' Town - Big Chest"], + 'Silver Arrows': ['Pyramid Fairy - Left'], + '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' + ] +} + + +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', 'Blue Shield', 'Red 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)'} + + +# todo: re-enter these +clustered_groups = [ + LocationGroup("MajorRoute1").locs([ + 'Library', 'Master Sword Pedestal', 'Old Man', 'Flute Spot', + 'Ether Tablet', 'Stumpy', 'Bombos Tablet', 'Mushroom', 'Bottle Merchant', 'Kakariko Tavern', + 'Sick Kid', 'Pyramid Fairy - Left', 'Pyramid Fairy - Right' + ]), + LocationGroup("MajorRoute2").locs([ + 'King Zora', 'Sahasrahla', 'Ice Rod Cave', 'Catfish', + 'Purple Chest', 'Waterfall Fairy - Left', 'Waterfall Fairy - Right', 'Blacksmith', + 'Magic Bat', 'Hobo', 'Potion Shop', 'Spike Cave', "King's Tomb" + ]), + LocationGroup("BigChest").locs([ + 'Sanctuary', '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' + ]), + LocationGroup("BossUncle").locs([ + "Link's Uncle", "Link's House", 'Secret Passage', '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']), + LocationGroup("HeartPieces LW").locs([ + 'Lost Woods Hideout', 'Kakariko Well - Top', "Blind's Hideout - Top", 'Maze Race', 'Sunken Treasure', + 'Bonk Rock Cave', 'Desert Ledge', "Aginah's Cave", 'Spectacle Rock Cave', 'Spectacle Rock', 'Pyramid', + 'Lumberjack Tree', "Zora's Ledge"]), + LocationGroup("HeartPieces DW").locs([ + 'Lake Hylia Island', 'Chest Game', 'Digging Game', 'Graveyard Cave', 'Mimic Cave', + 'Cave 45', 'Peg Cave', 'Bumper Cave Ledge', 'Checkerboard Cave', 'Mire Shed - Right', 'Floating Island', + 'Ganons Tower - Mini Helmasaur Room - Left', 'Ganons Tower - Mini Helmasaur Room - Right']), + LocationGroup("Minor Trash").locs([ + 'Ice Palace - Freezor Chest', 'Skull Woods - Pot Prison', 'Misery Mire - Bridge Chest', + 'Palace of Darkness - Dark Basement - Left', 'Palace of Darkness - Dark Maze - Top', + 'Palace of Darkness - Shooter Room', 'Palace of Darkness - The Arena - Bridge', + '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']), + LocationGroup("CompassTT").locs([ + "Thieves' Town - Ambush Chest", "Thieves' Town - Attic", + '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']), + LocationGroup("Early SKs").locs([ + 'Sewers - Dark Cross', 'Desert Palace - Torch', 'Tower of Hera - Basement Cage', + 'Palace of Darkness - Stalfos Basement', 'Palace of Darkness - Dark Basement - Right', + 'Palace of Darkness - Dark Maze - Bottom', 'Palace of Darkness - Harmless Hellway', + "Thieves' Town - Blind's Cell", 'Eastern Palace - Cannonball Chest', + 'Sewers - Secret Room - Right', 'Sewers - Secret Room - Left', + 'Sewers - Secret Room - Middle', 'Floodgate Chest' + ]), + LocationGroup("Late SKs").locs([ + 'Skull Woods - Bridge Room', 'Ice Palace - Spike Room', "Hyrule Castle - Zelda's Chest", + 'Ice Palace - Iced T Room', 'Misery Mire - Main Lobby', 'Swamp Palace - West Chest', + 'Turtle Rock - Chain Chomps', 'Turtle Rock - Crystaroller Room', + 'Turtle Rock - Eye Bridge - Bottom Left', "Ganons Tower - Bob's Torch", 'Ganons Tower - Tile Room', + 'Ganons Tower - Firesnake Room', 'Ganons Tower - Pre-Moldorm Chest', + ]), + LocationGroup("Kak-LDM").locs([ + "Blind's Hideout - Left", "Blind's Hideout - Right", "Blind's Hideout - Far Left", + "Blind's Hideout - Far Right", 'Chicken House', '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', + ]), + LocationGroup("BK-Bunny").locs([ + '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', + 'Superbunny Cave - Top', 'Superbunny Cave - Bottom', + ]), + LocationGroup("Early Drops").flags(True, True).locs([ + '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', + 'Thieves\' Town - Hallway Pot Key', 'Thieves\' Town - Spike Switch Pot Key', 'Hyrule Castle - Big Key Drop', + ]), + LocationGroup("Late Drops").flags(True, True).locs([ + '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', 'Ice Palace - Jelly Key Drop', 'Ice Palace - Conveyor Key Drop', + 'Ice Palace - Hammer Block Key Drop', 'Ice Palace - Many Pots Pot Key', 'Ganons Tower - Conveyor Cross Pot Key', + 'Ganons Tower - Double Switch Pot Key']), + LocationGroup("SS-Hype-Voo").locs([ + 'Mini Moldorm Cave - Left', + 'Mini Moldorm Cave - Right', 'Mini Moldorm Cave - Generous Guy', 'Mini Moldorm Cave - Far Left', + 'Mini Moldorm Cave - Far Right', 'Hype Cave - Top', 'Hype Cave - Middle Right', + 'Hype Cave - Middle Left', 'Hype Cave - Bottom', 'Hype Cave - Generous Guy', 'Brewery', + 'C-Shaped House', 'Palace of Darkness - The Arena - Ledge', + ]), + LocationGroup("DDM Hard").flags(True, True).locs([ + 'Hookshot Cave - Top Right', 'Hookshot Cave - Top Left', + 'Hookshot Cave - Bottom Right', 'Hookshot Cave - Bottom Left', + 'Misery Mire - Spike Chest', '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', 'Turtle Rock - Roller Room - Right', + 'Ganons Tower - Conveyor Star Pits Pot Key', 'Ganons Tower - Mini Helmasaur Key Drop' + ]), + LocationGroup("Kak Shop").flags(False, False, True).locs([ + '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', + 'Paradox Shop - Left', 'Paradox Shop - Middle', 'Paradox Shop - Right', + 'Kakariko Shop - Left', 'Kakariko Shop - Middle', 'Kakariko Shop - Right', + 'Capacity Upgrade - Left']), + LocationGroup("Hylia Shop").flags(False, False, True).locs([ + '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']), + LocationGroup("Map Validation").locs([ + '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', 'Ganons Tower - Validation Chest']), + LocationGroup("SahasWell+MireHopeDDMShop").flags(False, False, True).locs([ + 'Dark Death Mountain Shop - Left', 'Dark Death Mountain Shop - Middle', 'Dark Death Mountain Shop - Right', + 'Kakariko Well - Bottom', 'Kakariko Well - Left', 'Kakariko Well - Middle', 'Kakariko Well - Right', + "Sahasrahla's Hut - Left", "Sahasrahla's Hut - Right", "Sahasrahla's Hut - Middle", + 'Mire Shed - Left', 'Ganons Tower - Hope Room - Left', 'Ganons Tower - Hope Room - Right']), + LocationGroup("Tower Pain").flags(True).locs([ + 'Castle Tower - Room 03', 'Castle Tower - Dark Maze', + 'Ganons Tower - DMs Room - Bottom Left', 'Ganons Tower - DMs Room - Bottom Right', + 'Ganons Tower - DMs Room - Top Left', 'Ganons Tower - DMs Room - Top Right', + 'Ganons Tower - Randomizer Room - Bottom Left', 'Ganons Tower - Randomizer Room - Bottom Right', + 'Ganons Tower - Randomizer Room - Top Left', 'Ganons Tower - Randomizer Room - Top Right', + "Ganons Tower - Bob's Chest", 'Ganons Tower - Big Key Room - Right', 'Ganons Tower - Big Key Room - Left']), + LocationGroup("Retro Shops").flags(False, False, True, True).locs([ + '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', 'Swamp Palace - Entrance', + 'Ganons Tower - Compass Room - Bottom Left', 'Ganons Tower - Compass Room - Top Right', + 'Ganons Tower - Compass Room - Bottom Right', + ]) + +] + + +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 +} diff --git a/source/item/FillUtil.py b/source/item/FillUtil.py deleted file mode 100644 index 958c6b67..00000000 --- a/source/item/FillUtil.py +++ /dev/null @@ -1,20 +0,0 @@ -from collections import defaultdict - -from Dungeons import dungeon_prize - -class ItemPoolConfig(object): - - def __init__(self): - self.reserved_locations = defaultdict(set) - - -def create_item_pool_config(world): - config = ItemPoolConfig() - if world.algorithm in ['balanced']: - for player in range(1, world.players+1): - if world.restrict_boss_items[player]: - for dungeon in dungeon_prize: - if dungeon.startswith('Thieves'): - dungeon = "Thieves' Town" - config.reserved_locations[player].add(f'{dungeon} - Boss') - world.item_pool_config = config