From 7422eb5ccc951f4476136d4f7b3eb65e9a1fa33a Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 12 Dec 2019 15:01:12 -0700 Subject: [PATCH] Fixed Swordless rules Added rule for Freezor chest (for crossed and swordless) Added more "single exit" caves to possible inaccessible regions Prevented dungeon gen from assuming you could get GT Big Key at Aga 2 Prevented cross-dungeon contamination during key rule gen Fixed some key-sphere merging problems (I'm ready to get rid of spheres now) --- DoorShuffle.py | 7 ++--- DungeonGenerator.py | 65 +++++++++++++++++++++++++-------------------- KeyDoorShuffle.py | 38 +++++++++++++++----------- Rules.py | 9 ++++--- 4 files changed, 68 insertions(+), 51 deletions(-) diff --git a/DoorShuffle.py b/DoorShuffle.py index 042828c0..cb4f1aa0 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -303,7 +303,7 @@ def within_dungeon(world, player): sector_queue.append((key, sector_list, entrance_list)) last_key = key else: - ds = generate_dungeon(sector_list, origin_list_sans_drops, split_dungeon, world, player) + 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 @@ -1120,7 +1120,7 @@ def find_inaccessible_regions(world, player): def valid_inaccessible_region(r): - return r.type is not RegionType.Cave or len(r.exits) > 1 or r.name in ['Spiral Cave (Bottom)'] + return r.type is not RegionType.Cave or (len(r.exits) > 0 and r.name not in ['Links House', 'Chris Houlihan Room']) def add_inaccessible_doors(world, player): @@ -1178,7 +1178,8 @@ def check_required_paths(paths, world, player): start_regions = convert_regions(start_regs, world, player) initial = start_regs == tuple(entrances) if not initial or cached_initial_state is None: - state = ExplorationState(determine_init_crystal(initial, cached_initial_state, start_regions)) + init = determine_init_crystal(initial, cached_initial_state, start_regions) + state = ExplorationState(init, dungeon_name) for region in start_regions: state.visit_region(region) state.add_all_doors_check_unattached(region, world, player) diff --git a/DungeonGenerator.py b/DungeonGenerator.py index 43fb2784..a3d5a961 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -27,7 +27,7 @@ class GraphPiece: self.possible_bk_locations = set() -def generate_dungeon(available_sectors, entrance_region_names, split_dungeon, world, player): +def generate_dungeon(name, available_sectors, entrance_region_names, split_dungeon, world, player): logger = logging.getLogger('') entrance_regions = convert_regions(entrance_region_names, world, player) doors_to_connect = set() @@ -52,7 +52,7 @@ def generate_dungeon(available_sectors, entrance_region_names, split_dungeon, wo if itr > 5000: raise Exception('Generation taking too long. Ref %s' % entrance_region_names[0]) if depth not in dungeon_cache.keys(): - dungeon, hangers, hooks = gen_dungeon_info(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, world, player) dungeon_cache[depth] = dungeon, hangers, hooks valid = check_valid(dungeon, hangers, hooks, proposed_map, doors_to_connect, all_regions, bk_needed) else: @@ -109,10 +109,10 @@ def determine_if_bk_needed(sector, split_dungeon, world, player): return False -def gen_dungeon_info(available_sectors, entrance_regions, proposed_map, valid_doors, bk_needed, world, player): +def gen_dungeon_info(name, available_sectors, entrance_regions, proposed_map, valid_doors, bk_needed, world, player): # step 1 create dungeon: Dict dungeon = {} - original_state = extend_reachable_state_improved(entrance_regions, ExplorationState(), proposed_map, valid_doors, bk_needed, world, player) + original_state = extend_reachable_state_improved(entrance_regions, ExplorationState(dungeon=name), 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() @@ -123,7 +123,7 @@ def gen_dungeon_info(available_sectors, entrance_regions, proposed_map, valid_do 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(), proposed_map, valid_doors, False, world, player) + o_state = extend_reachable_state_improved([parent], ExplorationState(dungeon=name), 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 @@ -182,7 +182,7 @@ 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) + blue_start = ExplorationState(CrystalBarrier.Blue, o_state.dungeon) 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) @@ -257,6 +257,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): + return False for key in hangers.keys(): if len(hooks[key]) > 0 and len(hangers[key]) == 0: return False @@ -374,7 +376,7 @@ 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()] + 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): @@ -498,7 +500,7 @@ def connect_simple_door(exit_door, region, world, player): class ExplorationState(object): - def __init__(self, init_crystal=CrystalBarrier.Orange): + def __init__(self, init_crystal=CrystalBarrier.Orange, dungeon=None): self.unattached_doors = [] self.avail_doors = [] @@ -527,9 +529,10 @@ class ExplorationState(object): self.bk_found = set() self.non_door_entrances = [] + self.dungeon = dungeon def copy(self): - ret = ExplorationState() + ret = ExplorationState(dungeon=self.dungeon) ret.unattached_doors = list(self.unattached_doors) ret.avail_doors = list(self.avail_doors) ret.event_doors = list(self.event_doors) @@ -570,21 +573,22 @@ class ExplorationState(object): self.visited_orange.append(region) elif self.crystal == CrystalBarrier.Blue: self.visited_blue.append(region) - for location in region.locations: - if key_checks and location not in self.found_locations: - if location.name in key_only_locations: - self.key_locations += 1 - if location.name not in dungeon_events and '- Prize' not in location.name: - self.ttl_locations += 1 - if location not in self.found_locations: - self.found_locations.append(location) - if not bk_Flag: - self.bk_found.add(location) - 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]): - self.perform_event(flooded_keys_reverse[location.name], key_region) + 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: + self.key_locations += 1 + if location.name not in dungeon_events and '- Prize' not in location.name: + self.ttl_locations += 1 + if location not in self.found_locations: + self.found_locations.append(location) + if not bk_Flag: + self.bk_found.add(location) + 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]): + 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 self.avail_doors.extend(self.big_doors) @@ -720,7 +724,7 @@ 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, 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: @@ -771,6 +775,7 @@ class ExplorableDoor(object): return '%s (%s)' % (self.door.name, self.crystal.name) +# todo: delete this def extend_reachable_state(search_regions, state, world, player): local_state = state.copy() for region in search_regions: @@ -780,7 +785,7 @@ 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, 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 @@ -801,7 +806,7 @@ def extend_reachable_state_improved(search_regions, state, proposed_map, valid_d 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, 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) @@ -809,8 +814,10 @@ def extend_reachable_state_improved(search_regions, state, proposed_map, valid_d # cross-utility methods -def valid_region_to_explore(region, world, player): - return region is not None and (region.type == RegionType.Dungeon or region.name in world.inaccessible_regions[player]) +def valid_region_to_explore(region, name, world, player): + if region is None: + return False + return (region.type == RegionType.Dungeon and region.dungeon.name == name) or region.name in world.inaccessible_regions[player] def get_doors(world, region, player): diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index aa238514..7aa76c4b 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -32,9 +32,8 @@ class KeySphere(object): return False if len(set(self.key_only_locations).symmetric_difference(set(other.key_only_locations))) > 0: return False - # they only differ in child doors - I don't care - # if len(set(self.child_doors).symmetric_difference(set(other.child_doors))) > 0: - # return False + if len(set(self.child_doors).symmetric_difference(set(other.child_doors))) > 0: + return False return True @@ -156,7 +155,7 @@ def analyze_dungeon(key_layout, world, player): find_bk_locked_sections(key_layout, world) - init_bk = check_special_locations(key_layout.key_spheres['Origin'].free_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)]) doors_completed = set() @@ -473,7 +472,7 @@ def expand_counter_no_big_doors(door, key_counter, key_layout, ignored_doors): def create_key_spheres(key_layout, world, player): key_spheres = {} flat_proposal = key_layout.flat_prop - state = ExplorationState() + 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: @@ -498,12 +497,19 @@ def create_key_spheres(key_layout, world, player): 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)) - merge_sphere.bk_locked = old_sphere.bk_locked and child_kr.bk_locked 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 - 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} - # this feels so ugly, key counters are much smarter than this - would love to get rid of spheres + 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 @@ -519,9 +525,9 @@ def create_key_sphere(state, parent_sphere, door): parent_locations.update(p_region.key_only_locations) parent_locations.update(p_region.other_locations) p_region = p_region.parent_sphere - u_doors = set(unique_doors(state.small_doors+state.big_doors)).difference(parent_doors) + 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 = list(set(state.found_locations).difference(parent_locations)) + 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 @@ -712,7 +718,7 @@ def validate_key_layout_ex(key_layout, world, player): def validate_key_layout_main_loop(key_layout, world, player): flat_proposal = key_layout.flat_prop - state = ExplorationState() + 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: @@ -765,7 +771,7 @@ def validate_key_layout_sub_loop(state, checked_states, flat_proposal, world, pl def create_key_counters(key_layout, world, player): key_counters = {} flat_proposal = key_layout.flat_prop - state = ExplorationState() + 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: @@ -885,11 +891,11 @@ def validate_vanilla_key_logic(world, player): def val_hyrule(key_logic, world, player): - val_rule(key_logic.door_rules['Sewers Secret Room Key Door S'], 2) - val_rule(key_logic.door_rules['Sewers Dark Cross Key Door N'], 2) + val_rule(key_logic.door_rules['Sewers Secret Room Key Door S'], 3) + val_rule(key_logic.door_rules['Sewers Dark Cross Key Door N'], 3) val_rule(key_logic.door_rules['Hyrule Dungeon Map Room Key Door S'], 2) # why is allow_small actually false? - because chest key is forced elsewhere? - val_rule(key_logic.door_rules['Hyrule Dungeon Armory Interior Key Door N'], 4, True, 'Hyrule Castle - Zelda\'s Chest') + val_rule(key_logic.door_rules['Hyrule Dungeon Armory Interior Key Door N'], 3, True, 'Hyrule Castle - Zelda\'s Chest') # val_rule(key_logic.door_rules['Hyrule Dungeon Armory Interior Key Door N'], 4) diff --git a/Rules.py b/Rules.py index 1789bdb9..0a833fd2 100644 --- a/Rules.py +++ b/Rules.py @@ -363,6 +363,7 @@ def global_rules(world, player): set_rule(world.get_entrance('Ice Spike Room Up Stairs', player), lambda state: state.world.can_take_damage or state.has('Hookshot', player) or state.has('Cape', player) or state.has('Cane of Byrna', player)) set_rule(world.get_entrance('Ice Spike Room Down Stairs', player), lambda state: state.world.can_take_damage or state.has('Hookshot', player) or state.has('Cape', player) or state.has('Cane of Byrna', player)) set_rule(world.get_location('Ice Palace - Spike Room', player), lambda state: state.world.can_take_damage or state.has('Hookshot', player) or state.has('Cape', player) or state.has('Cane of Byrna', player)) + set_rule(world.get_location('Ice Palace - Freezor Chest', player), lambda state: state.can_melt_things(player)) set_rule(world.get_entrance('Ice Hookshot Ledge Path', player), lambda state: state.has('Hookshot', player)) set_rule(world.get_entrance('Ice Hookshot Balcony Path', player), lambda state: state.has('Hookshot', player)) set_rule(world.get_entrance('Ice Switch Room SE', player), lambda state: state.has('Cane of Somaria', player) or state.has('Convenient Block', player)) @@ -983,10 +984,12 @@ def open_rules(world, player): def swordless_rules(world, player): - set_rule(world.get_entrance('Agahnim 1', player), lambda state: (state.has('Hammer', player) or state.has('Fire Rod', player) or state.can_shoot_arrows(player) or state.has('Cane of Somaria', player)) and state.has_key('Small Key (Agahnims Tower)', player, 2)) + set_rule(world.get_entrance('Tower Altar NW', player), lambda state: True) + set_rule(world.get_entrance('Skull Vines NW', player), lambda state: True) + set_rule(world.get_entrance('Ice Lobby WS', player), lambda state: state.has('Fire Rod', player) or state.has('Bombos', player)) + set_rule(world.get_location('Ice Palace - Freezor Chest', player), lambda state: state.has('Fire Rod', player) or state.has('Bombos', player)) + set_rule(world.get_location('Ether Tablet', player), lambda state: state.has('Book of Mudora', player) and state.has('Hammer', player)) - set_rule(world.get_entrance('Skull Woods Torch Room', player), lambda state: state.has_key('Small Key (Skull Woods)', player, 3) and state.has('Fire Rod', player)) # no curtain - set_rule(world.get_entrance('Ice Palace Entrance Room', player), lambda state: state.has('Fire Rod', player) or state.has('Bombos', player)) #in swordless mode bombos pads are present in the relevant parts of ice palace set_rule(world.get_location('Ganon', player), lambda state: state.has('Hammer', player) and state.has_fire_source(player) and state.has('Silver Arrows', player) and state.can_shoot_arrows(player) and state.has_crystals(world.crystals_needed_for_ganon, player)) set_rule(world.get_entrance('Ganon Drop', player), lambda state: state.has('Hammer', player)) # need to damage ganon to get tiles to drop