From 88849e921c3b7bf7f0a3fa097642664e58e7ce0c Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 2 Jan 2020 11:37:47 -0700 Subject: [PATCH] Revert "Revert "Cross Dungeon initial work"" This reverts commit fadc085b67b35e69ea22184ffc8722e7997d9edb. --- BaseClasses.py | 77 ++++- DoorShuffle.py | 559 ++++++++++++++++-------------- Doors.py | 38 +- DungeonGenerator.py | 822 ++++++++++++++++++++++++++++++++++++++++++-- Dungeons.py | 18 +- Items.py | 1 + KeyDoorShuffle.py | 531 +++++++++++++--------------- Regions.py | 3 +- Rules.py | 33 +- 9 files changed, 1425 insertions(+), 657 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 7a11c515..8a455065 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -229,11 +229,15 @@ class World(object): if keys: for p in range(1, self.players + 1): + key_list = [] + player_dungeons = [x for x in self.dungeons if x.player == p] + for dungeon in player_dungeons: + if dungeon.big_key is not None: + key_list += [dungeon.big_key.name] + if len(dungeon.small_keys) > 0: + key_list += [x.name for x in dungeon.small_keys] from Items import ItemFactory - for item in ItemFactory(['Small Key (Escape)', 'Big Key (Eastern Palace)', 'Big Key (Desert Palace)', 'Small Key (Desert Palace)', 'Big Key (Tower of Hera)', 'Small Key (Tower of Hera)', 'Small Key (Agahnims Tower)', 'Small Key (Agahnims Tower)', - 'Big Key (Palace of Darkness)'] + ['Small Key (Palace of Darkness)'] * 6 + ['Big Key (Thieves Town)', 'Small Key (Thieves Town)', 'Big Key (Skull Woods)'] + ['Small Key (Skull Woods)'] * 3 + ['Big Key (Swamp Palace)', - 'Small Key (Swamp Palace)', 'Big Key (Ice Palace)'] + ['Small Key (Ice Palace)'] * 2 + ['Big Key (Misery Mire)', 'Big Key (Turtle Rock)', 'Big Key (Ganons Tower)'] + ['Small Key (Misery Mire)'] * 3 + ['Small Key (Turtle Rock)'] * 4 + ['Small Key (Ganons Tower)'] * 4, - p): + for item in ItemFactory(key_list, p): soft_collect(item) ret.sweep_for_events() return ret @@ -889,12 +893,30 @@ class Polarity: def __getitem__(self, item): return self.vector[item] + def __eq__(self, other): + for i in range(len(self.vector)): + if self.vector[i] != other.vector[i]: + return False + return True + def is_neutral(self): for i in range(len(self.vector)): if self.vector[i] != 0: return False return True + def complement(self): + result = Polarity() + for i in range(len(self.vector)): + result.vector[i] = pol_comp[pol_idx_2[i]](self.vector[i]) + return result + + def charge(self): + result = 0 + for i in range(len(self.vector)): + result += abs(self.vector[i]) + return result + pol_idx = { Direction.North: (0, 'Pos'), @@ -918,6 +940,11 @@ pol_add = { 'Add': lambda x, y: x + y, 'Mod': lambda x, y: (x + y) % 2 } +pol_comp = { + 'Add': lambda x: -x, + 'Mod': lambda x: 0 if x == 0 else 1 +} + @unique class CrystalBarrier(Enum): @@ -961,6 +988,7 @@ class Door(object): self.req_event = None # if a dungeon event is required for this door - swamp palace mostly self.controller = None self.dependents = [] + self.dead = False def getAddress(self): if self.type == DoorType.Normal: @@ -1035,6 +1063,10 @@ class Door(object): self.crystal = CrystalBarrier.Either return self + def kill(self): + self.dead = True + return self + def __eq__(self, other): return isinstance(other, self.__class__) and self.name == other.name @@ -1054,6 +1086,19 @@ class Sector(object): self.regions = [] self.outstanding_doors = [] self.name = None + self.r_name_set = None + self.chest_locations = 0 + self.key_only_locations = 0 + self.c_switch = False + self.orange_barrier = False + self.blue_barrier = False + self.bk_required = False + self.bk_provided = False + + def region_set(self): + if self.r_name_set is None: + self.r_name_set = dict.fromkeys(map(lambda r: r.name, self.regions)) + return self.r_name_set.keys() def polarity(self): pol = Polarity() @@ -1076,6 +1121,19 @@ class Sector(object): outflow = outflow + 1 return outflow + def adj_outflow(self): + outflow = 0 + for door in self.outstanding_doors: + if not door.blocked and not door.dead: + outflow = outflow + 1 + return outflow + + def __str__(self): + return str(self.__unicode__()) + + def __unicode__(self): + return '%s' % next(iter(self.region_set())) + class Boss(object): def __init__(self, name, enemizer_name, defeat_rule, player): @@ -1335,14 +1393,9 @@ class Spoiler(object): self.bosses[str(player)]["Ice Palace"] = self.world.get_dungeon("Ice Palace", player).boss.name self.bosses[str(player)]["Misery Mire"] = self.world.get_dungeon("Misery Mire", player).boss.name self.bosses[str(player)]["Turtle Rock"] = self.world.get_dungeon("Turtle Rock", player).boss.name - if self.world.mode != 'inverted': - self.bosses[str(player)]["Ganons Tower Basement"] = self.world.get_dungeon('Ganons Tower', player).bosses['bottom'].name - self.bosses[str(player)]["Ganons Tower Middle"] = self.world.get_dungeon('Ganons Tower', player).bosses['middle'].name - self.bosses[str(player)]["Ganons Tower Top"] = self.world.get_dungeon('Ganons Tower', player).bosses['top'].name - else: - self.bosses[str(player)]["Ganons Tower Basement"] = self.world.get_dungeon('Inverted Ganons Tower', player).bosses['bottom'].name - self.bosses[str(player)]["Ganons Tower Middle"] = self.world.get_dungeon('Inverted Ganons Tower', player).bosses['middle'].name - self.bosses[str(player)]["Ganons Tower Top"] = self.world.get_dungeon('Inverted Ganons Tower', player).bosses['top'].name + self.bosses[str(player)]["Ganons Tower Basement"] = [x for x in self.world.dungeons if x.player == player and 'bottom' in x.bosses.keys()][0].bosses['bottom'].name + self.bosses[str(player)]["Ganons Tower Middle"] = [x for x in self.world.dungeons if x.player == player and 'middle' in x.bosses.keys()][0].bosses['middle'].name + self.bosses[str(player)]["Ganons Tower Top"] = [x for x in self.world.dungeons if x.player == player and 'top' in x.bosses.keys()][0].bosses['top'].name self.bosses[str(player)]["Ganons Tower"] = "Agahnim 2" self.bosses[str(player)]["Ganon"] = "Ganon" diff --git a/DoorShuffle.py b/DoorShuffle.py index 4708c40d..77fe8ed5 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -6,13 +6,16 @@ import operator as op import time from functools import reduce -from BaseClasses import RegionType, Door, DoorType, Direction, Sector, Polarity, CrystalBarrier +from BaseClasses import RegionType, Door, DoorType, Direction, Sector, CrystalBarrier +from Regions import key_only_locations from Dungeons import hyrule_castle_regions, eastern_regions, desert_regions, hera_regions, tower_regions, pod_regions from Dungeons import dungeon_regions, region_starts, split_region_starts, flexible_starts -from Dungeons import drop_entrances +from Dungeons import drop_entrances, dungeon_bigs, dungeon_keys +from Items import ItemFactory from RoomData import DoorKind, PairedDoor from DungeonGenerator import ExplorationState, convert_regions, generate_dungeon -from KeyDoorShuffle import analyze_dungeon, validate_vanilla_key_logic, build_key_layout, validate_key_layout_ex +from DungeonGenerator import create_dungeon_builders, split_dungeon_builder, simple_dungeon_builder +from KeyDoorShuffle import analyze_dungeon, validate_vanilla_key_logic, build_key_layout, validate_key_layout def link_doors(world, player): @@ -53,15 +56,15 @@ def link_doors(world, player): elif world.doorShuffle == 'experimental': experiment(world, player) - mark_regions(world, player) if world.doorShuffle != 'vanilla': create_door_spoiler(world, player) +# todo: I think this function is not necessary def mark_regions(world, player): # traverse dungeons and make sure dungeon property is assigned - playerDungeons = [dungeon for dungeon in world.dungeons if dungeon.player == player] - for dungeon in playerDungeons: + player_dungeons = [dungeon for dungeon in world.dungeons if dungeon.player == player] + for dungeon in player_dungeons: queue = collections.deque(dungeon.regions) while len(queue) > 0: region = world.get_region(queue.popleft(), player) @@ -101,34 +104,31 @@ def create_door_spoiler(world, player): logger.debug('Door not found in queue: %s connected to %s', door_b.name, door_a.name) else: logger.warning('Door not connected: %s', door_a.name) - # for dp in world.paired_doors[player]: - # if dp.pair: - # logger.debug('Paired Doors: %s with %s (p%d)', dp.door_a, dp.door_b, player) - # else: - # logger.debug('Unpaired Doors: %s not paired with %s (p%d)', dp.door_a, dp.door_b, player) def vanilla_key_logic(world, player): - sectors = [] + builders = [] for dungeon in [dungeon for dungeon in world.dungeons if dungeon.player == player]: sector = Sector() sector.name = dungeon.name sector.regions.extend(convert_regions(dungeon.regions, world, player)) - sectors.append(sector) + builder = simple_dungeon_builder(sector.name, [sector], world, player) + builder.master_sector = sector + builders.append(builder) overworld_prep(world, player) entrances_map, potentials, connections = determine_entrance_list(world, player) - for sector in sectors: - start_regions = convert_regions(entrances_map[sector.name], world, player) - doors = convert_key_doors(default_small_key_doors[sector.name], world, player) - key_layout = build_key_layout(sector, start_regions, doors, world, player) - valid = validate_key_layout_ex(key_layout, world, player) + for builder in builders: + start_regions = convert_regions(entrances_map[builder.name], 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) + valid = validate_key_layout(key_layout, world, player) if not valid: - raise Exception('Vanilla key layout not valid %s' % sector.name) + raise Exception('Vanilla key layout not valid %s' % builder.name) if player not in world.key_logic.keys(): world.key_logic[player] = {} key_layout = analyze_dungeon(key_layout, world, player) - world.key_logic[player][sector.name] = key_layout.key_logic + world.key_logic[player][builder.name] = key_layout.key_logic validate_vanilla_key_logic(world, player) @@ -271,61 +271,78 @@ def pair_existing_key_doors(world, player, door_a, door_b): def within_dungeon(world, player): fix_big_key_doors_with_ugly_smalls(world, player) overworld_prep(world, player) - dungeon_sectors = [] entrances_map, potentials, connections = determine_entrance_list(world, player) + connections_tuple = (entrances_map, potentials, connections) + + dungeon_builders = {} for key in dungeon_regions.keys(): sector_list = convert_to_sectors(dungeon_regions[key], world, player) - if key in split_region_starts.keys(): - split_sectors = split_up_sectors(sector_list, split_region_starts[key]) - for idx, sub_sector_list in enumerate(split_sectors): - entrance_list = list(split_region_starts[key][idx]) - # shuffable entrances like pinball, left pit need to be added to entrance list - if key in flexible_starts.keys(): - add_shuffled_entrances(sub_sector_list, flexible_starts[key], entrance_list) - filtered_list = [x for x in entrance_list if x in entrances_map[key]] - dungeon_sectors.append((key, sub_sector_list, filtered_list)) - else: - dungeon_sectors.append((key, sector_list, list(entrances_map[key]))) - - enabled_entrances = {} - dungeon_layouts = [] - sector_queue = collections.deque(dungeon_sectors) - last_key = None - while len(sector_queue) > 0: - key, sector_list, entrance_list = sector_queue.popleft() - split_dungeon = key in split_region_starts.keys() - origin_list = list(entrance_list) - find_enabled_origins(sector_list, enabled_entrances, origin_list, entrances_map, key) - origin_list_sans_drops = remove_drop_origins(origin_list) - if len(origin_list_sans_drops) <= 0: - if last_key == key: - raise Exception('Infinte loop detected %s' % key) - sector_queue.append((key, sector_list, entrance_list)) - last_key = key - else: - ds = generate_dungeon(key, sector_list, origin_list_sans_drops, split_dungeon, world, player) - find_new_entrances(ds, connections, potentials, enabled_entrances, world, player) - ds.name = key - layout_starts = origin_list if len(entrance_list) <= 0 else entrance_list - dungeon_layouts.append((ds, layout_starts)) - last_key = None - - combine_layouts(dungeon_layouts, entrances_map) - world.dungeon_layouts[player] = {} - for sector, entrances in dungeon_layouts: - find_enabled_origins([sector], enabled_entrances, entrances, entrances_map, sector.name) - world.dungeon_layouts[player][sector.name] = (sector, entrances_map[sector.name]) + dungeon_builders[key] = simple_dungeon_builder(key, sector_list, world, player) + dungeon_builders[key].entrance_list = list(entrances_map[key]) + recombinant_builders = {} + handle_split_dungeons(dungeon_builders, recombinant_builders, entrances_map) + main_dungeon_generation(dungeon_builders, recombinant_builders, connections_tuple, world, player) paths = determine_required_paths(world) check_required_paths(paths, world, player) # shuffle_key_doors for dungeons start = time.process_time() - for sector, entrances in world.dungeon_layouts[player].values(): - shuffle_key_doors(sector, entrances, world, player) + for builder in world.dungeon_layouts[player].values(): + shuffle_key_doors(builder, world, player) logging.getLogger('').info('Key door shuffle time: %s', time.process_time()-start) +def handle_split_dungeons(dungeon_builders, recombinant_builders, entrances_map): + for name, split_list in split_region_starts.items(): + builder = dungeon_builders.pop(name) + recombinant_builders[name] = builder + split_builders = split_dungeon_builder(builder, split_list) + dungeon_builders.update(split_builders) + for sub_name, split_entrances in split_list.items(): + sub_builder = dungeon_builders[name+' '+sub_name] + entrance_list = list(split_entrances) + if name in flexible_starts.keys(): + add_shuffled_entrances(sub_builder.sectors, flexible_starts[name], entrance_list) + filtered_entrance_list = [x for x in entrance_list if x in entrances_map[name]] + sub_builder.entrance_list = filtered_entrance_list + + +def main_dungeon_generation(dungeon_builders, recombinant_builders, connections_tuple, world, player): + entrances_map, potentials, connections = connections_tuple + enabled_entrances = {} + sector_queue = collections.deque(dungeon_builders.values()) + last_key = None + while len(sector_queue) > 0: + builder = sector_queue.popleft() + split_dungeon = builder.name.startswith('Desert Palace') or builder.name.startswith('Skull Woods') + name = builder.name + if split_dungeon: + name = ' '.join(builder.name.split(' ')[:-1]) + origin_list = list(builder.entrance_list) + find_enabled_origins(builder.sectors, enabled_entrances, origin_list, entrances_map, name) + origin_list_sans_drops = remove_drop_origins(origin_list) + if len(origin_list_sans_drops) <= 0: + if last_key == builder.name: + raise Exception('Infinte loop detected %s' % builder.name) + sector_queue.append(builder) + last_key = builder.name + else: + logging.getLogger('').info('Generating dungeon: %s', builder.name) + ds = generate_dungeon(name, builder.sectors, origin_list_sans_drops, split_dungeon, world, player) + find_new_entrances(ds, connections, potentials, enabled_entrances, world, player) + ds.name = name + builder.master_sector = ds + builder.layout_starts = origin_list if len(builder.entrance_list) <= 0 else builder.entrance_list + last_key = None + combine_layouts(recombinant_builders, dungeon_builders, entrances_map) + world.dungeon_layouts[player] = {} + for builder in dungeon_builders.values(): + find_enabled_origins([builder.master_sector], enabled_entrances, builder.layout_starts, entrances_map, builder.name) + builder.path_entrances = entrances_map[builder.name] + world.dungeon_layouts[player] = dungeon_builders + + def determine_entrance_list(world, player): entrance_map = {} potential_entrances = {} @@ -347,6 +364,7 @@ def determine_entrance_list(world, player): return entrance_map, potential_entrances, connections +# todo: kill drop exceptions def drop_exception(name): return name in ['Skull Pot Circle', 'Skull Back Drop'] @@ -365,6 +383,8 @@ def find_enabled_origins(sectors, enabled, entrance_list, entrance_map, key): entrance_list.append(region.name) origin_reg, origin_dungeon = enabled[region.name] if origin_reg != region.name and origin_dungeon != region.dungeon: + if key not in entrance_map.keys(): + key = ' '.join(key.split(' ')[:-1]) entrance_map[key].append(region.name) if drop_exception(region.name): # only because they have unique regions entrance_list.append(region.name) @@ -593,34 +613,128 @@ def doors_fit_mandatory_pair(pair_list, a, b): def cross_dungeon(world, player): + fix_big_key_doors_with_ugly_smalls(world, player) + overworld_prep(world, player) + entrances_map, potentials, connections = determine_entrance_list(world, player) + connections_tuple = (entrances_map, potentials, connections) + all_sectors = [] for key in dungeon_regions.keys(): all_sectors.extend(convert_to_sectors(dungeon_regions[key], world, player)) - dungeon_split = split_up_sectors(all_sectors, default_dungeon_sets) - dungeon_sectors = [] - for idx, sector_list in enumerate(dungeon_split): - name = dungeon_x_idx_to_name[idx] - if name in split_region_starts.keys(): - split = split_up_sectors(sector_list, split_region_starts[name]) - for sub_idx, sub_sector_list in enumerate(split): - dungeon_sectors.append((name, sub_sector_list, split_region_starts[name][sub_idx])) + dungeon_builders = create_dungeon_builders(all_sectors, world, player) + for builder in dungeon_builders.values(): + builder.entrance_list = list(entrances_map[builder.name]) + dungeon_obj = world.get_dungeon(builder.name, player) + for sector in builder.sectors: + for region in sector.regions: + region.dungeon = dungeon_obj + for loc in region.locations: + if loc.name in key_only_locations: + key_name = dungeon_keys[builder.name] if loc.name != 'Hyrule Castle - Big Key Drop' else dungeon_bigs[builder.name] + loc.forced_item = loc.item = ItemFactory(key_name, player) + recombinant_builders = {} + handle_split_dungeons(dungeon_builders, recombinant_builders, entrances_map) + + main_dungeon_generation(dungeon_builders, recombinant_builders, connections_tuple, world, player) + + paths = determine_required_paths(world) + check_required_paths(paths, world, player) + + start = time.process_time() + total_keys = remaining = 29 + total_candidates = 0 + start_regions_map = {} + # Step 1: Find Small Key Door Candidates + for name, builder in dungeon_builders.items(): + dungeon = world.get_dungeon(name, player) + if not builder.bk_required or builder.bk_provided: + dungeon.big_key = None + elif builder.bk_required and not builder.bk_provided: + dungeon.big_key = ItemFactory(dungeon_bigs[name], player) + start_regions = convert_regions(builder.path_entrances, world, player) + find_small_key_door_candidates(builder, start_regions, world, player) + builder.key_doors_num = max(0, len(builder.candidates) - builder.key_drop_cnt) + total_candidates += builder.key_doors_num + start_regions_map[name] = start_regions + + # Step 2: Initial Key Number Assignment & Calculate Flexibility + for name, builder in dungeon_builders.items(): + calculated = int(round(builder.key_doors_num*total_keys/total_candidates)) + max_keys = builder.location_cnt - calc_used_dungeon_items(builder) + cand_len = max(0, len(builder.candidates) - builder.key_drop_cnt) + limit = min(max_keys, cand_len) + suggested = min(calculated, limit) + combo_size = ncr(len(builder.candidates), suggested + builder.key_drop_cnt) + while combo_size > 500000 and suggested > 0: + suggested -= 1 + combo_size = ncr(len(builder.candidates), suggested + builder.key_drop_cnt) + builder.key_doors_num = suggested + builder.key_drop_cnt + remaining -= suggested + builder.combo_size = combo_size + if suggested < limit: + builder.flex = limit - suggested + + # Step 3: Initial valid combination find - reduce flex if needed + for name, builder in dungeon_builders.items(): + suggested = builder.key_doors_num - builder.key_drop_cnt + find_valid_combination(builder, start_regions_map[name], world, player) + actual_chest_keys = builder.key_doors_num - builder.key_drop_cnt + if actual_chest_keys < suggested: + remaining += suggested - actual_chest_keys + builder.flex = 0 + + # Step 4: Try to assign remaining keys + builder_order = [x for x in dungeon_builders.values() if x.flex > 0] + builder_order.sort(key=lambda b: b.combo_size) + queue = collections.deque(builder_order) + logger = logging.getLogger('') + while len(queue) > 0 and remaining > 0: + builder = queue.popleft() + name = builder.name + logger.info('Cross Dungeon: Increasing key count by 1 for %s', name) + builder.key_doors_num += 1 + result = find_valid_combination(builder, start_regions_map[name], world, player, drop_keys=False) + if result: + remaining -= 1 + builder.flex -= 1 + if builder.flex > 0: + builder.combo_size = ncr(len(builder.candidates), builder.key_doors_num) + queue.append(builder) + queue = collections.deque(sorted(queue, key=lambda b: b.combo_size)) else: - dungeon_sectors.append((name, sector_list, region_starts[name])) - # todo - adjust dungeon item pools -- ? - dungeon_layouts = [] - # for key, sector_list, entrance_list in dungeon_sectors: - # ds = shuffle_dungeon_no_repeats_new(world, player, sector_list, entrance_list) - # ds.name = key - # dungeon_layouts.append((ds, entrance_list)) + logger.info('Cross Dungeon: Increase failed for %s', name) + builder.flex = 0 + logger.info('Cross Dungeon: Keys unable to assign in pool %s', remaining) - combine_layouts(dungeon_layouts) + # Last Step: Adjust Small Key Dungeon Pool + for name, builder in dungeon_builders.items(): + actual_chest_keys = max(builder.key_doors_num - builder.key_drop_cnt, 0) + dungeon = world.get_dungeon(name, player) + if actual_chest_keys == 0: + dungeon.small_keys = [] + else: + dungeon.small_keys = [ItemFactory(dungeon_keys[name], player)] * actual_chest_keys + logging.getLogger('').info('Cross Dungeon: Key door shuffle time: %s', time.process_time()-start) + # todo: pair down paired doors - excessive rom writes ATM - for layout in dungeon_layouts: - shuffle_key_doors(layout[0], layout[1], world, player) + # Re-assign dungeon bosses + gt = world.get_dungeon('Ganons Tower', player) + for name, builder in dungeon_builders.items(): + reassign_boss('GT Ice Armos', 'bottom', builder, gt, world, player) + reassign_boss('GT Lanmolas 2', 'middle', builder, gt, world, player) + reassign_boss('GT Moldorm', 'top', builder, gt, world, player) + + +def reassign_boss(boss_region, boss_key, builder, gt, world, player): + if boss_region in builder.master_sector.region_set(): + new_dungeon = world.get_dungeon(builder.name, player) + if new_dungeon != gt: + gt_boss = gt.bosses.pop(boss_key) + new_dungeon.bosses[boss_key] = gt_boss def experiment(world, player): - within_dungeon(world, player) + cross_dungeon(world, player) def convert_to_sectors(region_names, world, player): @@ -628,12 +742,12 @@ def convert_to_sectors(region_names, world, player): sectors = [] while len(region_list) > 0: region = region_list.pop() - sector = None new_sector = True region_chunk = [region] exits = [] exits.extend(region.exits) outstanding_doors = [] + matching_sectors = [] while len(exits) > 0: ext = exits.pop() if ext.connected_region is not None: @@ -645,182 +759,55 @@ def convert_to_sectors(region_names, world, player): if connect_region not in region_chunk: for existing in sectors: if connect_region in existing.regions: - sector = existing new_sector = False + if existing not in matching_sectors: + matching_sectors.append(existing) else: door = world.check_for_door(ext.name, player) if door is not None and door.controller is None and door.dest is None: outstanding_doors.append(door) - if new_sector: - sector = Sector() + sector = Sector() + if not new_sector: + for match in matching_sectors: + sector.regions.extend(match.regions) + sector.outstanding_doors.extend(match.outstanding_doors) + sectors.remove(match) sector.regions.extend(region_chunk) sector.outstanding_doors.extend(outstanding_doors) - if new_sector: - sectors.append(sector) + sectors.append(sector) return sectors # those with split region starts like Desert/Skull combine for key layouts -def combine_layouts(dungeon_layouts, entrances_map): - combined = {} - queue = collections.deque(dungeon_layouts) - while len(queue) > 0: - sector, entrance_list = queue.pop() - if sector.name in split_region_starts: - dungeon_layouts.remove((sector, entrance_list)) - # desert_entrances.extend(entrance_list) - if sector.name not in combined: - combined[sector.name] = sector - else: - combined[sector.name].regions.extend(sector.regions) - for key in combined.keys(): - dungeon_layouts.append((combined[key], list(entrances_map[key]))) - - -def split_up_sectors(sector_list, entrance_sets): - new_sector_grid = [] - leftover_sectors = [] - leftover_sectors.extend(sector_list) - for entrance_set in entrance_sets: - new_sector_list = [] - for sector in sector_list: - s_regions = list(map(lambda r: r.name, sector.regions)) - for entrance in entrance_set: - if entrance in s_regions: - new_sector_list.append(sector) - leftover_sectors.remove(sector) - break - new_sector_grid.append(new_sector_list) - shuffle_sectors(new_sector_grid, leftover_sectors) - return new_sector_grid - - -def sum_vector(sector_list, func): - result = [0, 0, 0] - for sector in sector_list: - vector = func(sector) - for i in range(len(result)): - result[i] = result[i] + vector[i] - return result - - -def is_polarity_neutral(sector_list): - pol = Polarity() - for sector in sector_list: - pol += sector.polarity() - return pol.is_neutral() - - -search_iterations = 0 - - -def is_proposal_valid(proposal, buckets, candidates): - logger = logging.getLogger('') - global search_iterations - search_iterations = search_iterations + 1 - if search_iterations % 100 == 0: - logger.debug('Iteration %s', search_iterations) - # check that proposal is complete - for i in range(len(proposal)): - if proposal[i] is -1: - return False # indicates an incomplete proposal - test_bucket = [] - for bucket_idx in range(len(buckets)): - test_bucket.append(list(buckets[bucket_idx])) - for i in range(len(proposal)): - test_bucket[proposal[i]].append(candidates[i]) - for test in test_bucket: - valid = is_polarity_neutral(test) - if not valid: - return False - for sector in test: - other_sectors = list(test) - other_sectors.remove(sector) - sector_mag = sector.magnitude() - other_mag = sum_vector(other_sectors, lambda x: x.magnitude()) - for i in range(len(sector_mag)): - if sector_mag[i] > 0 and other_mag[i] == 0: - return False - return True - - -def shuffle_sectors(buckets, candidates): - # for a faster search - instead of random - put the most likely culprits to cause problems at the end, least likely at the front - # unless we start checking for failures earlier in the algo - random.shuffle(candidates) - proposal = [-1]*len(candidates) - - solution = find_proposal_monte_carlo(proposal, buckets, candidates) - if solution is None: - raise Exception('Unable to find a proposal') - for i in range(len(solution)): - buckets[solution[i]].append(candidates[i]) - - -# monte carlo proposal generation -def find_proposal_monte_carlo(proposal, buckets, candidates): - n = len(candidates) - k = len(buckets) - hashes = set() - collisions = 0 - - while collisions < 10000: - hash = '' - for i in range(n): - proposal[i] = random.randrange(k) - hash = hash + str(proposal[i]) - if hash not in hashes: - collisions = 0 - if is_proposal_valid(proposal, buckets, candidates): - return proposal - hashes.add(hash) - else: - collisions = collisions + 1 - raise Exception('Too many collisions in a row, solutions space is sparse') - - -# this is a DFS search - fairly slow -def find_proposal(proposal, buckets, candidates): - size = len(candidates) - combination_grid = [] - for i in range(size): - combination_grid.append(list(range(len(buckets)))) - # randomize which bucket - for possible_buckets in combination_grid: - random.shuffle(possible_buckets) - - idx = 0 - while idx != size or not is_proposal_valid(proposal, buckets, candidates): - if idx == size: - idx = idx - 1 - while len(combination_grid[idx]) == 0: - if idx == -1: # this is the failure case - we shouldn't hit it - return None - combination_grid[idx] = list(range(len(buckets))) - idx = idx - 1 - proposal[idx] = combination_grid[idx].pop() - # can we detect a bad choice at this stage - idx = idx + 1 - return proposal +def combine_layouts(recombinant_builders, dungeon_builders, entrances_map): + for recombine in recombinant_builders.values(): + queue = collections.deque(dungeon_builders.values()) + while len(queue) > 0: + builder = queue.pop() + if builder.name.startswith(recombine.name): + del dungeon_builders[builder.name] + if recombine.master_sector is None: + recombine.master_sector = builder.master_sector + recombine.master_sector.name = recombine.name + else: + recombine.master_sector.regions.extend(builder.master_sector.regions) + recombine.layout_starts = list(entrances_map[recombine.name]) + dungeon_builders[recombine.name] = recombine def valid_region_to_explore(region, world, player): return region.type == RegionType.Dungeon or region.name in world.inaccessible_regions[player] -def shuffle_key_doors(dungeon_sector, entrances, world, player): - logger = logging.getLogger('') - logger.info('Shuffling Key doors for %s', dungeon_sector.name) - start_regions = convert_regions(entrances, world, player) +def shuffle_key_doors(builder, world, player): + start_regions = convert_regions(builder.path_entrances, world, player) # count number of key doors - this could be a table? num_key_doors = 0 - current_doors = [] skips = [] - for region in dungeon_sector.regions: + for region in builder.master_sector.regions: for ext in region.exits: d = world.check_for_door(ext.name, player) if d is not None and d.smallKey: - current_doors.append(d) if d not in skips: if d.type == DoorType.Interior: skips.append(d.dest) @@ -833,7 +820,22 @@ def shuffle_key_doors(dungeon_sector, entrances, world, player): skips.append(world.get_door(dp.door_a, player)) break num_key_doors += 1 + builder.key_doors_num = num_key_doors + find_small_key_door_candidates(builder, start_regions, world, player) + find_valid_combination(builder, start_regions, world, player) + +def find_current_key_doors(builder, world, player): + current_doors = [] + for region in builder.master_sector.regions: + for ext in region.exits: + d = world.check_for_door(ext.name, player) + if d is not None and d.smallKey: + current_doors.append(d) + return current_doors + + +def find_small_key_door_candidates(builder, start_regions, world, player): # traverse dungeon and find candidates candidates = [] checked_doors = set() @@ -847,42 +849,73 @@ def shuffle_key_doors(dungeon_sector, entrances, world, player): if candidate.type != DoorType.Normal or candidate.dest not in checked_doors or candidate.dest in candidates: flat_candidates.append(candidate) - # find valid combination of candidates paired_candidates = build_pair_list(flat_candidates) - if len(paired_candidates) < num_key_doors: - num_key_doors = len(paired_candidates) # reduce number of key doors - logger.info('Lowering key door count because not enough candidates: %s', dungeon_sector.name) - combinations = ncr(len(paired_candidates), num_key_doors) + builder.candidates = paired_candidates + + +def calc_used_dungeon_items(builder): + base = 4 + if builder.bk_required and not builder.bk_provided: + base += 1 + if builder.name == 'Hyrule Castle': + base -= 1 # Missing compass/map + if builder.name == 'Agahnims Tower': + base -= 2 # Missing both compass/map + # gt can lose map once compasses work + return base + + +def find_valid_combination(builder, start_regions, world, player, drop_keys=True): + logger = logging.getLogger('') + logger.info('Shuffling Key doors for %s', builder.name) + # find valid combination of candidates + if len(builder.candidates) < builder.key_doors_num: + if not drop_keys: + logger.info('No valid layouts for %s with %s doors', builder.name, builder.key_doors_num) + return False + builder.key_doors_num = len(builder.candidates) # reduce number of key doors + logger.info('Lowering key door count because not enough candidates: %s', builder.name) + combinations = ncr(len(builder.candidates), builder.key_doors_num) itr = 0 start = time.process_time() sample_list = list(range(0, int(combinations))) random.shuffle(sample_list) - proposal = kth_combination(sample_list[itr], paired_candidates, num_key_doors) + proposal = kth_combination(sample_list[itr], builder.candidates, builder.key_doors_num) - key_layout = build_key_layout(dungeon_sector, start_regions, proposal, world, player) - while not validate_key_layout_ex(key_layout, world, player): + key_layout = build_key_layout(builder, start_regions, proposal, world, player) + while not validate_key_layout(key_layout, world, player): itr += 1 - if itr >= combinations: - logger.info('Lowering key door count because no valid layouts: %s', dungeon_sector.name) - num_key_doors -= 1 - if num_key_doors < 0: - raise Exception('Bad dungeon %s - 0 key doors not valid' % dungeon_sector.name) - combinations = ncr(len(paired_candidates), num_key_doors) + stop_early = False + if itr % 1000 == 0: + mark = time.process_time()-start + if (mark > 10 and itr*100/combinations > 50) or (mark > 20 and itr*100/combinations > 25) or mark > 30: + stop_early = True + if itr >= combinations or stop_early: + if not drop_keys: + logger.info('No valid layouts for %s with %s doors', builder.name, builder.key_doors_num) + return False + logger.info('Lowering key door count because no valid layouts: %s', builder.name) + builder.key_doors_num -= 1 + if builder.key_doors_num < 0: + raise Exception('Bad dungeon %s - 0 key doors not valid' % builder.name) + combinations = ncr(len(builder.candidates), builder.key_doors_num) sample_list = list(range(0, int(combinations))) random.shuffle(sample_list) itr = 0 - proposal = kth_combination(sample_list[itr], paired_candidates, num_key_doors) - key_layout.reset(proposal) + start = time.process_time() # reset time since itr reset + proposal = kth_combination(sample_list[itr], builder.candidates, builder.key_doors_num) + key_layout.reset(proposal, builder, world, player) if (itr+1) % 1000 == 0: mark = time.process_time()-start logger.info('%s time elapsed. %s iterations/s', mark, itr/mark) # make changes if player not in world.key_logic.keys(): world.key_logic[player] = {} - key_layout_new = analyze_dungeon(key_layout, world, player) - reassign_key_doors(current_doors, proposal, world, player) - log_key_logic(dungeon_sector.name, key_layout_new.key_logic) - world.key_logic[player][dungeon_sector.name] = key_layout_new.key_logic + analyze_dungeon(key_layout, world, player) + reassign_key_doors(builder, proposal, world, player) + log_key_logic(builder.name, key_layout.key_logic) + world.key_logic[player][builder.name] = key_layout.key_logic + return True def log_key_logic(d_name, key_logic): @@ -910,7 +943,7 @@ def build_pair_list(flat_list): queue = collections.deque(flat_list) while len(queue) > 0: d = queue.pop() - if d.dest in queue: + if d.dest in queue and d.type != DoorType.SpiralStairs: paired_list.append((d, d.dest)) queue.remove(d.dest) else: @@ -989,10 +1022,10 @@ def ncr(n, r): return numerator / denominator -def reassign_key_doors(current_doors, proposal, world, player): +def reassign_key_doors(builder, proposal, world, player): logger = logging.getLogger('') flat_proposal = flatten_pair_list(proposal) - queue = collections.deque(current_doors) + queue = collections.deque(find_current_key_doors(builder, world, player)) while len(queue) > 0: d = queue.pop() if d.type is DoorType.SpiralStairs and d not in proposal: @@ -1085,7 +1118,7 @@ def determine_required_paths(world): paths['Hyrule Castle'].append('Hyrule Dungeon Cellblock') # noinspection PyTypeChecker paths['Hyrule Castle'].append(('Hyrule Dungeon Cellblock', 'Sanctuary')) - if world.doorShuffle in ['basic', 'experimental']: + if world.doorShuffle in ['basic', 'experimental']: # todo: crossed? paths['Thieves Town'].append('Thieves Attic Window') return paths @@ -1167,19 +1200,19 @@ def create_door(world, player, entName, region_name): def check_required_paths(paths, world, player): for dungeon_name in paths.keys(): - sector, entrances = world.dungeon_layouts[player][dungeon_name] + builder = world.dungeon_layouts[player][dungeon_name] if len(paths[dungeon_name]) > 0: states_to_explore = defaultdict(list) for path in paths[dungeon_name]: if type(path) is tuple: states_to_explore[tuple([path[0]])].append(path[1]) else: - states_to_explore[tuple(entrances)].append(path) + states_to_explore[tuple(builder.path_entrances)].append(path) cached_initial_state = None for start_regs, dest_regs in states_to_explore.items(): check_paths = convert_regions(dest_regs, world, player) start_regions = convert_regions(start_regs, world, player) - initial = start_regs == tuple(entrances) + initial = start_regs == tuple(builder.path_entrances) if not initial or cached_initial_state is None: init = determine_init_crystal(initial, cached_initial_state, start_regions) state = ExplorationState(init, dungeon_name) diff --git a/Doors.py b/Doors.py index f389cbc8..ed6cd4ba 100644 --- a/Doors.py +++ b/Doors.py @@ -145,7 +145,7 @@ def create_doors(world, player): create_door(player, 'Eastern Compass Room EN', Intr).dir(Ea, 0xa8, Top, High).pos(3), create_door(player, 'Eastern Hint Tile WN', Intr).dir(We, 0xa8, Top, High).pos(3), create_door(player, 'Eastern Hint Tile EN', Nrml).dir(Ea, 0xa8, Top, Low).pos(4), - create_door(player, 'Eastern Hint Tile Blocked Path SE', Nrml).dir(So, 0xa8, Right, High).small_key().pos(2), + create_door(player, 'Eastern Hint Tile Blocked Path SE', Nrml).dir(So, 0xa8, Right, High).small_key().pos(2).kill(), create_door(player, 'Eastern Hint Tile Push Block', Lgcl), create_door(player, 'Eastern Courtyard WN', Nrml).dir(We, 0xa9, Top, Low).pos(3), create_door(player, 'Eastern Courtyard EN', Nrml).dir(Ea, 0xa9, Top, Low).pos(4), @@ -226,7 +226,7 @@ def create_doors(world, player): create_door(player, 'Desert Four Statues ES', Intr).dir(Ea, 0x53, Bot, High).pos(1), create_door(player, 'Desert Beamos Hall WS', Intr).dir(We, 0x53, Bot, High).pos(1), create_door(player, 'Desert Beamos Hall NE', Nrml).dir(No, 0x53, Right, High).small_key().pos(2), - create_door(player, 'Desert Tiles 2 SE', Nrml).dir(So, 0x43, Right, High).small_key().pos(2), + create_door(player, 'Desert Tiles 2 SE', Nrml).dir(So, 0x43, Right, High).small_key().pos(2).kill(), create_door(player, 'Desert Tiles 2 NE', Intr).dir(No, 0x43, Right, High).small_key().pos(1), create_door(player, 'Desert Wall Slide SE', Intr).dir(So, 0x43, Right, High).small_key().pos(1), create_door(player, 'Desert Wall Slide NW', Nrml).dir(No, 0x43, Left, High).big_key().pos(0).no_entrance(), @@ -263,7 +263,7 @@ def create_doors(world, player): create_door(player, 'Hera 5F Pothole Chain', Hole), create_door(player, 'Hera 5F Normal Holes', Hole), create_door(player, 'Hera Fairies\' Warp', Warp), - create_door(player, 'Hera Boss Down Stairs', Sprl).dir(Dn, 0x07, 0, HTH).ss(S, 0x61, 0xb0), + create_door(player, 'Hera Boss Down Stairs', Sprl).dir(Dn, 0x07, 0, HTH).ss(S, 0x61, 0xb0).kill(), create_door(player, 'Hera Boss Outer Hole', Hole), create_door(player, 'Hera Boss Inner Hole', Hole), @@ -322,12 +322,12 @@ def create_doors(world, player): create_door(player, 'PoD Pit Room Freefall', Hole), create_door(player, 'PoD Pit Room Bomb Hole', Hole), create_door(player, 'PoD Big Key Landing Hole', Hole), - create_door(player, 'PoD Big Key Landing Down Stairs', Sprl).dir(Dn, 0x3a, 0, HTH).ss(A, 0x11, 0x00), + create_door(player, 'PoD Big Key Landing Down Stairs', Sprl).dir(Dn, 0x3a, 0, HTH).ss(A, 0x11, 0x00).kill(), create_door(player, 'PoD Basement Ledge Up Stairs', Sprl).dir(Up, 0x0a, 0, HTH).ss(A, 0x1a, 0xec).small_key().pos(0), create_door(player, 'PoD Basement Ledge Drop Down', Lgcl), create_door(player, 'PoD Stalfos Basement Warp', Warp), create_door(player, 'PoD Arena Main SW', Nrml).dir(So, 0x2a, Left, High).pos(4), - create_door(player, 'PoD Arena Bridge SE', Nrml).dir(So, 0x2a, Right, High).pos(5), + create_door(player, 'PoD Arena Bridge SE', Nrml).dir(So, 0x2a, Right, High).pos(5).kill(), create_door(player, 'PoD Arena Main NW', Nrml).dir(No, 0x2a, Left, High).small_key().pos(1), create_door(player, 'PoD Arena Main NE', Nrml).dir(No, 0x2a, Right, High).no_exit().trap(0x4).pos(0), create_door(player, 'PoD Arena Main Crystal Path', Lgcl), @@ -366,7 +366,7 @@ def create_doors(world, player): create_door(player, 'PoD Dark Basement W Up Stairs', Sprl).dir(Up, 0x6a, 0, HTH).ss(S, 0x1b, 0x3c, True), create_door(player, 'PoD Dark Basement E Up Stairs', Sprl).dir(Up, 0x6a, 1, HTH).ss(S, 0x1b, 0x9c, True), create_door(player, 'PoD Dark Alley NE', Nrml).dir(No, 0x6a, Right, High).big_key().pos(0), - create_door(player, 'PoD Mimics 2 SW', Nrml).dir(So, 0x1b, Left, High).pos(1), + create_door(player, 'PoD Mimics 2 SW', Nrml).dir(So, 0x1b, Left, High).pos(1).kill(), create_door(player, 'PoD Mimics 2 NW', Intr).dir(No, 0x1b, Left, High).pos(0), create_door(player, 'PoD Bow Statue SW', Intr).dir(So, 0x1b, Left, High).pos(0), create_door(player, 'PoD Bow Statue Down Ladder', Lddr).no_entrance(), @@ -444,7 +444,7 @@ def create_doors(world, player): create_door(player, 'Swamp Barrier EN', Nrml).dir(Ea, 0x34, Top, High).pos(0), create_door(player, 'Swamp Barrier - Orange', Lgcl), create_door(player, 'Swamp Barrier Ledge Hook Path', Lgcl), - create_door(player, 'Swamp Attic Down Stairs', Sprl).dir(Dn, 0x54, 0, HTH).ss(Z, 0x12, 0x80), + create_door(player, 'Swamp Attic Down Stairs', Sprl).dir(Dn, 0x54, 0, HTH).ss(Z, 0x12, 0x80).kill(), create_door(player, 'Swamp Attic Left Pit', Hole), create_door(player, 'Swamp Attic Right Pit', Hole), create_door(player, 'Swamp Push Statue S', Nrml).dir(So, 0x26, Mid, High).small_key().pos(0), @@ -460,7 +460,7 @@ def create_doors(world, player): create_door(player, 'Swamp Drain Left Up Stairs', Sprl).dir(Up, 0x76, 0, HTH).ss(S, 0x1b, 0x2c, True, True), create_door(player, 'Swamp Drain WN', Intr).dir(We, 0x76, Top, Low).pos(0), create_door(player, 'Swamp Drain Right Switch', Lgcl), - create_door(player, 'Swamp Drain Right Up Stairs', Sprl).dir(Up, 0x76, 1, HTH).ss(S, 0x1b, 0x9c, True, True), + create_door(player, 'Swamp Drain Right Up Stairs', Sprl).dir(Up, 0x76, 1, HTH).ss(S, 0x1b, 0x9c, True, True).kill(), create_door(player, 'Swamp Flooded Room Up Stairs', Sprl).dir(Up, 0x76, 2, HTH).ss(X, 0x1a, 0xac, True, True), create_door(player, 'Swamp Flooded Room WS', Intr).dir(We, 0x76, Bot, Low).pos(1), create_door(player, 'Swamp Flooded Spot Ladder', Lgcl), @@ -649,7 +649,7 @@ def create_doors(world, player): create_door(player, 'Ice Spike Room WS', Nrml).dir(We, 0x5f, Bot, High).small_key().pos(0), create_door(player, 'Ice Spike Room Down Stairs', Sprl).dir(Dn, 0x5f, 3, HTH).ss(Z, 0x11, 0x48, True, True), create_door(player, 'Ice Spike Room Up Stairs', Sprl).dir(Up, 0x5f, 4, HTH).ss(Z, 0x1a, 0xa4, True, True), - create_door(player, 'Ice Hammer Block Down Stairs', Sprl).dir(Dn, 0x3f, 0, HTH).ss(Z, 0x11, 0xb8, True, True), + create_door(player, 'Ice Hammer Block Down Stairs', Sprl).dir(Dn, 0x3f, 0, HTH).ss(Z, 0x11, 0xb8, True, True).kill(), create_door(player, 'Ice Hammer Block ES', Intr).dir(Ea, 0x3f, Bot, High).pos(0), create_door(player, 'Ice Tongue Pull WS', Intr).dir(We, 0x3f, Bot, High).pos(0), create_door(player, 'Ice Tongue Pull Up Ladder', Lddr), @@ -661,7 +661,7 @@ def create_doors(world, player): create_door(player, 'Ice Tall Hint WS', Intr).dir(We, 0x7e, Bot, High).pos(1), create_door(player, 'Ice Tall Hint EN', Nrml).dir(Ea, 0x7e, Top, High).pos(1), create_door(player, 'Ice Tall Hint SE', Nrml).dir(So, 0x7e, Right, High).small_key().pos(0), - create_door(player, 'Ice Hookshot Ledge WN', Nrml).dir(We, 0x7f, Top, High).no_exit().trap(0x4).pos(0), + create_door(player, 'Ice Hookshot Ledge WN', Nrml).dir(We, 0x7f, Top, High).no_exit().trap(0x4).pos(0).kill(), create_door(player, 'Ice Hookshot Ledge Path', Lgcl), create_door(player, 'Ice Hookshot Balcony Path', Lgcl), create_door(player, 'Ice Hookshot Balcony SW', Intr).dir(So, 0x7f, Left, High).pos(1), @@ -787,7 +787,7 @@ def create_doors(world, player): create_door(player, 'Mire BK Chest Ledge WS', Intr).dir(We, 0xd1, Bot, High).pos(0), create_door(player, 'Mire Warping Pool ES', Intr).dir(Ea, 0xd1, Bot, High).no_exit().pos(0), create_door(player, 'Mire Warping Pool Warp', Warp), - create_door(player, 'Mire Torches Top Down Stairs', Sprl).dir(Dn, 0x97, 0, HTH).ss(A, 0x11, 0xb0, True), + create_door(player, 'Mire Torches Top Down Stairs', Sprl).dir(Dn, 0x97, 0, HTH).ss(A, 0x11, 0xb0, True).kill(), create_door(player, 'Mire Torches Top SW', Intr).dir(So, 0x97, Left, High).pos(1), create_door(player, 'Mire Torches Bottom NW', Intr).dir(No, 0x97, Left, High).pos(1), create_door(player, 'Mire Torches Bottom WS', Intr).dir(We, 0x97, Bot, High).pos(0), @@ -901,10 +901,10 @@ def create_doors(world, player): create_door(player, 'GT Hope Room WN', Intr).dir(We, 0x8c, Top, High).small_key().pos(2), create_door(player, 'GT Torch SW', Intr).dir(So, 0x8c, Left, High).no_exit().pos(1), create_door(player, 'GT Big Chest NW', Intr).dir(So, 0x8c, Left, High).pos(1), - create_door(player, 'GT Blocked Stairs Down Stairs', Sprl).dir(Dn, 0x8c, 3, HTH).ss(Z, 0x12, 0x40, True, True), + create_door(player, 'GT Blocked Stairs Down Stairs', Sprl).dir(Dn, 0x8c, 3, HTH).ss(Z, 0x12, 0x40, True, True).kill(), create_door(player, 'GT Blocked Stairs Block Path', Lgcl), create_door(player, 'GT Big Chest SW', Nrml).dir(So, 0x8c, Left, High).pos(4), - create_door(player, 'GT Bob\'s Room SE', Nrml).dir(So, 0x8c, Right, High).pos(5), + create_door(player, 'GT Bob\'s Room SE', Nrml).dir(So, 0x8c, Right, High).pos(5).kill(), create_door(player, 'GT Bob\'s Room Hole', Hole), create_door(player, 'GT Tile Room WN', Nrml).dir(We, 0x8d, Top, High).pos(2), create_door(player, 'GT Tile Room EN', Intr).dir(Ea, 0x8d, Top, High).small_key().pos(1), @@ -913,7 +913,7 @@ def create_doors(world, player): create_door(player, 'GT Speed Torch WS', Intr).dir(We, 0x8d, Bot, High).pos(4), create_door(player, 'GT Pots n Blocks ES', Intr).dir(Ea, 0x8d, Bot, High).pos(4), create_door(player, 'GT Speed Torch SE', Nrml).dir(So, 0x8d, Right, High).trap(0x4).pos(0), - create_door(player, 'GT Crystal Conveyor NE', Nrml).dir(No, 0x9d, Right, High).pos(0), + create_door(player, 'GT Crystal Conveyor NE', Nrml).dir(No, 0x9d, Right, High).pos(0).kill(), create_door(player, 'GT Crystal Conveyor WN', Intr).dir(We, 0x9d, Top, High).pos(2), create_door(player, 'GT Compass Room EN', Intr).dir(Ea, 0x9d, Top, High).pos(2), create_door(player, 'GT Compass Room Warp', Warp), @@ -937,7 +937,7 @@ def create_doors(world, player): create_door(player, 'GT Hookshot ES', Intr).dir(Ea, 0x8b, Bot, High).small_key().pos(1), create_door(player, 'GT Map Room WS', Intr).dir(We, 0x8b, Bot, High).small_key().pos(1), create_door(player, 'GT Hookshot SW', Nrml).dir(So, 0x8b, Left, High).pos(3), - create_door(player, 'GT Double Switch NW', Nrml).dir(No, 0x9b, Left, High).pos(1), + create_door(player, 'GT Double Switch NW', Nrml).dir(No, 0x9b, Left, High).pos(1).kill(), create_door(player, 'GT Double Switch Orange Barrier', Lgcl), create_door(player, 'GT Double Switch Orange Barrier 2', Lgcl), create_door(player, 'GT Double Switch Blue Path', Lgcl), @@ -972,7 +972,7 @@ def create_doors(world, player): create_door(player, 'GT Warp Maze (Rails) WS', Nrml).dir(We, 0x7d, Bot, High).pos(1), create_door(player, 'GT Trap Room SE', Nrml).dir(So, 0x7d, Right, High).trap(0x4).pos(0), create_door(player, 'GT Conveyor Star Pits EN', Nrml).dir(Ea, 0x7b, Top, High).small_key().pos(1), - create_door(player, 'GT Hidden Star ES', Nrml).dir(Ea, 0x7b, Bot, High).pos(2), + create_door(player, 'GT Hidden Star ES', Nrml).dir(Ea, 0x7b, Bot, High).pos(2).kill(), create_door(player, 'GT Hidden Star Warp', Warp), create_door(player, 'GT DMs Room SW', Nrml).dir(So, 0x7b, Left, High).trap(0x4).pos(0), create_door(player, 'GT Falling Bridge WN', Nrml).dir(We, 0x7c, Top, High).small_key().pos(2), @@ -1009,7 +1009,7 @@ def create_doors(world, player): create_door(player, 'GT Gauntlet 4 SW', Intr).dir(So, 0x6d, Left, High).pos(1), create_door(player, 'GT Gauntlet 5 NW', Intr).dir(No, 0x6d, Left, High).pos(1), create_door(player, 'GT Gauntlet 5 WS', Nrml).dir(We, 0x6d, Bot, High).pos(2), - create_door(player, 'GT Beam Dash ES', Nrml).dir(Ea, 0x6c, Bot, High).pos(2), + create_door(player, 'GT Beam Dash ES', Nrml).dir(Ea, 0x6c, Bot, High).pos(2).kill(), create_door(player, 'GT Beam Dash WS', Intr).dir(We, 0x6c, Bot, High).pos(0), create_door(player, 'GT Lanmolas 2 ES', Intr).dir(Ea, 0x6c, Bot, High).pos(0), create_door(player, 'GT Lanmolas 2 NW', Intr).dir(No, 0x6c, Left, High).pos(1), @@ -1036,14 +1036,14 @@ def create_doors(world, player): create_door(player, 'GT Bomb Conveyor SW', Intr).dir(So, 0x3d, Left, High).pos(3), create_door(player, 'GT Crystal Circles NW', Intr).dir(No, 0x3d, Left, High).pos(3), create_door(player, 'GT Crystal Circles SW', Nrml).dir(So, 0x3d, Left, High).small_key().pos(2), - create_door(player, 'GT Left Moldorm Ledge NW', Nrml).dir(No, 0x4d, Left, High).small_key().pos(0), + create_door(player, 'GT Left Moldorm Ledge NW', Nrml).dir(No, 0x4d, Left, High).small_key().pos(0).kill(), create_door(player, 'GT Left Moldorm Ledge Drop Down', Lgcl), create_door(player, 'GT Right Moldorm Ledge Drop Down', Lgcl), create_door(player, 'GT Moldorm Gap', Lgcl), create_door(player, 'GT Moldorm Hole', Hole), create_door(player, 'GT Validation Block Path', Lgcl), create_door(player, 'GT Validation WS', Nrml).dir(We, 0x4d, Bot, High).pos(1), - create_door(player, 'GT Right Moldorm Ledge Down Stairs', Sprl).dir(Dn, 0x4d, 0, HTH).ss(S, 0x12, 0x80), + create_door(player, 'GT Right Moldorm Ledge Down Stairs', Sprl).dir(Dn, 0x4d, 0, HTH).ss(S, 0x12, 0x80).kill(), create_door(player, 'GT Moldorm Pit Up Stairs', Sprl).dir(Up, 0xa6, 0, HTH).ss(S, 0x1b, 0x6c), create_door(player, 'GT Frozen Over ES', Nrml).dir(Ea, 0x4c, Bot, High).no_exit().trap(0x4).pos(0), create_door(player, 'GT Frozen Over Up Stairs', Sprl).dir(Up, 0x4c, 0, HTH).ss(S, 0x1a, 0x6c, True), diff --git a/DungeonGenerator.py b/DungeonGenerator.py index a3d5a961..b5feaf74 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -3,9 +3,13 @@ import collections from collections import defaultdict from enum import Enum, unique import logging +from functools import reduce +import operator as op +from typing import List -from BaseClasses import DoorType, Direction, CrystalBarrier, RegionType, flooded_keys +from BaseClasses import DoorType, Direction, CrystalBarrier, RegionType, Polarity, flooded_keys from Regions import key_only_locations, dungeon_events, flooded_keys_reverse +from Dungeons import dungeon_regions @unique @@ -33,11 +37,13 @@ def generate_dungeon(name, available_sectors, entrance_region_names, split_dunge doors_to_connect = set() all_regions = set() bk_needed = False + bk_special = False for sector in available_sectors: for door in sector.outstanding_doors: doors_to_connect.add(door) all_regions.update(sector.regions) bk_needed = bk_needed or determine_if_bk_needed(sector, split_dungeon, world, player) + bk_special = bk_special or check_for_special(sector) proposed_map = {} choices_master = [[]] depth = 0 @@ -50,9 +56,10 @@ def generate_dungeon(name, available_sectors, entrance_region_names, split_dunge # what are my choices? itr += 1 if itr > 5000: - raise Exception('Generation taking too long. Ref %s' % entrance_region_names[0]) + raise Exception('Generation taking too long. Ref %s' % name) if depth not in dungeon_cache.keys(): - dungeon, hangers, hooks = gen_dungeon_info(name, available_sectors, entrance_regions, proposed_map, doors_to_connect, bk_needed, world, player) + dungeon, hangers, hooks = gen_dungeon_info(name, available_sectors, entrance_regions, proposed_map, + doors_to_connect, bk_needed, bk_special, world, player) dungeon_cache[depth] = dungeon, hangers, hooks valid = check_valid(dungeon, hangers, hooks, proposed_map, doors_to_connect, all_regions, bk_needed) else: @@ -68,7 +75,7 @@ def generate_dungeon(name, available_sectors, entrance_region_names, split_dunge if hanger is None: backtrack = True else: - logger.debug(' '*depth+"%d: Linking %s to %s", depth, hanger.name, hook.name) + logger.debug(' ' * depth + "%d: Linking %s to %s", depth, hanger.name, hook.name) proposed_map[hanger] = hook proposed_map[hook] = hanger last_choice = (hanger, hook) @@ -83,9 +90,9 @@ def generate_dungeon(name, available_sectors, entrance_region_names, split_dunge dungeon_cache.pop(depth, None) depth -= 1 if depth < 0: - raise Exception('Invalid dungeon. Ref %s' % entrance_region_names[0]) + raise Exception('Invalid dungeon. Ref %s' % name) a, b = choices_master[depth][-1] - logger.debug(' '*depth+"%d: Rescinding %s, %s", depth, a.name, b.name) + logger.debug(' ' * depth + "%d: Rescinding %s, %s", depth, a.name, b.name) proposed_map.pop(a, None) proposed_map.pop(b, None) queue = collections.deque(proposed_map.items()) @@ -96,6 +103,8 @@ def generate_dungeon(name, available_sectors, entrance_region_names, split_dunge master_sector = available_sectors.pop() for sub_sector in available_sectors: master_sector.regions.extend(sub_sector.regions) + master_sector.outstanding_doors.clear() + master_sector.r_name_set = None return master_sector @@ -109,38 +118,46 @@ def determine_if_bk_needed(sector, split_dungeon, world, player): return False -def gen_dungeon_info(name, available_sectors, entrance_regions, proposed_map, valid_doors, bk_needed, world, player): +def check_for_special(sector): + return 'Hyrule Dungeon Cellblock' in sector.region_set() + + +def gen_dungeon_info(name, available_sectors, entrance_regions, proposed_map, valid_doors, bk_needed, bk_special, world, player): # step 1 create dungeon: Dict dungeon = {} - original_state = extend_reachable_state_improved(entrance_regions, ExplorationState(dungeon=name), proposed_map, valid_doors, bk_needed, world, player) + start = ExplorationState(dungeon=name) + start.big_key_special = bk_special + original_state = extend_reachable_state_improved(entrance_regions, start, proposed_map, + valid_doors, bk_needed, world, player) dungeon['Origin'] = create_graph_piece_from_state(None, original_state, original_state, proposed_map) - doors_to_connect = set() hanger_set = set() o_state_cache = {} for sector in available_sectors: for door in sector.outstanding_doors: - doors_to_connect.add(door) if not door.stonewall and door not in proposed_map.keys(): hanger_set.add(door) parent = parent_region(door, world, player).parent_region - o_state = extend_reachable_state_improved([parent], ExplorationState(dungeon=name), proposed_map, valid_doors, False, world, player) + init_state = ExplorationState(dungeon=name) + init_state.big_key_special = start.big_key_special + o_state = extend_reachable_state_improved([parent], init_state, proposed_map, + valid_doors, False, world, player) o_state_cache[door.name] = o_state piece = create_graph_piece_from_state(door, o_state, o_state, proposed_map) dungeon[door.name] = piece check_blue_states(hanger_set, dungeon, o_state_cache, proposed_map, valid_doors, world, player) - # catalog hooks: Dict> - # and hangers: Dict> - avail_hooks = defaultdict(set) - hangers = defaultdict(set) + # catalog hooks: Dict> + # and hangers: Dict> + avail_hooks = defaultdict(list) + hangers = defaultdict(list) for key, piece in dungeon.items(): door_hang = piece.hanger_info if door_hang is not None: hanger = hanger_from_door(door_hang) - hangers[hanger].add(door_hang) + hangers[hanger].append(door_hang) for door, crystal in piece.hooks.items(): hook = hook_from_door(door) - avail_hooks[hook].add((door, crystal, door_hang)) + avail_hooks[hook].append((door, crystal, door_hang)) # thin out invalid hanger winnow_hangers(hangers, avail_hooks) @@ -183,28 +200,34 @@ def check_blue_states(hanger_set, dungeon, o_state_cache, proposed_map, valid_do def explore_blue_state(door, dungeon, o_state, proposed_map, valid_doors, world, player): parent = parent_region(door, world, player).parent_region blue_start = ExplorationState(CrystalBarrier.Blue, o_state.dungeon) + blue_start.big_key_special = o_state.big_key_special b_state = extend_reachable_state_improved([parent], blue_start, proposed_map, valid_doors, False, world, player) dungeon[door.name] = create_graph_piece_from_state(door, o_state, b_state, proposed_map) def make_a_choice(dungeon, hangers, avail_hooks, prev_choices): # choose a hanger - all_hooks = set() + all_hooks = {} origin = dungeon['Origin'] for key in avail_hooks.keys(): for hstuff in avail_hooks[key]: - all_hooks.add(hstuff[0]) + all_hooks[hstuff[0]] = None candidate_hangers = [] for key in hangers.keys(): candidate_hangers.extend(hangers[key]) candidate_hangers.sort(key=lambda x: x.name) # sorting to create predictable seeds random.shuffle(candidate_hangers) # randomize if equal preference stage_2_hangers = [] + if len(prev_choices) > 0: + prev_hanger = prev_choices[0][0] + if prev_hanger in candidate_hangers: + stage_2_hangers.append(prev_hanger) + candidate_hangers.remove(prev_hanger) hookable_hangers = collections.deque() queue = collections.deque(candidate_hangers) while len(queue) > 0: c_hang = queue.pop() - if c_hang in all_hooks: + if c_hang in all_hooks.keys(): hookable_hangers.append(c_hang) else: stage_2_hangers.append(c_hang) # prefer hangers that are not hooks @@ -257,7 +280,8 @@ def check_valid(dungeon, hangers, hooks, proposed_map, doors_to_connect, all_reg true_origin_hooks = [x for x in dungeon['Origin'].hooks.keys() if not x.bigKey or possible_bks > 0 or not bk_needed] if len(true_origin_hooks) == 0 and len(proposed_map.keys()) < len(doors_to_connect): return False - if len(true_origin_hooks) == 0 and bk_needed and possible_bks == 0 and len(proposed_map.keys()) == len(doors_to_connect): + if len(true_origin_hooks) == 0 and bk_needed and possible_bks == 0 and len(proposed_map.keys()) == len( + doors_to_connect): return False for key in hangers.keys(): if len(hooks[key]) > 0 and len(hangers[key]) == 0: @@ -353,7 +377,8 @@ def create_graph_piece_from_state(door, o_state, b_state, proposed_map): all_unattached[d] = CrystalBarrier.Null elif all_unattached[d] == CrystalBarrier.Blue and exp_d.crystal == CrystalBarrier.Orange: # the swapping case - logging.getLogger('').warning('Mismatched state @ %s (o:%s b:%s)', d.name, all_unattached[d], exp_d.crystal) + logging.getLogger('').warning('Mismatched state @ %s (o:%s b:%s)', d.name, all_unattached[d], + exp_d.crystal) elif all_unattached[d] == CrystalBarrier.Either: all_unattached[d] = exp_d.crystal # pessimism, and if not this, leave it alone else: @@ -376,7 +401,9 @@ def create_graph_piece_from_state(door, o_state, b_state, proposed_map): def filter_for_potential_bk_locations(locations): - return [x for x in locations if '- Big Chest' not in x.name and '- Prize' not in x.name and x.name not in dungeon_events and x.name not in key_only_locations.keys() and x.name not in ['Agahnim 1', 'Agahnim 2']] + return [x for x in locations if + '- Big Chest' not in x.name and '- Prize' not in x.name and x.name not in dungeon_events + and x.name not in key_only_locations.keys() and x.name not in ['Agahnim 1', 'Agahnim 2']] def parent_region(door, world, player): @@ -576,9 +603,9 @@ class ExplorationState(object): if region.type == RegionType.Dungeon: for location in region.locations: if key_checks and location not in self.found_locations: - if location.name in key_only_locations: + if location.name in key_only_locations and 'Small Key' in location.item.name: self.key_locations += 1 - if location.name not in dungeon_events and '- Prize' not in location.name: + if location.name not in dungeon_events and '- Prize' not in location.name and location.name not in ['Agahnim 1', 'Agahnim 2']: self.ttl_locations += 1 if location not in self.found_locations: self.found_locations.append(location) @@ -587,7 +614,8 @@ class ExplorationState(object): if location.name in dungeon_events and location.name not in self.events: if self.flooded_key_check(location): self.perform_event(location.name, key_region) - if location.name in flooded_keys_reverse.keys() and self.location_found(flooded_keys_reverse[location.name]): + if location.name in flooded_keys_reverse.keys() and self.location_found( + flooded_keys_reverse[location.name]): self.perform_event(flooded_keys_reverse[location.name], key_region) if key_checks and region.name == 'Hyrule Dungeon Cellblock' and not self.big_key_opened: self.big_key_opened = True @@ -625,7 +653,8 @@ class ExplorationState(object): if self.can_traverse(door): if door.dest is None and not self.in_door_list_ic(door, self.unattached_doors): self.append_door_to_list(door, self.unattached_doors) - elif door.req_event is not None and door.req_event not in self.events and not self.in_door_list(door, self.event_doors): + elif door.req_event is not None and door.req_event not in self.events and not self.in_door_list(door, + self.event_doors): self.append_door_to_list(door, self.event_doors) elif not self.in_door_list(door, self.avail_doors): self.append_door_to_list(door, self.avail_doors) @@ -639,7 +668,8 @@ class ExplorationState(object): if self.can_traverse(door): if door.dest is None and not self.in_door_list_ic(door, self.unattached_doors): self.append_door_to_list(door, self.unattached_doors) - elif door.req_event is not None and door.req_event not in self.events and not self.in_door_list(door, self.event_doors): + elif door.req_event is not None and door.req_event not in self.events and not self.in_door_list(door, + self.event_doors): self.append_door_to_list(door, self.event_doors) elif not self.in_door_list(door, self.avail_doors): self.append_door_to_list(door, self.avail_doors) @@ -656,7 +686,8 @@ class ExplorationState(object): other = self.find_door_in_list(door, self.unattached_doors) if self.crystal != other.crystal: other.crystal = CrystalBarrier.Either - elif door.req_event is not None and door.req_event not in self.events and not self.in_door_list(door, self.event_doors): + elif door.req_event is not None and door.req_event not in self.events and not self.in_door_list(door, + self.event_doors): self.append_door_to_list(door, self.event_doors, flag) elif not self.in_door_list(door, self.avail_doors): self.append_door_to_list(door, self.avail_doors, flag) @@ -664,7 +695,8 @@ class ExplorationState(object): def add_all_doors_check_key_region(self, region, key_region, world, player): for door in get_doors(world, region, player): if self.can_traverse(door): - if door.req_event is not None and door.req_event not in self.events and not self.in_door_list(door, self.event_doors): + if door.req_event is not None and door.req_event not in self.events and not self.in_door_list(door, + self.event_doors): self.append_door_to_list(door, self.event_doors) elif not self.in_door_list(door, self.avail_doors): self.append_door_to_list(door, self.avail_doors) @@ -724,7 +756,8 @@ class ExplorationState(object): return cnt def validate(self, door, region, world, player): - return self.can_traverse(door) and not self.visited(region) and valid_region_to_explore(region, self.dungeon, world, player) + return self.can_traverse(door) and not self.visited(region) and valid_region_to_explore(region, self.dungeon, + world, player) def in_door_list(self, door, door_list): for d in door_list: @@ -785,7 +818,8 @@ def extend_reachable_state(search_regions, state, world, player): explorable_door = local_state.next_avail_door() connect_region = world.get_entrance(explorable_door.door.name, player).connected_region if connect_region is not None: - if valid_region_to_explore(connect_region, local_state.dungeon, world, player) and not local_state.visited(connect_region): + if valid_region_to_explore(connect_region, local_state.dungeon, world, player) and not local_state.visited( + connect_region): local_state.visit_region(connect_region) local_state.add_all_doors_check_unattached(connect_region, world, player) return local_state @@ -799,20 +833,28 @@ def extend_reachable_state_improved(search_regions, state, proposed_map, valid_d while len(local_state.avail_doors) > 0: explorable_door = local_state.next_avail_door() if explorable_door.door.bigKey: - if isOrigin and local_state.count_locations_exclude_specials() == 0: - continue # we can't open this door + if isOrigin: + big_not_found = not special_big_key_found(local_state, world, player) if local_state.big_key_special else local_state.count_locations_exclude_specials() == 0 + if big_not_found: + continue # we can't open this door if explorable_door.door in proposed_map: connect_region = world.get_entrance(proposed_map[explorable_door.door].name, player).parent_region else: connect_region = world.get_entrance(explorable_door.door.name, player).connected_region if connect_region is not None: - if valid_region_to_explore(connect_region, local_state.dungeon, world, player) and not local_state.visited(connect_region): + if valid_region_to_explore(connect_region, local_state.dungeon, world, player) and not local_state.visited( + connect_region): flag = explorable_door.flag or explorable_door.door.bigKey local_state.visit_region(connect_region, bk_Flag=flag) local_state.add_all_doors_check_proposed(connect_region, proposed_map, valid_doors, flag, world, player) return local_state +def special_big_key_found(state, world, player): + cellblock = world.get_region('Hyrule Dungeon Cellblock', player) + return state.visited(cellblock) + + # cross-utility methods def valid_region_to_explore(region, name, world, player): if region is None: @@ -852,3 +894,713 @@ def convert_regions(region_names, world, player): for name in region_names: region_list.append(world.get_region(name, player)) return region_list + + +# Begin crossed mode sector shuffle + +class DungeonBuilder(object): + + def __init__(self, name): + self.name = name + self.sectors = [] + self.location_cnt = 0 + self.key_drop_cnt = 0 + self.bk_required = False + self.bk_provided = False + self.c_switch_required = False + self.c_switch_present = False + self.dead_ends = 0 + self.branches = 0 + self.mag_needed = [False, False, False] + self.unfulfilled = defaultdict(int) + self.all_entrances = None # used for sector segration/branching + self.entrance_list = None # used for overworld accessibility + self.layout_starts = None # used for overworld accessibility + self.master_sector = None + self.path_entrances = None # used for pathing/key doors, I think + + self.candidates = None + self.key_doors_num = None + self.combo_size = None + self.flex = 0 + + def polarity_complement(self): + pol = Polarity() + for sector in self.sectors: + pol += sector.polarity() + return pol.complement() + + def polarity(self): + pol = Polarity() + for sector in self.sectors: + pol += sector.polarity() + return pol + + +def simple_dungeon_builder(name, sector_list, world, player): + define_sector_features(sector_list, world, player) + builder = DungeonBuilder(name) + dummy_pool = dict.fromkeys(sector_list) + for sector in sector_list: + assign_sector(sector, builder, dummy_pool) + return builder + + +def create_dungeon_builders(all_sectors, world, player, dungeon_entrances=None): + logger = logging.getLogger('') + logger.info('Shuffling Dungeon Sectors') + if dungeon_entrances is None: + dungeon_entrances = default_dungeon_entrances + define_sector_features(all_sectors, world, player) + candidate_sectors = dict.fromkeys(all_sectors) + + dungeon_map = {} + for key in dungeon_regions.keys(): + dungeon_map[key] = DungeonBuilder(key) + for key in dungeon_boss_sectors.keys(): + current_dungeon = dungeon_map[key] + for r_name in dungeon_boss_sectors[key]: + assign_sector(find_sector(r_name, candidate_sectors), current_dungeon, candidate_sectors) + if key == 'Hyrule Castle' and world.mode == 'standard': + for r_name in ['Hyrule Dungeon Cellblock', 'Sanctuary']: # need to deliver zelda + assign_sector(find_sector(r_name, candidate_sectors), current_dungeon, candidate_sectors) + for key in dungeon_entrances.keys(): + current_dungeon = dungeon_map[key] + current_dungeon.all_entrances = dungeon_entrances[key] + for r_name in current_dungeon.all_entrances: + assign_sector(find_sector(r_name, candidate_sectors), current_dungeon, candidate_sectors) + # categorize sectors + free_location_sectors = {} + crystal_switches = {} + crystal_barriers = {} + polarized_sectors = {} + neutral_sectors = {} + for sector in candidate_sectors: + if sector.chest_locations > 0: + free_location_sectors[sector] = None + elif sector.c_switch: + crystal_switches[sector] = None + elif sector.blue_barrier: + crystal_barriers[sector] = None + elif sector.polarity().is_neutral(): + neutral_sectors[sector] = None + else: + polarized_sectors[sector] = None + logger.info('-Assigning Chest Locations') + assign_location_sectors(dungeon_map, free_location_sectors) + logger.info('-Assigning Crystal Switches and Barriers') + leftover = assign_crystal_switch_sectors(dungeon_map, crystal_switches) + for sector in leftover: + if sector.polarity().is_neutral(): + neutral_sectors[sector] = None + else: + polarized_sectors[sector] = None + # blue barriers + assign_crystal_barrier_sectors(dungeon_map, crystal_barriers) + # polarity: + logger.info('-Balancing Doors') + assign_polarized_sectors(dungeon_map, polarized_sectors, logger) + # the rest + assign_the_rest(dungeon_map, neutral_sectors) + return dungeon_map + + +def define_sector_features(sectors, world, player): + for sector in sectors: + if 'Hyrule Dungeon Cellblock' in sector.region_set(): + sector.bk_provided = True + if 'Thieves Blind\'s Cell' in sector.region_set(): + sector.bk_required = True + for region in sector.regions: + for loc in region.locations: + if '- Prize' in loc.name or loc.name in ['Agahnim 1', 'Agahnim 2', 'Hyrule Castle - Big Key Drop']: + pass + elif loc.event and 'Small Key' in loc.item.name: + sector.key_only_locations += 1 + elif loc.name not in dungeon_events: + sector.chest_locations += 1 + if '- Big Chest' in loc.name: + sector.bk_required = True + for ext in region.exits: + door = world.check_for_door(ext.name, player) + if door is not None: + if door.crystal == CrystalBarrier.Either: + sector.c_switch = True + elif door.crystal == CrystalBarrier.Orange: + sector.orange_barrier = True + elif door.crystal == CrystalBarrier.Blue: + sector.blue_barrier = True + if door.bigKey: + sector.bk_required = True + + +def assign_sector(sector, dungeon, candidate_sectors): + if sector is not None: + del candidate_sectors[sector] + dungeon.sectors.append(sector) + dungeon.location_cnt += sector.chest_locations + dungeon.key_drop_cnt += sector.key_only_locations + if sector.c_switch: + dungeon.c_switch_present = True + if sector.blue_barrier: + dungeon.c_switch_required = True + if sector.bk_required: + dungeon.bk_required = True + if sector.bk_provided: + dungeon.bk_provided = True + if sector.outflow() == 1: + dungeon.dead_ends += 1 + if sector.outflow() > 2: + dungeon.branches += sector.outflow() - 2 + + +def find_sector(r_name, sectors): + for s in sectors: + if r_name in s.region_set(): + return s + return None + + +def assign_location_sectors(dungeon_map, free_location_sectors): + valid = False + choices = None + sector_list = list(free_location_sectors) + random.shuffle(sector_list) + while not valid: + choices, d_idx, totals = weighted_random_locations(dungeon_map, sector_list) + for i, sector in enumerate(sector_list): + choice = d_idx[choices[i].name] + totals[choice] += sector.chest_locations + valid = True + for d_name, idx in d_idx.items(): + if totals[idx] < minimal_locations(d_name): + valid = False + break + for i, choice in enumerate(choices): + builder = dungeon_map[choice.name] + assign_sector(sector_list[i], builder, free_location_sectors) + + +def weighted_random_locations(dungeon_map, free_location_sectors): + population = [] + ttl_assigned = 0 + weights = [] + totals = [] + d_idx = {} + for i, dungeon_builder in enumerate(dungeon_map.values()): + population.append(dungeon_builder) + totals.append(dungeon_builder.location_cnt) + ttl_assigned += dungeon_builder.location_cnt + weights.append(6.375) + d_idx[dungeon_builder.name] = i + average = ttl_assigned / 13 + for i, db in enumerate(population): + if db.location_cnt < average: + weights[i] += average - db.location_cnt + if db.location_cnt > average: + weights[i] = max(0, weights[i] - db.location_cnt + average) + + choices = random.choices(population, weights, k=len(free_location_sectors)) + return choices, d_idx, totals + + +def minimal_locations(dungeon_name): + if dungeon_name == 'Hyrule Castle': + return 3 + if dungeon_name == 'Agahnims Tower': + return 2 # bump to 3 once compasses work + # reduce gt to 4 once compasses work + return 5 + + +def assign_crystal_switch_sectors(dungeon_map, crystal_switches, assign_one=False): + population = [] + some_c_switches_present = False + for name, builder in dungeon_map.items(): + if builder.c_switch_required and not builder.c_switch_present: + population.append(name) + if builder.c_switch_present: + some_c_switches_present = True + if len(population) == 0: # nothing needs a switch + if assign_one and not some_c_switches_present: # something should have one + choice = random.choice(list(dungeon_map.keys())) + builder = dungeon_map[choice] + assign_sector(random.choice(list(crystal_switches)), builder, crystal_switches) + return crystal_switches + sector_list = list(crystal_switches) + choices = random.choices(sector_list, k=len(population)) + for i, choice in enumerate(choices): + builder = dungeon_map[population[i]] + assign_sector(choice, builder, crystal_switches) + return crystal_switches + + +def assign_crystal_barrier_sectors(dungeon_map, crystal_barriers): + population = [] + for name, builder in dungeon_map.items(): + if builder.c_switch_present: + population.append(name) + sector_list = list(crystal_barriers) + random.shuffle(sector_list) + choices = random.choices(population, k=len(sector_list)) + for i, choice in enumerate(choices): + builder = dungeon_map[choice] + assign_sector(sector_list[i], builder, crystal_barriers) + + +def identify_polarity_issues(dungeon_map): + unconnected_builders = {} + for name, builder in dungeon_map.items(): + if len(builder.sectors) == 1: + continue + if len(builder.sectors) == 2: + def sector_filter(x, y): + return x != y + else: + def sector_filter(x, y): + return x != y and x.outflow() > 1 + for sector in builder.sectors: + others = [x for x in builder.sectors if sector_filter(x, sector)] + other_mag = sum_magnitude(others) + sector_mag = sector.magnitude() + for i in range(len(sector_mag)): + if sector_mag[i] > 0 and other_mag[i] == 0: + builder.mag_needed[i] = True + if name not in unconnected_builders.keys(): + unconnected_builders[name] = builder + return unconnected_builders + + +def identify_branching_issues(dungeon_map): + unconnected_builders = {} + for name, builder in dungeon_map.items(): + unsatisfied_doors = defaultdict(list) + satisfying_doors = defaultdict(list) + entrance_doors = defaultdict(list) + multi_purpose = defaultdict(list) + for sector in builder.sectors: + is_entrance = is_entrance_sector(builder, sector) + if is_entrance: + for door in sector.outstanding_doors: + dependent_doors = [x for x in sector.outstanding_doors if x != door] + if not door.blocked: + entrance_doors[hook_from_door(door)].append((door, dependent_doors)) + else: + unsatisfied_doors[hook_from_door(door)].append((door, dependent_doors)) + else: + outflow = sector.outflow() + outflow -= len([x for x in sector.outstanding_doors if x.dead]) + other_doors = [] + one_way_flag = False + for door in sector.outstanding_doors: + dependent_doors = [x for x in sector.outstanding_doors if x != door] + if door.blocked or door.dead or (outflow <= 1 and len(dependent_doors) == 0): + unsatisfied_doors[hook_from_door(door)].append((door, dependent_doors)) + one_way_flag = True + else: + other_doors.append((door, dependent_doors)) + if not one_way_flag and outflow >= 2: + for door, deps in other_doors: + multi_purpose[hook_from_door(door)].append((door, deps)) + elif one_way_flag or outflow <= 1: + for door, deps in other_doors: + satisfying_doors[hook_from_door(door)].append((door, deps)) + used_doors = set() + satisfied = is_satisfied([unsatisfied_doors, entrance_doors, satisfying_doors, multi_purpose]) + while not satisfied: + candidate_is_unsated = True + candidate, dep_list = choose_candidate([unsatisfied_doors]) + if candidate is None: + candidate_is_unsated = False + candidate, dep_list = choose_candidate([multi_purpose, satisfying_doors, entrance_doors]) # consider satifying doors here? + match_list = [satisfying_doors, multi_purpose, entrance_doors] + match_maker, match_deps = find_candidate_match(candidate, dep_list, candidate_is_unsated, match_list) + if match_maker is None: + unconnected_builders[name] = builder + builder.unfulfilled[hook_from_door(candidate)] += 1 + for hook, door_list in unsatisfied_doors.items(): + builder.unfulfilled[hook] += len(door_list) + satisfied = True + continue + used_doors.add(candidate) + used_doors.add(match_maker) + if candidate_is_unsated and len(match_deps) == 1: + for door in match_deps: + door_list = multi_purpose[hook_from_door(door)] + pair = find_door_in_list(door, door_list) + if pair[0] is not None: + door_list.remove(pair) + unsatisfied_doors[hook_from_door(door)].append((pair)) + satisfied = is_satisfied([unsatisfied_doors, entrance_doors, satisfying_doors, multi_purpose]) + return unconnected_builders + + +def is_entrance_sector(builder, sector): + for entrance in builder.all_entrances: + r_set = sector.region_set() + if entrance in r_set: + return True + return False + + +def is_satisfied(door_dict_list): + for door_dict in door_dict_list: + for door_list in door_dict.values(): + if len(door_list) > 0: + return False + return True + + +def choose_candidate(door_dict_list): + for door_dict in door_dict_list: + min_len = None + candidate_list = None + for dir, door_list in door_dict.items(): + curr_len = len(door_list) + if curr_len > 0 and (min_len is None or curr_len < min_len): + candidate_list = door_list + min_len = curr_len + if min_len is not None: + candidate, dep_list = candidate_list.pop() + return candidate, dep_list + return None, None + + +def find_candidate_match(candidate, dep_list, check_deps, door_dict_list): + dir = hanger_from_door(candidate) + backup_pair = None + backup_list = None + for door_dict in door_dict_list: + door_list = door_dict[dir] + pair = None + for match, match_deps in door_list: + if not check_deps or match not in dep_list: + pair = match, match_deps + break + elif len(filter_match_deps(candidate, match_deps)) > 0: + backup_pair = match, match_deps + backup_list = door_list + if pair is not None: + door_list.remove(pair) + return pair + if backup_pair is not None: + backup_list.remove(backup_pair) + logging.getLogger('').debug('Matching %s to %s unsure if safe', candidate, backup_pair[0]) + return backup_pair + return None, None + + +def find_door_in_list(door, door_list): + for d, deps in door_list: + if d == door: + return d, deps + return None, None + + +# todo: maybe filter by used doors too +# todo: I want the number of door that match is accessible by still +def filter_match_deps(candidate, match_deps): + return [x for x in match_deps if x != candidate] + + +def sum_magnitude(sector_list): + result = [0, 0, 0] + for sector in sector_list: + vector = sector.magnitude() + for i in range(len(result)): + result[i] = result[i] + vector[i] + return result + + +def sum_polarity(sector_list): + pol = Polarity() + for sector in sector_list: + pol += sector.polarity() + return pol + + +def assign_polarized_sectors(dungeon_map, polarized_sectors, logger): + # step 1: fix polarity connection issues + logger.info('--Basic Traversal') + unconnected_builders = identify_polarity_issues(dungeon_map) + while len(unconnected_builders) > 0: + for name, builder in unconnected_builders.items(): + candidates = find_connection_candidates(builder.mag_needed, polarized_sectors) + if len(candidates) == 0: + raise Exception('Cross Dungeon Builder: Cannot find a candidate for connectedness - restart?') + sector = random.choice(candidates) + assign_sector(sector, builder, polarized_sectors) + builder.mag_needed = [False, False, False] + unconnected_builders = identify_polarity_issues(dungeon_map) + + # step 2: fix neutrality issues + builder_order = list(dungeon_map.values()) + random.shuffle(builder_order) + for builder in builder_order: + logger.info('--Balancing %s', builder.name) + while not builder.polarity().is_neutral(): + candidates = find_neutralizing_candidates(builder.polarity(), polarized_sectors) + sectors = random.choice(candidates) + for sector in sectors: + assign_sector(sector, builder, polarized_sectors) + + # step 3: fix dead ends + problem_builders = identify_branching_issues(dungeon_map) + neutral_choices: List[List] = neutralize_the_rest(polarized_sectors) + while len(problem_builders) > 0: + for name, builder in problem_builders.items(): + candidates = find_branching_candidates(builder, neutral_choices) + choice = random.choice(candidates) + if valid_polarized_assignment(builder, choice): + neutral_choices.remove(choice) + for sector in choice: + assign_sector(sector, builder, polarized_sectors) + builder.unfulfilled.clear() + problem_builders = identify_branching_issues(dungeon_map) + + # step 4: assign randomly until gone - must maintain connectedness, neutral polarity + while len(polarized_sectors) > 0: + choices = random.choices(list(dungeon_map.keys()), k=len(neutral_choices)) + for i, choice in enumerate(choices): + builder = dungeon_map[choice] + if valid_polarized_assignment(builder, neutral_choices[i]): + for sector in neutral_choices[i]: + assign_sector(sector, builder, polarized_sectors) + + +def find_connection_candidates(mag_needed, sector_pool): + candidates = [] + for sector in sector_pool: + if sector.outflow() < 2: + continue + mag = sector.magnitude() + matches = False + for i, need in enumerate(mag_needed): + if need and mag[i] > 0: + matches = True + break + if matches: + candidates.append(sector) + return candidates + + +def find_neutralizing_candidates(polarity, sector_pool): + candidates = defaultdict(list) + original_charge = polarity.charge() + best_charge = original_charge + main_pool = list(sector_pool) + last_r = 0 + while len(candidates) == 0: + r_range = range(last_r + 1, last_r + 3) + for r in r_range: + if r > len(main_pool): + if len(candidates) == 0: + raise Exception('Cross Dungeon Builder: No possible neutralizers left') + else: + continue + last_r = r + combinations = ncr(len(main_pool), r) + for i in range(0, combinations): + choice = kth_combination(i, main_pool, r) + p_charge = (polarity + sum_polarity(choice)).charge() + if p_charge < original_charge and p_charge <= best_charge: + candidates[p_charge].append(choice) + if p_charge < best_charge: + best_charge = p_charge + candidate_list = candidates[best_charge] + best_len = 10 + official_cand = [] + for cand in candidate_list: + size = len(cand) + if size < best_len: + best_len = size + official_cand = [cand] + elif size == best_len: + official_cand.append(cand) + return official_cand + + +def find_branching_candidates(builder, neutral_choices): + candidates = [] + for choice in neutral_choices: + door_match = False + flow_match = False + for sector in choice: + if sector.adj_outflow() >= 2: + flow_match = True + for door in sector.outstanding_doors: + if builder.unfulfilled[hanger_from_door(door)] > 0: + door_match = True + if door_match and flow_match: + candidates.append(choice) + if len(candidates) == 0: + raise Exception('Cross Dungeon Builder: No more branching candidates!') + return candidates + + +def neutralize_the_rest(sector_pool): + neutral_choices = [] + main_pool = list(sector_pool) + failed_pool = [] + r_size = 1 + while len(main_pool) > 0 or len(failed_pool) > 0: + if len(main_pool) <= r_size: + main_pool.extend(failed_pool) + failed_pool.clear() + r_size += 1 + candidate = random.choice(main_pool) + main_pool.remove(candidate) + if r_size > len(main_pool): + raise Exception("Cross Dungeon Builder: no more neutral pairings possible") + combinations = ncr(len(main_pool), r_size) + itr = 0 + done = False + while not done: + ttl_polarity = candidate.polarity() + choice_set = kth_combination(itr, main_pool, r_size) + for choice in choice_set: + ttl_polarity += choice.polarity() + if ttl_polarity.is_neutral(): + choice_set.append(candidate) + neutral_choices.append(choice_set) + main_pool = [x for x in main_pool if x not in choice_set] + failed_pool = [x for x in failed_pool if x not in choice_set] + done = True + else: + itr += 1 + if itr >= combinations: + failed_pool.append(candidate) + done = True + return neutral_choices + + +def valid_polarized_assignment(builder, sector_list): + full_list = sector_list + builder.sectors + for sector in full_list: + others = [x for x in full_list if x != sector] + other_mag = sum_magnitude(others) + sector_mag = sector.magnitude() + for i in range(len(sector_mag)): + if sector_mag[i] > 0 and other_mag[i] == 0: + return True + # dead_ends = 0 + # branches = 0 + # for sector in sector_list: + # if sector.outflow == 1: + # dead_ends += 1 + # if sector.outflow() > 2: + # branches += sector.outflow() - 2 + # if builder.dead_ends + dead_ends > builder.branches + branches: + # return False + return (sum_polarity(sector_list) + sum_polarity(builder.sectors)).is_neutral() + + +def assign_the_rest(dungeon_map, neutral_sectors): + while len(neutral_sectors) > 0: + sector_list = list(neutral_sectors) + choices = random.choices(list(dungeon_map.keys()), k=len(sector_list)) + for i, choice in enumerate(choices): + builder = dungeon_map[choice] + if valid_polarized_assignment(builder, [sector_list[i]]): + assign_sector(sector_list[i], builder, neutral_sectors) + + +def split_dungeon_builder(builder, split_list): + logger = logging.getLogger('') + logger.info('Splitting Up Desert/Skull') + candidate_sectors = dict.fromkeys(builder.sectors) + + dungeon_map = {} + for name, split_entrances in split_list.items(): + key = builder.name + ' ' + name + dungeon_map[key] = sub_builder = DungeonBuilder(key) + sub_builder.all_entrances = split_entrances + for r_name in split_entrances: + assign_sector(find_sector(r_name, candidate_sectors), sub_builder, candidate_sectors) + + # categorize sectors + crystal_switches = {} + crystal_barriers = {} + polarized_sectors = {} + neutral_sectors = {} + for sector in candidate_sectors: + if sector.c_switch: + crystal_switches[sector] = None + elif sector.blue_barrier: + crystal_barriers[sector] = None + elif sector.polarity().is_neutral(): + neutral_sectors[sector] = None + else: + polarized_sectors[sector] = None + leftover = assign_crystal_switch_sectors(dungeon_map, crystal_switches, len(crystal_barriers) > 0) + for sector in leftover: + if sector.polarity().is_neutral(): + neutral_sectors[sector] = None + else: + polarized_sectors[sector] = None + # blue barriers + assign_crystal_barrier_sectors(dungeon_map, crystal_barriers) + # polarity: + logger.info('-Re-balancing Desert/Skull') + assign_polarized_sectors(dungeon_map, polarized_sectors, logger) + # the rest + assign_the_rest(dungeon_map, neutral_sectors) + return dungeon_map + + +# common functions - todo: move to a common place +def kth_combination(k, l, r): + if r == 0: + return [] + elif len(l) == r: + return l + else: + i = ncr(len(l) - 1, r - 1) + if k < i: + return l[0:1] + kth_combination(k, l[1:], r - 1) + else: + return kth_combination(k - i, l[1:], r) + + +def ncr(n, r): + if r == 0: + return 1 + r = min(r, n - r) + numerator = reduce(op.mul, range(n, n - r, -1), 1) + denominator = reduce(op.mul, range(1, r + 1), 1) + return int(numerator / denominator) + + +dungeon_boss_sectors = { + 'Hyrule Castle': [], + 'Eastern Palace': ['Eastern Boss'], + 'Desert Palace': ['Desert Boss'], + 'Tower of Hera': ['Hera Boss'], + 'Agahnims Tower': ['Tower Agahnim 1'], + 'Palace of Darkness': ['PoD Boss'], + 'Swamp Palace': ['Swamp Boss'], + 'Skull Woods': ['Skull Boss'], + 'Thieves Town': ['Thieves Attic Window', 'Thieves Blind\'s Cell', 'Thieves Boss'], + 'Ice Palace': ['Ice Boss'], + 'Misery Mire': ['Mire Boss'], + 'Turtle Rock': ['TR Boss'], + 'Ganons Tower': ['GT Agahnim 2'] +} + +default_dungeon_entrances = { + 'Hyrule Castle': ['Hyrule Castle Lobby', 'Hyrule Castle West Lobby', 'Hyrule Castle East Lobby', 'Sewers Rat Path', + 'Sanctuary'], + 'Eastern Palace': ['Eastern Lobby'], + 'Desert Palace': ['Desert Back Lobby', 'Desert Main Lobby', 'Desert West Lobby', 'Desert East Lobby'], + 'Tower of Hera': ['Hera Lobby'], + 'Agahnims Tower': ['Tower Lobby'], + 'Palace of Darkness': ['PoD Lobby'], + 'Swamp Palace': ['Swamp Lobby'], + 'Skull Woods': ['Skull 1 Lobby', 'Skull Pinball', 'Skull Left Drop', 'Skull Pot Circle', 'Skull 2 East Lobby', + 'Skull 2 West Lobby', 'Skull Back Drop', 'Skull 3 Lobby'], + 'Thieves Town': ['Thieves Lobby'], + 'Ice Palace': ['Ice Lobby'], + 'Misery Mire': ['Mire Lobby'], + 'Turtle Rock': ['TR Main Lobby', 'TR Eye Bridge', 'TR Big Chest Entrance', 'TR Lazy Eyes'], + 'Ganons Tower': ['GT Lobby'] +} diff --git a/Dungeons.py b/Dungeons.py index bccdb98a..c904a6b8 100644 --- a/Dungeons.py +++ b/Dungeons.py @@ -333,15 +333,15 @@ region_starts = { } split_region_starts = { - 'Desert Palace': [ - ['Desert Back Lobby'], - ['Desert Main Lobby', 'Desert West Lobby', 'Desert East Lobby'] - ], - 'Skull Woods': [ - ['Skull 1 Lobby', 'Skull Pot Circle'], - ['Skull 2 West Lobby', 'Skull 2 East Lobby', 'Skull Back Drop'], - ['Skull 3 Lobby'] - ] + 'Desert Palace': { + 'Back': ['Desert Back Lobby'], + 'Main': ['Desert Main Lobby', 'Desert West Lobby', 'Desert East Lobby'] + }, + 'Skull Woods': { + '1': ['Skull 1 Lobby', 'Skull Pot Circle'], + '2': ['Skull 2 West Lobby', 'Skull 2 East Lobby', 'Skull Back Drop'], + '3': ['Skull 3 Lobby'] + } } flexible_starts = { diff --git a/Items.py b/Items.py index 8d3fa104..81ca6b14 100644 --- a/Items.py +++ b/Items.py @@ -127,6 +127,7 @@ item_table = {'Bow': (True, False, None, 0x0B, 'You have\nchosen the\narcher cla 'Compass (Escape)': (False, True, 'Compass', 0x8F, 'Now you can find no boss!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Hyrule Castle'), 'Map (Escape)': (False, True, 'Map', 0x7F, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Hyrule Castle'), 'Small Key (Agahnims Tower)': (False, False, 'SmallKey', 0xA4, 'A small key to Agahnim', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Castle Tower'), + 'Big Key (Agahnims Tower)': (False, False, 'BigKey', 0x9B, 'A big key to Agahnim', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Castle Tower'), 'Small Key (Palace of Darkness)': (False, False, 'SmallKey', 0xA6, 'A small key to darkness', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Palace of Darkness'), 'Big Key (Palace of Darkness)': (False, False, 'BigKey', 0x99, 'A big key to darkness', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Palace of Darkness'), 'Compass (Palace of Darkness)': (False, True, 'Compass', 0x89, 'Now you can find Helmasaur King!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Palace of Darkness'), diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index 7aa76c4b..2f31f308 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -6,37 +6,6 @@ from Dungeons import dungeon_keys, dungeon_bigs from DungeonGenerator import ExplorationState -class KeySphere(object): - - def __init__(self): - self.access_door = None - self.free_locations = {} - self.prize_region = False - self.key_only_locations = {} - self.child_doors = {} - self.bk_locked = False - self.parent_sphere = None - self.other_locations = {} - - def __eq__(self, other): - if self.prize_region != other.prize_region: - return False - # already have merge function for this - # if self.bk_locked != other.bk_locked: - # return False - if len(self.free_locations) != len(other.free_locations): - return False - if len(self.key_only_locations) != len(other.key_only_locations): - return False - if len(set(self.free_locations).symmetric_difference(set(other.free_locations))) > 0: - return False - if len(set(self.key_only_locations).symmetric_difference(set(other.key_only_locations))) > 0: - return False - if len(set(self.child_doors).symmetric_difference(set(other.child_doors))) > 0: - return False - return True - - class KeyLayout(object): def __init__(self, sector, starts, proposal): @@ -45,20 +14,21 @@ class KeyLayout(object): self.proposal = proposal self.key_logic = KeyLogic(sector.name) - self.key_spheres = None self.key_counters = None self.flat_prop = None self.max_chests = None self.max_drops = None self.all_chest_locations = {} + self.big_key_special = False # bk special? # bk required? True if big chests or big doors exists - def reset(self, proposal): + def reset(self, proposal, builder, world, player): self.proposal = proposal self.flat_prop = flatten_pair_list(self.proposal) self.key_logic = KeyLogic(self.sector.name) + self.max_chests = calc_max_chests(builder, self, world, player) class KeyLogic(object): @@ -69,6 +39,8 @@ class KeyLogic(object): self.sm_restricted = set() self.small_key_name = dungeon_keys[dungeon_name] self.bk_name = dungeon_bigs[dungeon_name] + self.bk_doors = set() + self.bk_chests = set() self.logic_min = {} self.logic_max = {} @@ -96,34 +68,7 @@ class KeyCounter(object): self.used_keys = 0 self.big_key_opened = False self.important_location = False - - def update(self, key_sphere): - self.free_locations.update(key_sphere.free_locations) - self.key_only_locations.update(key_sphere.key_only_locations) - self.child_doors.update(dict.fromkeys([x for x in key_sphere.child_doors if x not in self.open_doors and x.dest not in self.open_doors])) - self.important_location = self.important_location or key_sphere.prize_region or self.special_region(key_sphere) - - @staticmethod - def special_region(key_sphere): - for other in key_sphere.other_locations: - # todo: zelda's cell is special in standard, and probably crossed too - if other.name in ['Attic Cracked Floor', 'Suspicious Maiden']: - return True - return False - - def open_door(self, door, flat_proposal): - if door in flat_proposal: - self.used_keys += 1 - del self.child_doors[door] - self.open_doors[door] = None - if door.dest in flat_proposal: - self.open_doors[door.dest] = None - if door.dest in self.child_doors: - del self.child_doors[door.dest] - elif door.bigKey: - self.big_key_opened = True - del self.child_doors[door] - self.open_doors[door] = None + self.other_locations = {} def used_smalls_loc(self, reserve=0): return max(self.used_keys + reserve - len(self.key_only_locations), 0) @@ -140,29 +85,35 @@ class KeyCounter(object): return ret -def build_key_layout(sector, start_regions, proposal, world, player): - key_layout = KeyLayout(sector, start_regions, proposal) +def build_key_layout(builder, start_regions, proposal, world, player): + key_layout = KeyLayout(builder.master_sector, start_regions, proposal) key_layout.flat_prop = flatten_pair_list(key_layout.proposal) - key_layout.max_chests = len(world.get_dungeon(key_layout.sector.name, player).small_keys) key_layout.max_drops = count_key_drops(key_layout.sector) + key_layout.max_chests = calc_max_chests(builder, key_layout, world, player) + key_layout.big_key_special = 'Hyrule Dungeon Cellblock' in key_layout.sector.region_set() return key_layout +def calc_max_chests(builder, key_layout, world, player): + if world.doorShuffle != 'crossed': + return len(world.get_dungeon(key_layout.sector.name, player).small_keys) + return builder.key_doors_num - key_layout.max_drops + + def analyze_dungeon(key_layout, world, player): key_layout.key_counters = create_key_counters(key_layout, world, player) - key_layout.key_spheres = create_key_spheres(key_layout, world, player) key_logic = key_layout.key_logic find_bk_locked_sections(key_layout, world) + key_logic.bk_chests.update(find_big_chest_locations(key_layout.all_chest_locations)) - init_bk = check_special_locations(key_layout.key_spheres['Origin'].free_locations.keys()) - key_counter = key_layout.key_counters[counter_id({}, init_bk, key_layout.flat_prop)] - queue = collections.deque([(key_layout.key_spheres['Origin'], key_counter)]) + original_key_counter = find_counter({}, False, key_layout) + queue = collections.deque([(None, original_key_counter)]) doors_completed = set() while len(queue) > 0: queue = collections.deque(sorted(queue, key=queue_sorter)) - key_sphere, key_counter = queue.popleft() + parent_door, key_counter = queue.popleft() chest_keys = available_chest_small_keys(key_counter, world) raw_avail = chest_keys + len(key_counter.key_only_locations) available = raw_avail - key_counter.used_keys @@ -170,29 +121,28 @@ def analyze_dungeon(key_layout, world, player): if not key_counter.big_key_opened: if chest_keys == count_locations_big_optional(key_counter.free_locations) and available <= possible_smalls: key_logic.bk_restricted.update(filter_big_chest(key_counter.free_locations)) - if not key_sphere.bk_locked and big_chest_in_locations(key_counter.free_locations): + if not key_counter.big_key_opened and big_chest_in_locations(key_counter.free_locations): key_logic.sm_restricted.update(find_big_chest_locations(key_counter.free_locations)) # todo: detect forced subsequent keys - see keypuzzles # try to relax the rules here? - smallest requirement that doesn't force a softlock child_queue = collections.deque() - for child in sorted(list(key_sphere.child_doors), key=lambda x: x.name): - next_sphere = key_layout.key_spheres[child.name] - # todo: empty_sphere are not always empty, Mire spike barrier is not empty if other doors open first - if not empty_sphere(next_sphere) and child not in doors_completed: - child_queue.append((child, next_sphere)) + for child in key_counter.child_doors.keys(): + if not child.bigKey or not key_layout.big_key_special or key_counter.big_key_opened: + odd_counter = create_odd_key_counter(child, key_counter, key_layout, world) + if not empty_counter(odd_counter) and child not in doors_completed: + child_queue.append((child, odd_counter)) while len(child_queue) > 0: - child, next_sphere = child_queue.popleft() + child, odd_counter = child_queue.popleft() if not child.bigKey: - best_counter = find_best_counter(child, key_counter, key_layout, world, False) + best_counter = find_best_counter(child, odd_counter, key_counter, key_layout, world, False) rule = create_rule(best_counter, key_counter, key_layout, world) - check_for_self_lock_key(rule, next_sphere, key_layout, world) - bk_restricted_rules(rule, next_sphere, key_counter, key_layout, world) + check_for_self_lock_key(rule, child, best_counter, key_layout, world) + bk_restricted_rules(rule, child, odd_counter, key_counter, key_layout, world) key_logic.door_rules[child.name] = rule - doors_completed.add(next_sphere.access_door) - next_counter = find_next_counter(child, key_counter, next_sphere, key_layout) - queue.append((next_sphere, next_counter)) - check_rules(key_layout) - return key_layout + doors_completed.add(child) + next_counter = find_next_counter(child, key_counter, key_layout) + queue.append((child, next_counter)) + check_rules(original_key_counter, key_layout) def count_key_drops(sector): @@ -205,43 +155,42 @@ def count_key_drops(sector): def queue_sorter(queue_item): - sphere, counter = queue_item - if sphere.access_door is None: + door, counter = queue_item + if door is None: return 0 - return 1 if sphere.access_door.bigKey else 0 + return 1 if door.bigKey else 0 def find_bk_locked_sections(key_layout, world): - key_spheres = key_layout.key_spheres + key_counters = key_layout.key_counters key_logic = key_layout.key_logic bk_key_not_required = set() big_chest_allowed_big_key = world.accessibility != 'locations' - for key in key_spheres.keys(): - sphere = key_spheres[key] - key_layout.all_chest_locations.update(sphere.free_locations) - if sphere.bk_locked and (sphere.prize_region or KeyCounter.special_region(sphere)): + for counter in key_counters.values(): + key_layout.all_chest_locations.update(counter.free_locations) + if counter.big_key_opened and counter.important_location: big_chest_allowed_big_key = False - if not sphere.bk_locked: - bk_key_not_required.update(sphere.free_locations) + if not counter.big_key_opened: + bk_key_not_required.update(counter.free_locations) key_logic.bk_restricted.update(dict.fromkeys(set(key_layout.all_chest_locations).difference(bk_key_not_required))) if not big_chest_allowed_big_key: key_logic.bk_restricted.update(find_big_chest_locations(key_layout.all_chest_locations)) -def empty_sphere(sphere): - if len(sphere.key_only_locations) != 0 or len(sphere.free_locations) != 0 or len(sphere.child_doors) != 0: +def empty_counter(counter): + if len(counter.key_only_locations) != 0 or len(counter.free_locations) != 0 or len(counter.child_doors) != 0: return False - return not sphere.prize_region + return not counter.important_location -def relative_empty_sphere(sphere, key_counter): - if len(set(sphere.key_only_locations).difference(key_counter.key_only_locations)) > 0: +def relative_empty_counter(odd_counter, key_counter): + if len(set(odd_counter.key_only_locations).difference(key_counter.key_only_locations)) > 0: return False - if len(set(sphere.free_locations).difference(key_counter.free_locations)) > 0: + if len(set(odd_counter.free_locations).difference(key_counter.free_locations)) > 0: return False new_child_door = False - for child in sphere.child_doors: + for child in odd_counter.child_doors: if unique_child_door(child, key_counter): new_child_door = True break @@ -260,23 +209,15 @@ def unique_child_door(child, key_counter): return True -def increment_key_counter(door, sphere, key_counter, flat_proposal): - new_counter = key_counter.copy() - new_counter.open_door(door, flat_proposal) - new_counter.update(sphere) - return new_counter - - -def find_best_counter(door, key_counter, key_layout, world, skip_bk): # try to waste as many keys as possible? - door_sphere = key_layout.key_spheres[door.name] - ignored_doors = {door, door.dest} +def find_best_counter(door, odd_counter, key_counter, key_layout, world, skip_bk): # try to waste as many keys as possible? + ignored_doors = {door, door.dest} if door is not None else {} finished = False opened_doors = dict(key_counter.open_doors) bk_opened = key_counter.big_key_opened # new_counter = key_counter last_counter = key_counter while not finished: - door_set = find_potential_open_doors(last_counter, ignored_doors, skip_bk) + door_set = find_potential_open_doors(last_counter, ignored_doors, key_layout, skip_bk) if door_set is None or len(door_set) == 0: finished = True continue @@ -286,7 +227,7 @@ def find_best_counter(door, key_counter, key_layout, world, skip_bk): # try to new_counter = find_counter(proposed_doors, bk_open, key_layout) bk_open = new_counter.big_key_opened # this means the new_door invalidates the door / leads to the same stuff - if relative_empty_sphere(door_sphere, new_counter): + if relative_empty_counter(odd_counter, new_counter): ignored_doors.add(new_door) else: if not key_wasted(new_door, last_counter, new_counter, key_layout, world): @@ -298,17 +239,20 @@ def find_best_counter(door, key_counter, key_layout, world, skip_bk): # try to return last_counter -def find_potential_open_doors(key_counter, ignored_doors, skip_bk): +def find_potential_open_doors(key_counter, ignored_doors, key_layout, skip_bk): small_doors = [] big_doors = [] for other in key_counter.child_doors: if other not in ignored_doors and other.dest not in ignored_doors: if other.bigKey: - if not skip_bk: + if not skip_bk and (not key_layout.big_key_special or key_counter.big_key_opened): big_doors.append(other) elif other.dest not in small_doors: small_doors.append(other) - big_key_available = len(key_counter.free_locations) - key_counter.used_smalls_loc(1) > 0 + if key_layout.big_key_special: + big_key_available = key_counter.big_key_opened + else: + big_key_available = len(key_counter.free_locations) - key_counter.used_smalls_loc(1) > 0 if len(small_doors) == 0 and (not skip_bk and (len(big_doors) == 0 or not big_key_available)): return None return small_doors + big_doors @@ -338,10 +282,10 @@ def key_wasted(new_door, old_counter, new_counter, key_layout, world): return False -def find_next_counter(new_door, old_counter, next_sphere, key_layout): +def find_next_counter(new_door, old_counter, key_layout): proposed_doors = {**old_counter.open_doors, **dict.fromkeys([new_door, new_door.dest])} - bk_open = old_counter.big_key_opened or new_door.bigKey or check_special_locations(next_sphere.free_locations) - return key_layout.key_counters[counter_id(proposed_doors, bk_open, key_layout.flat_prop)] + bk_open = old_counter.big_key_opened or new_door.bigKey + return find_counter(proposed_doors, bk_open, key_layout) def check_special_locations(locations): @@ -358,8 +302,8 @@ def calc_avail_keys(key_counter, world): def create_rule(key_counter, prev_counter, key_layout, world): - prev_chest_keys = available_chest_small_keys(prev_counter, world) - prev_avail = prev_chest_keys + len(prev_counter.key_only_locations) + # prev_chest_keys = available_chest_small_keys(prev_counter, world) + # prev_avail = prev_chest_keys + len(prev_counter.key_only_locations) chest_keys = available_chest_small_keys(key_counter, world) key_gain = len(key_counter.key_only_locations) - len(prev_counter.key_only_locations) raw_avail = chest_keys + len(key_counter.key_only_locations) @@ -377,29 +321,78 @@ def create_rule(key_counter, prev_counter, key_layout, world): return DoorRules(rule_num) -def check_for_self_lock_key(rule, sphere, key_layout, world): +def check_for_self_lock_key(rule, door, parent_counter, key_layout, world): if world.accessibility != 'locations': - counter = KeyCounter(key_layout.max_chests) - counter.update(sphere) + counter = find_inverted_counter(door, parent_counter, key_layout, world) if not self_lock_possible(counter): return - queue = collections.deque(counter.child_doors) - already_queued = set(counter.child_doors) - while len(queue) > 0: - child = queue.popleft() - if child not in counter.open_doors: - counter = increment_key_counter(child, key_layout.key_spheres[child.name], counter, key_layout.flat_prop) - if not self_lock_possible(counter): - return - for new_door in counter.child_doors: - if new_door not in already_queued: - queue.append(new_door) - already_queued.add(new_door) if len(counter.free_locations) == 1 and len(counter.key_only_locations) == 0 and not counter.important_location: rule.allow_small = True rule.small_location = next(iter(counter.free_locations)) +def find_inverted_counter(door, parent_counter, key_layout, world): + # open all doors in counter + counter = open_all_counter(parent_counter, key_layout, door=door) + max_counter = find_max_counter(key_layout) + # find the difference + inverted_counter = KeyCounter(key_layout.max_chests) + inverted_counter.free_locations = dict_difference(max_counter.free_locations, counter.free_locations) + inverted_counter.key_only_locations = dict_difference(max_counter.key_only_locations, counter.key_only_locations) + # child doors? used_keys? + inverted_counter.open_doors = dict_difference(max_counter.open_doors, counter.open_doors) + inverted_counter.other_locations = dict_difference(max_counter.other_locations, counter.other_locations) + for loc in inverted_counter.other_locations: + if important_location(loc, world): + inverted_counter.important_location = True + return inverted_counter + + +def open_all_counter(parent_counter, key_layout, door=None, skipBk=False): + changed = True + counter = parent_counter + proposed_doors = dict.fromkeys(parent_counter.open_doors.keys()) + while changed: + changed = False + doors_to_open = {} + for child in counter.child_doors: + if door is None or (child != door and child != door.dest): + if skipBk: + if not child.bigKey: + doors_to_open[child] = None + elif not child.bigKey or not key_layout.big_key_special or counter.big_key_opened: + doors_to_open[child] = None + if len(doors_to_open.keys()) > 0: + proposed_doors = {**proposed_doors, **doors_to_open} + bk_hint = counter.big_key_opened + for d in doors_to_open.keys(): + bk_hint = bk_hint or d.bigKey + counter = find_counter(proposed_doors, bk_hint, key_layout) + changed = True + return counter + + +def open_some_counter(parent_counter, key_layout, ignored_doors): + changed = True + counter = parent_counter + proposed_doors = dict.fromkeys(parent_counter.open_doors.keys()) + while changed: + changed = False + doors_to_open = {} + for child in counter.child_doors: + if child not in ignored_doors: + if not child.bigKey: + doors_to_open[child] = None + if len(doors_to_open.keys()) > 0: + proposed_doors = {**proposed_doors, **doors_to_open} + bk_hint = counter.big_key_opened + for d in doors_to_open.keys(): + bk_hint = bk_hint or d.bigKey + counter = find_counter(proposed_doors, bk_hint, key_layout) + changed = True + return counter + + def self_lock_possible(counter): return len(counter.free_locations) <= 1 and len(counter.key_only_locations) == 0 and not counter.important_location @@ -415,136 +408,27 @@ def available_chest_small_keys(key_counter, world): return key_counter.max_chests -def bk_restricted_rules(rule, sphere, key_counter, key_layout, world): - if sphere.bk_locked: +def bk_restricted_rules(rule, door, odd_counter, key_counter, key_layout, world): + if key_counter.big_key_opened: return - best_counter = find_best_counter(sphere.access_door, key_counter, key_layout, world, True) + best_counter = find_best_counter(door, odd_counter, key_counter, key_layout, world, True) bk_number = create_rule(best_counter, key_counter, key_layout, world).small_key_num if bk_number == rule.small_key_num: return - post_counter = KeyCounter(key_layout.max_chests) - post_counter.update(sphere) - other_doors_beyond_me = [x for x in post_counter.child_doors if not x.bigKey] - queue = collections.deque(other_doors_beyond_me) - already_queued = set(other_doors_beyond_me) - while len(queue) > 0: - child = queue.popleft() - if child not in post_counter.open_doors: - post_counter = increment_key_counter(child, key_layout.key_spheres[child.name], post_counter, key_layout.flat_prop) - for new_door in post_counter.child_doors: - if not new_door.bigKey and new_door not in already_queued and new_door.dest not in already_queued: - queue.append(new_door) - already_queued.add(new_door) - unique_loc = set(post_counter.free_locations).difference(set(best_counter.free_locations)) + door_open = find_next_counter(door, best_counter, key_layout) + ignored_doors = dict_intersection(best_counter.child_doors, door_open.child_doors) + dest_ignored = [] + for door in ignored_doors.keys(): + if door.dest not in ignored_doors: + dest_ignored.append(door.dest) + ignored_doors = {**ignored_doors, **dict.fromkeys(dest_ignored)} + post_counter = open_some_counter(door_open, key_layout, ignored_doors.keys()) + unique_loc = dict_difference(post_counter.free_locations, best_counter.free_locations) if len(unique_loc) > 0: rule.alternate_small_key = bk_number rule.alternate_big_key_loc.update(unique_loc) -def expand_counter_no_big_doors(door, key_counter, key_layout, ignored_doors): - door_sphere = key_layout.key_spheres[door.name] - small_doors = set() - for other in key_counter.child_doors: - if other != door and other not in ignored_doors: - if other.dest not in small_doors and not other.bigKey: - small_doors.add(other) - if len(small_doors) == 0: - return key_counter - new_counter = key_counter - last_counter = key_counter - new_ignored = set(ignored_doors) - for new_door in small_doors: - new_sphere = key_layout.key_spheres[new_door.name] - new_counter = increment_key_counter(new_door, new_sphere, new_counter, key_layout.flat_prop) - # this means the new_door invalidates the door / leads to the same stuff - if relative_empty_sphere(door_sphere, new_counter): - new_counter = last_counter - new_ignored.add(new_door) - else: - last_counter = new_counter - old_counter = None - while old_counter != new_counter: - old_counter = new_counter - new_counter = expand_counter_no_big_doors(door, old_counter, key_layout, new_ignored) - return new_counter - - -def create_key_spheres(key_layout, world, player): - key_spheres = {} - flat_proposal = key_layout.flat_prop - state = ExplorationState(dungeon=key_layout.sector.name) - state.key_locations = len(world.get_dungeon(key_layout.sector.name, player).small_keys) - state.big_key_special = world.get_region('Hyrule Dungeon Cellblock', player) in key_layout.sector.regions - for region in key_layout.start_regions: - state.visit_region(region, key_checks=True) - state.add_all_doors_check_keys(region, flat_proposal, world, player) - expand_key_state(state, flat_proposal, world, player) - key_spheres['Origin'] = create_key_sphere(state, None, None) - queue = collections.deque([(key_spheres['Origin'], state)]) - while len(queue) > 0: - next_key_sphere, parent_state = queue.popleft() - for door in next_key_sphere.child_doors: - child_state = parent_state.copy() - # open the door - open_a_door(door, child_state, flat_proposal) - expand_key_state(child_state, flat_proposal, world, player) - child_kr = create_key_sphere(child_state, next_key_sphere, door) - if door.name not in key_spheres.keys(): - key_spheres[door.name] = child_kr - queue.append((child_kr, child_state)) - else: - merge_sphere = old_sphere = key_spheres[door.name] - if empty_sphere(old_sphere) and not empty_sphere(child_kr): - key_spheres[door.name] = merge_sphere = child_kr - queue.append((child_kr, child_state)) - if not empty_sphere(old_sphere) and not empty_sphere(child_kr) and not old_sphere == child_kr: - # ugly sphere merge function - just union locations - ugh - if old_sphere.bk_locked != child_kr.bk_locked: - if old_sphere.bk_locked: - merge_sphere.child_doors = child_kr.child_doors - merge_sphere.free_locations = child_kr.free_locations - merge_sphere.key_only_locations = child_kr.key_only_locations - else: - merge_sphere.child_doors = {**old_sphere.child_doors, **child_kr.child_doors} - merge_sphere.free_locations = {**old_sphere.free_locations, **child_kr.free_locations} - merge_sphere.key_only_locations = {**old_sphere.key_only_locations, **child_kr.key_only_locations} - merge_sphere.bk_locked = old_sphere.bk_locked and child_kr.bk_locked - # this feels so ugly, key counters are much smarter than this - would love to get rid of spheres - return key_spheres - - -def create_key_sphere(state, parent_sphere, door): - key_sphere = KeySphere() - key_sphere.parent_sphere = parent_sphere - p_region = parent_sphere - parent_doors = set() - parent_locations = set() - while p_region is not None: - parent_doors.update(p_region.child_doors) - parent_locations.update(p_region.free_locations) - parent_locations.update(p_region.key_only_locations) - parent_locations.update(p_region.other_locations) - p_region = p_region.parent_sphere - u_doors = [x for x in unique_doors(state.small_doors+state.big_doors) if x not in parent_doors] - key_sphere.child_doors.update(dict.fromkeys(u_doors)) - region_locations = [x for x in state.found_locations if x not in parent_locations] - for loc in region_locations: - if '- Prize' in loc.name or loc.name in ['Agahnim 1', 'Agahnim 2']: - key_sphere.prize_region = True - key_sphere.other_locations[loc] = None - elif loc.event and 'Small Key' in loc.item.name: - key_sphere.key_only_locations[loc] = None - elif loc.name not in dungeon_events: - key_sphere.free_locations[loc] = None - else: - key_sphere.other_locations[loc] = None - # todo: Cellblock in a dungeon with a big_key door or chest - Crossed Mode - key_sphere.bk_locked = state.big_key_opened if not state.big_key_special else False - if door is not None: - key_sphere.access_door = door - return key_sphere - - def open_a_door(door, child_state, flat_proposal): if door.bigKey: child_state.big_key_opened = True @@ -610,7 +494,7 @@ def filter_big_chest(locations): def count_locations_exclude_big_chest(state): cnt = 0 for loc in state.found_locations: - if '- Big Chest' not in loc.name and '- Prize' not in loc.name: + if '- Big Chest' not in loc.name and '- Prize' not in loc.name and loc.name not in dungeon_events and loc.name not in ['Agahnim 1', 'Agahnim 2', 'Hyrule Castle - Big Key Drop']: cnt += 1 return cnt @@ -648,21 +532,33 @@ def flatten_pair_list(paired_list): return flat_list -def check_rules(key_layout): +def check_rules(original_counter, key_layout): all_key_only = set() key_only_map = {} - for sphere in key_layout.key_spheres.values(): - for loc in sphere.key_only_locations: + queue = collections.deque([(None, original_counter)]) + completed = set() + completed.add(cid(original_counter, key_layout)) + while len(queue) > 0: + queue = collections.deque(sorted(queue, key=queue_sorter)) + access_door, counter = queue.popleft() + for loc in counter.key_only_locations: if loc not in all_key_only: all_key_only.add(loc) access_rules = [] key_only_map[loc] = access_rules else: access_rules = key_only_map[loc] - if sphere.access_door is None or sphere.access_door.name not in key_layout.key_logic.door_rules.keys(): + if access_door is None or access_door.name not in key_layout.key_logic.door_rules.keys(): access_rules.append(DoorRules(0)) else: - access_rules.append(key_layout.key_logic.door_rules[sphere.access_door.name]) + access_rules.append(key_layout.key_logic.door_rules[access_door.name]) + for child in counter.child_doors.keys(): + if not child.bigKey or not key_layout.big_key_special or counter.big_key_opened: + next_counter = find_next_counter(child, counter, key_layout) + c_id = cid(next_counter, key_layout) + if c_id not in completed: + completed.add(c_id) + queue.append((child, next_counter)) min_rule_bk = defaultdict(list) min_rule_non_bk = defaultdict(list) check_non_bk = False @@ -712,30 +608,26 @@ def adjust_key_location_mins(key_layout, min_rules, getter, setter): # Soft lock stuff -def validate_key_layout_ex(key_layout, world, player): - return validate_key_layout_main_loop(key_layout, world, player) - - -def validate_key_layout_main_loop(key_layout, world, player): +def validate_key_layout(key_layout, world, player): flat_proposal = key_layout.flat_prop state = ExplorationState(dungeon=key_layout.sector.name) - state.key_locations = len(world.get_dungeon(key_layout.sector.name, player).small_keys) + state.key_locations = key_layout.max_chests state.big_key_special = world.get_region('Hyrule Dungeon Cellblock', player) in key_layout.sector.regions for region in key_layout.start_regions: state.visit_region(region, key_checks=True) state.add_all_doors_check_keys(region, flat_proposal, world, player) - return validate_key_layout_sub_loop(state, {}, flat_proposal, world, player) + return validate_key_layout_sub_loop(key_layout, state, {}, flat_proposal, world, player) -def validate_key_layout_sub_loop(state, checked_states, flat_proposal, world, player): +def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposal, world, player): expand_key_state(state, flat_proposal, world, player) - smalls_avail = len(state.small_doors) > 0 + smalls_avail = len(state.small_doors) > 0 # de-dup crystal repeats num_bigs = 1 if len(state.big_doors) > 0 else 0 # all or nothing if not smalls_avail and num_bigs == 0: return True # I think that's the end ttl_locations = state.ttl_locations if state.big_key_opened else count_locations_exclude_big_chest(state) - available_small_locations = min(ttl_locations - state.used_locations, state.key_locations - state.used_smalls) - available_big_locations = ttl_locations - state.used_locations if not state.big_key_special else 0 + available_small_locations = cnt_avail_small_locations(key_layout, ttl_locations, state, world) + available_big_locations = cnt_avail_big_locations(ttl_locations, state, world) if (not smalls_avail or available_small_locations == 0) and (state.big_key_opened or num_bigs == 0 or available_big_locations == 0): return False else: @@ -747,7 +639,7 @@ def validate_key_layout_sub_loop(state, checked_states, flat_proposal, world, pl state_copy.used_smalls += 1 code = state_id(state_copy, flat_proposal) if code not in checked_states.keys(): - valid = validate_key_layout_sub_loop(state_copy, checked_states, flat_proposal, world, player) + valid = validate_key_layout_sub_loop(key_layout, state_copy, checked_states, flat_proposal, world, player) checked_states[code] = valid else: valid = checked_states[code] @@ -759,7 +651,7 @@ def validate_key_layout_sub_loop(state, checked_states, flat_proposal, world, pl state_copy.used_locations += 1 code = state_id(state_copy, flat_proposal) if code not in checked_states.keys(): - valid = validate_key_layout_sub_loop(state_copy, checked_states, flat_proposal, world, player) + valid = validate_key_layout_sub_loop(key_layout, state_copy, checked_states, flat_proposal, world, player) checked_states[code] = valid else: valid = checked_states[code] @@ -768,6 +660,18 @@ def validate_key_layout_sub_loop(state, checked_states, flat_proposal, world, pl return True +def cnt_avail_small_locations(key_layout, ttl_locations, state, world): + if not world.keysanity and world.mode != 'retro': + return min(ttl_locations - state.used_locations, state.key_locations - state.used_smalls) + return key_layout.max_chests + state.key_locations - state.used_smalls + + +def cnt_avail_big_locations(ttl_locations, state, world): + if not world.keysanity: + return ttl_locations - state.used_locations if not state.big_key_special else 0 + return 1 if not state.big_key_special else 0 + + def create_key_counters(key_layout, world, player): key_counters = {} flat_proposal = key_layout.flat_prop @@ -779,36 +683,39 @@ def create_key_counters(key_layout, world, player): state.add_all_doors_check_keys(region, flat_proposal, world, player) expand_key_state(state, flat_proposal, world, player) code = state_id(state, key_layout.flat_prop) - key_counters[code] = create_key_counter_x(state, key_layout, world, player) + key_counters[code] = create_key_counter(state, key_layout, world, player) queue = collections.deque([(key_counters[code], state)]) while len(queue) > 0: - next_key_sphere, parent_state = queue.popleft() - for door in next_key_sphere.child_doors: + next_key_counter, parent_state = queue.popleft() + for door in next_key_counter.child_doors: child_state = parent_state.copy() - # open the door - open_a_door(door, child_state, flat_proposal) - expand_key_state(child_state, flat_proposal, world, player) - code = state_id(child_state, key_layout.flat_prop) - if code not in key_counters.keys(): - child_kr = create_key_counter_x(child_state, key_layout, world, player) - key_counters[code] = child_kr - queue.append((child_kr, child_state)) + if door.bigKey: + key_layout.key_logic.bk_doors.add(door) + # open the door, if possible + if not door.bigKey or not child_state.big_key_special or child_state.big_key_opened: + open_a_door(door, child_state, flat_proposal) + expand_key_state(child_state, flat_proposal, world, player) + code = state_id(child_state, key_layout.flat_prop) + if code not in key_counters.keys(): + child_kr = create_key_counter(child_state, key_layout, world, player) + key_counters[code] = child_kr + queue.append((child_kr, child_state)) return key_counters -def create_key_counter_x(state, key_layout, world, player): +def create_key_counter(state, key_layout, world, player): key_counter = KeyCounter(key_layout.max_chests) key_counter.child_doors.update(dict.fromkeys(unique_doors(state.small_doors+state.big_doors))) for loc in state.found_locations: - if '- Prize' in loc.name or loc.name in ['Agahnim 1', 'Agahnim 2']: - key_counter.important_location = True - # todo: zelda's cell is special in standard, and probably crossed too - elif loc.name in ['Attic Cracked Floor', 'Suspicious Maiden']: + if important_location(loc, world): key_counter.important_location = True + key_counter.other_locations[loc] = None elif loc.event and 'Small Key' in loc.item.name: key_counter.key_only_locations[loc] = None elif loc.name not in dungeon_events: key_counter.free_locations[loc] = None + else: + key_counter.other_locations[loc] = None key_counter.open_doors.update(dict.fromkeys(state.opened_doors)) key_counter.used_keys = count_unique_sm_doors(state.opened_doors) if state.big_key_special: @@ -823,6 +730,34 @@ def create_key_counter_x(state, key_layout, world, player): return key_counter +def important_location(loc, world): + important_locations = ['Agahnim 1', 'Agahnim 2', 'Attic Cracked Floor', 'Suspicious Maiden'] + if world.mode == 'standard' or world.doorShuffle == 'crossed': + important_locations.append('Hyrule Dungeon Cellblock') + return '- Prize' in loc.name or loc.name in important_locations + + +def create_odd_key_counter(door, parent_counter, key_layout, world): + odd_counter = KeyCounter(key_layout.max_chests) + next_counter = find_next_counter(door, parent_counter, key_layout) + odd_counter.free_locations = dict_difference(next_counter.free_locations, parent_counter.free_locations) + odd_counter.key_only_locations = dict_difference(next_counter.key_only_locations, parent_counter.key_only_locations) + odd_counter.child_doors = dict_difference(next_counter.child_doors, parent_counter.child_doors) + odd_counter.other_locations = dict_difference(next_counter.other_locations, parent_counter.other_locations) + for loc in odd_counter.other_locations: + if important_location(loc, world): + odd_counter.important_location = True + return odd_counter + + +def dict_difference(dict_a, dict_b): + return dict.fromkeys([x for x in dict_a.keys() if x not in dict_b.keys()]) + + +def dict_intersection(dict_a, dict_b): + return dict.fromkeys([x for x in dict_a.keys() if x in dict_b.keys()]) + + def state_id(state, flat_proposal): s_id = '1' if state.big_key_opened else '0' for d in flat_proposal: @@ -857,6 +792,10 @@ def find_counter_hint(opened_doors, bk_hint, key_layout): return None +def find_max_counter(key_layout): + return find_counter_hint(dict.fromkeys(key_layout.flat_prop), False, key_layout) + + def counter_id(opened_doors, bk_unlocked, flat_proposal): s_id = '1' if bk_unlocked else '0' for d in flat_proposal: @@ -864,6 +803,10 @@ def counter_id(opened_doors, bk_unlocked, flat_proposal): return s_id +def cid(counter, key_layout): + return counter_id(counter.open_doors, counter.big_key_opened, key_layout.flat_prop) + + # class SoftLockException(Exception): # pass @@ -900,7 +843,7 @@ def val_hyrule(key_logic, world, player): def val_eastern(key_logic, world, player): - val_rule(key_logic.door_rules['Eastern Dark Square Key Door WN'], 2, False, None, 1, {'Eastern Palace - Big Key Chest'}) + val_rule(key_logic.door_rules['Eastern Dark Square Key Door WN'], 2, True, 'Eastern Palace - Big Key Chest', 1, {'Eastern Palace - Big Key Chest'}) val_rule(key_logic.door_rules['Eastern Darkness Up Stairs'], 2) assert world.get_location('Eastern Palace - Big Chest', player) in key_logic.bk_restricted assert world.get_location('Eastern Palace - Boss', player) in key_logic.bk_restricted @@ -981,7 +924,7 @@ def val_ice(key_logic, world, player): def val_mire(key_logic, world, player): mire_west_wing = {'Misery Mire - Big Key Chest', 'Misery Mire - Compass Chest'} - # val_rule(key_logic.door_rules['Mire Spikes NW'], 3) # todo: is sometimes 3 or 5? best_counter order matters + val_rule(key_logic.door_rules['Mire Spikes NW'], 5) # todo: is sometimes 3 or 5? best_counter order matters val_rule(key_logic.door_rules['Mire Hub WS'], 5, False, None, 3, mire_west_wing) val_rule(key_logic.door_rules['Mire Conveyor Crystal WS'], 6, False, None, 4, mire_west_wing) assert world.get_location('Misery Mire - Boss', player) in key_logic.bk_restricted diff --git a/Regions.py b/Regions.py index 25b60ff4..134d97db 100644 --- a/Regions.py +++ b/Regions.py @@ -219,7 +219,7 @@ def create_regions(world, player): create_dungeon_region(player, 'Hyrule Dungeon Armory Boomerang', 'Hyrule Castle', ['Hyrule Castle - Boomerang Chest', 'Hyrule Castle - Boomerang Guard Key Drop'], ['Hyrule Dungeon Armory Boomerang WS']), create_dungeon_region(player, 'Hyrule Dungeon Armory North Branch', 'Hyrule Castle', None, ['Hyrule Dungeon Armory Interior Key Door S', 'Hyrule Dungeon Armory Down Stairs']), create_dungeon_region(player, 'Hyrule Dungeon Staircase', 'Hyrule Castle', None, ['Hyrule Dungeon Staircase Up Stairs', 'Hyrule Dungeon Staircase Down Stairs']), - create_dungeon_region(player, 'Hyrule Dungeon Cellblock', 'Hyrule Castle', ['Hyrule Castle - Zelda\'s Chest'], ['Hyrule Dungeon Cellblock Up Stairs']), + create_dungeon_region(player, 'Hyrule Dungeon Cellblock', 'Hyrule Castle', ['Hyrule Castle - Big Key Drop', 'Hyrule Castle - Zelda\'s Chest'], ['Hyrule Dungeon Cellblock Up Stairs']), create_dungeon_region(player, 'Sewers Behind Tapestry', 'Hyrule Castle', None, ['Sewers Behind Tapestry S', 'Sewers Behind Tapestry Down Stairs']), @@ -817,6 +817,7 @@ key_only_locations = { 'Hyrule Castle - Map Guard Key Drop': 'Small Key (Escape)', 'Hyrule Castle - Boomerang Guard Key Drop': 'Small Key (Escape)', 'Hyrule Castle - Key Rat Key Drop': 'Small Key (Escape)', + 'Hyrule Castle - Big Key Drop': 'Big Key (Escape)', 'Eastern Palace - Dark Square Pot Key': 'Small Key (Eastern Palace)', 'Eastern Palace - Dark Eyegore Key Drop': 'Small Key (Eastern Palace)', 'Desert Palace - Desert Tiles 1 Pot Key': 'Small Key (Desert Palace)', diff --git a/Rules.py b/Rules.py index bdd1d262..2f24ea90 100644 --- a/Rules.py +++ b/Rules.py @@ -263,17 +263,12 @@ def global_rules(world, player): set_rule(world.get_entrance('Eastern Duo Eyegores NE', player), lambda state: state.can_shoot_arrows(player)) set_rule(world.get_entrance('Eastern Single Eyegore NE', player), lambda state: state.can_shoot_arrows(player)) set_rule(world.get_entrance('Eastern Map Balcony Hook Path', player), lambda state: state.has('Hookshot', player)) - # Big key rules - set_rule(world.get_location('Eastern Palace - Big Chest', player), lambda state: state.has('Big Key (Eastern Palace)', player)) - set_rule(world.get_entrance('Eastern Big Key NE', player), lambda state: state.has('Big Key (Eastern Palace)', player)) - set_rule(world.get_entrance('Eastern Courtyard N', player), lambda state: state.has('Big Key (Eastern Palace)', player)) # Boss rules. Same as below but no BK or arrow requirement. set_defeat_dungeon_boss_rule(world.get_location('Eastern Palace - Prize', player)) set_defeat_dungeon_boss_rule(world.get_location('Eastern Palace - Boss', player)) # Desert - set_rule(world.get_location('Desert Palace - Big Chest', player), lambda state: state.has('Big Key (Desert Palace)', player)) set_rule(world.get_location('Desert Palace - Torch', player), lambda state: state.has_Boots(player)) set_rule(world.get_entrance('Desert Wall Slide NW', player), lambda state: state.has_fire_source(player)) add_rule(world.get_entrance('Desert Wall Slide NW', player), lambda state: state.has('Big Key (Desert Palace)', player)) @@ -281,9 +276,7 @@ def global_rules(world, player): set_defeat_dungeon_boss_rule(world.get_location('Desert Palace - Boss', player)) # Tower of Hera - set_rule(world.get_location('Tower of Hera - Big Chest', player), lambda state: state.has('Big Key (Tower of Hera)', player)) set_rule(world.get_location('Tower of Hera - Big Key Chest', player), lambda state: state.has_fire_source(player)) - set_rule(world.get_entrance('Hera Startile Corner NW', player), lambda state: state.has('Big Key (Tower of Hera)', player)) set_defeat_dungeon_boss_rule(world.get_location('Tower of Hera - Boss', player)) set_defeat_dungeon_boss_rule(world.get_location('Tower of Hera - Prize', player)) @@ -293,8 +286,6 @@ def global_rules(world, player): set_rule(world.get_entrance('PoD Mimics 1 NW', player), lambda state: state.can_shoot_arrows(player)) set_rule(world.get_entrance('PoD Mimics 2 NW', player), lambda state: state.can_shoot_arrows(player)) set_rule(world.get_entrance('PoD Bow Statue Down Ladder', player), lambda state: state.can_shoot_arrows(player)) - set_rule(world.get_entrance('PoD Dark Alley NE', player), lambda state: state.has('Big Key (Palace of Darkness)', player)) - set_rule(world.get_location('Palace of Darkness - Big Chest', player), lambda state: state.has('Big Key (Palace of Darkness)', player)) set_rule(world.get_entrance('PoD Map Balcony Drop Down', player), lambda state: state.has('Hammer', player)) set_rule(world.get_entrance('PoD Dark Pegs WN', player), lambda state: state.has('Hammer', player)) set_rule(world.get_entrance('PoD Dark Pegs Up Ladder', player), lambda state: state.has('Hammer', player)) @@ -329,20 +320,16 @@ def global_rules(world, player): set_rule(world.get_entrance('Swamp Waterway N', player), lambda state: state.has('Flippers', player)) set_rule(world.get_entrance('Swamp Waterway NE', player), lambda state: state.has('Flippers', player)) set_rule(world.get_location('Swamp Palace - Waterway Pot Key', player), lambda state: state.has('Flippers', player)) - set_rule(world.get_location('Swamp Palace - Big Chest', player), lambda state: state.has('Big Key (Swamp Palace)', player)) set_defeat_dungeon_boss_rule(world.get_location('Swamp Palace - Boss', player)) set_defeat_dungeon_boss_rule(world.get_location('Swamp Palace - Prize', player)) set_rule(world.get_entrance('Skull Big Chest Hookpath', player), lambda state: state.has('Hookshot', player)) - set_rule(world.get_location('Skull Woods - Big Chest', player), lambda state: state.has('Big Key (Skull Woods)', player)) set_rule(world.get_entrance('Skull Torch Room EN', player), lambda state: state.has('Fire Rod', player)) set_rule(world.get_entrance('Skull Vines NW', player), lambda state: state.has_sword(player)) set_defeat_dungeon_boss_rule(world.get_location('Skull Woods - Boss', player)) set_defeat_dungeon_boss_rule(world.get_location('Skull Woods - Prize', player)) - set_rule(world.get_entrance('Thieves BK Corner NE', player), lambda state: state.has('Big Key (Thieves Town)', player)) # blind can't have the small key? - not necessarily true anymore - but likely still - set_rule(world.get_location('Thieves\' Town - Big Chest', player), lambda state: (state.has('Big Key (Thieves Town)', player) and state.has('Hammer', player))) for entrance in ['Thieves Basement Block Path', 'Thieves Blocked Entry Path', 'Thieves Conveyor Block Path', 'Thieves Conveyor Bridge Block Path']: set_rule(world.get_entrance(entrance, player), lambda state: state.can_lift_rocks(player)) for location in ['Thieves\' Town - Blind\'s Cell', 'Thieves\' Town - Boss']: @@ -355,7 +342,6 @@ def global_rules(world, player): set_rule(world.get_location('Thieves\' Town - Prize', player), lambda state: state.has('Maiden Unmasked', player) and world.get_location('Thieves\' Town - Prize', player).parent_region.dungeon.boss.can_defeat(state)) set_rule(world.get_entrance('Ice Lobby WS', player), lambda state: state.can_melt_things(player)) - set_rule(world.get_location('Ice Palace - Big Chest', player), lambda state: state.has('Big Key (Ice Palace)', player)) set_rule(world.get_entrance('Ice Hammer Block ES', player), lambda state: state.can_lift_rocks(player) and state.has('Hammer', player)) set_rule(world.get_location('Ice Palace - Hammer Block Key Drop', player), lambda state: state.can_lift_rocks(player) and state.has('Hammer', player)) set_rule(world.get_location('Ice Palace - Map Chest', player), lambda state: state.can_lift_rocks(player) and state.has('Hammer', player)) @@ -376,11 +362,7 @@ def global_rules(world, player): set_rule(world.get_entrance('Mire Post-Gap Gap', player), lambda state: state.has_Boots(player) or state.has('Hookshot', player)) set_rule(world.get_entrance('Mire Falling Bridge WN', player), lambda state: state.has_Boots(player) or state.has('Hookshot', player)) # this is due to the fact the the door opposite is blocked set_rule(world.get_entrance('Mire 2 NE', player), lambda state: state.has_sword(player) or state.has('Fire Rod', player) or state.has('Ice Rod', player) or state.has('Hammer', player) or state.has('Cane of Somaria', player) or state.can_shoot_arrows(player)) # need to defeat wizzrobes, bombs don't work ... - set_rule(world.get_location('Misery Mire - Big Chest', player), lambda state: state.has('Big Key (Misery Mire)', player) and (state.has_Boots(player) or state.has('Hookshot', player))) set_rule(world.get_location('Misery Mire - Spike Chest', player), lambda state: (state.world.can_take_damage and state.has_hearts(player, 4)) or state.has('Cane of Byrna', player) or state.has('Cape', player)) - set_rule(world.get_entrance('Mire BK Door Room N', player), lambda state: state.has('Big Key (Misery Mire)', player)) - set_rule(world.get_entrance('Mire Square Rail NW', player), lambda state: state.has('Big Key (Misery Mire)', player)) - set_rule(world.get_entrance('Mire Antechamber NW', player), lambda state: state.has('Big Key (Misery Mire)', player)) set_rule(world.get_entrance('Mire Left Bridge Hook Path', player), lambda state: state.has('Hookshot', player)) set_rule(world.get_entrance('Mire Tile Room NW', player), lambda state: state.has_fire_source(player)) set_rule(world.get_entrance('Mire Attic Hint Hole', player), lambda state: state.has_fire_source(player)) @@ -397,15 +379,13 @@ def global_rules(world, player): set_rule(world.get_entrance('TR Hub NW', player), lambda state: state.has('Cane of Somaria', player)) set_rule(world.get_entrance('TR Hub NE', player), lambda state: state.has('Cane of Somaria', player)) set_rule(world.get_entrance('TR Torches NW', player), lambda state: state.has('Cane of Somaria', player) and state.has('Fire Rod', player)) - set_rule(world.get_location('Turtle Rock - Big Chest', player), lambda state: state.has('Big Key (Turtle Rock)', player)) set_rule(world.get_entrance('TR Big Chest Entrance Gap', player), lambda state: state.has('Cane of Somaria', player) or state.has('Hookshot', player)) set_rule(world.get_entrance('TR Big Chest Gap', player), lambda state: state.has('Cane of Somaria', player) or state.has('Hookshot', player)) - set_rule(world.get_entrance('TR Dodgers NE', player), lambda state: state.has('Big Key (Turtle Rock)', player)) set_rule(world.get_entrance('TR Dark Ride Up Stairs', player), lambda state: state.has('Cane of Somaria', player)) set_rule(world.get_entrance('TR Dark Ride SW', player), lambda state: state.has('Cane of Somaria', player)) set_rule(world.get_entrance('TR Crystal Maze Cane Path', player), lambda state: state.has('Cane of Somaria', player)) set_rule(world.get_entrance('TR Final Abyss South Stairs', player), lambda state: state.has('Cane of Somaria', player)) - set_rule(world.get_entrance('TR Final Abyss NW', player), lambda state: state.has('Cane of Somaria', player) and state.has('Big Key (Turtle Rock)', player)) + set_rule(world.get_entrance('TR Final Abyss NW', player), lambda state: state.has('Cane of Somaria', player)) set_rule(world.get_location('Turtle Rock - Eye Bridge - Bottom Left', player), lambda state: state.has('Cane of Byrna', player) or state.has('Cape', player) or state.has('Mirror Shield', player)) set_rule(world.get_location('Turtle Rock - Eye Bridge - Bottom Right', player), lambda state: state.has('Cane of Byrna', player) or state.has('Cape', player) or state.has('Mirror Shield', player)) set_rule(world.get_location('Turtle Rock - Eye Bridge - Top Left', player), lambda state: state.has('Cane of Byrna', player) or state.has('Cape', player) or state.has('Mirror Shield', player)) @@ -426,7 +406,6 @@ def global_rules(world, player): set_rule(world.get_entrance('GT Hookshot North-South Path', player), lambda state: state.has('Hookshot', player) or state.has_Boots(player)) set_rule(world.get_entrance('GT Firesnake Room Hook Path', player), lambda state: state.has('Hookshot', player)) # I am tempted to stick an invincibility rule for getting across falling bridge - set_rule(world.get_location('Ganons Tower - Big Chest', player), lambda state: state.has('Big Key (Ganons Tower)', player)) set_rule(world.get_entrance('GT Ice Armos NE', player), lambda state: world.get_region('GT Ice Armos', player).dungeon.bosses['bottom'].can_defeat(state)) set_rule(world.get_entrance('GT Ice Armos WS', player), lambda state: world.get_region('GT Ice Armos', player).dungeon.bosses['bottom'].can_defeat(state)) @@ -434,7 +413,6 @@ def global_rules(world, player): set_rule(world.get_entrance('GT Mimics 1 ES', player), lambda state: state.can_shoot_arrows(player)) set_rule(world.get_entrance('GT Mimics 2 WS', player), lambda state: state.can_shoot_arrows(player)) set_rule(world.get_entrance('GT Mimics 2 NE', player), lambda state: state.can_shoot_arrows(player)) - set_rule(world.get_entrance('GT Dash Hall NE', player), lambda state: state.has('Big Key (Ganons Tower)', player)) # consider access to refill room # consider can_kill_most_things to gauntlet set_rule(world.get_entrance('GT Lanmolas 2 ES', player), lambda state: world.get_region('GT Lanmolas 2', player).dungeon.bosses['middle'].can_defeat(state)) @@ -442,7 +420,6 @@ def global_rules(world, player): set_rule(world.get_entrance('GT Torch Cross ES', player), lambda state: state.has_fire_source(player)) set_rule(world.get_entrance('GT Falling Torches NE', player), lambda state: state.has_fire_source(player)) set_rule(world.get_entrance('GT Moldorm Gap', player), lambda state: state.has('Hookshot', player) and world.get_region('GT Moldorm', player).dungeon.bosses['top'].can_defeat(state)) - set_rule(world.get_entrance('GT Brightly Lit Hall NW', player), lambda state: state.has('Big Key (Ganons Tower)', player)) set_defeat_dungeon_boss_rule(world.get_location('Agahnim 2', player)) add_key_logic_rules(world, player) @@ -1693,6 +1670,14 @@ def add_key_logic_rules(world, player): forbid_item(location, d_logic.bk_name, player) for location in d_logic.sm_restricted: forbid_item(location, d_logic.small_key_name, player) + for door in d_logic.bk_doors: + add_rule(world.get_entrance(door.name, player), create_rule(d_logic.bk_name, player)) + for chest in d_logic.bk_chests: + add_rule(world.get_location(chest.name, player), create_rule(d_logic.bk_name, player)) + + +def create_rule(item_name, player): + return lambda state: state.has(item_name, player) def create_key_rule(small_key_name, player, keys):