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