diff --git a/DoorShuffle.py b/DoorShuffle.py index 3eb02a6f..474df204 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -178,6 +178,7 @@ def create_door_spoiler(world, player): queue = deque(world.dungeon_layouts[player].values()) while len(queue) > 0: builder = queue.popleft() + std_flag = world.mode[player] == 'standard' and builder.name == 'Hyrule Castle' and world.shuffle[player] == 'vanilla' done = set() start_regions = set(convert_regions(builder.layout_starts, world, player)) # todo: set all_entrances for basic reg_queue = deque(start_regions) @@ -206,11 +207,15 @@ def create_door_spoiler(world, player): logger.warning('This is a bug during door spoiler') elif not isinstance(door_b, Region): logger.warning('Door not connected: %s', door_a.name) - if connect and connect.type == RegionType.Dungeon and connect not in visited: + if valid_connection(connect, std_flag, world, player) and connect not in visited: visited.add(connect) reg_queue.append(connect) +def valid_connection(region, std_flag, world, player): + return region and (region.type == RegionType.Dungeon or region.name in world.inaccessible_regions[player] or + (std_flag and region.name == 'Hyrule Castle Ledge')) + def vanilla_key_logic(world, player): builders = [] world.dungeon_layouts[player] = {} @@ -3331,6 +3336,9 @@ def find_inaccessible_regions(world, player): ledge = world.get_region('Hyrule Castle Ledge', player) if any(x for x in ledge.exits if x.connected_region and x.connected_region.name == 'Agahnims Tower Portal'): world.inaccessible_regions[player].append('Hyrule Castle Ledge') + # this should be considered as part of the inaccessible regions, dungeonssimple? + if world.mode[player] == 'standard' and world.shuffle[player] == 'vanilla': + world.inaccessible_regions[player].append('Hyrule Castle Ledge') logger = logging.getLogger('') #logger.debug('Inaccessible Regions:') #for r in world.inaccessible_regions[player]: diff --git a/Main.py b/Main.py index 1223b79e..db7c39d8 100644 --- a/Main.py +++ b/Main.py @@ -36,7 +36,7 @@ from source.overworld.EntranceShuffle2 import link_entrances_new from source.tools.BPS import create_bps_from_data from source.classes.CustomSettings import CustomSettings -version_number = '1.2.0.20' +version_number = '1.2.0.21' version_branch = '-u' __version__ = f'{version_number}{version_branch}' diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 336c604e..cae2bc56 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -109,6 +109,10 @@ These are now independent of retro mode and have three options: None, Random, an # Bug Fixes and Notes +* 1.2.0.21u + * Fix that should force items needed for leaving Zelda's cell to before the throne room, so S&Q isn't mandatory + * Small fix for Tavern Shuffle (thanks Catobat) + * Several small generation fixes * 1.2.0.20u * New generation feature that allows Spiral Stair to link to themselves (thank Catobat) * Added logic for trap doors that could be opened using existing room triggers diff --git a/Rules.py b/Rules.py index 4cdd6e61..07d4c82f 100644 --- a/Rules.py +++ b/Rules.py @@ -1577,13 +1577,15 @@ def standard_rules(world, player): add_rule(ent, lambda state: standard_escape_rule(state)) set_rule(world.get_location('Zelda Pickup', player), lambda state: state.has('Big Key (Escape)', player)) - set_rule(world.get_entrance('Hyrule Castle Throne Room Tapestry', player), lambda state: state.has('Zelda Herself', player)) set_rule(world.get_entrance('Hyrule Castle Tapestry Backwards', player), lambda state: state.has('Zelda Herself', player)) def check_rule_list(state, r_list): return True if len(r_list) <= 0 else r_list[0](state) and check_rule_list(state, r_list[1:]) rule_list, debug_path = find_rules_for_zelda_delivery(world, player) - set_rule(world.get_location('Zelda Drop Off', player), lambda state: state.has('Zelda Herself', player) and check_rule_list(state, rule_list)) + set_rule(world.get_entrance('Hyrule Castle Throne Room Tapestry', player), + lambda state: state.has('Zelda Herself', player) and check_rule_list(state, rule_list)) + set_rule(world.get_location('Zelda Drop Off', player), + lambda state: state.has('Zelda Herself', player) and check_rule_list(state, rule_list)) for entrance in ['Links House SC', 'Links House ES', 'Central Bonk Rocks SW', 'Hyrule Castle WN', 'Hyrule Castle ES', 'Bonk Fairy (Light)', 'Hyrule Castle Main Gate (South)', 'Hyrule Castle Main Gate (North)', 'Hyrule Castle Ledge Drop']: diff --git a/source/dungeon/DungeonStitcher.py b/source/dungeon/DungeonStitcher.py index 504fc03b..84139157 100644 --- a/source/dungeon/DungeonStitcher.py +++ b/source/dungeon/DungeonStitcher.py @@ -72,11 +72,13 @@ def generate_dungeon_find_proposal(builder, entrance_region_names, split_dungeon entrance_regions = [x for x in entrance_regions if x not in excluded.keys()] doors_to_connect, idx = {}, 0 all_regions = set() + bk_special = False for sector in builder.sectors: for door in sector.outstanding_doors: doors_to_connect[door.name] = door, idx idx += 1 all_regions.update(sector.regions) + bk_special |= check_for_special(sector.regions) finished = False # flag if standard and this is hyrule castle paths = determine_paths_for_dungeon(world, player, all_regions, name) @@ -95,9 +97,9 @@ def generate_dungeon_find_proposal(builder, entrance_region_names, split_dungeon if hash_code not in hash_code_set: hash_code_set.add(hash_code) explored_state = explore_proposal(name, entrance_regions, all_regions, proposed_map, doors_to_connect, - world, player) + bk_special, world, player) if check_valid(name, explored_state, proposed_map, doors_to_connect, all_regions, - paths, entrance_regions, world, player): + paths, entrance_regions, bk_special, world, player): finished = True else: proposed_map, hash_code = modify_proposal(proposed_map, explored_state, doors_to_connect, @@ -229,21 +231,24 @@ def modify_proposal(proposed_map, explored_state, doors_to_connect, hash_code_se return proposed_map, hash_code -def explore_proposal(name, entrance_regions, all_regions, proposed_map, valid_doors, world, player): +def explore_proposal(name, entrance_regions, all_regions, proposed_map, valid_doors, bk_special, world, player): start = ExplorationState(dungeon=name) + bk_relevant = (world.door_type_mode[player] == 'original' and not world.bigkeyshuffle[player]) or bk_special + start.big_key_special = bk_special original_state = extend_reachable_state_lenient(entrance_regions, start, proposed_map, - all_regions, valid_doors, world, player) + all_regions, valid_doors, bk_relevant, world, player) return original_state def check_valid(name, exploration_state, proposed_map, doors_to_connect, all_regions, - paths, entrance_regions, world, player): + paths, entrance_regions, bk_special, world, player): all_visited = set() all_visited.update(exploration_state.visited_blue) all_visited.update(exploration_state.visited_orange) if len(all_regions.difference(all_visited)) > 0: return False - if not valid_paths(name, paths, entrance_regions, doors_to_connect, all_regions, proposed_map, world, player): + if not valid_paths(name, paths, entrance_regions, doors_to_connect, all_regions, proposed_map, + bk_special, world, player): return False return True @@ -266,7 +271,7 @@ def check_for_special(regions): return False -def valid_paths(name, paths, entrance_regions, valid_doors, all_regions, proposed_map, world, player): +def valid_paths(name, paths, entrance_regions, valid_doors, all_regions, proposed_map, bk_special, world, player): for path in paths: if type(path) is tuple: target = path[1] @@ -278,12 +283,13 @@ def valid_paths(name, paths, entrance_regions, valid_doors, all_regions, propose else: target = path start_regions = entrance_regions - if not valid_path(name, start_regions, target, valid_doors, proposed_map, all_regions, world, player): + if not valid_path(name, start_regions, target, valid_doors, proposed_map, all_regions, + bk_special, world, player): return False return True -def valid_path(name, starting_regions, target, valid_doors, proposed_map, all_regions, world, player): +def valid_path(name, starting_regions, target, valid_doors, proposed_map, all_regions, bk_special, world, player): target_regions = set() if type(target) is not list: for region in all_regions: @@ -296,8 +302,10 @@ def valid_path(name, starting_regions, target, valid_doors, proposed_map, all_re target_regions.add(region) start = ExplorationState(dungeon=name) + bk_relevant = (world.door_type_mode[player] == 'original' and not world.bigkeyshuffle[player]) or bk_special + start.big_key_special = bk_special original_state = extend_reachable_state_lenient(starting_regions, start, proposed_map, all_regions, - valid_doors, world, player) + valid_doors, bk_relevant, world, player) for exp_door in original_state.unattached_doors: if not exp_door.door.blocked or exp_door.door.trapFlag != 0: @@ -531,7 +539,7 @@ class ExplorationState(object): self.crystal = exp_door.crystal return exp_door - def visit_region(self, region, key_region=None, key_checks=False, bk_flag=False): + def visit_region(self, region, key_region=None, key_checks=False, bk_relevant=False): if region.type != RegionType.Dungeon: self.crystal = CrystalBarrier.Orange if self.crystal == CrystalBarrier.Either: @@ -552,8 +560,14 @@ class ExplorationState(object): 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 bk_relevant: + if self.big_key_special: + if special_big_key_found(self): + self.bk_found.add(location) + self.re_add_big_key_doors() + else: + self.bk_found.add(location) + self.re_add_big_key_doors() 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) @@ -574,6 +588,14 @@ class ExplorationState(object): return True return False + def re_add_big_key_doors(self): + self.big_key_opened = True + queue = collections.deque(self.big_doors) + while len(queue) > 0: + exp_door = queue.popleft() + self.avail_doors.append(exp_door) + self.big_doors.remove(exp_door) + def perform_event(self, location_name, key_region): self.events.add(location_name) queue = collections.deque(self.event_doors) @@ -640,7 +662,7 @@ class ExplorationState(object): self.append_door_to_list(door, self.avail_doors, flag) # same as above but traps are ignored, and flag is not used - def add_all_doors_check_proposed_2(self, region, proposed_map, valid_doors, world, player): + def add_all_doors_check_proposed_2(self, region, proposed_map, valid_doors, bk_relevant, world, player): for door in get_doors(world, region, player): if door in proposed_map and door.name in valid_doors: self.visited_doors.add(door) @@ -654,14 +676,18 @@ 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) + elif (bk_relevant and (door.bigKey or door.name in get_special_big_key_doors(world, player)) + and not self.big_key_opened): + if not self.in_door_list(door, self.big_doors): + self.append_door_to_list(door, self.big_doors) elif not self.in_door_list(door, self.avail_doors): self.append_door_to_list(door, self.avail_doors) # same as above but traps are checked for - def add_all_doors_check_proposed_3(self, region, proposed_map, valid_doors, world, player): + def add_all_doors_check_proposed_3(self, region, proposed_map, valid_doors, bk_relevant, world, player): for door in get_doors(world, region, player): if door in proposed_map and door.name in valid_doors: self.visited_doors.add(door) @@ -675,9 +701,13 @@ 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) + elif (bk_relevant and (door.bigKey or door.name in get_special_big_key_doors(world, player)) + and not self.big_key_opened): + if not self.in_door_list(door, self.big_doors): + self.append_door_to_list(door, self.big_doors) elif not self.in_door_list(door, self.avail_doors): self.append_door_to_list(door, self.avail_doors) @@ -863,16 +893,22 @@ def extend_reachable_state_improved(search_regions, state, proposed_map, all_reg return local_state -def extend_reachable_state_lenient(search_regions, state, proposed_map, all_regions, valid_doors, world, player): +# bk_relevant means the big key doors need to be checks +def extend_reachable_state_lenient(search_regions, state, proposed_map, all_regions, valid_doors, bk_relevant, + world, player): local_state = state.copy() for region in search_regions: - local_state.visit_region(region) + local_state.visit_region(region, bk_relevant=bk_relevant) if world.trap_door_mode[player] == 'vanilla': - local_state.add_all_doors_check_proposed_3(region, proposed_map, valid_doors, world, player) + local_state.add_all_doors_check_proposed_3(region, proposed_map, valid_doors, bk_relevant, world, player) else: - local_state.add_all_doors_check_proposed_2(region, proposed_map, valid_doors, world, player) + local_state.add_all_doors_check_proposed_2(region, proposed_map, valid_doors, bk_relevant, world, player) while len(local_state.avail_doors) > 0: explorable_door = local_state.next_avail_door() + if explorable_door.door.bigKey: + if bk_relevant and (not special_big_key_found(local_state) if local_state.big_key_special + else local_state.count_locations_exclude_specials(world, player) == 0): + continue if explorable_door.door in proposed_map: connect_region = world.get_entrance(proposed_map[explorable_door.door].name, player).parent_region else: @@ -880,11 +916,13 @@ def extend_reachable_state_lenient(search_regions, state, proposed_map, all_regi if connect_region is not None: if (valid_region_to_explore_in_regions(connect_region, all_regions, world, player) and not local_state.visited(connect_region)): - local_state.visit_region(connect_region) + local_state.visit_region(connect_region, bk_relevant=bk_relevant) if world.trap_door_mode[player] == 'vanilla': - local_state.add_all_doors_check_proposed_3(connect_region, proposed_map, valid_doors, world, player) + local_state.add_all_doors_check_proposed_3(connect_region, proposed_map, valid_doors, + bk_relevant, world, player) else: - local_state.add_all_doors_check_proposed_2(connect_region, proposed_map, valid_doors, world, player) + local_state.add_all_doors_check_proposed_2(connect_region, proposed_map, valid_doors, + bk_relevant, world, player) return local_state diff --git a/test/customizer/zelda_escape.yaml b/test/customizer/zelda_escape.yaml new file mode 100644 index 00000000..e779bbac --- /dev/null +++ b/test/customizer/zelda_escape.yaml @@ -0,0 +1,14 @@ +meta: + players: 1 +settings: + 1: + door_shuffle: crossed + intensity: 3 + mode: standard + pottery: keys + dropshuffle: 'on' +doors: + 1: + doors: + Hyrule Dungeon Cellblock Up Stairs: + dest: Ice Hammer Block Down Stairs