diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 261dc125..5845e58b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,10 +32,10 @@ jobs: steps: # checkout commit - name: Checkout commit - uses: actions/checkout@v2 + uses: actions/checkout@v3 # install python - name: Install python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} architecture: "x64" @@ -50,21 +50,21 @@ jobs: pip install pyinstaller # get parent directory - name: Get Repo Name - uses: mad9000/actions-find-and-replace-string@1 + uses: mad9000/actions-find-and-replace-string@3 id: repoName with: source: ${{ github.repository }} find: '${{ github.repository_owner }}/' replace: '' - name: Get Parent Directory Path (!Windows) - uses: mad9000/actions-find-and-replace-string@1 + uses: mad9000/actions-find-and-replace-string@3 id: parentDirNotWin with: source: ${{ github.workspace }} find: '${{ steps.repoName.outputs.value }}/${{ steps.repoName.outputs.value }}' replace: ${{ steps.repoName.outputs.value }} - name: Get Parent Directory Path (Windows) - uses: mad9000/actions-find-and-replace-string@1 + uses: mad9000/actions-find-and-replace-string@3 id: parentDir with: source: ${{ steps.parentDirNotWin.outputs.value }} @@ -92,7 +92,7 @@ jobs: python ./resources/ci/common/prepare_binary.py # upload binary artifacts for later step - name: Upload Binary Artifacts - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: binaries-${{ matrix.os-name }} path: ${{ steps.parentDir.outputs.value }}/artifact @@ -117,10 +117,10 @@ jobs: steps: # checkout commit - name: Checkout commit - uses: actions/checkout@v2 + uses: actions/checkout@v3 # install python - name: Install Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} architecture: "x64" @@ -134,21 +134,21 @@ jobs: python ./resources/ci/common/install.py # get parent directory - name: Get Repo Name - uses: mad9000/actions-find-and-replace-string@1 + uses: mad9000/actions-find-and-replace-string@3 id: repoName with: source: ${{ github.repository }} find: '${{ github.repository_owner }}/' replace: '' - name: Get Parent Directory Path (!Windows) - uses: mad9000/actions-find-and-replace-string@1 + uses: mad9000/actions-find-and-replace-string@3 id: parentDirNotWin with: source: ${{ github.workspace }} find: '${{ steps.repoName.outputs.value }}/${{ steps.repoName.outputs.value }}' replace: ${{ steps.repoName.outputs.value }} - name: Get Parent Directory Path (Windows) - uses: mad9000/actions-find-and-replace-string@1 + uses: mad9000/actions-find-and-replace-string@3 id: parentDir with: source: ${{ steps.parentDirNotWin.outputs.value }} @@ -156,7 +156,7 @@ jobs: replace: ${{ steps.repoName.outputs.value }} # download binary artifact - name: Download Binary Artifact - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: binaries-${{ matrix.os-name }} path: ./ @@ -170,13 +170,13 @@ jobs: python ./resources/ci/common/prepare_release.py # upload appversion artifact for later step - name: Upload AppVersion Artifact - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: appversion-${{ matrix.os-name }} path: ./resources/app/meta/manifests/app_version.txt # upload archive artifact for later step - name: Upload Archive Artifact - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: archive-${{ matrix.os-name }} path: ${{ steps.parentDir.outputs.value }}/deploy @@ -202,24 +202,24 @@ jobs: steps: # checkout commit - name: Checkout commit - uses: actions/checkout@v2 + uses: actions/checkout@v3 # get parent directory - name: Get Repo Name - uses: mad9000/actions-find-and-replace-string@1 + uses: mad9000/actions-find-and-replace-string@3 id: repoName with: source: ${{ github.repository }} find: '${{ github.repository_owner }}/' replace: '' - name: Get Parent Directory Path (!Windows) - uses: mad9000/actions-find-and-replace-string@1 + uses: mad9000/actions-find-and-replace-string@3 id: parentDirNotWin with: source: ${{ github.workspace }} find: '${{ steps.repoName.outputs.value }}/${{ steps.repoName.outputs.value }}' replace: ${{ steps.repoName.outputs.value }} - name: Get Parent Directory Path (Windows) - uses: mad9000/actions-find-and-replace-string@1 + uses: mad9000/actions-find-and-replace-string@3 id: parentDir with: source: ${{ steps.parentDirNotWin.outputs.value }} @@ -230,25 +230,25 @@ jobs: python -m pip install pytz requests # download appversion artifact - name: Download AppVersion Artifact - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: appversion-${{ matrix.os-name }} path: ${{ steps.parentDir.outputs.value }}/build # download ubuntu archive artifact - name: Download Ubuntu Archive Artifact - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: archive-ubuntu-latest path: ${{ steps.parentDir.outputs.value }}/deploy/linux # download macos archive artifact - name: Download MacOS Archive Artifact - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: archive-macOS-latest path: ${{ steps.parentDir.outputs.value }}/deploy/macos # download windows archive artifact - name: Download Windows Archive Artifact - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: archive-windows-latest path: ${{ steps.parentDir.outputs.value }}/deploy/windows diff --git a/.gitignore b/.gitignore index 9c901e75..a2d08623 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,9 @@ *.bmbp *.log *_Spoiler.json +*_custom.yaml +*_meta.txt +*.bps *.pyc *.sfc *.srm diff --git a/.mailmap b/.mailmap new file mode 100644 index 00000000..1aecc0db --- /dev/null +++ b/.mailmap @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/BaseClasses.py b/BaseClasses.py index 922eab75..be3b7ac8 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -21,7 +21,7 @@ from source.dungeon.RoomObject import RoomObject class World(object): def __init__(self, players, owShuffle, owCrossed, owMixed, shuffle, doorShuffle, logic, mode, swords, difficulty, difficulty_adjustments, - timer, progressive, goal, algorithm, accessibility, shuffle_ganon, retro, custom, customitemarray, hints): + timer, progressive, goal, algorithm, accessibility, shuffle_ganon, custom, customitemarray, hints): self.players = players self.teams = 1 self.owShuffle = owShuffle.copy() @@ -34,6 +34,9 @@ class World(object): self.shuffle = shuffle.copy() self.doorShuffle = doorShuffle.copy() self.intensity = {} + self.door_type_mode = {} + self.trap_door_mode = {} + self.key_logic_algorithm = {} self.logic = logic.copy() self.mode = mode.copy() self.swords = swords.copy() @@ -71,7 +74,6 @@ class World(object): self.fix_trock_exit = {} self.shuffle_ganon = shuffle_ganon self.fix_gtower_exit = self.shuffle_ganon - self.retro = retro.copy() self.custom = custom self.customitemarray = customitemarray self.can_take_damage = True @@ -105,10 +107,6 @@ class World(object): self.pot_contents = {} for player in range(1, players + 1): - # If World State is Retro, set to Open and set Retro flag - if self.mode[player] == "retro": - self.mode[player] = "open" - self.retro[player] = True def set_player_attr(attr, val): self.__dict__.setdefault(attr, {})[player] = val set_player_attr('_region_cache', {}) @@ -132,13 +130,16 @@ class World(object): set_player_attr('can_access_trock_front', None) set_player_attr('can_access_trock_big_chest', None) set_player_attr('can_access_trock_middle', None) - set_player_attr('fix_fake_world', logic[player] not in ['owglitches', 'nologic'] or shuffle[player] in ['lean', 'crossed', 'insanity', 'madness_legacy']) + set_player_attr('fix_fake_world', logic[player] not in ['owglitches', 'nologic'] + or shuffle[player] in ['lean', 'crossed', 'insanity']) set_player_attr('mapshuffle', False) set_player_attr('compassshuffle', False) - set_player_attr('keyshuffle', False) + set_player_attr('keyshuffle', 'none') set_player_attr('bigkeyshuffle', False) set_player_attr('restrict_boss_items', 'none') set_player_attr('bombbag', False) + set_player_attr('flute_mode', False) + set_player_attr('bow_mode', False) set_player_attr('difficulty_requirements', None) set_player_attr('boss_shuffle', 'none') set_player_attr('enemy_shuffle', 'none') @@ -151,6 +152,7 @@ class World(object): set_player_attr('crystals_ganon_orig', {}) set_player_attr('crystals_gt_orig', {}) set_player_attr('open_pyramid', 'auto') + set_player_attr('take_any', 'none') set_player_attr('treasure_hunt_icon', 'Triforce Piece') set_player_attr('treasure_hunt_count', 0) set_player_attr('treasure_hunt_total', 0) @@ -160,16 +162,27 @@ class World(object): set_player_attr('collection_rate', False) set_player_attr('colorizepots', False) set_player_attr('pot_pool', {}) + set_player_attr('decoupledoors', False) + set_player_attr('door_type_mode', 'original') + set_player_attr('trap_door_mode', 'optional') + set_player_attr('key_logic_algorithm', 'default') set_player_attr('shopsanity', False) set_player_attr('mixed_travel', 'prevent') set_player_attr('standardize_palettes', 'standardize') set_player_attr('force_fix', {'gt': False, 'sw': False, 'pod': False, 'tr': False}) - set_player_attr('prizes', {'pull': [0, 0, 0], 'crab': [0, 0], 'stun': 0, 'fish': 0}) + set_player_attr('prizes', {'dig;': [], 'pull': [0, 0, 0], 'crab': [0, 0], 'stun': 0, 'fish': 0, 'enemies': []}) set_player_attr('exp_cache', defaultdict(dict)) set_player_attr('enabled_entrances', {}) + def finish_init(self): + for player in range(1, self.players + 1): + if self.mode[player] == 'retro': + self.mode[player] = 'open' + if self.goal[player] == 'completionist': + self.accessibility[player] = 'locations' + def get_name_string_for_object(self, obj): return obj.name if self.players == 1 else f'{obj.name} ({self.get_player_names(obj.player)})' @@ -451,7 +464,8 @@ class World(object): def push_precollected(self, item): item.world = self - if (item.smallkey and self.keyshuffle[item.player]) or (item.bigkey and self.bigkeyshuffle[item.player]): + if ((item.smallkey and self.keyshuffle[item.player] != 'none') + or (item.bigkey and self.bigkeyshuffle[item.player])): item.advancement = True self.precollected_items.append(item) self.state.collect(item, True) @@ -540,7 +554,10 @@ class World(object): if self.has_beaten_game(state): return True - prog_locations = [location for location in self.get_locations() if location.item is not None and (location.item.advancement or location.event) and location not in state.locations_checked] + prog_locations = [location for location in self.get_locations() if location.item is not None + and (location.item.advancement or location.event + or self.goal[location.player] == 'completionist') + and location not in state.locations_checked] while prog_locations: sphere = [] @@ -572,6 +589,7 @@ class CollectionState(object): self.world = parent if not skip_init: self.prog_items = Counter() + self.forced_keys = Counter() self.reachable_regions = {player: dict() for player in range(1, parent.players + 1)} self.blocked_connections = {player: dict() for player in range(1, parent.players + 1)} self.events = [] @@ -586,6 +604,7 @@ class CollectionState(object): self.opened_doors = {player: set() for player in range(1, parent.players + 1)} self.dungeons_to_check = {player: defaultdict(dict) for player in range(1, parent.players + 1)} self.dungeon_limits = None + self.placing_item = None # self.trace = None def update_reachable_regions(self, player): @@ -603,12 +622,13 @@ class CollectionState(object): queue = deque(self.blocked_connections[player].items()) self.traverse_world(queue, rrp, bc, player) - unresolved_events = [x for y in self.reachable_regions[player] for x in y.locations - if x.event and x.item and (x.item.smallkey or x.item.bigkey or x.item.advancement) - and x not in self.locations_checked and x.can_reach(self)] - unresolved_events = self._do_not_flood_the_keys(unresolved_events) - if len(unresolved_events) == 0: - self.check_key_doors_in_dungeons(rrp, player) + if self.world.key_logic_algorithm[player] == 'default': + unresolved_events = [x for y in self.reachable_regions[player] for x in y.locations + if x.event and x.item and (x.item.smallkey or x.item.bigkey or x.item.advancement) + and x not in self.locations_checked and x.can_reach(self)] + unresolved_events = self._do_not_flood_the_keys(unresolved_events) + if len(unresolved_events) == 0: + self.check_key_doors_in_dungeons(rrp, player) def traverse_world(self, queue, rrp, bc, player): # run BFS on all connections, and keep track of those blocked by missing items @@ -655,7 +675,7 @@ class CollectionState(object): if key_logic.sm_doors[door]: self.reached_doors[player].add(key_logic.sm_doors[door].name) if not connection.can_reach(self): - checklist_key = 'Universal' if self.world.retro[player] else dungeon_name + checklist_key = 'Universal' if self.world.keyshuffle[player] == 'universal' else dungeon_name checklist = self.dungeons_to_check[player][checklist_key] checklist[connection.name] = (connection, crystal_state) elif door.name not in self.opened_doors[player]: @@ -692,6 +712,7 @@ class CollectionState(object): def check_key_doors_in_dungeons(self, rrp, player): for dungeon_name, checklist in self.dungeons_to_check[player].items(): + # todo: optimization idea - abort exploration if there are unresolved events now if self.apply_dungeon_exploration(rrp, player, dungeon_name, checklist): continue init_door_candidates = self.should_explore_child_state(self, dungeon_name, player) @@ -800,6 +821,15 @@ class CollectionState(object): rrp[k] = missing_regions[k] possible_path = terminal_states[0].path[k] self.path[k] = paths[k] = possible_path + for conn in k.exits: + if self.is_small_door(conn): + door = conn.door if conn.door.smallKey else conn.door.controller + key_logic = self.world.key_logic[player][dungeon_name] + if door.name not in self.reached_doors[player]: + self.door_counter[player][0][dungeon_name] += 1 + self.reached_doors[player].add(door.name) + if key_logic.sm_doors[door]: + self.reached_doors[player].add(key_logic.sm_doors[door].name) missing_bc = {} for blocked, crystal in common_bc.items(): if (blocked not in bc and blocked.parent_region in rrp @@ -827,7 +857,7 @@ class CollectionState(object): return None def set_dungeon_limits(self, player, dungeon_name): - if self.world.retro[player] and self.world.mode[player] == 'standard': + if self.world.keyshuffle[player] == 'universal' and self.world.mode[player] == 'standard': self.dungeon_limits = ['Hyrule Castle', 'Agahnims Tower'] else: self.dungeon_limits = [dungeon_name] @@ -851,14 +881,16 @@ class CollectionState(object): door_candidates.append(door.name) return door_candidates door_candidates, skip = [], set() - if state.world.accessibility[player] != 'locations' and remaining_keys == 0 and dungeon_name in state.world.key_logic[player]: + if (state.world.accessibility[player] != 'locations' and remaining_keys == 0 and dungeon_name != 'Universal' + and state.placing_item and state.placing_item.name == small_key_name): key_logic = state.world.key_logic[player][dungeon_name] for door, paired in key_logic.sm_doors.items(): if door.name in key_logic.door_rules: rule = key_logic.door_rules[door.name] key = KeyRuleType.AllowSmall if (key in rule.new_rules and key_total >= rule.new_rules[key] and door.name not in skip - and door.name in state.reached_doors[player] and door.name not in state.opened_doors[player]): + and door.name in state.reached_doors[player] and door.name not in state.opened_doors[player] + and rule.small_location.item is None): if paired: door_candidates.append((door.name, paired.name)) skip.add(paired.name) @@ -880,6 +912,7 @@ class CollectionState(object): def copy(self): ret = CollectionState(self.world, skip_init=True) ret.prog_items = self.prog_items.copy() + ret.forced_keys = self.forced_keys.copy() ret.reachable_regions = {player: copy.copy(self.reachable_regions[player]) for player in range(1, self.world.players + 1)} ret.blocked_connections = {player: copy.copy(self.blocked_connections[player]) for player in range(1, self.world.players + 1)} ret.events = copy.copy(self.events) @@ -894,6 +927,7 @@ class CollectionState(object): player: defaultdict(dict, {name: copy.copy(checklist) for name, checklist in self.dungeons_to_check[player].items()}) for player in range(1, self.world.players + 1)} + ret.placing_item = self.placing_item return ret def apply_dungeon_exploration(self, rrp, player, dungeon_name, checklist): @@ -977,7 +1011,7 @@ class CollectionState(object): 'Golden Sword', 'Progressive Sword', 'Progressive Glove', 'Silver Arrows', 'Green Pendant', 'Blue Pendant', 'Red Pendant', 'Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 5', 'Crystal 6', 'Crystal 7', 'Blue Boomerang', 'Red Boomerang', 'Blue Shield', 'Red Shield', - 'Mirror Shield', 'Progressive Shield', 'Bug Catching Net', 'Cane of Byrna', 'Ocarina (Activated)' + 'Mirror Shield', 'Progressive Shield', 'Bug Catching Net', 'Cane of Byrna', 'Ocarina (Activated)', 'Boss Heart Container', 'Sanctuary Heart Container', 'Piece of Heart', 'Magic Upgrade (1/2)', 'Magic Upgrade (1/4)'] or item_name.startswith(('Bottle', 'Small Key', 'Big Key')) @@ -1018,7 +1052,7 @@ class CollectionState(object): new_locations = True while new_locations: reachable_events = [location for location in locations if location.event and - (not key_only or (not self.world.keyshuffle[location.item.player] and location.item.smallkey) or (not self.world.bigkeyshuffle[location.item.player] and location.item.bigkey)) + (not key_only or (self.world.keyshuffle[location.item.player] == 'none' and location.item.smallkey) or (not self.world.bigkeyshuffle[location.item.player] and location.item.bigkey)) and location.can_reach(self)] reachable_events = self._do_not_flood_the_keys(reachable_events) new_locations = False @@ -1078,7 +1112,7 @@ class CollectionState(object): return self.prog_items[item, player] >= count def has_sm_key(self, item, player, count=1): - if self.world.retro[player]: + if self.world.keyshuffle[player] == 'universal': if self.world.mode[player] == 'standard' and self.world.doorShuffle[player] == 'vanilla' and item == 'Small Key (Escape)': return True # Cannot access the shop until escape is finished. This is safe because the key is manually placed in make_custom_item_pool return self.can_buy_unlimited('Small Key (Universal)', player) @@ -1086,6 +1120,14 @@ class CollectionState(object): return (item, player) in self.prog_items return self.prog_items[item, player] >= count + def has_sm_key_strict(self, item, player, count=1): + if self.world.keyshuffle[player] == 'universal': + if self.world.mode[player] == 'standard' and self.world.doorShuffle[player] == 'vanilla' and item == 'Small Key (Escape)': + return True # Cannot access the shop until escape is finished. This is safe because the key is manually placed in make_custom_item_pool + return self.can_buy_unlimited('Small Key (Universal)', player) + obtained = self.prog_items[item, player] - self.forced_keys[item, player] + return obtained >= count + def can_buy_unlimited(self, item, player): for shop in self.world.shops[player]: if shop.region.player == player and shop.has_unlimited(item) and shop.region.can_reach(self): @@ -1118,6 +1160,12 @@ class CollectionState(object): def item_count(self, item, player): return self.prog_items[item, player] + def everything(self, player): + all_locations = self.world.get_filled_locations(player) + all_locations.remove(self.world.get_location('Ganon', player)) + return (len([x for x in self.locations_checked if x.player == player]) + >= len(all_locations)) + def has_crystals(self, count, player): crystals = ['Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 5', 'Crystal 6', 'Crystal 7'] return len([crystal for crystal in crystals if self.has(crystal, player)]) >= count @@ -1207,7 +1255,7 @@ class CollectionState(object): or self.has('Cane of Somaria', player)) def can_shoot_arrows(self, player): - if self.world.retro[player]: + if self.world.bow_mode[player] in ['retro', 'retro_silvers']: #todo: Non-progressive silvers grant wooden arrows, but progressive bows do not. Always require shop arrows to be safe return self.has('Bow', player) and (self.can_buy_unlimited('Single Arrow', player) or self.has('Single Arrow', player)) return self.has('Bow', player) @@ -1249,11 +1297,12 @@ class CollectionState(object): def can_flute(self, player): if self.world.mode[player] == 'standard' and not self.has('Zelda Delivered', player): - return False + return False # can't flute in rain state if any(map(lambda i: i.name in ['Ocarina', 'Ocarina (Activated)'], self.world.precollected_items)): return True lw = self.world.get_region('Kakariko Area', player) - return self.has('Ocarina (Activated)', player) or (self.has('Ocarina', player) and lw.can_reach(self) and self.is_not_bunny(lw, player)) + return self.has('Ocarina (Activated)', player) or (self.has('Ocarina', player) and lw.can_reach(self) + and self.is_not_bunny(lw, player)) def can_melt_things(self, player): return self.has('Fire Rod', player) or (self.has('Bombos', player) and self.has_sword(player)) @@ -1308,6 +1357,8 @@ class CollectionState(object): def collect(self, item, event=False, location=None): if location: self.locations_checked.add(location) + if item and item.smallkey and location.forced_item is not None: + self.forced_keys[item.name, item.player] += 1 if not item: return changed = False @@ -1496,14 +1547,14 @@ class Region(object): return False def can_fill(self, item): - inside_dungeon_item = ((item.smallkey and not self.world.keyshuffle[item.player]) + inside_dungeon_item = ((item.smallkey and self.world.keyshuffle[item.player] == 'none') or (item.bigkey and not self.world.bigkeyshuffle[item.player]) or (item.map and not self.world.mapshuffle[item.player]) or (item.compass and not self.world.compassshuffle[item.player])) - sewer_hack = self.world.mode[item.player] == 'standard' and item.name == 'Small Key (Escape)' - if sewer_hack or inside_dungeon_item: + # not all small keys to escape must be in escape + # sewer_hack = self.world.mode[item.player] == 'standard' and item.name == 'Small Key (Escape)' + if inside_dungeon_item: return self.dungeon and self.dungeon.is_dungeon_item(item) and item.player == self.player - return True def can_cause_bunny(self, player): @@ -1538,7 +1589,7 @@ class Entrance(object): def can_reach(self, state): # Destination Pickup OW Only No Ledges Can S&Q Allow Mirror - multi_step_locations = { 'Pyramid Area': ('Big Bomb', True, True, False, True), + multi_step_locations = { 'Pyramid Crack': ('Big Bomb', True, True, False, True), 'Missing Smith': ('Frog', True, False, True, True), 'Middle Aged Man': ('Dark Blacksmith Ruins', True, False, True, True), 'Old Man Drop Off': ('Lost Old Man', True, False, False, False), @@ -1557,7 +1608,13 @@ class Entrance(object): while len(self.temp_path): exit = self.temp_path.pop(0) path = (exit.name, (exit.parent_region.name, path)) - item_name = self.connected_region.locations[0].item.name if self.connected_region.locations[0].item else 'Deliver Item' + item_name = 'Deliver Item' + if len(self.connected_region.locations) > 0 and self.connected_region.locations[0].item: + item_name = self.connected_region.locations[0].item.name + for loc in self.parent_region.locations: + if loc.event and not loc.real and loc.item and loc.item.name.find('Farmable') < 0: + item_name = loc.item.name + break path = (item_name, (self.parent_region.name, path)) state.path[self] = (self.name, path) return True @@ -1602,45 +1659,39 @@ class Entrance(object): exits_to_traverse = list() found = False - if not found and allow_mirror_reentry and state.has('Magic Mirror', self.player): + if not found and allow_mirror_reentry and state.has_Mirror(self.player): # check for path using mirror portal re-entry at location of the follower pickup # this is checked first as this often the shortest path follower_region = start_region if follower_region.type not in [RegionType.LightWorld, RegionType.DarkWorld]: follower_region = [i for i in start_region.entrances if i.parent_region.name != 'Menu'][0].parent_region if (follower_region.world.mode[self.player] != 'inverted') == (follower_region.type == RegionType.LightWorld): - from OWEdges import OWTileRegions - from OverworldShuffle import ow_connections - owid = OWTileRegions[follower_region.name] - (mirror_map_orig, other_world) = ow_connections[owid % 0x40] - mirror_map = list(mirror_map_orig).copy() - mirror_map.extend(other_world) - mirror_exit = None - while len(mirror_map): + from OverworldShuffle import get_mirror_edges + mirror_map = get_mirror_edges(follower_region.world, follower_region, self.player) + while len(mirror_map) and not found: if mirror_map[0][1] == follower_region.name: mirror_exit = mirror_map[0][0] - break + mirror_region = follower_region.world.get_entrance(mirror_exit, self.player).parent_region + if mirror_exit and mirror_region: + if mirror_region.can_reach(state): + traverse_paths(mirror_region, self.parent_region) + break # no need to continue if there is no path from the mirror re-entry to dest mirror_map.pop(0) - if mirror_exit: - mirror_region = follower_region.world.get_entrance(mirror_exit, self.player).parent_region - if mirror_region.can_reach(state): - traverse_paths(mirror_region, self.parent_region) - if found: - path = state.path.get(mirror_region, (mirror_region.name, None)) - path = (follower_region.name, (mirror_exit, path)) - item_name = step_location.item.name if step_location.item else 'Pick Up Item' - if start_region.name != follower_region.name: - path = (start_region.name, (start_region.entrances[0].name, path)) - path = (f'{step_location.parent_region.name} Exit', ('Leave Item Area', (item_name, path))) - else: - path = (item_name, path) - path = ('Use Mirror Portal', (follower_region.name, path)) - while len(self.temp_path): - exit = self.temp_path.pop(0) - path = (exit.name, (exit.parent_region.name, path)) - item_name = self.connected_region.locations[0].item.name if self.connected_region.locations[0].item else 'Deliver Item' - path = (self.parent_region.name, path) - state.path[self] = (self.name, path) + if found: + path = state.path.get(mirror_region, (mirror_region.name, None)) + path = (follower_region.name, (mirror_exit, path)) + item_name = step_location.item.name if step_location.item else 'Pick Up Item' + if start_region.name != follower_region.name: + path = (start_region.name, (start_region.entrances[0].name, path)) + path = (f'{step_location.parent_region.name} Exit', ('Leave Item Area', (item_name, path))) + else: + path = (item_name, path) + path = ('Use Mirror Portal', (follower_region.name, path)) + while len(self.temp_path): + exit = self.temp_path.pop(0) + path = (exit.name, (exit.parent_region.name, path)) + path = (self.parent_region.name, path) + state.path[self] = (self.name, path) if not found: # check normal paths @@ -1651,7 +1702,7 @@ class Entrance(object): exit = self.parent_region.world.get_entrance('Links House S&Q', self.player) traverse_paths(exit.connected_region, self.parent_region, [exit]) - if not found and allow_mirror_reentry and state.has('Magic Mirror', self.player): + if not found and allow_mirror_reentry and state.has_Mirror(self.player): # check for paths using mirror portal re-entry at location of final destination # this is checked last as this is the most complicated/exhaustive check follower_region = start_region @@ -1662,14 +1713,9 @@ class Entrance(object): if dest_region.type not in [RegionType.LightWorld, RegionType.DarkWorld]: dest_region = start_region.entrances[0].parent_region if (dest_region.world.mode[self.player] != 'inverted') != (dest_region.type == RegionType.LightWorld): - from OWEdges import OWTileRegions - from OverworldShuffle import ow_connections - owid = OWTileRegions[dest_region.name] - (mirror_map_orig, other_world) = ow_connections.copy()[owid % 0x40] - mirror_map = list(mirror_map_orig).copy() - mirror_map.extend(other_world) - mirror_map = [(x, d) for (x, d) in mirror_map if x in [e.name for e in dest_region.exits]] # loop thru potential places to leave a mirror portal + from OverworldShuffle import get_mirror_edges + mirror_map = get_mirror_edges(dest_region.world, dest_region, self.player) while len(mirror_map) and not found: mirror_exit = dest_region.world.get_entrance(mirror_map[0][0], self.player) if mirror_exit.connected_region.type != dest_region.type: @@ -2140,6 +2186,9 @@ class Door(object): return world.get_room(self.roomIndex, self.player).kind(self) return None + def dungeon_name(self): + return self.entrance.parent_region.dungeon.name if self.entrance.parent_region.dungeon else 'Cave' + def __eq__(self, other): return isinstance(other, self.__class__) and self.name == other.name @@ -2276,7 +2325,6 @@ class Sector(object): self.item_logic = set() self.chest_location_set = set() - def region_set(self): if self.r_name_set is None: self.r_name_set = dict.fromkeys(map(lambda r: r.name, self.regions)) @@ -2558,7 +2606,7 @@ class Location(object): def gen_name(self): name = self.name world = self.parent_region.world if self.parent_region and self.parent_region.world else None - if self.parent_region.dungeon and world and world.doorShuffle[self.player] == 'crossed': + if self.parent_region.dungeon and world and world.doorShuffle[self.player] not in ['basic', 'vanilla']: name += f' @ {self.parent_region.dungeon.name}' if world and world.players > 1: name += f' ({world.get_player_names(self.player)})' @@ -2643,7 +2691,7 @@ class Item(object): return item_dungeon def is_inside_dungeon_item(self, world): - return ((self.smallkey and not world.keyshuffle[self.player]) + return ((self.smallkey and world.keyshuffle[self.player] == 'none') or (self.bigkey and not world.bigkeyshuffle[self.player]) or (self.compass and not world.compassshuffle[self.player]) or (self.map and not world.mapshuffle[self.player])) @@ -2654,6 +2702,11 @@ class Item(object): def __unicode__(self): return self.world.get_name_string_for_object(self) if self.world else f'{self.name} (Player {self.player})' + def __eq__(self, other): + if type(other) is str: + return self.name == other + return self.name == other.name and self.player == other.player + # have 6 address that need to be filled class Crystal(Item): @@ -2794,9 +2847,10 @@ class Spoiler(object): 'versions': {'Door':ERVersion, 'Overworld':ORVersion}, 'logic': self.world.logic, 'mode': self.world.mode, - 'retro': self.world.retro, 'bombbag': self.world.bombbag, 'weapons': self.world.swords, + 'flute_mode': self.world.flute_mode, + 'bow_mode': self.world.bow_mode, 'goal': self.world.goal, 'ow_shuffle': self.world.owShuffle, 'ow_terrain': self.world.owTerrain, @@ -2809,9 +2863,15 @@ class Spoiler(object): 'shuffle': self.world.shuffle, 'shuffleganon': self.world.shuffle_ganon, 'shufflelinks': self.world.shufflelinks, + 'shuffletavern': self.world.shuffletavern, + 'take_any': self.world.take_any, 'overworld_map': self.world.overworld_map, 'door_shuffle': self.world.doorShuffle, 'intensity': self.world.intensity, + 'door_type_mode': self.world.door_type_mode, + 'trap_door_mode': self.world.trap_door_mode, + 'key_logic': self.world.key_logic_algorithm, + 'decoupledoors': self.world.decoupledoors, 'dungeon_counters': self.world.dungeon_counters, 'item_pool': self.world.difficulty, 'item_functionality': self.world.difficulty_adjustments, @@ -2996,10 +3056,9 @@ class Spoiler(object): outfile.write('Settings Code:'.ljust(line_width) + '%s\n' % self.metadata["code"][player]) outfile.write('Logic:'.ljust(line_width) + '%s\n' % self.metadata['logic'][player]) outfile.write('Mode:'.ljust(line_width) + '%s\n' % self.metadata['mode'][player]) - outfile.write('Retro:'.ljust(line_width) + '%s\n' % yn(self.metadata['retro'][player])) outfile.write('Swords:'.ljust(line_width) + '%s\n' % self.metadata['weapons'][player]) outfile.write('Goal:'.ljust(line_width) + '%s\n' % self.metadata['goal'][player]) - if self.metadata['goal'][player] in ['triforcehunt', 'trinity']: + if self.metadata['goal'][player] in ['triforcehunt', 'trinity', 'ganonhunt']: outfile.write('Triforce Pieces Required:'.ljust(line_width) + '%s\n' % self.metadata['triforcegoal'][player]) outfile.write('Triforce Pieces Total:'.ljust(line_width) + '%s\n' % self.metadata['triforcepool'][player]) outfile.write('Crystals Required for GT:'.ljust(line_width) + '%s\n' % str(self.world.crystals_gt_orig[player])) @@ -3008,6 +3067,9 @@ class Spoiler(object): outfile.write('Restricted Boss Items:'.ljust(line_width) + '%s\n' % self.metadata['restricted_boss_items'][player]) outfile.write('Difficulty:'.ljust(line_width) + '%s\n' % self.metadata['item_pool'][player]) outfile.write('Item Functionality:'.ljust(line_width) + '%s\n' % self.metadata['item_functionality'][player]) + outfile.write('Flute Mode:'.ljust(line_width) + '%s\n' % self.metadata['flute_mode'][player]) + outfile.write('Bow Mode:'.ljust(line_width) + '%s\n' % self.metadata['bow_mode'][player]) + outfile.write('Take Any Caves:'.ljust(line_width) + '%s\n' % self.metadata['take_any'][player]) outfile.write('Shopsanity:'.ljust(line_width) + '%s\n' % yn(self.metadata['shopsanity'][player])) outfile.write('Bombbag:'.ljust(line_width) + '%s\n' % yn(self.metadata['bombbag'][player])) outfile.write('Pseudoboots:'.ljust(line_width) + '%s\n' % yn(self.metadata['pseudoboots'][player])) @@ -3025,12 +3087,17 @@ class Spoiler(object): if self.metadata['shuffle'][player] != 'vanilla': outfile.write('Shuffle GT/Ganon:'.ljust(line_width) + '%s\n' % yn(self.metadata['shuffleganon'][player])) outfile.write('Shuffle Links:'.ljust(line_width) + '%s\n' % yn(self.metadata['shufflelinks'][player])) + outfile.write('Shuffle Tavern:'.ljust(line_width) + '%s\n' % yn(self.metadata['shuffletavern'][player])) if self.metadata['shuffle'][player] != 'vanilla' or self.metadata['ow_mixed'][player]: outfile.write('Overworld Map:'.ljust(line_width) + '%s\n' % self.metadata['overworld_map'][player]) outfile.write('Pyramid Hole Pre-opened:'.ljust(line_width) + '%s\n' % self.metadata['open_pyramid'][player]) outfile.write('Door Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['door_shuffle'][player]) if self.metadata['door_shuffle'][player] != 'vanilla': outfile.write('Intensity:'.ljust(line_width) + '%s\n' % self.metadata['intensity'][player]) + outfile.write('Door Type Mode:'.ljust(line_width) + '%s\n' % self.metadata['door_type_mode'][player]) + outfile.write('Trap Door Mode:'.ljust(line_width) + '%s\n' % self.metadata['trap_door_mode'][player]) + outfile.write('Key Logic Algorithm:'.ljust(line_width) + '%s\n' % self.metadata['key_logic'][player]) + outfile.write('Decouple Doors:'.ljust(line_width) + '%s\n' % yn(self.metadata['decoupledoors'][player])) outfile.write('Experimental:'.ljust(line_width) + '%s\n' % yn(self.metadata['experimental'][player])) outfile.write('Dungeon Counters:'.ljust(line_width) + '%s\n' % self.metadata['dungeon_counters'][player]) outfile.write('Enemy Drop Shuffle:'.ljust(line_width) + '%s\n' % yn(self.metadata['dropshuffle'][player])) @@ -3038,13 +3105,14 @@ class Spoiler(object): outfile.write('Pot Shuffle (Legacy):'.ljust(line_width) + '%s\n' % yn(self.metadata['potshuffle'][player])) outfile.write('Map Shuffle:'.ljust(line_width) + '%s\n' % yn(self.metadata['mapshuffle'][player])) outfile.write('Compass Shuffle:'.ljust(line_width) + '%s\n' % yn(self.metadata['compassshuffle'][player])) - outfile.write('Small Key Shuffle:'.ljust(line_width) + '%s\n' % yn(self.metadata['keyshuffle'][player])) + outfile.write('Small Key Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['keyshuffle'][player]) outfile.write('Big Key Shuffle:'.ljust(line_width) + '%s\n' % yn(self.metadata['bigkeyshuffle'][player])) outfile.write('Boss Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['boss_shuffle'][player]) outfile.write('Enemy Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['enemy_shuffle'][player]) outfile.write('Enemy Health:'.ljust(line_width) + '%s\n' % self.metadata['enemy_health'][player]) outfile.write('Enemy Damage:'.ljust(line_width) + '%s\n' % self.metadata['enemy_damage'][player]) outfile.write('Hints:'.ljust(line_width) + '%s\n' % yn(self.metadata['hints'][player])) + outfile.write('Race:'.ljust(line_width) + '%s\n' % yn(self.world.settings.world_rep['meta']['race'])) if self.startinventory: outfile.write('Starting Inventory:'.ljust(line_width)) @@ -3311,28 +3379,29 @@ class Pot(object): return hash((self.x, self.y, self.room)) -# byte 0: DDOO OEEE (DR, OR, ER) -dr_mode = {"basic": 1, "crossed": 2, "vanilla": 0} -or_mode = {"vanilla": 0, "parallel": 1, "full": 1} -er_mode = {"vanilla": 0, "simple": 1, "restricted": 2, "full": 3, "lite": 4, "lean": 5, "crossed": 6, "insanity": 7, "dungeonsfull": 8, "dungeonssimple": 9} +# byte 0: DDDE EEEE (DR, ER) +dr_mode = {"basic": 1, "crossed": 2, "vanilla": 0, "partitioned": 3} +er_mode = {"vanilla": 0, "simple": 1, "restricted": 2, "full": 3, "crossed": 4, "insanity": 5, 'lite': 8, + 'lean': 9, "dungeonsfull": 7, "dungeonssimple": 6} -# byte 1: LLLW WSSR (logic, mode, sword, retro) +# byte 1: LLLW WSS? (logic, mode, sword) logic_mode = {"noglitches": 0, "minorglitches": 1, "nologic": 2, "owglitches": 3, "majorglitches": 4} world_mode = {"open": 0, "standard": 1, "inverted": 2} sword_mode = {"random": 0, "assured": 1, "swordless": 2, "vanilla": 3} # byte 2: GGGD DFFH (goal, diff, item_func, hints) -goal_mode = {"ganon": 0, "pedestal": 1, "dungeons": 2, "triforcehunt": 3, "crystals": 4, "trinity": 5} +goal_mode = {'ganon': 0, 'pedestal': 1, 'dungeons': 2, 'triforcehunt': 3, 'crystals': 4, 'trinity': 5, + 'ganonhunt': 6, 'completionist': 7} diff_mode = {"normal": 0, "hard": 1, "expert": 2} func_mode = {"normal": 0, "hard": 1, "expert": 2} -# byte 3: S?MM PIII (shop, unused, mixed, palettes, intensity) +# byte 3: SDMM PIII (shop, decouple doors, mixed, palettes, intensity) # keydrop now has it's own byte mixed_travel_mode = {"prevent": 0, "allow": 1, "force": 2} # intensity is 3 bits (reserves 4-7 levels) -# new byte 4: ?DDD PPPP (unused, drop, pottery) -# dropshuffle reserves 2 bits, pottery needs 2 but reserves 2 for future modes) +# new byte 4: TDDD PPPP (tavern shuffle, drop, pottery) +# dropshuffle reserves 2 bits, pottery needs 4) pottery_mode = {'none': 0, 'keys': 2, 'lottery': 3, 'dungeon': 4, 'cave': 5, 'cavekeys': 6, 'reduced': 7, 'clustered': 8, 'nonempty': 9} @@ -3342,7 +3411,8 @@ counter_mode = {"default": 0, "off": 1, "on": 2, "pickup": 3} # byte 6: CCCC CPAA (crystals ganon, pyramid, access access_mode = {"items": 0, "locations": 1, "none": 2} -# byte 7: BSMC ??EE (big, small, maps, compass, bosses, enemies) +# byte 7: B?MC DDEE (big, ?, maps, compass, door_type, enemies) +door_type_mode = {'original': 0, 'big': 1, 'all': 2, 'chaos': 3} enemy_mode = {"none": 0, "shuffled": 1, "chaos": 2, "random": 2, "legacy": 3} # byte 8: HHHD DPBS (enemy_health, enemy_dmg, potshuffle, bomb logic, shuffle links) @@ -3356,12 +3426,31 @@ rb_mode = {"none": 0, "mapcompass": 1, "dungeon": 2} algo_mode = {"balanced": 0, "equitable": 1, "vanilla_fill": 2, "dungeon_only": 3, "district": 4, 'major_only': 5} boss_mode = {"none": 0, "simple": 1, "full": 2, "chaos": 3, 'random': 3, 'unique': 4} +# byte 10: settings_version + +# byte 11: OOOT WCCC (OWR layout, free terrain, whirlpools, OWR crossed) +or_mode = {"vanilla": 0, "parallel": 1, "full": 2} +orcrossed_mode = {"none": 0, "polar": 1, "grouped": 2, "limited": 3, "chaos": 4} + +# byte 12: KMB? FF?? (keep similar, mixed/tile flip, bonk drops, flute spots) +flutespot_mode = {"vanilla": 0, "balanced": 1, "random": 2} + +# byte 13: FBBB TTSS (flute_mode, bow_mode, take_any, small_key_mode) +flute_mode = {'normal': 0, 'active': 1} +keyshuffle_mode = {'none': 0, 'wild': 1, 'universal': 2} # reserved 8 modes? +take_any_mode = {'none': 0, 'random': 1, 'fixed': 2} +bow_mode = {'progressive': 0, 'silvers': 1, 'retro': 2, 'retro_silvers': 3} + # additions -# psuedoboots does not effect code -# sfx_shuffle and other adjust items does not effect settings code +# byte 14: POOT TKKK (pseudoboots, overworld_map, trap_door_mode, key_logic_algo) +overworld_map_mode = {'default': 0, 'compass': 1, 'map': 2} +trap_door_mode = {'vanilla': 0, 'optional': 1, 'boss': 2, 'oneway': 3} +key_logic_algo = {'default': 0, 'partial': 1, 'strict': 2} + +# sfx_shuffle and other adjust items does not affect settings code # Bump this when making changes that are not backwards compatible (nearly all of them) -settings_version = 0 +settings_version = 1 class Settings(object): @@ -3369,43 +3458,57 @@ class Settings(object): @staticmethod def make_code(w, p): code = bytes([ - (dr_mode[w.doorShuffle[p]] << 6) | (or_mode[w.owShuffle[p]] << 5) | (0x10 if w.owCrossed[p] != 'none' else 0) | (0x08 if w.owMixed[p] else 0) | er_mode[w.shuffle[p]], + (dr_mode[w.doorShuffle[p]] << 5) | er_mode[w.shuffle[p]], (logic_mode[w.logic[p]] << 5) | (world_mode[w.mode[p]] << 3) - | (sword_mode[w.swords[p]] << 1) | (1 if w.retro[p] else 0), + | (sword_mode[w.swords[p]] << 1), (goal_mode[w.goal[p]] << 5) | (diff_mode[w.difficulty[p]] << 3) | (func_mode[w.difficulty_adjustments[p]] << 1) | (1 if w.hints[p] else 0), - (0x80 if w.shopsanity[p] else 0) | (mixed_travel_mode[w.mixed_travel[p]] << 4) + (0x80 if w.shopsanity[p] else 0) | (0x40 if w.decoupledoors[p] else 0) + | (mixed_travel_mode[w.mixed_travel[p]] << 4) | (0x8 if w.standardize_palettes[p] == "original" else 0) | (0 if w.intensity[p] == "random" else w.intensity[p]), - (0x10 if w.dropshuffle[p] else 0) | (pottery_mode[w.pottery[p]]), + (0x80 if w.shuffletavern[p] else 0) | (0x10 if w.dropshuffle[p] else 0) | (pottery_mode[w.pottery[p]]), ((8 if w.crystals_gt_orig[p] == "random" else int(w.crystals_gt_orig[p])) << 3) | (counter_mode[w.dungeon_counters[p]] << 1) | (1 if w.experimental[p] else 0), ((8 if w.crystals_ganon_orig[p] == "random" else int(w.crystals_ganon_orig[p])) << 3) - | (0x4 if w.is_pyramid_open(p) else 0) | access_mode[w.accessibility[p]], + | (0x4 if w.open_pyramid[p] else 0) | access_mode[w.accessibility[p]], - (0x80 if w.bigkeyshuffle[p] else 0) | (0x40 if w.keyshuffle[p] else 0) + (0x80 if w.bigkeyshuffle[p] else 0) | (0x20 if w.mapshuffle[p] else 0) | (0x10 if w.compassshuffle[p] else 0) - | (enemy_mode[w.enemy_shuffle[p]]), + | (door_type_mode[w.door_type_mode[p]] << 2) | (enemy_mode[w.enemy_shuffle[p]]), (e_health[w.enemy_health[p]] << 5) | (e_dmg[w.enemy_damage[p]] << 3) | (0x4 if w.potshuffle[p] else 0) | (0x2 if w.bombbag[p] else 0) | (1 if w.shufflelinks[p] else 0), (rb_mode[w.restrict_boss_items[p]] << 6) | (algo_mode[w.algorithm] << 3) | (boss_mode[w.boss_shuffle[p]]), - settings_version]) + settings_version, + + (or_mode[w.owShuffle[p]] << 5) | (0x10 if w.owTerrain[p] else 0) + | (0x08 if w.owWhirlpoolShuffle[p] else 0) | orcrossed_mode[w.owCrossed[p]], + + (0x80 if w.owKeepSimilar[p] else 0) | (0x40 if w.owMixed[p] else 0) + | (0x20 if w.shuffle_bonk_drops[p] else 0) | (flutespot_mode[w.owFluteShuffle[p]] << 4), + + (flute_mode[w.flute_mode[p]] << 7 | bow_mode[w.bow_mode[p]] << 4 + | take_any_mode[w.take_any[p]] << 2 | keyshuffle_mode[w.keyshuffle[p]]), + + ((0x80 if w.pseudoboots[p] else 0) | overworld_map_mode[w.overworld_map[p]] << 6 + | trap_door_mode[w.trap_door_mode[p]] << 4 | key_logic_algo[w.key_logic_algorithm[p]]), + ]) return base64.b64encode(code, "+-".encode()).decode() @staticmethod def adjust_args_from_code(code, player, args): settings, p = base64.b64decode(code.encode(), "+-".encode()), player - if len(settings) < 11: + if len(settings) < 14: raise Exception('Provided code is incompatible with this version') if settings[10] != settings_version: raise Exception('Provided code is incompatible with this version') @@ -3413,9 +3516,8 @@ class Settings(object): def r(d): return {y: x for x, y in d.items()} - args.shuffle[p] = r(er_mode)[settings[0] & 0x0F] - args.ow_shuffle[p] = r(or_mode)[(settings[0] & 0x30) >> 4] - args.door_shuffle[p] = r(dr_mode)[(settings[0] & 0xC0) >> 6] + args.shuffle[p] = r(er_mode)[settings[0] & 0x1F] + args.door_shuffle[p] = r(dr_mode)[(settings[0] & 0xE0) >> 5] args.logic[p] = r(logic_mode)[(settings[1] & 0xE0) >> 5] args.mode[p] = r(world_mode)[(settings[1] & 0x18) >> 3] args.swords[p] = r(sword_mode)[(settings[1] & 0x6) >> 1] @@ -3423,16 +3525,16 @@ class Settings(object): args.item_functionality[p] = r(func_mode)[(settings[2] & 0x6) >> 1] args.goal[p] = r(goal_mode)[(settings[2] & 0xE0) >> 5] args.accessibility[p] = r(access_mode)[settings[6] & 0x3] - args.retro[p] = True if settings[1] & 0x01 else False + # args.retro[p] = True if settings[1] & 0x01 else False args.hints[p] = True if settings[2] & 0x01 else False args.shopsanity[p] = True if settings[3] & 0x80 else False - # args.keydropshuffle[p] = True if settings[3] & 0x40 else False + args.decoupledoors[p] = True if settings[3] & 0x40 else False args.mixed_travel[p] = r(mixed_travel_mode)[(settings[3] & 0x30) >> 4] args.standardize_palettes[p] = "original" if settings[3] & 0x8 else "standardize" intensity = settings[3] & 0x7 args.intensity[p] = "random" if intensity == 0 else intensity - # args.shuffleswitches[p] = True if settings[4] & 0x80 else False + args.shuffletavern[p] = True if settings[4] & 0x80 else False args.dropshuffle[p] = True if settings[4] & 0x10 else False args.pottery[p] = r(pottery_mode)[settings[4] & 0x0F] @@ -3446,10 +3548,10 @@ class Settings(object): args.openpyramid[p] = True if settings[6] & 0x4 else False args.bigkeyshuffle[p] = True if settings[7] & 0x80 else False - args.keyshuffle[p] = True if settings[7] & 0x40 else False + # args.keyshuffle[p] = True if settings[7] & 0x40 else False args.mapshuffle[p] = True if settings[7] & 0x20 else False args.compassshuffle[p] = True if settings[7] & 0x10 else False - # args.shufflebosses[p] = r(boss_mode)[(settings[7] & 0xc) >> 2] + args.door_type_mode[p] = r(door_type_mode)[(settings[7] & 0xc) >> 2] args.shuffleenemies[p] = r(enemy_mode)[settings[7] & 0x3] args.enemy_health[p] = r(e_health)[(settings[8] & 0xE0) >> 5] @@ -3457,11 +3559,34 @@ class Settings(object): args.shufflepots[p] = True if settings[8] & 0x4 else False args.bombbag[p] = True if settings[8] & 0x2 else False args.shufflelinks[p] = True if settings[8] & 0x1 else False + if len(settings) > 9: args.restrict_boss_items[p] = r(rb_mode)[(settings[9] & 0xC0) >> 6] args.algorithm = r(algo_mode)[(settings[9] & 0x38) >> 3] args.shufflebosses[p] = r(boss_mode)[(settings[9] & 0x07)] + args.ow_shuffle[p] = r(or_mode)[(settings[11] & 0xE0) >> 5] + args.ow_terrain[p] = True if settings[11] & 0x10 else False + args.ow_whirlpool[p] = True if settings[11] & 0x08 else False + args.ow_crossed[p] = r(orcrossed_mode)[(settings[11] & 0x07)] + + args.ow_keepsimilar[p] = True if settings[12] & 0x80 else False + args.ow_mixed[p] = True if settings[12] & 0x40 else False + args.bonk_drops[p] = True if settings[12] & 0x20 else False + args.ow_fluteshuffle[p] = r(flutespot_mode)[(settings[12] & 0x0C) >> 2] + + if len(settings) > 13: + args.flute_mode[p] = r(flute_mode)[(settings[13] & 0x80) >> 7] + args.bow_mode[p] = r(bow_mode)[(settings[13] & 0x70) >> 4] + args.take_any[p] = r(take_any_mode)[(settings[13] & 0xC) >> 2] + args.keyshuffle[p] = r(keyshuffle_mode)[settings[13] & 0x3] + + if len(settings) > 14: + args.pseudoboots[p] = True if settings[14] & 0x80 else False + args.overworld_map[p] = r(overworld_map_mode)[(settings[14] & 0x60) >> 6] + args.trap_door_mode[p] = r(trap_door_mode)[(settings[14] & 0x14) >> 4] + args.key_logic_algorithm[p] = r(key_logic_algo)[settings[14] & 0x07] + class KeyRuleType(FastEnum): WorstCase = 0 diff --git a/Bosses.py b/Bosses.py index d84df921..7052bb5c 100644 --- a/Bosses.py +++ b/Bosses.py @@ -164,6 +164,22 @@ def place_bosses(world, player): all_bosses = sorted(boss_table.keys()) #s orted to be deterministic on older pythons placeable_bosses = [boss for boss in all_bosses if boss not in ['Agahnim', 'Agahnim2', 'Ganon']] + used_bosses = [] + + if world.customizer and world.customizer.get_bosses(): + custom_bosses = world.customizer.get_bosses() + if player in custom_bosses: + for location, boss in custom_bosses[player].items(): + level = None + if '(' in location: + i = location.find('(') + level = location[i+1:location.find(')')] + location = location[:i-1] + if can_place_boss(world, player, boss, location, level): + loc_text = location + (' ('+level+')' if level else '') + place_boss(boss, level, location, loc_text, world, player) + boss_locations.remove([location, level]) + used_bosses.append((boss, level)) # temporary hack for swordless kholdstare: if world.boss_shuffle[player] in ["simple", "full", "unique"]: @@ -178,6 +194,8 @@ def place_bosses(world, player): bosses = placeable_bosses + ['Armos Knights', 'Lanmolas', 'Moldorm'] else: # all bosses present, the three duplicates chosen at random bosses = placeable_bosses + random.sample(placeable_bosses, 3) + for u, level in used_bosses: + placeable_bosses.remove(u) logging.getLogger('').debug('Bosses chosen %s', bosses) @@ -201,6 +219,9 @@ def place_bosses(world, player): place_boss(boss, level, loc, loc_text, world, player) elif world.boss_shuffle[player] == 'unique': bosses = list(placeable_bosses) + for u, level in used_bosses: + if not level: + bosses.remove(u) gt_bosses = [] for [loc, level] in boss_locations: diff --git a/CHANGELOG.md b/CHANGELOG.md index e5301933..48a0a29b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.3.0.0 +- Merged in all DR Customizer features since its initial release up to v1.2.0.9 +- Major revamp of Aerinon's ER 2.0 to better support OWR modes +- Fixed various incorrect logic issues (Inverted flute spots, Bomb Shop start, etc) +- Flute is disabled in rain state except glitched modes + ## 0.2.11.4 - Fixed broken OWG logic issues - Gave chickens a base price for shopsanity diff --git a/CLI.py b/CLI.py index bfd09183..ad1b157c 100644 --- a/CLI.py +++ b/CLI.py @@ -9,6 +9,7 @@ import sys from source.classes.BabelFish import BabelFish from Utils import update_deprecated_args +from source.classes.CustomSettings import CustomSettings class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter): @@ -32,11 +33,25 @@ def parse_cli(argv, no_defaults=False): parser = argparse.ArgumentParser(add_help=False) parser.add_argument('--settingsfile', help="input json file of settings", type=str) parser.add_argument('--multi', default=defval(settings["multi"]), type=lambda value: min(max(int(value), 1), 255)) + parser.add_argument('--customizer', help='input yaml file for customizations', type=str) + parser.add_argument('--print_custom_yaml', help='print example yaml for current settings', + default=False, action="store_true") + parser.add_argument('--mystery', dest="mystery", default=False, action="store_true") + multiargs, _ = parser.parse_known_args(argv) if multiargs.settingsfile: settings = apply_settings_file(settings, multiargs.settingsfile) + player_num = multiargs.multi + if multiargs.customizer: + custom = CustomSettings() + custom.load_yaml(multiargs.customizer) + cp = custom.determine_players() + if cp: + player_num = cp + + parser = argparse.ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter) # get args @@ -78,38 +93,56 @@ def parse_cli(argv, no_defaults=False): parser.add_argument('--securerandom', default=defval(settings["securerandom"]), action='store_true') parser.add_argument('--teams', default=defval(1), type=lambda value: max(int(value), 1)) parser.add_argument('--settingsfile', dest="filename", help="input json file of settings", type=str) + parser.add_argument('--customizer', dest="customizer", help='input yaml file for customizations', type=str) + parser.add_argument('--print_custom_yaml', dest="print_custom_yaml", default=False, action="store_true") - if multiargs.multi: - for player in range(1, multiargs.multi + 1): + if player_num: + for player in range(1, player_num + 1): parser.add_argument(f'--p{player}', default=defval(''), help=argparse.SUPPRESS) ret = parser.parse_args(argv) if ret.keysanity: - ret.mapshuffle, ret.compassshuffle, ret.keyshuffle, ret.bigkeyshuffle = [True] * 4 + ret.mapshuffle, ret.compassshuffle, ret.bigkeyshuffle = [True] * 3 + ret.keyshuffle = 'wild' if ret.keydropshuffle: ret.dropshuffle = True ret.pottery = 'keys' if ret.pottery == 'none' else ret.pottery - if multiargs.multi: + if ret.retro or ret.mode == 'retro': + if ret.bow_mode == 'progressive': + ret.bow_mode = 'retro' + elif ret.bow_mode == 'silvers': + ret.bow_mode = 'retro_silvers' + ret.take_any = 'random' if ret.take_any == 'none' else ret.take_any + ret.keyshuffle = 'universal' + + if player_num: defaults = copy.deepcopy(ret) - for player in range(1, multiargs.multi + 1): + for player in range(1, player_num + 1): playerargs = parse_cli(shlex.split(getattr(ret, f"p{player}")), True) + + if playerargs.filename: + playersettings = apply_settings_file({}, playerargs.filename) + for k, v in playersettings.items(): + setattr(playerargs, k, v) for name in ['logic', 'mode', 'swords', 'goal', 'difficulty', 'item_functionality', 'ow_shuffle', 'ow_terrain', 'ow_crossed', 'ow_keepsimilar', 'ow_mixed', 'ow_whirlpool', 'ow_fluteshuffle', + 'flute_mode', 'bow_mode', 'take_any', 'boots_hint', 'shuffle', 'door_shuffle', 'intensity', 'crystals_ganon', 'crystals_gt', 'openpyramid', 'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory', 'usestartinventory', 'bombbag', 'shuffleganon', 'overworld_map', 'restrict_boss_items', 'triforce_pool_min', 'triforce_pool_max', 'triforce_goal_min', 'triforce_goal_max', - 'triforce_min_difference', 'triforce_goal', 'triforce_pool', 'shufflelinks', 'pseudoboots', - 'retro', 'accessibility', 'hints', 'beemizer', 'experimental', 'dungeon_counters', + 'triforce_min_difference', 'triforce_goal', 'triforce_pool', 'shufflelinks', 'shuffletavern', + 'pseudoboots', 'retro', 'accessibility', 'hints', 'beemizer', 'experimental', 'dungeon_counters', 'shufflebosses', 'shuffleenemies', 'enemy_health', 'enemy_damage', 'shufflepots', 'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor', 'heartbeep', 'remote_items', 'shopsanity', 'dropshuffle', 'pottery', 'keydropshuffle', 'mixed_travel', 'standardize_palettes', 'code', 'reduce_flashing', 'shuffle_sfx', - 'msu_resume', 'collection_rate', 'colorizepots', 'bonk_drops']: + 'msu_resume', 'collection_rate', 'colorizepots', 'decoupledoors', 'door_type_mode', + 'bonk_drops', 'trap_door_mode', 'key_logic_algorithm']: value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name) if player == 1: setattr(ret, name, {1: value}) @@ -135,19 +168,22 @@ def parse_settings(): "retro": False, "bombbag": False, "mode": "open", + "boots_hint": False, "logic": "noglitches", "goal": "ganon", "crystals_gt": "7", "crystals_ganon": "7", "swords": "random", + "flute_mode": "normal", + "bow_mode": "progressive", "difficulty": "normal", "item_functionality": "normal", "timer": "none", "progressive": "on", "accessibility": "items", "algorithm": "balanced", - 'mystery': False, - 'suppress_meta': False, + "mystery": False, + "suppress_meta": False, "restrict_boss_items": "none", # Shuffle Ganon defaults to TRUE @@ -163,7 +199,9 @@ def parse_settings(): "bonk_drops": False, "shuffle": "vanilla", "shufflelinks": False, + "shuffletavern": False, "overworld_map": "default", + "take_any": "none", "pseudoboots": False, "shuffleenemies": "none", @@ -173,18 +211,22 @@ def parse_settings(): "enemizercli": os.path.join(".", "EnemizerCLI", "EnemizerCLI.Core"), "shopsanity": False, - 'keydropshuffle': False, - 'dropshuffle': False, - 'pottery': 'none', - 'colorizepots': False, - 'shufflepots': False, + "keydropshuffle": False, + "dropshuffle": False, + "pottery": "none", + "colorizepots": False, + "shufflepots": False, "mapshuffle": False, "compassshuffle": False, - "keyshuffle": False, + "keyshuffle": "none", "bigkeyshuffle": False, "keysanity": False, "door_shuffle": "vanilla", "intensity": 3, + "door_type_mode": "original", + "trap_door_mode": "optional", + "key_logic_algorithm": "default", + "decoupledoors": False, "experimental": False, "dungeon_counters": "default", "mixed_travel": "prevent", @@ -214,8 +256,8 @@ def parse_settings(): "uw_palettes": "default", "reduce_flashing": False, "shuffle_sfx": False, - 'msu_resume': False, - 'collection_rate': False, + "msu_resume": False, + "collection_rate": False, # Spoiler defaults to TRUE # Playthrough defaults to TRUE diff --git a/DoorShuffle.py b/DoorShuffle.py index 145ff35d..8e65a4cc 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -4,19 +4,23 @@ import logging import time from enum import unique, Flag from typing import DefaultDict, Dict, List +from itertools import chain -from BaseClasses import RegionType, Region, Door, DoorType, Direction, Sector, CrystalBarrier, DungeonInfo, dungeon_keys -from BaseClasses import PotFlags, LocationType +from BaseClasses import RegionType, Region, Door, DoorType, Sector, CrystalBarrier, DungeonInfo, dungeon_keys +from BaseClasses import PotFlags, LocationType, Direction from Doors import reset_portals from Dungeons import dungeon_regions, region_starts, standard_starts, split_region_starts from Dungeons import dungeon_bigs, dungeon_hints from Items import ItemFactory from RoomData import DoorKind, PairedDoor, reset_rooms -from DungeonGenerator import ExplorationState, convert_regions, generate_dungeon, pre_validate, determine_required_paths, drop_entrances +from source.dungeon.DungeonStitcher import GenerationException, generate_dungeon +from source.dungeon.DungeonStitcher import ExplorationState as ExplorationState2 +from DungeonGenerator import ExplorationState, convert_regions, determine_required_paths, drop_entrances from DungeonGenerator import create_dungeon_builders, split_dungeon_builder, simple_dungeon_builder, default_dungeon_entrances -from DungeonGenerator import dungeon_portals, dungeon_drops, GenerationException -from DungeonGenerator import valid_region_to_explore as valid_region_to_explore_lim +from DungeonGenerator import dungeon_portals, dungeon_drops, connect_doors, count_reserved_locations +from DungeonGenerator import valid_region_to_explore from KeyDoorShuffle import analyze_dungeon, build_key_layout, validate_key_layout, determine_prize_lock +from KeyDoorShuffle import validate_bk_layout from Utils import ncr, kth_combination @@ -84,7 +88,7 @@ def link_doors_prep(world, player): find_inaccessible_regions(world, player) - if world.intensity[player] >= 3 and world.doorShuffle[player] in ['basic', 'crossed']: + if world.intensity[player] >= 3 and world.doorShuffle[player] != 'vanilla': choose_portals(world, player) else: if world.shuffle[player] == 'vanilla': @@ -125,15 +129,27 @@ def link_doors_prep(world, player): vanilla_key_logic(world, player) -def link_doors_main(world, player): +def create_dungeon_pool(world, player): + pool = None if world.doorShuffle[player] == 'basic': - within_dungeon(world, player) + pool = [([name], regions) for name, regions in dungeon_regions.items()] + elif world.doorShuffle[player] == 'partitioned': + groups = [['Hyrule Castle', 'Eastern Palace', 'Desert Palace', 'Tower of Hera', 'Agahnims Tower'], + ['Palace of Darkness', 'Swamp Palace', 'Skull Woods', 'Thieves Town'], + ['Ice Palace', 'Misery Mire', 'Turtle Rock', 'Ganons Tower']] + pool = [(group, list(chain.from_iterable([dungeon_regions[d] for d in group]))) for group in groups] elif world.doorShuffle[player] == 'crossed': - cross_dungeon(world, player) + pool = [(list(dungeon_regions.keys()), sum((r for r in dungeon_regions.values()), []))] elif world.doorShuffle[player] != 'vanilla': logging.getLogger('').error('Invalid door shuffle setting: %s' % world.doorShuffle[player]) raise Exception('Invalid door shuffle setting: %s' % world.doorShuffle[player]) + return pool + +def link_doors_main(world, player): + pool = create_dungeon_pool(world, player) + if pool: + main_dungeon_pool(pool, world, player) if world.doorShuffle[player] != 'vanilla': create_door_spoiler(world, player) @@ -161,6 +177,9 @@ def mark_regions(world, player): def create_door_spoiler(world, player): logger = logging.getLogger('') + shuffled_door_types = [DoorType.Normal, DoorType.SpiralStairs] + if world.intensity[player] > 1: + shuffled_door_types += [DoorType.Open, DoorType.StraightStairs, DoorType.Ladder] queue = deque(world.dungeon_layouts[player].values()) while len(queue) > 0: @@ -174,20 +193,23 @@ def create_door_spoiler(world, player): for ext in next.exits: door_a = ext.door connect = ext.connected_region - if door_a and door_a.type in [DoorType.Normal, DoorType.SpiralStairs, DoorType.Open, - DoorType.StraightStairs, DoorType.Ladder] and door_a not in done: + if door_a and door_a.type in shuffled_door_types and door_a not in done: done.add(door_a) + door_b = door_a.dest if door_b and not isinstance(door_b, Region): - done.add(door_b) - if not door_a.blocked and not door_b.blocked: - world.spoiler.set_door(door_a.name, door_b.name, 'both', player, builder.name) - elif door_a.blocked: - world.spoiler.set_door(door_b.name, door_a.name, 'entrance', player, builder.name) - elif door_b.blocked: + if world.decoupledoors[player]: world.spoiler.set_door(door_a.name, door_b.name, 'entrance', player, builder.name) else: - logger.warning('This is a bug during door spoiler') + done.add(door_b) + if not door_a.blocked and not door_b.blocked: + world.spoiler.set_door(door_a.name, door_b.name, 'both', player, builder.name) + elif door_a.blocked: + world.spoiler.set_door(door_b.name, door_a.name, 'entrance', player, builder.name) + elif door_b.blocked: + world.spoiler.set_door(door_a.name, door_b.name, 'entrance', player, builder.name) + else: + 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: @@ -208,22 +230,39 @@ def vanilla_key_logic(world, player): world.dungeon_layouts[player][builder.name] = builder add_inaccessible_doors(world, player) - for builder in builders: - origin_list = find_accessible_entrances(world, player, builder) - start_regions = convert_regions(origin_list, 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: - logging.getLogger('').info('Vanilla key layout not valid %s', builder.name) - builder.key_door_proposal = doors - if player not in world.key_logic.keys(): - world.key_logic[player] = {} - analyze_dungeon(key_layout, world, player) - world.key_logic[player][builder.name] = key_layout.key_logic - world.key_layout[player][builder.name] = key_layout - log_key_logic(builder.name, key_layout.key_logic) - # if world.shuffle[player] == 'vanilla' and world.owShuffle[player] == 'vanilla' and world.owCrossed[player] == 'none' and not world.owMixed[player] and world.accessibility[player] == 'items' and not world.retro[player] and not world.keydropshuffle[player]: + entrances_map, potentials, connections = determine_entrance_list(world, player) + enabled_entrances = world.enabled_entrances[player] = {} + builder_queue = deque(builders) + last_key, loops = None, 0 + while len(builder_queue) > 0: + builder = builder_queue.popleft() + origin_list = entrances_map[builder.name] + find_enabled_origins(builder.sectors, enabled_entrances, origin_list, entrances_map, builder.name) + if len(origin_list) <= 0: + if last_key == builder.name or loops > 1000: + origin_name = (world.get_region(origin_list[0], player).entrances[0].parent_region.name + if len(origin_list) > 0 else 'no origin') + raise GenerationException(f'Infinite loop detected for "{builder.name}" located at {origin_name}') + builder_queue.append(builder) + last_key = builder.name + loops += 1 + else: + find_new_entrances(builder.master_sector, entrances_map, connections, potentials, + enabled_entrances, world, player) + start_regions = convert_regions(origin_list, 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: + logging.getLogger('').info('Vanilla key layout not valid %s', builder.name) + builder.key_door_proposal = doors + if player not in world.key_logic.keys(): + world.key_logic[player] = {} + analyze_dungeon(key_layout, world, player) + world.key_logic[player][builder.name] = key_layout.key_logic + world.key_layout[player][builder.name] = key_layout + log_key_logic(builder.name, key_layout.key_logic) + # if world.shuffle[player] == 'vanilla' and world.accessibility[player] == 'items' and not world.retro[player] and not world.keydropshuffle[player]: # validate_vanilla_key_logic(world, player) @@ -257,9 +296,23 @@ def convert_key_doors(k_doors, world, player): def connect_custom(world, player): - if hasattr(world, 'custom_doors') and world.custom_doors[player]: - for entrance, ext in world.custom_doors[player]: - connect_two_way(world, entrance, ext, player) + if world.customizer and world.customizer.get_doors(): + custom_doors = world.customizer.get_doors() + if player not in custom_doors: + return + custom_doors = custom_doors[player] + if 'doors' not in custom_doors: + return + for door, dest in custom_doors['doors'].items(): + d = world.get_door(door, player) + if d.type not in [DoorType.Interior, DoorType.Logical]: + if isinstance(dest, str): + connect_two_way(world, door, dest, player) + elif 'dest' in dest: + if 'one-way' in dest and dest['one-way']: + connect_one_way(world, door, dest['dest'], player) + else: + connect_two_way(world, door, dest['dest'], player) def connect_simple_door(world, exit_name, region_name, player): @@ -279,12 +332,7 @@ def connect_door_only(world, exit_name, region, player): def connect_interior_doors(a, b, world, player): door_a = world.get_door(a, player) door_b = world.get_door(b, player) - if door_a.blocked: - connect_one_way(world, b, a, player) - elif door_b.blocked: - connect_one_way(world, a, b, player) - else: - connect_two_way(world, a, b, player) + connect_two_way(world, a, b, player) def connect_two_way(world, entrancename, exitname, player): @@ -326,9 +374,6 @@ def connect_one_way(world, entrancename, exitname, player): y = world.check_for_door(exitname, player) if x is not None: x.dest = y - if y is not None: - y.dest = x - def unmark_ugly_smalls(world, player): for d in ['Eastern Hint Tile Blocked Path SE', 'Eastern Darkness S', 'Thieves Hallway SE', 'Mire Left Bridge S', @@ -378,8 +423,20 @@ def pair_existing_key_doors(world, player, door_a, door_b): def choose_portals(world, player): - if world.doorShuffle[player] in ['basic', 'crossed']: - cross_flag = world.doorShuffle[player] == 'crossed' + if world.doorShuffle[player] != ['vanilla']: + shuffle_flag = world.doorShuffle[player] != 'basic' + allowed = {} + if world.doorShuffle[player] == 'basic': + allowed = {name: {name} for name in dungeon_regions} + elif world.doorShuffle[player] == 'partitioned': + groups = [['Hyrule Castle', 'Eastern Palace', 'Desert Palace', 'Tower of Hera', 'Agahnims Tower'], + ['Palace of Darkness', 'Swamp Palace', 'Skull Woods', 'Thieves Town'], + ['Ice Palace', 'Misery Mire', 'Turtle Rock', 'Ganons Tower']] + allowed = {name: set(group) for group in groups for name in group} + elif world.doorShuffle[player] == 'crossed': + all_dungeons = set(dungeon_regions.keys()) + allowed = {name: all_dungeons for name in dungeon_regions} + # key drops allow the big key in the right place in Desert Tiles 2 bk_shuffle = world.bigkeyshuffle[player] or world.pottery[player] not in ['none', 'cave'] std_flag = world.mode[player] == 'standard' @@ -423,12 +480,15 @@ def choose_portals(world, player): master_door_list = [x for x in world.doors if x.player == player and x.portalAble] portal_assignment = defaultdict(list) shuffled_info = list(info_map.items()) - if cross_flag: + + custom = customizer_portals(master_door_list, world, player) + + if shuffle_flag: random.shuffle(shuffled_info) for dungeon, info in shuffled_info: outstanding_portals = list(dungeon_portals[dungeon]) hc_flag = std_flag and dungeon == 'Hyrule Castle' - rupee_bow_flag = hc_flag and world.retro[player] # rupee bow + rupee_bow_flag = hc_flag and world.bow_mode[player].startswith('retro') # rupee bow if hc_flag: sanc = world.get_portal('Sanctuary', player) sanc.destination = True @@ -437,17 +497,17 @@ def choose_portals(world, player): info.required_passage[target_region] = [x for x in possible_portals if x != sanc.name] info.required_passage = {x: y for x, y in info.required_passage.items() if len(y) > 0} for target_region, possible_portals in info.required_passage.items(): - candidates = find_portal_candidates(master_door_list, dungeon, need_passage=True, crossed=cross_flag, - bk_shuffle=bk_shuffle, rupee_bow=rupee_bow_flag) - choice, portal = assign_portal(candidates, possible_portals, world, player) + candidates = find_portal_candidates(master_door_list, dungeon, custom, allowed, need_passage=True, + bk_shuffle=bk_shuffle, standard=std_flag, rupee_bow=rupee_bow_flag) + choice, portal = assign_portal(candidates, possible_portals, custom, world, player) portal.destination = True clean_up_portal_assignment(portal_assignment, dungeon, portal, master_door_list, outstanding_portals) dead_end_choices = info.total - 1 - len(portal_assignment[dungeon]) for i in range(0, dead_end_choices): - candidates = find_portal_candidates(master_door_list, dungeon, dead_end_allowed=True, - crossed=cross_flag, bk_shuffle=bk_shuffle, rupee_bow=rupee_bow_flag) + candidates = find_portal_candidates(master_door_list, dungeon, custom, allowed, dead_end_allowed=True, + bk_shuffle=bk_shuffle, standard=std_flag, rupee_bow=rupee_bow_flag) possible_portals = outstanding_portals if not info.sole_entrance else [x for x in outstanding_portals if x != info.sole_entrance] - choice, portal = assign_portal(candidates, possible_portals, world, player) + choice, portal = assign_portal(candidates, possible_portals, custom, world, player) if choice.deadEnd: if choice.passage: portal.destination = True @@ -456,9 +516,9 @@ def choose_portals(world, player): clean_up_portal_assignment(portal_assignment, dungeon, portal, master_door_list, outstanding_portals) the_rest = info.total - len(portal_assignment[dungeon]) for i in range(0, the_rest): - candidates = find_portal_candidates(master_door_list, dungeon, crossed=cross_flag, + candidates = find_portal_candidates(master_door_list, dungeon, custom, allowed, bk_shuffle=bk_shuffle, standard=hc_flag, rupee_bow=rupee_bow_flag) - choice, portal = assign_portal(candidates, outstanding_portals, world, player) + choice, portal = assign_portal(candidates, outstanding_portals, custom, world, player) clean_up_portal_assignment(portal_assignment, dungeon, portal, master_door_list, outstanding_portals) for portal in world.dungeon_portals[player]: @@ -496,6 +556,49 @@ def choose_portals(world, player): world.swamp_patch_required[player] = True +def customizer_portals(master_door_list, world, player): + custom_portals = {} + assigned_doors = set() + if world.customizer and world.customizer.get_doors(): + custom_doors = world.customizer.get_doors()[player] + if custom_doors and 'lobbies' in custom_doors: + for portal, assigned_door in custom_doors['lobbies'].items(): + door = next((x for x in master_door_list if x.name == assigned_door), None) + if door is None: + raise Exception(f'{assigned_door} not found. Check for typos') + custom_portals[portal] = door + assigned_doors.add(door) + if custom_doors and 'doors' in custom_doors: + for src_door, dest in custom_doors['doors'].items(): + door = world.get_door(src_door, player) + assigned_doors.add(door) + if isinstance(dest, str): + door = world.get_door(dest, player) + assigned_doors.add(door) + elif 'dest' in dest: + door = world.get_door(dest['dest'], player) + assigned_doors.add(door) + # restricts connected doors to the customized portals + if assigned_doors: + pool = create_dungeon_pool(world, player) + if pool: + pool_map = {} + for pool, region_list in pool: + sector_pool = convert_to_sectors(region_list, world, player) + merge_sectors(sector_pool, world, player) + for p in pool: + pool_map[p] = sector_pool + for portal, assigned_door in custom_portals.items(): + portal_region = world.get_door(assigned_door, player).entrance.parent_region + portal_dungeon = world.get_region(f'{portal} Portal', player).dungeon.name + sector_pool = pool_map[portal_dungeon] + sector = next((s for s in sector_pool if portal_region in s.regions), None) + for door in sector.outstanding_doors: + if door.portalAble: + door.dungeonLink = portal_dungeon + return custom_portals, assigned_doors + + def analyze_portals(world, player): info_map = {} for dungeon, portal_list in dungeon_portals.items(): @@ -575,13 +678,16 @@ def disconnect_portal(portal, world, player): chosen_door.entranceFlag = False -def find_portal_candidates(door_list, dungeon, need_passage=False, dead_end_allowed=False, crossed=False, +def find_portal_candidates(door_list, dungeon, custom, allowed, need_passage=False, dead_end_allowed=False, bk_shuffle=False, standard=False, rupee_bow=False): - ret = [x for x in door_list if bk_shuffle or not x.bk_shuffle_req] - if crossed: - ret = [x for x in ret if not x.dungeonLink or x.dungeonLink == dungeon or x.dungeonLink.startswith('link')] + custom_portals, assigned_doors = custom + if assigned_doors: + ret = [x for x in door_list if x not in assigned_doors] else: - ret = [x for x in ret if x.entrance.parent_region.dungeon.name == dungeon] + ret = door_list + ret = [x for x in ret if bk_shuffle or not x.bk_shuffle_req] + ret = [x for x in ret if not x.dungeonLink or x.dungeonLink == dungeon or x.dungeonLink.startswith('link')] + ret = [x for x in ret if x.entrance.parent_region.dungeon.name in allowed[dungeon]] if need_passage: ret = [x for x in ret if x.passage] if not dead_end_allowed: @@ -593,9 +699,13 @@ def find_portal_candidates(door_list, dungeon, need_passage=False, dead_end_allo return ret -def assign_portal(candidates, possible_portals, world, player): - candidate = random.choice(candidates) +def assign_portal(candidates, possible_portals, custom, world, player): + custom_portals, assigned_doors = custom portal_choice = random.choice(possible_portals) + if portal_choice in custom_portals: + candidate = custom_portals[portal_choice] + else: + candidate = random.choice(candidates) portal = world.get_portal(portal_choice, player) while candidate.lw_restricted and not portal.light_world: candidates.remove(candidate) @@ -684,6 +794,13 @@ def create_dungeon_entrances(world, player): choice = random.choice(filtered_choices) r_name = portal.door.entrance.parent_region.name split_map[key][choice].append(r_name) + elif key == 'Hyrule Castle' and world.mode[player] == 'standard': + for portal_name in portal_list: + portal = world.get_portal(portal_name, player) + choice = 'Sewers' if portal_name == 'Sanctuary' else 'Dungeon' + r_name = portal.door.entrance.parent_region.name + split_map[key][choice].append(r_name) + entrance_map[key].append(r_name) else: for portal_name in portal_list: portal = world.get_portal(portal_name, player) @@ -699,42 +816,201 @@ def find_entrance_region(portal): return None -# def unpair_all_doors(world, player): -# for paired_door in world.paired_doors[player]: -# paired_door.pair = False - -def within_dungeon(world, player): +# each dungeon_pool members is a pair of lists: dungeon names and regions in those dungeons +def main_dungeon_pool(dungeon_pool, world, player): add_inaccessible_doors(world, player) entrances_map, potentials, connections = determine_entrance_list(world, player) connections_tuple = (entrances_map, potentials, connections) + entrances, splits = create_dungeon_entrances(world, player) dungeon_builders = {} - for key in dungeon_regions.keys(): - sector_list = convert_to_sectors(dungeon_regions[key], world, player) - dungeon_builders[key] = simple_dungeon_builder(key, sector_list) - dungeon_builders[key].entrance_list = list(entrances_map[key]) + door_type_pools = [] + for pool, region_list in dungeon_pool: + if len(pool) == 1: + dungeon_key = next(iter(pool)) + sector_pool = convert_to_sectors(region_list, world, player) + merge_sectors(sector_pool, world, player) + dungeon_builders[dungeon_key] = simple_dungeon_builder(dungeon_key, sector_pool) + dungeon_builders[dungeon_key].entrance_list = list(entrances_map[dungeon_key]) + else: + if 'Hyrule Castle' in pool: + hc = world.get_dungeon('Hyrule Castle', player) + hc_compass = ItemFactory('Compass (Escape)', player) + hc_compass.advancement = world.restrict_boss_items[player] != 'none' + if hc.dungeon_items.count(hc_compass) < 1: + hc.dungeon_items.append(hc_compass) + if 'Agahnims Tower' in pool: + at = world.get_dungeon('Agahnims Tower', player) + at_compass = ItemFactory('Compass (Agahnims Tower)', player) + at_compass.advancement = world.restrict_boss_items[player] != 'none' + if at.dungeon_items.count(at_compass) < 1: + at.dungeon_items.append(at_compass) + at_map = ItemFactory('Map (Agahnims Tower)', player) + at_map.advancement = world.restrict_boss_items[player] != 'none' + if at.dungeon_items.count(at_map) < 1: + at.dungeon_items.append(at_map) + sector_pool = convert_to_sectors(region_list, world, player) + merge_sectors(sector_pool, world, player) + # todo: which dungeon to create + dungeon_builders.update(create_dungeon_builders(sector_pool, connections_tuple, + world, player, pool, entrances, splits)) + door_type_pools.append((pool, DoorTypePool(pool, world, player))) + + update_forced_keys(dungeon_builders, entrances_map, world, player) recombinant_builders = {} - entrances, splits = create_dungeon_entrances(world, player) builder_info = entrances, splits, connections_tuple, world, player handle_split_dungeons(dungeon_builders, recombinant_builders, entrances_map, builder_info) + main_dungeon_generation(dungeon_builders, recombinant_builders, connections_tuple, world, player) + setup_custom_door_types(world, player) paths = determine_required_paths(world, player) + shuffle_door_types(door_type_pools, paths, world, player) + check_required_paths(paths, world, player) - # shuffle_key_doors for dungeons - logging.getLogger('').info(world.fish.translate("cli", "cli", "shuffling.keydoors")) - start = time.process_time() - for builder in world.dungeon_layouts[player].values(): - shuffle_key_doors(builder, world, player) - logging.getLogger('').info('%s: %s', world.fish.translate("cli", "cli", "keydoor.shuffle.time"), time.process_time()-start) - smooth_door_pairs(world, player) + for pool, door_type_pool in door_type_pools: + for name in pool: + builder = world.dungeon_layouts[player][name] + region_set = builder.master_sector.region_set() + builder.bk_required = (builder.bk_door_proposal or any(x in region_set for x in special_bk_regions) + or len(world.key_logic[player][name].bk_chests) > 0) + 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) + all_dungeon_items_cnt = len(list(y for x in world.dungeons if x.player == player for y in x.all_items)) + target_items = 34 + if world.keyshuffle[player] == 'universal': + target_items += 1 if world.dropshuffle[player] else 0 # the hc big key + else: + target_items += 29 # small keys in chests + if world.dropshuffle[player]: + target_items += 14 # 13 dropped smalls + 1 big + if world.pottery[player] not in ['none', 'cave']: + target_items += 19 # 19 pot keys + d_items = target_items - all_dungeon_items_cnt + world.pool_adjustment[player] = d_items + cross_dungeon_clean_up(world, player) + + +special_bk_regions = ['Hyrule Dungeon Cellblock', "Thieves Blind's Cell"] + + +def cross_dungeon_clean_up(world, player): + # Re-assign dungeon bosses + gt = world.get_dungeon('Ganons Tower', player) + for name, builder in world.dungeon_layouts[player].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) + + sanctuary = world.get_region('Sanctuary', player) + d_name = sanctuary.dungeon.name + if d_name != 'Hyrule Castle': + possible_portals = [] + for portal_name in dungeon_portals[d_name]: + portal = world.get_portal(portal_name, player) + if portal.door.name == 'Sanctuary S': + possible_portals.clear() + possible_portals.append(portal) + break + if not portal.destination and not portal.deadEnd: + possible_portals.append(portal) + if len(possible_portals) == 1: + world.sanc_portal[player] = possible_portals[0] + else: + reachable_portals = [] + for portal in possible_portals: + start_area = portal.door.entrance.parent_region + state = ExplorationState(dungeon=d_name) + state.visit_region(start_area) + state.add_all_doors_check_unattached(start_area, world, player) + explore_state(state, world, player) + if state.visited_at_all(sanctuary): + reachable_portals.append(portal) + world.sanc_portal[player] = random.choice(reachable_portals) if world.intensity[player] >= 3: - portal = world.get_portal('Sanctuary', player) + if player in world.sanc_portal: + portal = world.sanc_portal[player] + else: + portal = world.get_portal('Sanctuary', player) target = portal.door.entrance.parent_region connect_simple_door(world, 'Sanctuary Mirror Route', target, player) + check_entrance_fixes(world, player) + + if world.standardize_palettes[player] == 'standardize' and world.doorShuffle[player] != 'basic': + palette_assignment(world, player) + + refine_hints(world.dungeon_layouts[player]) + refine_boss_exits(world, player) + + +def update_forced_keys(dungeon_builders, entrances_map, 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.forced_item: + 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) + + +def finish_up_work(world, player): + dungeon_builders = world.dungeon_layouts[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) + + sanctuary = world.get_region('Sanctuary', player) + d_name = sanctuary.dungeon.name + if d_name != 'Hyrule Castle': + possible_portals = [] + for portal_name in dungeon_portals[d_name]: + portal = world.get_portal(portal_name, player) + if portal.door.name == 'Sanctuary S': + possible_portals.clear() + possible_portals.append(portal) + break + if not portal.destination and not portal.deadEnd: + possible_portals.append(portal) + if len(possible_portals) == 1: + world.sanc_portal[player] = possible_portals[0] + else: + reachable_portals = [] + for portal in possible_portals: + start_area = portal.door.entrance.parent_region + state = ExplorationState(dungeon=d_name) + state.visit_region(start_area) + state.add_all_doors_check_unattached(start_area, world, player) + explore_state(state, world, player) + if state.visited_at_all(sanctuary): + reachable_portals.append(portal) + world.sanc_portal[player] = random.choice(reachable_portals) + if world.intensity[player] >= 3: + if player in world.sanc_portal: + portal = world.sanc_portal[player] + else: + portal = world.get_portal('Sanctuary', player) + target = portal.door.entrance.parent_region + connect_simple_door(world, 'Sanctuary Mirror Route', target, player) + + check_entrance_fixes(world, player) + + if world.standardize_palettes[player] == 'standardize' and world.doorShuffle[player] not in ['basic']: + palette_assignment(world, player) + + refine_hints(dungeon_builders) refine_boss_exits(world, player) @@ -773,7 +1049,8 @@ def main_dungeon_generation(dungeon_builders, recombinant_builders, connections_ logging.getLogger('').info(world.fish.translate("cli", "cli", "generating.dungeon")) while len(sector_queue) > 0: builder = sector_queue.popleft() - split_dungeon = builder.name.startswith('Desert Palace') or builder.name.startswith('Skull Woods') + split_dungeon = (builder.name.startswith('Desert Palace') or builder.name.startswith('Skull Woods') + or (builder.name.startswith('Hyrule Castle') and world.mode[player] == 'standard')) name = builder.name if split_dungeon: name = ' '.join(builder.name.split(' ')[:-1]) @@ -781,9 +1058,11 @@ def main_dungeon_generation(dungeon_builders, recombinant_builders, connections_ del dungeon_builders[builder.name] continue origin_list = list(builder.entrance_list) + find_standard_origins(builder, recombinant_builders, origin_list) find_enabled_origins(builder.sectors, enabled_entrances, origin_list, entrances_map, name) split_dungeon = treat_split_as_whole_dungeon(split_dungeon, name, origin_list, world, player) - if len(origin_list) <= 0 or not pre_validate(builder, origin_list, split_dungeon, world, player): + # todo: figure out pre-validate, ensure all needed origins are enabled? + if len(origin_list) <= 0: # or not pre_validate(builder, origin_list, split_dungeon, world, player): if last_key == builder.name or loops > 1000: origin_name = world.get_region(origin_list[0], player).entrances[0].parent_region.name if len(origin_list) > 0 else 'no origin' raise GenerationException(f'Infinite loop detected for "{builder.name}" located at {origin_name}') @@ -797,7 +1076,7 @@ def main_dungeon_generation(dungeon_builders, recombinant_builders, connections_ 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) + combine_layouts(recombinant_builders, dungeon_builders, entrances_map, world, player) world.dungeon_layouts[player] = {} for builder in dungeon_builders.values(): builder.entrance_list = builder.layout_starts = builder.path_entrances = find_accessible_entrances(world, player, builder) @@ -833,14 +1112,14 @@ def determine_entrance_list(world, player): connections = {} for key, portal_list in dungeon_portals.items(): entrance_map[key] = [] - r_names = {} + r_names = [] if key in dungeon_drops.keys(): for drop in dungeon_drops[key]: - r_names[drop] = None + r_names.append((drop, None)) for portal_name in portal_list: portal = world.get_portal(portal_name, player) - r_names[portal.door.entrance.parent_region.name] = portal - for region_name, portal in r_names.items(): + r_names.append((portal.door.entrance.parent_region.name, portal)) + for region_name, portal in r_names: if portal: region = world.get_region(portal.name + ' Portal', player) else: @@ -871,6 +1150,14 @@ def add_shuffled_entrances(sectors, region_list, entrance_list): entrance_list.append(region.name) +def find_standard_origins(builder, recomb_builders, origin_list): + if builder.name == 'Hyrule Castle Sewers': + throne_door = recomb_builders['Hyrule Castle'].throne_door + sewer_entrance = throne_door.entrance.parent_region.name + if sewer_entrance not in origin_list: + origin_list.append(sewer_entrance) + + def find_enabled_origins(sectors, enabled, entrance_list, entrance_map, key): for sector in sectors: for region in sector.regions: @@ -995,15 +1282,19 @@ def cross_dungeon(world, player): if world.restrict_boss_items[player] != 'none': hc_compass.advancement = at_compass.advancement = at_map.advancement = True hc = world.get_dungeon('Hyrule Castle', player) - hc.dungeon_items.append(hc_compass) + if hc.dungeon_items.count(hc_compass) < 1: + hc.dungeon_items.append(hc_compass) at = world.get_dungeon('Agahnims Tower', player) - at.dungeon_items.append(at_compass) - at.dungeon_items.append(at_map) + if at.dungeon_items.count(at_compass) < 1: + at.dungeon_items.append(at_compass) + if at.dungeon_items.count(at_map) < 1: + at.dungeon_items.append(at_map) + setup_custom_door_types(world, player) assign_cross_keys(dungeon_builders, world, player) all_dungeon_items_cnt = len(list(y for x in world.dungeons if x.player == player for y in x.all_items)) target_items = 34 - if world.retro[player]: + if world.keyshuffle[player] == 'universal': target_items += 1 if world.dropshuffle[player] else 0 # the hc big key else: target_items += 29 # small keys in chests @@ -1013,7 +1304,8 @@ def cross_dungeon(world, player): target_items += 19 # 19 pot keys d_items = target_items - all_dungeon_items_cnt world.pool_adjustment[player] = d_items - smooth_door_pairs(world, player) + if not world.decoupledoors[player]: + smooth_door_pairs(world, player) # Re-assign dungeon bosses gt = world.get_dungeon('Ganons Tower', player) @@ -1064,10 +1356,33 @@ def cross_dungeon(world, player): refine_boss_exits(world, player) +def filter_key_door_pool(pool, selected_custom): + new_pool = [] + for cand in pool: + found = False + for custom in selected_custom: + if isinstance(cand, Door): + if isinstance(custom, Door): + found = cand.name == custom.name + else: + found = cand.name == custom[0].name or cand.name == custom[1].name + else: + if isinstance(custom, Door): + found = cand[0].name == custom.name or cand[1].name == custom.name + else: + found = (cand[0].name == custom[0].name or cand[0].name == custom[1].name + or cand[1].name == custom[0].name or cand[1].name == custom[1].name) + if found: + break + if not found: + new_pool.append(cand) + return new_pool + + def assign_cross_keys(dungeon_builders, world, player): logging.getLogger('').info(world.fish.translate("cli", "cli", "shuffling.keydoors")) start = time.process_time() - if world.retro[player]: + if world.keyshuffle[player] == 'universal': remaining = 29 if world.dropshuffle[player]: remaining += 13 @@ -1075,9 +1390,13 @@ def assign_cross_keys(dungeon_builders, world, player): remaining += 19 else: remaining = len(list(x for dgn in world.dungeons if dgn.player == player for x in dgn.small_keys)) - total_keys = remaining total_candidates = 0 start_regions_map = {} + if player in world.custom_door_types: + custom_key_doors = world.custom_door_types[player]['Key Door'] + else: + custom_key_doors = defaultdict(list) + key_door_pool, key_doors_assigned = {}, {} # Step 1: Find Small Key Door Candidates for name, builder in dungeon_builders.items(): dungeon = world.get_dungeon(name, player) @@ -1087,22 +1406,27 @@ def assign_cross_keys(dungeon_builders, world, player): 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) + key_door_pool[name] = list(builder.candidates) + if custom_key_doors[name]: + key_door_pool[name] = filter_key_door_pool(key_door_pool[name], custom_key_doors[name]) + remaining -= len(custom_key_doors[name]) + builder.key_doors_num = max(0, len(key_door_pool[name]) - builder.key_drop_cnt) total_candidates += builder.key_doors_num start_regions_map[name] = start_regions + total_keys = remaining # 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 = max(0, builder.location_cnt - calc_used_dungeon_items(builder)) - cand_len = max(0, len(builder.candidates) - builder.key_drop_cnt) + max_keys = max(0, builder.location_cnt - calc_used_dungeon_items(builder, world, player)) + cand_len = max(0, len(key_door_pool[name]) - 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) + combo_size = ncr(len(key_door_pool[name]), 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 + combo_size = ncr(len(key_door_pool[name]), suggested + builder.key_drop_cnt) + builder.key_doors_num = suggested + builder.key_drop_cnt + len(custom_key_doors[name]) remaining -= suggested builder.combo_size = combo_size if suggested < limit: @@ -1110,7 +1434,7 @@ def assign_cross_keys(dungeon_builders, world, player): # 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 + suggested = builder.key_doors_num - builder.key_drop_cnt - len(custom_key_doors[name]) builder.total_keys = builder.key_doors_num find_valid_combination(builder, start_regions_map[name], world, player) actual_chest_keys = builder.key_doors_num - builder.key_drop_cnt @@ -1146,7 +1470,7 @@ def assign_cross_keys(dungeon_builders, world, player): # Last Step: Adjust Small Key Dungeon Pool for name, builder in dungeon_builders.items(): reassign_key_doors(builder, world, player) - if not world.retro[player]: + if world.keyshuffle[player] != 'universal': log_key_logic(builder.name, world.key_logic[player][builder.name]) actual_chest_keys = max(builder.key_doors_num - builder.key_drop_cnt, 0) dungeon = world.get_dungeon(name, player) @@ -1167,7 +1491,7 @@ def reassign_boss(boss_region, boss_key, builder, gt, world, player): def check_entrance_fixes(world, player): # I believe these modes will be fine - if world.shuffle[player] not in ['insanity', 'insanity_legacy', 'madness_legacy']: + if world.shuffle[player] not in ['insanity']: checks = { 'Palace of Darkness': 'pod', 'Skull Woods Final Section': 'sw', @@ -1305,7 +1629,12 @@ def refine_boss_exits(world, player): if len(reachable_portals) == 0: reachable_portals = possible_portals unreachable = world.inaccessible_regions[player] - filtered = [x for x in reachable_portals if x.door.entrance.connected_region.name not in unreachable] + filtered = [] + for reachable in reachable_portals: + for entrance in reachable.door.entrance.connected_region.entrances: + parent = entrance.parent_region + if parent.type != RegionType.Dungeon and parent.name not in unreachable: + filtered.append(reachable) if 0 < len(filtered) < len(reachable_portals): reachable_portals = filtered chosen_one = random.choice(reachable_portals) if len(reachable_portals) > 1 else reachable_portals[0] @@ -1384,7 +1713,7 @@ def merge_sectors(all_sectors, world, player): # those with split region starts like Desert/Skull combine for key layouts -def combine_layouts(recombinant_builders, dungeon_builders, entrances_map): +def combine_layouts(recombinant_builders, dungeon_builders, entrances_map, world, player): for recombine in recombinant_builders.values(): queue = deque(dungeon_builders.values()) while len(queue) > 0: @@ -1396,16 +1725,402 @@ def combine_layouts(recombinant_builders, dungeon_builders, entrances_map): recombine.master_sector.name = recombine.name else: recombine.master_sector.regions.extend(builder.master_sector.regions) + if recombine.name == 'Hyrule Castle': + recombine.master_sector.regions.extend(recombine.throne_sector.regions) + throne_n = world.get_door('Hyrule Castle Throne Room N', player) + connect_doors(throne_n, recombine.throne_door) recombine.layout_starts = list(entrances_map[recombine.name]) dungeon_builders[recombine.name] = recombine -# todo: this allows cross-dungeon exploring via HC Ledge or Inaccessible Regions -# todo: @deprecated -def valid_region_to_explore(region, world, player): - return region and (region.type == RegionType.Dungeon - or region.name in world.inaccessible_regions[player] - or (region.name == 'Hyrule Castle Ledge' and world.mode[player] == 'standard')) +def setup_custom_door_types(world, player): + if not hasattr(world, 'custom_door_types'): + world.custom_door_types = defaultdict(dict) + if world.customizer and world.customizer.get_doors(): + # type_conv = {'Bomb Door': DoorKind.Bombable , 'Dash Door', DoorKind.Dashable, 'Key Door', DoorKind.SmallKey} + custom_doors = world.customizer.get_doors() + if player not in custom_doors: + return + custom_doors = custom_doors[player] + if 'doors' not in custom_doors: + return + customizeable_types = ['Key Door', 'Dash Door', 'Bomb Door', 'Trap Door', 'Big Key Door'] + world.custom_door_types[player] = type_map = {x: defaultdict(list) for x in customizeable_types} + for door, dest in custom_doors['doors'].items(): + if isinstance(dest, dict): + if 'type' in dest: + door_kind = dest['type'] + d = world.get_door(door, player) + dungeon = d.entrance.parent_region.dungeon + if d.type == DoorType.SpiralStairs: + type_map[door_kind][dungeon.name].append(d) + else: + # check if the dest is paired + if d.dest and d.dest.type in [DoorType.Interior, DoorType.Normal] and door_kind != 'Trap Door': + type_map[door_kind][dungeon.name].append((d, d.dest)) + else: + type_map[door_kind][dungeon.name].append(d) + + +class DoorTypePool: + def __init__(self, pool, world, player): + self.smalls = 0 + self.bombable = 0 + self.dashable = 0 + self.bigs = 0 + self.traps = 0 + self.tricky = 0 + self.hidden = 0 + # todo: custom pools? + for dungeon in pool: + counts = door_type_counts[dungeon] + if world.door_type_mode[player] == 'chaos': + counts = self.chaos_shuffle(counts) + self.smalls += counts[0] + self.bigs += counts[1] + self.traps += counts[2] + self.bombable += counts[3] + self.dashable += counts[4] + self.hidden += counts[5] + self.tricky += counts[6] + + def chaos_shuffle(self, counts): + weights = [1, 2, 4, 3, 2] + return [random.choices(self.get_choices(counts[i]), weights=weights)[0] for i, c in enumerate(counts)] + + @staticmethod + def get_choices(number): + return [max(number+i, 0) for i in range(-1, 4)] + + +class BuilderDoorCandidates: + def __init__(self): + self.small = [] + self.big = [] + self.trap = [] + self.bomb_dash = [] + + +def shuffle_door_types(door_type_pools, paths, world, player): + start_regions_map = {} + for name, builder in world.dungeon_layouts[player].items(): + start_regions = convert_regions(find_possible_entrances(world, player, builder), world, player) + start_regions_map[name] = start_regions + builder.candidates = BuilderDoorCandidates() + + all_custom = defaultdict(list) + if player in world.custom_door_types: + for custom_dict in world.custom_door_types[player].values(): + for dungeon, doors in custom_dict.items(): + all_custom[dungeon].extend(doors) + + world.paired_doors[player].clear() + used_doors = shuffle_trap_doors(door_type_pools, paths, start_regions_map, all_custom, world, player) + # big keys + used_doors = shuffle_big_key_doors(door_type_pools, used_doors, start_regions_map, all_custom, world, player) + # small keys + used_doors = shuffle_small_key_doors(door_type_pools, used_doors, start_regions_map, all_custom, world, player) + # bombable / dashable + used_doors = shuffle_bomb_dash_doors(door_type_pools, used_doors, start_regions_map, all_custom, world, player) + # handle paired list + + +def shuffle_trap_doors(door_type_pools, paths, start_regions_map, all_custom, world, player): + used_doors = set() + for pool, door_type_pool in door_type_pools: + if world.trap_door_mode[player] != 'oneway': + ttl = 0 + suggestion_map, trap_map, flex_map = {}, {}, {} + remaining = door_type_pool.traps + if player in world.custom_door_types and 'Trap Door' in world.custom_door_types[player]: + custom_trap_doors = world.custom_door_types[player]['Trap Door'] + else: + custom_trap_doors = defaultdict(list) + for dungeon in pool: + builder = world.dungeon_layouts[player][dungeon] + if 'Mire Warping Pool' in builder.master_sector.region_set(): + custom_trap_doors[dungeon].append(world.get_door('Mire Warping Pool ES', player)) + world.custom_door_types[player]['Trap Door'] = custom_trap_doors + find_trappable_candidates(builder, world, player) + if all_custom[dungeon]: + builder.candidates.trap = filter_key_door_pool(builder.candidates.trap, all_custom[dungeon]) + remaining -= len(custom_trap_doors[dungeon]) + ttl += len(builder.candidates.trap) + if ttl == 0: + continue + for dungeon in pool: + builder = world.dungeon_layouts[player][dungeon] + proportion = len(builder.candidates.trap) + calc = int(round(proportion * door_type_pool.traps/ttl)) + suggested = min(proportion, calc) + remaining -= suggested + suggestion_map[dungeon] = suggested + flex_map[dungeon] = (proportion - suggested) if suggested < proportion else 0 + for dungeon in pool: + builder = world.dungeon_layouts[player][dungeon] + valid_traps, trap_number = find_valid_trap_combination(builder, suggestion_map[dungeon], + start_regions_map[dungeon], paths, world, player, + drop=True) + trap_map[dungeon] = valid_traps + if trap_number < suggestion_map[dungeon]: + flex_map[dungeon] = 0 + remaining += suggestion_map[dungeon] - trap_number + suggestion_map[dungeon] = trap_number + builder_order = [x for x in pool if flex_map[x] > 0] + random.shuffle(builder_order) + queue = deque(builder_order) + while len(queue) > 0 and remaining > 0: + dungeon = queue.popleft() + builder = world.dungeon_layouts[player][dungeon] + increased = suggestion_map[dungeon] + 1 + valid_traps, trap_number = find_valid_trap_combination(builder, increased, start_regions_map[dungeon], + paths, world, player) + if valid_traps: + trap_map[dungeon] = valid_traps + remaining -= 1 + suggestion_map[dungeon] = increased + flex_map[dungeon] -= 1 + if flex_map[dungeon] > 0: + queue.append(dungeon) + # time to re-assign + else: + trap_map = {dungeon: [] for dungeon in pool} + for dungeon in pool: + builder = world.dungeon_layouts[player][dungeon] + if 'Mire Warping Pool' in builder.master_sector.region_set(): + trap_map[dungeon].append(world.get_door('Mire Warping Pool ES', player)) + reassign_trap_doors(trap_map, world, player) + for name, traps in trap_map.items(): + used_doors.update(traps) + return used_doors + + +def shuffle_big_key_doors(door_type_pools, used_doors, start_regions_map, all_custom, world, player): + for pool, door_type_pool in door_type_pools: + ttl = 0 + suggestion_map, bk_map, flex_map = {}, {}, {} + remaining = door_type_pool.bigs + if player in world.custom_door_types and 'Big Key Door' in world.custom_door_types[player]: + custom_bk_doors = world.custom_door_types[player]['Big Key Door'] + else: + custom_bk_doors = defaultdict(list) + + for dungeon in pool: + builder = world.dungeon_layouts[player][dungeon] + find_big_key_candidates(builder, start_regions_map[dungeon], used_doors, world, player) + if all_custom[dungeon]: + builder.candidates.big = filter_key_door_pool(builder.candidates.big, all_custom[dungeon]) + remaining -= len(custom_bk_doors[dungeon]) + ttl += len(builder.candidates.big) + if ttl == 0: + continue + remaining = max(0, remaining) + for dungeon in pool: + builder = world.dungeon_layouts[player][dungeon] + proportion = len(builder.candidates.big) + calc = int(round(proportion * remaining/ttl)) + suggested = min(proportion, calc) + remaining -= suggested + suggestion_map[dungeon] = suggested + flex_map[dungeon] = (proportion - suggested) if suggested < proportion else 0 + for dungeon in pool: + builder = world.dungeon_layouts[player][dungeon] + valid_doors, bk_number = find_valid_bk_combination(builder, suggestion_map[dungeon], + start_regions_map[dungeon], world, player, True) + bk_map[dungeon] = valid_doors + if bk_number < suggestion_map[dungeon]: + flex_map[dungeon] = 0 + remaining += suggestion_map[dungeon] - bk_number + suggestion_map[dungeon] = bk_number + builder_order = [x for x in pool if flex_map[x] > 0] + random.shuffle(builder_order) + queue = deque(builder_order) + while len(queue) > 0 and remaining > 0: + dungeon = queue.popleft() + builder = world.dungeon_layouts[player][dungeon] + increased = suggestion_map[dungeon] + 1 + valid_doors, bk_number = find_valid_bk_combination(builder, increased, start_regions_map[dungeon], + world, player) + if valid_doors: + bk_map[dungeon] = valid_doors + remaining -= 1 + suggestion_map[dungeon] = increased + flex_map[dungeon] -= 1 + if flex_map[dungeon] > 0: + queue.append(dungeon) + # time to re-assign + reassign_big_key_doors(bk_map, world, player) + for name, big_list in bk_map.items(): + used_doors.update(flatten_pair_list(big_list)) + return used_doors + + +def shuffle_small_key_doors(door_type_pools, used_doors, start_regions_map, all_custom, world, player): + max_computation = 11 # this is around 6 billion worse case factorial don't want to exceed this much + for pool, door_type_pool in door_type_pools: + ttl = 0 + suggestion_map, small_map, flex_map = {}, {}, {} + remaining = door_type_pool.smalls + total_keys = remaining + if player in world.custom_door_types and 'Key Door' in world.custom_door_types[player]: + custom_key_doors = world.custom_door_types[player]['Key Door'] + else: + custom_key_doors = defaultdict(list) + total_adjustable = len(pool) > 1 + for dungeon in pool: + builder = world.dungeon_layouts[player][dungeon] + if not total_adjustable: + builder.total_keys = total_keys + find_small_key_door_candidates(builder, start_regions_map[dungeon], used_doors, world, player) + custom_doors = 0 + if all_custom[dungeon]: + builder.candidates.small = filter_key_door_pool(builder.candidates.small, all_custom[dungeon]) + custom_doors = len(custom_key_doors[dungeon]) + remaining -= custom_doors + builder.key_doors_num = max(0, len(builder.candidates.small) - builder.key_drop_cnt) + custom_doors + total_keys -= builder.key_drop_cnt + ttl += builder.key_doors_num + remaining = max(0, remaining) + for dungeon in pool: + builder = world.dungeon_layouts[player][dungeon] + calculated = int(round(builder.key_doors_num*total_keys/ttl)) + max_keys = max(0, builder.location_cnt - calc_used_dungeon_items(builder, world, player)) + cand_len = max(0, len(builder.candidates.small) - builder.key_drop_cnt) + limit = min(max_keys, cand_len, max_computation) + suggested = min(calculated, limit) + key_door_num = min(suggested + builder.key_drop_cnt, max_computation) + combo_size = ncr(len(builder.candidates.small), key_door_num) + suggestion_map[dungeon] = builder.key_doors_num = key_door_num + remaining -= key_door_num + builder.key_drop_cnt + builder.combo_size = combo_size + flex_map[dungeon] = (limit - key_door_num) if key_door_num < limit else 0 + for dungeon in pool: + builder = world.dungeon_layouts[player][dungeon] + if total_adjustable: + builder.total_keys = max(suggestion_map[dungeon], builder.key_drop_cnt) + valid_doors, small_number = find_valid_combination(builder, suggestion_map[dungeon], + start_regions_map[dungeon], world, player) + small_map[dungeon] = valid_doors + actual_chest_keys = small_number - builder.key_drop_cnt + if actual_chest_keys < suggestion_map[dungeon]: + if total_adjustable: + builder.total_keys = actual_chest_keys + builder.key_drop_cnt + flex_map[dungeon] = 0 + remaining += suggestion_map[dungeon] - actual_chest_keys + suggestion_map[dungeon] = small_number + builder_order = [world.dungeon_layouts[player][x] for x in pool if flex_map[x] > 0] + builder_order.sort(key=lambda b: b.combo_size) + queue = deque(builder_order) + while len(queue) > 0 and remaining > 0: + builder = queue.popleft() + dungeon = builder.name + increased = suggestion_map[dungeon] + 1 + if increased > max_computation: + continue + builder.key_doors_num = increased + valid_doors, small_number = find_valid_combination(builder, increased, start_regions_map[dungeon], + world, player) + if valid_doors: + small_map[dungeon] = valid_doors + remaining -= 1 + suggestion_map[dungeon] = increased + flex_map[dungeon] -= 1 + if total_adjustable: + builder.total_keys = max(increased, builder.key_drop_cnt) + if flex_map[dungeon] > 0: + builder.combo_size = ncr(len(builder.candidates.small), builder.key_doors_num) + queue.append(builder) + queue = deque(sorted(queue, key=lambda b: b.combo_size)) + else: + builder.key_doors_num -= 1 + # time to re-assign + reassign_key_doors(small_map, world, player) + for dungeon_name in pool: + if world.keyshuffle[player] != 'universal': + builder = world.dungeon_layouts[player][dungeon_name] + log_key_logic(builder.name, world.key_logic[player][builder.name]) + if world.doorShuffle[player] != 'basic': + actual_chest_keys = max(builder.key_doors_num - builder.key_drop_cnt, 0) + dungeon = world.get_dungeon(dungeon_name, player) + if actual_chest_keys == 0: + dungeon.small_keys = [] + else: + dungeon.small_keys = [ItemFactory(dungeon_keys[dungeon_name], player)] * actual_chest_keys + + for name, small_list in small_map.items(): + used_doors.update(flatten_pair_list(small_list)) + return used_doors + + +def shuffle_bomb_dash_doors(door_type_pools, used_doors, start_regions_map, all_custom, world, player): + for pool, door_type_pool in door_type_pools: + ttl = 0 + suggestion_map, bd_map = {}, {} + remaining_bomb = door_type_pool.bombable + remaining_dash = door_type_pool.dashable + + if player in world.custom_door_types and 'Bomb Door' in world.custom_door_types[player]: + custom_bomb_doors = world.custom_door_types[player]['Bomb Door'] + custom_dash_doors = world.custom_door_types[player]['Dash Door'] + else: + custom_bomb_doors = defaultdict(list) + custom_dash_doors = defaultdict(list) + + for dungeon in pool: + builder = world.dungeon_layouts[player][dungeon] + find_bd_candidates(builder, start_regions_map[dungeon], used_doors, world, player) + if all_custom[dungeon]: + builder.candidates.bomb_dash = filter_key_door_pool(builder.candidates.bomb_dash, all_custom[dungeon]) + remaining_bomb -= len(custom_bomb_doors[dungeon]) + remaining_dash -= len(custom_dash_doors[dungeon]) + ttl += len(builder.candidates.bomb_dash) + if ttl == 0: + continue + for dungeon in pool: + builder = world.dungeon_layouts[player][dungeon] + proportion = len(builder.candidates.bomb_dash) + calc = int(round(proportion * door_type_pool.bombable/ttl)) + suggested_bomb = min(proportion, calc) + remaining_bomb -= suggested_bomb + calc = int(round(proportion * door_type_pool.dashable/ttl)) + suggested_dash = min(proportion, calc) + remaining_dash -= suggested_dash + suggestion_map[dungeon] = suggested_bomb, suggested_dash + for dungeon in pool: + builder = world.dungeon_layouts[player][dungeon] + bomb_doors, dash_doors, bd_number = find_valid_bd_combination(builder, suggestion_map[dungeon], world, player) + bd_map[dungeon] = (bomb_doors, dash_doors) + if bd_number < suggestion_map[dungeon][0] + suggestion_map[dungeon][1]: + remaining_bomb += suggestion_map[dungeon][0] - len(bomb_doors) + remaining_dash += suggestion_map[dungeon][1] - len(dash_doors) + suggestion_map[dungeon] = len(bomb_doors), len(dash_doors) + builder_order = [x for x in pool] + random.shuffle(builder_order) + queue = deque(builder_order) + while len(queue) > 0 and (remaining_bomb > 0 or remaining_dash > 0): + dungeon = queue.popleft() + builder = world.dungeon_layouts[player][dungeon] + type_pool = [] + if remaining_bomb > 0: + type_pool.append('bomb') + if remaining_dash > 0: + type_pool.append('dash') + type_choice = random.choice(type_pool) + pair = suggestion_map[dungeon] + pair = pair[0] + (1 if type_choice == 'bomb' else 0), pair[1] + (1 if type_choice == 'dash' else 0) + bomb_doors, dash_doors, bd_number = find_valid_bd_combination(builder, pair, world, player) + if bomb_doors and dash_doors: + bd_map[dungeon] = (bomb_doors, dash_doors) + remaining_bomb -= (1 if type_choice == 'bomb' else 0) + remaining_dash -= (1 if type_choice == 'dash' else 0) + suggestion_map[dungeon] = pair + queue.append(dungeon) + # time to re-assign + reassign_bd_doors(bd_map, world, player) + for name, pair in bd_map.items(): + used_doors.update(flatten_pair_list(pair[0])) + used_doors.update(flatten_pair_list(pair[1])) + return used_doors def shuffle_key_doors(builder, world, player): @@ -1446,83 +2161,513 @@ def find_current_key_doors(builder): return current_doors -def find_small_key_door_candidates(builder, start_regions, world, player): +def find_trappable_candidates(builder, world, player): + if world.door_type_mode[player] not in ['original', 'big']: # all, chaos + r_set = builder.master_sector.region_set() + filtered_doors = [ext.door for r in r_set for ext in world.get_region(r, player).exits + if ext.door and ext.door.type in [DoorType.Interior, DoorType.Normal]] + for d in filtered_doors: + # I only support the first 3 due to the trapFlag right now + if 0 <= d.doorListPos < 3 and not d.entranceFlag and d.name != 'Skull Small Hall WS': + room = world.get_room(d.roomIndex, player) + kind = room.kind(d) + if d.type == DoorType.Interior: + if (kind in [DoorKind.Normal, DoorKind.SmallKey, DoorKind.Bombable, DoorKind.Dashable, + DoorKind.BigKey] + or (d.blocked and d.trapFlag != 0 and 'Boss' not in d.name and 'Agahnim' not in d.name) + or (kind == DoorKind.TrapTriggerable and d.direction in [Direction.South, Direction.East]) + or (kind == DoorKind.Trap2 and d.direction in [Direction.North, Direction.West])): + builder.candidates.trap.append(d) + elif d.type == DoorType.Normal: + if (kind in [DoorKind.Normal, DoorKind.SmallKey, DoorKind.Bombable, DoorKind.Dashable, + DoorKind.BigKey] + or (d.blocked and d.trapFlag != 0 and 'Boss' not in d.name and 'Agahnim' not in d.name)): + builder.candidates.trap.append(d) + else: + r_set = builder.master_sector.region_set() + for r in r_set: + for ext in world.get_region(r, player).exits: + if ext.door: + d = ext.door + if d.blocked and d.trapFlag != 0 and exclude_boss_traps(d): + builder.candidates.trap.append(d) + + +def find_valid_trap_combination(builder, suggested, start_regions, paths, world, player, drop=True): + trap_door_pool = builder.candidates.trap + trap_doors_needed = suggested + if player in world.custom_door_types and 'Trap Door' in world.custom_door_types[player]: + custom_trap_doors = world.custom_door_types[player]['Trap Door'][builder.name] + else: + custom_trap_doors = [] + if custom_trap_doors: + trap_door_pool = filter_key_door_pool(trap_door_pool, custom_trap_doors) + trap_doors_needed -= len(custom_trap_doors) + trap_doors_needed = max(0, trap_doors_needed) + if len(trap_door_pool) < trap_doors_needed: + if not drop: + return None, 0 + trap_doors_needed = len(trap_door_pool) + combinations = ncr(len(trap_door_pool), trap_doors_needed) + itr = 0 + sample_list = build_sample_list(combinations, 1000) + proposal = kth_combination(sample_list[itr], trap_door_pool, trap_doors_needed) + proposal.extend(custom_trap_doors) + + start_regions, event_starts = filter_start_regions(builder, start_regions, world, player) + while not validate_trap_layout(proposal, builder, start_regions, paths, world, player): + itr += 1 + if itr >= len(sample_list): + if not drop: + return None, 0 + trap_doors_needed -= 1 + if trap_doors_needed < 0: + raise Exception(f'Bad dungeon {builder.name} - maybe custom trap doors are bad') + combinations = ncr(len(trap_door_pool), trap_doors_needed) + sample_list = build_sample_list(combinations, 1000) + itr = 0 + proposal = kth_combination(sample_list[itr], trap_door_pool, trap_doors_needed) + proposal.extend(custom_trap_doors) + builder.trap_door_proposal = proposal + return proposal, trap_doors_needed + + +# eliminate start region if portal marked as destination +def filter_start_regions(builder, start_regions, world, player): + std_flag = world.mode[player] == 'standard' and builder.name == 'Hyrule Castle' + excluded = {} # todo: drop lobbies, might be better to white list instead (two entrances per region) + event_doors = {} + for region in start_regions: + portal = next((x for x in world.dungeon_portals[player] if x.door.entrance.parent_region == region), None) + if portal and portal.destination: + # make sure that a drop is not accessible for this "destination" + drop_region = next((x.parent_region for x in region.entrances + if x.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld] + or x.parent_region.name == 'Sewer Drop'), None) + if not drop_region: + excluded[region] = None + if portal and not portal.destination: + portal_entrance_region = portal.door.entrance.parent_region.name + if portal_entrance_region not in builder.path_entrances: + excluded[region] = None + if std_flag and (not portal or portal.find_portal_entrance().parent_region.name != 'Hyrule Castle Courtyard'): + excluded[region] = None + if portal is None: + entrance = next((x for x in region.entrances + if x.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld] + or x.parent_region.name == 'Sewer Drop'), None) + event_doors[entrance] = None + else: + event_doors[portal.find_portal_entrance()] = None + + return [x for x in start_regions if x not in excluded.keys()], event_doors + + +def validate_trap_layout(proposal, builder, start_regions, paths, world, player): + flag, state = check_required_paths_with_traps(paths, proposal, builder.name, start_regions, world, player) + if not flag: + return False + bk_special_loc = find_bk_special_location(builder, world, player) + if bk_special_loc: + if not state.found_forced_bk(): + return False + if world.accessibility[player] != 'beatable': + all_locations = [l for r in builder.master_sector.region_set() for l in world.get_region(r, player).locations] + if any(l not in state.found_locations for l in all_locations): + return False + return True + + +def find_bk_special_location(builder, world, player): + for r_name in builder.master_sector.region_set(): + region = world.get_region(r_name, player) + for loc in region.locations: + if loc.forced_big_key(): + return loc + return None + + +def check_required_paths_with_traps(paths, proposal, dungeon_name, start_regions, world, player): + cached_initial_state = None + if len(paths[dungeon_name]) > 0: + common_starts = tuple(start_regions) + states_to_explore = {common_starts: ([], 'all')} + for path in paths[dungeon_name]: + if type(path) is tuple: + states_to_explore[tuple([path[0]])] = (path[1], 'any') + else: + # if common_starts not in states_to_explore: + # states_to_explore[common_starts] = ([], 'all') + states_to_explore[common_starts][0].append(path) + for start_regs, info in states_to_explore.items(): + dest_regs, path_type = info + if type(dest_regs) is not list: + dest_regs = [dest_regs] + check_paths = convert_regions(dest_regs, world, player) + start_regions = convert_regions(start_regs, world, player) + initial = start_regs == common_starts + if not initial or cached_initial_state is None: + if cached_initial_state and any(not cached_initial_state.visited_at_all(r) for r in start_regions): + return False, None # can't start processing the initial state because start regs aren't reachable + init = determine_init_crystal(initial, cached_initial_state, start_regions) + state = ExplorationState2(init, dungeon_name) + for region in start_regions: + state.visit_region(region) + state.add_all_doors_check_proposed_traps(region, proposal, world, player) + explore_state_proposed_traps(state, proposal, world, player) + if initial and cached_initial_state is None: + cached_initial_state = state + else: + state = cached_initial_state + if path_type == 'any': + valid, bad_region = check_if_any_regions_visited(state, check_paths) + else: + valid, bad_region = check_if_all_regions_visited(state, check_paths) + if not valid: + return False, None + return True, cached_initial_state + + +def reassign_trap_doors(trap_map, world, player): + logger = logging.getLogger('') + for name, traps in trap_map.items(): + builder = world.dungeon_layouts[player][name] + queue = deque(find_current_trap_doors(builder, world, player)) + while len(queue) > 0: + d = queue.pop() + if d.type is DoorType.Interior and d not in traps: + room = world.get_room(d.roomIndex, player) + kind = room.kind(d) + if kind == DoorKind.Trap: + new_type = (DoorKind.TrapTriggerable if d.direction in [Direction.South, Direction.East] else + DoorKind.Trap2) + room.change(d.doorListPos, new_type) + elif kind in [DoorKind.Trap2, DoorKind.TrapTriggerable]: + room.change(d.doorListPos, DoorKind.Normal) + d.blocked = False + # connect_one_way(world, d.name, d.dest.name, player) + elif d.type is DoorType.Normal and d not in traps: + world.get_room(d.roomIndex, player).change(d.doorListPos, DoorKind.Normal) + d.blocked = False + for d in traps: + change_door_to_trap(d, world, player) + world.spoiler.set_door_type(f'{d.name} ({d.dungeon_name()})', 'Trap Door', player) + logger.debug(f'Trap Door: {d.name} ({d.dungeon_name()})') + + +def exclude_boss_traps(d): + return ' Boss ' not in d.name and ' Agahnim ' not in d.name and d.name not in ['Skull Spike Corner SW'] + + +def find_current_trap_doors(builder, world, player): + checker = exclude_boss_traps if world.trap_door_mode[player] in ['vanilla', 'optional'] else (lambda x: True) + current_doors = [] + for region in builder.master_sector.regions: + for ext in region.exits: + d = ext.door + if d and d.blocked and d.trapFlag != 0 and checker(d): + current_doors.append(d) + return current_doors + + +def change_door_to_trap(d, world, player): + room = world.get_room(d.roomIndex, player) + if d.type is DoorType.Interior: + kind = room.kind(d) + new_kind = None + if kind == DoorKind.Trap: + new_kind = DoorKind.Trap + elif kind == DoorKind.TrapTriggerable and d.direction in [Direction.South, Direction.East]: + new_kind = DoorKind.Trap + elif kind == DoorKind.Trap2 and d.direction in [Direction.North, Direction.West]: + new_kind = DoorKind.Trap + elif d.direction in [Direction.South, Direction.East]: + new_kind = DoorKind.Trap2 + elif d.direction in [Direction.North, Direction.West]: + new_kind = DoorKind.TrapTriggerable + if new_kind: + d.blocked = True + pos = 3 if d.type == DoorType.Normal else 4 + verify_door_list_pos(d, room, world, player, pos) + d.trapFlag = {0: 0x4, 1: 0x2, 2: 0x1, 3: 0x8}[d.doorListPos] + room.change(d.doorListPos, new_kind) + if d.entrance.connected_region is not None: + d.entrance.connected_region.entrances.remove(d.entrance) + d.entrance.connected_region = None + elif d.type is DoorType.Normal: + d.blocked = True + verify_door_list_pos(d, room, world, player, pos=3) + d.trapFlag = {0: 0x4, 1: 0x2, 2: 0x1}[d.doorListPos] + room.change(d.doorListPos, DoorKind.Trap) + if d.entrance.connected_region is not None: + d.entrance.connected_region.entrances.remove(d.entrance) + d.entrance.connected_region = None + + +def find_big_key_candidates(builder, start_regions, used, world, player): + if world.door_type_mode[player] != 'original': # big, all, chaos + # traverse dungeon and find candidates + candidates = [] + checked_doors = set() + for region in start_regions: + possible, checked = find_big_key_door_candidates(region, checked_doors, used, world, player) + candidates.extend([x for x in possible if x not in candidates]) + checked_doors.update(checked) + flat_candidates = [] + for candidate in candidates: + # not valid if: Normal Coupled and Pair in is Checked and Pair is not in Candidates + if (world.decoupledoors[player] or candidate.type != DoorType.Normal + or candidate.dest not in checked_doors or candidate.dest in candidates): + flat_candidates.append(candidate) + + paired_candidates = build_pair_list(flat_candidates) + builder.candidates.big = paired_candidates + else: + r_set = builder.master_sector.region_set() + for r in r_set: + for ext in world.get_region(r, player).exits: + if ext.door: + d = ext.door + if d.bigKey and d.type in [DoorType.Normal, DoorType.Interior]: + builder.candidates.big.append(d) + + +def find_big_key_door_candidates(region, checked, used, world, player): + decoupled = world.decoupledoors[player] + dungeon_name = region.dungeon.name + candidates = [] + checked_doors = list(checked) + queue = deque([(region, None, None)]) + while len(queue) > 0: + current, last_door, last_region = queue.pop() + for ext in current.exits: + d = ext.door + controlled = d + if d and d.controller: + d = d.controller + if (d and not d.blocked and d.dest is not last_door and d.dest is not last_region + and d not in checked_doors): + valid = False + if (0 <= d.doorListPos < 4 and d.type in [DoorType.Interior, DoorType.Normal] + and not d.entranceFlag and d.direction in [Direction.North, Direction.South] and d not in used): + room = world.get_room(d.roomIndex, player) + position, kind = room.doorList[d.doorListPos] + if d.type == DoorType.Interior: + valid = kind in okay_interiors + if valid and d.dest not in candidates: # interior doors are not separable yet + candidates.append(d.dest) + elif d.type == DoorType.Normal: + valid = kind in okay_normals + if valid and not decoupled: + d2 = d.dest + if d2 not in candidates and d2 not in used: + if d2.type == DoorType.Normal: + room_b = world.get_room(d2.roomIndex, player) + pos_b, kind_b = room_b.doorList[d2.doorListPos] + valid &= kind_b in okay_normals and valid_key_door_pair(d, d2) + if valid and 0 <= d2.doorListPos < 4: + candidates.append(d2) + if valid and d not in candidates: + candidates.append(d) + connected = ext.connected_region + if valid_region_to_explore(connected, dungeon_name, world, player): + queue.append((ext.connected_region, controlled, current)) + if d is not None: + checked_doors.append(d) + return candidates, checked_doors + + +def find_valid_bk_combination(builder, suggested, start_regions, world, player, drop=True): + bk_door_pool = builder.candidates.big + bk_doors_needed = suggested + if player in world.custom_door_types and 'Big Key Door' in world.custom_door_types[player]: + custom_bk_doors = world.custom_door_types[player]['Big Key Door'][builder.name] + else: + custom_bk_doors = [] + if custom_bk_doors: + bk_door_pool = filter_key_door_pool(bk_door_pool, custom_bk_doors) + bk_doors_needed -= len(custom_bk_doors) + bk_doors_needed = max(0, bk_doors_needed) + if len(bk_door_pool) < bk_doors_needed: + if not drop: + return None, 0 + bk_doors_needed = len(bk_door_pool) + combinations = ncr(len(bk_door_pool), bk_doors_needed) + itr = 0 + sample_list = build_sample_list(combinations, 10000) + proposal = kth_combination(sample_list[itr], bk_door_pool, bk_doors_needed) + proposal.extend(custom_bk_doors) + + start_regions, event_starts = filter_start_regions(builder, start_regions, world, player) + while not validate_bk_layout(proposal, builder, start_regions, world, player): + itr += 1 + if itr >= len(sample_list): + if not drop: + return None, 0 + bk_doors_needed -= 1 + if bk_doors_needed < 0: + raise Exception(f'Bad dungeon {builder.name} - maybe custom bk doors are bad') + combinations = ncr(len(bk_door_pool), bk_doors_needed) + sample_list = build_sample_list(combinations, 10000) + itr = 0 + proposal = kth_combination(sample_list[itr], bk_door_pool, bk_doors_needed) + proposal.extend(custom_bk_doors) + builder.bk_door_proposal = proposal + return proposal, bk_doors_needed + + +def find_current_bk_doors(builder): + current_doors = [] + for region in builder.master_sector.regions: + for ext in region.exits: + d = ext.door + if d and d.type != DoorType.Logical and d.bigKey: + current_doors.append(d) + return current_doors + + +def reassign_big_key_doors(bk_map, world, player): + logger = logging.getLogger('') + for name, big_doors in bk_map.items(): + flat_proposal = flatten_pair_list(big_doors) + builder = world.dungeon_layouts[player][name] + queue = deque(find_current_bk_doors(builder)) + while len(queue) > 0: + d = queue.pop() + if d.type is DoorType.Interior and d not in flat_proposal and d.dest not in flat_proposal: + if not d.entranceFlag: + world.get_room(d.roomIndex, player).change(d.doorListPos, DoorKind.Normal) + d.bigKey = False + elif d.type is DoorType.Normal and d not in flat_proposal: + if not d.entranceFlag: + world.get_room(d.roomIndex, player).change(d.doorListPos, DoorKind.Normal) + d.bigKey = False + for obj in big_doors: + if type(obj) is tuple: + d1 = obj[0] + d2 = obj[1] + if d1.type is DoorType.Interior: + change_door_to_big_key(d1, world, player) + d2.bigKey = True # ensure flag is set + if d2.smallKey: + d2.smallKey = False + else: + world.paired_doors[player].append(PairedDoor(d1.name, d2.name)) + change_door_to_big_key(d1, world, player) + change_door_to_big_key(d2, world, player) + world.spoiler.set_door_type(f'{d1.name} <-> {d2.name} ({d1.dungeon_name()})', 'Big Key Door', player) + logger.debug(f'Big Key Door: {d1.name} <-> {d2.name} ({d1.dungeon_name()})') + else: + d = obj + if d.type is DoorType.Interior: + change_door_to_big_key(d, world, player) + if world.door_type_mode[player] != 'original': + d.dest.bigKey = True # ensure flag is set when bk doors are double sided + elif d.type is DoorType.SpiralStairs: + pass # we don't have spiral stairs candidates yet that aren't already key doors + elif d.type is DoorType.Normal: + change_door_to_big_key(d, world, player) + if not world.decoupledoors[player] and d.dest and world.door_type_mode[player] != 'original': + if d.dest.type in [DoorType.Normal]: + dest_room = world.get_room(d.dest.roomIndex, player) + if stateful_door(d.dest, dest_room.kind(d.dest)): + change_door_to_big_key(d.dest, world, player) + add_pair(d, d.dest, world, player) + world.spoiler.set_door_type(f'{d.name} ({d.dungeon_name()})', 'Big Key Door', player) + logger.debug(f'Big Key Door: {d.name} ({d.dungeon_name()})') + + +def change_door_to_big_key(d, world, player): + d.bigKey = True + if d.smallKey: + d.smallKey = False + room = world.get_room(d.roomIndex, player) + if room.doorList[d.doorListPos][1] != DoorKind.BigKey: + verify_door_list_pos(d, room, world, player) + room.change(d.doorListPos, DoorKind.BigKey) + + +def find_small_key_door_candidates(builder, start_regions, used, world, player): # traverse dungeon and find candidates candidates = [] checked_doors = set() for region in start_regions: - possible, checked = find_key_door_candidates(region, checked_doors, world, player) + possible, checked = find_key_door_candidates(region, checked_doors, used, world, player) candidates.extend([x for x in possible if x not in candidates]) checked_doors.update(checked) flat_candidates = [] for candidate in candidates: - # not valid if: Normal and Pair in is Checked and Pair is not in Candidates - if candidate.type != DoorType.Normal or candidate.dest not in checked_doors or candidate.dest in candidates: + # not valid if: Normal Coupled and Pair in is Checked and Pair is not in Candidates + if (world.decoupledoors[player] or candidate.type != DoorType.Normal + or candidate.dest not in checked_doors or candidate.dest in candidates): flat_candidates.append(candidate) paired_candidates = build_pair_list(flat_candidates) - builder.candidates = paired_candidates + builder.candidates.small = paired_candidates -def calc_used_dungeon_items(builder): - base = 4 - if builder.bk_required and not builder.bk_provided: +def calc_used_dungeon_items(builder, world, player): + basic_flag = world.doorShuffle[player] == 'basic' + base = 0 if basic_flag else 2 # at least 2 items per dungeon, except in basic + base = max(count_reserved_locations(world, player, builder.location_set), base) + if not world.bigkeyshuffle[player]: + if builder.bk_required and not builder.bk_provided: + base += 1 + if not world.compassshuffle[player] and (builder.name not in ['Hyrule Castle', 'Agahnims Tower'] or not basic_flag): + base += 1 + if not world.mapshuffle[player] and (builder.name != 'Agahnims Tower' or not basic_flag): 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): +def find_valid_combination(builder, target, start_regions, world, player, drop_keys=True): logger = logging.getLogger('') + key_door_pool = list(builder.candidates.small) + key_doors_needed = target + if player in world.custom_door_types and 'Key Door' in world.custom_door_types[player]: + custom_key_doors = world.custom_door_types[player]['Key Door'][builder.name] + else: + custom_key_doors = [] + if custom_key_doors: # could validate that each custom item is in the candidates + key_door_pool = filter_key_door_pool(key_door_pool, custom_key_doors) + key_doors_needed -= len(custom_key_doors) + key_doors_needed = max(0, key_doors_needed) # find valid combination of candidates - if len(builder.candidates) < builder.key_doors_num: + if len(key_door_pool) < key_doors_needed: 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('%s: %s', world.fish.translate("cli","cli","lowering.keys.candidates"), builder.name) - combinations = ncr(len(builder.candidates), builder.key_doors_num) + return None, 0 + builder.key_doors_num -= key_doors_needed - len(key_door_pool) # reduce number of key doors + key_doors_needed = len(key_door_pool) + logger.info('%s: %s', world.fish.translate("cli", "cli", "lowering.keys.candidates"), builder.name) + combinations = ncr(len(key_door_pool), key_doors_needed) itr = 0 start = time.process_time() - sample_list = list(range(0, int(combinations))) - random.shuffle(sample_list) - proposal = kth_combination(sample_list[itr], builder.candidates, builder.key_doors_num) + sample_list = build_sample_list(combinations) + proposal = kth_combination(sample_list[itr], key_door_pool, key_doors_needed) + proposal.extend(custom_key_doors) + builder.key_doors_num = len(proposal) + start_regions, event_starts = filter_start_regions(builder, start_regions, world, player) - # eliminate start region if portal marked as destination - excluded = {} - for region in start_regions: - portal = next((x for x in world.dungeon_portals[player] if x.door.entrance.parent_region == region), None) - if portal and portal.destination: - excluded[region] = None - start_regions = [x for x in start_regions if x not in excluded.keys()] - - key_layout = build_key_layout(builder, start_regions, proposal, world, player) + key_layout = build_key_layout(builder, start_regions, proposal, event_starts, world, player) determine_prize_lock(key_layout, world, player) while not validate_key_layout(key_layout, world, player): itr += 1 - 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 itr >= len(sample_list): if not drop_keys: logger.info('No valid layouts for %s with %s doors', builder.name, builder.key_doors_num) - return False + return None, 0 logger.info('%s: %s', world.fish.translate("cli","cli","lowering.keys.layouts"), 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) + key_doors_needed -= 1 + if key_doors_needed < 0: + raise Exception(f'Bad dungeon {builder.name} - less than 0 key doors or invalid custom key door') + combinations = ncr(len(key_door_pool), max(0, key_doors_needed)) + sample_list = build_sample_list(combinations) itr = 0 start = time.process_time() # reset time since itr reset - proposal = kth_combination(sample_list[itr], builder.candidates, builder.key_doors_num) + proposal = kth_combination(sample_list[itr], key_door_pool, key_doors_needed) + proposal.extend(custom_key_doors) key_layout.reset(proposal, builder, world, player) if (itr+1) % 1000 == 0: mark = time.process_time()-start @@ -1534,7 +2679,202 @@ def find_valid_combination(builder, start_regions, world, player, drop_keys=True builder.key_door_proposal = proposal world.key_logic[player][builder.name] = key_layout.key_logic world.key_layout[player][builder.name] = key_layout - return True + return builder.key_door_proposal, key_doors_needed + len(custom_key_doors) + + +def find_bd_candidates(builder, start_regions, used, world, player): + # traverse dungeon and find candidates + candidates = [] + checked_doors = set() + for region in start_regions: + possible, checked = find_bd_door_candidates(region, checked_doors, used, world, player) + candidates.extend([x for x in possible if x not in candidates]) + checked_doors.update(checked) + flat_candidates = [] + for candidate in candidates: + # not valid if: Normal Coupled and Pair in is Checked and Pair is not in Candidates + if (world.decoupledoors[player] or candidate.type != DoorType.Normal + or candidate.dest not in checked_doors or candidate.dest in candidates): + flat_candidates.append(candidate) + builder.candidates.bomb_dash = build_pair_list(flat_candidates) + + +def find_bd_door_candidates(region, checked, used, world, player): + decoupled = world.decoupledoors[player] + dungeon_name = region.dungeon.name + candidates = [] + checked_doors = list(checked) + queue = deque([(region, None, None)]) + while len(queue) > 0: + current, last_door, last_region = queue.pop() + for ext in current.exits: + d = ext.door + controlled = d + if d and d.controller: + d = d.controller + if (d and not d.blocked and d.dest is not last_door and d.dest is not last_region + and d not in checked_doors): + valid = False + if (0 <= d.doorListPos < 4 and d.type in [DoorType.Interior, DoorType.Normal] and not d.entranceFlag + and d not in used): + room = world.get_room(d.roomIndex, player) + position, kind = room.doorList[d.doorListPos] + if d.type == DoorType.Interior: + # interior doors are not separable yet + valid = kind in okay_interiors and d.dest not in used + if valid and d.dest not in candidates: + candidates.append(d.dest) + elif d.type == DoorType.Normal: + valid = kind in okay_normals + if valid and not decoupled: + d2 = d.dest + if d2 not in candidates and d2 not in used: + if d2.type == DoorType.Normal: + room_b = world.get_room(d2.roomIndex, player) + pos_b, kind_b = room_b.doorList[d2.doorListPos] + valid &= kind_b in okay_normals and valid_key_door_pair(d, d2) + if valid and 0 <= d2.doorListPos < 4: + candidates.append(d2) + if valid and d not in candidates: + candidates.append(d) + connected = ext.connected_region + if valid_region_to_explore(connected, dungeon_name, world, player): + queue.append((ext.connected_region, controlled, current)) + if d is not None: + checked_doors.append(d) + return candidates, checked_doors + + +def find_valid_bd_combination(builder, suggested, world, player): + # bombable/dashable doors could be excluded in escape in standard until we can guarantee bomb access + # if world.mode[player] == 'standard' and builder.name == 'Hyrule Castle': + # return None, None, 0 + bd_door_pool = builder.candidates.bomb_dash + bomb_doors_needed, dash_doors_needed = suggested + ttl_needed = bomb_doors_needed + dash_doors_needed + if player in world.custom_door_types and 'Bomb Door' in world.custom_door_types[player]: + custom_bomb_doors = world.custom_door_types[player]['Bomb Door'][builder.name] + custom_dash_doors = world.custom_door_types[player]['Dash Door'][builder.name] + else: + custom_bomb_doors = [] + custom_dash_doors = [] + if custom_bomb_doors: + bd_door_pool = filter_key_door_pool(bd_door_pool, custom_bomb_doors) + bomb_doors_needed -= len(custom_bomb_doors) + if custom_dash_doors: + bd_door_pool = filter_key_door_pool(bd_door_pool, custom_dash_doors) + dash_doors_needed -= len(custom_dash_doors) + while len(bd_door_pool) < bomb_doors_needed + dash_doors_needed: + test = random.choice([True, False]) + if test: + bomb_doors_needed -= 1 + if bomb_doors_needed < 0: + bomb_doors_needed = 0 + else: + dash_doors_needed -= 1 + if dash_doors_needed < 0: + dash_doors_needed = 0 + bomb_proposal = random.sample(bd_door_pool, k=bomb_doors_needed) + bomb_proposal.extend(custom_bomb_doors) + dash_pool = [x for x in bd_door_pool if x not in bomb_proposal] + dash_proposal = random.sample(dash_pool, k=dash_doors_needed) + dash_proposal.extend(custom_dash_doors) + return bomb_proposal, dash_proposal, ttl_needed + + +def reassign_bd_doors(bd_map, world, player): + for name, pair in bd_map.items(): + flat_bomb_proposal = flatten_pair_list(pair[0]) + flat_dash_proposal = flatten_pair_list(pair[1]) + + def not_in_proposal(door): + return (door not in flat_bomb_proposal and door.dest not in flat_bomb_proposal and + door not in flat_dash_proposal and door.dest not in flat_bomb_proposal) + + builder = world.dungeon_layouts[player][name] + queue = deque(find_current_bd_doors(builder, world)) + while len(queue) > 0: + d = queue.pop() + if d.type is DoorType.Interior and not_in_proposal(d): + if not d.entranceFlag: + world.get_room(d.roomIndex, player).change(d.doorListPos, DoorKind.Normal) + elif d.type is DoorType.Normal and not_in_proposal(d): + if not d.entranceFlag: + world.get_room(d.roomIndex, player).change(d.doorListPos, DoorKind.Normal) + do_bombable_dashable(pair[0], DoorKind.Bombable, world, player) + do_bombable_dashable(pair[1], DoorKind.Dashable, world, player) + + +def do_bombable_dashable(proposal, kind, world, player): + for obj in proposal: + if type(obj) is tuple: + d1 = obj[0] + d2 = obj[1] + if d1.type is DoorType.Interior: + change_door_to_kind(d1, kind, world, player) + else: + names = [d1.name, d2.name] + found = False + for dp in world.paired_doors[player]: + if dp.door_a in names and dp.door_b in names: + dp.pair = True + found = True + elif dp.door_a in names: + dp.pair = False + elif dp.door_b in names: + dp.pair = False + if not found: + world.paired_doors[player].append(PairedDoor(d1.name, d2.name)) + change_door_to_kind(d1, kind, world, player) + change_door_to_kind(d2, kind, world, player) + spoiler_type = 'Bomb Door' if kind == DoorKind.Bombable else 'Dash Door' + world.spoiler.set_door_type(f'{d1.name} <-> {d2.name} ({d1.dungeon_name()})', spoiler_type, player) + else: + d = obj + if d.type is DoorType.Interior: + change_door_to_kind(d, kind, world, player) + elif d.type is DoorType.Normal: + change_door_to_kind(d, kind, world, player) + if not world.decoupledoors[player] and d.dest: + if d.dest.type in okay_normals and not std_forbidden(d.dest, world, player): + dest_room = world.get_room(d.dest.roomIndex, player) + if stateful_door(d.dest, dest_room.kind(d.dest)): + change_door_to_kind(d.dest, kind, world, player) + add_pair(d, d.dest, world, player) + spoiler_type = 'Bomb Door' if kind == DoorKind.Bombable else 'Dash Door' + world.spoiler.set_door_type(f'{d.name} ({d.dungeon_name()})', spoiler_type, player) + + +def find_current_bd_doors(builder, world): + current_doors = [] + for region in builder.master_sector.regions: + for ext in region.exits: + d = ext.door + if d and d.type in [DoorType.Interior, DoorType.Normal]: + kind = d.kind(world) + if kind in [DoorKind.Dashable, DoorKind.Bombable]: + current_doors.append(d) + return current_doors + + +def change_door_to_kind(d, kind, world, player): + room = world.get_room(d.roomIndex, player) + if room.doorList[d.doorListPos][1] != kind: + verify_door_list_pos(d, room, world, player) + room.change(d.doorListPos, kind) + + +def build_sample_list(combinations, max_combinations=10000): + if combinations <= max_combinations: + sample_list = list(range(0, int(combinations))) + else: + num_set = set() + while len(num_set) < max_combinations: + num_set.add(random.randint(0, combinations)) + sample_list = list(num_set) + sample_list.sort() + random.shuffle(sample_list) + return sample_list def log_key_logic(d_name, key_logic): @@ -1570,7 +2910,8 @@ def build_pair_list(flat_list): queue = deque(flat_list) while len(queue) > 0: d = queue.pop() - if d.dest in queue and d.type != DoorType.SpiralStairs: + paired = d.dest.dest == d + if d.dest in queue and d.type != DoorType.SpiralStairs and paired: paired_list.append((d, d.dest)) queue.remove(d.dest) else: @@ -1589,10 +2930,14 @@ def flatten_pair_list(paired_list): return flat_list -okay_normals = [DoorKind.Normal, DoorKind.SmallKey, DoorKind.Bombable, DoorKind.Dashable, DoorKind.DungeonChanger] +okay_normals = [DoorKind.Normal, DoorKind.SmallKey, DoorKind.Bombable, DoorKind.Dashable, + DoorKind.DungeonChanger, DoorKind.BigKey] + +okay_interiors = [DoorKind.Normal, DoorKind.SmallKey, DoorKind.Bombable, DoorKind.Dashable, DoorKind.BigKey] -def find_key_door_candidates(region, checked, world, player): +def find_key_door_candidates(region, checked, used, world, player): + decoupled = world.decoupledoors[player] dungeon_name = region.dungeon.name candidates = [] checked_doors = list(checked) @@ -1601,38 +2946,39 @@ def find_key_door_candidates(region, checked, world, player): current, last_door, last_region = queue.pop() for ext in current.exits: d = ext.door + controlled = d if d and d.controller: d = d.controller - if d and not d.blocked and d.dest is not last_door and d.dest is not last_region and d not in checked_doors: + if (d and not d.blocked and d.dest is not last_door and d.dest is not last_region + and d not in checked_doors): valid = False if (0 <= d.doorListPos < 4 and d.type in [DoorType.Interior, DoorType.Normal, DoorType.SpiralStairs] - and not d.entranceFlag): + and not d.entranceFlag and d not in used): room = world.get_room(d.roomIndex, player) position, kind = room.doorList[d.doorListPos] if d.type == DoorType.Interior: - valid = kind in [DoorKind.Normal, DoorKind.SmallKey, DoorKind.Bombable, DoorKind.Dashable] - if valid and d.dest not in candidates: # interior doors are not separable yet + valid = kind in okay_interiors and d.dest not in used + # interior doors are not separable yet + if valid and d.dest not in candidates: candidates.append(d.dest) elif d.type == DoorType.SpiralStairs: valid = kind in [DoorKind.StairKey, DoorKind.StairKey2, DoorKind.StairKeyLow] elif d.type == DoorType.Normal: - d2 = d.dest - if d2 not in candidates: - if d2.type == DoorType.Normal: - room_b = world.get_room(d2.roomIndex, player) - pos_b, kind_b = room_b.doorList[d2.doorListPos] - valid = kind in okay_normals and kind_b in okay_normals and valid_key_door_pair(d, d2) - else: - valid = kind in okay_normals - if valid and 0 <= d2.doorListPos < 4: - candidates.append(d2) - else: - valid = True + valid = kind in okay_normals + if valid and not decoupled: + d2 = d.dest + if d2 not in candidates and d2 not in used: + if d2.type == DoorType.Normal: + room_b = world.get_room(d2.roomIndex, player) + pos_b, kind_b = room_b.doorList[d2.doorListPos] + valid &= kind_b in okay_normals and valid_key_door_pair(d, d2) + if valid and 0 <= d2.doorListPos < 4: + candidates.append(d2) if valid and d not in candidates: candidates.append(d) connected = ext.connected_region - if valid_region_to_explore_lim(connected, dungeon_name, world, player): - queue.append((ext.connected_region, d, current)) + if valid_region_to_explore(connected, dungeon_name, world, player): + queue.append((ext.connected_region, controlled, current)) if d is not None: checked_doors.append(d) return candidates, checked_doors @@ -1644,87 +2990,111 @@ def valid_key_door_pair(door1, door2): return len(door1.entrance.parent_region.exits) <= 1 or len(door2.entrance.parent_region.exits) <= 1 -def reassign_key_doors(builder, world, player): +def reassign_key_doors(small_map, world, player): logger = logging.getLogger('') - logger.debug('Key doors for %s', builder.name) - proposal = builder.key_door_proposal - flat_proposal = flatten_pair_list(proposal) - queue = deque(find_current_key_doors(builder)) - while len(queue) > 0: - d = queue.pop() - if d.type is DoorType.SpiralStairs and d not in proposal: - room = world.get_room(d.roomIndex, player) - if room.doorList[d.doorListPos][1] == DoorKind.StairKeyLow: - room.delete(d.doorListPos) - else: - if len(room.doorList) > 1: - room.mirror(d.doorListPos) # I think this works for crossed now - else: + for name, small_doors in small_map.items(): + logger.debug(f'Key doors for {name}') + builder = world.dungeon_layouts[player][name] + proposal = builder.key_door_proposal + flat_proposal = flatten_pair_list(proposal) + queue = deque(find_current_key_doors(builder)) + while len(queue) > 0: + d = queue.pop() + if d.type is DoorType.SpiralStairs and d not in proposal: + room = world.get_room(d.roomIndex, player) + if room.doorList[d.doorListPos][1] == DoorKind.StairKeyLow: room.delete(d.doorListPos) - d.smallKey = False - elif d.type is DoorType.Interior and d not in flat_proposal and d.dest not in flat_proposal: - if not d.entranceFlag: - world.get_room(d.roomIndex, player).change(d.doorListPos, DoorKind.Normal) - d.smallKey = False - d.dest.smallKey = False - queue.remove(d.dest) - elif d.type is DoorType.Normal and d not in flat_proposal: - if not d.entranceFlag: - world.get_room(d.roomIndex, player).change(d.doorListPos, DoorKind.Normal) - d.smallKey = False - for dp in world.paired_doors[player]: - if dp.door_a == d.name or dp.door_b == d.name: - dp.pair = False - for obj in proposal: - if type(obj) is tuple: - d1 = obj[0] - d2 = obj[1] - if d1.type is DoorType.Interior: - change_door_to_small_key(d1, world, player) - d2.smallKey = True # ensure flag is set - else: - names = [d1.name, d2.name] - found = False + else: + if len(room.doorList) > 1: + room.mirror(d.doorListPos) # I think this works for crossed now + else: + room.delete(d.doorListPos) + d.smallKey = False + elif d.type is DoorType.Interior and d not in flat_proposal and d.dest not in flat_proposal: + if not d.entranceFlag: + world.get_room(d.roomIndex, player).change(d.doorListPos, DoorKind.Normal) + d.smallKey = False + d.dest.smallKey = False + queue.remove(d.dest) + elif d.type is DoorType.Normal and d not in flat_proposal: + if not d.entranceFlag: + world.get_room(d.roomIndex, player).change(d.doorListPos, DoorKind.Normal) + d.smallKey = False for dp in world.paired_doors[player]: - if dp.door_a in names and dp.door_b in names: - dp.pair = True - found = True - elif dp.door_a in names: + if dp.door_a == d.name or dp.door_b == d.name: dp.pair = False - elif dp.door_b in names: - dp.pair = False - if not found: - world.paired_doors[player].append(PairedDoor(d1.name, d2.name)) - change_door_to_small_key(d1, world, player) - change_door_to_small_key(d2, world, player) - world.spoiler.set_door_type(d1.name+' <-> '+d2.name, 'Key Door', player) - logger.debug('Key Door: %s', d1.name+' <-> '+d2.name) - else: - d = obj - if d.type is DoorType.Interior: - change_door_to_small_key(d, world, player) - d.dest.smallKey = True # ensure flag is set - elif d.type is DoorType.SpiralStairs: - pass # we don't have spiral stairs candidates yet that aren't already key doors - elif d.type is DoorType.Normal: - change_door_to_small_key(d, world, player) - world.spoiler.set_door_type(d.name, 'Key Door', player) - logger.debug('Key Door: %s', d.name) + for obj in proposal: + if type(obj) is tuple: + d1 = obj[0] + d2 = obj[1] + if d1.type is DoorType.Interior: + change_door_to_small_key(d1, world, player) + d2.smallKey = True # ensure flag is set + else: + names = [d1.name, d2.name] + found = False + for dp in world.paired_doors[player]: + if dp.door_a in names and dp.door_b in names: + dp.pair = True + found = True + elif dp.door_a in names: + dp.pair = False + elif dp.door_b in names: + dp.pair = False + if not found: + world.paired_doors[player].append(PairedDoor(d1.name, d2.name)) + change_door_to_small_key(d1, world, player) + change_door_to_small_key(d2, world, player) + world.spoiler.set_door_type(f'{d1.name} <-> {d2.name} ({d1.dungeon_name()})', 'Key Door', player) + logger.debug(f'Key Door: {d1.name} <-> {d2.name} ({d1.dungeon_name()})') + else: + d = obj + if d.type is DoorType.Interior: + change_door_to_small_key(d, world, player) + d.dest.smallKey = True # ensure flag is set + elif d.type is DoorType.SpiralStairs: + pass # we don't have spiral stairs candidates yet that aren't already key doors + elif d.type is DoorType.Normal: + change_door_to_small_key(d, world, player) + if not world.decoupledoors[player] and d.dest: + if d.dest.type in [DoorType.Normal]: + dest_room = world.get_room(d.dest.roomIndex, player) + if stateful_door(d.dest, dest_room.kind(d.dest)): + change_door_to_small_key(d.dest, world, player) + add_pair(d, d.dest, world, player) + world.spoiler.set_door_type(f'{d.name} ({d.dungeon_name()})', 'Key Door', player) + logger.debug(f'Key Door: {d.name} ({d.dungeon_name()})') def change_door_to_small_key(d, world, player): d.smallKey = True room = world.get_room(d.roomIndex, player) if room.doorList[d.doorListPos][1] != DoorKind.SmallKey: + verify_door_list_pos(d, room, world, player) room.change(d.doorListPos, DoorKind.SmallKey) +def verify_door_list_pos(d, room, world, player, pos=4): + if d.doorListPos >= pos: + new_index = room.next_free(pos) + if new_index is not None: + room.swap(new_index, d.doorListPos) + other = next(x for x in world.doors if x.player == player and x.roomIndex == d.roomIndex + and x.doorListPos == new_index) + other.doorListPos = d.doorListPos + d.doorListPos = new_index + else: + raise Exception(f'Invalid stateful door: {d.name}. Only {pos} stateful doors per supertile') + + def smooth_door_pairs(world, player): all_doors = [x for x in world.doors if x.player == player] skip = set() bd_candidates = defaultdict(list) for door in all_doors: if door.type in [DoorType.Normal, DoorType.Interior] and door not in skip and not door.entranceFlag: + if not door.dest: + continue partner = door.dest skip.add(partner) room_a = world.get_room(door.roomIndex, player) @@ -1794,7 +3164,7 @@ def remove_pair(door, world, player): def stateful_door(door, kind): if 0 <= door.doorListPos < 4: - return kind in [DoorKind.Normal, DoorKind.SmallKey, DoorKind.Bombable, DoorKind.Dashable] #, DoorKind.BigKey] + return kind in [DoorKind.Normal, DoorKind.SmallKey, DoorKind.Bombable, DoorKind.Dashable, DoorKind.BigKey] return False @@ -1803,6 +3173,19 @@ def std_forbidden(door, world, player): 'Hyrule Castle Throne Room N' in [door.name, door.dest.name]) +def custom_door_kind(custom_key, kind, bd_candidates, counts, world, player): + if custom_key in world.custom_door_types[player]: + for door_a, door_b in world.custom_door_types[player][custom_key]: + change_pair_type(door_a, kind, world, player) + d_name = door_a.entrance.parent_region.dungeon.name + bd_list = next(bd_list for dungeon, bd_list in bd_candidates.items() if dungeon.name == d_name) + if door_a in bd_list: + bd_list.remove(door_a) + if door_b in bd_list: + bd_list.remove(door_b) + counts[d_name] += 1 + + dashable_forbidden = { 'Swamp Trench 1 Key Ledge NW', 'Swamp Left Elbow WN', 'Swamp Right Elbow SE', 'Mire Hub WN', 'Mire Hub WS', 'Mire Hub Top NW', 'Mire Hub NE', 'Ice Dead End WS' @@ -1823,15 +3206,20 @@ def filter_dashable_candidates(candidates, world): def shuffle_bombable_dashable(bd_candidates, world, player): + dash_counts = defaultdict(int) + bomb_counts = defaultdict(int) + if world.custom_door_types[player]: + custom_door_kind('Dash Door', DoorKind.Dashable, bd_candidates, dash_counts, world, player) + custom_door_kind('Bomb Door', DoorKind.Bombable, bd_candidates, bomb_counts, world, player) if world.doorShuffle[player] == 'basic': for dungeon, candidates in bd_candidates.items(): - diff = bomb_dash_counts[dungeon.name][1] + diff = bomb_dash_counts[dungeon.name][1] - dash_counts[dungeon.name] if diff > 0: dash_candidates = filter_dashable_candidates(candidates, world) for chosen in random.sample(dash_candidates, min(diff, len(candidates))): change_pair_type(chosen, DoorKind.Dashable, world, player) candidates.remove(chosen) - diff = bomb_dash_counts[dungeon.name][0] + diff = bomb_dash_counts[dungeon.name][0] - bomb_counts[dungeon.name] if diff > 0: for chosen in random.sample(candidates, min(diff, len(candidates))): change_pair_type(chosen, DoorKind.Bombable, world, player) @@ -1840,26 +3228,32 @@ def shuffle_bombable_dashable(bd_candidates, world, player): remove_pair_type_if_present(excluded, world, player) elif world.doorShuffle[player] == 'crossed': all_candidates = sum(bd_candidates.values(), []) - dash_candidates = filter_dashable_candidates(all_candidates, world) - for chosen in random.sample(dash_candidates, min(8, len(all_candidates))): - change_pair_type(chosen, DoorKind.Dashable, world, player) - all_candidates.remove(chosen) - for chosen in random.sample(all_candidates, min(12, len(all_candidates))): - change_pair_type(chosen, DoorKind.Bombable, world, player) - all_candidates.remove(chosen) + desired_dashables = 8 - sum(dash_counts.values(), 0) + desired_bombables = 12 - sum(bomb_counts.values(), 0) + if desired_dashables > 0: + dash_candidates = filter_dashable_candidates(all_candidates, world) + for chosen in random.sample(dash_candidates, min(desired_dashables, len(all_candidates))): + change_pair_type(chosen, DoorKind.Dashable, world, player) + all_candidates.remove(chosen) + if desired_bombables > 0: + for chosen in random.sample(all_candidates, min(desired_bombables, len(all_candidates))): + change_pair_type(chosen, DoorKind.Bombable, world, player) + all_candidates.remove(chosen) for excluded in all_candidates: remove_pair_type_if_present(excluded, world, player) def change_pair_type(door, new_type, world, player): room_a = world.get_room(door.roomIndex, player) + verify_door_list_pos(door, room_a, world, player) room_a.change(door.doorListPos, new_type) if door.type != DoorType.Interior: room_b = world.get_room(door.dest.roomIndex, player) + verify_door_list_pos(door.dest, room_b, world, player) room_b.change(door.dest.doorListPos, new_type) add_pair(door, door.dest, world, player) spoiler_type = 'Bomb Door' if new_type == DoorKind.Bombable else 'Dash Door' - world.spoiler.set_door_type(door.name + ' <-> ' + door.dest.name, spoiler_type, player) + world.spoiler.set_door_type(f'{door.name} <-> {door.dest.name} ({door.dungeon_name()})', spoiler_type, player) def remove_pair_type_if_present(door, world, player): @@ -1879,6 +3273,10 @@ def find_inaccessible_regions(world, player): world.inaccessible_regions[player] = [] start_regions = ['Links House' if not world.is_bombshop_start(player) else 'Big Bomb Shop', 'Sanctuary' if world.mode[player] != 'inverted' else 'Dark Sanctuary Hint'] regs = convert_regions(start_regions, world, player) + if all(all(not e.connected_region for e in r.exits) for r in regs): + # if attempting to find inaccessible regions before any connections made above, assume eventual access to Pyramid S&Q + start_regions = ['Pyramid Area' if not world.is_tile_swapped(0x1b, player) else 'Hyrule Castle Ledge'] + regs = convert_regions(start_regions, world, player) all_regions = [r for r in world.regions if r.player == player and r.type is not RegionType.Dungeon] visited_regions = set() queue = deque(regs) @@ -1951,6 +3349,14 @@ def find_accessible_entrances(world, player, builder): return visited_entrances +def find_possible_entrances(world, player, builder): + entrances = [region.name for region in + (portal.door.entrance.parent_region for portal in world.dungeon_portals[player]) + if region.dungeon.name == builder.name] + entrances.extend(drop_entrances[builder.name]) + return entrances + + def valid_inaccessible_region(world, r, player): return r.type is not RegionType.Cave or (len(r.exits) > 0 and r.name not in ['Links House' if not world.is_bombshop_start(player) else 'Big Bomb Shop', 'Chris Houlihan Room']) @@ -1968,7 +3374,8 @@ def create_doors_for_inaccessible_region(inaccessible_region, world, player): for ext in region.exits: create_door(world, player, ext.name, region.name) if ext.connected_region is None: - logging.getLogger('').warning('Exit not connected to any region: %s', ext.name) + # TODO: Since Open/Inverted regions are merged into one world model, some exits are left disconnected intentionally + logging.getLogger('').debug('Exit not connected to any region: %s', ext.name) elif ext.connected_region.name.endswith(' Portal'): for more_exts in ext.connected_region.exits: create_door(world, player, more_exts.name, ext.connected_region.name) @@ -2057,11 +3464,22 @@ def explore_state(state, world, player): while len(state.avail_doors) > 0: door = state.next_avail_door().door connect_region = world.get_entrance(door.name, player).connected_region - if state.can_traverse(door) and not state.visited(connect_region) and valid_region_to_explore(connect_region, world, player): + if (state.can_traverse(door) and not state.visited(connect_region) + and valid_region_to_explore(connect_region, state.dungeon, world, player)): state.visit_region(connect_region) state.add_all_doors_check_unattached(connect_region, world, player) +def explore_state_proposed_traps(state, proposed_traps, world, player): + while len(state.avail_doors) > 0: + door = state.next_avail_door().door + connect_region = world.get_entrance(door.name, player).connected_region + if (not state.visited(connect_region) + and valid_region_to_explore(connect_region, state.dungeon, world, player)): + state.visit_region(connect_region) + state.add_all_doors_check_proposed_traps(connect_region, proposed_traps, world, player) + + def explore_state_not_inaccessible(state, world, player): while len(state.avail_doors) > 0: door = state.next_avail_door().door @@ -2119,6 +3537,7 @@ class DROptions(Flag): # Open_Desert_Wall = 0x80 # No longer pre-opening desert wall - unused Hide_Total = 0x100 DarkWorld_Spawns = 0x200 + BigKeyDoor_Shuffle = 0x400 # DATA GOES DOWN HERE @@ -2303,6 +3722,9 @@ logical_connections = [ ('Mire Hub Top Blue Barrier', 'Mire Hub Switch'), ('Mire Hub Switch Blue Barrier N', 'Mire Hub Top'), ('Mire Hub Switch Blue Barrier S', 'Mire Hub'), + ('Mire Falling Bridge Hook Path', 'Mire Falling Bridge - Chest'), + ('Mire Falling Bridge Hook Only Path', 'Mire Falling Bridge - Chest'), + ('Mire Falling Bridge Failure Path', 'Mire Falling Bridge - Failure'), ('Mire Map Spike Side Drop Down', 'Mire Lone Shooter'), ('Mire Map Spike Side Blue Barrier', 'Mire Crystal Dead End'), ('Mire Map Spot Blue Barrier', 'Mire Crystal Dead End'), @@ -2435,6 +3857,7 @@ vanilla_logical_connections = [ ('Ice Cross Right Push Block Bottom', 'Ice Compass Room'), ('Ice Cross Bottom Push Block Right', 'Ice Pengator Switch'), ('Ice Cross Top Push Block Right', 'Ice Pengator Switch'), + ('Mire Falling Bridge Primary Path', 'Mire Lone Shooter'), ] spiral_staircases = [ @@ -2598,6 +4021,7 @@ interior_doors = [ ('Hyrule Dungeon Armory Interior Key Door S', 'Hyrule Dungeon Armory Interior Key Door N'), ('Hyrule Dungeon Armory ES', 'Hyrule Dungeon Armory Boomerang WS'), ('Hyrule Dungeon Map Room Key Door S', 'Hyrule Dungeon North Abyss Key Door N'), + ('Sewers Dark Aquabats N', 'Sewers Key Rat S'), ('Sewers Rat Path WS', 'Sewers Secret Room ES'), ('Sewers Rat Path WN', 'Sewers Secret Room EN'), ('Sewers Yet More Rats S', 'Sewers Pull Switch N'), @@ -2768,7 +4192,7 @@ interior_doors = [ ] key_doors = [ - ('Sewers Key Rat Key Door N', 'Sewers Secret Room Key Door S'), + ('Sewers Key Rat NE', 'Sewers Secret Room Key Door S'), ('Sewers Dark Cross Key Door N', 'Sewers Water S'), ('Eastern Dark Square Key Door WN', 'Eastern Cannonball Ledge Key Door EN'), ('Eastern Darkness Up Stairs', 'Eastern Attic Start Down Stairs'), @@ -2788,7 +4212,7 @@ key_doors = [ default_small_key_doors = { 'Hyrule Castle': [ - ('Sewers Key Rat Key Door N', 'Sewers Secret Room Key Door S'), + ('Sewers Key Rat NE', 'Sewers Secret Room Key Door S'), ('Sewers Dark Cross Key Door N', 'Sewers Water S'), ('Hyrule Dungeon Map Room Key Door S', 'Hyrule Dungeon North Abyss Key Door N'), ('Hyrule Dungeon Armory Interior Key Door N', 'Hyrule Dungeon Armory Interior Key Door S') @@ -2888,8 +4312,8 @@ default_door_connections = [ ('Hyrule Castle Throne Room N', 'Sewers Behind Tapestry S'), ('Hyrule Dungeon Guardroom N', 'Hyrule Dungeon Armory S'), ('Sewers Dark Cross Key Door N', 'Sewers Water S'), - ('Sewers Water W', 'Sewers Key Rat E'), - ('Sewers Key Rat Key Door N', 'Sewers Secret Room Key Door S'), + ('Sewers Water W', 'Sewers Dark Aquabats ES'), + ('Sewers Key Rat NE', 'Sewers Secret Room Key Door S'), ('Eastern Lobby Bridge N', 'Eastern Cannonball S'), ('Eastern Cannonball N', 'Eastern Courtyard Ledge S'), ('Eastern Cannonball Ledge WN', 'Eastern Big Key EN'), @@ -2903,7 +4327,7 @@ default_door_connections = [ ('Eastern Map Valley SW', 'Eastern Dark Square NW'), ('Eastern Attic Start WS', 'Eastern False Switches ES'), ('Eastern Cannonball Hell WS', 'Eastern Single Eyegore ES'), - ('Desert Compass NW', 'Desert Cannonball S'), + ('Desert Compass NE', 'Desert Cannonball S'), ('Desert Beamos Hall NE', 'Desert Tiles 2 SE'), ('PoD Middle Cage N', 'PoD Pit Room S'), ('PoD Pit Room NW', 'PoD Arena Main SW'), @@ -3181,4 +4605,21 @@ bomb_dash_counts = { 'Ganons Tower': (2, 1) } +# small, big, trap, bomb, dash, hidden, tricky +door_type_counts = { + 'Hyrule Castle': (4, 0, 1, 0, 2, 0, 0), + 'Eastern Palace': (2, 2, 0, 0, 0, 0, 0), + 'Desert Palace': (4, 1, 0, 0, 0, 0, 0), + 'Agahnims Tower': (4, 0, 1, 0, 0, 1, 0), + 'Swamp Palace': (6, 0, 0, 2, 0, 0, 0), + 'Palace of Darkness': (6, 1, 1, 3, 2, 0, 0), + 'Misery Mire': (6, 3, 5, 2, 0, 0, 0), + 'Skull Woods': (5, 0, 1, 2, 0, 1, 0), + 'Ice Palace': (6, 1, 3, 0, 0, 0, 0), + 'Tower of Hera': (1, 1, 0, 0, 0, 0, 0), + 'Thieves Town': (3, 1, 2, 1, 1, 0, 0), + 'Turtle Rock': (6, 2, 2, 0, 2, 0, 1), # 2 bombs kind of for entrances, but I put 0 here + 'Ganons Tower': (8, 2, 5, 2, 1, 0, 0) +} + diff --git a/Doors.py b/Doors.py index a7c1d9f3..0bd13742 100644 --- a/Doors.py +++ b/Doors.py @@ -66,7 +66,7 @@ def create_doors(world, player): create_door(player, 'Hyrule Castle Back Hall Down Stairs', Sprl).dir(Dn, 0x01, 0, HTL).ss(A, 0x2a, 0x00), create_door(player, 'Hyrule Castle Throne Room Tapestry', Lgcl), create_door(player, 'Hyrule Castle Tapestry Backwards', Lgcl), - create_door(player, 'Hyrule Castle Throne Room N', Nrml).dir(No, 0x51, Mid, High).pos(1), + create_door(player, 'Hyrule Castle Throne Room N', Nrml).dir(No, 0x51, Mid, High).pos(0), create_door(player, 'Hyrule Castle Throne Room South Stairs', StrS).dir(So, 0x51, Mid, Low), # hyrule dungeon level @@ -104,8 +104,10 @@ def create_doors(world, player): create_door(player, 'Sewers Dark Cross Key Door N', Nrml).dir(No, 0x32, Mid, High).small_key().pos(0), create_door(player, 'Sewers Water S', Nrml).dir(So, 0x22, Mid, High).small_key().pos(0).portal(Z, 0x22), create_door(player, 'Sewers Water W', Nrml).dir(We, 0x22, Bot, High).pos(1), - create_door(player, 'Sewers Key Rat E', Nrml).dir(Ea, 0x21, Bot, High).pos(1), - create_door(player, 'Sewers Key Rat Key Door N', Nrml).dir(No, 0x21, Right, High).small_key().pos(0), + create_door(player, 'Sewers Dark Aquabats ES', Nrml).dir(Ea, 0x21, Bot, High).pos(2), + create_door(player, 'Sewers Dark Aquabats N', Intr).dir(No, 0x21, Mid, High).pos(1), + create_door(player, 'Sewers Key Rat S', Intr).dir(So, 0x21, Mid, High).pos(1), + create_door(player, 'Sewers Key Rat NE', Nrml).dir(No, 0x21, Right, High).small_key().pos(0), create_door(player, 'Sewers Secret Room Key Door S', Nrml).dir(So, 0x11, Right, High).small_key().pos(2).portal(X, 0x02), create_door(player, 'Sewers Rat Path WS', Intr).dir(We, 0x11, Bot, High).pos(1), create_door(player, 'Sewers Rat Path WN', Intr).dir(We, 0x11, Top, High).pos(0), @@ -205,7 +207,7 @@ def create_doors(world, player): create_door(player, 'Desert East Wing ES', Intr).dir(Ea, 0x85, Bot, High).pos(3), create_door(player, 'Desert East Wing Key Door EN', Intr).dir(Ea, 0x85, Top, High).small_key().pos(1), create_door(player, 'Desert Compass Key Door WN', Intr).dir(We, 0x85, Top, High).small_key().pos(1), - create_door(player, 'Desert Compass NW', Nrml).dir(No, 0x85, Right, High).trap(0x4).pos(0), + create_door(player, 'Desert Compass NE', Nrml).dir(No, 0x85, Right, High).trap(0x4).pos(0), create_door(player, 'Desert Cannonball S', Nrml).dir(So, 0x75, Right, High).pos(1).portal(X, 0x02), create_door(player, 'Desert Arrow Pot Corner S Edge', Open).dir(So, 0x75, None, High).edge(6, Z, 0x20), create_door(player, 'Desert Arrow Pot Corner W Edge', Open).dir(We, 0x75, None, High).edge(2, Z, 0x20), @@ -348,7 +350,7 @@ def create_doors(world, player): create_door(player, 'Tower Catwalk North Stairs', StrS).dir(No, 0x40, Left, High), create_door(player, 'Tower Antechamber South Stairs', StrS).dir(So, 0x30, Left, High), create_door(player, 'Tower Antechamber NW', Intr).dir(No, 0x30, Left, High).pos(1), - create_door(player, 'Tower Altar SW', Intr).dir(So, 0x30, Left, High).no_exit().pos(1), + create_door(player, 'Tower Altar SW', Intr).dir(So, 0x30, Left, High).no_exit().trap(0x2).pos(1), create_door(player, 'Tower Altar NW', Nrml).dir(No, 0x30, Left, High).pos(0), create_door(player, 'Tower Agahnim 1 SW', Nrml).dir(So, 0x20, Left, High).no_exit().trap(0x4).pos(0), @@ -667,7 +669,7 @@ def create_doors(world, player): create_door(player, 'Thieves Conveyor Maze SW', Intr).dir(So, 0xbc, Left, High).pos(6), create_door(player, 'Thieves Pot Alcove Top NW', Intr).dir(No, 0xbc, Left, High).pos(6), create_door(player, 'Thieves Conveyor Maze EN', Intr).dir(Ea, 0xbc, Top, High).pos(2), - create_door(player, 'Thieves Hallway WN', Intr).dir(We, 0xbc, Top, High).no_exit().pos(2), + create_door(player, 'Thieves Hallway WN', Intr).dir(We, 0xbc, Top, High).no_exit().trap(0x1).pos(2), create_door(player, 'Thieves Conveyor Maze Down Stairs', Sprl).dir(Dn, 0xbc, 0, HTH).ss(A, 0x11, 0x80, True, True), create_door(player, 'Thieves Boss SE', Nrml).dir(So, 0xac, Right, High).no_exit().trap(0x4).pos(0), create_door(player, 'Thieves Spike Track ES', Nrml).dir(Ea, 0xbb, Bot, High).pos(5), @@ -742,7 +744,7 @@ def create_doors(world, player): create_door(player, 'Ice Big Key Push Block', Lgcl), create_door(player, 'Ice Big Key Down Ladder', Lddr).dir(So, 0x1f, 3, High), create_door(player, 'Ice Stalfos Hint SE', Intr).dir(So, 0x3e, Right, High).pos(0), - create_door(player, 'Ice Conveyor NE', Intr).dir(No, 0x3e, Right, High).no_exit().pos(0), + create_door(player, 'Ice Conveyor NE', Intr).dir(No, 0x3e, Right, High).no_exit().trap(0x4).pos(0), create_door(player, 'Ice Conveyor to Crystal', Lgcl), create_door(player, 'Ice Conveyor Crystal Exit', Lgcl), create_door(player, 'Ice Conveyor SW', Nrml).dir(So, 0x3e, Left, High).small_key().pos(1).portal(Z, 0x20), @@ -760,7 +762,7 @@ def create_doors(world, player): create_door(player, 'Ice Firebar ES', Intr).dir(Ea, 0x5e, Bot, High).pos(3), create_door(player, 'Ice Firebar Down Ladder', Lddr).dir(So, 0x5e, 5, High), create_door(player, 'Ice Spike Cross NE', Intr).dir(No, 0x5e, Right, High).pos(1), - create_door(player, 'Ice Falling Square SE', Intr).dir(So, 0x5e, Right, High).no_exit().pos(1), + create_door(player, 'Ice Falling Square SE', Intr).dir(So, 0x5e, Right, High).no_exit().trap(0x1).pos(1), create_door(player, 'Ice Falling Square Hole', Hole), 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), @@ -773,7 +775,7 @@ def create_doors(world, player): create_door(player, 'Ice Freezors Hole', Hole), create_door(player, 'Ice Freezors Bomb Hole', Hole), # combine these two? -- they have to lead to the same spot create_door(player, 'Ice Freezors Ledge Hole', Hole), - create_door(player, 'Ice Freezors Ledge ES', Intr).dir(Ea, 0x7e, Bot, High).pos(2), + create_door(player, 'Ice Freezors Ledge ES', Intr).dir(Ea, 0x7e, Bot, High).pos(1), 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(2), create_door(player, 'Ice Tall Hint SE', Nrml).dir(So, 0x7e, Right, High).small_key().pos(0).portal(X, 0x02), @@ -840,12 +842,16 @@ def create_doors(world, player): create_door(player, 'Mire Hub Top NW', Nrml).dir(No, 0xc2, Left, High).pos(2), create_door(player, 'Mire Lone Shooter WS', Nrml).dir(We, 0xc3, Bot, High).pos(6), create_door(player, 'Mire Lone Shooter ES', Intr).dir(Ea, 0xc3, Bot, High).pos(3), - create_door(player, 'Mire Falling Bridge WS', Intr).dir(We, 0xc3, Bot, High).no_exit().pos(3), + create_door(player, 'Mire Falling Bridge WS', Intr).dir(We, 0xc3, Bot, High).no_exit().trap(0x8).pos(3), create_door(player, 'Mire Falling Bridge W', Intr).dir(We, 0xc3, Mid, High).pos(2), - create_door(player, 'Mire Failure Bridge E', Intr).dir(Ea, 0xc3, Mid, High).no_exit().pos(2), + create_door(player, 'Mire Failure Bridge E', Intr).dir(Ea, 0xc3, Mid, High).no_exit().trap(0x1).pos(2), create_door(player, 'Mire Failure Bridge W', Nrml).dir(We, 0xc3, Mid, High).pos(5), create_door(player, 'Mire Falling Bridge WN', Intr).dir(We, 0xc3, Top, High).pos(1), - create_door(player, 'Mire Map Spike Side EN', Intr).dir(Ea, 0xc3, Top, High).no_exit().pos(1), + create_door(player, 'Mire Falling Bridge Hook Path', Lgcl), + create_door(player, 'Mire Falling Bridge Hook Only Path', Lgcl), + create_door(player, 'Mire Falling Bridge Primary Path', Lgcl), # dynamic + create_door(player, 'Mire Falling Bridge Failure Path', Lgcl), + create_door(player, 'Mire Map Spike Side EN', Intr).dir(Ea, 0xc3, Top, High).no_exit().trap(0x2).pos(1), create_door(player, 'Mire Map Spot WN', Nrml).dir(We, 0xc3, Top, High).small_key().pos(0), create_door(player, 'Mire Crystal Dead End NW', Nrml).dir(No, 0xc3, Left, High).pos(4), create_door(player, 'Mire Map Spike Side Drop Down', Lgcl), @@ -903,7 +909,7 @@ def create_doors(world, player): create_door(player, 'Mire Tile Room NW', Intr).dir(No, 0xc1, Left, High).pos(3), create_door(player, 'Mire Compass Room SW', Intr).dir(So, 0xc1, Left, High).pos(3), create_door(player, 'Mire Compass Room EN', Intr).dir(Ea, 0xc1, Top, High).pos(2), - create_door(player, 'Mire Wizzrobe Bypass WN', Intr).dir(We, 0xc1, Top, High).no_exit().pos(2), + create_door(player, 'Mire Wizzrobe Bypass WN', Intr).dir(We, 0xc1, Top, High).no_exit().trap(0x1).pos(2), create_door(player, 'Mire Compass Blue Barrier', Lgcl), create_door(player, 'Mire Compass Chest Exit', Lgcl), create_door(player, 'Mire Neglected Room NE', Nrml).dir(No, 0xd1, Right, High).pos(2), @@ -912,7 +918,7 @@ def create_doors(world, player): create_door(player, 'Mire Neglected Room SE', Intr).dir(So, 0xd1, Right, High).pos(3), create_door(player, 'Mire Chest View NE', Intr).dir(No, 0xd1, Right, High).pos(3), 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 ES', Intr).dir(Ea, 0xd1, Bot, High).no_exit().trap(0x4).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).kill(), create_door(player, 'Mire Torches Top SW', Intr).dir(So, 0x97, Left, High).pos(1), @@ -1011,7 +1017,7 @@ def create_doors(world, player): create_door(player, 'TR Big Chest Entrance SE', Nrml).dir(So, 0x24, Right, High).pos(4).kill().portal(X, 0x00), create_door(player, 'TR Big Chest Entrance Gap', Lgcl), create_door(player, 'TR Big Chest NE', Intr).dir(No, 0x24, Right, High).pos(3), - create_door(player, 'TR Dodgers SE', Intr).dir(So, 0x24, Right, High).no_exit().pos(3), + create_door(player, 'TR Dodgers SE', Intr).dir(So, 0x24, Right, High).no_exit().trap(0x8).pos(3), create_door(player, 'TR Dodgers NE', Nrml).dir(No, 0x24, Right, High).big_key().pos(0), create_door(player, 'TR Lazy Eyes SE', Nrml).dir(So, 0x23, Right, High).pos(0).portal(X, 0x00), create_door(player, 'TR Lazy Eyes ES', Nrml).dir(Ea, 0x23, Bot, High).pos(1), @@ -1073,7 +1079,7 @@ def create_doors(world, player): create_door(player, 'GT Hope Room EN', Nrml).dir(Ea, 0x8c, Top, High).trap(0x4).pos(0), create_door(player, 'GT Torch EN', Intr).dir(Ea, 0x8c, Top, High).small_key().pos(2), 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 Torch SW', Intr).dir(So, 0x8c, Left, High).no_exit().trap(0x2).pos(1), create_door(player, 'GT Big Chest NW', Intr).dir(No, 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).kill(), create_door(player, 'GT Blocked Stairs Block Path', Lgcl), @@ -1179,7 +1185,7 @@ def create_doors(world, player): create_door(player, 'GT Ice Armos NE', Intr).dir(No, 0x1c, Right, High).pos(0), create_door(player, 'GT Big Key Room SE', Intr).dir(So, 0x1c, Right, High).pos(0), create_door(player, 'GT Ice Armos WS', Intr).dir(We, 0x1c, Bot, High).pos(1), - create_door(player, 'GT Four Torches ES', Intr).dir(Ea, 0x1c, Bot, High).no_exit().pos(1), + create_door(player, 'GT Four Torches ES', Intr).dir(Ea, 0x1c, Bot, High).no_exit().trap(0x2).pos(1), create_door(player, 'GT Four Torches NW', Intr).dir(No, 0x1c, Left, High).pos(2), create_door(player, 'GT Fairy Abyss SW', Intr).dir(So, 0x1c, Left, High).pos(2), create_door(player, 'GT Four Torches Up Stairs', Sprl).dir(Up, 0x1c, 0, HTH).ss(Z, 0x1b, 0x2c, True, True), @@ -1211,7 +1217,7 @@ def create_doors(world, player): 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), - create_door(player, 'GT Quad Pot SW', Intr).dir(So, 0x6c, Left, High).no_exit().pos(1), + create_door(player, 'GT Quad Pot SW', Intr).dir(So, 0x6c, Left, High).no_exit().trap(0x2).pos(1), create_door(player, 'GT Quad Pot Up Stairs', Sprl).dir(Up, 0x6c, 0, HTH).ss(A, 0x1b, 0x6c, True, True), create_door(player, 'GT Wizzrobes 1 Down Stairs', Sprl).dir(Dn, 0xa5, 0, HTH).ss(A, 0x12, 0x80, True, True), create_door(player, 'GT Wizzrobes 1 SW', Intr).dir(So, 0xa5, Left, High).pos(2), @@ -1467,6 +1473,8 @@ def create_doors(world, player): world.get_door('GT Spike Crystal Right to Left Barrier - Orange', player).barrier(CrystalBarrier.Orange) world.get_door('GT Spike Crystal Left to Right Bypass', player).barrier(CrystalBarrier.Blue) + world.get_door('Sanctuary Mirror Route', player).barrier(CrystalBarrier.Orange) + # kill certain doors if world.intensity[player] == 1: # due to ladder & warp being fixed world.get_door('PoD Mimics 2 SW', player).kill() @@ -1480,6 +1488,8 @@ def create_doors(world, player): controller_door(south_controller, world.get_door('Ice Cross Top Push Block Bottom', player)) controller_door(east_controller, world.get_door('Ice Cross Bottom Push Block Right', player)) controller_door(east_controller, world.get_door('Ice Cross Top Push Block Right', player)) + primary_controller = world.get_door('Mire Falling Bridge WS', player) + controller_door(primary_controller, world.get_door('Mire Falling Bridge Primary Path', player)) assign_entrances(world, player) @@ -1511,6 +1521,7 @@ def create_doors(world, player): world.get_door("GT Bob\'s Room SE", player).passage = False world.get_door('Desert Tiles 2 SE', player).bk_shuffle_req = True # key-drop note: allows this to be a portal world.get_door('Swamp Lobby S', player).standard_restricted = True + world.get_door('Sanctuary S', player).standard_restricted = True world.get_door('PoD Mimics 2 SW', player).rupee_bow_restricted = True # bow statue # enemizer logic could get rid of the following restriction world.get_door('PoD Pit Room S', player).rupee_bow_restricted = True # so mimics 1 shouldn't be required @@ -1572,7 +1583,7 @@ def reset_portals(world, player): def create_paired_doors(world, player): world.paired_doors[player] = [ - PairedDoor('Sewers Secret Room Key Door S', 'Sewers Key Rat Key Door N', True), + PairedDoor('Sewers Secret Room Key Door S', 'Sewers Key Rat NE', True), PairedDoor('TR Pokey 2 ES', 'TR Lava Island WS', True), # TR Pokey Key PairedDoor('TR Dodgers NE', 'TR Lava Escape SE', True), # TR Big key door by pipes PairedDoor('PoD Falling Bridge WN', 'PoD Dark Maze EN', True), # Pod Dark maze door diff --git a/DungeonGenerator.py b/DungeonGenerator.py index f0038f45..f97f8ecf 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -10,11 +10,14 @@ import time from typing import List from BaseClasses import DoorType, Direction, CrystalBarrier, RegionType, Polarity, PolSlot, flooded_keys, Sector -from BaseClasses import Hook, hook_from_door +from BaseClasses import Hook, hook_from_door, Door from Regions import dungeon_events, flooded_keys_reverse -from Dungeons import dungeon_regions, split_region_starts +from Dungeons import split_region_starts from RoomData import DoorKind +from source.dungeon.DungeonStitcher import generate_dungeon_find_proposal +from source.dungeon.DungeonStitcher import GenerationException as OtherGenException + class GraphPiece: @@ -43,7 +46,10 @@ def pre_validate(builder, entrance_region_names, split_dungeon, world, player): for sector in builder.sectors: for door in sector.outstanding_doors: doors_to_connect[door.name] = door - all_regions.update(sector.regions) + if world.mode[player] == 'standard' and builder.name == 'Hyrule Castle Dungeon': + all_regions.update([x for x in sector.regions if x.name != 'Hyrule Castle Behind Tapestry']) + else: + all_regions.update(sector.regions) bk_special |= check_for_special(sector.regions) bk_needed = False for sector in builder.sectors: @@ -59,7 +65,7 @@ def generate_dungeon(builder, entrance_region_names, split_dungeon, world, playe if builder.valid_proposal: # we made this earlier in gen, just use it proposed_map = builder.valid_proposal else: - proposed_map = generate_dungeon_find_proposal(builder, entrance_region_names, split_dungeon, world, player) + proposed_map = generate_dungeon_find_proposal_old(builder, entrance_region_names, split_dungeon, world, player) builder.valid_proposal = proposed_map queue = collections.deque(proposed_map.items()) while len(queue) > 0: @@ -77,7 +83,7 @@ def generate_dungeon(builder, entrance_region_names, split_dungeon, world, playe return master_sector -def generate_dungeon_find_proposal(builder, entrance_region_names, split_dungeon, world, player): +def generate_dungeon_find_proposal_old(builder, entrance_region_names, split_dungeon, world, player): logger = logging.getLogger('') name = builder.name entrance_regions = convert_regions(entrance_region_names, world, player) @@ -91,18 +97,27 @@ def generate_dungeon_find_proposal(builder, entrance_region_names, split_dungeon p_region = portal.door.entrance.connected_region access_region = next(x.parent_region for x in p_region.entrances if x.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld]) + if ((access_region.name in world.inaccessible_regions[player] and + region.name not in world.enabled_entrances[player]) + or (world.mode[player] == 'standard' and access_region.name != 'Hyrule Castle Courtyard' + and 'Hyrule Castle' in builder.name)): + excluded[region] = None + else: # for non-portals, holes and sewers in std + access_region = next((x.parent_region for x in region.entrances + if x.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld] + or x.parent_region.name == 'Sewer Drop'), None) + if access_region is None: + if builder.sewers_access is None: + excluded[region] = None + else: + if access_region.name == 'Sewer Drop': + if world.mode[player] == 'standard' and (builder.sewers_access is None + or builder.sewers_access.entrance.parent_region != region): + excluded[region] = None + access_region = next(x.parent_region for x in access_region.entrances) if (access_region.name in world.inaccessible_regions[player] and region.name not in world.enabled_entrances[player]): excluded[region] = None - elif len(region.entrances) == 1: # for holes - access_region = next(x.parent_region for x in region.entrances - if x.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld] - or x.parent_region.name == 'Sewer Drop') - if access_region.name == 'Sewer Drop': - access_region = next(x.parent_region for x in access_region.entrances) - if (access_region.name in world.inaccessible_regions[player] and - region.name not in world.enabled_entrances[player]): - excluded[region] = None entrance_regions = [x for x in entrance_regions if x not in excluded.keys()] doors_to_connect = {} all_regions = set() @@ -143,7 +158,11 @@ def generate_dungeon_find_proposal(builder, entrance_region_names, split_dungeon dungeon, hangers, hooks = gen_dungeon_info(name, builder.sectors, entrance_regions, all_regions, proposed_map, doors_to_connect, bk_needed, bk_special, world, player) dungeon_cache[depth] = dungeon, hangers, hooks - valid = check_valid(name, dungeon, hangers, hooks, proposed_map, doors_to_connect, all_regions, + if len(proposed_map) != len(doors_to_connect) and builder.name == 'Hyrule Castle Dungeon': + check_regions = all_regions.difference({world.get_region('Hyrule Castle Behind Tapestry', player)}) + else: + check_regions = all_regions + valid = check_valid(name, dungeon, hangers, hooks, proposed_map, doors_to_connect, check_regions, bk_needed, bk_special, paths, entrance_regions, world, player) else: dungeon, hangers, hooks = dungeon_cache[depth] @@ -565,9 +584,15 @@ def determine_paths_for_dungeon(world, player, all_regions, name): non_hole_portals.append(portal.door.entrance.parent_region.name) if portal.destination: paths.append(portal.door.entrance.parent_region.name) - if world.mode[player] == 'standard' and name == 'Hyrule Castle': - paths.append('Hyrule Dungeon Cellblock') - paths.append(('Hyrule Dungeon Cellblock', 'Sanctuary')) + if world.mode[player] == 'standard': + if name == 'Hyrule Castle': + paths.append('Hyrule Dungeon Cellblock') + paths.append(('Hyrule Dungeon Cellblock', 'Sanctuary')) + if name == 'Hyrule Castle Sewers': + paths.append('Sanctuary') + if name == 'Hyrule Castle Dungeon': + paths.append('Hyrule Dungeon Cellblock') + paths.append(('Hyrule Dungeon Cellblock', 'Hyrule Castle Throne Room')) if world.doorShuffle[player] in ['basic'] and name == 'Thieves Town': paths.append('Thieves Attic Window') elif 'Thieves Attic Window' in all_r_names: @@ -747,6 +772,13 @@ def connect_simple_door(exit_door, region): special_big_key_doors = ['Hyrule Dungeon Cellblock Door', "Thieves Blind's Cell Door"] +std_special_big_key_doors = ['Hyrule Castle Throne Room Tapestry'] + special_big_key_doors + + +def get_special_big_key_doors(world, player): + if world.mode[player] == 'standard': + return std_special_big_key_doors + return special_big_key_doors class ExplorationState(object): @@ -786,6 +818,7 @@ class ExplorationState(object): self.prize_door_set = {} self.prize_doors = [] self.prize_doors_opened = False + self.prize_received = False def copy(self): ret = ExplorationState(dungeon=self.dungeon) @@ -817,15 +850,24 @@ class ExplorationState(object): ret.prize_door_set = dict(self.prize_door_set) ret.prize_doors = list(self.prize_doors) ret.prize_doors_opened = self.prize_doors_opened + ret.prize_received = self.prize_received return ret + def init_zelda_event_doors(self, event_starts, player): + for entrance in event_starts: + event_door = Door(player, entrance.name, DoorType.Logical) + event_door.req_event = 'Zelda Drop Off' + event_door.entrance = entrance + event_door.crystal = CrystalBarrier.Orange # always start in orange + self.append_door_to_list(event_door, self.event_doors) + def next_avail_door(self): self.avail_doors.sort(key=lambda x: 0 if x.flag else 1 if x.door.bigKey else 2) exp_door = self.avail_doors.pop() 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_flag=False): if region.type != RegionType.Dungeon: self.crystal = CrystalBarrier.Orange if self.crystal == CrystalBarrier.Either: @@ -844,9 +886,9 @@ class ExplorationState(object): self.key_locations += 1 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: # todo: special logic for TT Boss? + if location not in self.found_locations: self.found_locations.append(location) - if not bk_Flag: + if not bk_flag and (not location.forced_item or 'Big Key' in location.item.name): self.bk_found.add(location) if location.name in dungeon_events and location.name not in self.events: if self.flooded_key_check(location): @@ -962,6 +1004,21 @@ class ExplorationState(object): elif not self.in_door_list(door, self.avail_doors): self.append_door_to_list(door, self.avail_doors) + def add_all_doors_check_big_keys(self, region, big_key_door_proposal, world, player): + for door in get_doors(world, region, player): + if self.can_traverse(door): + if door.controller: + door = door.controller + if (door in big_key_door_proposal + 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 door.req_event is not None and door.req_event not in self.events: + if 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) + def visited(self, region): if self.crystal == CrystalBarrier.Either: return region in self.visited_blue and region in self.visited_orange @@ -1161,6 +1218,7 @@ class DungeonBuilder(object): self.name = name self.sectors = [] self.location_cnt = 0 + self.location_set = set() self.key_drop_cnt = 0 self.dungeon_items = None # during fill how many dungeon items are left self.free_items = None # during fill how many dungeon items are left @@ -1191,6 +1249,8 @@ class DungeonBuilder(object): self.combo_size = None self.flex = 0 self.key_door_proposal = None + self.bk_door_proposal = None + self.trap_door_proposal = None self.allowance = None if 'Stonewall' in name: @@ -1206,6 +1266,11 @@ class DungeonBuilder(object): self.split_dungeon_map = None self.exception_list = [] + self.throne_door = None + self.throne_sector = None + self.chosen_lobby = None + self.sewers_access = None + def polarity_complement(self): pol = Polarity() for sector in self.sectors: @@ -1235,7 +1300,7 @@ def simple_dungeon_builder(name, sector_list): return builder -def create_dungeon_builders(all_sectors, connections_tuple, world, player, +def create_dungeon_builders(all_sectors, connections_tuple, world, player, dungeon_pool, dungeon_entrances=None, split_dungeon_entrances=None): logger = logging.getLogger('') logger.info('Shuffling Dungeon Sectors') @@ -1251,14 +1316,12 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player, global_pole = GlobalPolarity(candidate_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 key in dungeon_pool: + current_dungeon = dungeon_map[key] = DungeonBuilder(key) for r_name in dungeon_boss_sectors[key]: assign_sector(find_sector(r_name, candidate_sectors), current_dungeon, candidate_sectors, global_pole) if key == 'Hyrule Castle' and world.mode[player] == 'standard': - for r_name in ['Hyrule Dungeon Cellblock', 'Sanctuary']: # need to deliver zelda + for r_name in ['Hyrule Dungeon Cellblock', 'Sanctuary', 'Hyrule Castle Throne Room']: # need to deliver zelda assign_sector(find_sector(r_name, candidate_sectors), current_dungeon, candidate_sectors, global_pole) if key == 'Thieves Town' and world.get_dungeon("Thieves Town", player).boss.enemizer_name == 'Blind': @@ -1266,7 +1329,7 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player, candidate_sectors, global_pole) entrances_map, potentials, connections = connections_tuple accessible_sectors, reverse_d_map = set(), {} - for key in dungeon_entrances.keys(): + for key in dungeon_pool: current_dungeon = dungeon_map[key] current_dungeon.all_entrances = dungeon_entrances[key] for r_name in current_dungeon.all_entrances: @@ -1280,12 +1343,17 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player, sector = find_sector(r_name, all_sectors) reverse_d_map[sector] = key if world.mode[player] == 'standard': - current_dungeon = dungeon_map['Hyrule Castle'] - standard_stair_check(dungeon_map, current_dungeon, candidate_sectors, global_pole) + if 'Hyrule Castle' in dungeon_map: + current_dungeon = dungeon_map['Hyrule Castle'] + standard_stair_check(dungeon_map, current_dungeon, candidate_sectors, global_pole) complete_dungeons = {x: y for x, y in dungeon_map.items() if sum(len(sector.outstanding_doors) for sector in y.sectors) <= 0} [dungeon_map.pop(key) for key in complete_dungeons.keys()] + if not dungeon_map: + dungeon_map.update(complete_dungeons) + return dungeon_map + # categorize sectors identify_destination_sectors(accessible_sectors, reverse_d_map, dungeon_map, connections, dungeon_entrances, split_dungeon_entrances) @@ -1296,8 +1364,8 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player, sanc = find_sector('Sanctuary', candidate_sectors) if sanc: # only run if sanc if a candidate lw_builders = [] - for name, portal_list in dungeon_portals.items(): - for portal_name in portal_list: + for name in dungeon_pool: + for portal_name in dungeon_portals[name]: if world.get_portal(portal_name, player).light_world: lw_builders.append(dungeon_map[name]) break @@ -1305,7 +1373,8 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player, sanc_builder = random.choice(lw_builders) assign_sector(sanc, sanc_builder, candidate_sectors, global_pole) - bow_sectors, retro_std_flag = {}, world.retro[player] and world.mode[player] == 'standard' + retro_std_flag = world.bow_mode[player].startswith('retro') and world.mode[player] == 'standard' + non_hc_sectors = {} free_location_sectors = {} crystal_switches = {} crystal_barriers = {} @@ -1313,7 +1382,9 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player, neutral_sectors = {} for sector in candidate_sectors: if retro_std_flag and 'Bow' in sector.item_logic: # these need to be distributed outside of HC - bow_sectors[sector] = None + non_hc_sectors[sector] = None + elif world.mode[player] == 'standard' and 'Open Floodgate' in sector.item_logic: + non_hc_sectors[sector] = None elif sector.chest_locations > 0: free_location_sectors[sector] = None elif sector.c_switch: @@ -1324,8 +1395,8 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player, neutral_sectors[sector] = None else: polarized_sectors[sector] = None - if bow_sectors: - assign_bow_sectors(dungeon_map, bow_sectors, global_pole) + if non_hc_sectors: + assign_non_hc_sectors(dungeon_map, non_hc_sectors, global_pole) leftover = assign_location_sectors_minimal(dungeon_map, free_location_sectors, global_pole, world, player) free_location_sectors = scatter_extra_location_sectors(dungeon_map, leftover, global_pole) for sector in free_location_sectors: @@ -1369,7 +1440,8 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player, def standard_stair_check(dungeon_map, dungeon, candidate_sectors, global_pole): # this is because there must be at least one non-dead stairway in hc to get out # this check may not be necessary - filtered_sectors = [x for x in candidate_sectors if any(y for y in x.outstanding_doors if not y.dead and y.type == DoorType.SpiralStairs)] + filtered_sectors = [x for x in candidate_sectors if 'Open Floodgate' not in x.item_logic and + any(y for y in x.outstanding_doors if not y.dead and y.type == DoorType.SpiralStairs)] valid = False while not valid: chosen_sector = random.choice(filtered_sectors) @@ -1388,6 +1460,8 @@ def identify_destination_sectors(accessible_sectors, reverse_d_map, dungeon_map, if ent_name in found_connections: continue sector = find_sector(ent_name, reverse_d_map.keys()) + if sector is None: + continue if sector in accessible_sectors: found_connections.add(ent_name) accessible_overworld.add(region) # todo: drops don't give ow access @@ -1398,6 +1472,8 @@ def identify_destination_sectors(accessible_sectors, reverse_d_map, dungeon_map, explored = False else: d_name = reverse_d_map[sector] + if d_name not in dungeon_map: + return if d_name not in split_dungeon_entrances: for r_name in dungeon_entrances[d_name]: ent_sector = find_sector(r_name, dungeon_map[d_name].sectors) @@ -1451,6 +1527,8 @@ def calc_allowance_and_dead_ends(builder, connections_tuple, world, player): builder.branches -= 1 if entrance not in drop_entrances_allowance: needed_connections.append(entrance) + if builder.sewers_access: + starting_allowance += 1 builder.allowance = starting_allowance for entrance in needed_connections: sector = find_sector(entrance, builder.sectors) @@ -1493,7 +1571,7 @@ def define_sector_features(sectors): sector.bk_required = True for ext in region.exits: door = ext.door - if door is not None: + if door is not None and not door.blocked: if door.crystal == CrystalBarrier.Either: sector.c_switch = True elif door.crystal == CrystalBarrier.Orange: @@ -1505,6 +1583,8 @@ def define_sector_features(sectors): if region.name in ['PoD Mimics 2', 'PoD Bow Statue Right', 'PoD Mimics 1', 'GT Mimics 1', 'GT Mimics 2', 'Eastern Single Eyegore', 'Eastern Duo Eyegores']: sector.item_logic.add('Bow') + if region.name in ['Swamp Lobby', 'Swamp Entrance']: + sector.item_logic.add('Open Floodgate') def assign_sector(sector, dungeon, candidate_sectors, global_pole): @@ -1518,6 +1598,7 @@ def assign_sector_helper(sector, builder): builder.sectors.append(sector) builder.location_cnt += sector.chest_locations builder.key_drop_cnt += sector.key_only_locations + builder.location_set.update(sector.chest_location_set) if sector.c_switch: builder.c_switch_present = True if sector.blue_barrier: @@ -1555,8 +1636,8 @@ def find_sector(r_name, sectors): return None -def assign_bow_sectors(dungeon_map, bow_sectors, global_pole): - sector_list = list(bow_sectors) +def assign_non_hc_sectors(dungeon_map, non_hc_sectors, global_pole): + sector_list = list(non_hc_sectors) random.shuffle(sector_list) population = [] for name in dungeon_map: @@ -1565,7 +1646,7 @@ def assign_bow_sectors(dungeon_map, bow_sectors, global_pole): choices = random.choices(population, k=len(sector_list)) for i, choice in enumerate(choices): builder = dungeon_map[choice] - assign_sector(sector_list[i], builder, bow_sectors, global_pole) + assign_sector(sector_list[i], builder, non_hc_sectors, global_pole) def scatter_extra_location_sectors(dungeon_map, free_location_sectors, global_pole): @@ -1743,6 +1824,7 @@ def ensure_crystal_switches_reachable(dungeon_map, crystal_switches, polarized_s for name, builder in dungeon_map.items(): if builder.c_switch_present and builder.c_switch_required and not builder.c_locked: invalid_builders.append(builder) + random.shuffle(invalid_builders) while len(invalid_builders) > 0: valid_builders = [] for builder in invalid_builders: @@ -1768,6 +1850,7 @@ def ensure_crystal_switches_reachable(dungeon_map, crystal_switches, polarized_s if eq.c_switch: reachable_crystals[hook_from_door(eq.door)] = True valid_ent_sectors = [] + random.shuffle(entrance_sectors) for entrance_sector in entrance_sectors: other_sectors = [x for x in builder.sectors if x != entrance_sector] reachable, access = is_c_switch_reachable(entrance_sector, reachable_crystals, other_sectors) @@ -1785,7 +1868,12 @@ def ensure_crystal_switches_reachable(dungeon_map, crystal_switches, polarized_s while not valid: if len(candidates) <= 0: raise GenerationException(f'need to provide more sophisticated crystal connection for {entrance_sector}') - sector, which_list = random.choice(list(candidates.items())) + # prioritize candidates + if any(x == 'Crystals' for x in candidates.values()): + cand_list = [x for x in candidates.items() if x[1] == 'Crystals'] + else: + cand_list = list(candidates.items()) + sector, which_list = random.choice(cand_list) del candidates[sector] valid = global_pole.is_valid_choice(dungeon_map, builder, [sector]) if which_list == 'Polarized': @@ -2077,16 +2165,18 @@ def assign_polarized_sectors(dungeon_map, polarized_sectors, global_pole, builde while len(problem_builders) > 0: for name, builder in problem_builders.items(): candidates = find_branching_candidates(builder, neutral_choices, builder_info) - valid, choice = False, None + valid, choice, package = False, None, None while not valid: if len(candidates) <= 0: raise GenerationException('Cross Dungeon Builder: Complex branch problems: %s' % name) - choice = random.choice(candidates) - candidates.remove(choice) + choice, package = random.choice(candidates) + candidates.remove((choice, package)) valid = global_pole.is_valid_choice(dungeon_map, builder, choice) and valid_polarized_assignment(builder, choice) neutral_choices.remove(choice) for sector in choice: assign_sector(sector, builder, polarized_sectors, global_pole) + if package: + builder.throne_door, builder.throne_sector, builder.chosen_lobby = package builder.unfulfilled.clear() problem_builders = identify_branching_issues(problem_builders, builder_info) @@ -2107,16 +2197,21 @@ def assign_polarized_sectors(dungeon_map, polarized_sectors, global_pole, builde chosen_sectors = defaultdict(list) for i, choice in enumerate(choices): chosen_sectors[choice].extend(neutral_choices[i]) - all_valid = True + all_valid, package_map = True, {} for name, sector_list in chosen_sectors.items(): - if not valid_assignment(dungeon_map[name], sector_list, builder_info): + flag, package = valid_assignment(dungeon_map[name], sector_list, builder_info) + if not flag: all_valid = False break + if package: + package_map[dungeon_map[name]] = package if all_valid: for i, choice in enumerate(choices): builder = dungeon_map[choice] for sector in neutral_choices[i]: assign_sector(sector, builder, polarized_sectors, global_pole) + if builder in package_map: + builder.throne_door, builder.throne_sector, builder.chosen_lobby = package_map[builder] tries += 1 @@ -2629,9 +2724,9 @@ def weed_candidates(builder, candidates, best_charge): def find_branching_candidates(builder, neutral_choices, builder_info): candidates = [] for choice in neutral_choices: - resolved, problem_list = check_for_valid_layout(builder, choice, builder_info) + resolved, problem_list, package = check_for_valid_layout(builder, choice, builder_info) if resolved: - candidates.append(choice) + candidates.append((choice, package)) return candidates @@ -2786,13 +2881,13 @@ def categorize_groupings(sectors): def valid_assignment(builder, sector_list, builder_info): if not valid_entrance(builder, sector_list, builder_info): - return False + return False, None if not valid_c_switch(builder, sector_list): - return False + return False, None if not valid_polarized_assignment(builder, sector_list): - return False - resolved, problems = check_for_valid_layout(builder, sector_list, builder_info) - return resolved + return False, None + resolved, problems, package = check_for_valid_layout(builder, sector_list, builder_info) + return resolved, package def valid_entrance(builder, sector_list, builder_info): @@ -2898,31 +2993,56 @@ def assign_the_rest(dungeon_map, neutral_sectors, global_pole, builder_info): chosen_sectors = defaultdict(list) for i, choice in enumerate(choices): chosen_sectors[choice].append(neutral_sector_list[i]) - all_valid = True + all_valid, package_map = True, {} for name, sector_list in chosen_sectors.items(): - if not valid_assignment(dungeon_map[name], sector_list, builder_info): + flag, package = valid_assignment(dungeon_map[name], sector_list, builder_info) + if not flag: all_valid = False break + if package: + package_map[dungeon_map[name]] = package if all_valid: for name, sector_list in chosen_sectors.items(): builder = dungeon_map[name] for sector in sector_list: assign_sector(sector, builder, neutral_sectors, global_pole) + if builder in package_map: + builder.throne_door, builder.throne_sector, builder.chosen_lobby = package_map[builder] tries += 1 def split_dungeon_builder(builder, split_list, builder_info): + ents, splits, c_tuple, world, player = builder_info if builder.split_dungeon_map and len(builder.exception_list) == 0: for name, proposal in builder.valid_proposal.items(): builder.split_dungeon_map[name].valid_proposal = proposal + if builder.name == 'Hyrule Castle': + builder.chosen_lobby.outstanding_doors.remove(builder.throne_door) + builder.throne_sector.outstanding_doors.remove(world.get_door('Hyrule Castle Throne Room N', player)) return builder.split_dungeon_map # we made this earlier in gen, just use it attempts, comb_w_replace, merge_attempt, merge_limit = 0, None, 0, len(split_list) - 1 while attempts < 5: # does not solve coin flips 3% of the time try: candidate_sectors = dict.fromkeys(builder.sectors) - global_pole = GlobalPolarity(candidate_sectors) + if builder.name == 'Hyrule Castle': + throne_sector = find_sector('Hyrule Castle Throne Room', candidate_sectors) + chosen_lobbies = {r_name for x in split_list.values() for r_name in x} + choices = {} + for sector in candidate_sectors: + if sector.adj_outflow() > 1 and sector != throne_sector: + for door in sector.outstanding_doors: + if door.direction == Direction.South and door.entrance.parent_region not in chosen_lobbies: + choices[door] = sector + chosen_door = random.choice(list(choices.keys())) + split_list['Sewers'].append(chosen_door.entrance.parent_region.name) + choices[chosen_door].outstanding_doors.remove(chosen_door) + builder.throne_door = chosen_door + builder.throne_sector = throne_sector + builder.chosen_lobby = choices[chosen_door] + throne_sector.outstanding_doors.remove(world.get_door('Hyrule Castle Throne Room N', player)) + global_pole = GlobalPolarity(candidate_sectors) dungeon_map, sub_builder, merge_keys = {}, None, [] if merge_attempt > 0: candidates = [] @@ -2932,7 +3052,6 @@ def split_dungeon_builder(builder, split_list, builder_info): continue elif len(split_entrances) <= 0: continue - ents, splits, c_tuple, world, player = builder_info r_name = split_entrances[0] p = next((x for x in world.dungeon_portals[player] if x.door.entrance.parent_region.name == r_name), None) if p and not p.deadEnd: @@ -2953,14 +3072,29 @@ def split_dungeon_builder(builder, split_list, builder_info): sub_builder.all_entrances = list(split_entrances) for r_name in split_entrances: assign_sector(find_sector(r_name, candidate_sectors), sub_builder, candidate_sectors, global_pole) + if builder.name == 'Hyrule Castle': + assign_sector(find_sector('Hyrule Castle Throne Room', candidate_sectors), + dungeon_map['Hyrule Castle Dungeon'], candidate_sectors, global_pole) + assign_sector(find_sector('Hyrule Dungeon Cellblock', candidate_sectors), + dungeon_map['Hyrule Castle Dungeon'], candidate_sectors, global_pole) + dungeon_map['Hyrule Castle Dungeon'].throne_door = world.get_door('Hyrule Castle Throne Room N', player) + dungeon_map['Hyrule Castle Sewers'].sewers_access = builder.throne_door + if len(candidate_sectors) == 0: + return dungeon_map comb_w_replace = len(dungeon_map) ** len(candidate_sectors) return balance_split(candidate_sectors, dungeon_map, global_pole, builder_info) except (GenerationException, NeutralizingException): - if comb_w_replace and comb_w_replace <= 10000: + if comb_w_replace and comb_w_replace <= 10000 and not builder.throne_door: attempts += 5 # all the combinations were tried already, no use repeating else: attempts += 1 - if attempts >= 5 and merge_attempt < merge_limit: + if builder.throne_door: + previous = find_sector(builder.throne_door.entrance.parent_region.name, builder.sectors) + previous.outstanding_doors.append(builder.throne_door) + builder.throne_sector.outstanding_doors.append(world.get_door('Hyrule Castle Throne Room N', player)) + split_list['Sewers'].remove(builder.throne_door.entrance.parent_region.name) + builder.throne_door = None + if attempts >= 5 and merge_attempt < merge_limit and builder.name != 'Hyrule Castle': merge_attempt, attempts = merge_attempt + 1, 0 raise GenerationException('Unable to resolve in 5 attempts') @@ -2981,16 +3115,21 @@ def balance_split(candidate_sectors, dungeon_map, global_pole, builder_info): chosen_sectors = defaultdict(list) for i, choice in enumerate(choices): chosen_sectors[choice].append(main_sector_list[i]) - all_valid = True + all_valid, package_map = True, {} for name, builder in dungeon_map.items(): - if not valid_assignment(builder, chosen_sectors[name], builder_info): + flag, package = valid_assignment(builder, chosen_sectors[name], builder_info) + if not flag: all_valid = False break + if package: + package_map[builder] = package if all_valid: for name, sector_list in chosen_sectors.items(): builder = dungeon_map[name] for sector in sector_list: assign_sector(sector, builder, candidate_sectors, global_pole) + if builder in package_map: + builder.throne_door, builder.throne_sector, builder.chosen_lobby = package_map[builder] return dungeon_map tries += 1 raise GenerationException('Split Dungeon Builder: Impossible dungeon. Ref %s' % next(iter(dungeon_map.keys()))) @@ -3380,11 +3519,13 @@ class DungeonAccess: def identify_branching_issues(dungeon_map, builder_info): unconnected_builders = {} for name, builder in dungeon_map.items(): - resolved, unreached_doors = check_for_valid_layout(builder, [], builder_info) + resolved, unreached_doors, package = check_for_valid_layout(builder, [], builder_info) if not resolved: unconnected_builders[name] = builder for hook, door_list in unreached_doors.items(): builder.unfulfilled[hook] += len(door_list) + elif package: + builder.throne_door, builder.throne_sector, builder.chosen_lobby = package return unconnected_builders @@ -3402,7 +3543,8 @@ def check_for_valid_layout(builder, sector_list, builder_info): for portal in world.dungeon_portals[player]: if not portal.destination and portal.name in dungeon_portals[builder.name]: possible_regions.add(portal.door.entrance.parent_region.name) - if builder.name in dungeon_drops.keys(): + if builder.name in dungeon_drops.keys() and (builder.name != 'Hyrule Castle' + or world.mode[player] != 'standard'): possible_regions.update(dungeon_drops[builder.name]) independents = find_independent_entrances(possible_regions, world, player) for name, split_build in builder.split_dungeon_map.items(): @@ -3421,16 +3563,28 @@ def check_for_valid_layout(builder, sector_list, builder_info): proposal = generate_dungeon_find_proposal(split_build, entrance_regions, split, world, player) # record split proposals builder.valid_proposal[name] = proposal + package = None + if temp_builder.name == 'Hyrule Castle': + temp_builder.chosen_lobby.outstanding_doors.append(temp_builder.throne_door) + temp_builder.throne_sector.outstanding_doors.append(world.get_door('Hyrule Castle Throne Room N', player)) + package = temp_builder.throne_door, temp_builder.throne_sector, temp_builder.chosen_lobby + split_list['Sewers'].remove(temp_builder.throne_door.entrance.parent_region.name) builder.exception_list = list(sector_list) - return True, {} - except (GenerationException, NeutralizingException): + return True, {}, package + except (GenerationException, NeutralizingException, OtherGenException) as e: + logging.getLogger('').debug(f'Bailing on this layout for {builder.name}', exc_info=1) builder.split_dungeon_map = None builder.valid_proposal = None + if temp_builder.name == 'Hyrule Castle' and temp_builder.throne_door: + temp_builder.chosen_lobby.outstanding_doors.append(temp_builder.throne_door) + temp_builder.throne_sector.outstanding_doors.append(world.get_door('Hyrule Castle Throne Room N', player)) + old_entrance = temp_builder.throne_door.entrance.parent_region.name + split_dungeon_entrances[builder.name]['Sewers'].remove(old_entrance) unreached_doors = resolve_equations(builder, sector_list) - return False, unreached_doors + return False, unreached_doors, None else: unreached_doors = resolve_equations(builder, sector_list) - return len(unreached_doors) == 0, unreached_doors + return len(unreached_doors) == 0, unreached_doors, None def find_independent_entrances(entrance_regions, world, player): @@ -3831,22 +3985,20 @@ def find_free_equation(equations): def copy_door_equations(builder, sector_list): equations = {} for sector in builder.sectors + sector_list: - if sector.equations is None: - # todo: sort equations? - sector.equations = calc_sector_equations(sector) + sector.equations = calc_sector_equations(sector, builder.sewers_access) curr_list = equations[sector] = [] for equation in sector.equations: curr_list.append(equation.copy()) return equations -def calc_sector_equations(sector): +def calc_sector_equations(sector, sewers_flag=False): equations = [] - is_entrance = sector.is_entrance_sector() and not sector.destination_entrance + is_entrance = (sector.is_entrance_sector() and not sector.destination_entrance) or sewers_flag if is_entrance: flagged_equations = [] for door in sector.outstanding_doors: - equation, flag = calc_door_equation(door, sector, True) + equation, flag = calc_door_equation(door, sector, True, sewers_flag) if flag: flagged_equations.append(equation) equations.append(equation) @@ -3862,9 +4014,9 @@ def calc_sector_equations(sector): return equations -def calc_door_equation(door, sector, look_for_entrance): +def calc_door_equation(door, sector, look_for_entrance, sewers_flag=None): if look_for_entrance and not door.blocked: - flag = sector.is_entrance_sector() + flag = sector.is_entrance_sector() or sewers_flag if flag: eq = DoorEquation(door) eq.benefit[hook_from_door(door)].append(door) diff --git a/Dungeons.py b/Dungeons.py index 188cf59f..9d862b05 100644 --- a/Dungeons.py +++ b/Dungeons.py @@ -5,7 +5,7 @@ from Items import ItemFactory def create_dungeons(world, player): def make_dungeon(name, id, default_boss, dungeon_regions, big_key, small_keys, dungeon_items): - dungeon = Dungeon(name, dungeon_regions, big_key, [] if world.retro[player] else small_keys, + dungeon = Dungeon(name, dungeon_regions, big_key, [] if world.keyshuffle[player] == 'universal' else small_keys, dungeon_items, player, id) dungeon.boss = BossFactory(default_boss, player) for region in dungeon.regions: @@ -52,10 +52,10 @@ hyrule_castle_regions = [ 'Hyrule Dungeon South Abyss', 'Hyrule Dungeon South Abyss Catwalk', 'Hyrule Dungeon Guardroom', 'Hyrule Dungeon Armory Main', 'Hyrule Dungeon Armory Boomerang', 'Hyrule Dungeon Armory North Branch', 'Hyrule Dungeon Staircase', 'Hyrule Dungeon Cellblock', 'Hyrule Dungeon Cell', 'Sewers Behind Tapestry', - 'Sewers Rope Room', 'Sewers Dark Cross', 'Sewers Water', 'Sewers Key Rat', 'Sewers Rat Path', - 'Sewers Secret Room Blocked Path', 'Sewers Secret Room', 'Sewers Yet More Rats', 'Sewers Pull Switch', 'Sanctuary', - 'Sanctuary Portal', 'Hyrule Castle West Portal', 'Hyrule Castle South Portal', 'Hyrule Castle East Portal' - + 'Sewers Rope Room', 'Sewers Dark Cross', 'Sewers Water', 'Sewers Dark Aquabats', 'Sewers Key Rat', + 'Sewers Rat Path', 'Sewers Secret Room Blocked Path', 'Sewers Secret Room', 'Sewers Yet More Rats', + 'Sewers Pull Switch', 'Sanctuary', 'Sanctuary Portal', 'Hyrule Castle West Portal', 'Hyrule Castle South Portal', + 'Hyrule Castle East Portal' ] eastern_regions = [ @@ -155,14 +155,16 @@ ice_regions = [ mire_regions = [ 'Mire Lobby', 'Mire Post-Gap', 'Mire 2', 'Mire Hub', 'Mire Hub Right', 'Mire Hub Top', 'Mire Hub Switch', - 'Mire Lone Shooter', 'Mire Failure Bridge', 'Mire Falling Bridge', 'Mire Map Spike Side', 'Mire Map Spot', - 'Mire Crystal Dead End', 'Mire Hidden Shooters', 'Mire Hidden Shooters Blocked', 'Mire Cross', 'Mire Minibridge', - 'Mire BK Door Room', 'Mire Spikes', 'Mire Ledgehop', 'Mire Bent Bridge', 'Mire Over Bridge', 'Mire Right Bridge', - 'Mire Left Bridge', 'Mire Fishbone', 'Mire South Fish', 'Mire Spike Barrier', 'Mire Square Rail', 'Mire Lone Warp', - 'Mire Wizzrobe Bypass', 'Mire Conveyor Crystal', 'Mire Conveyor - Crystal', 'Mire Tile Room', 'Mire Compass Room', 'Mire Compass Chest', - 'Mire Neglected Room', 'Mire Chest View', 'Mire Conveyor Barrier', 'Mire BK Chest Ledge', 'Mire Warping Pool', - 'Mire Torches Top', 'Mire Torches Bottom', 'Mire Attic Hint', 'Mire Dark Shooters', 'Mire Key Rupees', - 'Mire Block X', 'Mire Tall Dark and Roomy', 'Mire Tall Dark and Roomy - Ranged Crystal', 'Mire Crystal Right', 'Mire Crystal Mid', 'Mire Crystal Left', + 'Mire Lone Shooter', 'Mire Failure Bridge', 'Mire Falling Bridge - Primary', 'Mire Falling Bridge - Failure', + 'Mire Falling Bridge - Chest', 'Mire Map Spike Side', 'Mire Map Spot', 'Mire Crystal Dead End', + 'Mire Hidden Shooters', 'Mire Hidden Shooters Blocked', 'Mire Cross', 'Mire Minibridge', 'Mire BK Door Room', + 'Mire Spikes', 'Mire Ledgehop', 'Mire Bent Bridge', 'Mire Over Bridge', 'Mire Right Bridge', 'Mire Left Bridge', + 'Mire Fishbone', 'Mire South Fish', 'Mire Spike Barrier', 'Mire Square Rail', 'Mire Lone Warp', + 'Mire Wizzrobe Bypass', 'Mire Conveyor Crystal', 'Mire Conveyor - Crystal', 'Mire Tile Room', 'Mire Compass Room', + 'Mire Compass Chest', 'Mire Neglected Room', 'Mire Chest View', 'Mire Conveyor Barrier', 'Mire BK Chest Ledge', + 'Mire Warping Pool', 'Mire Torches Top', 'Mire Torches Bottom', 'Mire Attic Hint', 'Mire Dark Shooters', + 'Mire Key Rupees', 'Mire Block X', 'Mire Tall Dark and Roomy', 'Mire Tall Dark and Roomy - Ranged Crystal', + 'Mire Crystal Right', 'Mire Crystal Mid', 'Mire Crystal Left', 'Mire Crystal Top', 'Mire Shooter Rupees', 'Mire Falling Foes', 'Mire Firesnake Skip', 'Mire Antechamber', 'Mire Boss', 'Mire Portal' ] diff --git a/ER_hint_reference.txt b/ER_hint_reference.txt index 8bc73fc9..a4972fba 100644 --- a/ER_hint_reference.txt +++ b/ER_hint_reference.txt @@ -91,6 +91,7 @@ While the exact verbage of location names and item names can be found in the sou Overworld Entrance naming: +Links House: The hero's old residence Turtle Rock: Turtle Rock Main Misery Mire: Misery Mire Ice Palace: Ice Palace @@ -141,6 +142,7 @@ Lake Hylia Fairy: A cave NE of Lake Hylia Light Hype Fairy: The cave south of your house Desert Fairy: The cave near the desert Chicken House: The chicken lady's house +Tavern North: A backdoor Aginahs Cave: The open desert cave Sahasrahlas Hut: The house near armos Cave Shop (Lake Hylia): The cave NW Lake Hylia diff --git a/EntranceShuffle.py b/EntranceShuffle.py index 1bd1e72b..dc6e6fd4 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -10,7 +10,7 @@ from Utils import stack_size3a entrance_pool = list() exit_pool = list() entrance_exits = list() -ignore_pool = False +ignore_pool = True suppress_spoiler = True def link_entrances(world, player): @@ -39,7 +39,6 @@ def link_entrances(world, player): if not world.is_tile_swapped(0x1b, player): drop_connections.append(tuple(('Pyramid Hole', 'Pyramid'))) dropexit_connections.append(tuple(('Pyramid Entrance', 'Pyramid Exit'))) - connect_simple(world, 'Other World S&Q', 'Pyramid Area', player) else: entrance_pool.remove('Pyramid Hole') entrance_pool.append('Inverted Pyramid Hole') @@ -47,7 +46,6 @@ def link_entrances(world, player): entrance_pool.append('Inverted Pyramid Entrance') drop_connections.append(tuple(('Inverted Pyramid Hole', 'Pyramid'))) dropexit_connections.append(tuple(('Inverted Pyramid Entrance', 'Pyramid Exit'))) - connect_simple(world, 'Other World S&Q', 'Hyrule Castle Ledge', player) unbias_some_entrances(Dungeon_Exits, Cave_Exits, Old_Man_House, Cave_Three_Exits) Cave_Exits.extend(Cave_Exits_Directional) @@ -56,16 +54,6 @@ def link_entrances(world, player): for exitname, regionname in mandatory_connections: connect_simple(world, exitname, regionname, player) - if not world.is_bombshop_start(player): - connect_simple(world, 'Links House S&Q', 'Links House', player) - else: - connect_simple(world, 'Links House S&Q', 'Big Bomb Shop', player) - - if not invFlag: - connect_simple(world, 'Sanctuary S&Q', 'Sanctuary', player) - else: - connect_simple(world, 'Sanctuary S&Q', 'Dark Sanctuary Hint', player) - connect_simple(world, 'Tavern North', 'Tavern', player) suppress_spoiler = False @@ -74,7 +62,8 @@ def link_entrances(world, player): # if we do not shuffle, set default connections if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull']: - for entrancename, exitname in default_connections + default_pot_connections + drop_connections + default_item_connections + default_shop_connections: + for entrancename, exitname in (default_connections + default_pot_connections + + default_takeany_connections + drop_connections + default_item_connections + default_shop_connections): connect_logical(world, entrancename, exitname, player, exitname.endswith(' Exit')) for entrancename, exitname in default_connector_connections + dropexit_connections: connect_logical(world, entrancename, exitname, player, True) @@ -221,6 +210,8 @@ def link_entrances(world, player): bomb_shop_doors = list(entrance_pool) if world.logic[player] in ['noglitches', 'minorglitches'] or world.is_tile_swapped(0x1b, player): bomb_shop_doors = [e for e in entrance_pool if e not in ['Pyramid Fairy']] + if world.is_tile_swapped(0x03, player): + bomb_shop_doors = [x for x in bomb_shop_doors if x not in ['Spectacle Rock Cave', 'Spectacle Rock Cave (Bottom)']] bomb_shop = random.choice(bomb_shop_doors) connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) @@ -267,6 +258,8 @@ def link_entrances(world, player): bomb_shop_doors = list(entrance_pool) if world.logic[player] in ['noglitches', 'minorglitches'] or world.is_tile_swapped(0x1b, player): bomb_shop_doors = [e for e in entrance_pool if e not in ['Pyramid Fairy']] + if world.is_tile_swapped(0x03, player): + bomb_shop_doors = [x for x in bomb_shop_doors if x not in ['Spectacle Rock Cave', 'Spectacle Rock Cave (Bottom)']] bomb_shop = random.choice(bomb_shop_doors) connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) @@ -325,6 +318,8 @@ def link_entrances(world, player): bomb_shop_doors = [e for e in entrance_pool if e not in list(zip(*drop_connections + dropexit_connections))[0]] if world.logic[player] in ['noglitches', 'minorglitches'] or world.is_tile_swapped(0x1b, player): bomb_shop_doors = [e for e in bomb_shop_doors if e not in ['Pyramid Fairy']] + if world.is_tile_swapped(0x03, player): + bomb_shop_doors = [x for x in bomb_shop_doors if x not in ['Spectacle Rock Cave', 'Spectacle Rock Cave (Bottom)']] bomb_shop = random.choice(bomb_shop_doors) connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) @@ -342,7 +337,10 @@ def link_entrances(world, player): # place remaining doors connect_doors(world, list(entrance_pool), list(exit_pool), player) elif world.shuffle[player] == 'lite': - for entrancename, exitname in default_connections + ([] if world.shopsanity[player] else default_shop_connections) + ([] if world.pottery[player] not in ['none', 'keys', 'dungeon'] else default_pot_connections): + for entrancename, exitname in (default_connections + + ([] if world.shopsanity[player] else default_shop_connections) + + ([] if world.pottery[player] not in ['none', 'keys', 'dungeon'] else default_pot_connections) + + ([] if world.take_any[player] == 'fixed' else default_takeany_connections)): connect_logical(world, entrancename, exitname, player, False) if invFlag: world.get_entrance('Dark Sanctuary Hint Exit', player).connect(world.get_entrance('Dark Sanctuary Hint', player).parent_region) @@ -426,13 +424,18 @@ def link_entrances(world, player): bomb_shop_doors = list(entrance_pool) if world.logic[player] in ['noglitches', 'minorglitches'] or world.is_tile_swapped(0x1b, player): bomb_shop_doors = [e for e in entrance_pool if e not in ['Pyramid Fairy']] + if world.is_tile_swapped(0x03, player): + bomb_shop_doors = [x for x in bomb_shop_doors if x not in ['Spectacle Rock Cave', 'Spectacle Rock Cave (Bottom)']] bomb_shop = random.choice(bomb_shop_doors) connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) # place remaining doors connect_doors(world, list(entrance_pool), list(exit_pool), player) elif world.shuffle[player] == 'lean': - for entrancename, exitname in default_connections + ([] if world.shopsanity[player] else default_shop_connections) + ([] if world.pottery[player] not in ['none', 'keys', 'dungeon'] else default_pot_connections): + for entrancename, exitname in (default_connections + + ([] if world.shopsanity[player] else default_shop_connections) + + ([] if world.pottery[player] not in ['none', 'keys', 'dungeon'] else default_pot_connections) + + ([] if world.take_any[player] == 'fixed' else default_takeany_connections)): connect_logical(world, entrancename, exitname, player, False) if invFlag: world.get_entrance('Dark Sanctuary Hint Exit', player).connect(world.get_entrance('Dark Sanctuary Hint', player).parent_region) @@ -481,6 +484,8 @@ def link_entrances(world, player): bomb_shop_doors = list(entrance_pool) if world.logic[player] in ['noglitches', 'minorglitches'] or world.is_tile_swapped(0x1b, player): bomb_shop_doors = [e for e in entrance_pool if e not in ['Pyramid Fairy']] + if world.is_tile_swapped(0x03, player): + bomb_shop_doors = [x for x in bomb_shop_doors if x not in ['Spectacle Rock Cave', 'Spectacle Rock Cave (Bottom)']] bomb_shop = random.choice(bomb_shop_doors) connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) @@ -531,6 +536,8 @@ def link_entrances(world, player): bomb_shop_doors = list(entrance_pool) if world.logic[player] in ['noglitches', 'minorglitches'] or world.is_tile_swapped(0x1b, player): bomb_shop_doors = [e for e in entrance_pool if e not in ['Pyramid Fairy']] + if world.is_tile_swapped(0x03, player): + bomb_shop_doors = [x for x in bomb_shop_doors if x not in ['Spectacle Rock Cave', 'Spectacle Rock Cave (Bottom)']] bomb_shop = random.choice(bomb_shop_doors) connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) @@ -615,6 +622,8 @@ def link_entrances(world, player): bomb_shop_doors = list(entrance_pool) if world.logic[player] in ['noglitches', 'minorglitches'] or world.is_tile_swapped(0x1b, player): bomb_shop_doors = [e for e in entrance_pool if e not in ['Pyramid Fairy']] + if world.is_tile_swapped(0x03, player): + bomb_shop_doors = [x for x in bomb_shop_doors if x not in ['Spectacle Rock Cave', 'Spectacle Rock Cave (Bottom)']] random.shuffle(bomb_shop_doors) bomb_shop = bomb_shop_doors.pop() pool.remove(bomb_shop) @@ -704,9 +713,9 @@ def connect_logical(world, entrancename, exitname, player, isTwoWay = False): def connect_entrance(world, entrancename, exitname, player, mark_two_way=True): if not ignore_pool: logging.getLogger('').debug('Connecting %s -> %s', entrancename, exitname) - assert entrancename in entrance_pool, 'Entrance not in pool: ' + entrancename + assert entrancename in entrance_pool, f'Entrance not in pool: {entrancename}' if mark_two_way: - assert exitname in exit_pool, 'Exit not in pool: ' + exitname + assert exitname in exit_pool, f'Exit not in pool: {exitname}' entrance = world.get_entrance(entrancename, player) # check if we got an entrance or a region to connect to @@ -807,7 +816,7 @@ def connect_mandatory_exits(world, entrances, caves, must_be_exits, player, must # if world.logic[player] in ['owglitches', 'nologic']: # import OverworldGlitchRules - # for entrance in OverworldGlitchRules.get_non_mandatory_exits(world.mode[player] == 'inverted'): + # for entrance in OverworldGlitchRules.get_non_mandatory_exits(world, player): # if entrance in must_be_exits: # must_be_exits.remove(entrance) # entrances.append(entrance) @@ -1353,8 +1362,13 @@ def place_links_house(world, player, ignore_list=[]): links_house_doors = [i for i in get_distant_entrances(world, dark_sanc, player) if i in entrance_pool] else: links_house_doors = [i for i in get_starting_entrances(world, player, world.shuffle[player] != 'insanity') if i in entrance_pool] + if world.is_bombshop_start(player) and world.is_tile_swapped(0x03, player): + links_house_doors = [x for x in links_house_doors if x not in ['Spectacle Rock Cave', 'Spectacle Rock Cave (Bottom)']] if world.shuffle[player] in ['lite', 'lean']: - links_house_doors = [e for e in links_house_doors if e in list(zip(*(default_item_connections + (default_shop_connections if world.shopsanity[player] else []) + (default_pot_connections if world.pottery[player] not in ['none', 'keys', 'dungeon'] else []))))[0]] + links_house_doors = [e for e in links_house_doors if e in list(zip(*(default_item_connections + + (default_shop_connections if world.shopsanity[player] else []) + + (default_pot_connections if world.pottery[player] not in ['none', 'keys', 'dungeon'] else []) + + (default_takeany_connections if world.take_any[player] == 'fixed' else []))))[0]] #TODO: Need to improve Links House placement to choose a better sector or eliminate entrances that are after ledge drops links_house_doors = [e for e in links_house_doors if e not in ignore_list] @@ -1370,7 +1384,7 @@ def place_links_house(world, player, ignore_list=[]): def place_dark_sanc(world, player, ignore_list=[]): if not world.shufflelinks[player]: - sanc_doors = [i for i in get_distant_entrances(world, 'Big Bomb Shop', player) if i in entrance_pool] + sanc_doors = [i for i in get_distant_entrances(world, 'Big Bomb Shop' if world.is_bombshop_start(player) else 'Links House', player) if i in entrance_pool] else: sanc_doors = [i for i in get_starting_entrances(world, player, world.shuffle[player] != 'insanity') if i in entrance_pool] if world.shuffle[player] in ['lite', 'lean']: @@ -1388,8 +1402,7 @@ def place_blacksmith(world, links_house, player): invFlag = world.mode[player] == 'inverted' assumed_inventory = list() - region = world.get_region('Frog Prison', player) - if world.logic[player] in ['noglitches', 'minorglitches'] and region.type == (RegionType.DarkWorld if not invFlag else RegionType.LightWorld): + if world.logic[player] in ['noglitches', 'minorglitches'] and (world.is_tile_swapped(0x29, player) == invFlag): assumed_inventory.append('Titans Mitts') links_region = world.get_entrance(links_house, player).parent_region.name @@ -1402,7 +1415,10 @@ def place_blacksmith(world, links_house, player): sanc_region = world.get_entrance('Sanctuary Exit', player).connected_region.name blacksmith_doors = list(OrderedDict.fromkeys(blacksmith_doors + list(build_accessible_entrance_list(world, sanc_region, player, assumed_inventory, False, True, True)))) if world.shuffle[player] in ['lite', 'lean']: - blacksmith_doors = [e for e in blacksmith_doors if e in list(zip(*(default_item_connections + (default_shop_connections if world.shopsanity[player] else []) + (default_pot_connections if world.pottery[player] not in ['none', 'keys', 'dungeon'] else []))))[0]] + blacksmith_doors = [e for e in blacksmith_doors if e in list(zip(*(default_item_connections + + (default_shop_connections if world.shopsanity[player] else []) + + (default_pot_connections if world.pottery[player] not in ['none', 'keys', 'dungeon'] else []) + + (default_takeany_connections if world.take_any[player] == 'fixed' else []))))[0]] assert len(blacksmith_doors), 'No valid candidates to place Blacksmiths Hut' blacksmith_hut = random.choice(blacksmith_doors) @@ -1420,6 +1436,7 @@ def place_old_man(world, pool, player, ignore_list=[]): old_man_entrances = [e for e in old_man_entrances if e != 'Old Man House (Bottom)' and e not in ignore_list] if world.shuffle[player] in ['lite', 'lean']: old_man_entrances = [e for e in old_man_entrances if e in pool] + assert len(old_man_entrances), 'No available entrances left to place Old Man Cave' random.shuffle(old_man_entrances) old_man_exit = None while not old_man_exit: @@ -1469,72 +1486,166 @@ def junk_fill_inaccessible(world, player): if not exit.connected_region and exit.name in entrance_pool: inaccessible_entrances.append(exit.name) - junk_locations = [e for e in list(zip(*(default_connections + ([] if world.pottery[player] not in ['none', 'keys', 'dungeon'] else default_pot_connections))))[1] if e in exit_pool] + junk_locations = [e for e in list(zip(*(default_connections + + ([] if world.pottery[player] not in ['none', 'keys', 'dungeon'] else default_pot_connections) + + ([] if world.take_any[player] == 'fixed' else default_takeany_connections))))[1] if e in exit_pool] random.shuffle(junk_locations) for entrance in inaccessible_entrances: connect_entrance(world, entrance, junk_locations.pop(), player) def connect_inaccessible_regions(world, lw_entrances, dw_entrances, caves, player, ignore_list=[]): - invFlag = world.mode[player] == 'inverted' + def find_inacessible_ow_regions(): + nonlocal inaccessible_regions + find_inaccessible_regions(world, player) + inaccessible_regions = list(world.inaccessible_regions[player]) + + # find OW regions that don't have a multi-entrance dungeon exit connected + glitch_regions = ['Central Cliffs', 'Eastern Cliff', 'Desert Northeast Cliffs', 'Hyrule Castle Water', + 'Dark Central Cliffs', 'Darkness Cliff', 'Mire Northeast Cliffs', 'Pyramid Water'] + multi_dungeon_exits = { + 'Hyrule Castle South Portal', 'Hyrule Castle West Portal', 'Hyrule Castle East Portal', 'Sanctuary Portal', + 'Desert South Portal', 'Desert West Portal', + 'Skull 2 East Portal', 'Skull 2 West Portal', + 'Turtle Rock Main Portal', 'Turtle Rock Lazy Eyes Portal', 'Turtle Rock Eye Bridge Portal' + } + for region_name in world.inaccessible_regions[player]: + if (world.logic[player] in ['noglitches', 'minorglitches'] and region_name in glitch_regions) \ + or (region_name == 'Pyramid Exit Ledge' and (world.shuffle[player] != 'insanity' or world.is_tile_swapped(0x1b, player))) \ + or (region_name == 'Spiral Mimic Ledge Extend' and not world.is_tile_swapped(0x05, player)): + # removing irrelevant and resolved regions + inaccessible_regions.remove(region_name) + continue + region = world.get_region(region_name, player) + if region.type not in [RegionType.LightWorld, RegionType.DarkWorld]: + inaccessible_regions.remove(region_name) + continue + if world.shuffle[player] != 'insanity': + for exit in region.exits: + # because dungeon regions haven't been connected yet, the inaccessibility check won't be able to know it's reachable yet + if exit.connected_region and exit.connected_region.name in multi_dungeon_exits: + resolved_regions.append(region_name) + break - if stack_size3a() > 500: - from DungeonGenerator import GenerationException - raise GenerationException(f'Infinite loop detected at \'connect_inaccessible_regions\'') + inaccessible_regions = list() + resolved_regions = list() + find_inacessible_ow_regions() - random.shuffle(lw_entrances) - random.shuffle(dw_entrances) - - find_inaccessible_regions(world, player) - - # remove regions that have a dungeon entrance - accessible_regions = list() - for region_name in world.inaccessible_regions[player]: + # keep track of neighboring regions for later consolidation + must_exit_links = OrderedDict() + for region_name in inaccessible_regions: region = world.get_region(region_name, player) - for exit in region.exits: - if exit.connected_region and exit.connected_region.type == RegionType.Dungeon: - accessible_regions.append(region_name) - break - for region_name in accessible_regions.copy(): - accessible_regions = list(OrderedDict.fromkeys(accessible_regions + list(build_accessible_region_list(world, region_name, player, True, True, False, False)))) - world.inaccessible_regions[player] = [r for r in world.inaccessible_regions[player] if r not in accessible_regions] - - # split inaccessible into 2 lists for each world - inaccessible_regions = list(world.inaccessible_regions[player]) + must_exit_links[region_name] = [x.connected_region.name for x in region.exits if x.connected_region and x.connected_region.name in inaccessible_regions] + + # group neighboring regions together, separated by one-ways + def consolidate_group(region): + processed_regions.append(region) + must_exit_links_copy.pop(region) + region_group.append(region) + for dest_region in must_exit_links[region]: + if region in must_exit_links[dest_region]: + if dest_region not in processed_regions: + consolidate_group(dest_region) + else: + one_ways.append(tuple((region, dest_region))) + + processed_regions = list() + must_exit_candidates = list() + one_ways = list() + must_exit_links_copy = must_exit_links.copy() + while len(must_exit_links_copy): + region_group = list() + region_name = next(iter(must_exit_links_copy)) + consolidate_group(region_name) + must_exit_candidates.append(region_group) + + # get available entrances in each group + for regions in must_exit_candidates: + entrances = list() + for region_name in regions: + region = world.get_region(region_name, player) + entrances = entrances + [x.name for x in region.exits if x.spot_type == 'Entrance' and not x.connected_region] + entrances = [e for e in entrances if e in entrance_pool and e not in ignore_list] + must_exit_candidates[must_exit_candidates.index(regions)] = tuple((regions, entrances)) + + # necessary for circular relations between region groups, it will pick the last group + # and fill one of those entrances, and we don't want it to bias the same group + random.shuffle(must_exit_candidates) + + # remove must exit candidates that would be made accessible thru other region groups + def find_group(region): + for group in must_exit_candidates: + regions, _ = group + if region in regions: + return group + raise Exception(f'Could not find region group for {region}') + + def cascade_ignore(group): + nonlocal ignored_regions + regions, _ = group + ignored_regions = ignored_regions + regions + for from_region, to_region in one_ways: + if from_region in regions and to_region not in ignored_regions: + cascade_ignore(find_group(to_region)) + + def process_group(group): + nonlocal processed_regions, ignored_regions + regions, entrances = group + must_exit_candidates_copy.remove(group) + processed_regions = processed_regions + regions + if regions[0] not in ignored_regions: + for from_region, to_region in one_ways: + if to_region in regions and from_region not in ignored_regions + processed_regions: + process_group(find_group(from_region)) # process the parent region group + if regions[0] not in ignored_regions: + # this is the top level region + if any(r in resolved_regions for r in regions): + cascade_ignore(group) + else: + if len(entrances): + # we will fulfill must exit here and cascade access to children + must_exit_regions.append(group) + cascade_ignore(group) + else: + ignored_regions = ignored_regions + regions + + processed_regions = list() + ignored_regions = list() must_exit_regions = list() - otherworld_must_exit_regions = list() - for region_name in inaccessible_regions.copy(): - region = world.get_region(region_name, player) - if region.type not in [RegionType.LightWorld, RegionType.DarkWorld] or not any((not exit.connected_region and exit.spot_type == 'Entrance') for exit in region.exits) \ - or (region_name == 'Pyramid Exit Ledge' and (world.shuffle[player] != 'insanity' or world.is_tile_swapped(0x1b, player))) \ - or region_name in ['Hyrule Castle Water', 'Pyramid Water']: - inaccessible_regions.remove(region_name) - elif region.type == (RegionType.LightWorld if not invFlag else RegionType.DarkWorld): - must_exit_regions.append(region_name) - elif region.type == (RegionType.DarkWorld if not invFlag else RegionType.LightWorld): - otherworld_must_exit_regions.append(region_name) - - def connect_one(region_name, pool): - inaccessible_entrances = list() - region = world.get_region(region_name, player) - for exit in region.exits: - if not exit.connected_region and exit.name in [e for e in entrance_pool if e not in ignore_list] and (world.shuffle[player] not in ['lite', 'lean'] or exit.name in pool): - inaccessible_entrances.append(exit.name) - if len(inaccessible_entrances): - random.shuffle(inaccessible_entrances) - connect_mandatory_exits(world, pool, caves, [inaccessible_entrances.pop()], player) - connect_inaccessible_regions(world, lw_entrances, dw_entrances, caves, player, ignore_list) - - # connect one connector at a time to ensure multiple connectors aren't assigned to the same inaccessible set of regions - pool = [e for e in (lw_entrances if world.shuffle[player] in ['lean', 'crossed', 'insanity'] else dw_entrances) if e in entrance_pool] - if len(otherworld_must_exit_regions) > 0 and len(pool): - random.shuffle(otherworld_must_exit_regions) - connect_one(otherworld_must_exit_regions[0], pool) - elif len(must_exit_regions) > 0: - pool = [e for e in lw_entrances if e in entrance_pool] + must_exit_candidates_copy = must_exit_candidates.copy() + while len(must_exit_candidates_copy): + region_group = next(iter(must_exit_candidates_copy)) + process_group(region_group) + + # connect must exits + random.shuffle(must_exit_regions) + must_exits_lw = list() + must_exits_dw = list() + for regions, entrances in must_exit_regions: + region = world.get_region(regions[0], player) + if region.type == RegionType.LightWorld: + must_exits_lw.append(random.choice(entrances)) + else: + must_exits_dw.append(random.choice(entrances)) + if world.shuffle[player] in ['lean', 'crossed', 'insanity']: # cross world + pool = [e for e in lw_entrances + dw_entrances if e in entrance_pool and e not in must_exits_lw + must_exits_dw] + connect_mandatory_exits(world, pool, caves, must_exits_lw + must_exits_dw, player) + else: + pool = [e for e in lw_entrances if e in entrance_pool and e not in must_exits_lw] if len(pool): - random.shuffle(must_exit_regions) - connect_one(must_exit_regions[0], pool) + connect_mandatory_exits(world, pool, caves, must_exits_lw, player) + pool = [e for e in dw_entrances if e in entrance_pool and e not in must_exits_dw] + if len(pool): + connect_mandatory_exits(world, pool, caves, must_exits_dw, player) + + # check accessibility afterwards + resolved_regions = list() + find_inacessible_ow_regions() + inaccessible_regions = [e for e in inaccessible_regions if e not in resolved_regions] + # TODO: Instead of line above, this should cascade from the resolved regions down to regions it can access + if len(inaccessible_regions) > 0: + logging.getLogger('').debug(f'Could not resolve inaccessible regions: [{", ".join(inaccessible_regions)}]') + logging.getLogger('').debug(f'^ This is most often a false positive because Dungeon regions aren\'t connected yet') def unbias_some_entrances(Dungeon_Exits, Cave_Exits, Old_Man_House, Cave_Three_Exits): @@ -1604,6 +1715,7 @@ def unbias_dungeons(Dungeon_Exits): def build_accessible_entrance_list(world, start_region, player, assumed_inventory=[], cross_world=False, region_rules=True, exit_rules=True, include_one_ways=False): from Main import copy_world_premature from Items import ItemFactory + from OverworldShuffle import one_way_ledges for p in range(1, world.players + 1): world.key_logic[p] = {} @@ -1672,6 +1784,8 @@ def get_starting_entrances(world, player, force_starting_world=True): def get_distant_entrances(world, start_entrance, player): + from OverworldShuffle import one_way_ledges + # get walkable sector in which initial entrance was placed start_region = world.get_entrance(start_entrance, player).parent_region.name regions = next(s for s in world.owsectors[player] if any(start_region in w for w in s)) @@ -2039,11 +2153,9 @@ Exit_Pool_Base = ['Links House Exit', 'Skull Pot Circle', 'Pyramid'] -# these are connections that cannot be shuffled and always exist. They link together separate parts of the world we need to divide into regions -mandatory_connections = [('Old Man S&Q', 'Old Man House'), - - # UW Connections - ('Lost Woods Hideout (top to bottom)', 'Lost Woods Hideout (bottom)'), +# these are connections that cannot be shuffled and always exist. +# They link together underworld regions +mandatory_connections = [('Lost Woods Hideout (top to bottom)', 'Lost Woods Hideout (bottom)'), ('Lumberjack Tree (top to bottom)', 'Lumberjack Tree (bottom)'), ('Kakariko Well (top to bottom)', 'Kakariko Well (bottom)'), ('Kakariko Well (top to back)', 'Kakariko Well (back)'), @@ -2084,8 +2196,6 @@ mandatory_connections = [('Old Man S&Q', 'Old Man House'), default_connections = [('Bonk Fairy (Light)', 'Bonk Fairy (Light)'), ('Lake Hylia Fairy', 'Lake Hylia Healer Fairy'), ('Lake Hylia Fortune Teller', 'Lake Hylia Fortune Teller'), - ('Light Hype Fairy', 'Swamp Healer Fairy'), - ('Desert Fairy', 'Desert Healer Fairy'), ('Lost Woods Gamble', 'Lost Woods Gamble'), ('Fortune Teller (Light)', 'Fortune Teller (Light)'), ('Bush Covered House', 'Bush Covered House'), @@ -2095,16 +2205,20 @@ default_connections = [('Bonk Fairy (Light)', 'Bonk Fairy (Light)'), ('East Dark World Hint', 'East Dark World Hint'), ('Dark Lake Hylia Fairy', 'Dark Lake Hylia Healer Fairy'), - ('Dark Lake Hylia Ledge Fairy', 'Dark Lake Hylia Ledge Healer Fairy'), ('Dark Lake Hylia Ledge Hint', 'Dark Lake Hylia Ledge Hint'), - ('Bonk Fairy (Dark)', 'Bonk Fairy (Dark)'), ('Dark Sanctuary Hint', 'Dark Sanctuary Hint'), ('Fortune Teller (Dark)', 'Fortune Teller (Dark)'), ('Archery Game', 'Archery Game'), - ('Dark Desert Fairy', 'Dark Desert Healer Fairy'), - ('Dark Death Mountain Fairy', 'Dark Death Mountain Healer Fairy'), + ('Dark Desert Fairy', 'Dark Desert Healer Fairy') ] +default_takeany_connections = [('Light Hype Fairy', 'Swamp Healer Fairy'), + ('Desert Fairy', 'Desert Healer Fairy'), + ('Dark Lake Hylia Ledge Fairy', 'Dark Lake Hylia Ledge Healer Fairy'), + ('Bonk Fairy (Dark)', 'Bonk Fairy (Dark)'), + ('Dark Death Mountain Fairy', 'Dark Death Mountain Healer Fairy') + ] + default_pot_connections = [('Lumberjack House', 'Lumberjack House'), ('Snitch Lady (East)', 'Snitch Lady (East)'), ('Snitch Lady (West)', 'Snitch Lady (West)'), @@ -2249,53 +2363,6 @@ open_default_dungeon_connections = [('Ganons Tower', 'Ganons Tower Exit'), inverted_default_dungeon_connections = [('Ganons Tower', 'Agahnims Tower Exit'), ('Agahnims Tower', 'Ganons Tower Exit') ] - -one_way_ledges = { - 'West Death Mountain (Bottom)': {'West Death Mountain (Top)', - 'Spectacle Rock Ledge'}, - 'East Death Mountain (Bottom)': {'East Death Mountain (Top East)', - 'Spiral Cave Ledge'}, - 'Fairy Ascension Plateau': {'Fairy Ascension Ledge'}, - 'Mountain Entry Area': {'Mountain Entry Ledge'}, - 'Sanctuary Area': {'Bonk Rock Ledge'}, - 'Graveyard Area': {'Graveyard Ledge'}, - 'Potion Shop Water': {'Potion Shop Area', - 'Potion Shop Northeast'}, - 'Zora Approach Water': {'Zora Approach Area'}, - 'Hyrule Castle Area': {'Hyrule Castle Ledge'}, - 'Wooden Bridge Water': {'Wooden Bridge Area', - 'Wooden Bridge Northeast'}, - 'Maze Race Area': {'Maze Race Ledge', - 'Maze Race Prize'}, - 'Flute Boy Approach Area': {'Cave 45 Ledge'}, - 'Desert Area': {'Desert Ledge', - 'Desert Palace Entrance (North) Spot', - 'Desert Checkerboard Ledge', - 'Desert Palace Mouth', - 'Desert Palace Stairs', - 'Bombos Tablet Ledge', - 'Desert Palace Teleporter Ledge'}, - 'Desert Pass Area': {'Desert Pass Ledge'}, - 'Lake Hylia Water': {'Lake Hylia South Shore', - 'Lake Hylia Island'}, - 'West Dark Death Mountain (Bottom)': {'West Dark Death Mountain (Top)'}, - 'West Dark Death Mountain (Top)': {'Dark Death Mountain Floating Island'}, - 'East Dark Death Mountain (Bottom)': {'East Dark Death Mountain (Top)'}, - 'Turtle Rock Area': {'Turtle Rock Ledge'}, - 'Bumper Cave Area': {'Bumper Cave Ledge'}, - 'Qirn Jump Water': {'Qirn Jump Area'}, - 'Dark Witch Water': {'Dark Witch Area', - 'Dark Witch Northeast'}, - 'Catfish Approach Water': {'Catfish Approach Area'}, - 'Pyramid Area': {'Pyramid Exit Ledge'}, - 'Broken Bridge Water': {'Broken Bridge West', - 'Broken Bridge Area', - 'Broken Bridge Northeast'}, - 'Misery Mire Area': {'Misery Mire Teleporter Ledge'}, - 'Ice Lake Water': {'Ice Lake Area', - 'Ice Lake Ledge (West)', - 'Ice Lake Ledge (East)'} -} # format: # Key=Name # addr = (door_index, exitdata, ow_flag) # multiexit @@ -2378,7 +2445,7 @@ door_addresses = {'Links House': (0x00, (0x0104, 0x2c 'Light Hype Fairy': (0x6B, (0x0115, 0x34, 0x00a0, 0x0c04, 0x0900, 0x0c58, 0x0988, 0x0c73, 0x0985, 0x0a, 0xf6, 0x0000, 0x0000), 0x02), 'Desert Fairy': (0x71, (0x0115, 0x3a, 0x0000, 0x0e00, 0x0400, 0x0e26, 0x0468, 0x0e6d, 0x0485, 0x00, 0x00, 0x0000, 0x0000), 0x00), 'Kings Grave': (0x5A, (0x0113, 0x14, 0x0320, 0x0456, 0x0900, 0x04a6, 0x0998, 0x04c3, 0x097d, 0x0a, 0xf6, 0x0000, 0x0000), 0x20), - 'Tavern North': (0x42, (0x0103, 0x18, 0x1440, 0x08a7, 0x0206, 0x08f9, 0x0288, 0x0914, 0x0293, 0xf7, 0x09, 0xFFFF, 0x0000), 0x00), # do not use, buggy + 'Tavern North': (0x42, (0x0103, 0x18, 0x1440, 0x08a7, 0x0206, 0x091b, 0x0288, 0x0914, 0x0293, 0xf7, 0x09, 0xFFFF, 0x0000), 0x00), 'Chicken House': (0x4A, (0x0108, 0x18, 0x1120, 0x0837, 0x0106, 0x0888, 0x0188, 0x08a4, 0x0193, 0x07, 0xf9, 0x1530, 0x0000), 0x00), 'Aginahs Cave': (0x70, (0x010a, 0x30, 0x0656, 0x0cc6, 0x02aa, 0x0d18, 0x0328, 0x0d33, 0x032f, 0x08, 0xf8, 0x0000, 0x0000), 0x00), 'Sahasrahlas Hut': (0x44, (0x0105, 0x1e, 0x0610, 0x06d4, 0x0c76, 0x0727, 0x0cf0, 0x0743, 0x0cfb, 0x0a, 0xf6, 0x0000, 0x0000), 0x00), @@ -2659,7 +2726,7 @@ ow_prize_table = {'Links House': (0x8b1, 0xb2d), 'Light Hype Fairy': (0x940, 0xc80), 'Desert Fairy': (0x420, 0xe00), 'Kings Grave': (0x920, 0x520), - 'Tavern North': None, # can't mark this one technically + 'Tavern North': (0x270, 0x900), 'Chicken House': (0x120, 0x880), 'Aginahs Cave': (0x2e0, 0xd00), 'Sahasrahlas Hut': (0xcf0, 0x6c0), diff --git a/Fill.py b/Fill.py index 701a2c7c..4507ddeb 100644 --- a/Fill.py +++ b/Fill.py @@ -13,7 +13,7 @@ from source.item.FillUtil import filter_pot_locations, valid_pot_items def get_dungeon_item_pool(world): - return [item for dungeon in world.dungeons for item in dungeon.all_items] + return [item for dungeon in world.dungeons for item in dungeon.all_items if item.location is None] def promote_dungeon_items(world): @@ -24,7 +24,6 @@ def promote_dungeon_items(world): item.advancement = True elif item.map or item.compass: item.priority = True - dungeon_tracking(world) def dungeon_tracking(world): @@ -35,11 +34,11 @@ def dungeon_tracking(world): def fill_dungeons_restrictive(world, shuffled_locations): - dungeon_tracking(world) # with shuffled dungeon items they are distributed as part of the normal item pool for item in world.get_items(): - if (item.smallkey and world.keyshuffle[item.player]) or (item.bigkey and world.bigkeyshuffle[item.player]): + if ((item.smallkey and world.keyshuffle[item.player] != 'none') + or (item.bigkey and world.bigkeyshuffle[item.player])): item.advancement = True elif (item.map and world.mapshuffle[item.player]) or (item.compass and world.compassshuffle[item.player]): item.priority = True @@ -50,7 +49,7 @@ def fill_dungeons_restrictive(world, shuffled_locations): (bigs if i.bigkey else smalls if i.smallkey else others).append(i) unplaced_smalls = list(smalls) for i in world.itempool: - if i.smallkey and world.keyshuffle[i.player]: + if i.smallkey and world.keyshuffle[i.player] != 'none': unplaced_smalls.append(i) def fill(base_state, items, key_pool): @@ -72,11 +71,13 @@ def fill_dungeons_restrictive(world, shuffled_locations): def fill_restrictive(world, base_state, locations, itempool, key_pool=None, single_player_placement=False, vanilla=False): - def sweep_from_pool(): + def sweep_from_pool(placing_item=None): new_state = base_state.copy() for item in itempool: new_state.collect(item, True) + new_state.placing_item = placing_item new_state.sweep_for_events() + new_state.placing_item = None return new_state unplaced_items = [] @@ -93,7 +94,7 @@ def fill_restrictive(world, base_state, locations, itempool, key_pool=None, sing while any(player_items.values()) and locations: items_to_place = [[itempool.remove(items[-1]), items.pop()][-1] for items in player_items.values() if items] - maximum_exploration_state = sweep_from_pool() + maximum_exploration_state = sweep_from_pool(placing_item=items_to_place[0]) has_beaten_game = world.has_beaten_game(maximum_exploration_state) for item_to_place in items_to_place: @@ -104,6 +105,8 @@ def fill_restrictive(world, base_state, locations, itempool, key_pool=None, sing spot_to_fill = None item_locations = filter_locations(item_to_place, locations, world, vanilla) + verify(item_to_place, item_locations, maximum_exploration_state, single_player_placement, + perform_access_check, key_pool, world) for location in item_locations: spot_to_fill = verify_spot_to_fill(location, item_to_place, maximum_exploration_state, single_player_placement, perform_access_check, key_pool, world) @@ -127,9 +130,6 @@ def fill_restrictive(world, base_state, locations, itempool, key_pool=None, sing raise FillError('No more spots to place %s' % item_to_place) world.push_item(spot_to_fill, item_to_place, False) - if item_to_place.smallkey: - with suppress(ValueError): - key_pool.remove(item_to_place) track_outside_keys(item_to_place, spot_to_fill, world) track_dungeon_items(item_to_place, spot_to_fill, world) locations.remove(spot_to_fill) @@ -142,25 +142,33 @@ def verify_spot_to_fill(location, item_to_place, max_exp_state, single_player_pl key_pool, world): if item_to_place.smallkey or item_to_place.bigkey: # a better test to see if a key can go there location.item = item_to_place + location.event = True + if item_to_place.smallkey: + with suppress(ValueError): + key_pool.remove(item_to_place) test_state = max_exp_state.copy() test_state.stale[item_to_place.player] = True else: test_state = max_exp_state if not single_player_placement or location.player == item_to_place.player: + test_state.sweep_for_events() if location.can_fill(test_state, item_to_place, perform_access_check): - if valid_key_placement(item_to_place, location, key_pool, world): + if valid_key_placement(item_to_place, location, key_pool, test_state, world): if item_to_place.crystal or valid_dungeon_placement(item_to_place, location, world): return location if item_to_place.smallkey or item_to_place.bigkey: location.item = None + location.event = False + if item_to_place.smallkey: + key_pool.append(item_to_place) return None -def valid_key_placement(item, location, key_pool, world): +def valid_key_placement(item, location, key_pool, collection_state, world): if not valid_reserved_placement(item, location, world): return False if ((not item.smallkey and not item.bigkey) or item.player != location.player - or world.retro[item.player] or world.logic[item.player] == 'nologic'): + or world.keyshuffle[item.player] == 'universal' or world.logic[item.player] == 'nologic'): return True dungeon = location.parent_region.dungeon if dungeon: @@ -172,8 +180,16 @@ def valid_key_placement(item, location, key_pool, world): if key_logic.prize_location: prize_loc = world.get_location(key_logic.prize_location, location.player) cr_count = world.crystals_needed_for_gt[location.player] - wild_keys = world.keyshuffle[item.player] - return key_logic.check_placement(unplaced_keys, wild_keys, location if item.bigkey else None, prize_loc, cr_count) + wild_keys = world.keyshuffle[item.player] != 'none' + if wild_keys: + reached_keys = {x for x in collection_state.locations_checked + if x.item and x.item.name == key_logic.small_key_name and x.item.player == item.player} + else: + reached_keys = set() # will be calculated using key logic in a moment + self_locking_keys = sum(1 for d, rule in key_logic.door_rules.items() if rule.allow_small + and rule.small_location.item and rule.small_location.item.name == key_logic.small_key_name) + return key_logic.check_placement(unplaced_keys, wild_keys, reached_keys, self_locking_keys, + location if item.bigkey else None, prize_loc, cr_count) else: return not item.is_inside_dungeon_item(world) @@ -204,6 +220,7 @@ def track_outside_keys(item, location, world): if loc_dungeon and loc_dungeon.name == item_dungeon: return # this is an inside key world.key_logic[item.player][item_dungeon].outside_keys += 1 + world.key_logic[item.player][item_dungeon].outside_keys_locations.add(location) def track_dungeon_items(item, location, world): @@ -216,7 +233,7 @@ def track_dungeon_items(item, location, world): def is_dungeon_item(item, world): - return ((item.smallkey and not world.keyshuffle[item.player]) + return ((item.smallkey and world.keyshuffle[item.player] == 'none') or (item.bigkey and not world.bigkeyshuffle[item.player]) or (item.compass and not world.compassshuffle[item.player]) or (item.map and not world.mapshuffle[item.player])) @@ -288,7 +305,7 @@ def last_ditch_placement(item_to_place, locations, world, state, base_state, ite possible_swaps = [x for x in state.locations_checked if x.item.type == 'Crystal'] else: possible_swaps = [x for x in state.locations_checked - if x.item.type not in ['Event', 'Crystal'] and not x.forced_item] + if x.item.type not in ['Event', 'Crystal'] and not x.forced_item and not x.locked] swap_locations = sorted(possible_swaps, key=location_preference) return try_possible_swaps(swap_locations, item_to_place, locations, world, base_state, itempool, key_pool, single_player_placement) @@ -344,7 +361,9 @@ def find_spot_for_item(item_to_place, locations, world, base_state, pool, test_state = maximum_exploration_state if (not single_player_placement or location.player == item_to_place.player) \ and location.can_fill(test_state, item_to_place, perform_access_check) \ - and valid_key_placement(item_to_place, location, pool if (keys_in_itempool and keys_in_itempool[item_to_place.player]) else world.itempool, world): + and valid_key_placement(item_to_place, location, + pool if (keys_in_itempool and keys_in_itempool[item_to_place.player]) else world.itempool, + test_state, world): return location if item_to_place.smallkey or item_to_place.bigkey: location.item = old_item @@ -385,6 +404,7 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None random.shuffle(fill_locations) random.shuffle(world.itempool) + config_sort(world) progitempool = [item for item in world.itempool if item.advancement] prioitempool = [item for item in world.itempool if not item.advancement and item.priority] restitempool = [item for item in world.itempool if not item.advancement and not item.priority] @@ -405,7 +425,7 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None else: max_trash = gt_count scaled_trash = math.floor(max_trash * scale_factor) - if world.goal[player] in ['triforcehunt', 'trinity']: + if world.goal[player] in ['triforcehunt', 'trinity', 'ganonhunt']: gftower_trash_count = random.randint(scaled_trash, max_trash) else: gftower_trash_count = random.randint(0, scaled_trash) @@ -424,9 +444,16 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None random.shuffle(fill_locations) fill_locations.reverse() - # Make sure the escape small key is placed first in standard with key shuffle to prevent running out of spots - # todo: crossed - progitempool.sort(key=lambda item: 1 if item.name == 'Small Key (Escape)' and world.keyshuffle[item.player] and world.mode[item.player] == 'standard' else 0) + # Make sure the escape keys ire placed first in standard to prevent running out of spots + def std_item_sort(item): + if world.mode[item.player] == 'standard': + if item.name == 'Small Key (Escape)': + return 1 + if item.name == 'Big Key (Escape)': + return 2 + return 0 + + progitempool.sort(key=std_item_sort) key_pool = [x for x in progitempool if x.smallkey] # sort maps and compasses to the back -- this may not be viable in equitable & ambrosia @@ -475,6 +502,20 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None ensure_good_pots(world) +def config_sort(world): + if world.item_pool_config.verify: + config_sort_helper(world, world.item_pool_config.verify) + elif world.item_pool_config.preferred: + config_sort_helper(world, world.item_pool_config.preferred) + + +def config_sort_helper(world, sort_dict): + pref = list(sort_dict.keys()) + pref_len = len(pref) + world.itempool.sort(key=lambda i: pref_len - pref.index((i.name, i.player)) + if (i.name, i.player) in sort_dict else 0) + + def calc_trash_locations(world, player): total_count, gt_count = 0, 0 for loc in world.get_locations(): @@ -490,14 +531,15 @@ def calc_trash_locations(world, player): def ensure_good_pots(world, write_skips=False): for loc in world.get_locations(): - # # convert Arrows 5 when necessary - # if (loc.item.name in {'Arrows (5)'} - # and loc.type not in [LocationType.Pot, LocationType.Bonk]): - # loc.item = ItemFactory(invalid_location_replacement[loc.item.name], loc.item.player) - # convert Nothing when necessary + if loc.item is None: + loc.item = ItemFactory('Nothing', loc.player) + # convert Arrows 5 and Nothing when necessary if (loc.item.name in {'Nothing'} and (loc.type != LocationType.Pot or loc.item.player != loc.player)): loc.item = ItemFactory(invalid_location_replacement[loc.item.name], loc.item.player) + if (loc.item.name in {'Arrows (5)'} + and (loc.type not in [LocationType.Pot, LocationType.Bonk] or loc.item.player != loc.player)): + loc.item = ItemFactory(invalid_location_replacement[loc.item.name], loc.item.player) # # can be placed here by multiworld balancing or shop balancing # # change it to something normal for the player it got swapped to # elif (loc.item.name in {'Chicken', 'Big Magic'} @@ -507,7 +549,7 @@ def ensure_good_pots(world, write_skips=False): # else: # loc.item = ItemFactory(invalid_location_replacement[loc.item.name], loc.player) # do the arrow retro check - if world.retro[loc.item.player] and loc.item.name in {'Arrows (5)', 'Arrows (10)'}: + if world.bow_mode[loc.item.player].startswith('retro') and loc.item.name in {'Arrows (5)', 'Arrows (10)'}: loc.item = ItemFactory('Rupees (5)', loc.item.player) # don't write out all pots to spoiler if write_skips: @@ -528,10 +570,17 @@ def fast_fill_helper(world, item_pool, fill_locations): def fast_fill(world, item_pool, fill_locations): - while item_pool and fill_locations: + config = world.item_pool_config + fast_pool = [x for x in item_pool if (x.name, x.player) not in config.restricted] + filtered_pool = [x for x in item_pool if (x.name, x.player) in config.restricted] + filtered_fill(world, filtered_pool, fill_locations) + while fast_pool and fill_locations: spot_to_fill = fill_locations.pop() - item_to_place = item_pool.pop() + item_to_place = fast_pool.pop() world.push_item(spot_to_fill, item_to_place, False) + item_pool.clear() + item_pool.extend(filtered_pool) + item_pool.extend(fast_pool) def fast_fill_pot_for_multiworld(world, item_pool, fill_locations): @@ -641,6 +690,40 @@ def sell_keys(world, player): world.itempool.remove(universal_key) +def verify(item_to_place, item_locations, state, spp, pac, key_pool, world): + if world.item_pool_config.verify: + logger = logging.getLogger('') + item_name = 'Bottle' if item_to_place.name.startswith('Bottle') else item_to_place.name + item_player = item_to_place.player + config = world.item_pool_config + if (item_name, item_player) in config.verify: + tests = config.verify[(item_name, item_player)] + issues = [] + for location in item_locations: + if location.name in tests: + expected = tests[location.name] + spot = verify_spot_to_fill(location, item_to_place, state, spp, pac, key_pool, world) + if spot and (item_to_place.smallkey or item_to_place.bigkey): + location.item = None + location.event = False + if item_to_place.smallkey: + key_pool.append(item_to_place) + if (expected and spot) or (not expected and spot is None): + logger.debug(f'Placing {item_name} ({item_player}) at {location.name} was {expected}') + config.verify_count += 1 + if config.verify_count >= config.verify_target: + exit() + else: + issues.append((item_name, item_player, location.name, expected)) + if len(issues) > 0: + for name, player, loc, expected in issues: + if expected: + logger.error(f'Could not place {name} ({player}) at {loc}') + else: + logger.error(f'{name} ({player}) should not be allowed at {loc}') + raise Exception(f'Test failed placing {name}') + + def balance_multiworld_progression(world): state = CollectionState(world) checked_locations = set() @@ -672,7 +755,7 @@ def balance_multiworld_progression(world): candidate_items = collections.defaultdict(set) while True: for location in balancing_sphere: - if location.event and (world.keyshuffle[location.item.player] or not location.item.smallkey) and (world.bigkeyshuffle[location.item.player] or not location.item.bigkey): + if location.event and (world.keyshuffle[location.item.player] != 'none' or not location.item.smallkey) and (world.bigkeyshuffle[location.item.player] or not location.item.bigkey): balancing_state.collect(location.item, True, location) player = location.item.player if player in balancing_players and not location.locked and location.player != player: @@ -747,7 +830,7 @@ def balance_multiworld_progression(world): sphere_locations.add(location) for location in sphere_locations: - if location.event and (world.keyshuffle[location.item.player] or not location.item.smallkey) and (world.bigkeyshuffle[location.item.player] or not location.item.bigkey): + if location.event and (world.keyshuffle[location.item.player] != 'none' or not location.item.smallkey) and (world.bigkeyshuffle[location.item.player] or not location.item.bigkey): state.collect(location.item, True, location) checked_locations |= sphere_locations @@ -823,7 +906,9 @@ def balance_money_progression(world): return True if item.name in ['Progressive Armor', 'Blue Mail', 'Red Mail']: return True - if world.retro[player] and (item.name in ['Single Arrow', 'Small Key (Universal)']): + if world.keyshuffle[player] == 'universal' and item.name == 'Small Key (Universal)': + return True + if world.bow_mode[player].startswith('retro') and item.name == 'Single Arrow': return True if location.name in pay_for_locations: return True @@ -963,31 +1048,85 @@ def balance_money_progression(world): wallet[location.item.player] += rupee_chart[location.item.name] def set_prize_drops(world, player): - prizes = [0xD8, 0xD8, 0xD8, 0xD8, 0xD9, 0xD8, 0xD8, 0xD9, 0xDA, 0xD9, 0xDA, 0xDB, 0xDA, 0xD9, 0xDA, 0xDA, 0xE0, 0xDF, 0xDF, 0xDA, 0xE0, 0xDF, 0xD8, 0xDF, - 0xDC, 0xDC, 0xDC, 0xDD, 0xDC, 0xDC, 0xDE, 0xDC, 0xE1, 0xD8, 0xE1, 0xE2, 0xE1, 0xD8, 0xE1, 0xE2, 0xDF, 0xD9, 0xD8, 0xE1, 0xDF, 0xDC, 0xD9, 0xD8, - 0xD8, 0xE3, 0xE0, 0xDB, 0xDE, 0xD8, 0xDB, 0xE2, 0xD9, 0xDA, 0xDB, 0xD9, 0xDB, 0xD9, 0xDB] - - # randomize last 7 slots - new_prizes = random.sample(prizes, 7) + prizes = [0xD8, 0xD8, 0xD8, 0xD8, 0xD9, 0xD8, 0xD8, 0xD9, + 0xDA, 0xD9, 0xDA, 0xDB, 0xDA, 0xD9, 0xDA, 0xDA, + 0xE0, 0xDF, 0xDF, 0xDA, 0xE0, 0xDF, 0xD8, 0xDF, + 0xDC, 0xDC, 0xDC, 0xDD, 0xDC, 0xDC, 0xDE, 0xDC, + 0xE1, 0xD8, 0xE1, 0xE2, 0xE1, 0xD8, 0xE1, 0xE2, + 0xDF, 0xD9, 0xD8, 0xE1, 0xDF, 0xDC, 0xD9, 0xD8, + 0xD8, 0xE3, 0xE0, 0xDB, 0xDE, 0xD8, 0xDB, 0xE2, + 0xD9, 0xDA, 0xDB, 0xD9, 0xDB, 0xD9, 0xDB] + dig_prizes = [0xB2, 0xD8, 0xD8, 0xD8, 0xD8, 0xD8, 0xD8, 0xD8, 0xD8, + 0xD9, 0xD9, 0xD9, 0xD9, 0xD9, 0xDA, 0xDA, 0xDA, 0xDA, 0xDA, + 0xDB, 0xDB, 0xDB, 0xDB, 0xDB, 0xDC, 0xDC, 0xDC, 0xDC, 0xDC, + 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDE, 0xDE, 0xDE, 0xDE, 0xDE, + 0xDF, 0xDF, 0xDF, 0xDF, 0xDF, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, + 0xE1, 0xE1, 0xE1, 0xE1, 0xE1, 0xE2, 0xE2, 0xE2, 0xE2, 0xE2, + 0xE3, 0xE3, 0xE3, 0xE3, 0xE3] + + def chunk(l,n): + return [l[i:i+n] for i in range(0, len(l), n)] + + possible_prizes = { + 'Small Heart': 0xD8, 'Fairy': 0xE3, + 'Rupee (1)': 0xD9, 'Rupees (5)': 0xDA, 'Rupees (20)': 0xDB, + 'Big Magic': 0xE0, 'Small Magic': 0xDF, + 'Single Bomb': 0xDC, 'Bombs (4)': 0xDD, + 'Bombs (8)': 0xDE, 'Arrows (5)': 0xE1, 'Arrows (10)': 0xE2 + } #weights, if desired 13, 1, 9, 7, 6, 3, 6, 7, 1, 2, 5, 3 + uniform_prizes = list(possible_prizes.values()) + prizes[-7:] = random.sample(prizes, 7) + + #shuffle order of 7 main packs + packs = chunk(prizes[:56], 8) + random.shuffle(packs) + prizes[:56] = [drop for pack in packs for drop in pack] + + if world.customizer: + drops = world.customizer.get_drops() + if drops: + for player, drop_config in drops.items(): + for pack_num in range(1, 8): + if f'Pack {pack_num}' in drop_config: + for prize, idx in enumerate(drop_config[f'Pack {pack_num}']): + chosen = random.choice(uniform_prizes) if prize == 'Random' else possible_prizes[prize] + prizes[(pack_num-1)*8 + idx] = chosen + for tree_pull_tier in range(1, 4): + if f'Tree Pull Tier {tree_pull_tier}' in drop_config: + prize = drop_config[f'Tree Pull Tier {tree_pull_tier}'] + chosen = random.choice(uniform_prizes) if prize == 'Random' else possible_prizes[prize] + prizes[63-tree_pull_tier] = chosen # (62 through 60 in reverse) + for key, pos in {'Crab Normal': 59, 'Crab Special': 58, 'Stun Prize': 57, 'Fish': 56}.items(): + if key in drop_config: + prize = drop_config[key] + chosen = random.choice(uniform_prizes) if prize == 'Random' else possible_prizes[prize] + prizes[pos] = chosen if world.difficulty_adjustments[player] in ['hard', 'expert']: prize_replacements = {0xE0: 0xDF, # Fairy -> heart 0xE3: 0xD8} # Big magic -> small magic - new_prizes = [prize_replacements.get(prize, prize) for prize in new_prizes] + prizes = [prize_replacements.get(prize, prize) for prize in prizes] + dig_prizes = [prize_replacements.get(prize, prize) for prize in dig_prizes] - if world.retro[player]: + if world.bow_mode[player].startswith('retro'): prize_replacements = {0xE1: 0xDA, #5 Arrows -> Blue Rupee 0xE2: 0xDB} #10 Arrows -> Red Rupee - new_prizes = [prize_replacements.get(prize, prize) for prize in new_prizes] + prizes = [prize_replacements.get(prize, prize) for prize in prizes] + dig_prizes = [prize_replacements.get(prize, prize) for prize in dig_prizes] # write tree pull prizes - world.prizes[player]['pull'] = [ new_prizes.pop(), new_prizes.pop(), new_prizes.pop() ] + world.prizes[player]['dig'] = dig_prizes + + # write tree pull prizes + world.prizes[player]['pull'] = [ prizes.pop(), prizes.pop(), prizes.pop() ] # rupee crab prizes - world.prizes[player]['crab'] = [ new_prizes.pop(), new_prizes.pop() ] + world.prizes[player]['crab'] = [ prizes.pop(), prizes.pop() ] # stunned enemy prize - world.prizes[player]['stun'] = new_prizes.pop() + world.prizes[player]['stun'] = prizes.pop() # saved fish prize - world.prizes[player]['fish'] = new_prizes.pop() \ No newline at end of file + world.prizes[player]['fish'] = prizes.pop() + + world.prizes[player]['enemies'] = prizes \ No newline at end of file diff --git a/Gui.py b/Gui.py index 948b2b4e..5ca88ac1 100755 --- a/Gui.py +++ b/Gui.py @@ -190,7 +190,7 @@ def guiMain(args=None): self.pages["startinventory"].content.pack(side=TOP, fill=BOTH, expand=True) # Custom Controls - self.pages["custom"].content = custom_page(self,self.pages["custom"]) + self.pages["custom"].content = custom_page(self, self.pages["custom"]) self.pages["custom"].content.pack(side=TOP, fill=BOTH, expand=True) def validation(P): diff --git a/InitialSram.py b/InitialSram.py index 0be80de2..d7d79a18 100644 --- a/InitialSram.py +++ b/InitialSram.py @@ -76,7 +76,7 @@ class InitialSram: if startingstate.has('Bow', player): equip[0x340] = 3 if startingstate.has('Silver Arrows', player) else 1 equip[0x38E] |= 0x20 # progressive flag to get the correct hint in all cases - if not world.retro[player]: + if not world.bow_mode[player].startswith('retro'): equip[0x38E] |= 0x80 if startingstate.has('Silver Arrows', player): equip[0x38E] |= 0x40 @@ -145,8 +145,9 @@ class InitialSram: 'Big Key (Misery Mire)': (0x367, 0x01), 'Compass (Misery Mire)': (0x365, 0x01), 'Map (Misery Mire)': (0x369, 0x01), 'Big Key (Turtle Rock)': (0x366, 0x08), 'Compass (Turtle Rock)': (0x364, 0x08), 'Map (Turtle Rock)': (0x368, 0x08), 'Big Key (Ganons Tower)': (0x366, 0x04), 'Compass (Ganons Tower)': (0x364, 0x04), 'Map (Ganons Tower)': (0x368, 0x04)} - set_or_table = {'Flippers': (0x356, 1, 0x379, 0x02),'Pegasus Boots': (0x355, 1, 0x379, 0x04), - 'Shovel': (0x34C, 1, 0x38C, 0x04), 'Ocarina': (0x34C, 3, 0x38C, 0x01), + set_or_table = {'Flippers': (0x356, 1, 0x379, 0x02), 'Pegasus Boots': (0x355, 1, 0x379, 0x04), + 'Shovel': (0x34C, 1, 0x38C, 0x04), 'Ocarina': (0x34C, 2, 0x38C, 0x02), + 'Ocarina (Activated)': (0x34C, 3, 0x38C, 0x01), 'Mushroom': (0x344, 1, 0x38C, 0x20 | 0x08), 'Magic Powder': (0x344, 2, 0x38C, 0x10), 'Blue Boomerang': (0x341, 1, 0x38C, 0x80), 'Red Boomerang': (0x341, 2, 0x38C, 0x40)} keys = {'Small Key (Eastern Palace)': [0x37E], 'Small Key (Desert Palace)': [0x37F], @@ -190,7 +191,7 @@ class InitialSram: elif item.name in bombs: starting_bombs += bombs[item.name] elif item.name in arrows: - if world.retro[player]: + if world.bow_mode[player].startswith('retro'): equip[0x38E] |= 0x80 starting_arrows = 1 else: diff --git a/ItemList.py b/ItemList.py index 26b20c03..9a0eec7b 100644 --- a/ItemList.py +++ b/ItemList.py @@ -1,4 +1,4 @@ -from collections import namedtuple +from collections import namedtuple, defaultdict import logging import math import RaceRandom as random @@ -6,7 +6,7 @@ import RaceRandom as random from BaseClasses import LocationType, Region, RegionType, Shop, ShopType, Location, CollectionState, PotItem from EntranceShuffle import connect_entrance from Regions import shop_to_location_table, retro_shops, shop_table_by_location, valid_pot_location -from Fill import FillError, fill_restrictive, fast_fill, get_dungeon_item_pool +from Fill import FillError, fill_restrictive, get_dungeon_item_pool, track_dungeon_items, track_outside_keys from PotShuffle import vanilla_pots from Tables import bonk_prize_lookup from Items import ItemFactory @@ -182,8 +182,12 @@ def get_custom_array_key(item): def generate_itempool(world, player): - if (world.difficulty[player] not in ['normal', 'hard', 'expert'] or world.goal[player] not in ['ganon', 'pedestal', 'dungeons', 'triforcehunt', 'trinity', 'crystals'] - or world.mode[player] not in ['open', 'standard', 'inverted'] or world.timer not in ['none', 'display', 'timed', 'timed-ohko', 'ohko', 'timed-countdown'] or world.progressive not in ['on', 'off', 'random']): + if (world.difficulty[player] not in ['normal', 'hard', 'expert'] + or world.goal[player] not in ['ganon', 'pedestal', 'dungeons', 'triforcehunt', 'trinity', 'crystals', + 'ganonhunt', 'completionist'] + or world.mode[player] not in ['open', 'standard', 'inverted'] + or world.timer not in ['none', 'display', 'timed', 'timed-ohko', 'ohko', 'timed-countdown'] + or world.progressive not in ['on', 'off', 'random']): raise NotImplementedError('Not supported yet') if world.timer in ['ohko', 'timed-ohko']: @@ -279,13 +283,17 @@ def generate_itempool(world, player): world.get_location('Zelda Drop Off', player).locked = True # set up item pool - if world.custom and player in world.customitemarray: - (pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_total, treasure_hunt_icon, lamps_needed_for_dark_rooms) = make_custom_item_pool(world.progressive, world.shuffle[player], world.difficulty[player], world.timer, world.goal[player], world.mode[player], world.swords[player], world.retro[player], world.bombbag[player], world.customitemarray[player]) + skip_pool_adjustments = False + if world.customizer and world.customizer.get_item_pool(): + (pool, placed_items, precollected_items, clock_mode, lamps_needed_for_dark_rooms) = make_customizer_pool(world, player) + skip_pool_adjustments = True + elif world.custom and player in world.customitemarray: + (pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_total, treasure_hunt_icon, lamps_needed_for_dark_rooms) = make_custom_item_pool(world, player, world.progressive, world.shuffle[player], world.difficulty[player], world.timer, world.goal[player], world.mode[player], world.swords[player], world.bombbag[player], world.customitemarray[player]) world.rupoor_cost = min(world.customitemarray[player]["rupoorcost"], 9999) else: - (pool, placed_items, precollected_items, clock_mode, lamps_needed_for_dark_rooms) = get_pool_core(world.progressive, world.shuffle[player], world.difficulty[player], world.treasure_hunt_total[player], world.timer, world.goal[player], world.mode[player], world.swords[player], world.retro[player], world.bombbag[player], world.doorShuffle[player], world.logic[player], world.is_tile_swapped(0x18, player)) + (pool, placed_items, precollected_items, clock_mode, lamps_needed_for_dark_rooms) = get_pool_core(world, player, world.progressive, world.shuffle[player], world.difficulty[player], world.treasure_hunt_total[player], world.timer, world.goal[player], world.mode[player], world.swords[player], world.bombbag[player], world.doorShuffle[player], world.logic[player], world.flute_mode[player] == 'active' or world.is_tile_swapped(0x18, player)) - if player in world.pool_adjustment.keys(): + if player in world.pool_adjustment.keys() and not skip_pool_adjustments: amt = world.pool_adjustment[player] if amt < 0: trash_options = [x for x in pool if x in trash_items] @@ -298,6 +306,7 @@ def generate_itempool(world, player): for _ in range(0, amt): pool.append('Rupees (20)') + start_inventory = list(world.precollected_items) for item in precollected_items: world.push_precollected(ItemFactory(item, player)) @@ -311,7 +320,8 @@ def generate_itempool(world, player): if not found_sword and world.swords[player] != 'swordless': found_sword = True possible_weapons.append(item) - if item in ['Progressive Bow', 'Bow'] and not found_bow and not world.retro[player]: + if (item in ['Progressive Bow', 'Bow'] and not found_bow + and not world.bow_mode[player].startswith('retro')): found_bow = True possible_weapons.append(item) if item in ['Hammer', 'Fire Rod', 'Cane of Somaria', 'Cane of Byrna']: @@ -331,7 +341,7 @@ def generate_itempool(world, player): world.get_location(location, player).event = True world.get_location(location, player).locked = True - if world.shopsanity[player]: + if world.shopsanity[player] and not skip_pool_adjustments: for shop in world.shops[player]: if shop.region.name in shop_to_location_table: for index, slot in enumerate(shop.inventory): @@ -359,13 +369,13 @@ def generate_itempool(world, player): world.clock_mode = clock_mode goal = world.goal[player] - if goal in ['triforcehunt', 'trinity']: + if goal in ['triforcehunt', 'trinity', 'ganonhunt']: g, t = set_default_triforce(goal, world.treasure_hunt_count[player], world.treasure_hunt_total[player]) world.treasure_hunt_count[player], world.treasure_hunt_total[player] = g, t world.treasure_hunt_icon[player] = 'Triforce Piece' world.itempool.extend([item for item in get_dungeon_item_pool(world) if item.player == player - and ((item.smallkey and world.keyshuffle[player]) + and ((item.smallkey and world.keyshuffle[player] != 'none') or (item.bigkey and world.bigkeyshuffle[player]) or (item.map and world.mapshuffle[player]) or (item.compass and world.compassshuffle[player]))]) @@ -374,7 +384,9 @@ def generate_itempool(world, player): # rather than making all hearts/heart pieces progression items (which slows down generation considerably) # We mark one random heart container as an advancement item (or 4 heart pieces in expert mode) if world.difficulty[player] in ['normal', 'hard'] and not (world.custom and player in world.customitemarray and world.customitemarray[player]["heartcontainer"] == 0): - next(item for item in items if item.name == 'Boss Heart Container').advancement = True + container = next((item for item in items if item.name == 'Boss Heart Container'), None) + if container: + container.advancement = True elif world.difficulty[player] in ['expert'] and not (world.custom and player in world.customitemarray and world.customitemarray[player]["heartpiece"] < 4): adv_heart_pieces = (item for item in items if item.name == 'Piece of Heart') for i in range(4): @@ -391,11 +403,33 @@ def generate_itempool(world, player): return item if not choice else ItemFactory("Bee Trap", player) if choice == 'trap' else ItemFactory("Bee", player) return item - world.itempool += [beemizer(item) for item in items] + if not skip_pool_adjustments: + world.itempool += [beemizer(item) for item in items] + else: + world.itempool += items # shuffle medallions - mm_medallion = ['Ether', 'Quake', 'Bombos'][random.randint(0, 2)] - tr_medallion = ['Ether', 'Quake', 'Bombos'][random.randint(0, 2)] + mm_medallion, tr_medallion = None, None + if world.customizer and world.customizer.get_medallions() and player in world.customizer.get_medallions(): + medal_map = world.customizer.get_medallions() + if player in medal_map: + custom_medallions = medal_map[player] + if 'Misery Mire' in custom_medallions: + mm_medallion = custom_medallions['Misery Mire'] + if isinstance(mm_medallion, dict): + mm_medallion = random.choices(list(mm_medallion.keys()), list(mm_medallion.values()), k=1)[0] + if mm_medallion == 'Random': + mm_medallion = None + if 'Turtle Rock' in custom_medallions: + tr_medallion = custom_medallions['Turtle Rock'] + if isinstance(tr_medallion, dict): + tr_medallion = random.choices(list(tr_medallion.keys()), list(tr_medallion.values()), k=1)[0] + if tr_medallion == 'Random': + tr_medallion = None + if not mm_medallion: + mm_medallion = ['Ether', 'Quake', 'Bombos'][random.randint(0, 2)] + if not tr_medallion: + tr_medallion = ['Ether', 'Quake', 'Bombos'][random.randint(0, 2)] world.required_medallions[player] = (mm_medallion, tr_medallion) # shuffle bottle refills @@ -409,22 +443,39 @@ def generate_itempool(world, player): set_up_shops(world, player) - if world.retro[player]: - set_up_take_anys(world, player) - if world.dropshuffle[player]: + if world.take_any[player] != 'none': + set_up_take_anys(world, player, skip_pool_adjustments) + if world.keyshuffle[player] == 'universal': + if world.dropshuffle[player] and not skip_pool_adjustments: world.itempool += [ItemFactory('Small Key (Universal)', player)] * 13 - if world.pottery[player] not in ['none', 'cave']: + if world.pottery[player] not in ['none', 'cave'] and not skip_pool_adjustments: world.itempool += [ItemFactory('Small Key (Universal)', player)] * 19 create_dynamic_shop_locations(world, player) - if world.pottery[player] not in ['none', 'keys']: + if world.pottery[player] not in ['none', 'keys'] and not skip_pool_adjustments: add_pot_contents(world, player) if world.shuffle_bonk_drops[player]: create_dynamic_bonkdrop_locations(world, player) add_bonkdrop_contents(world, player) + # modfiy based on start inventory, if any + modify_pool_for_start_inventory(start_inventory, world, player) + + # increase pool if not enough items + ttl_locations = sum(1 for x in world.get_unfilled_locations(player) if '- Prize' not in x.name) + pool_size = count_player_dungeon_item_pool(world, player) + pool_size += sum(1 for x in world.itempool if x.player == player) + + if pool_size < ttl_locations: + retro_bow = world.bow_mode[player].startswith('retro') + amount_to_add = ttl_locations - pool_size + filler_additions = random.choices(list(filler_items.keys()), filler_items.values(), k=amount_to_add) + for item in filler_additions: + item_name = 'Rupees (5)' if retro_bow and item == 'Arrows (10)' else item + world.itempool.append(ItemFactory(item_name, player)) + take_any_locations = [ 'Snitch Lady (East)', 'Snitch Lady (West)', 'Bush Covered House', 'Light World Bomb Hut', @@ -436,8 +487,12 @@ take_any_locations = [ 'Palace of Darkness Hint', 'East Dark World Hint', 'Archery Game', 'Dark Lake Hylia Ledge Hint', 'Dark Lake Hylia Ledge Spike Cave', 'Fortune Teller (Dark)', 'Dark Sanctuary Hint', 'Dark Desert Hint'] +fixed_take_anys = [ + 'Desert Healer Fairy', 'Swamp Healer Fairy', 'Dark Death Mountain Healer Fairy', + 'Dark Lake Hylia Ledge Healer Fairy', 'Bonk Fairy (Dark)'] -def set_up_take_anys(world, player): + +def set_up_take_anys(world, player, skip_adjustments=False): if world.mode[player] == 'inverted': if 'Dark Sanctuary Hint' in take_any_locations: take_any_locations.remove('Dark Sanctuary Hint') @@ -445,9 +500,12 @@ def set_up_take_anys(world, player): if 'Archery Game' in take_any_locations: take_any_locations.remove('Archery Game') - take_any_candidates = [x for x in take_any_locations if len(world.get_region(x, player).locations) == 0] - - regions = random.sample(take_any_candidates, 5) + if world.take_any[player] == 'random': + take_any_candidates = [x for x in take_any_locations if len(world.get_region(x, player).locations) == 0] + regions = random.sample(take_any_candidates, 5) + elif world.take_any[player] == 'fixed': + regions = list(fixed_take_anys) + random.shuffle(regions) old_man_take_any = Region("Old Man Sword Cave", RegionType.Cave, 'the sword cave', player) world.regions.append(old_man_take_any) @@ -462,12 +520,13 @@ def set_up_take_anys(world, player): sword = next((item for item in world.itempool if item.type == 'Sword' and item.player == player), None) if sword: - world.itempool.append(ItemFactory('Rupees (20)', player)) - if not world.shopsanity[player]: - world.itempool.remove(sword) + if not skip_adjustments: + world.itempool.append(ItemFactory('Rupees (20)', player)) + if not world.shopsanity[player]: + world.itempool.remove(sword) old_man_take_any.shop.add_inventory(0, sword.name, 0, 0, create_location=True) else: - if world.shopsanity[player]: + if world.shopsanity[player] and not skip_adjustments: world.itempool.append(ItemFactory('Rupees (300)', player)) old_man_take_any.shop.add_inventory(0, 'Rupees (300)', 0, 0, create_location=world.shopsanity[player]) @@ -485,7 +544,7 @@ def set_up_take_anys(world, player): world.shops[player].append(take_any.shop) take_any.shop.add_inventory(0, 'Blue Potion', 0, 0, create_location=world.shopsanity[player]) take_any.shop.add_inventory(1, 'Boss Heart Container', 0, 0, create_location=world.shopsanity[player]) - if world.shopsanity[player]: + if world.shopsanity[player] and not skip_adjustments: world.itempool.append(ItemFactory('Blue Potion', player)) world.itempool.append(ItemFactory('Boss Heart Container', player)) @@ -668,38 +727,47 @@ def fill_prizes(world, attempts=15): def set_up_shops(world, player): - if world.retro[player]: + retro_bow = world.bow_mode[player].startswith('retro') + universal_keys = world.keyshuffle[player] == 'universal' + if retro_bow or universal_keys: if world.shopsanity[player]: - removals = [next(item for item in world.itempool if item.name == 'Arrows (10)' and item.player == player)] - red_pots = [item for item in world.itempool if item.name == 'Red Potion' and item.player == player][:5] - shields_n_hearts = [item for item in world.itempool if item.name in ['Blue Shield', 'Small Heart'] and item.player == player] - removals.extend([item for item in world.itempool if item.name == 'Arrow Upgrade (+5)' and item.player == player]) - removals.extend(red_pots) - removals.extend(random.sample(shields_n_hearts, 5)) + removals = [] + if retro_bow: + removals = [next(item for item in world.itempool if item.name == 'Arrows (10)' and item.player == player)] + removals.extend([item for item in world.itempool if item.name == 'Arrow Upgrade (+5)' and item.player == player]) + shields_n_hearts = [item for item in world.itempool if item.name in ['Blue Shield', 'Small Heart'] and item.player == player] + removals.extend(random.sample(shields_n_hearts, 5)) + if universal_keys: + red_pots = [item for item in world.itempool if item.name == 'Red Potion' and item.player == player][:5] + removals.extend(red_pots) for remove in removals: world.itempool.remove(remove) - for i in range(6): # replace the Arrows (10) and randomly selected hearts/blue shield - arrow_item = ItemFactory('Single Arrow', player) - arrow_item.advancement = True - world.itempool.append(arrow_item) - for i in range(5): # replace the red potions - world.itempool.append(ItemFactory('Small Key (Universal)', player)) - world.itempool.append(ItemFactory('Rupees (50)', player)) # replaces the arrow upgrade + if retro_bow: + for i in range(6): # replace the Arrows (10) and randomly selected hearts/blue shield + arrow_item = ItemFactory('Single Arrow', player) + arrow_item.advancement = True + world.itempool.append(arrow_item) + world.itempool.append(ItemFactory('Rupees (50)', player)) # replaces the arrow upgrade + if universal_keys: + for i in range(5): # replace the red potions + world.itempool.append(ItemFactory('Small Key (Universal)', player)) # TODO: move hard+ mode changes for shields here, utilizing the new shops else: - rss = world.get_region('Red Shield Shop', player).shop - if not rss.locked: - rss.custom = True - rss.add_inventory(2, 'Single Arrow', 80) + if retro_bow: + rss = world.get_region('Red Shield Shop', player).shop + if not rss.locked: + rss.custom = True + rss.add_inventory(2, 'Single Arrow', 80) + rss.locked = True + cap_shop = world.get_region('Capacity Upgrade', player).shop + cap_shop.inventory[1] = None # remove arrow capacity upgrades in retro for shop in random.sample([s for s in world.shops[player] if not s.locked and s.region.player == player], 5): shop.custom = True shop.locked = True - shop.add_inventory(0, 'Single Arrow', 80) - shop.add_inventory(1, 'Small Key (Universal)', 100) - shop.add_inventory(2, 'Bombs (10)', 50) - rss.locked = True - cap_shop = world.get_region('Capacity Upgrade', player).shop - cap_shop.inventory[1] = None # remove arrow capacity upgrades in retro + if retro_bow: + shop.add_inventory(0, 'Single Arrow', 80) + if universal_keys: + shop.add_inventory(1, 'Small Key (Universal)', 100) if world.bombbag[player]: if world.shopsanity[player]: removals = [item for item in world.itempool if item.name == 'Bomb Upgrade (+5)' and item.player == player] @@ -712,10 +780,11 @@ def set_up_shops(world, player): def customize_shops(world, player): - found_bomb_upgrade, found_arrow_upgrade = False, world.retro[player] + retro_bow = world.bow_mode[player].startswith('retro') + found_bomb_upgrade, found_arrow_upgrade = False, retro_bow possible_replacements = [] shops_to_customize = shop_to_location_table.copy() - if world.retro[player]: + if world.take_any[player] != 'none': shops_to_customize.update(retro_shops) for shop_name, loc_list in shops_to_customize.items(): shop = world.get_region(shop_name, player).shop @@ -738,7 +807,7 @@ def customize_shops(world, player): price = 0 else: price = 120 if shop_name == 'Potion Shop' and item.name == 'Red Potion' else item.price - if world.retro[player] and item.name == 'Single Arrow': + if retro_bow and item.name == 'Single Arrow': price = 80 # randomize price shop.add_inventory(idx, item.name, randomize_price(price), max_repeat, player=item.player) @@ -765,7 +834,7 @@ def customize_shops(world, player): if not found_arrow_upgrade and len(possible_replacements) > 0: choices = [] for shop, idx, loc, item in possible_replacements: - if item.name == 'Arrows (10)' or (item.name == 'Single Arrow' and not world.retro[player]): + if item.name == 'Arrows (10)' or (item.name == 'Single Arrow' and not retro_bow): choices.append((shop, idx, loc, item)) if len(choices) > 0: shop, idx, loc, item = random.choice(choices) @@ -909,11 +978,12 @@ rupee_chart = {'Rupee (1)': 1, 'Rupees (5)': 5, 'Rupees (20)': 20, 'Rupees (50)' def add_pot_contents(world, player): + retro_bow = world.bow_mode[player].startswith('retro') for super_tile, pot_list in vanilla_pots.items(): for pot in pot_list: if pot.item not in [PotItem.Hole, PotItem.Key, PotItem.Switch]: if valid_pot_location(pot, world.pot_pool[player], world, player): - item = ('Rupees (5)' if world.retro[player] and pot_items[pot.item] == 'Arrows (5)' + item = ('Rupees (5)' if retro_bow and pot_items[pot.item] == 'Arrows (5)' else pot_items[pot.item]) world.itempool.append(ItemFactory(item, player)) @@ -929,13 +999,15 @@ def add_bonkdrop_contents(world, player): count -= 1 -def get_pool_core(progressive, shuffle, difficulty, treasure_hunt_total, timer, goal, mode, swords, retro, bombbag, door_shuffle, logic, flute_activated): +def get_pool_core(world, player, progressive, shuffle, difficulty, treasure_hunt_total, timer, goal, mode, swords, + bombbag, door_shuffle, logic, flute_activated): pool = [] placed_items = {} precollected_items = [] clock_mode = None - if treasure_hunt_total == 0 and goal in ['triforcehunt', 'trinity']: - treasure_hunt_total = 30 if goal == 'triforcehunt' else 10 + if goal in ['triforcehunt', 'trinity', 'ganonhunt']: + if treasure_hunt_total == 0: + treasure_hunt_total = 30 if goal in ['triforcehunt', 'ganonhunt'] else 10 # triforce pieces max out triforcepool = ['Triforce Piece'] * min(treasure_hunt_total, max_goal) @@ -965,12 +1037,8 @@ def get_pool_core(progressive, shuffle, difficulty, treasure_hunt_total, timer, lamps_needed_for_dark_rooms = 1 - # insanity shuffle doesn't have fake LW/DW logic so for now guaranteed Mirror and Moon Pearl at the start - if shuffle == 'insanity_legacy': - place_item('Link\'s House', 'Magic Mirror') - place_item('Sanctuary', 'Moon Pearl') - else: - pool.extend(['Magic Mirror', 'Moon Pearl']) + # old insanity shuffle didn't have fake LW/DW logic so this used to be conditional + pool.extend(['Magic Mirror', 'Moon Pearl']) if timer == 'display': clock_mode = 'stopwatch' @@ -1004,7 +1072,7 @@ def get_pool_core(progressive, shuffle, difficulty, treasure_hunt_total, timer, else: pool.extend(diff.basicarmor) - if want_progressives(): + if 'silvers' not in world.bow_mode[player]: pool.extend(['Progressive Bow'] * 2) elif swords != 'swordless': pool.extend(diff.basicbow) @@ -1042,7 +1110,7 @@ def get_pool_core(progressive, shuffle, difficulty, treasure_hunt_total, timer, elif timer == 'timed-ohko': pool.extend(diff.timedohko) clock_mode = 'countdown-ohko' - if goal in ['triforcehunt', 'trinity']: + if goal in ['triforcehunt', 'trinity', 'ganonhunt']: pool.extend(triforcepool) for extra in diff.extras: @@ -1052,13 +1120,14 @@ def get_pool_core(progressive, shuffle, difficulty, treasure_hunt_total, timer, if goal in ['pedestal', 'trinity'] and swords != 'vanilla': place_item('Master Sword Pedestal', 'Triforce') - if retro: - pool = [item.replace('Single Arrow','Rupees (5)') for item in pool] - pool = [item.replace('Arrows (10)','Rupees (5)') for item in pool] - pool = [item.replace('Arrow Upgrade (+5)','Rupees (5)') for item in pool] - pool = [item.replace('Arrow Upgrade (+10)','Rupees (5)') for item in pool] + if world.bow_mode[player].startswith('retro'): + pool = [item.replace('Single Arrow', 'Rupees (5)') for item in pool] + pool = [item.replace('Arrows (10)', 'Rupees (5)') for item in pool] + pool = [item.replace('Arrow Upgrade (+5)', 'Rupees (5)') for item in pool] + pool = [item.replace('Arrow Upgrade (+10)', 'Rupees (5)') for item in pool] + if world.keyshuffle[player] == 'universal': pool.extend(diff.retro) - if door_shuffle != 'vanilla': # door shuffle needs more keys for retro + if door_shuffle != 'vanilla': # door shuffle needs more keys for universal keys replace = 'Rupees (20)' if difficulty == 'normal' else 'Rupees (5)' indices = [i for i, x in enumerate(pool) if x == replace] for i in range(0, min(10, len(indices))): @@ -1074,7 +1143,70 @@ def get_pool_core(progressive, shuffle, difficulty, treasure_hunt_total, timer, return (pool, placed_items, precollected_items, clock_mode, lamps_needed_for_dark_rooms) -def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, swords, retro, bombbag, customitemarray): +item_alternates = { + # Bows + 'Progressive Bow (Alt)': ('Progressive Bow', 1), + 'Bow': ('Progressive Bow', 1), + 'Silver Arrows': ('Progressive Bow', 2), + # Gloves + 'Power Glove': ('Progressive Glove', 1), + 'Titans Mitts': ('Progressive Glove', 2), + # Swords + 'Sword and Shield': ('Progressive Sword', 1), # could find a way to also remove a shield, but mostly not impactful + 'Fighter Sword': ('Progressive Sword', 1), + 'Master Sword': ('Progressive Sword', 2), + 'Tempered Sword': ('Progressive Sword', 3), + 'Golden Sword': ('Progressive Sword', 4), + # Shields + 'Blue Shield': ('Progressive Shield', 1), + 'Red Shield': ('Progressive Shield', 2), + 'Mirror Shield': ('Progressive Shield', 3), + # Armors + 'Blue Mail': ('Progressive Armor', 1), + 'Red Mail': ('Progressive Armor', 2), + + 'Magic Upgrade (1/4)': ('Magic Upgrade (1/2)', 2), + 'Ocarina': ('Ocarina (Activated)', 1), + 'Ocarina (Activated)': ('Ocarina', 1), + 'Boss Heart Container': ('Sanctuary Heart Container', 1), + 'Sanctuary Heart Container': ('Boss Heart Container', 1), + 'Power Star': ('Triforce Piece', 1) +} + + +def modify_pool_for_start_inventory(start_inventory, world, player): + # skips custom item pools - these shouldn't be adjusted + if (world.customizer and world.customizer.get_item_pool()) or world.custom: + return + for item in start_inventory: + if item.player == player: + if item in world.itempool: + world.itempool.remove(item) + elif item.name in item_alternates: + alt = item_alternates[item.name] + i = alt[1] + while i > 0: + alt_item = ItemFactory([alt[0]], player)[0] + if alt_item in world.itempool: + world.itempool.remove(alt_item) + i = i-1 + elif 'Bottle' in item.name: + bottle_item = next((x for x in world.itempool if 'Bottle' in item.name and x.player == player), None) + if bottle_item is not None: + world.itempool.remove(bottle_item) + if item.dungeon: + d = world.get_dungeon(item.dungeon, item.player) + match = next((i for i in d.all_items if i.name == item.name), None) + if match: + if match.map or match.compass: + d.dungeon_items.remove(match) + elif match.smallkey: + d.small_keys.remove(match) + elif match.bigkey: + d.big_key.remove(match) + + +def make_custom_item_pool(world, player, progressive, shuffle, difficulty, timer, goal, mode, swords, bombbag, customitemarray): pool = [] placed_items = {} precollected_items = [] @@ -1099,7 +1231,7 @@ def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, s customitemarray["triforce"] = total_items_to_place # Triforce Pieces - if goal in ['triforcehunt', 'trinity']: + if goal in ['triforcehunt', 'trinity', 'ganonhunt']: g, t = set_default_triforce(goal, customitemarray["triforcepiecesgoal"], customitemarray["triforcepieces"]) customitemarray["triforcepiecesgoal"], customitemarray["triforcepieces"] = g, t @@ -1137,8 +1269,8 @@ def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, s treasure_hunt_count = max(min(customitemarray["triforcepiecesgoal"], max_goal), 1) treasure_hunt_icon = 'Triforce Piece' # Ensure game is always possible to complete here, force sufficient pieces if the player is unwilling. - if ((customitemarray["triforcepieces"] < treasure_hunt_count) and (goal in ['triforcehunt', 'trinity']) - and (customitemarray["triforce"] == 0)): + if ((customitemarray["triforcepieces"] < treasure_hunt_count) + and (goal in ['triforcehunt', 'trinity', 'ganonhunt']) and (customitemarray["triforce"] == 0)): extrapieces = treasure_hunt_count - customitemarray["triforcepieces"] pool.extend(['Triforce Piece'] * extrapieces) itemtotal = itemtotal + extrapieces @@ -1155,7 +1287,7 @@ def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, s itemtotal = itemtotal + 1 if mode == 'standard': - if retro: + if world.keyshuffle[player] == 'universal': key_location = random.choice(['Secret Passage', 'Hyrule Castle - Boomerang Chest', 'Hyrule Castle - Map Chest', 'Hyrule Castle - Zelda\'s Chest', 'Sewers - Dark Cross']) place_item(key_location, 'Small Key (Universal)') pool.extend(['Small Key (Universal)'] * max((customitemarray["generickeys"] - 1), 0)) @@ -1166,23 +1298,32 @@ def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, s pool.extend(['Fighter Sword'] * customitemarray["sword1"]) pool.extend(['Progressive Sword'] * customitemarray["progressivesword"]) + pool.extend(['Magic Mirror'] * customitemarray["mirror"]) + pool.extend(['Moon Pearl'] * customitemarray["pearl"]) - if shuffle == 'insanity_legacy': - place_item('Link\'s House', 'Magic Mirror') - place_item('Sanctuary', 'Moon Pearl') - pool.extend(['Magic Mirror'] * max((customitemarray["mirror"] -1 ), 0)) - pool.extend(['Moon Pearl'] * max((customitemarray["pearl"] - 1), 0)) - else: - pool.extend(['Magic Mirror'] * customitemarray["mirror"]) - pool.extend(['Moon Pearl'] * customitemarray["pearl"]) - - if retro: + if world.keyshuffle[player] == 'universal': itemtotal = itemtotal - 28 # Corrects for small keys not being in item pool in Retro Mode if itemtotal < total_items_to_place: nothings = total_items_to_place - itemtotal # print("Placing " + str(nothings) + " Nothings") pool.extend(['Nothing'] * nothings) + start_inventory = [x for x in world.precollected_items if x.player == player] + if not start_inventory: + if world.logic[player] in ['owglitches', 'nologic']: + precollected_items.append('Pegasus Boots') + if 'Pegasus Boots' in pool: + pool.remove('Pegasus Boots') + pool.append('Rupees (20)') + if world.swords[player] == 'assured': + precollected_items.append('Progressive Sword') + if 'Progressive Sword' in pool: + pool.remove('Progressive Sword') + pool.append('Rupees (50)') + elif 'Fighter Sword' in pool: + pool.remove('Fighter Sword') + pool.append('Rupees (50)') + return (pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_total, treasure_hunt_icon, lamps_needed_for_dark_rooms) def set_default_triforce(goal, custom_goal, custom_total): @@ -1208,12 +1349,62 @@ def make_customizer_pool(world, player): assert loc not in placed_items placed_items[loc] = item + dungeon_locations, dungeon_count = defaultdict(set), defaultdict(int) + for l in world.get_unfilled_locations(player): + if l.parent_region.dungeon: + dungeon = l.parent_region.dungeon + dungeon_locations[dungeon.name].add(l) + if dungeon.name not in dungeon_count: + d_count = 1 if dungeon.big_key else 0 + d_count += len(dungeon.small_keys) + len(dungeon.dungeon_items) + dungeon_count[dungeon.name] = d_count + diff = difficulties[world.difficulty[player]] for item_name, amount in world.customizer.get_item_pool()[player].items(): if isinstance(amount, int): if item_name == 'Bottle (Random)': for _ in range(amount): pool.append(random.choice(diff.bottles)) + elif item_name.startswith('Small Key') and item_name != 'Small Key (Universal)': + d_item = ItemFactory(item_name, player) + if world.keyshuffle[player] == 'none': + d_name = d_item.dungeon + dungeon = world.get_dungeon(d_name, player) + target_amount = max(amount, len(dungeon.small_keys)) + additional_amount = target_amount - len(dungeon.small_keys) + possible_fit = min(additional_amount, len(dungeon_locations[d_name])-dungeon_count[d_name]) + if possible_fit > 0: + dungeon_count[d_name] += possible_fit + dungeon.small_keys.extend([d_item] * amount) + additional_amount -= possible_fit + if additional_amount > 0: + pool.extend([item_name] * amount) + else: + dungeon = world.get_dungeon(d_item.dungeon, player) + target_amount = max(amount, len(dungeon.small_keys)) + additional_amount = target_amount - len(dungeon.small_keys) + dungeon.small_keys.extend([d_item] * additional_amount) + elif item_name.startswith('Big Key') or item_name.startswith('Map') or item_name.startswith('Compass'): + d_item = ItemFactory(item_name, player) + if ((d_item.bigkey and not world.bigkeyshuffle[player]) + or (d_item.compass and not world.compassshuffle[player]) + or (d_item.map and not world.mapshuffle[player])): + d_name = d_item.dungeon + dungeon = world.get_dungeon(d_name, player) + current_amount = 1 if d_item == dungeon.big_key or d_item in dungeon.dungeon_items else 0 + additional_amount = amount - current_amount + possible_fit = min(additional_amount, len(dungeon_locations[d_name])-dungeon_count[d_name]) + if possible_fit > 0: + dungeon_count[d_name] += possible_fit + dungeon.dungeon_items.extend([d_item] * amount) + additional_amount -= possible_fit + if additional_amount > 0: + pool.extend([item_name] * amount) + else: + dungeon = world.get_dungeon(d_item.dungeon, player) + current_amount = 1 if d_item == dungeon.big_key or d_item in dungeon.dungeon_items else 0 + additional_amount = amount - current_amount + dungeon.dungeon_items.extend([d_item] * additional_amount) else: pool.extend([item_name] * amount) @@ -1225,16 +1416,85 @@ def make_customizer_pool(world, player): elif timer == 'ohko': clock_mode = 'ohko' - if world.goal[player] == 'pedestal': + if world.goal[player] in ['pedestal', 'trinity']: place_item('Master Sword Pedestal', 'Triforce') + guaranteed_items = alwaysitems + ['Magic Mirror', 'Moon Pearl'] + missing_items = [] + if world.shopsanity[player]: + guaranteed_items.extend(['Blue Potion', 'Green Potion', 'Red Potion']) + if world.keyshuffle[player] == 'universal': + guaranteed_items.append('Small Key (Universal)') + for item in guaranteed_items: + if item not in pool: + missing_items.append(item) + + glove_count = sum(1 for i in pool if i == 'Progressive Glove') + glove_count = 2 if next((i for i in pool if i == 'Titans Glove'), None) is not None else glove_count + for i in range(glove_count, 2): + missing_items.append('Progressive Glove') + + if world.bombbag[player]: + if 'Bomb Upgrade (+10)' not in pool: + missing_items.append('Bomb Upgrade (+10)') + + if world.swords[player] != 'swordless': + beam_swords = {'Master Sword', 'Tempered Sword', 'Golden Sword'} + sword_count = sum(1 for i in pool if i in 'Progressive Sword') + sword_count = 2 if next((i for i in pool if i in beam_swords), None) is not None else sword_count + for i in range(sword_count, 2): + missing_items.append('Progressive Sword') + + bow_found = next((i for i in pool if i in {'Bow', 'Progressive Bow'}), None) + if not bow_found: + missing_items.append('Progressive Bow') + logging.getLogger('').warning(f'The following items are not in the custom item pool {", ".join(missing_items)}') + + g, t = set_default_triforce(world.goal[player], world.treasure_hunt_count[player], + world.treasure_hunt_total[player]) + if t != 0: + pieces = sum(1 for i in pool if i == 'Triforce Piece') + if pieces < t: + pool.extend(['Triforce Piece'] * (t - pieces)) + + if not world.customizer.get_start_inventory(): + if world.logic[player] in ['owglitches', 'nologic']: + precollected_items.append('Pegasus Boots') + if 'Pegasus Boots' in pool: + pool.remove('Pegasus Boots') + pool.append('Rupees (20)') + if world.swords[player] == 'assured': + precollected_items.append('Progressive Sword') + if 'Progressive Sword' in pool: + pool.remove('Progressive Sword') + pool.append('Rupees (50)') + elif 'Fighter Sword' in pool: + pool.remove('Fighter Sword') + pool.append('Rupees (50)') + return pool, placed_items, precollected_items, clock_mode, 1 +filler_items = { + 'Arrows (10)': 12, + 'Bombs (3)': 16, + 'Rupees (300)': 5, + 'Rupees (100)': 1, + 'Rupees (50)': 7, + 'Rupees (20)': 28, + 'Rupees (5)': 4, +} + + +def count_player_dungeon_item_pool(world, player): + return sum(1 for dungeon in world.dungeons for item in dungeon.all_items + if dungeon.player == player and item.location is None and is_dungeon_item(item.name, world, player)) + + # location pool doesn't support larger values at this time def set_default_triforce(goal, custom_goal, custom_total): triforce_goal, triforce_total = 0, 0 - if goal == 'triforcehunt': + if goal in ['triforcehunt', 'ganonhunt']: triforce_goal, triforce_total = 20, 30 elif goal == 'trinity': triforce_goal, triforce_total = 8, 10 @@ -1246,31 +1506,128 @@ def set_default_triforce(goal, custom_goal, custom_total): # A quick test to ensure all combinations generate the correct amount of items. -def test(): - for difficulty in ['normal', 'hard', 'expert']: - for goal in ['ganon', 'triforcehunt', 'pedestal', 'trinity']: - for timer in ['none', 'display', 'timed', 'timed-ohko', 'ohko', 'timed-countdown']: - for mode in ['open', 'standard', 'inverted', 'retro']: - for swords in ['random', 'assured', 'swordless', 'vanilla']: - for progressive in ['on', 'off']: - for shuffle in ['vanilla', 'full', 'crossed', 'insanity']: - for logic in ['noglitches', 'minorglitches', 'owglitches', 'nologic']: - for retro in [True, False]: - for door_shuffle in ['basic', 'crossed', 'vanilla']: - for bombbag in [True, False]: - out = get_pool_core(progressive, shuffle, difficulty, 30, timer, goal, mode, swords, retro, bombbag, door_shuffle, logic, False) - count = len(out[0]) + len(out[1]) +# def test(): +# for difficulty in ['normal', 'hard', 'expert']: +# for goal in ['ganon', 'triforcehunt', 'pedestal', 'trinity']: +# for timer in ['none', 'display', 'timed', 'timed-ohko', 'ohko', 'timed-countdown']: +# for mode in ['open', 'standard', 'inverted', 'retro']: +# for swords in ['random', 'assured', 'swordless', 'vanilla']: +# for progressive in ['on', 'off']: +# for shuffle in ['vanilla', 'full', 'crossed', 'insanity']: +# for logic in ['noglitches', 'minorglitches', 'owglitches', 'nologic']: +# for retro in [True, False]: +# for door_shuffle in ['basic', 'crossed', 'vanilla']: +# for bombbag in [True, False]: +# out = get_pool_core(progressive, shuffle, difficulty, 30, timer, goal, mode, swords, retro, bombbag, door_shuffle, logic, False) +# count = len(out[0]) + len(out[1]) - correct_count = total_items_to_place - if goal in ['pedestal', 'trinity'] and swords != 'vanilla': - # pedestal goals generate one extra item - correct_count += 1 - if retro: - correct_count += 28 - try: - assert count == correct_count, "expected {0} items but found {1} items for {2}".format(correct_count, count, (progressive, shuffle, difficulty, timer, goal, mode, swords, retro, bombbag)) - except AssertionError as e: - print(e) -if __name__ == '__main__': - test() +def fill_specific_items(world): + if world.customizer: + placements = world.customizer.get_placements() + dungeon_pool = get_dungeon_item_pool(world) + prize_pool = [] + prize_set = {'Red Pendant', 'Blue Pendant', 'Green Pendant', 'Crystal 1', 'Crystal 2', + 'Crystal 3', 'Crystal 4', 'Crystal 7', 'Crystal 5', 'Crystal 6'} + for p in range(1, world.players + 1): + prize_pool.extend(prize_set) + if placements: + for player, placement_list in placements.items(): + for location, item in placement_list.items(): + loc = world.get_location(location, player) + item_to_place, event_flag = get_item_and_event_flag(item, world, player, + dungeon_pool, prize_set, prize_pool) + if item_to_place: + world.push_item(loc, item_to_place, False) + track_outside_keys(item_to_place, loc, world) + track_dungeon_items(item_to_place, loc, world) + loc.event = (event_flag or item_to_place.advancement + or item_to_place.bigkey or item_to_place.smallkey) + advanced_placements = world.customizer.get_advanced_placements() + if advanced_placements: + for player, placement_list in advanced_placements.items(): + for placement in placement_list: + if placement['type'] == 'LocationGroup': + item = placement['item'] + item_to_place, event_flag = get_item_and_event_flag(item, world, player, + dungeon_pool, prize_set, prize_pool) + if not item_to_place: + continue + locations = placement['locations'] + handled = False + while not handled: + if isinstance(locations, dict): + chosen_loc = random.choices(list(locations.keys()), list(locations.values()), k=1)[0] + else: # if isinstance(locations, list): + chosen_loc = random.choice(locations) + if chosen_loc == 'Random': + if is_dungeon_item(item_to_place.name, world, item_to_place.player): + dungeon_pool.append(item_to_place) + elif item_to_place.name in prize_set: + prize_pool.append(item_to_place.name) + else: + world.itempool.append(item_to_place) + else: + loc = world.get_location(chosen_loc, player) + if loc.item: + continue + world.push_item(loc, item_to_place, False) + track_outside_keys(item_to_place, loc, world) + track_dungeon_items(item_to_place, loc, world) + loc.event = (event_flag or item_to_place.advancement + or item_to_place.bigkey or item_to_place.smallkey) + handled = True + elif placement['type'] == 'NotLocationGroup': + item = placement['item'] + item_parts = item.split('#') + item_player = player if len(item_parts) < 2 else int(item_parts[1]) + item_name = item_parts[0] + world.item_pool_config.restricted[(item_name, item_player)] = placement['locations'] + elif placement['type'] == 'PreferredLocationGroup': + item = placement['item'] + item_parts = item.split('#') + item_player = player if len(item_parts) < 2 else int(item_parts[1]) + item_name = item_parts[0] + world.item_pool_config.preferred[(item_name, item_player)] = placement['locations'] + elif placement['type'] == 'Verification': + item = placement['item'] + item_parts = item.split('#') + item_player = player if len(item_parts) < 2 else int(item_parts[1]) + item_name = item_parts[0] + world.item_pool_config.verify[(item_name, item_player)] = placement['locations'] + world.item_pool_config.verify_target += len(placement['locations']) + + +def get_item_and_event_flag(item, world, player, dungeon_pool, prize_set, prize_pool): + item_parts = item.split('#') + item_player = player if len(item_parts) < 2 else int(item_parts[1]) + item_name = item_parts[0] + event_flag = False + if is_dungeon_item(item_name, world, item_player): + item_to_place = next(x for x in dungeon_pool + if x.name == item_name and x.player == item_player) + dungeon_pool.remove(item_to_place) + event_flag = True + elif item_name in prize_set: + item_player = player # prizes must be for that player + item_to_place = ItemFactory(item_name, item_player) + prize_pool.remove(item_name) + event_flag = True + else: + matcher = lambda x: x.name == item_name and x.player == item_player + if item_name == 'Bottle': + matcher = lambda x: x.name.startswith(item_name) and x.player == item_player + item_to_place = next((x for x in world.itempool if matcher(x)), None) + if item_to_place is None: + return None, event_flag + else: + world.itempool.remove(item_to_place) + return item_to_place, event_flag + + +def is_dungeon_item(item, world, player): + return ((item.startswith('Small Key') and world.keyshuffle[player] == 'none') + or (item.startswith('Big Key') and not world.bigkeyshuffle[player]) + or (item.startswith('Compass') and not world.compassshuffle[player]) + or (item.startswith('Map') and not world.mapshuffle[player])) + diff --git a/Items.py b/Items.py index 6f37f059..6bde89ef 100644 --- a/Items.py +++ b/Items.py @@ -14,8 +14,7 @@ def ItemFactory(items, player): advancement, priority, type, code, price, pedestal_hint, pedestal_credit, sickkid_credit, zora_credit, witch_credit, fluteboy_credit, hint_text = item_table[item] ret.append(Item(item, advancement, priority, type, code, price, pedestal_hint, pedestal_credit, sickkid_credit, zora_credit, witch_credit, fluteboy_credit, hint_text, player)) else: - logging.getLogger('').warning('Unknown Item: %s', item) - return None + raise RuntimeError(f'Unknown Item: {item}') if singleton: return ret[0] diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index fc250852..2cbda4be 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -5,7 +5,7 @@ from collections import defaultdict, deque from BaseClasses import DoorType, dungeon_keys, KeyRuleType, RegionType from Regions import dungeon_events from Dungeons import dungeon_keys, dungeon_bigs, dungeon_table -from DungeonGenerator import ExplorationState, special_big_key_doors, count_locations_exclude_big_chest, prize_or_event +from DungeonGenerator import ExplorationState, get_special_big_key_doors, count_locations_exclude_big_chest, prize_or_event from DungeonGenerator import reserved_location, blind_boss_unavail @@ -14,6 +14,7 @@ class KeyLayout(object): def __init__(self, sector, starts, proposal): self.sector = sector self.start_regions = starts + self.event_starts = [] self.proposal = proposal self.key_logic = KeyLogic(sector.name) @@ -58,13 +59,16 @@ class KeyLogic(object): self.placement_rules = [] self.location_rules = {} self.outside_keys = 0 + self.outside_keys_locations = set() self.dungeon = dungeon_name self.sm_doors = {} self.prize_location = None - def check_placement(self, unplaced_keys, wild_keys, big_key_loc=None, prize_loc=None, cr_count=7): + def check_placement(self, unplaced_keys, wild_keys, reached_keys, self_locking_keys, + big_key_loc=None, prize_loc=None, cr_count=7): for rule in self.placement_rules: - if not rule.is_satisfiable(self.outside_keys, wild_keys, unplaced_keys, big_key_loc, prize_loc, cr_count): + if not rule.is_satisfiable(self.outside_keys_locations, wild_keys, reached_keys, self_locking_keys, + unplaced_keys, big_key_loc, prize_loc, cr_count): return False if big_key_loc: for rule_a, rule_b in itertools.combinations(self.placement_rules, 2): @@ -158,7 +162,8 @@ class PlacementRule(object): left -= rule_needed return False - def is_satisfiable(self, outside_keys, wild_keys, unplaced_keys, big_key_loc, prize_location, cr_count): + def is_satisfiable(self, outside_keys_locations, wild_keys, reached_keys, self_locking_keys, unplaced_keys, + big_key_loc, prize_location, cr_count): if self.prize_relevance and prize_location: if self.prize_relevance == 'BigBomb': if prize_location.item.name not in ['Crystal 5', 'Crystal 6']: @@ -185,10 +190,11 @@ class PlacementRule(object): check_locations = self.check_locations_wo_bk if bk_blocked else self.check_locations_w_bk if not bk_blocked and check_locations is None: return True - available_keys = outside_keys + available_keys = len(outside_keys_locations) # todo: sometimes we need an extra empty chest to accomodate the big key too # dungeon bias seed 563518200 for example threshold = self.needed_keys_wo_bk if bk_blocked else self.needed_keys_w_bk + threshold -= self_locking_keys if not wild_keys: empty_chests = 0 for loc in check_locations: @@ -199,7 +205,8 @@ class PlacementRule(object): place_able_keys = min(empty_chests, unplaced_keys) available_keys += place_able_keys else: - available_keys += unplaced_keys + available_keys += len(reached_keys.difference(outside_keys_locations)) # already placed small keys + available_keys += unplaced_keys # small keys not yet placed return available_keys >= threshold @@ -223,13 +230,14 @@ class KeyCounter(object): return max(self.used_keys + reserve - len(self.key_only_locations), 0) -def build_key_layout(builder, start_regions, proposal, world, player): +def build_key_layout(builder, start_regions, proposal, event_starts, world, player): key_layout = KeyLayout(builder.master_sector, start_regions, proposal) key_layout.flat_prop = flatten_pair_list(key_layout.proposal) 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 = check_bk_special(key_layout.sector.region_set(), world, player) key_layout.all_locations = find_all_locations(key_layout.sector) + key_layout.event_starts = list(event_starts.keys()) return key_layout @@ -251,7 +259,7 @@ def find_all_locations(sector): def calc_max_chests(builder, key_layout, world, player): - if world.doorShuffle[player] != 'crossed': + if world.doorShuffle[player] in ['basic', 'vanilla']: return len(world.get_dungeon(key_layout.sector.name, player).small_keys) return max(0, builder.key_doors_num - key_layout.max_drops) @@ -275,7 +283,7 @@ def analyze_dungeon(key_layout, world, player): key_logic.bk_chests.update(find_big_chest_locations(key_layout.all_chest_locations)) key_logic.bk_chests.update(find_big_key_locked_locations(key_layout.all_chest_locations)) key_logic.prize_location = dungeon_table[key_layout.sector.name].prize - if world.retro[player] and world.mode[player] != 'standard': + if world.keyshuffle[player] == 'universal' and world.mode[player] != 'standard': return original_key_counter = find_counter({}, False, key_layout, False) @@ -301,10 +309,10 @@ def analyze_dungeon(key_layout, world, player): key_logic.bk_restricted.update(filter_big_chest(key_counter.free_locations)) # note to self: this is due to the enough_small_locations function in validate_key_layout_sub_loop # I don't like this exception here or there - elif available < possible_smalls and avail_bigs and non_big_locs > 0: - max_ctr = find_max_counter(key_layout) - bk_lockdown = [x for x in max_ctr.free_locations if x not in key_counter.free_locations] - key_logic.bk_restricted.update(filter_big_chest(bk_lockdown)) + # elif available < possible_smalls and avail_bigs and non_big_locs > 0: + # max_ctr = find_max_counter(key_layout) + # bk_lockdown = [x for x in max_ctr.free_locations if x not in key_counter.free_locations] + # key_logic.bk_restricted.update(filter_big_chest(bk_lockdown)) # try to relax the rules here? - smallest requirement that doesn't force a softlock child_queue = deque() for child in key_counter.child_doors.keys(): @@ -926,7 +934,7 @@ def self_lock_possible(counter): def available_chest_small_keys(key_counter, world, player): - if not world.keyshuffle[player] and not world.retro[player]: + if world.keyshuffle[player] == 'none': cnt = 0 for loc in key_counter.free_locations: if key_counter.big_key_opened or '- Big Chest' not in loc.name: @@ -937,7 +945,7 @@ def available_chest_small_keys(key_counter, world, player): def available_chest_small_keys_logic(key_counter, world, player, sm_restricted): - if not world.keyshuffle[player] and not world.retro[player]: + if world.keyshuffle[player] == 'none': cnt = 0 for loc in key_counter.free_locations: if loc not in sm_restricted and (key_counter.big_key_opened or '- Big Chest' not in loc.name): @@ -1000,7 +1008,7 @@ def find_worst_counter_wo_bk(small_key_num, accessible_set, door, odd_ctr, key_c def open_a_door(door, child_state, flat_proposal, world, player): - if door.bigKey or door.name in special_big_key_doors: + if door.bigKey or door.name in get_special_big_key_doors(world, player): child_state.big_key_opened = True child_state.avail_doors.extend(child_state.big_doors) child_state.opened_doors.extend(set([d.door for d in child_state.big_doors])) @@ -1172,6 +1180,16 @@ def expand_key_state(state, flat_proposal, world, player): state.add_all_doors_check_keys(connect_region, flat_proposal, world, player) +def expand_big_key_state(state, flat_proposal, world, player): + while len(state.avail_doors) > 0: + exp_door = state.next_avail_door() + door = exp_door.door + connect_region = world.get_entrance(door.name, player).connected_region + if state.validate(door, connect_region, world, player): + state.visit_region(connect_region, key_checks=True) + state.add_all_doors_check_big_keys(connect_region, flat_proposal, world, player) + + def flatten_pair_list(paired_list): flat_list = [] for d in paired_list: @@ -1401,13 +1419,51 @@ def prize_relevance(key_layout, dungeon_entrance, is_atgt_swapped): return None +def prize_relevance_sig2(start_regions, d_name, dungeon_entrance, is_atgt_swapped): + if len(start_regions) > 1 and dungeon_entrance and dungeon_table[d_name].prize: + if dungeon_entrance.name == ('Agahmins Tower' if is_atgt_swapped else 'Ganons Tower'): + return 'GT' + elif dungeon_entrance.name == 'Pyramid Fairy': + return 'BigBomb' + return None + + +def validate_bk_layout(proposal, builder, start_regions, world, player): + bk_special = check_bk_special(builder.master_sector.regions, world, player) + if world.bigkeyshuffle[player] and (world.dropshuffle[player] or not bk_special): + return True + flat_proposal = flatten_pair_list(proposal) + state = ExplorationState(dungeon=builder.name) + state.big_key_special = bk_special + for region in start_regions: + dungeon_entrance, portal_door = find_outside_connection(region) + prize_relevant_flag = prize_relevance_sig2(start_regions, builder.name, dungeon_entrance, world.is_atgt_swapped(player)) + if prize_relevant_flag: + state.append_door_to_list(portal_door, state.prize_doors) + state.prize_door_set[portal_door] = dungeon_entrance + # key_layout.prize_relevant = prize_relevant_flag + else: + state.visit_region(region, key_checks=True) + state.add_all_doors_check_big_keys(region, flat_proposal, world, player) + expand_big_key_state(state, flat_proposal, world, player) + if bk_special: + for loc in state.found_locations: + if loc.forced_big_key(): + return True + else: + return state.count_locations_exclude_specials(world, player) > 0 + return False + + # Soft lock stuff def validate_key_layout(key_layout, world, player): - # retro is all good - except for hyrule castle in standard mode - if (world.retro[player] and (world.mode[player] != 'standard' or key_layout.sector.name != 'Hyrule Castle')) or world.logic[player] == 'nologic': + # universal key is all good - except for hyrule castle in standard mode + if world.logic[player] == 'nologic' or (world.keyshuffle[player] == 'universal' and + (world.mode[player] != 'standard' or key_layout.sector.name != 'Hyrule Castle')): return True flat_proposal = key_layout.flat_prop state = ExplorationState(dungeon=key_layout.sector.name) + state.init_zelda_event_doors(key_layout.event_starts, player) state.key_locations = key_layout.max_chests state.big_key_special = check_bk_special(key_layout.sector.regions, world, player) for region in key_layout.start_regions: @@ -1435,14 +1491,16 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa else: ttl_locations = count_locations_exclude_big_chest(state.found_locations, world, player) ttl_small_key_only = count_small_key_only_locations(state) - available_small_locations = cnt_avail_small_locations(ttl_locations, ttl_small_key_only, state, world, player) + available_small_locations = cnt_avail_small_locations(ttl_locations, ttl_small_key_only, state, + key_layout, world, player) available_big_locations = cnt_avail_big_locations(ttl_locations, state, world, player) if invalid_self_locking_key(key_layout, state, prev_state, prev_avail, world, player): return False # todo: allow more key shuffles - refine placement rules # if (not smalls_avail or available_small_locations == 0) and (state.big_key_opened or num_bigs == 0 or available_big_locations == 0): found_forced_bk = state.found_forced_bk() - smalls_done = not smalls_avail or not enough_small_locations(state, available_small_locations) + smalls_done = not smalls_avail or available_small_locations == 0 + # or not enough_small_locations(state, available_small_locations) bk_done = state.big_key_opened or num_bigs == 0 or (available_big_locations == 0 and not found_forced_bk) # prize door should not be opened if the boss is reachable - but not reached yet allow_for_prize_lock = (key_layout.prize_can_lock and @@ -1532,8 +1590,8 @@ def enough_small_locations(state, avail_small_loc): def determine_prize_lock(key_layout, world, player): - if ((world.retro[player] and (world.mode[player] != 'standard' or key_layout.sector.name != 'Hyrule Castle')) - or world.logic[player] == 'nologic'): + if world.logic[player] == 'nologic' or (world.keyshuffle[player] == 'universal' and + (world.mode[player] != 'standard' or key_layout.sector.name != 'Hyrule Castle')): return # done, doesn't matter what flat_proposal = key_layout.flat_prop state = ExplorationState(dungeon=key_layout.sector.name) @@ -1564,18 +1622,24 @@ def determine_prize_lock(key_layout, world, player): key_layout.prize_can_lock = True -def cnt_avail_small_locations(free_locations, key_only, state, world, player): - if not world.keyshuffle[player] and not world.retro[player]: +def cnt_avail_small_locations(free_locations, key_only, state, key_layout, world, player): + std_flag = world.mode[player] == 'standard' and key_layout.sector.name == 'Hyrule Castle' + if world.keyshuffle[player] == 'none' or std_flag: bk_adj = 1 if state.big_key_opened and not state.big_key_special else 0 - avail_chest_keys = min(free_locations - bk_adj, state.key_locations - key_only) + # this is the secret passage, could expand to Uncle/Links House with appropriate logic + std_adj = 1 if std_flag and world.keyshuffle[player] != 'none' else 0 + avail_chest_keys = min(free_locations + std_adj - bk_adj, state.key_locations - key_only) return max(0, avail_chest_keys + key_only - state.used_smalls) return state.key_locations - state.used_smalls def cnt_avail_small_locations_by_ctr(free_locations, counter, layout, world, player): - if not world.keyshuffle[player] and not world.retro[player]: + std_flag = world.mode[player] == 'standard' and layout.sector.name == 'Hyrule Castle' + if world.keyshuffle[player] == 'none' or std_flag: bk_adj = 1 if counter.big_key_opened and not layout.big_key_special else 0 - avail_chest_keys = min(free_locations - bk_adj, layout.max_chests) + # this is the secret passage, could expand to Uncle/Links House with appropriate logic + std_adj = 1 if std_flag and world.keyshuffle[player] != 'none' else 0 + avail_chest_keys = min(free_locations + std_adj - bk_adj, layout.max_chests) return max(0, avail_chest_keys + len(counter.key_only_locations) - counter.used_keys) return layout.max_chests + len(counter.key_only_locations) - counter.used_keys @@ -1599,12 +1663,13 @@ def create_key_counters(key_layout, world, player): key_layout.found_doors.clear() flat_proposal = key_layout.flat_prop state = ExplorationState(dungeon=key_layout.sector.name) + state.init_zelda_event_doors(key_layout.event_starts, player) if world.doorShuffle[player] == 'vanilla': builder = world.dungeon_layouts[player][key_layout.sector.name] state.key_locations = len(builder.key_door_proposal) - builder.key_drop_cnt else: builder = world.dungeon_layouts[player][key_layout.sector.name] - state.key_locations = builder.total_keys - builder.key_drop_cnt + state.key_locations = max(0, builder.total_keys - builder.key_drop_cnt) state.big_key_special = False for region in key_layout.sector.regions: for location in region.locations: @@ -1631,10 +1696,10 @@ def create_key_counters(key_layout, world, player): if door.dest in flat_proposal and door.type != DoorType.SpiralStairs: key_layout.found_doors.add(door.dest) child_state = parent_state.copy() - if door.bigKey or door.name in special_big_key_doors: + if door.bigKey or door.name in get_special_big_key_doors(world, player): key_layout.key_logic.bk_doors.add(door) # open the door, if possible - if can_open_door(door, child_state, world, player): + if can_open_door(door, child_state, key_layout, world, player): open_a_door(door, child_state, flat_proposal, world, player) expand_key_state(child_state, flat_proposal, world, player) code = state_id(child_state, key_layout.flat_prop) @@ -1655,14 +1720,15 @@ def find_outside_connection(region): return None, None -def can_open_door(door, state, world, player): +def can_open_door(door, state, key_layout, world, player): if state.big_key_opened: ttl_locations = count_free_locations(state, world, player) else: ttl_locations = count_locations_exclude_big_chest(state.found_locations, world, player) if door.smallKey: ttl_small_key_only = count_small_key_only_locations(state) - available_small_locations = cnt_avail_small_locations(ttl_locations, ttl_small_key_only, state, world, player) + available_small_locations = cnt_avail_small_locations(ttl_locations, ttl_small_key_only, state, + key_layout, world, player) return available_small_locations > 0 elif door.bigKey: available_big_locations = cnt_avail_big_locations(ttl_locations, state, world, player) @@ -1865,7 +1931,7 @@ def val_hyrule(key_logic, world, player): val_rule(key_logic.door_rules['Hyrule Dungeon Map Room Key Door S'], 1) val_rule(key_logic.door_rules['Hyrule Dungeon Armory Interior Key Door N'], 2) val_rule(key_logic.door_rules['Sewers Dark Cross Key Door N'], 3) - val_rule(key_logic.door_rules['Sewers Key Rat Key Door N'], 4) + val_rule(key_logic.door_rules['Sewers Key Rat NE'], 4) else: 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) @@ -2016,14 +2082,14 @@ def val_rule(rule, skn, allow=False, loc=None, askn=None, setCheck=None): # Soft lock stuff def validate_key_placement(key_layout, world, player): - if world.retro[player] or world.accessibility[player] == 'none': + if world.keyshuffle[player] == 'universal' or world.accessibility[player] == 'none': return True # Can't keylock in retro. Expected if beatable only. max_counter = find_max_counter(key_layout) keys_outside = 0 big_key_outside = False smallkey_name = dungeon_keys[key_layout.sector.name] bigkey_name = dungeon_bigs[key_layout.sector.name] - if world.keyshuffle[player]: + if world.keyshuffle[player] != 'none': keys_outside = key_layout.max_chests - sum(1 for i in max_counter.free_locations if i.item is not None and i.item.name == smallkey_name and i.item.player == player) if world.bigkeyshuffle[player]: max_counter = find_max_counter(key_layout) diff --git a/Main.py b/Main.py index 3ea1afb4..468462b5 100644 --- a/Main.py +++ b/Main.py @@ -16,7 +16,7 @@ from OverworldGlitchRules import create_owg_connections from PotShuffle import shuffle_pots, shuffle_pot_switches from Regions import create_regions, create_shops, mark_light_dark_world_regions, create_dungeon_regions, adjust_locations from OWEdges import create_owedges -from OverworldShuffle import link_overworld, update_world_regions, create_flute_exits +from OverworldShuffle import link_overworld, update_world_regions, create_dynamic_exits from EntranceShuffle import link_entrances from Rom import patch_rom, patch_race_rom, patch_enemizer, apply_rom_settings, LocalRom, JsonRom, get_hash_string from Doors import create_doors @@ -25,14 +25,18 @@ from RoomData import create_rooms from Rules import set_rules from Dungeons import create_dungeons from Fill import distribute_items_restrictive, promote_dungeon_items, fill_dungeons_restrictive, ensure_good_pots +from Fill import dungeon_tracking from Fill import sell_potions, sell_keys, balance_multiworld_progression, balance_money_progression, lock_shop_locations, set_prize_drops -from ItemList import generate_itempool, difficulties, fill_prizes, customize_shops, create_farm_locations +from ItemList import generate_itempool, difficulties, fill_prizes, customize_shops, fill_specific_items, create_farm_locations from Utils import output_path, parse_player_names +from source.item.District import init_districts from source.item.FillUtil import create_item_pool_config, massage_item_pool, district_item_pool_config +from source.overworld.EntranceShuffle2 import link_entrances_new from source.tools.BPS import create_bps_from_data +from source.classes.CustomSettings import CustomSettings -__version__ = '1.0.1.3-u' +__version__ = '1.2.0.9-u' from source.classes.BabelFish import BabelFish @@ -58,32 +62,44 @@ def main(args, seed=None, fish=None): if args.securerandom: random.use_secure() - + seeded = False # initialize the world if args.code: for player, code in args.code.items(): if code: Settings.adjust_args_from_code(code, player, args) + customized = None + if args.customizer: + customized = CustomSettings() + customized.load_yaml(args.customizer) + seed = customized.determine_seed(seed) + seeded = True + customized.adjust_args(args) world = World(args.multi, args.ow_shuffle, args.ow_crossed, args.ow_mixed, args.shuffle, args.door_shuffle, args.logic, args.mode, args.swords, args.difficulty, args.item_functionality, args.timer, args.progressive, args.goal, args.algorithm, - args.accessibility, args.shuffleganon, args.retro, args.custom, args.customitemarray, args.hints) + args.accessibility, args.shuffleganon, args.custom, args.customitemarray, args.hints) + world.customizer = customized if customized else None logger = logging.getLogger('') if seed is None: random.seed(None) world.seed = random.randint(0, 999999999) else: world.seed = int(seed) - random.seed(world.seed) + if not seeded: + random.seed(world.seed) if args.securerandom: world.seed = ''.join(random.choice(string.ascii_uppercase + string.ascii_lowercase + string.digits) for _ in range(9)) + world.boots_hint = args.boots_hint.copy() world.remote_items = args.remote_items.copy() world.mapshuffle = args.mapshuffle.copy() world.compassshuffle = args.compassshuffle.copy() world.keyshuffle = args.keyshuffle.copy() world.bigkeyshuffle = args.bigkeyshuffle.copy() world.bombbag = args.bombbag.copy() + world.flute_mode = args.flute_mode.copy() + world.bow_mode = args.bow_mode.copy() world.crystals_needed_for_ganon = {player: random.randint(0, 7) if args.crystals_ganon[player] == 'random' else int(args.crystals_ganon[player]) for player in range(1, world.players + 1)} world.crystals_needed_for_gt = {player: random.randint(0, 7) if args.crystals_gt[player] == 'random' else int(args.crystals_gt[player]) for player in range(1, world.players + 1)} world.crystals_ganon_orig = args.crystals_ganon.copy() @@ -100,6 +116,10 @@ def main(args, seed=None, fish=None): world.enemy_damage = args.enemy_damage.copy() world.beemizer = args.beemizer.copy() world.intensity = {player: random.randint(1, 3) if args.intensity[player] == 'random' else int(args.intensity[player]) for player in range(1, world.players + 1)} + world.door_type_mode = args.door_type_mode.copy() + world.trap_door_mode = args.trap_door_mode.copy() + world.key_logic_algorithm = args.key_logic_algorithm.copy() + world.decoupledoors = args.decoupledoors.copy() world.experimental = args.experimental.copy() world.dungeon_counters = args.dungeon_counters.copy() world.fish = fish @@ -112,13 +132,16 @@ def main(args, seed=None, fish=None): world.treasure_hunt_count = {k: int(v) for k, v in args.triforce_goal.items()} world.treasure_hunt_total = {k: int(v) for k, v in args.triforce_pool.items()} world.shufflelinks = args.shufflelinks.copy() + world.shuffletavern = args.shuffletavern.copy() world.pseudoboots = args.pseudoboots.copy() world.overworld_map = args.overworld_map.copy() + world.take_any = args.take_any.copy() world.restrict_boss_items = args.restrict_boss_items.copy() world.collection_rate = args.collection_rate.copy() world.colorizepots = args.colorizepots.copy() world.rom_seeds = {player: random.randint(0, 999999999) for player in range(1, world.players + 1)} + world.finish_init() from OverworldShuffle import __version__ as ORVersion logger.info( @@ -139,7 +162,9 @@ def main(args, seed=None, fish=None): for player, name in enumerate(team, 1): world.player_names[player].append(name) logger.info('') - + world.settings = CustomSettings() + world.settings.create_from_world(world, args.race) + outfilebase = f'OR_{args.outputname if args.outputname else world.seed}' for player in range(1, world.players + 1): @@ -151,7 +176,9 @@ def main(args, seed=None, fish=None): if args.usestartinventory[player]: for tok in filter(None, args.startinventory[player].split(',')): - item = ItemFactory(tok.strip(), player) + name = tok.strip() + name = name if name != 'Ocarina' or world.flute_mode[player] != 'active' else 'Ocarina (Activated)' + item = ItemFactory(name, player) if item: world.push_precollected(item) @@ -172,6 +199,15 @@ def main(args, seed=None, fish=None): adjust_locations(world, player) place_bosses(world, player) + if world.customizer and world.customizer.get_start_inventory(): + for p, inv_list in world.customizer.get_start_inventory().items(): + for inv_item in inv_list: + item = ItemFactory(inv_item.strip(), p) + if item: + world.push_precollected(item) + if args.print_custom_yaml: + world.settings.record_info(world) + if any(world.potshuffle.values()): logger.info(world.fish.translate("cli", "cli", "shuffling.pots")) for player in range(1, world.players + 1): @@ -188,17 +224,22 @@ def main(args, seed=None, fish=None): create_shops(world, player) update_world_regions(world, player) mark_light_dark_world_regions(world, player) - create_flute_exits(world, player) + create_dynamic_exits(world, player) + + init_districts(world) logger.info(world.fish.translate("cli","cli","shuffling.world")) for player in range(1, world.players + 1): - link_entrances(world, player) + link_entrances_new(world, player) logger.info(world.fish.translate("cli", "cli", "shuffling.prep")) + for player in range(1, world.players + 1): link_doors_prep(world, player) + if args.print_custom_yaml: + world.settings.record_entrances(world) create_item_pool_config(world) logger.info(world.fish.translate("cli", "cli", "shuffling.dungeons")) @@ -206,6 +247,9 @@ def main(args, seed=None, fish=None): for player in range(1, world.players + 1): link_doors(world, player) mark_light_dark_world_regions(world, player) + if args.print_custom_yaml: + world.settings.record_doors(world) + logger.info(world.fish.translate("cli", "cli", "generating.itempool")) for player in range(1, world.players + 1): @@ -224,19 +268,21 @@ def main(args, seed=None, fish=None): for player in range(1, world.players + 1): if world.shopsanity[player]: sell_potions(world, player) - if world.retro[player]: + if world.keyshuffle[player] == 'universal': sell_keys(world, player) else: lock_shop_locations(world, player) massage_item_pool(world) + if args.print_custom_yaml: + world.settings.record_item_pool(world) + dungeon_tracking(world) + fill_specific_items(world) + logger.info(world.fish.translate("cli", "cli", "placing.dungeon.prizes")) fill_prizes(world) - # used for debugging - # fill_specific_items(world) - logger.info(world.fish.translate("cli","cli","placing.dungeon.items")) if args.algorithm != 'equitable': @@ -270,6 +316,7 @@ def main(args, seed=None, fish=None): balance_multiworld_progression(world) # if we only check for beatable, we can do this sanity check first before creating the rom + world.clear_exp_cache() if not world.can_beat_game(log_error=True): raise RuntimeError(world.fish.translate("cli", "cli", "cannot.beat.game")) @@ -280,6 +327,10 @@ def main(args, seed=None, fish=None): balance_money_progression(world) ensure_good_pots(world, True) + if args.print_custom_yaml: + world.settings.record_item_placements(world) + world.settings.write_to_file(output_path(f'{outfilebase}_custom.yaml')) + rom_names = [] jsonout = {} enemized = False @@ -398,7 +449,7 @@ def copy_world(world): # ToDo: Not good yet ret = World(world.players, world.owShuffle, world.owCrossed, world.owMixed, world.shuffle, world.doorShuffle, world.logic, world.mode, world.swords, world.difficulty, world.difficulty_adjustments, world.timer, world.progressive, world.goal, world.algorithm, - world.accessibility, world.shuffle_ganon, world.retro, world.custom, world.customitemarray, world.hints) + world.accessibility, world.shuffle_ganon, world.custom, world.customitemarray, world.hints) ret.teams = world.teams ret.player_names = copy.deepcopy(world.player_names) ret.remote_items = world.remote_items.copy() @@ -427,6 +478,7 @@ def copy_world(world): ret.keyshuffle = world.keyshuffle.copy() ret.bigkeyshuffle = world.bigkeyshuffle.copy() ret.bombbag = world.bombbag.copy() + ret.flute_mode = world.flute_mode.copy() ret.crystals_needed_for_ganon = world.crystals_needed_for_ganon.copy() ret.crystals_needed_for_gt = world.crystals_needed_for_gt.copy() ret.crystals_ganon_orig = world.crystals_ganon_orig.copy() @@ -445,6 +497,7 @@ def copy_world(world): ret.enemy_damage = world.enemy_damage.copy() ret.beemizer = world.beemizer.copy() ret.intensity = world.intensity.copy() + ret.decoupledoors = world.decoupledoors.copy() ret.experimental = world.experimental.copy() ret.shopsanity = world.shopsanity.copy() ret.dropshuffle = world.dropshuffle.copy() @@ -463,10 +516,11 @@ def copy_world(world): update_world_regions(ret, player) if world.logic[player] in ('owglitches', 'nologic'): create_owg_connections(ret, player) - create_flute_exits(ret, player) + create_dynamic_exits(ret, player) create_dungeon_regions(ret, player) create_owedges(ret, player) create_shops(ret, player) + #create_doors(ret, player) create_rooms(ret, player) create_dungeons(ret, player) @@ -517,12 +571,9 @@ def copy_world(world): new_location.item = item item.location = new_location item.world = ret - if location.event: - new_location.event = True - if location.locked: - new_location.locked = True - if location.skip: - new_location.skip = True + new_location.event = location.event + new_location.locked = location.locked + new_location.skip = location.skip # these need to be modified properly by set_rules new_location.access_rule = lambda state: True new_location.item_rule = lambda state: True @@ -576,7 +627,7 @@ def copy_world_premature(world, player): # ToDo: Not good yet ret = World(world.players, world.owShuffle, world.owCrossed, world.owMixed, world.shuffle, world.doorShuffle, world.logic, world.mode, world.swords, world.difficulty, world.difficulty_adjustments, world.timer, world.progressive, world.goal, world.algorithm, - world.accessibility, world.shuffle_ganon, world.retro, world.custom, world.customitemarray, world.hints) + world.accessibility, world.shuffle_ganon, world.custom, world.customitemarray, world.hints) ret.teams = world.teams ret.player_names = copy.deepcopy(world.player_names) ret.remote_items = world.remote_items.copy() @@ -642,7 +693,7 @@ def copy_world_premature(world, player): update_world_regions(ret, player) if world.logic[player] in ('owglitches', 'nologic'): create_owg_connections(ret, player) - create_flute_exits(ret, player) + create_dynamic_exits(ret, player) create_dungeon_regions(ret, player) create_owedges(ret, player) create_shops(ret, player) @@ -731,7 +782,8 @@ def create_playthrough(world): world = copy_world(world) # get locations containing progress items - prog_locations = [location for location in world.get_filled_locations() if location.item.advancement] + prog_locations = [location for location in world.get_filled_locations() if location.item.advancement + or world.goal[location.player] == 'completionist'] optional_locations = ['Trench 1 Switch', 'Trench 2 Switch', 'Ice Block Drop', 'Skull Star Tile'] optional_locations.extend(['Hyrule Castle Courtyard Tree Pull', 'Mountain Entry Area Tree Pull']) # adding pre-aga tree pulls optional_locations.extend(['Lumberjack Area Crab Drop', 'South Pass Area Crab Drop']) # adding pre-aga bush crabs @@ -770,13 +822,15 @@ def create_playthrough(world): for num, sphere in reversed(list(enumerate(collection_spheres))): to_delete = set() for location in sphere: + if world.goal[location.player] == 'completionist': + continue # every location for that player is required # we remove the item at location and check if game is still beatable logging.getLogger('').debug('Checking if %s (Player %d) is required to beat the game.', location.item.name, location.item.player) old_item = location.item location.item = None # todo: this is not very efficient, but I'm not sure how else to do it for this backwards logic - # world.clear_exp_cache() - if world.can_beat_game(state_cache[num]): + world.clear_exp_cache() + if world.can_beat_game(state_cache[max(num-1, 0)]): logging.getLogger('').debug(f'{old_item.name} (Player {old_item.player}) is not required') to_delete.add(location) else: diff --git a/Mystery.py b/Mystery.py index cf759ccc..27dc1a92 100644 --- a/Mystery.py +++ b/Mystery.py @@ -1,22 +1,21 @@ import argparse import logging -from pathlib import Path -import os import RaceRandom as random -import urllib.request -import urllib.parse -import yaml from DungeonRandomizer import parse_cli from Main import main as DRMain from source.classes.BabelFish import BabelFish from yaml.constructor import SafeConstructor +from source.tools.MysteryUtils import roll_settings, get_weights + + def add_bool(self, node): return self.construct_scalar(node) SafeConstructor.add_constructor(u'tag:yaml.org,2002:bool', add_bool) + def main(): parser = argparse.ArgumentParser(add_help=False) parser.add_argument('--multi', default=1, type=lambda value: min(max(int(value), 1), 255)) @@ -107,198 +106,6 @@ def main(): DRMain(erargs, seed, BabelFish()) -def get_weights(path): - if os.path.exists(Path(path)): - with open(path, "r", encoding="utf-8") as f: - return yaml.load(f, Loader=yaml.SafeLoader) - elif urllib.parse.urlparse(path).scheme in ['http', 'https']: - return yaml.load(urllib.request.urlopen(path), Loader=yaml.FullLoader) - -def roll_settings(weights): - def get_choice(option, root=None): - root = weights if root is None else root - if option not in root: - return None - if type(root[option]) is not dict: - return root[option] - if not root[option]: - return None - return random.choices(list(root[option].keys()), weights=list(map(int,root[option].values())))[0] - - def get_choice_default(option, root=weights, default=None): - choice = get_choice(option, root) - if choice is None and default is not None: - return default - return choice - - while True: - subweights = weights.get('subweights', {}) - if len(subweights) == 0: - break - chances = ({k: int(v['chance']) for (k, v) in subweights.items()}) - subweight_name = random.choices(list(chances.keys()), weights=list(chances.values()))[0] - subweights = weights.get('subweights', {}).get(subweight_name, {}).get('weights', {}) - subweights['subweights'] = subweights.get('subweights', {}) - weights = {**weights, **subweights} - - ret = argparse.Namespace() - - ret.algorithm = get_choice('algorithm') - - glitch_map = {'none': 'noglitches', 'no_logic': 'nologic', 'owglitches': 'owglitches', - 'owg': 'owglitches', 'minorglitches': 'minorglitches'} - glitches_required = get_choice('glitches_required') - if glitches_required is not None: - if glitches_required not in glitch_map.keys(): - print(f'Logic did not match one of: {", ".join(glitch_map.keys())}') - glitches_required = 'none' - ret.logic = glitch_map[glitches_required] - - item_placement = get_choice('item_placement') - # not supported in ER - - dungeon_items = get_choice('dungeon_items') - ret.mapshuffle = get_choice('map_shuffle') == 'on' if 'map_shuffle' in weights else dungeon_items in ['mc', 'mcs', 'full'] - ret.compassshuffle = get_choice('compass_shuffle') == 'on' if 'compass_shuffle' in weights else dungeon_items in ['mc', 'mcs', 'full'] - ret.keyshuffle = get_choice('smallkey_shuffle') == 'on' if 'smallkey_shuffle' in weights else dungeon_items in ['mcs', 'full'] - ret.bigkeyshuffle = get_choice('bigkey_shuffle') == 'on' if 'bigkey_shuffle' in weights else dungeon_items in ['full'] - - ret.accessibility = get_choice('accessibility') - ret.restrict_boss_items = get_choice('restrict_boss_items') - - overworld_shuffle = get_choice('overworld_shuffle') - ret.ow_shuffle = overworld_shuffle if overworld_shuffle != 'none' else 'vanilla' - ret.ow_terrain = get_choice('overworld_terrain') == 'on' - valid_options = {'none', 'polar', 'grouped', 'limited', 'chaos'} - ret.ow_crossed = get_choice('overworld_crossed') - ret.ow_crossed = ret.ow_crossed if ret.ow_crossed in valid_options else 'none' - ret.ow_keepsimilar = get_choice('overworld_keepsimilar') == 'on' - ret.ow_mixed = get_choice('overworld_swap') == 'on' - ret.ow_whirlpool = get_choice('whirlpool_shuffle') == 'on' - overworld_flute = get_choice('flute_shuffle') - ret.ow_fluteshuffle = overworld_flute if overworld_flute != 'none' else 'vanilla' - ret.bonk_drops = get_choice('bonk_drops') == 'on' - entrance_shuffle = get_choice('entrance_shuffle') - ret.shuffle = entrance_shuffle if entrance_shuffle != 'none' else 'vanilla' - overworld_map = get_choice('overworld_map') - ret.overworld_map = overworld_map if overworld_map != 'default' else 'default' - door_shuffle = get_choice('door_shuffle') - ret.door_shuffle = door_shuffle if door_shuffle != 'none' else 'vanilla' - ret.intensity = get_choice('intensity') - ret.experimental = get_choice('experimental') == 'on' - ret.collection_rate = get_choice('collection_rate') == 'on' - - ret.dungeon_counters = get_choice('dungeon_counters') if 'dungeon_counters' in weights else 'default' - if ret.dungeon_counters == 'default': - ret.dungeon_counters = 'pickup' if ret.door_shuffle != 'vanilla' or ret.compassshuffle == 'on' else 'off' - - ret.pseudoboots = get_choice('pseudoboots') == 'on' - ret.shopsanity = get_choice('shopsanity') == 'on' - ret.dropshuffle = get_choice('dropshuffle') == 'on' - ret.pottery = get_choice('pottery') if 'pottery' in weights else 'none' - ret.colorizepots = get_choice('colorizepots') == 'on' - ret.shufflepots = get_choice('pot_shuffle') == 'on' - ret.mixed_travel = get_choice('mixed_travel') if 'mixed_travel' in weights else 'prevent' - ret.standardize_palettes = get_choice('standardize_palettes') if 'standardize_palettes' in weights else 'standardize' - - goal = get_choice('goals') - if goal is not None: - ret.goal = {'ganon': 'ganon', - 'fast_ganon': 'crystals', - 'dungeons': 'dungeons', - 'pedestal': 'pedestal', - 'triforce-hunt': 'triforcehunt', - 'trinity': 'trinity' - }[goal] - - ret.openpyramid = get_choice('open_pyramid') if 'open_pyramid' in weights else 'auto' - - ret.shuffleganon = get_choice('shuffleganon') == 'on' - ret.shufflelinks = get_choice('shufflelinks') == 'on' - - ret.crystals_gt = get_choice('tower_open') - ret.crystals_ganon = get_choice('ganon_open') - - from ItemList import set_default_triforce - default_tf_goal, default_tf_pool = set_default_triforce(ret.goal, 0, 0) - goal_min = get_choice_default('triforce_goal_min', default=default_tf_goal) - goal_max = get_choice_default('triforce_goal_max', default=default_tf_goal) - pool_min = get_choice_default('triforce_pool_min', default=default_tf_pool) - pool_max = get_choice_default('triforce_pool_max', default=default_tf_pool) - ret.triforce_goal = random.randint(int(goal_min), int(goal_max)) - min_diff = get_choice_default('triforce_min_difference', default=(default_tf_pool-default_tf_goal)) - ret.triforce_pool = random.randint(max(int(pool_min), ret.triforce_goal + int(min_diff)), int(pool_max)) - - ret.mode = get_choice('world_state') - if ret.mode == 'retro': - ret.mode = 'open' - ret.retro = True - ret.retro = get_choice('retro') == 'on' # this overrides world_state if used - - ret.bombbag = get_choice('bombbag') == 'on' - - ret.hints = get_choice('hints') == 'on' - - swords = get_choice('weapons') - if swords is not None: - ret.swords = {'randomized': 'random', - 'assured': 'assured', - 'vanilla': 'vanilla', - 'swordless': 'swordless' - }[swords] - - ret.difficulty = get_choice('item_pool') - - ret.item_functionality = get_choice('item_functionality') - - old_style_bosses = {'basic': 'simple', - 'normal': 'full', - 'chaos': 'random'} - boss_choice = get_choice('boss_shuffle') - if boss_choice in old_style_bosses.keys(): - boss_choice = old_style_bosses[boss_choice] - ret.shufflebosses = boss_choice - - enemy_choice = get_choice('enemy_shuffle') - if enemy_choice == 'chaos': - enemy_choice = 'random' - ret.shuffleenemies = enemy_choice - - old_style_damage = {'none': 'default', - 'chaos': 'random'} - damage_choice = get_choice('enemy_damage') - if damage_choice in old_style_damage: - damage_choice = old_style_damage[damage_choice] - ret.enemy_damage = damage_choice - - ret.enemy_health = get_choice('enemy_health') - - ret.beemizer = get_choice('beemizer') if 'beemizer' in weights else '0' - - inventoryweights = weights.get('startinventory', {}) - startitems = [] - for item in inventoryweights.keys(): - if get_choice(item, inventoryweights) == 'on': - startitems.append(item) - ret.startinventory = ','.join(startitems) - if len(startitems) > 0: - ret.usestartinventory = True - - if 'rom' in weights: - romweights = weights['rom'] - ret.sprite = get_choice('sprite', romweights) - ret.disablemusic = get_choice('disablemusic', romweights) == 'on' - ret.quickswap = get_choice('quickswap', romweights) == 'on' - ret.reduce_flashing = get_choice('reduce_flashing', romweights) == 'on' - ret.msu_resume = get_choice('msu_resume', romweights) == 'on' - ret.fastmenu = get_choice('menuspeed', romweights) - ret.heartcolor = get_choice('heartcolor', romweights) - ret.heartbeep = get_choice('heartbeep', romweights) - ret.ow_palettes = get_choice('ow_palettes', romweights) - ret.uw_palettes = get_choice('uw_palettes', romweights) - ret.shuffle_sfx = get_choice('shuffle_sfx', romweights) == 'on' - - return ret if __name__ == '__main__': main() diff --git a/OWEdges.py b/OWEdges.py index 7fe4b016..1e40a0c4 100644 --- a/OWEdges.py +++ b/OWEdges.py @@ -1608,7 +1608,6 @@ OWExitTypes = { 'Hyrule Castle Main Gate (North)', 'Hyrule Castle Courtyard Bush (North)', 'Hyrule Castle Outer East Rock', - 'Blacksmith Water Exit', 'Wooden Bridge Bush (South)', 'Wooden Bridge Bush (North)', 'Bat Cave Ledge Peg', @@ -1655,6 +1654,7 @@ OWExitTypes = { 'Skull Woods Forgotten Bush (East)', 'GT Entry Approach', 'GT Entry Leave', + 'East Dark Death Mountain Bushes', 'Bumper Cave Entrance Rock', 'Skull Woods Pass Bush Row (West)', 'Skull Woods Pass Bush Row (East)', @@ -1673,7 +1673,6 @@ OWExitTypes = { 'Village of Outcasts Pegs', 'Grassy Lawn Pegs', 'Pyramid Crack', - 'Hammerpegs Water Exit', 'Broken Bridge Hammer Rock (South)', 'Broken Bridge Hammer Rock (North)', 'Broken Bridge Hookshot Gap', @@ -1683,7 +1682,6 @@ OWExitTypes = { 'Archery Game Rock (North)', 'Frog Rock (Inner)', 'Archery Game Rock (South)', - 'Big Bomb Shop Water Exit', 'Hammer Bridge Pegs (North)', 'Hammer Bridge Pegs (South)', 'Hammer Bridge Pier', @@ -1733,195 +1731,7 @@ OWExitTypes = { 'Qirn Jump Whirlpool', 'Bomber Corner Whirlpool' ], - 'Mirror': ['Skull Woods Back Mirror Spot', - 'Skull Woods Forgotten (West) Mirror Spot', - 'Skull Woods Forgotten (East) Mirror Spot', - 'Skull Woods Portal Entry Mirror Spot', - 'Skull Woods Forgotten (Middle) Mirror Spot', - 'Skull Woods Front Mirror Spot', - 'Dark Lumberjack Mirror Spot', - 'West Dark Death Mountain (Top) Mirror Spot', - 'Bubble Boy Mirror Spot', - 'West Dark Death Mountain (Bottom) Mirror Spot', - 'East Dark Death Mountain (Top West) Mirror Spot', - 'East Dark Death Mountain (Top East) Mirror Spot', - 'TR Ledge (West) Mirror Spot', - 'TR Ledge (East) Mirror Spot', - 'TR Isolated Mirror Spot', - 'East Dark Death Mountain (Bottom Plateau) Mirror Spot', - 'East Dark Death Mountain (Bottom Left) Mirror Spot', - 'East Dark Death Mountain (Bottom) Mirror Spot', - 'Dark Floating Island Mirror Spot', - 'Turtle Rock Mirror Spot', - 'Turtle Rock Ledge Mirror Spot', - 'Bumper Cave Area Mirror Spot', - 'Bumper Cave Entry Mirror Spot', - 'Bumper Cave Ledge Mirror Spot', - 'Catfish Mirror Spot', - 'Skull Woods Pass West Mirror Spot', - 'Skull Woods Pass East Top Mirror Spot', - 'Skull Woods Pass Portal Mirror Spot', - 'Skull Woods Pass East Bottom Mirror Spot', - 'Outcast Fortune Mirror Spot', - 'Outcast Pond Mirror Spot', - 'Dark Chapel Mirror Spot', - 'Dark Chapel Ledge Mirror Spot', - 'Dark Graveyard Mirror Spot', - 'Dark Graveyard Ledge Mirror Spot', - 'Dark Graveyard Grave Mirror Spot', - 'Qirn Jump Mirror Spot', - 'Qirn Jump East Mirror Spot', - 'Dark Witch Mirror Spot', - 'Dark Witch Northeast Mirror Spot', - 'Catfish Approach Mirror Spot', - 'Catfish Approach Ledge Mirror Spot', - 'Village of Outcasts Mirror Spot', - 'Village of Outcasts Southwest Mirror Spot', - 'Hammer House Mirror Spot', - 'Shield Shop Mirror Spot', - 'Pyramid Mirror Spot', - 'Pyramid Pass Mirror Spot', - 'Pyramid Courtyard Mirror Spot', - 'Pyramid Uncle Mirror Spot', - 'Pyramid From Ledge Mirror Spot', - 'Pyramid Entry Mirror Spot', - 'Broken Bridge West Mirror Spot', - 'Broken Bridge East Mirror Spot', - 'Broken Bridge Northeast Mirror Spot', - 'Palace of Darkness Mirror Spot', - 'Hammer Pegs Mirror Spot', - 'Hammer Pegs Entry Mirror Spot', - 'Dark Dunes Mirror Spot', - 'Dig Game Mirror Spot', - 'Dig Game Ledge Mirror Spot', - 'Frog Mirror Spot', - 'Frog Prison Mirror Spot', - 'Archery Game Mirror Spot', - 'Stumpy Mirror Spot', - 'Stumpy Pass Mirror Spot', - 'Dark Bonk Rocks Mirror Spot', - 'Big Bomb Shop Mirror Spot', - 'Hammer Bridge North Mirror Spot', - 'Hammer Bridge South Mirror Spot', - 'Dark Hobo Mirror Spot', - 'Dark Tree Line Mirror Spot', - 'Darkness Nook Mirror Spot', - 'Misery Mire Mirror Spot', - 'Misery Mire Ledge Mirror Spot', - 'Misery Mire Blocked Mirror Spot', - 'Misery Mire Main Mirror Spot', - 'Stumpy Approach Mirror Spot', - 'Stumpy Bush Entry Mirror Spot', - 'Dark C Whirlpool Mirror Spot', - 'Dark C Whirlpool Outer Mirror Spot', - 'Hype Cave Mirror Spot', - 'Ice Lake Mirror Spot', - 'Ice Lake Southwest Mirror Spot', - 'Ice Lake Southeast Mirror Spot', - 'Ice Lake Northeast Mirror Spot', - 'Ice Palace Mirror Spot', - 'Ice Lake Moat Mirror Spot', - 'Shopping Mall Mirror Spot', - 'Swamp Nook Mirror Spot', - 'Swamp Nook Southeast Mirror Spot', - 'Swamp Nook Pegs Mirror Spot', - 'Swamp Mirror Spot', - 'Dark South Pass Mirror Spot', - 'Bomber Corner Mirror Spot', - 'Lost Woods East Mirror Spot', - 'Lost Woods Entry Mirror Spot', - 'Lost Woods Pedestal Mirror Spot', - 'Lost Woods Southwest Mirror Spot', - 'Lost Woods East (Forgotten) Mirror Spot', - 'Lost Woods West (Forgotten) Mirror Spot', - 'Lumberjack Mirror Spot', - 'West Death Mountain (Top) Mirror Spot', - 'Spectacle Rock Mirror Spot', - 'East Death Mountain (Top West) Mirror Spot', - 'East Death Mountain (Top East) Mirror Spot', - 'Fairy Ascension Mirror Spot', - 'Death Mountain Bridge Mirror Spot', - 'Spiral Cave Mirror Spot', - 'Mimic Cave Mirror Spot', - 'Isolated Ledge Mirror Spot', - 'Floating Island Mirror Spot', - 'TR Pegs Area Mirror Spot', - 'Mountain Entry Mirror Spot', - 'Mountain Entry Entrance Mirror Spot', - 'Mountain Entry Ledge Mirror Spot', - 'Zora Waterfall Mirror Spot', - 'Lost Woods Pass West Mirror Spot', - 'Lost Woods Pass East Top Mirror Spot', - 'Lost Woods Pass Portal Mirror Spot', - 'Lost Woods Pass East Bottom Mirror Spot', - 'Kakariko Fortune Mirror Spot', - 'Kakariko Pond Mirror Spot', - 'Sanctuary Mirror Spot', - 'Bonk Rock Ledge Mirror Spot', - 'Graveyard Ledge Mirror Spot', - 'Kings Grave Mirror Spot', - 'River Bend Mirror Spot', - 'River Bend East Mirror Spot', - 'Potion Shop Mirror Spot', - 'Potion Shop Northeast Mirror Spot', - 'Zora Approach Mirror Spot', - 'Zora Approach Ledge Mirror Spot', - 'Kakariko Mirror Spot', - 'Kakariko Grass Mirror Spot', - 'Forgotton Forest Mirror Spot', - 'Forgotton Forest Fence Mirror Spot', - 'HC Ledge Mirror Spot', - 'HC Courtyard Mirror Spot', - 'HC Area Mirror Spot', - 'HC East Entry Mirror Spot', - 'HC Courtyard Left Mirror Spot', - 'HC Area South Mirror Spot', - 'Wooden Bridge Mirror Spot', - 'Wooden Bridge Northeast Mirror Spot', - 'Wooden Bridge West Mirror Spot', - 'Eastern Palace Mirror Spot', - 'Blacksmith Entry Mirror Spot', - 'Blacksmith Mirror Spot', - 'Bat Cave Ledge Mirror Spot', - 'Sand Dunes Mirror Spot', - 'Maze Race Mirror Spot', - 'Maze Race Ledge Mirror Spot', - 'Kakariko Suburb Mirror Spot', - 'Kakariko Suburb South Mirror Spot', - 'Flute Boy Mirror Spot', - 'Flute Boy Pass Mirror Spot', - 'Central Bonk Rocks Mirror Spot', - 'Links House Mirror Spot', - 'Stone Bridge Mirror Spot', - 'Stone Bridge South Mirror Spot', - 'Hobo Mirror Spot', - 'Tree Line Mirror Spot', - 'Eastern Nook Mirror Spot', - 'Desert Mirror Spot', - 'Desert Ledge Mirror Spot', - 'Checkerboard Mirror Spot', - 'DP Stairs Mirror Spot', - 'DP Entrance (North) Mirror Spot', - 'Bombos Tablet Ledge Mirror Spot', - 'Cave 45 Mirror Spot', - 'Flute Boy Entry Mirror Spot', - 'C Whirlpool Mirror Spot', - 'C Whirlpool Outer Mirror Spot', - 'Statues Mirror Spot', - 'Lake Hylia Mirror Spot', - 'Lake Hylia Northeast Mirror Spot', - 'South Shore Mirror Spot', - 'South Shore East Mirror Spot', - 'Lake Hylia Island Mirror Spot', - 'Lake Hylia Water Mirror Spot', - 'Lake Hylia Water D Mirror Spot', - 'Lake Hylia Central Island Mirror Spot', - 'Ice Cave Mirror Spot', - 'Desert Pass Ledge Mirror Spot', - 'Desert Pass Mirror Spot', - 'Dam Mirror Spot', - 'South Pass Mirror Spot', - 'Octoballoon Mirror Spot' + 'Mirror': ['Mirror To Bombos Tablet Ledge' ] } diff --git a/OverworldGlitchRules.py b/OverworldGlitchRules.py index 9a170f45..2372993f 100644 --- a/OverworldGlitchRules.py +++ b/OverworldGlitchRules.py @@ -312,7 +312,10 @@ def overworld_glitches_rules(world, player): #add_alternate_rule(world.get_location('Zora\'s Ledge', player), lambda state: state.has_Boots(player)) #revisit when we can guarantee water walk # Adding additional item requirements to OWG Clips - add_additional_rule(world.get_entrance('VoO To Dig Game Hook Clip', player), lambda state: state.has('Hookshot', player)) + if world.is_tile_swapped(0x18, player) != world.is_tile_swapped(0x28, player): + add_additional_rule(world.get_entrance('Kakariko To Dig Game Hook Clip', player), lambda state: state.has('Hookshot', player)) + else: + add_additional_rule(world.get_entrance('VoO To Dig Game Hook Clip', player), lambda state: state.has('Hookshot', player)) add_additional_rule(world.get_entrance('Tree Line Water Clip', player), lambda state: state.has('Flippers', player)) add_additional_rule(world.get_entrance('Dark Tree Line Water Clip', player), lambda state: state.has('Flippers', player)) @@ -508,6 +511,6 @@ mirror_clips = [ ] mirror_offsets = [ - (['DM Offset Mirror', 'DDM Offset Mirror'], ['West Death Mountain (Bottom)', 'West Dark Death Mountain (Bottom)'], ['Hyrule Castle Ledge', 'Pyramid Crack'], ['Pyramid Area', 'Hyrule Castle Courtyard']) - #(['DM To HC Ledge Offset Mirror', 'DDM To HC Ledge Offset Mirror'], ['West Death Mountain (Bottom)', 'West Dark Death Mountain (Bottom)'], ['Hyrule Castle Ledge', None], ['Pyramid Area', None]) + (['DM Offset Mirror', 'DDM Offset Mirror'], ['West Death Mountain (Bottom)', 'West Dark Death Mountain (Bottom)'], ['Hyrule Castle Courtyard Northeast', 'Pyramid Crack'], ['Pyramid Area', 'Hyrule Castle Courtyard']), + (['DM To HC Ledge Offset Mirror', 'DDM To HC Ledge Offset Mirror'], ['West Death Mountain (Bottom)', 'West Dark Death Mountain (Bottom)'], ['Hyrule Castle Ledge', None], ['Pyramid Area', None]) ] \ No newline at end of file diff --git a/OverworldShuffle.py b/OverworldShuffle.py index 4922ffce..61fe3668 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -7,7 +7,7 @@ from OWEdges import OWTileRegions, OWEdgeGroups, OWEdgeGroupsTerrain, OWExitType from OverworldGlitchRules import create_owg_connections from Utils import bidict -version_number = '0.2.11.4' +version_number = '0.3.0.0' # branch indicator is intentionally different across branches version_branch = '' @@ -168,6 +168,21 @@ def link_overworld(world, player): world.spoiler.set_map('swaps', text_output, world.owswaps[player][0], player) # apply tile logical connections + if not world.is_bombshop_start(player): + connect_simple(world, 'Links House S&Q', 'Links House', player) + else: + connect_simple(world, 'Links House S&Q', 'Big Bomb Shop', player) + + if not world.mode[player] == 'inverted': + connect_simple(world, 'Sanctuary S&Q', 'Sanctuary', player) + else: + connect_simple(world, 'Sanctuary S&Q', 'Dark Sanctuary Hint', player) + + if not world.is_tile_swapped(0x1b, player): + connect_simple(world, 'Other World S&Q', 'Pyramid Area', player) + else: + connect_simple(world, 'Other World S&Q', 'Hyrule Castle Ledge', player) + for owid in ow_connections.keys(): if not world.is_tile_swapped(owid, player): for (exitname, regionname) in ow_connections[owid][0]: @@ -829,13 +844,66 @@ def adjust_edge_groups(world, trimmed_groups, edges_to_swap, player): def create_flute_exits(world, player): for region in (r for r in world.regions if r.player == player and r.terrain == Terrain.Land and r.name not in ['Zoras Domain', 'Master Sword Meadow', 'Hobo Bridge']): - if region.type == (RegionType.LightWorld if world.mode != 'inverted' else RegionType.DarkWorld): + if region.type == (RegionType.LightWorld if world.mode[player] != 'inverted' else RegionType.DarkWorld): exitname = 'Flute From ' + region.name exit = Entrance(region.player, exitname, region) exit.spot_type = 'Flute' exit.access_rule = lambda state: state.can_flute(player) exit.connect(world.get_region('Flute Sky', player)) region.exits.append(exit) + +def get_mirror_exit_name(from_region, to_region): + if from_region in mirror_connections and to_region in mirror_connections[from_region]: + if len(mirror_connections[from_region]) == 1: + return f'Mirror From {from_region}' + else: + return f'Mirror To {to_region}' + return None + +def get_mirror_edges(world, region, player): + mirror_exits = list() + if (world.mode[player] != 'inverted') == (region.type == RegionType.DarkWorld): + # get mirror edges leaving the region + if region.name in mirror_connections: + for dest_region_name in mirror_connections[region.name]: + mirror_exits.append(tuple([get_mirror_exit_name(region.name, dest_region_name), dest_region_name])) + else: + # get mirror edges leading into the region + owid = OWTileRegions[region.name] + for other_world_region_name in OWTileRegions.inverse[(owid + 0x40) % 0x80]: + if other_world_region_name in mirror_connections: + for dest_region_name in mirror_connections[other_world_region_name]: + if dest_region_name == region.name: + mirror_exits.append(tuple([get_mirror_exit_name(other_world_region_name, region.name), region.name])) + return mirror_exits + +def create_mirror_exits(world, player): + mirror_exits = set() + for region in (r for r in world.regions if r.player == player and r.name not in ['Zoras Domain', 'Master Sword Meadow', 'Hobo Bridge']): + if region.type == (RegionType.DarkWorld if world.mode[player] != 'inverted' else RegionType.LightWorld): + if region.name in mirror_connections: + for region_dest_name in mirror_connections[region.name]: + exitname = get_mirror_exit_name(region.name, region_dest_name) + + assert exitname not in mirror_exits, f'Mirror Exit with name already exists: {exitname}' + + exit = Entrance(region.player, exitname, region) + exit.spot_type = 'Mirror' + to_region = world.get_region(region_dest_name, player) + if region.terrain == Terrain.Water or to_region.terrain == Terrain.Water: + exit.access_rule = lambda state: state.has('Flippers', player) and state.has_Pearl(player) and state.has_Mirror(player) + else: + exit.access_rule = lambda state: state.has_Mirror(player) + exit.connect(to_region) + region.exits.append(exit) + + mirror_exits.add(exitname) + elif region.terrain == Terrain.Land: + pass + +def create_dynamic_exits(world, player): + create_flute_exits(world, player) + create_mirror_exits(world, player) world.initialize_regions() def categorize_world_regions(world, player): @@ -1149,7 +1217,9 @@ test_connections = [ ] # these are connections that cannot be shuffled and always exist. They link together separate parts of the world we need to divide into regions -mandatory_connections = [# Intra-tile OW Connections +mandatory_connections = [('Old Man S&Q', 'Old Man House'), + + # Intra-tile OW Connections ('Lost Woods Bush (West)', 'Lost Woods East Area'), #pearl ('Lost Woods Bush (East)', 'Lost Woods West Area'), #pearl ('West Death Mountain Drop', 'West Death Mountain (Bottom)'), @@ -1267,6 +1337,7 @@ mandatory_connections = [# Intra-tile OW Connections ('GT Entry Leave', 'West Dark Death Mountain (Top)'), ('Floating Island Drop', 'East Dark Death Mountain (Top)'), ('Dark Death Mountain Drop (East)', 'East Dark Death Mountain (Bottom)'), + ('East Dark Death Mountain Bushes', 'East Dark Death Mountain (Bushes)'), ('Turtle Rock Ledge Drop', 'Turtle Rock Area'), ('Bumper Cave Entrance Rock', 'Bumper Cave Entrance'), #glove ('Bumper Cave Ledge Drop', 'Bumper Cave Area'), @@ -1359,47 +1430,14 @@ default_flute_connections = [ ] ow_connections = { - 0x00: ([ - ('Lost Woods East Mirror Spot', 'Lost Woods East Area'), - ('Lost Woods Entry Mirror Spot', 'Lost Woods West Area'), - ('Lost Woods Pedestal Mirror Spot', 'Lost Woods West Area'), - ('Lost Woods Southwest Mirror Spot', 'Lost Woods West Area'), - ('Lost Woods East (Forgotten) Mirror Spot', 'Lost Woods East Area'), - ('Lost Woods West (Forgotten) Mirror Spot', 'Lost Woods West Area') - ], [ - ('Skull Woods Back Mirror Spot', 'Skull Woods Forest (West)'), - ('Skull Woods Forgotten (West) Mirror Spot', 'Skull Woods Forgotten Path (Southwest)'), - ('Skull Woods Forgotten (East) Mirror Spot', 'Skull Woods Forgotten Path (Northeast)'), - ('Skull Woods Portal Entry Mirror Spot', 'Skull Woods Portal Entry'), - ('Skull Woods Forgotten (Middle) Mirror Spot', 'Skull Woods Forgotten Path (Northeast)'), - ('Skull Woods Front Mirror Spot', 'Skull Woods Forest') - ]), - 0x02: ([ - ('Lumberjack Mirror Spot', 'Lumberjack Area') - ], [ - ('Dark Lumberjack Mirror Spot', 'Dark Lumberjack Area') - ]), 0x03: ([ - ('Spectacle Rock Mirror Spot', 'Spectacle Rock Ledge'), - ('West Death Mountain (Top) Mirror Spot', 'West Death Mountain (Top)'), ('West Death Mountain Teleporter', 'West Dark Death Mountain (Bottom)') ], [ ('Spectacle Rock Leave', 'West Death Mountain (Top)'), ('Spectacle Rock Approach', 'Spectacle Rock Ledge'), - ('West Dark Death Mountain (Top) Mirror Spot', 'West Dark Death Mountain (Top)'), - ('Bubble Boy Mirror Spot', 'West Dark Death Mountain (Bottom)'), - ('West Dark Death Mountain (Bottom) Mirror Spot', 'West Dark Death Mountain (Bottom)'), ('Dark Death Mountain Teleporter (West)', 'West Death Mountain (Bottom)') ]), 0x05: ([ - ('East Death Mountain (Top West) Mirror Spot', 'East Death Mountain (Top West)'), - ('East Death Mountain (Top East) Mirror Spot', 'East Death Mountain (Top East)'), - ('Spiral Cave Mirror Spot', 'Spiral Cave Ledge'), - ('Mimic Cave Mirror Spot', 'Mimic Cave Ledge'), - ('Isolated Ledge Mirror Spot', 'Fairy Ascension Ledge'), - ('Fairy Ascension Mirror Spot', 'Fairy Ascension Plateau'), - ('Death Mountain Bridge Mirror Spot', 'East Death Mountain (Bottom Left)'), - ('Floating Island Mirror Spot', 'Death Mountain Floating Island'), ('East Death Mountain Teleporter', 'East Dark Death Mountain (Bottom)') ], [ ('Floating Island Bridge (West)', 'East Death Mountain (Top East)'), @@ -1411,252 +1449,91 @@ ow_connections = { ('Spiral Ledge Approach', 'Spiral Cave Ledge'), ('Mimic Ledge Approach', 'Mimic Cave Ledge'), ('Spiral Mimic Ledge Drop', 'Fairy Ascension Ledge'), - ('East Dark Death Mountain (Top West) Mirror Spot', 'East Dark Death Mountain (Top)'), - ('East Dark Death Mountain (Top East) Mirror Spot', 'East Dark Death Mountain (Top)'), - ('TR Ledge (West) Mirror Spot', 'Dark Death Mountain Ledge'), - ('TR Ledge (East) Mirror Spot', 'Dark Death Mountain Ledge'), - ('TR Isolated Mirror Spot', 'Dark Death Mountain Isolated Ledge'), - ('East Dark Death Mountain (Bottom Plateau) Mirror Spot', 'East Dark Death Mountain (Bottom)'), - ('East Dark Death Mountain (Bottom Left) Mirror Spot', 'East Dark Death Mountain (Bottom Left)'), - ('East Dark Death Mountain (Bottom) Mirror Spot', 'East Dark Death Mountain (Bottom)'), - ('Dark Floating Island Mirror Spot', 'Dark Death Mountain Floating Island'), ('Dark Death Mountain Teleporter (East)', 'East Death Mountain (Bottom)') ]), 0x07: ([ - ('TR Pegs Area Mirror Spot', 'Death Mountain TR Pegs'), ('TR Pegs Teleporter', 'Turtle Rock Ledge') ], [ ('Turtle Rock Tail Ledge Drop', 'Turtle Rock Ledge'), - ('Turtle Rock Mirror Spot', 'Turtle Rock Area'), - ('Turtle Rock Ledge Mirror Spot', 'Turtle Rock Ledge'), ('Turtle Rock Teleporter', 'Death Mountain TR Pegs Ledge') ]), - 0x0a: ([ - ('Mountain Entry Mirror Spot', 'Mountain Entry Area'), - ('Mountain Entry Entrance Mirror Spot', 'Mountain Entry Entrance'), - ('Mountain Entry Ledge Mirror Spot', 'Mountain Entry Ledge') - ], [ - ('Bumper Cave Area Mirror Spot', 'Bumper Cave Area'), - ('Bumper Cave Entry Mirror Spot', 'Bumper Cave Entrance'), - ('Bumper Cave Ledge Mirror Spot', 'Bumper Cave Ledge') - ]), - 0x0f: ([ - ('Zora Waterfall Mirror Spot', 'Zora Waterfall Area') - ], [ - ('Catfish Mirror Spot', 'Catfish Area') - ]), 0x10: ([ - ('Lost Woods Pass West Mirror Spot', 'Lost Woods Pass West Area'), - ('Lost Woods Pass East Top Mirror Spot', 'Lost Woods Pass East Top Area'), - ('Lost Woods Pass Portal Mirror Spot', 'Lost Woods Pass Portal Area'), - ('Lost Woods Pass East Bottom Mirror Spot', 'Lost Woods Pass East Bottom Area'), ('Kakariko Teleporter', 'Skull Woods Pass Portal Area') ], [ - ('Skull Woods Pass West Mirror Spot', 'Skull Woods Pass West Area'), - ('Skull Woods Pass East Top Mirror Spot', 'Skull Woods Pass East Top Area'), - ('Skull Woods Pass Portal Mirror Spot', 'Skull Woods Pass Portal Area'), - ('Skull Woods Pass East Bottom Mirror Spot', 'Skull Woods Pass East Bottom Area'), ('West Dark World Teleporter', 'Lost Woods Pass Portal Area') ]), - 0x11: ([ - ('Kakariko Fortune Mirror Spot', 'Kakariko Fortune Area') - ], [ - ('Outcast Fortune Mirror Spot', 'Dark Fortune Area') - ]), - 0x12: ([ - ('Kakariko Pond Mirror Spot', 'Kakariko Pond Area') - ], [ - ('Outcast Pond Mirror Spot', 'Outcast Pond Area') - ]), - 0x13: ([ - ('Sanctuary Mirror Spot', 'Sanctuary Area'), - ('Bonk Rock Ledge Mirror Spot', 'Bonk Rock Ledge') - ], [ - ('Dark Chapel Mirror Spot', 'Dark Chapel Area'), - ('Dark Chapel Ledge Mirror Spot', 'Dark Chapel Area') - ]), 0x14: ([ - ('Graveyard Ledge Mirror Spot', 'Graveyard Ledge'), - ('Kings Grave Mirror Spot', 'Kings Grave Area') + ], [ ('Graveyard Ladder (Top)', 'Graveyard Area'), - ('Graveyard Ladder (Bottom)', 'Graveyard Ledge'), - ('Dark Graveyard Mirror Spot', 'Dark Graveyard Area'), - ('Dark Graveyard Ledge Mirror Spot', 'Dark Graveyard Area'), - ('Dark Graveyard Grave Mirror Spot', 'Dark Graveyard Area') - ]), - 0x15: ([ - ('River Bend Mirror Spot', 'River Bend Area'), - ('River Bend East Mirror Spot', 'River Bend East Bank') - ], [ - ('Qirn Jump Mirror Spot', 'Qirn Jump Area'), - ('Qirn Jump East Mirror Spot', 'Qirn Jump East Bank') - ]), - 0x16: ([ - ('Potion Shop Mirror Spot', 'Potion Shop Area'), - ('Potion Shop Northeast Mirror Spot', 'Potion Shop Northeast') - ], [ - ('Dark Witch Mirror Spot', 'Dark Witch Area'), - ('Dark Witch Northeast Mirror Spot', 'Dark Witch Northeast') - ]), - 0x17: ([ - ('Zora Approach Mirror Spot', 'Zora Approach Area'), - ('Zora Approach Ledge Mirror Spot', 'Zora Approach Ledge') - ], [ - ('Catfish Approach Mirror Spot', 'Catfish Approach Area'), - ('Catfish Approach Ledge Mirror Spot', 'Catfish Approach Ledge') - ]), - 0x18: ([ - ('Kakariko Mirror Spot', 'Kakariko Area'), - ('Kakariko Grass Mirror Spot', 'Kakariko Area') - ], [ - ('Village of Outcasts Mirror Spot', 'Village of Outcasts Area'), - ('Village of Outcasts Southwest Mirror Spot', 'Village of Outcasts Area'), - ('Hammer House Mirror Spot', 'Dark Grassy Lawn') - ]), - 0x1a: ([ - ('Forgotton Forest Mirror Spot', 'Forgotten Forest Area'), - ('Forgotton Forest Fence Mirror Spot', 'Forgotten Forest Area') - ], [ - ('Shield Shop Mirror Spot', 'Shield Shop Area') + ('Graveyard Ladder (Bottom)', 'Graveyard Ledge') ]), 0x1b: ([ - ('HC Ledge Mirror Spot', 'Hyrule Castle Ledge'), - ('HC Courtyard Mirror Spot', 'Hyrule Castle Courtyard'), - ('HC Area Mirror Spot', 'Hyrule Castle Area'), - ('HC Courtyard Left Mirror Spot', 'Hyrule Castle Courtyard'), - ('HC Area South Mirror Spot', 'Hyrule Castle Area'), - ('HC East Entry Mirror Spot', 'Hyrule Castle East Entry'), ('Top of Pyramid', 'Pyramid Area'), ('Top of Pyramid (Inner)', 'Pyramid Area') ], [ - ('Pyramid Mirror Spot', 'Pyramid Area'), - ('Pyramid Pass Mirror Spot', 'Pyramid Pass'), - ('Pyramid Courtyard Mirror Spot', 'Pyramid Area'), - ('Pyramid Uncle Mirror Spot', 'Pyramid Area'), - ('Pyramid From Ledge Mirror Spot', 'Pyramid Area'), - ('Pyramid Entry Mirror Spot', 'Pyramid Area'), ('Post Aga Inverted Teleporter', 'Hyrule Castle Area') ]), - 0x1d: ([ - ('Wooden Bridge Mirror Spot', 'Wooden Bridge Area'), - ('Wooden Bridge Northeast Mirror Spot', 'Wooden Bridge Area'), - ('Wooden Bridge West Mirror Spot', 'Wooden Bridge Area') - ], [ - ('Broken Bridge West Mirror Spot', 'Broken Bridge West'), - ('Broken Bridge East Mirror Spot', 'Broken Bridge Area'), - ('Broken Bridge Northeast Mirror Spot', 'Broken Bridge Northeast') - ]), 0x1e: ([ - ('Eastern Palace Mirror Spot', 'Eastern Palace Area'), ('Eastern Palace Ledge Drop', 'Eastern Palace Area'), # OWG ('Palace of Darkness Ledge Drop', 'Palace of Darkness Area') # OWG ], [ - ('Palace of Darkness Mirror Spot', 'Palace of Darkness Area'), ('Eastern Palace Ledge Drop', 'Palace of Darkness Area'), # OWG ('Palace of Darkness Ledge Drop', 'Eastern Palace Area') # OWG ]), - 0x22: ([ - ('Blacksmith Mirror Spot', 'Blacksmith Area'), - ('Blacksmith Entry Mirror Spot', 'Blacksmith Area'), - ('Bat Cave Ledge Mirror Spot', 'Bat Cave Ledge') - ], [ - ('Hammer Pegs Mirror Spot', 'Hammer Pegs Area'), - ('Hammer Pegs Entry Mirror Spot', 'Hammer Pegs Entry') - ]), 0x25: ([ - ('Sand Dunes Mirror Spot', 'Sand Dunes Area'), ('Sand Dunes Ledge Drop', 'Sand Dunes Area'), # OWG ('Dark Dunes Ledge Drop', 'Dark Dunes Area') # OWG ], [ - ('Dark Dunes Mirror Spot', 'Dark Dunes Area'), ('Sand Dunes Ledge Drop', 'Dark Dunes Area'), # OWG ('Dark Dunes Ledge Drop', 'Sand Dunes Area') # OWG ]), - 0x28: ([ - ('Maze Race Mirror Spot', 'Maze Race Ledge'), - ('Maze Race Ledge Mirror Spot', 'Maze Race Ledge') - ], [ - ('Dig Game Mirror Spot', 'Dig Game Area'), - ('Dig Game Ledge Mirror Spot', 'Dig Game Ledge') - ]), 0x29: ([ - ('Kakariko Suburb Mirror Spot', 'Kakariko Suburb Area'), - ('Kakariko Suburb South Mirror Spot', 'Kakariko Suburb Area'), ('Suburb Cliff Ledge Drop', 'Kakariko Suburb Area'), # OWG ('Archery Game Cliff Ledge Drop', 'Archery Game Area') # OWG ], [ - ('Frog Mirror Spot', 'Frog Area'), - ('Frog Prison Mirror Spot', 'Frog Prison'), - ('Archery Game Mirror Spot', 'Archery Game Area'), ('Suburb Cliff Ledge Drop', 'Archery Game Area'), # OWG ('Archery Game Cliff Ledge Drop', 'Kakariko Suburb Area') # OWG ]), - 0x2a: ([ - ('Flute Boy Mirror Spot', 'Flute Boy Area'), - ('Flute Boy Pass Mirror Spot', 'Flute Boy Pass') - ], [ - ('Stumpy Mirror Spot', 'Stumpy Area'), - ('Stumpy Pass Mirror Spot', 'Stumpy Pass') - ]), 0x2b: ([ - ('Central Bonk Rocks Mirror Spot', 'Central Bonk Rocks Area'), ('Central Bonk Rocks Cliff Ledge Drop', 'Central Bonk Rocks Area'), # OWG ('Dark Bonk Rocks Cliff Ledge Drop', 'Dark Bonk Rocks Area') # OWG ], [ - ('Dark Bonk Rocks Mirror Spot', 'Dark Bonk Rocks Area'), ('Central Bonk Rocks Cliff Ledge Drop', 'Dark Bonk Rocks Area'), # OWG ('Dark Bonk Rocks Cliff Ledge Drop', 'Central Bonk Rocks Area') # OWG ]), 0x2c: ([ - ('Links House Mirror Spot', 'Links House Area'), ('Links House Cliff Ledge Drop', 'Links House Area'), # OWG ('Bomb Shop Cliff Ledge Drop', 'Big Bomb Shop Area') # OWG ], [ - ('Big Bomb Shop Mirror Spot', 'Big Bomb Shop Area'), ('Links House Cliff Ledge Drop', 'Big Bomb Shop Area'), # OWG ('Bomb Shop Cliff Ledge Drop', 'Links House Area') # OWG ]), 0x2d: ([ - ('Stone Bridge Mirror Spot', 'Stone Bridge North Area'), - ('Stone Bridge South Mirror Spot', 'Stone Bridge South Area'), - ('Hobo Mirror Spot', 'Stone Bridge Water'), ('Stone Bridge East Ledge Drop', 'Stone Bridge North Area'), # OWG ('Hammer Bridge North Ledge Drop', 'Hammer Bridge North Area'), # OWG ('Stone Bridge Cliff Ledge Drop', 'Stone Bridge South Area'), # OWG ('Hammer Bridge South Cliff Ledge Drop', 'Hammer Bridge South Area') # OWG ], [ - ('Hammer Bridge North Mirror Spot', 'Hammer Bridge North Area'), - ('Hammer Bridge South Mirror Spot', 'Hammer Bridge South Area'), - ('Dark Hobo Mirror Spot', 'Hammer Bridge Water'), ('Stone Bridge East Ledge Drop', 'Hammer Bridge North Area'), # OWG ('Hammer Bridge North Ledge Drop', 'Stone Bridge North Area'), # OWG ('Stone Bridge Cliff Ledge Drop', 'Hammer Bridge South Area'), # OWG ('Hammer Bridge South Cliff Ledge Drop', 'Stone Bridge South Area') # OWG ]), 0x2e: ([ - ('Tree Line Mirror Spot', 'Tree Line Area'), ('Tree Line Ledge Drop', 'Tree Line Area'), # OWG ('Dark Tree Line Ledge Drop', 'Dark Tree Line Area') # OWG ], [ - ('Dark Tree Line Mirror Spot', 'Dark Tree Line Area'), ('Tree Line Ledge Drop', 'Dark Tree Line Area'), # OWG ('Dark Tree Line Ledge Drop', 'Tree Line Area') # OWG ]), 0x2f: ([ - ('Eastern Nook Mirror Spot', 'Eastern Nook Area'), ('East Hyrule Teleporter', 'Palace of Darkness Nook Area') ], [ - ('Darkness Nook Mirror Spot', 'Palace of Darkness Nook Area'), ('East Dark World Teleporter', 'Eastern Nook Area') ]), 0x30: ([ - ('Desert Mirror Spot', 'Desert Area'), - ('Desert Ledge Mirror Spot', 'Desert Ledge'), - ('Checkerboard Mirror Spot', 'Desert Checkerboard Ledge'), - ('DP Stairs Mirror Spot', 'Desert Palace Stairs'), - ('DP Entrance (North) Mirror Spot', 'Desert Palace Entrance (North) Spot'), - ('Bombos Tablet Ledge Mirror Spot', 'Bombos Tablet Ledge'), + ('Mirror To Bombos Tablet Ledge', 'Bombos Tablet Ledge'), # OWG ('Desert Teleporter', 'Misery Mire Teleporter Ledge'), ('Desert Boss Cliff Ledge Drop', 'Desert Palace Entrance (North) Spot'), # OWG ('Mire Cliff Ledge Drop', 'Misery Mire Area'), # OWG @@ -1664,10 +1541,6 @@ ow_connections = { ], [ ('Checkerboard Ledge Approach', 'Desert Checkerboard Ledge'), ('Checkerboard Ledge Leave', 'Desert Area'), - ('Misery Mire Mirror Spot', 'Misery Mire Area'), - ('Misery Mire Ledge Mirror Spot', 'Misery Mire Area'), - ('Misery Mire Blocked Mirror Spot', 'Misery Mire Area'), - ('Misery Mire Main Mirror Spot', 'Misery Mire Area'), ('Misery Mire Teleporter', 'Desert Palace Teleporter Ledge'), ('Desert Boss Cliff Ledge Drop', 'Misery Mire Area'), # OWG ('Mire Cliff Ledge Drop', 'Desert Palace Entrance (North) Spot'), # OWG @@ -1675,21 +1548,15 @@ ow_connections = { ]), 0x32: ([ ('Cave 45 Ledge Drop', 'Flute Boy Approach Area'), - ('Flute Boy Entry Mirror Spot', 'Flute Boy Bush Entry'), - ('Cave 45 Mirror Spot', 'Cave 45 Ledge'), ('Cave 45 Cliff Ledge Drop', 'Cave 45 Ledge'), # OWG ('Stumpy Approach Cliff Ledge Drop', 'Stumpy Approach Area') # OWG ], [ ('Cave 45 Inverted Leave', 'Flute Boy Approach Area'), ('Cave 45 Inverted Approach', 'Cave 45 Ledge'), - ('Stumpy Approach Mirror Spot', 'Stumpy Approach Area'), - ('Stumpy Bush Entry Mirror Spot', 'Stumpy Approach Bush Entry'), ('Cave 45 Cliff Ledge Drop', 'Stumpy Approach Area'), # OWG ('Stumpy Approach Cliff Ledge Drop', 'Cave 45 Ledge') # OWG ]), 0x33: ([ - ('C Whirlpool Mirror Spot', 'C Whirlpool Area'), - ('C Whirlpool Outer Mirror Spot', 'C Whirlpool Outer Area'), ('South Hyrule Teleporter', 'Dark C Whirlpool Portal Area'), ('C Whirlpool Cliff Ledge Drop', 'C Whirlpool Area'), # OWG ('Dark C Whirlpool Cliff Ledge Drop', 'Dark C Whirlpool Area'), # OWG @@ -1700,8 +1567,6 @@ ow_connections = { ('Desert C Whirlpool Cliff Ledge Drop', 'C Whirlpool Outer Area'), # OWG ('Mire C Whirlpool Cliff Ledge Drop', 'Dark C Whirlpool Outer Area') # OWG ], [ - ('Dark C Whirlpool Mirror Spot', 'Dark C Whirlpool Area'), - ('Dark C Whirlpool Outer Mirror Spot', 'Dark C Whirlpool Outer Area'), ('South Dark World Teleporter', 'C Whirlpool Portal Area'), ('C Whirlpool Cliff Ledge Drop', 'Dark C Whirlpool Area'), # OWG ('Dark C Whirlpool Cliff Ledge Drop', 'C Whirlpool Area'), # OWG @@ -1713,23 +1578,13 @@ ow_connections = { ('Mire C Whirlpool Cliff Ledge Drop', 'C Whirlpool Outer Area') # OWG ]), 0x34: ([ - ('Statues Mirror Spot', 'Statues Area'), ('Statues Cliff Ledge Drop', 'Statues Area'), # OWG ('Hype Cliff Ledge Drop', 'Hype Cave Area') # OWG ], [ - ('Hype Cave Mirror Spot', 'Hype Cave Area'), ('Statues Cliff Ledge Drop', 'Hype Cave Area'), # OWG ('Hype Cliff Ledge Drop', 'Statues Area') # OWG ]), 0x35: ([ - ('Lake Hylia Mirror Spot', 'Lake Hylia Area'), - ('Lake Hylia Northeast Mirror Spot', 'Lake Hylia Northeast Bank'), - ('South Shore Mirror Spot', 'Lake Hylia South Shore'), - ('South Shore East Mirror Spot', 'Lake Hylia South Shore'), - ('Lake Hylia Island Mirror Spot', 'Lake Hylia Island'), - ('Lake Hylia Central Island Mirror Spot', 'Lake Hylia Central Island'), - ('Lake Hylia Water Mirror Spot', 'Lake Hylia Water'), - ('Lake Hylia Water D Mirror Spot', 'Lake Hylia Water D'), ('Lake Hylia Teleporter', 'Ice Palace Area'), #('Ice Palace Ledge Drop', 'Ice Lake Moat'), ('Lake Hylia Area Cliff Ledge Drop', 'Lake Hylia Area'), # OWG @@ -1738,58 +1593,281 @@ ow_connections = { ('Ice Palace Island FAWT Ledge Drop', 'Ice Lake Moat') # OWG ], [ ('Lake Hylia Island Pier', 'Lake Hylia Island'), - ('Ice Lake Mirror Spot', 'Ice Lake Area'), - ('Ice Lake Southwest Mirror Spot', 'Ice Lake Ledge (West)'), - ('Ice Lake Southeast Mirror Spot', 'Ice Lake Ledge (East)'), - ('Ice Lake Northeast Mirror Spot', 'Ice Lake Northeast Bank'), - ('Ice Palace Mirror Spot', 'Ice Palace Area'), - ('Ice Lake Moat Mirror Spot', 'Ice Lake Moat'), ('Ice Palace Teleporter', 'Lake Hylia Water D'), ('Lake Hylia Area Cliff Ledge Drop', 'Ice Lake Area'), # OWG ('Ice Lake Area Cliff Ledge Drop', 'Lake Hylia Area'), # OWG ('Lake Hylia Island FAWT Ledge Drop', 'Ice Lake Moat'), # OWG ('Ice Palace Island FAWT Ledge Drop', 'Lake Hylia Island') # OWG ]), - 0x37: ([ - ('Ice Cave Mirror Spot', 'Ice Cave Area') - ], [ - ('Shopping Mall Mirror Spot', 'Shopping Mall Area') - ]), 0x3a: ([ - ('Desert Pass Ledge Mirror Spot', 'Desert Pass Ledge'), - ('Desert Pass Mirror Spot', 'Desert Pass Area'), ('Desert Pass Cliff Ledge Drop', 'Desert Pass Area'), # OWG ('Swamp Nook Cliff Ledge Drop', 'Swamp Nook Area') # OWG ], [ ('Desert Pass Ladder (North)', 'Desert Pass Area'), ('Desert Pass Ladder (South)', 'Desert Pass Ledge'), - ('Swamp Nook Mirror Spot', 'Swamp Nook Area'), - ('Swamp Nook Southeast Mirror Spot', 'Swamp Nook Area'), - ('Swamp Nook Pegs Mirror Spot', 'Swamp Nook Area'), ('Desert Pass Cliff Ledge Drop', 'Swamp Nook Area'), # OWG ('Swamp Nook Cliff Ledge Drop', 'Desert Pass Area') # OWG ]), 0x3b: ([ - ('Dam Mirror Spot', 'Dam Area'), ('Dam Cliff Ledge Drop', 'Dam Area'), # OWG ('Swamp Cliff Ledge Drop', 'Swamp Area') # OWG ], [ - ('Swamp Mirror Spot', 'Swamp Area'), ('Dam Cliff Ledge Drop', 'Swamp Area'), # OWG ('Swamp Cliff Ledge Drop', 'Dam Area') # OWG - ]), - 0x3c: ([ - ('South Pass Mirror Spot', 'South Pass Area') - ], [ - ('Dark South Pass Mirror Spot', 'Dark South Pass Area') - ]), - 0x3f: ([ - ('Octoballoon Mirror Spot', 'Octoballoon Area') - ], [ - ('Bomber Corner Mirror Spot', 'Bomber Corner Area') ]) } +mirror_connections = { + 'Skull Woods Forest': ['Lost Woods East Area'], + 'Skull Woods Portal Entry': ['Lost Woods West Area'], + 'Skull Woods Forest (West)': ['Lost Woods West Area'], + 'Skull Woods Forgotten Path (Southwest)': ['Lost Woods West Area'], + 'Skull Woods Forgotten Path (Northeast)': ['Lost Woods East Area', 'Lost Woods West Area'], + + 'Dark Lumberjack Area': ['Lumberjack Area'], + + 'West Dark Death Mountain (Top)': ['West Death Mountain (Top)'], + 'West Dark Death Mountain (Bottom)': ['Spectacle Rock Ledge'], + + 'Dark Death Mountain Floating Island': ['Death Mountain Floating Island'], + 'East Dark Death Mountain (Top)': ['East Death Mountain (Top West)', 'East Death Mountain (Top East)'], + 'Dark Death Mountain Ledge': ['Spiral Cave Ledge', 'Mimic Cave Ledge'], + 'Dark Death Mountain Isolated Ledge': ['Fairy Ascension Ledge'], + 'East Dark Death Mountain (Bushes)': ['Fairy Ascension Plateau'], + 'East Dark Death Mountain (Bottom Left)': ['East Death Mountain (Bottom Left)'], + + 'Turtle Rock Area': ['Death Mountain TR Pegs'], + + 'Bumper Cave Area': ['Mountain Entry Area'], + 'Bumper Cave Entrance': ['Mountain Entry Entrance'], + 'Bumper Cave Ledge': ['Mountain Entry Ledge'], + + 'Catfish Area': ['Zora Waterfall Area'], + + 'Skull Woods Pass West Area': ['Lost Woods Pass West Area'], + 'Skull Woods Pass East Top Area': ['Lost Woods Pass East Top Area'], + 'Skull Woods Pass Portal Area': ['Lost Woods Pass Portal Area'], + 'Skull Woods Pass East Bottom Area': ['Lost Woods Pass East Bottom Area'], + + 'Dark Fortune Area': ['Kakariko Fortune Area'], + + 'Outcast Pond Area': ['Kakariko Pond Area'], + + 'Dark Chapel Area': ['Sanctuary Area', 'Bonk Rock Ledge'], + + 'Dark Graveyard Area': ['Graveyard Area'], + 'Dark Graveyard North': ['Graveyard Ledge', 'Kings Grave Area'], + + 'Qirn Jump Area': ['River Bend Area'], + 'Qirn Jump East Bank': ['River Bend East Bank'], + + 'Dark Witch Area': ['Potion Shop Area'], + 'Dark Witch Northeast': ['Potion Shop Northeast'], + + 'Catfish Approach Area': ['Zora Approach Area'], + 'Catfish Approach Ledge': ['Zora Approach Ledge'], + + 'Village of Outcasts Area': ['Kakariko Area'], + 'Dark Grassy Lawn': ['Kakariko Area'], + + 'Shield Shop Area': ['Forgotten Forest Area'], + 'Shield Shop Fence': ['Forgotten Forest Area'], + + 'Pyramid Area': ['Hyrule Castle Ledge', 'Hyrule Castle Courtyard', 'Hyrule Castle Area', 'Hyrule Castle East Entry'], + 'Pyramid Exit Ledge': ['Hyrule Castle Courtyard'], + 'Pyramid Pass': ['Hyrule Castle Area'], + + 'Broken Bridge Area': ['Wooden Bridge Area'], + 'Broken Bridge Northeast': ['Wooden Bridge Area'], + 'Broken Bridge West': ['Wooden Bridge Area'], + + 'Palace of Darkness Area': ['Eastern Palace Area'], + + 'Hammer Pegs Area': ['Blacksmith Area', 'Bat Cave Ledge'], + 'Hammer Pegs Entry': ['Blacksmith Area'], + + 'Dark Dunes Area': ['Sand Dunes Area'], + + 'Dig Game Area': ['Maze Race Ledge'], + 'Dig Game Ledge': ['Maze Race Ledge'], + + 'Frog Area': ['Kakariko Suburb Area'], + 'Archery Game Area': ['Kakariko Suburb Area'], + + 'Stumpy Area': ['Flute Boy Area'], + 'Stumpy Pass': ['Flute Boy Pass'], + + 'Dark Bonk Rocks Area': ['Central Bonk Rocks Area'], + + 'Big Bomb Shop Area': ['Links House Area'], + + 'Hammer Bridge North Area': ['Stone Bridge North Area'], + 'Hammer Bridge South Area': ['Stone Bridge South Area'], + 'Hammer Bridge Water': ['Stone Bridge Water'], + + 'Dark Tree Line Area': ['Tree Line Area'], + + 'Palace of Darkness Nook Area': ['Eastern Nook Area'], + + 'Misery Mire Area': ['Desert Area', 'Desert Ledge', 'Desert Checkerboard Ledge', 'Desert Palace Stairs', 'Desert Palace Entrance (North) Spot'], + + 'Stumpy Approach Area': ['Cave 45 Ledge'], + 'Stumpy Approach Bush Entry': ['Flute Boy Bush Entry'], + + 'Dark C Whirlpool Area': ['C Whirlpool Area'], + 'Dark C Whirlpool Outer Area': ['C Whirlpool Outer Area'], + + 'Hype Cave Area': ['Statues Area'], + + 'Ice Lake Area': ['Lake Hylia Area'], + 'Ice Lake Northeast Bank': ['Lake Hylia Northeast Bank'], + 'Ice Lake Ledge (West)': ['Lake Hylia South Shore'], + 'Ice Lake Ledge (East)': ['Lake Hylia South Shore'], + 'Ice Lake Water': ['Lake Hylia Island'], + 'Ice Palace Area': ['Lake Hylia Central Island'], + 'Ice Lake Moat': ['Lake Hylia Water', 'Lake Hylia Water D'], #needs flippers + + 'Shopping Mall Area': ['Ice Cave Area'], + + 'Swamp Nook Area': ['Desert Pass Area', 'Desert Pass Ledge'], + + 'Swamp Area': ['Dam Area'], + + 'Dark South Pass Area': ['South Pass Area'], + + 'Bomber Corner Area': ['Octoballoon Area'], + + + 'Lost Woods West Area': ['Skull Woods Forest (West)', 'Skull Woods Forgotten Path (Southwest)', 'Skull Woods Portal Entry'], + #'Lost Woods West Area': ['Skull Woods Forgotten Path (Northeast)'], # technically yes, but we dont need it + 'Lost Woods East Area': ['Skull Woods Forgotten Path (Northeast)', 'Skull Woods Forest'], + + 'Lumberjack Area': ['Dark Lumberjack Area'], + + 'West Death Mountain (Top)': ['West Dark Death Mountain (Top)'], + 'Spectacle Rock Ledge': ['West Dark Death Mountain (Bottom)'], + 'West Death Mountain (Bottom)': ['West Dark Death Mountain (Bottom)'], + + 'East Death Mountain (Top West)': ['East Dark Death Mountain (Top)'], + 'East Death Mountain (Top East)': ['East Dark Death Mountain (Top)'], + 'Spiral Cave Ledge': ['Dark Death Mountain Ledge'], + 'Mimic Cave Ledge': ['Dark Death Mountain Ledge'], + 'Fairy Ascension Ledge': ['Dark Death Mountain Isolated Ledge'], + 'Fairy Ascension Plateau': ['East Dark Death Mountain (Bottom)'], + 'East Death Mountain (Bottom Left)': ['East Dark Death Mountain (Bottom Left)'], + 'East Death Mountain (Bottom)': ['East Dark Death Mountain (Bottom)'], + 'Death Mountain Floating Island': ['Dark Death Mountain Floating Island'], + + 'Death Mountain TR Pegs': ['Turtle Rock Area'], + 'Death Mountain TR Pegs Ledge': ['Turtle Rock Ledge'], + + 'Mountain Entry Area': ['Bumper Cave Area'], + 'Mountain Entry Entrance': ['Bumper Cave Entrance'], + 'Mountain Entry Ledge': ['Bumper Cave Ledge'], + + 'Zora Waterfall Area': ['Catfish Area'], + + 'Lost Woods Pass West Area': ['Skull Woods Pass West Area'], + 'Lost Woods Pass East Top Area': ['Skull Woods Pass East Top Area'], + 'Lost Woods Pass Portal Area': ['Skull Woods Pass Portal Area'], + 'Lost Woods Pass East Bottom Area': ['Skull Woods Pass East Bottom Area'], + + 'Kakariko Fortune Area': ['Dark Fortune Area'], + + 'Kakariko Pond Area': ['Outcast Pond Area'], + + 'Sanctuary Area': ['Dark Chapel Area'], + 'Bonk Rock Ledge': ['Dark Chapel Area'], + + 'Graveyard Area': ['Dark Graveyard Area'], + 'Graveyard Ledge': ['Dark Graveyard Area'], + 'Kings Grave Area': ['Dark Graveyard Area'], + + 'River Bend Area': ['Qirn Jump Area'], + 'River Bend East Bank': ['Qirn Jump East Bank'], + + 'Potion Shop Area': ['Dark Witch Area'], + 'Potion Shop Northeast': ['Dark Witch Northeast'], + + 'Zora Approach Area': ['Catfish Approach Area'], + 'Zora Approach Ledge': ['Catfish Approach Ledge'], + + 'Kakariko Area': ['Village of Outcasts Area'], + 'Kakariko Southwest': ['Village of Outcasts Area'], + 'Kakariko Grass Yard': ['Dark Grassy Lawn'], + + 'Forgotten Forest Area': ['Shield Shop Area'], + + 'Hyrule Castle Area': ['Pyramid Area', 'Pyramid Pass'], + 'Hyrule Castle Southwest': ['Pyramid Pass'], + 'Hyrule Castle Courtyard': ['Pyramid Area'], + 'Hyrule Castle Courtyard Northeast': ['Pyramid Area'], + 'Hyrule Castle Ledge': ['Pyramid Area'], + 'Hyrule Castle East Entry': ['Pyramid Area'], + + 'Wooden Bridge Area': ['Broken Bridge Area', 'Broken Bridge West'], + 'Wooden Bridge Northeast': ['Broken Bridge Northeast'], + + 'Eastern Palace Area': ['Palace of Darkness Area'], + + 'Blacksmith Area': ['Hammer Pegs Area', 'Hammer Pegs Entry'], + + 'Sand Dunes Area': ['Dark Dunes Area'], + + 'Maze Race Area': ['Dig Game Area'], + 'Maze Race Ledge': ['Dig Game Ledge'], + + 'Kakariko Suburb Area': ['Frog Area', 'Frog Prison', 'Archery Game Area'], + + 'Flute Boy Area': ['Stumpy Area'], + 'Flute Boy Pass': ['Stumpy Pass'], + + 'Central Bonk Rocks Area': ['Dark Bonk Rocks Area'], + + 'Links House Area': ['Big Bomb Shop Area'], + + 'Stone Bridge North Area': ['Hammer Bridge North Area'], + 'Stone Bridge South Area': ['Hammer Bridge South Area'], + 'Stone Bridge Water': ['Hammer Bridge Water'], + + 'Tree Line Area': ['Dark Tree Line Area'], + + 'Eastern Nook Area': ['Palace of Darkness Nook Area'], + + 'Desert Area': ['Misery Mire Area'], + 'Desert Ledge': ['Misery Mire Area'], + 'Desert Palace Entrance (North) Spot': ['Misery Mire Area'], + 'Desert Checkerboard Ledge': ['Misery Mire Area'], + 'Desert Palace Stairs': ['Misery Mire Area'], + + 'Flute Boy Approach Area': ['Stumpy Approach Area'], + 'Cave 45 Ledge': ['Stumpy Approach Area'], + 'Flute Boy Bush Entry': ['Stumpy Approach Bush Entry'], + + 'C Whirlpool Area': ['Dark C Whirlpool Area'], + 'C Whirlpool Outer Area': ['Dark C Whirlpool Outer Area'], + + 'Statues Area': ['Hype Cave Area'], + + 'Lake Hylia Area': ['Ice Lake Area'], + 'Lake Hylia South Shore': ['Ice Lake Ledge (West)', 'Ice Lake Ledge (East)'], + 'Lake Hylia Northeast Bank': ['Ice Lake Northeast Bank'], + 'Lake Hylia Central Island': ['Ice Palace Area'], + 'Lake Hylia Water D': ['Ice Lake Moat'], + + 'Ice Cave Area': ['Shopping Mall Area'], + + 'Desert Pass Area': ['Swamp Nook Area'], + 'Desert Pass Southeast': ['Swamp Nook Area'], + 'Desert Pass Ledge': ['Swamp Nook Area'], + + 'Dam Area': ['Swamp Area'], + + 'South Pass Area': ['Dark South Pass Area'], + + 'Octoballoon Area': ['Bomber Corner Area'] +} + parallelsimilar_connections = [('Maze Race ES', 'Kakariko Suburb WS'), ('Dig Game EC', 'Frog WC') ] @@ -1939,6 +2017,52 @@ default_connections = [('Lost Woods NW', 'Master Sword Meadow SC'), ('East Dark Death Mountain EN', 'Turtle Rock WN') ] +one_way_ledges = { + 'West Death Mountain (Bottom)': {'West Death Mountain (Top)', + 'Spectacle Rock Ledge'}, + 'East Death Mountain (Bottom)': {'East Death Mountain (Top East)', + 'Spiral Cave Ledge'}, + 'Fairy Ascension Plateau': {'Fairy Ascension Ledge'}, + 'Mountain Entry Area': {'Mountain Entry Ledge'}, + 'Sanctuary Area': {'Bonk Rock Ledge'}, + 'Graveyard Area': {'Graveyard Ledge'}, + 'Potion Shop Water': {'Potion Shop Area', + 'Potion Shop Northeast'}, + 'Zora Approach Water': {'Zora Approach Area'}, + 'Hyrule Castle Area': {'Hyrule Castle Ledge'}, + 'Hyrule Castle Courtyard': {'Hyrule Castle Ledge'}, + 'Wooden Bridge Water': {'Wooden Bridge Area', + 'Wooden Bridge Northeast'}, + 'Maze Race Area': {'Maze Race Ledge', + 'Maze Race Prize'}, + 'Flute Boy Approach Area': {'Cave 45 Ledge'}, + 'Desert Area': {'Desert Ledge', + 'Desert Checkerboard Ledge', + 'Desert Palace Mouth', + 'Bombos Tablet Ledge', + 'Desert Palace Teleporter Ledge'}, + 'Desert Pass Area': {'Desert Pass Ledge'}, + 'Lake Hylia Water': {'Lake Hylia South Shore', + 'Lake Hylia Island'}, + 'West Dark Death Mountain (Bottom)': {'West Dark Death Mountain (Top)'}, + 'East Dark Death Mountain (Top)': {'Dark Death Mountain Floating Island'}, + 'East Dark Death Mountain (Bottom)': {'East Dark Death Mountain (Top)'}, + 'Turtle Rock Area': {'Turtle Rock Ledge'}, + 'Bumper Cave Area': {'Bumper Cave Ledge'}, + 'Qirn Jump Water': {'Qirn Jump Area'}, + 'Dark Witch Water': {'Dark Witch Area', + 'Dark Witch Northeast'}, + 'Catfish Approach Water': {'Catfish Approach Area'}, + 'Pyramid Area': {'Pyramid Exit Ledge'}, + 'Broken Bridge Water': {'Broken Bridge West', + 'Broken Bridge Area', + 'Broken Bridge Northeast'}, + 'Misery Mire Area': {'Misery Mire Teleporter Ledge'}, + 'Ice Lake Water': {'Ice Lake Area', + 'Ice Lake Ledge (West)', + 'Ice Lake Ledge (East)'} +} + isolated_regions = [ 'Death Mountain Floating Island', 'Mimic Cave Ledge', diff --git a/PotShuffle.py b/PotShuffle.py index cdb829f4..152f6756 100644 --- a/PotShuffle.py +++ b/PotShuffle.py @@ -93,12 +93,12 @@ vanilla_pots = { Pot(28, 23, PotItem.Nothing, 'Ice Pengator Switch', obj=RoomObject(0x1FC388, [0x3B, 0xBB, 0xFA])), Pot(86, 26, PotItem.Nothing, 'Ice Big Key', obj=RoomObject(0x1FC397, [0xAF, 0xD3, 0xFA])), Pot(86, 27, PotItem.Nothing, 'Ice Big Key', obj=RoomObject(0x1FC39A, [0xAF, 0xDB, 0xFA]))], - 0x21: [Pot(160, 20, PotItem.Nothing, 'Sewers Key Rat', obj=RoomObject(0x0A8C71, [0x43, 0xA7, 0xFA])), - Pot(168, 24, PotItem.SmallMagic, 'Sewers Key Rat', obj=RoomObject(0x0A8C7A, [0x53, 0xC7, 0xFA])), - Pot(48, 28, PotItem.Heart, 'Sewers Key Rat', obj=RoomObject(0x0A8C80, [0x63, 0xE3, 0xFA])), - Pot(82, 28, PotItem.SmallMagic, 'Sewers Key Rat', obj=RoomObject(0x0A8C7D, [0xA7, 0xE3, 0xFA])), - Pot(100, 28, PotItem.Nothing, 'Sewers Key Rat', obj=RoomObject(0x0A8C74, [0xCB, 0xE3, 0xFA])), - Pot(104, 28, PotItem.Nothing, 'Sewers Key Rat', obj=RoomObject(0x0A8C77, [0xD3, 0xE3, 0xFA]))], + 0x21: [Pot(160, 20, PotItem.Nothing, 'Sewers Dark Aquabats', obj=RoomObject(0x0A8C71, [0x43, 0xA7, 0xFA])), + Pot(168, 24, PotItem.SmallMagic, 'Sewers Dark Aquabats', obj=RoomObject(0x0A8C7A, [0x53, 0xC7, 0xFA])), + Pot(48, 28, PotItem.Heart, 'Sewers Dark Aquabats', obj=RoomObject(0x0A8C80, [0x63, 0xE3, 0xFA])), + Pot(82, 28, PotItem.SmallMagic, 'Sewers Dark Aquabats', obj=RoomObject(0x0A8C7D, [0xA7, 0xE3, 0xFA])), + Pot(100, 28, PotItem.Nothing, 'Sewers Dark Aquabats', obj=RoomObject(0x0A8C74, [0xCB, 0xE3, 0xFA])), + Pot(104, 28, PotItem.Nothing, 'Sewers Dark Aquabats', obj=RoomObject(0x0A8C77, [0xD3, 0xE3, 0xFA]))], 0x23: [Pot(86, 26, PotItem.OneRupee, 'TR Lazy Eyes', obj=RoomObject(0x1FED09, [0xAF, 0xD3, 0xFA])), Pot(90, 26, PotItem.Heart, 'TR Lazy Eyes', obj=RoomObject(0x1FED0C, [0xB7, 0xD3, 0xFA])), Pot(94, 26, PotItem.OneRupee, 'TR Lazy Eyes', obj=RoomObject(0x1FED0F, [0xBF, 0xD3, 0xFA])), @@ -879,7 +879,8 @@ def shuffle_pots(world, player): elif old_pot.item == PotItem.Switch: available_pots = (pot for pot in new_pots if (pot.room == old_pot.room or pot.room in movable_switch_rooms[old_pot.room]) and not (pot.flags & PotFlags.NoSwitch)) elif old_pot.item == PotItem.Key: - if world.doorShuffle[player] == 'vanilla' and not world.retro[player] and world.pottery[player] == 'none' and world.logic[player] != 'nologic': + if (world.doorShuffle[player] == 'vanilla' and world.keyshuffle[player] != 'universal' + and world.pottery[player] == 'none' and world.logic[player] != 'nologic'): available_pots = (pot for pot in new_pots if pot.room not in invalid_key_rooms) else: available_pots = new_pots @@ -890,7 +891,7 @@ def shuffle_pots(world, player): new_pot = random.choice(available_pots) new_pot.item = old_pot.item - if world.retro[player] and new_pot.item == PotItem.FiveArrows: + if world.bow_mode[player].startswith('retro') and new_pot.item == PotItem.FiveArrows: new_pot.item = PotItem.FiveRupees if new_pot.item == PotItem.Key: @@ -938,7 +939,7 @@ def shuffle_pot_switches(world, player): new_pot = random.choice(available_pots) new_pot.item, old_pot.item = old_pot.item, new_pot.item - if world.retro[player] and new_pot.item == PotItem.FiveArrows: + if world.bow_mode[player].startswith('retro') and new_pot.item == PotItem.FiveArrows: new_pot.item = PotItem.FiveRupees if new_pot.item == PotItem.Switch and (new_pot.flags & PotFlags.SwitchLogicChange): diff --git a/RELEASENOTES.md b/RELEASENOTES.md index dcb80a60..d5a492b1 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,270 +1,169 @@ -## New Features +# New Features -## Pottery Lottery and Key Drop Shuffle Changes +One major change with this update is that big key doors and certain trap doors are no longer guaranteed to be vanilla in Dungeon Door Shuffle modes even if you choose not to shuffle those types. A newer algorithm for putting dungeons together has been written and it will remove big key doors and trap doors when necessary to ensure progress can be made. -### Pottery +Please note that retro features are now independently customizable as referenced below. Selecting Retro mode or World State: Retro will change Bow Mode to Retro (Progressive). Take Anys to Random, and Small Keys to Universal. -New pottery option that control which pots (and large blocks) are in the locations pool: +## Flute Mode -* None: No pots are in the pool, like normal randomizer -* Key Pots: The pots that have keys are in the pool. This is about half of the old keydropshuffle option -* Cave Pots: The pots that are not found in dungeons are in the pool. (Includes the large block in Spike Cave). Does -not include key pots. -* CaveKeys: Both non-dungeon pots and pots that used to have keys are in the pool. -* Reduced: Same as CaveKeys but also roughly a quarter of dungeon pots are added to the location pool picked at random. This is a dynamic mode so pots in the pool will be colored. Pots out of the pool will have vanilla contents. -* Clustered: LIke reduced but pot are grouped by logical sets and roughly 50% of pots are chosen from those group. This is a dynamic mode like the above. -* Nonempty: All pots that had some sort of objects under them are chosen to be in the location pool. This excludes most large blocks and some pots out of dungeons. -* Dungeon Pots: The pots that are in dungeons are in the pool. (Includes serveral large blocks) -* Lottery: All pots and large blocks are in the pool +Normal mode for flute means you need to activate it at the village statue after finding it like usual. +Activated flute mode mean you can use it immediately upon finding it. the flute SFX plays to let you know this is the case. -By default, switches remain in their vanilla location (unless you turn on the legacy option below) +## Bow Mode -CLI `--pottery