From 391db7b5c4ba35c9d51d8be090441ff80566ebe5 Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 14 Sep 2021 15:02:18 -0600 Subject: [PATCH] Clustered bias algorithm Fixes for various other algorithms --- DoorShuffle.py | 1 + Fill.py | 131 +++++++++---- Main.py | 4 +- source/item/BiasedFill.py | 399 ++++++++++++++++++++++++-------------- 4 files changed, 344 insertions(+), 191 deletions(-) diff --git a/DoorShuffle.py b/DoorShuffle.py index 8ee16a33..a7e2ee72 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -44,6 +44,7 @@ def link_doors(world, player): reset_rooms(world, player) world.get_door("Skull Pinball WS", player).no_exit() world.swamp_patch_required[player] = orig_swamp_patch + link_doors_prep(world, player) def link_doors_prep(world, player): diff --git a/Fill.py b/Fill.py index 339a01ce..872f09e7 100644 --- a/Fill.py +++ b/Fill.py @@ -6,7 +6,7 @@ import logging 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 +from source.item.BiasedFill import filter_locations, classify_major_items, replace_trash_item, vanilla_fallback def get_dungeon_item_pool(world): @@ -107,18 +107,17 @@ def fill_restrictive(world, base_state, locations, itempool, keys_in_itempool=No if spot_to_fill: break if spot_to_fill is None: - # we filled all reachable spots. Maybe the game can be beaten anyway? - unplaced_items.insert(0, item_to_place) - if world.can_beat_game(): - if world.accessibility[item_to_place.player] != 'none': - logging.getLogger('').warning('Not all items placed. Game beatable anyway.' - f' (Could not place {item_to_place})') - continue - 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) + spot_to_fill = recovery_placement(item_to_place, locations, world, maximum_exploration_state, + base_state, itempool, perform_access_check, item_locations, + keys_in_itempool, single_player_placement) if spot_to_fill is None: + # we filled all reachable spots. Maybe the game can be beaten anyway? + unplaced_items.insert(0, item_to_place) + if world.can_beat_game(): + if world.accessibility[item_to_place.player] != 'none': + logging.getLogger('').warning('Not all items placed. Game beatable anyway.' + f' (Could not place {item_to_place})') + continue raise FillError('No more spots to place %s' % item_to_place) world.push_item(spot_to_fill, item_to_place, False) @@ -214,6 +213,55 @@ def is_dungeon_item(item, world): or (item.map and not world.mapshuffle[item.player])) +def recovery_placement(item_to_place, locations, world, state, base_state, itempool, perform_access_check, attempted, + keys_in_itempool=None, single_player_placement=False): + if world.algorithm in ['balanced', 'equitable']: + return last_ditch_placement(item_to_place, locations, world, state, base_state, itempool, keys_in_itempool, + single_player_placement) + elif world.algorithm == 'vanilla_bias': + if item_to_place.type == 'Crystal': + possible_swaps = [x for x in state.locations_checked if x.item.type == 'Crystal'] + return try_possible_swaps(possible_swaps, item_to_place, locations, world, base_state, itempool, + keys_in_itempool, single_player_placement) + else: + i, config = 0, world.item_pool_config + tried = set(attempted) + if not item_to_place.is_inside_dungeon_item(world): + while i < len(config.location_groups[item_to_place.player]): + fallback_locations = config.location_groups[item_to_place.player][i].locations + other_locs = [x for x in locations if x.name in fallback_locations] + for location in other_locs: + spot_to_fill = verify_spot_to_fill(location, item_to_place, state, single_player_placement, + perform_access_check, itempool, keys_in_itempool, world) + if spot_to_fill: + return spot_to_fill + i += 1 + tried.update(other_locs) + else: + other_locations = vanilla_fallback(item_to_place, locations, world) + for location in other_locations: + spot_to_fill = verify_spot_to_fill(location, item_to_place, state, single_player_placement, + perform_access_check, itempool, keys_in_itempool, world) + if spot_to_fill: + return spot_to_fill + tried.update(other_locations) + other_locations = [x for x in locations if x not in tried] + for location in other_locations: + spot_to_fill = verify_spot_to_fill(location, item_to_place, state, single_player_placement, + perform_access_check, itempool, keys_in_itempool, world) + if spot_to_fill: + return spot_to_fill + return None + else: + other_locations = [x for x in locations if x not in attempted] + for location in other_locations: + spot_to_fill = verify_spot_to_fill(location, item_to_place, state, single_player_placement, + perform_access_check, itempool, keys_in_itempool, world) + if spot_to_fill: + return spot_to_fill + return None + + def last_ditch_placement(item_to_place, locations, world, state, base_state, itempool, keys_in_itempool=None, single_player_placement=False): def location_preference(loc): @@ -232,7 +280,12 @@ def last_ditch_placement(item_to_place, locations, world, state, base_state, ite possible_swaps = [x for x in state.locations_checked if x.item.type not in ['Event', 'Crystal'] and not x.forced_item] swap_locations = sorted(possible_swaps, key=location_preference) + return try_possible_swaps(swap_locations, item_to_place, locations, world, base_state, itempool, + keys_in_itempool, single_player_placement) + +def try_possible_swaps(swap_locations, item_to_place, locations, world, base_state, itempool, + keys_in_itempool=None, single_player_placement=False): for location in swap_locations: old_item = location.item new_pool = list(itempool) + [old_item] @@ -301,6 +354,9 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None prioitempool = [item for item in world.itempool if not item.advancement and item.priority] restitempool = [item for item in world.itempool if not item.advancement and not item.priority] + gftower_trash &= world.algorithm in ['balanced', 'equitable', 'dungeon_bias'] + # dungeon bias may fill up the dungeon... and push items out into the overworld + # fill in gtower locations with trash first for player in range(1, world.players + 1): if not gftower_trash or not world.ganonstower_vanilla[player] or world.doorShuffle[player] == 'crossed' or world.logic[player] in ['owglitches', 'nologic']: @@ -325,40 +381,36 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None # 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) + + # sort maps and compasses to the back -- this may not be viable in equitable & ambrosia + progitempool.sort(key=lambda item: 0 if item.map or item.compass else 1) + 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 ['major_bias', 'dungeon_bias', 'cluster_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) or world.algorithm == 'cluster_bias': 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_items = [item for item in world.itempool if item.name == 'Rupee (1)'] + num_ph_items = len(placeholder_items) + if num_ph_items > 0: placeholder_locations = filter_locations('Placeholder', fill_locations, world) - placeholder_items = [item for item in world.itempool if item.name == 'Rupee (1)'] + num_ph_locations = len(placeholder_locations) + if num_ph_items < num_ph_locations < len(fill_locations): + for _ in range(num_ph_locations - num_ph_items): + placeholder_items.append(replace_trash_item(restitempool, 'Rupee (1)')) + assert len(placeholder_items) == len(placeholder_locations) for i in placeholder_items: restitempool.remove(i) for l in placeholder_locations: fill_locations.remove(l) filtered_fill(world, placeholder_items, placeholder_locations) - 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 if world.algorithm == 'vanilla_bias': fast_vanilla_fill(world, restitempool, fill_locations) @@ -389,6 +441,7 @@ def filtered_fill(world, item_pool, fill_locations): # sweep once to pick up preplaced items world.state.sweep_for_events() + def fast_vanilla_fill(world, item_pool, fill_locations): while item_pool and fill_locations: item_to_place = item_pool.pop() diff --git a/Main.py b/Main.py index 455cbe41..29e4fe57 100644 --- a/Main.py +++ b/Main.py @@ -207,12 +207,10 @@ def main(args, seed=None, fish=None): logger.info(world.fish.translate("cli","cli","placing.dungeon.items")) - if args.algorithm in ['balanced', 'dungeon_bias', 'entangled']: + if args.algorithm != 'equitable': 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: promote_dungeon_items(world) diff --git a/source/item/BiasedFill.py b/source/item/BiasedFill.py index 45301093..47aab289 100644 --- a/source/item/BiasedFill.py +++ b/source/item/BiasedFill.py @@ -1,5 +1,6 @@ import RaceRandom as random import logging +from math import ceil from collections import defaultdict from DoorShuffle import validate_vanilla_reservation @@ -29,7 +30,7 @@ class LocationGroup(object): self.retro = False def locs(self, locs): - self.locations = locs + self.locations = list(locs) return self def flags(self, k, d=False, s=False, r=False): @@ -52,9 +53,10 @@ def create_item_pool_config(world): 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.restrict_boss_items[dungeon.player] != 'none': + 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 = {} @@ -70,9 +72,12 @@ def create_item_pool_config(world): # todo: retro (universal keys...) # retro + shops config.location_groups[player] = [ + LocationGroup('Major').locs(mode_grouping['Overworld Major'] + mode_grouping['Big Chests'] + mode_grouping['Heart Containers']), LocationGroup('bkhp').locs(mode_grouping['Heart Pieces']), LocationGroup('bktrash').locs(mode_grouping['Overworld Trash'] + mode_grouping['Dungeon Trash']), LocationGroup('bkgt').locs(mode_grouping['GT Trash'])] + for loc_name in mode_grouping['Big Chests'] + mode_grouping['Heart Containers']: + config.reserved_locations[player].add(loc_name) elif world.algorithm == 'major_bias': config.location_groups = [ LocationGroup('MajorItems'), @@ -102,6 +107,7 @@ def create_item_pool_config(world): pass # todo: 5 locations for single arrow representation? config.item_pool[player] = determine_major_items(world, player) config.location_groups[0].locations = set(groups.locations) + config.reserved_locations[player].add(groups.locations) backup = (mode_grouping['Heart Pieces'] + mode_grouping['Dungeon Trash'] + mode_grouping['Shops'] + mode_grouping['Overworld Trash'] + mode_grouping['GT Trash'] + mode_grouping['RetroShops']) config.location_groups[1].locations = set(backup) @@ -121,6 +127,36 @@ def create_item_pool_config(world): backup = (mode_grouping['Heart Pieces'] + mode_grouping['Overworld Major'] + mode_grouping['Overworld Trash'] + mode_grouping['Shops'] + mode_grouping['RetroShops']) config.location_groups[1].locations = set(backup) + elif world.algorithm == 'cluster_bias': + config.location_groups = [ + LocationGroup('Clusters'), + ] + item_cnt = defaultdict(int) + config.item_pool = {} + for player in range(1, world.players + 1): + config.item_pool[player] = determine_major_items(world, player) + item_cnt[player] += count_major_items(world, player) + # set cluster choices + cluster_choices = figure_out_clustered_choices(world) + + chosen_locations = defaultdict(set) + placeholder_cnt = 0 + for player in range(1, world.players + 1): + number_of_clusters = ceil(item_cnt[player] / 13) + location_cnt = 0 + while item_cnt[player] > location_cnt: + chosen_clusters = random.sample(cluster_choices[player], number_of_clusters) + for loc_group in chosen_clusters: + for location in loc_group.locations: + if not location_prefilled(location, world, player): + world.item_pool_config.reserved_locations[player].add(location) + chosen_locations[location].add(player) + location_cnt += 1 + cluster_choices[player] = [x for x in cluster_choices[player] if x not in chosen_clusters] + number_of_clusters = 1 + placeholder_cnt += location_cnt - item_cnt[player] + config.placeholders = placeholder_cnt + config.location_groups[0].locations = chosen_locations elif world.algorithm == 'entangled' and world.players > 1: config.location_groups = [ LocationGroup('Entangled'), @@ -169,6 +205,14 @@ def create_item_pool_config(world): config.location_groups[0].locations = chosen_locations +def location_prefilled(location, world, player): + if world.swords[player] == 'vanilla': + return location in vanilla_swords + if world.goal[player] == 'pedestal': + return location == 'Master Sword Pedestal' + return False + + def previously_reserved(location, world, player): if '- Boss' in location.name: if world.restrict_boss_items[player] == 'mapcompass' and (not world.compassshuffle[player] @@ -188,7 +232,7 @@ def massage_item_pool(world): 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 + if (not item.compass and not item.map) or item not in player_pool[item.player]: player_pool[item.player].append(item) player_locations = defaultdict(list) for player in player_pool: @@ -220,6 +264,9 @@ def massage_item_pool(world): deleted = trash_options.pop() world.itempool.remove(deleted) removed += 1 + if world.item_pool_config.placeholders > len(single_rupees): + for _ in range(world.item_pool_config.placeholders-len(single_rupees)): + single_rupees.append(ItemFactory('Rupee (1)', random.randint(1, world.players))) placeholders = random.sample(single_rupees, world.item_pool_config.placeholders) world.itempool += placeholders removed -= len(placeholders) @@ -227,6 +274,19 @@ def massage_item_pool(world): world.itempool.append(ItemFactory('Rupees (5)', random.randint(1, world.players))) +def replace_trash_item(item_pool, replacement): + trash_options = [x for x in item_pool if x.name in trash_items] + random.shuffle(trash_options) + trash_options = sorted(trash_options, key=lambda x: trash_items[x.name], reverse=True) + if len(trash_options) == 0: + logging.getLogger('').warning(f'Too many good items in pool, not enough room for placeholders') + deleted = trash_options.pop() + item_pool.remove(deleted) + replace_item = ItemFactory(replacement, deleted.player) + item_pool.append(replace_item) + return replace_item + + def validate_reservation(location, dungeon, world, player): world.item_pool_config.reserved_locations[player].add(location.name) if world.doorShuffle[player] != 'vanilla': @@ -265,12 +325,22 @@ def count_major_items(world, player): 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 + if world.swords[player] != "random": + if world.swords[player] == 'assured': + major_item_set -= 1 + if world.swords[player] in ['vanilla', 'swordless']: + major_item_set -= 4 + if world.retro[player]: + if world.shopsanity[player]: + major_item_set -= 1 # sword in old man cave + if world.keyshuffle[player]: + major_item_set -= 29 + # universal keys + major_item_set += 19 if world.difficulty[player] == 'normal' else 14 + if world.mode[player] == 'standard' and world.doorShuffle[player] == 'vanilla': + major_item_set -= 1 # a key in escape + if world.doorShuffle[player] != 'vanilla': + major_item_set += 10 # tries to add up to 10 more universal keys for door rando # todo: starting equipment? return major_item_set @@ -354,16 +424,68 @@ def classify_major_items(world): 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 figure_out_clustered_choices(world): + cluster_candidates = {} + for player in range(1, world.players + 1): + cluster_candidates[player] = [LocationGroup(x.name).locs(x.locations) for x in clustered_groups] + backups = list(reversed(leftovers)) + if world.bigkeyshuffle[player]: + bk_grp = LocationGroup('BigKeys').locs(mode_grouping['Big Keys']) + if world.keydropshuffle[player]: + bk_grp.locations.append(mode_grouping['Big Key Drops']) + for i in range(13-len(bk_grp.locations)): + bk_grp.locations.append(backups.pop()) + cluster_candidates[player].append(bk_grp) + if world.compassshuffle[player]: + cmp_grp = LocationGroup('Compasses').locs(mode_grouping['Compasses']) + if len(cmp_grp.locations) + len(backups) >= 13: + for i in range(13-len(cmp_grp.locations)): + cmp_grp.locations.append(backups.pop()) + cluster_candidates[player].append(cmp_grp) + else: + backups.extend(reversed(cmp_grp.locations)) + if world.mapshuffle[player]: + mp_grp = LocationGroup('Maps').locs(mode_grouping['Maps']) + if len(mp_grp.locations) + len(backups) >= 13: + for i in range(13-len(mp_grp.locations)): + mp_grp.locations.append(backups.pop()) + cluster_candidates[player].append(mp_grp) + else: + backups.extend(reversed(mp_grp.locations)) + if world.shopsanity[player]: + cluster_candidates[player].append(LocationGroup('Shopsanity1').locs(other_clusters['Shopsanity1'])) + cluster_candidates[player].append(LocationGroup('Shopsanity2').locs(other_clusters['Shopsanity2'])) + extras = list(other_clusters['ShopsanityLeft']) + if world.retro[player]: + extras.extend(mode_grouping['RetroShops']) + if len(extras)+len(backups) >= 13: + for i in range(13-len(extras)): + extras.append(backups.pop()) + cluster_candidates[player].append(LocationGroup('ShopExtra').locs(extras)) + else: + backups.extend(reversed(extras)) + if world.keyshuffle[player] or world.retro[player]: + cluster_candidates[player].append(LocationGroup('SmallKey1').locs(other_clusters['SmallKey1'])) + cluster_candidates[player].append(LocationGroup('SmallKey2').locs(other_clusters['SmallKey2'])) + extras = list(other_clusters['SmallKeyLeft']) + if world.keydropshuffle[player]: + cluster_candidates[player].append(LocationGroup('KeyDrop1').locs(other_clusters['KeyDrop1'])) + cluster_candidates[player].append(LocationGroup('KeyDrop2').locs(other_clusters['KeyDrop2'])) + extras.extend(other_clusters['KeyDropLeft']) + if len(extras)+len(backups) >= 13: + for i in range(13-len(extras)): + extras.append(backups.pop()) + cluster_candidates[player].append(LocationGroup('SmallKeyExtra').locs(extras)) + else: + backups.extend(reversed(extras)) + return cluster_candidates + + +def vanilla_fallback(item_to_place, locations, world): + if item_to_place.is_inside_dungeon_item(world): + return [x for x in locations if x.name in vanilla_fallback_dungeon_set + and x.parent_region.dungeon and x.parent_region.dungeon.name == item_to_place.dungeon] + return [] def filter_locations(item_to_place, locations, world): @@ -391,7 +513,7 @@ def filter_locations(item_to_place, locations, world): 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: + if (world.algorithm == 'entangled' and world.players > 1) or world.algorithm == 'cluster_bias': 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 @@ -417,7 +539,7 @@ vanilla_mapping = { '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'], + 'Progressive Bow': ['Eastern Palace - Big Chest', 'Pyramid Fairy - Right'], 'Book of Mudora': ['Library'], 'Hammer': ['Palace of Darkness - Big Chest'], 'Hookshot': ['Swamp Palace - Big Chest'], @@ -443,10 +565,10 @@ vanilla_mapping = { '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'], + 'Golden Sword': ['Pyramid Fairy - Left'], + 'Progressive Sword': ["Link's Uncle", 'Blacksmith', 'Master Sword Pedestal', 'Pyramid Fairy - Left'], 'Progressive Glove': ['Desert Palace - Big Chest', "Thieves' Town - Big Chest"], - 'Silver Arrows': ['Pyramid Fairy - Left'], + 'Silver Arrows': ['Pyramid Fairy - Right'], 'Single Arrow': ['Palace of Darkness - Dark Basement - Left'], 'Arrows (10)': ['Chicken House', 'Mini Moldorm Cave - Far Right', 'Sewers - Secret Room - Right', 'Paradox Cave Upper - Right', 'Mire Shed - Right', 'Ganons Tower - Hope Room - Left', @@ -702,6 +824,11 @@ mode_grouping = { ] } +vanilla_fallback_dungeon_set = set(mode_grouping['Dungeon Trash'] + mode_grouping['Big Keys'] + + mode_grouping['GT Trash'] + mode_grouping['Small Keys'] + + mode_grouping['Compasses'] + mode_grouping['Maps'] + mode_grouping['Key Drops'] + + mode_grouping['Big Key Drops']) + major_items = {'Bombos', 'Book of Mudora', 'Cane of Somaria', 'Ether', 'Fire Rod', 'Flippers', 'Ocarina', 'Hammer', 'Hookshot', 'Ice Rod', 'Lamp', 'Cape', 'Magic Powder', 'Mushroom', 'Pegasus Boots', 'Quake', 'Shovel', @@ -714,148 +841,122 @@ major_items = {'Bombos', 'Book of Mudora', 'Cane of Somaria', 'Ether', 'Fire Rod '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' + 'Ice Rod Cave', 'Library', 'Old Man', 'Magic Bat', 'Ether Tablet', 'Hobo', 'Purple Chest', 'Spike Cave', + 'Sahasrahla', 'Superbunny Cave - Bottom', 'Superbunny Cave - Top', + 'Ganons Tower - Randomizer Room - Bottom Left', 'Ganons Tower - Randomizer Room - Bottom Right' ]), LocationGroup("MajorRoute2").locs([ - '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" + 'Mushroom', 'Secret Passage', 'Bottle Merchant', 'Flute Spot', 'Catfish', 'Stumpy', 'Waterfall Fairy - Left', + 'Waterfall Fairy - Right', 'Master Sword Pedestal', "Thieves' Town - Attic", 'Sewers - Secret Room - Right', + 'Sewers - Secret Room - Left', 'Sewers - Secret Room - Middle' ]), - LocationGroup("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("MajorRoute3").locs([ + 'Kakariko Tavern', 'Sick Kid', 'King Zora', 'Potion Shop', 'Bombos Tablet', "King's Tomb", 'Blacksmith', + 'Pyramid Fairy - Left', 'Pyramid Fairy - Right', 'Hookshot Cave - Top Right', 'Hookshot Cave - Top Left', + 'Hookshot Cave - Bottom Right', 'Hookshot Cave - Bottom Left' ]), - LocationGroup("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([ + LocationGroup("Dungeon Major").locs([ + 'Eastern Palace - Big Chest', 'Desert Palace - Big Chest', 'Tower of Hera - Big Chest', + 'Palace of Darkness - Big Chest', 'Swamp Palace - Big Chest', 'Skull Woods - Big Chest', + "Thieves' Town - Big Chest", 'Misery Mire - Big Chest', 'Hyrule Castle - Boomerang Chest', + 'Ice Palace - Big Chest', 'Turtle Rock - Big Chest', 'Ganons Tower - Big Chest', "Link's Uncle"]), + LocationGroup("Dungeon Heart").locs([ + 'Sanctuary', 'Eastern Palace - Boss', 'Desert Palace - Boss', 'Tower of Hera - Boss', + 'Palace of Darkness - Boss', 'Swamp Palace - Boss', 'Skull Woods - Boss', "Thieves' Town - Boss", + 'Ice Palace - Boss', 'Misery Mire - Boss', 'Turtle Rock - Boss', "Link's House", + 'Ganons Tower - Validation Chest']), + LocationGroup("HeartPieces1").locs([ + 'Kakariko Well - Top', 'Lost Woods Hideout', 'Maze Race', 'Lumberjack Tree', 'Bonk Rock Cave', 'Graveyard Cave', + 'Checkerboard Cave', "Zora's Ledge", 'Digging Game', 'Desert Ledge', 'Bumper Cave Ledge', 'Floating Island', + 'Swamp Palace - Waterfall Room']), + LocationGroup("HeartPieces2").locs([ + "Blind's Hideout - Top", 'Sunken Treasure', "Aginah's Cave", 'Mimic Cave', 'Spectacle Rock Cave', 'Cave 45', + 'Spectacle Rock', 'Lake Hylia Island', 'Chest Game', 'Mire Shed - Right', 'Pyramid', 'Peg Cave', + 'Eastern Palace - Cannonball Chest']), + LocationGroup("BlindHope").locs([ "Blind's Hideout - Left", "Blind's Hideout - Right", "Blind's Hideout - Far Left", - "Blind's Hideout - Far Right", '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', + "Blind's Hideout - Far Right", 'Floodgate Chest', 'Spiral Cave', 'Palace of Darkness - Dark Maze - Bottom', + 'Palace of Darkness - Dark Maze - Top', 'Swamp Palace - Flooded Room - Left', + 'Swamp Palace - Flooded Room - Right', "Thieves' Town - Ambush Chest", 'Ganons Tower - Hope Room - Left', + 'Ganons Tower - Hope Room - Right']), + LocationGroup('WellHype').locs([ + 'Kakariko Well - Left', 'Kakariko Well - Middle', 'Kakariko Well - Right', 'Kakariko Well - Bottom', + 'Paradox Cave Upper - Left', 'Paradox Cave Upper - Right', 'Hype Cave - Top', 'Hype Cave - Middle Right', + 'Hype Cave - Middle Left', 'Hype Cave - Bottom', 'Hype Cave - Generous Guy', + 'Ganons Tower - DMs Room - Bottom Left', 'Ganons Tower - DMs Room - Bottom Right', ]), - LocationGroup("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('MiniMoldormLasers').locs([ + 'Mini Moldorm Cave - Left', 'Mini Moldorm Cave - Right', 'Mini Moldorm Cave - Generous Guy', + 'Mini Moldorm Cave - Far Left', 'Mini Moldorm Cave - Far Right', 'Chicken House', 'Brewery', + 'Palace of Darkness - Dark Basement - Left', 'Ice Palace - Freezor Chest', 'Swamp Palace - West Chest', + 'Turtle Rock - Eye Bridge - Bottom Right', 'Turtle Rock - Eye Bridge - Top Left', + 'Turtle Rock - Eye Bridge - Top Right', ]), - LocationGroup("Early Drops").flags(True, True).locs([ + LocationGroup('ParadoxCloset').locs([ + "Sahasrahla's Hut - Left", "Sahasrahla's Hut - Right", "Sahasrahla's Hut - Middle", + 'Paradox Cave Lower - Far Left', 'Paradox Cave Lower - Left', 'Paradox Cave Lower - Right', + 'Paradox Cave Lower - Far Right', 'Paradox Cave Lower - Middle', "Hyrule Castle - Zelda's Chest", + 'C-Shaped House', 'Mire Shed - Left', 'Ganons Tower - Compass Room - Bottom Right', + 'Ganons Tower - Compass Room - Bottom Left', + ]) +] + +other_clusters = { + 'SmallKey1': [ + 'Sewers - Dark Cross', 'Tower of Hera - Basement Cage', 'Palace of Darkness - Shooter Room', + 'Palace of Darkness - The Arena - Bridge', 'Palace of Darkness - Stalfos Basement', + 'Palace of Darkness - Dark Basement - Right', "Thieves' Town - Blind's Cell", 'Skull Woods - Bridge Room', + 'Ice Palace - Iced T Room', 'Misery Mire - Main Lobby', 'Misery Mire - Bridge Chest', + 'Misery Mire - Spike Chest', "Ganons Tower - Bob's Torch"], + 'SmallKey2': [ + 'Desert Palace - Torch', 'Castle Tower - Room 03', 'Castle Tower - Dark Maze', + 'Palace of Darkness - The Arena - Ledge', 'Palace of Darkness - Harmless Hellway', 'Swamp Palace - Entrance', + 'Skull Woods - Pot Prison', 'Skull Woods - Pinball Room', 'Ice Palace - Spike Room', + 'Turtle Rock - Roller Room - Right', 'Turtle Rock - Chain Chomps', 'Turtle Rock - Crystaroller Room', + 'Turtle Rock - Eye Bridge - Bottom Left'], + 'SmallKeyLeft': [ + 'Ganons Tower - Tile Room', 'Ganons Tower - Firesnake Room', 'Ganons Tower - Pre-Moldorm Chest'], + 'KeyDrop1': [ 'Hyrule Castle - Map Guard Key Drop', 'Hyrule Castle - Boomerang Guard Key Drop', - 'Hyrule Castle - Key Rat Key Drop', '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', + 'Hyrule Castle - Key Rat Key Drop', 'Swamp Palace - Hookshot Pot Key', 'Swamp Palace - Trench 2 Pot Key', + 'Swamp Palace - Waterway Pot Key', 'Skull Woods - West Lobby Pot Key', 'Skull Woods - Spike Corner Key Drop', + 'Ice Palace - Jelly Key Drop', 'Ice Palace - Conveyor Key Drop', 'Misery Mire - Spikes Pot Key', + 'Misery Mire - Fishbone Pot Key', 'Misery Mire - Conveyor Crystal Key Drop'], + 'KeyDrop2': [ + 'Eastern Palace - Dark Square Pot Key', 'Eastern Palace - Dark Eyegore Key Drop', + 'Desert Palace - Desert Tiles 1 Pot Key', 'Desert Palace - Beamos Hall Pot Key', + 'Desert Palace - Desert Tiles 2 Pot Key', 'Swamp Palace - Pot Row Pot Key', 'Swamp Palace - Trench 1 Pot Key', + "Thieves' Town - Hallway Pot Key", "Thieves' Town - Spike Switch Pot Key", 'Ice Palace - Hammer Block Key Drop', + 'Ice Palace - Many Pots Pot Key', 'Turtle Rock - Pokey 1 Key Drop', 'Turtle Rock - Pokey 2 Key Drop'], + 'KeyDropLeft': [ 'Castle Tower - Dark Archer Key Drop', 'Castle Tower - Circle of Pots Key Drop', - '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', + 'Ganons Tower - Conveyor Cross Pot Key', 'Ganons Tower - Double Switch Pot Key', + 'Ganons Tower - Conveyor Star Pits Pot Key', 'Ganons Tower - Mini Helmasuar Key Drop'], + 'Shopsanity1': [ 'Dark Lumberjack Shop - Left', 'Dark Lumberjack Shop - Middle', 'Dark Lumberjack Shop - Right', + 'Dark Lake Hylia Shop - Left', 'Dark Lake Hylia Shop - Middle', 'Dark Lake Hylia Shop - Right', 'Paradox Shop - Left', 'Paradox Shop - Middle', 'Paradox Shop - Right', - 'Kakariko Shop - Left', 'Kakariko Shop - Middle', 'Kakariko Shop - Right', - 'Capacity Upgrade - Left']), - LocationGroup("Hylia Shop").flags(False, False, True).locs([ + 'Kakariko Shop - Left', 'Kakariko Shop - Middle', 'Kakariko Shop - Right', 'Capacity Upgrade - Left'], + 'Shopsanity2': [ 'Red Shield Shop - Left', 'Red Shield Shop - Middle', 'Red Shield Shop - Right', 'Village of Outcasts Shop - Left', 'Village of Outcasts Shop - Middle', 'Village of Outcasts Shop - Right', 'Dark Potion Shop - Left', 'Dark Potion Shop - Middle', 'Dark Potion Shop - Right', - 'Lake Hylia Shop - Left', 'Lake Hylia Shop - Middle', 'Lake Hylia Shop - Right', - 'Capacity Upgrade - Right']), - 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', - ]) + 'Lake Hylia Shop - Left', 'Lake Hylia Shop - Middle', 'Lake Hylia Shop - Right', 'Capacity Upgrade - Right', + ], + 'ShopsanityLeft': ['Potion Shop - Left', 'Potion Shop - Middle', 'Potion Shop - Right'] +} +leftovers = [ + 'Ganons Tower - DMs Room - Top Right', 'Ganons Tower - DMs Room - Top Left', + 'Ganons Tower - Compass Room - Top Right', 'Ganons Tower - Randomizer Room - Top Left', + 'Ganons Tower - Randomizer Room - Top Right',"Ganons Tower - Bob's Chest", 'Ganons Tower - Big Key Room - Left', + 'Ganons Tower - Big Key Room - Right', 'Ganons Tower - Mini Helmasaur Room - Left', + 'Ganons Tower - Mini Helmasaur Room - Right', ] +vanilla_swords = {"Link's Uncle", 'Master Sword Pedestal', 'Blacksmith', 'Pyramid Fairy - Left'} trash_items = { 'Nothing': -1,