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..ce2e419e 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,7 @@ class World(object): self.shuffle = shuffle.copy() self.doorShuffle = doorShuffle.copy() self.intensity = {} + self.door_type_mode = {} self.logic = logic.copy() self.mode = mode.copy() self.swords = swords.copy() @@ -71,7 +72,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 +105,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 +128,15 @@ 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 +149,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 +159,23 @@ 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('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' + 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 +457,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) @@ -586,6 +593,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): @@ -655,7 +663,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]: @@ -827,7 +835,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 +859,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) @@ -894,6 +904,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 +988,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 +1029,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 +1089,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) @@ -1207,7 +1218,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) @@ -1253,7 +1264,8 @@ class CollectionState(object): 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)) @@ -1496,7 +1508,7 @@ 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])) @@ -2276,7 +2288,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 +2569,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 +2654,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])) @@ -2794,9 +2805,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 +2821,13 @@ 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, + 'decoupledoors': self.world.decoupledoors, 'dungeon_counters': self.world.dungeon_counters, 'item_pool': self.world.difficulty, 'item_functionality': self.world.difficulty_adjustments, @@ -2996,7 +3012,6 @@ 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']: @@ -3008,6 +3023,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 +3043,15 @@ 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('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,7 +3059,7 @@ 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]) @@ -3311,12 +3332,12 @@ 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} @@ -3326,13 +3347,13 @@ goal_mode = {"ganon": 0, "pedestal": 1, "dungeons": 2, "triforcehunt": 3, "cryst 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 +3363,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 +3378,27 @@ 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_silver': 3} + # additions # psuedoboots does not effect code # sfx_shuffle and other adjust items does not effect 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 +3406,52 @@ 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]]) + ]) 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 +3459,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 +3468,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 +3491,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 +3502,28 @@ 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] + 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/CLI.py b/CLI.py index bfd09183..7210c080 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,50 @@ 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) 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']: value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name) if player == 1: setattr(ret, name, {1: value}) @@ -135,19 +162,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 +193,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 +205,20 @@ 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", + "decoupledoors": False, "experimental": False, "dungeon_counters": "default", "mixed_travel": "prevent", @@ -214,8 +248,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..c047e9b4 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -4,19 +4,24 @@ 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 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 generate_dungeon +from DungeonGenerator import ExplorationState, convert_regions, pre_validate, 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, check_bk_special from Utils import ncr, kth_combination @@ -84,7 +89,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': @@ -126,14 +131,21 @@ def link_doors_prep(world, player): def link_doors_main(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]) - + if pool: + main_dungeon_pool(pool, world, player) if world.doorShuffle[player] != 'vanilla': create_door_spoiler(world, player) @@ -161,6 +173,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 +189,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,11 +226,12 @@ def vanilla_key_logic(world, player): world.dungeon_layouts[player][builder.name] = builder add_inaccessible_doors(world, player) + entrances_map, potentials, connections = determine_entrance_list(world, player) for builder in builders: - origin_list = find_accessible_entrances(world, player, builder) + origin_list = entrances_map[builder.name] 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) + 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) @@ -257,9 +276,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 +312,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 +354,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 +403,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 +460,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 +477,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, + candidates = find_portal_candidates(master_door_list, dungeon, custom, allowed, need_passage=True, bk_shuffle=bk_shuffle, rupee_bow=rupee_bow_flag) - choice, portal = assign_portal(candidates, possible_portals, world, player) + 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, 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 +496,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 +536,29 @@ 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) + 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) + else: + door = world.get_door(dest['dest'], player) + assigned_doors.add(door) + return custom_portals, assigned_doors + + def analyze_portals(world, player): info_map = {} for dungeon, portal_list in dungeon_portals.items(): @@ -575,13 +638,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 +659,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 +754,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 +776,198 @@ 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' + 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' + at.dungeon_items.append(at_compass) + at_map = ItemFactory('Map (Agahnims Tower)', player) + at_map.advancement = world.restrict_boss_items[player] != 'none' + 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 +1006,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 +1015,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 +1033,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 +1069,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 +1107,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: @@ -1000,10 +1244,11 @@ def cross_dungeon(world, player): at.dungeon_items.append(at_compass) 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 +1258,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 +1310,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 +1344,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 +1360,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 +1388,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 +1424,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 +1445,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 +1583,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 +1667,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 +1679,385 @@ 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 + # todo: dash/bomb door pool specific + 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 + if d.dest.type in [DoorType.Interior, DoorType.Normal]: + 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, 1] + 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, 5)] + + +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() + + world.paired_doors[player].clear() + used_doors = shuffle_trap_doors(door_type_pools, paths, start_regions_map, world, player) + # big keys + used_doors = shuffle_big_key_doors(door_type_pools, used_doors, start_regions_map, world, player) + # small keys + used_doors = shuffle_small_key_doors(door_type_pools, used_doors, start_regions_map, world, player) + # bombable / dashable + used_doors = shuffle_bomb_dash_doors(door_type_pools, used_doors, start_regions_map, world, player) + # handle paired list + + +def shuffle_trap_doors(door_type_pools, paths, start_regions_map, world, player): + used_doors = set() + for pool, door_type_pool in door_type_pools: + ttl = 0 + suggestion_map, trap_map, flex_map = {}, {}, {} + remaining = door_type_pool.traps + if player in world.custom_door_types: + 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] + find_trappable_candidates(builder, world, player) + if custom_trap_doors[dungeon]: + builder.candidates.trap = filter_key_door_pool(builder.candidates.trap, custom_trap_doors[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 + 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, 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: + 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 custom_bk_doors[dungeon]: + builder.candidates.big = filter_key_door_pool(builder.candidates.big, custom_bk_doors[dungeon]) + remaining -= len(custom_bk_doors[dungeon]) + ttl += len(builder.candidates.big) + if ttl == 0: + continue + for dungeon in pool: + builder = world.dungeon_layouts[player][dungeon] + proportion = len(builder.candidates.big) + calc = int(round(proportion * door_type_pool.bigs/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, world, player): + 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: + 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) + if custom_key_doors[dungeon]: + builder.candidates.small = filter_key_door_pool(builder.candidates.small, custom_key_doors[dungeon]) + remaining -= len(custom_key_doors[dungeon]) + builder.key_doors_num = max(0, len(builder.candidates.small) - builder.key_drop_cnt) + total_keys -= builder.key_drop_cnt + ttl += builder.key_doors_num + 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) + suggested = min(calculated, limit) + combo_size = ncr(len(builder.candidates.small), suggested + builder.key_drop_cnt) + while combo_size > 500000 and suggested > 0: + suggested -= 1 + combo_size = ncr(len(builder.candidates.small), suggested + builder.key_drop_cnt) + suggestion_map[dungeon] = builder.key_doors_num = suggested + builder.key_drop_cnt + remaining -= suggested + builder.key_drop_cnt + builder.combo_size = combo_size + flex_map[dungeon] = (limit - suggested) if suggested < limit else 0 + for dungeon in pool: + builder = world.dungeon_layouts[player][dungeon] + if total_adjustable: + builder.total_keys = suggestion_map[dungeon] + 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 + 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 + 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 = actual_chest_keys + 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, 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: + 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 custom_bomb_doors[dungeon]: + builder.candidates.bomb_dash = filter_key_door_pool(builder.candidates.bomb_dash, custom_bomb_doors[dungeon]) + remaining_bomb -= len(custom_bomb_doors[dungeon]) + if custom_dash_doors[dungeon]: + builder.candidates.bomb_dash = filter_key_door_pool(builder.candidates.bomb_dash, custom_dash_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 +2098,499 @@ 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: + 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 'Boss' not in d.name and 'Agahnim' not in d.name: + 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: + 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) + 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)) + 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(d.name, 'Trap Door', player) + logger.debug('Trap Door: %s', d.name) + + +def find_current_trap_doors(builder): + 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: + 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.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: + 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) + 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 + 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(d1.name+' <-> '+d2.name, 'Big Key Door', player) + logger.debug(f'Big Key Door: {d1.name} <-> {d2.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(d.name, 'Big Key Door', player) + logger.debug(f'Big Key Door: {d.name}') + + +def change_door_to_big_key(d, world, player): + d.bigKey = True + 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: + 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) + # 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) + 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 +2602,198 @@ 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 + + +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: + 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 + else: + dash_doors_needed -= 1 + 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(d1.name+' <-> '+d2.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(d.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 +2829,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 +2849,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 +2865,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 +2909,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(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) + 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(d.name, 'Key Door', player) + logger.debug('Key Door: %s', d.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 +3083,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 +3092,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 +3125,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,22 +3147,28 @@ 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' @@ -1951,6 +3264,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']) @@ -2057,11 +3378,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 +3451,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 +3636,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 +3771,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 +3935,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 +4106,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 +4126,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 +4226,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'), @@ -3181,4 +4519,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, 2, 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..32ffd9ae 100644 --- a/Doors.py +++ b/Doors.py @@ -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), @@ -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), @@ -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), @@ -1480,6 +1486,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) @@ -1572,7 +1580,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..b1ce5456 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 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: @@ -786,6 +811,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,8 +843,17 @@ 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() @@ -844,9 +879,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 +997,20 @@ 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 special_big_key_doors) 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 +1210,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 +1241,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 +1258,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 +1292,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 +1308,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 +1321,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: @@ -1286,6 +1341,10 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player, 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 +1355,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 +1364,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 +1373,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 +1386,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 +1431,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 +1451,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 +1463,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 +1518,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 +1562,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 +1574,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 +1589,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 +1627,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 +1637,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): @@ -2077,16 +2149,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 +2181,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 +2708,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 +2865,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 +2977,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 +3036,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 +3056,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 +3099,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,7 +3503,7 @@ 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(): @@ -3402,7 +3525,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 +3545,27 @@ 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): 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 +3966,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 +3995,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..3497e427 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -2039,7 +2039,8 @@ 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 +# 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 @@ -2378,7 +2379,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 +2660,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..021888e8 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: @@ -160,7 +161,7 @@ def valid_key_placement(item, location, key_pool, 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,7 +173,7 @@ 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] + wild_keys = world.keyshuffle[item.player] != 'none' return key_logic.check_placement(unplaced_keys, wild_keys, location if item.bigkey else None, prize_loc, cr_count) else: return not item.is_inside_dungeon_item(world) @@ -216,7 +217,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 +289,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) @@ -426,7 +427,8 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None # 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) + progitempool.sort(key=lambda item: 1 if item.name == 'Small Key (Escape)' + and world.keyshuffle[item.player] != 'none' and world.mode[item.player] == 'standard' else 0) 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 @@ -490,14 +492,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 +510,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 +531,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): @@ -672,7 +682,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 +757,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 +833,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 +975,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..fa94d448 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 @@ -147,6 +147,7 @@ class InitialSram: '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), + '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..b1f195af 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 @@ -279,13 +279,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] @@ -311,7 +315,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 +336,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): @@ -365,7 +370,7 @@ def generate_itempool(world, player): 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 +379,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 +398,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,16 +438,17 @@ 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]: @@ -436,8 +466,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 +479,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 +499,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 +523,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 +706,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 +759,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 +786,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 +813,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 +957,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,7 +978,8 @@ 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 = [] @@ -965,12 +1015,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 +1050,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) @@ -1052,13 +1098,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 +1121,7 @@ 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): +def make_custom_item_pool(world, player, progressive, shuffle, difficulty, timer, goal, mode, swords, bombbag, customitemarray): pool = [] placed_items = {} precollected_items = [] @@ -1155,7 +1202,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,17 +1213,10 @@ 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 @@ -1208,12 +1248,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,12 +1315,73 @@ 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)) + + ttl_locations = sum(1 for x in world.get_unfilled_locations(player) if '- Prize' not in x.name) + pool_size = len(get_player_dungeon_item_pool(world, player)) + len(pool) + + if pool_size < ttl_locations: + amount_to_add = ttl_locations - pool_size + pool.extend(random.choices(list(filler_items.keys()), filler_items.values(), k=amount_to_add)) + 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 get_player_dungeon_item_pool(world, player): + return [item for dungeon in world.dungeons for item in dungeon.all_items + if dungeon.player == player and item.location is None] + + # 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 @@ -1246,31 +1397,120 @@ 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 + 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'] + + +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..12150c86 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -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) @@ -223,13 +224,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 +253,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 +277,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 +303,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 +928,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 +939,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): @@ -1172,6 +1174,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 +1413,51 @@ def prize_relevance(key_layout, dungeon_entrance, is_atgt_swapped): return None +def prize_relevance_sig2(start_regions, d_name, dungeon_entrance, world, player): + if len(start_regions) > 1 and dungeon_entrance and dungeon_table[d_name].prize: + if dungeon_entrance.name == ('Agahmins Tower' if world.is_atgt_swapped(player) 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, 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 len(state.bk_found) > 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: @@ -1442,7 +1492,8 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa # 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 +1583,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) @@ -1565,7 +1616,7 @@ def determine_prize_lock(key_layout, world, player): def cnt_avail_small_locations(free_locations, key_only, state, world, player): - if not world.keyshuffle[player] and not world.retro[player]: + if world.keyshuffle[player] == 'none': 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) return max(0, avail_chest_keys + key_only - state.used_smalls) @@ -1573,7 +1624,7 @@ def cnt_avail_small_locations(free_locations, key_only, state, world, player): def cnt_avail_small_locations_by_ctr(free_locations, counter, layout, world, player): - if not world.keyshuffle[player] and not world.retro[player]: + if world.keyshuffle[player] == 'none': 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) return max(0, avail_chest_keys + len(counter.key_only_locations) - counter.used_keys) @@ -1599,12 +1650,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: @@ -1865,7 +1917,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 +2068,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..ca8f6856 100644 --- a/Main.py +++ b/Main.py @@ -25,14 +25,17 @@ 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.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.0-u' from source.classes.BabelFish import BabelFish @@ -58,32 +61,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 +115,8 @@ 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.decoupledoors = args.decoupledoors.copy() world.experimental = args.experimental.copy() world.dungeon_counters = args.dungeon_counters.copy() world.fish = fish @@ -112,13 +129,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 +159,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) + outfilebase = f'OR_{args.outputname if args.outputname else world.seed}' for player in range(1, world.players + 1): @@ -151,7 +173,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 +196,25 @@ 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 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) + 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): @@ -196,9 +239,12 @@ def main(args, seed=None, fish=None): link_entrances(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 +252,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 +273,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 +321,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 +332,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 +454,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 +483,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 +502,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() @@ -517,12 +575,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 +631,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() @@ -775,8 +830,8 @@ def create_playthrough(world): 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/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..1905b64f 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,270 +1,109 @@ -## 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