diff --git a/BaseClasses.py b/BaseClasses.py index 5832c956..34e5c898 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -26,6 +26,7 @@ class World(object): self.owCrossed = owCrossed.copy() self.owKeepSimilar = {} self.owMixed = owMixed.copy() + self.owWhirlpoolShuffle = {} self.owFluteShuffle = {} self.shuffle = shuffle.copy() self.doorShuffle = doorShuffle.copy() @@ -76,6 +77,7 @@ class World(object): self.spoiler = Spoiler(self) self.lamps_needed_for_dark_rooms = 1 self.owswaps = {} + self.owwhirlpools = {} self.owedges = [] self._owedge_cache = {} self.owflutespots = {} @@ -105,6 +107,7 @@ class World(object): set_player_attr('_region_cache', {}) set_player_attr('player_names', []) set_player_attr('owswaps', [[],[],[]]) + set_player_attr('owwhirlpools', []) set_player_attr('remote_items', False) set_player_attr('required_medallions', ['Ether', 'Quake']) set_player_attr('swamp_patch_required', False) @@ -112,7 +115,7 @@ class World(object): set_player_attr('ganon_at_pyramid', True) set_player_attr('ganonstower_vanilla', True) set_player_attr('sewer_light_cone', self.mode[player] == 'standard') - set_player_attr('fix_trock_doors', self.shuffle[player] != 'vanilla' or ((self.mode[player] == 'inverted') != (0x05 in self.owswaps[player][0] and self.owMixed[player]))) + set_player_attr('fix_trock_doors', self.shuffle[player] != 'vanilla' or ((self.mode[player] == 'inverted') != 0x05 in self.owswaps[player][0])) set_player_attr('fix_skullwoods_exit', self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple'] or self.doorShuffle[player] not in ['vanilla']) set_player_attr('fix_palaceofdarkness_exit', self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple']) set_player_attr('fix_trock_exit', self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple']) @@ -120,7 +123,7 @@ 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 ['crossed', 'insanity', 'madness_legacy']) + set_player_attr('fix_fake_world', logic[player] not in ['owglitches', 'nologic'] or shuffle[player] in ['lean', 'crossed', 'insanity', 'madness_legacy']) set_player_attr('mapshuffle', False) set_player_attr('compassshuffle', False) set_player_attr('keyshuffle', False) @@ -621,7 +624,7 @@ class CollectionState(object): queue.append((new_entrance, new_crystal_state)) # else those connections that are not accessible yet if self.is_small_door(connection): - door = connection.door + door = connection.door if connection.door.smallKey else connection.door.controller dungeon_name = connection.parent_region.dungeon.name key_logic = self.world.key_logic[player][dungeon_name] if door.name not in self.reached_doors[player]: @@ -635,7 +638,7 @@ class CollectionState(object): checklist[connection.name] = (connection, crystal_state) elif door.name not in self.opened_doors[player]: opened_doors = self.opened_doors[player] - door = connection.door + door = connection.door if connection.door.smallKey else connection.door.controller if door.name not in opened_doors: self.door_counter[player][1][dungeon_name] += 1 opened_doors.add(door.name) @@ -1018,7 +1021,12 @@ class CollectionState(object): @staticmethod def is_small_door(connection): - return connection and connection.door and connection.door.smallKey + return connection and connection.door and (connection.door.smallKey or + CollectionState.is_controlled_by_small(connection)) + + @staticmethod + def is_controlled_by_small(connection): + return connection.door.controller and connection.door.controller.smallKey def is_door_open(self, door_name, player): return door_name in self.opened_doors[player] @@ -1251,7 +1259,7 @@ class CollectionState(object): def can_use_bombs(self, player): if self.world.swords[player] == 'bombs': return self.has_bomb_level(player, 1) - return (not self.world.bombbag[player] or self.has('Bomb Upgrade (+10)', player)) and self.can_farm_bombs(player) + return (not self.world.bombbag[player] or self.has('Bomb Upgrade (+10)', player)) and ((hasattr(self.world, "override_bomb_check") and self.world.override_bomb_check) or self.can_farm_bombs(player)) def can_hit_crystal(self, player): return (self.can_use_bombs(player) @@ -1384,6 +1392,8 @@ class CollectionState(object): return self.has('Fire Rod', player) or self.has('Lamp', player) def can_flute(self, player): + if any(map(lambda i: i.name == 'Ocarina', self.world.precollected_items)): + return True lw = self.world.get_region('Kakariko Area', player) return self.has('Ocarina', player) and lw.can_reach(self) and self.is_not_bunny(lw, player) @@ -1952,6 +1962,7 @@ class Door(object): self.bk_shuffle_req = False self.standard_restricted = False # flag if portal is not allowed in HC in standard self.lw_restricted = False # flag if portal is not allowed in DW + self.rupee_bow_restricted = False # flag if portal is not allowed in HC in standard+rupee_bow # self.incognitoPos = -1 # self.sectorLink = False @@ -2708,6 +2719,57 @@ class Spoiler(object): else: self.doorTypes[(doorNames, player)] = OrderedDict([('player', player), ('doorNames', doorNames), ('type', type)]) + def parse_meta(self): + from Main import __version__ as ERVersion + from OverworldShuffle import __version__ as ORVersion + + self.startinventory = list(map(str, self.world.precollected_items)) + self.metadata = {'version': ERVersion, + '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, + 'goal': self.world.goal, + 'ow_shuffle': self.world.owShuffle, + 'ow_crossed': self.world.owCrossed, + 'ow_keepsimilar': self.world.owKeepSimilar, + 'ow_mixed': self.world.owMixed, + 'ow_whirlpool': self.world.owWhirlpoolShuffle, + 'ow_fluteshuffle': self.world.owFluteShuffle, + 'shuffle': self.world.shuffle, + 'shuffleganon': self.world.shuffle_ganon, + 'shufflelinks': self.world.shufflelinks, + 'door_shuffle': self.world.doorShuffle, + 'intensity': self.world.intensity, + 'item_pool': self.world.difficulty, + 'item_functionality': self.world.difficulty_adjustments, + 'gt_crystals': self.world.crystals_needed_for_gt, + 'ganon_crystals': self.world.crystals_needed_for_ganon, + 'ganon_vulnerability_item': self.world.ganon_item, + 'open_pyramid': self.world.open_pyramid, + 'accessibility': self.world.accessibility, + 'hints': self.world.hints, + 'mapshuffle': self.world.mapshuffle, + 'compassshuffle': self.world.compassshuffle, + 'keyshuffle': self.world.keyshuffle, + 'bigkeyshuffle': self.world.bigkeyshuffle, + 'boss_shuffle': self.world.boss_shuffle, + 'enemy_shuffle': self.world.enemy_shuffle, + 'enemy_health': self.world.enemy_health, + 'enemy_damage': self.world.enemy_damage, + 'potshuffle': self.world.potshuffle, + 'players': self.world.players, + 'teams': self.world.teams, + 'experimental': self.world.experimental, + 'keydropshuffle': self.world.keydropshuffle, + 'shopsanity': self.world.shopsanity, + 'triforcegoal': self.world.treasure_hunt_count, + 'triforcepool': self.world.treasure_hunt_total, + 'code': {p: Settings.make_code(self.world, p) for p in range(1, self.world.players + 1)} + } + def parse_data(self): self.medallions = OrderedDict() if self.world.players == 1: @@ -2718,8 +2780,6 @@ class Spoiler(object): self.medallions[f'Misery Mire ({self.world.get_player_names(player)})'] = self.world.required_medallions[player][0] self.medallions[f'Turtle Rock ({self.world.get_player_names(player)})'] = self.world.required_medallions[player][1] - self.startinventory = list(map(str, self.world.precollected_items)) - self.locations = OrderedDict() listed_locations = set() @@ -2790,53 +2850,8 @@ class Spoiler(object): for portal in self.world.dungeon_portals[player]: self.set_lobby(portal.name, portal.door.name, player) - from Main import __version__ as ERVersion - from OverworldShuffle import __version__ as ORVersion - self.metadata = {'version': ERVersion, - '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, - 'goal': self.world.goal, - 'ow_shuffle': self.world.owShuffle, - 'ow_crossed': self.world.owCrossed, - 'ow_keepsimilar': self.world.owKeepSimilar, - 'ow_mixed': self.world.owMixed, - 'ow_fluteshuffle': self.world.owFluteShuffle, - 'shuffle': self.world.shuffle, - 'door_shuffle': self.world.doorShuffle, - 'intensity': self.world.intensity, - 'item_pool': self.world.difficulty, - 'item_functionality': self.world.difficulty_adjustments, - 'gt_crystals': self.world.crystals_needed_for_gt, - 'ganon_crystals': self.world.crystals_needed_for_ganon, - 'ganon_vulnerability_item': self.world.ganon_item, - 'open_pyramid': self.world.open_pyramid, - 'accessibility': self.world.accessibility, - 'hints': self.world.hints, - 'mapshuffle': self.world.mapshuffle, - 'compassshuffle': self.world.compassshuffle, - 'keyshuffle': self.world.keyshuffle, - 'bigkeyshuffle': self.world.bigkeyshuffle, - 'boss_shuffle': self.world.boss_shuffle, - 'enemy_shuffle': self.world.enemy_shuffle, - 'enemy_health': self.world.enemy_health, - 'enemy_damage': self.world.enemy_damage, - 'potshuffle': self.world.potshuffle, - 'players': self.world.players, - 'teams': self.world.teams, - 'experimental': self.world.experimental, - 'keydropshuffle': self.world.keydropshuffle, - 'shopsanity': self.world.shopsanity, - 'triforcegoal': self.world.treasure_hunt_count, - 'triforcepool': self.world.treasure_hunt_total, - 'seed': self.world.seed, - 'code': {p: Settings.make_code(self.world, p) for p in range(1, self.world.players + 1)} - } - def to_json(self): + self.parse_meta() self.parse_data() out = OrderedDict() out['Overworld'] = list(self.overworlds.values()) @@ -2858,8 +2873,8 @@ class Spoiler(object): return json.dumps(out) - def to_file(self, filename): - self.parse_data() + def meta_to_file(self, filename): + self.parse_meta() with open(filename, 'w') as outfile: line_width = 35 outfile.write('ALttP Entrance Randomizer - Seed: %s\n\n' % (self.world.seed)) @@ -2871,9 +2886,6 @@ class Spoiler(object): for player in range(1, self.world.players + 1): if self.world.players > 1: outfile.write('\nPlayer %d: %s\n' % (player, self.world.get_player_names(player))) - if len(self.hashes) > 0: - for team in range(self.world.teams): - outfile.write('%s%s\n' % (f"Hash - {self.world.player_names[player][team]} (Team {team+1}): " if self.world.teams > 1 else 'Hash: ', self.hashes[player, team])) 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]) @@ -2883,39 +2895,76 @@ class Spoiler(object): if self.metadata['goal'][player] == 'triforcehunt': outfile.write('Triforce Pieces Required:'.ljust(line_width) + '%s\n' % self.metadata['triforcegoal'][player]) outfile.write('Triforce Pieces Total:'.ljust(line_width) + '%s\n' % self.metadata['triforcepool'][player]) + outfile.write('Crystals Required for GT:'.ljust(line_width) + '%s\n' % str(self.world.crystals_gt_orig[player])) + outfile.write('Crystals Required for Ganon:'.ljust(line_width) + '%s\n' % str(self.world.crystals_ganon_orig[player])) + outfile.write('Ganon Vulnerability Item:'.ljust(line_width) + '%s\n' % str(self.metadata['ganon_vulnerability_item'][player])) + outfile.write('Accessibility:'.ljust(line_width) + '%s\n' % self.metadata['accessibility'][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('Shopsanity:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['shopsanity'][player] else 'No')) + outfile.write('Bombbag:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['bombbag'][player] else 'No')) outfile.write('Overworld Layout Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['ow_shuffle'][player]) if self.metadata['ow_shuffle'][player] != 'vanilla': outfile.write('Keep Similar OW Edges Together:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['ow_keepsimilar'][player] else 'No')) outfile.write('Crossed OW:'.ljust(line_width) + '%s\n' % self.metadata['ow_crossed'][player]) outfile.write('Swapped OW (Mixed):'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['ow_mixed'][player] else 'No')) + outfile.write('Whirlpool Shuffle:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['ow_whirlpool'][player] else 'No')) outfile.write('Flute Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['ow_fluteshuffle'][player]) outfile.write('Entrance Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['shuffle'][player]) + outfile.write('Shuffle GT/Ganon:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['shuffleganon'][player] else 'No')) + outfile.write('Shuffle Links:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['shufflelinks'][player] else 'No')) + outfile.write('Pyramid Hole Pre-opened:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['open_pyramid'][player] else 'No')) outfile.write('Door Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['door_shuffle'][player]) outfile.write('Intensity:'.ljust(line_width) + '%s\n' % self.metadata['intensity'][player]) - addition = ' (Random)' if self.world.crystals_gt_orig[player] == 'random' else '' - outfile.write('Crystals required for GT:'.ljust(line_width) + '%s\n' % (str(self.metadata['gt_crystals'][player]) + addition)) - addition = ' (Random)' if self.world.crystals_ganon_orig[player] == 'random' else '' - outfile.write('Crystals required for Ganon:'.ljust(line_width) + '%s\n' % (str(self.metadata['ganon_crystals'][player]) + addition)) - addition = ' (Random)' if self.world.ganon_item_orig[player] == 'random' else '' - outfile.write('Ganon Vulnerability Item:'.ljust(line_width) + '%s\n' % (str(self.metadata['ganon_vulnerability_item'][player]) + addition)) - outfile.write('Pyramid hole pre-opened:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['open_pyramid'][player] else 'No')) - outfile.write('Accessibility:'.ljust(line_width) + '%s\n' % self.metadata['accessibility'][player]) - outfile.write('Map shuffle:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['mapshuffle'][player] else 'No')) - outfile.write('Compass shuffle:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['compassshuffle'][player] else 'No')) - outfile.write('Small Key shuffle:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['keyshuffle'][player] else 'No')) - outfile.write('Big Key shuffle:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['bigkeyshuffle'][player] else 'No')) - outfile.write('Boss shuffle:'.ljust(line_width) + '%s\n' % self.metadata['boss_shuffle'][player]) - outfile.write('Enemy shuffle:'.ljust(line_width) + '%s\n' % self.metadata['enemy_shuffle'][player]) - outfile.write('Enemy health:'.ljust(line_width) + '%s\n' % self.metadata['enemy_health'][player]) - outfile.write('Enemy damage:'.ljust(line_width) + '%s\n' % self.metadata['enemy_damage'][player]) - outfile.write('Pot shuffle:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['potshuffle'][player] else 'No')) - outfile.write('Hints:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['hints'][player] else 'No')) outfile.write('Experimental:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['experimental'][player] else 'No')) - outfile.write('Key Drops shuffled:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['keydropshuffle'][player] else 'No')) - outfile.write('Shopsanity:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['shopsanity'][player] else 'No')) - outfile.write('Bombbag:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['bombbag'][player] else 'No')) + outfile.write('Pot Shuffle:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['potshuffle'][player] else 'No')) + outfile.write('Key Drop Shuffle:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['keydropshuffle'][player] else 'No')) + outfile.write('Map Shuffle:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['mapshuffle'][player] else 'No')) + outfile.write('Compass Shuffle:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['compassshuffle'][player] else 'No')) + outfile.write('Small Key Shuffle:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['keyshuffle'][player] else 'No')) + outfile.write('Big Key Shuffle:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['bigkeyshuffle'][player] else 'No')) + outfile.write('Boss Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['boss_shuffle'][player]) + outfile.write('Enemy Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['enemy_shuffle'][player]) + outfile.write('Enemy Health:'.ljust(line_width) + '%s\n' % self.metadata['enemy_health'][player]) + outfile.write('Enemy Damage:'.ljust(line_width) + '%s\n' % self.metadata['enemy_damage'][player]) + outfile.write('Hints:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['hints'][player] else 'No')) + + if self.startinventory: + outfile.write('Starting Inventory:'.ljust(line_width)) + outfile.write('\n'.ljust(line_width+1).join(self.startinventory)) + + def to_file(self, filename): + self.parse_data() + with open(filename, 'a') as outfile: + line_width = 35 + if self.world.players > 1: + outfile.write('\nHashes:') + for player in range(1, self.world.players + 1): + if self.world.players > 1: + outfile.write('\nPlayer %d: %s\n' % (player, self.world.get_player_names(player))) + if len(self.hashes) > 0: + for team in range(self.world.teams): + outfile.write('%s%s\n' % (f"Hash - {self.world.player_names[player][team]} (Team {team+1}): " if self.world.teams > 1 else 'Hash: ', self.hashes[player, team])) + outfile.write('\n\nRequirements:\n\n') + for dungeon, medallion in self.medallions.items(): + outfile.write(f'{dungeon}:'.ljust(line_width) + '%s Medallion\n' % medallion) + for player in range(1, self.world.players + 1): + player_name = '' if self.world.players == 1 else str(' (Player ' + str(player) + ')') + if self.world.crystals_gt_orig[player] == 'random': + outfile.write(str('Crystals Required for GT' + player_name + ':').ljust(line_width) + '%s\n' % (str(self.metadata['gt_crystals'][player]))) + if self.world.crystals_ganon_orig[player] == 'random': + outfile.write(str('Crystals Required for Ganon' + player_name + ':').ljust(line_width) + '%s\n' % (str(self.metadata['ganon_crystals'][player]))) + + if self.overworlds: + # overworlds: overworld transitions; + outfile.write('\n\nOverworld:\n\n') + outfile.write('\n'.join(['%s%s %s %s' % (f'{self.world.get_player_names(entry["player"])}: ' if self.world.players > 1 else '', self.world.fish.translate("meta","overworlds",entry['entrance']), '<=>' if entry['direction'] == 'both' else '<=' if entry['direction'] == 'exit' else '=>', self.world.fish.translate("meta","overworlds",entry['exit'])) for entry in self.overworlds.values()])) + + if self.entrances: + # entrances: To/From overworld; Checking w/ & w/out "Exit" and translating accordingly + outfile.write('\n\nEntrances:\n\n') + outfile.write('\n'.join(['%s%s %s %s' % (f'{self.world.get_player_names(entry["player"])}: ' if self.world.players > 1 else '', self.world.fish.translate("meta","entrances",entry['entrance']), '<=>' if entry['direction'] == 'both' else '<=' if entry['direction'] == 'exit' else '=>', self.world.fish.translate("meta","entrances",entry['exit'])) for entry in self.entrances.values()])) + if self.doors: outfile.write('\n\nDoors:\n\n') outfile.write('\n'.join( @@ -2936,21 +2985,7 @@ class Spoiler(object): # doorTypes: Small Key, Bombable, Bonkable outfile.write('\n\nDoor Types:\n\n') outfile.write('\n'.join(['%s%s %s' % ('Player {0}: '.format(entry['player']) if self.world.players > 1 else '', self.world.fish.translate("meta","doors",entry['doorNames']), self.world.fish.translate("meta","doorTypes",entry['type'])) for entry in self.doorTypes.values()])) - if self.entrances: - # entrances: To/From overworld; Checking w/ & w/out "Exit" and translating accordingly - outfile.write('\n\nEntrances:\n\n') - outfile.write('\n'.join(['%s%s %s %s' % (f'{self.world.get_player_names(entry["player"])}: ' if self.world.players > 1 else '', self.world.fish.translate("meta","entrances",entry['entrance']), '<=>' if entry['direction'] == 'both' else '<=' if entry['direction'] == 'exit' else '=>', self.world.fish.translate("meta","entrances",entry['exit'])) for entry in self.entrances.values()])) - if self.overworlds: - # overworlds: overworld transitions; - outfile.write('\n\nOverworld:\n\n') - outfile.write('\n'.join(['%s%s %s %s' % (f'{self.world.get_player_names(entry["player"])}: ' if self.world.players > 1 else '', self.world.fish.translate("meta","overworlds",entry['entrance']), '<=>' if entry['direction'] == 'both' else '<=' if entry['direction'] == 'exit' else '=>', self.world.fish.translate("meta","overworlds",entry['exit'])) for entry in self.overworlds.values()])) - outfile.write('\n\nMedallions:\n') - for dungeon, medallion in self.medallions.items(): - outfile.write(f'\n{dungeon}: {medallion} Medallion') - if self.startinventory: - outfile.write('\n\nStarting Inventory:\n\n') - outfile.write('\n'.join(self.startinventory)) - + # locations: Change up location names; in the instance of a location with multiple sections, it'll try to translate the room name # items: Item names outfile.write('\n\nLocations:\n\n') @@ -2967,6 +3002,8 @@ class Spoiler(object): outfile.write(f'\n\nBosses ({self.world.get_player_names(player)}):\n\n') outfile.write('\n'.join([f'{x}: {y}' for x, y in bossmap.items() if y not in ['Agahnim', 'Agahnim 2', 'Ganon']])) + def playthru_to_file(self, filename): + with open(filename, 'a') as outfile: # locations: Change up location names; in the instance of a location with multiple sections, it'll try to translate the room name # items: Item names outfile.write('\n\nPlaythrough:\n\n') @@ -2995,7 +3032,6 @@ class Spoiler(object): outfile.write('\n'.join(path_listings)) - flooded_keys = { 'Trench 1 Switch': 'Swamp Palace - Trench 1 Pot Key', 'Trench 2 Switch': 'Swamp Palace - Trench 2 Pot Key' @@ -3071,7 +3107,7 @@ class Pot(object): # 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": 3, "full": 3, "crossed": 4, "insanity": 5, "dungeonsfull": 7, "dungeonssimple": 7} +er_mode = {"vanilla": 0, "simple": 1, "restricted": 2, "full": 3, "lite": 4, "lean": 5, "crossed": 6, "insanity": 7, "dungeonsfull": 8, "dungeonssimple": 9} # byte 1: LLLW WSSS (logic, mode, sword) logic_mode = {"noglitches": 0, "minorglitches": 1, "nologic": 2, "owglitches": 3, "majorglitches": 4} diff --git a/CHANGELOG.md b/CHANGELOG.md index 43071f28..f5d855fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,26 @@ # Changelog +### 0.2.1.2 +- Fixed issue with whirlpools not changing world when in Crossed OW + +### 0.2.1.1 +- Many fixes to ER: infinite loops, preventing cross-world scenarios in non-cross-world modes +- Spoiler log improvements, outputs in stages so a Spoiler is available if an error occurs +- Added no_race option for Mystery +- Fixed output_path in Mystery to use the saved setting if none is specified on CLI + +### 0.2.1.0 +- Implemented Whirlpool Shuffle + +### 0.2.0.0 +- Massive overhaul of ER algorithm +- Added 2 new ER modes (Lite and Lean) +- Added new mystery options (Logic/Shuffle Ganon) +- Smith deletion on S+Q only occurs if Blacksmith not reachable from starting locations +- Spoiler log improvements to prevent spoiling in the beginning 'meta' section +- Various minor fixes and improvements +- ~~Merged DR v0.5.1.4 - ROM bug fixes/keylogic improvements~~ + ### 0.1.9.4 - Hotfix for bad 0.1.9.3 version diff --git a/CLI.py b/CLI.py index c0b13d53..5bbcf572 100644 --- a/CLI.py +++ b/CLI.py @@ -94,10 +94,10 @@ def parse_cli(argv, no_defaults=False): playerargs = parse_cli(shlex.split(getattr(ret, f"p{player}")), True) for name in ['logic', 'mode', 'swords', 'goal', 'difficulty', 'item_functionality', - 'ow_shuffle', 'ow_crossed', 'ow_keepsimilar', 'ow_mixed', 'ow_fluteshuffle', + 'ow_shuffle', 'ow_crossed', 'ow_keepsimilar', 'ow_mixed', 'ow_whirlpool', 'ow_fluteshuffle', 'shuffle', 'door_shuffle', 'intensity', 'crystals_ganon', 'crystals_gt', 'ganon_item', 'openpyramid', 'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory', - 'bombbag', + 'bombbag', 'shuffleganon', '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', @@ -150,6 +150,7 @@ def parse_settings(): "ow_crossed": "none", "ow_keepsimilar": False, "ow_mixed": False, + "ow_whirlpool": False, "ow_fluteshuffle": "vanilla", "shuffle": "vanilla", "shufflelinks": False, diff --git a/DoorShuffle.py b/DoorShuffle.py index ca60be21..0c2a9992 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -368,7 +368,8 @@ def choose_portals(world, player): if world.doorShuffle[player] in ['basic', 'crossed']: cross_flag = world.doorShuffle[player] == 'crossed' - bk_shuffle = world.bigkeyshuffle[player] + # key drops allow the big key in the right place in Desert Tiles 2 + bk_shuffle = world.bigkeyshuffle[player] or world.keydropshuffle[player] std_flag = world.mode[player] == 'standard' # roast incognito doors world.get_room(0x60, player).delete(5) @@ -415,6 +416,7 @@ def choose_portals(world, player): 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 if hc_flag: sanc = world.get_portal('Sanctuary', player) sanc.destination = True @@ -424,14 +426,14 @@ def choose_portals(world, player): info.required_passage = {x: y for x, y in info.required_passage.items() if len(y) > 0} for target_region, possible_portals in info.required_passage.items(): candidates = find_portal_candidates(master_door_list, dungeon, need_passage=True, crossed=cross_flag, - bk_shuffle=bk_shuffle) + bk_shuffle=bk_shuffle, rupee_bow=rupee_bow_flag) choice, portal = assign_portal(candidates, possible_portals, 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) + crossed=cross_flag, 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) if choice.deadEnd: @@ -443,7 +445,7 @@ def choose_portals(world, player): 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, - bk_shuffle=bk_shuffle, standard=hc_flag) + bk_shuffle=bk_shuffle, standard=hc_flag, rupee_bow=rupee_bow_flag) choice, portal = assign_portal(candidates, outstanding_portals, world, player) clean_up_portal_assignment(portal_assignment, dungeon, portal, master_door_list, outstanding_portals) @@ -562,7 +564,7 @@ def disconnect_portal(portal, world, player): def find_portal_candidates(door_list, dungeon, need_passage=False, dead_end_allowed=False, crossed=False, - bk_shuffle=False, standard=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')] @@ -574,6 +576,8 @@ def find_portal_candidates(door_list, dungeon, need_passage=False, dead_end_allo ret = [x for x in ret if not x.deadEnd] if standard: ret = [x for x in ret if not x.standard_restricted] + if rupee_bow: + ret = [x for x in ret if not x.rupee_bow_restricted] return ret @@ -1833,8 +1837,8 @@ def find_inaccessible_regions(world, player): queue.append(connect) world.inaccessible_regions[player].extend([r.name for r in all_regions.difference(visited_regions) if valid_inaccessible_region(r)]) if (world.mode[player] == 'inverted') != (0x1b in world.owswaps[player][0] and world.owMixed[player]): - ledge = world.get_region('Hyrule Castle Ledge', 1) - if any(x for x in ledge.exits if x.connected_region.name == 'Agahnims Tower Portal'): + ledge = world.get_region('Hyrule Castle Ledge', player) + if any(x for x in ledge.exits if x.connected_region and x.connected_region.name == 'Agahnims Tower Portal'): world.inaccessible_regions[player].append('Hyrule Castle Ledge') logger = logging.getLogger('') logger.debug('Inaccessible Regions:') @@ -2047,7 +2051,7 @@ class DROptions(Flag): Town_Portal = 0x02 # If on, Players will start with mirror scroll Map_Info = 0x04 Debug = 0x08 - # Rails = 0x10 # Unused bit now + Fix_EG = 0x10 # used to be Rails = 0x10 # Unused bit now OriginalPalettes = 0x20 # Open_PoD_Wall = 0x40 # No longer pre-opening pod wall - unused # Open_Desert_Wall = 0x80 # No longer pre-opening desert wall - unused diff --git a/Doors.py b/Doors.py index b0979a5d..a23f46f0 100644 --- a/Doors.py +++ b/Doors.py @@ -1490,8 +1490,11 @@ def create_doors(world, player): world.get_door('GT Petting Zoo SE', player).dead_end() world.get_door('GT DMs Room SW', player).dead_end() world.get_door("GT Bob\'s Room SE", player).passage = False - world.get_door('Desert Tiles 2 SE', player).bk_shuffle_req = True # key-drop note (todo) - world.get_door('Swamp Lobby S', player).standard_restricted = True # key-drop note (todo) + world.get_door('Desert Tiles 2 SE', player).bk_shuffle_req = True # key-drop note: allows this to be a portal + world.get_door('Swamp Lobby S', player).standard_restricted = True + world.get_door('PoD Mimics 2 SW', player).rupee_bow_restricted = True # bow statue + # enemizer logic could get rid of the following restriction + world.get_door('PoD Pit Room S', player).rupee_bow_restricted = True # so mimics 1 shouldn't be required # can't unlink from boss right now world.get_door('Hera Lobby S', player).dungeonLink = 'Tower of Hera' diff --git a/EntranceShuffle.py b/EntranceShuffle.py index a44edf0e..e845587f 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -1,28 +1,36 @@ -import RaceRandom as random - -# ToDo: With shuffle_ganon option, prevent gtower from linking to an exit only location through a 2 entrance cave. +import logging from collections import defaultdict +import RaceRandom as random +from BaseClasses import CollectionState, RegionType, Terrain +from OWEdges import OWTileRegions entrance_pool = list() exit_pool = list() +entrance_exits = list() ignore_pool = False +suppress_spoiler = True def link_entrances(world, player): invFlag = world.mode[player] == 'inverted' - global entrance_pool, exit_pool, ignore_pool + global entrance_pool, exit_pool, ignore_pool, suppress_spoiler, entrance_exits + entrance_exits = list() + ignore_pool = False + suppress_spoiler = True + links_house = False entrance_pool = Entrance_Pool_Base.copy() exit_pool = Exit_Pool_Base.copy() drop_connections = default_drop_connections.copy() dropexit_connections = default_dropexit_connections.copy() - isolated_entrances = Isolated_LH_Doors.copy() - # modifications to lists - Dungeon_Exits = Dungeon_Exits_Base.copy() + Dungeon_Exits = LW_Dungeon_Exits + DW_Mid_Dungeon_Exits + DW_Late_Dungeon_Exits Cave_Exits = Cave_Exits_Base.copy() Old_Man_House = Old_Man_House_Base.copy() Cave_Three_Exits = Cave_Three_Exits_Base.copy() + sectors = build_sectors(world, player) + + # modifications to lists if invFlag == (0x1b in world.owswaps[player][0] and world.owMixed[player]): drop_connections.append(tuple(('Pyramid Hole', 'Pyramid'))) dropexit_connections.append(tuple(('Pyramid Entrance', 'Pyramid Exit'))) @@ -42,6 +50,7 @@ def link_entrances(world, player): connect_simple(world, 'Old Man S&Q', 'West Dark Death Mountain (Bottom)', player) unbias_some_entrances(Dungeon_Exits, Cave_Exits, Old_Man_House, Cave_Three_Exits) + Cave_Exits.extend(Cave_Exits_Directional) # setup mandatory connections for exitname, regionname in mandatory_connections: @@ -58,15 +67,14 @@ def link_entrances(world, player): connect_custom(world, player) - if invFlag == (0x05 in world.owswaps[player][0] and world.owMixed[player]): - isolated_entrances.append('Mimic Cave') - # if we do not shuffle, set default connections if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull']: for entrancename, exitname in default_connections + drop_connections + default_item_connections + default_shop_connections: connect_logical(world, entrancename, exitname, player, False) for entrancename, exitname in default_connector_connections + dropexit_connections: connect_logical(world, entrancename, exitname, player, True) + if invFlag: + world.get_entrance('Dark Sanctuary Hint Exit', player).connect(world.get_entrance('Dark Sanctuary Hint', player).parent_region) if not invFlag: for entrancename, exitname in open_default_connections: @@ -107,24 +115,48 @@ def link_entrances(world, player): for entrancename, exitname in inverted_default_dungeon_connections: connect_logical(world, entrancename, exitname, player, True) elif world.shuffle[player] == 'dungeonssimple': + suppress_spoiler = False simple_shuffle_dungeons(world, player) elif world.shuffle[player] == 'dungeonsfull': + suppress_spoiler = False full_shuffle_dungeons(world, Dungeon_Exits, player) elif world.shuffle[player] == 'simple': + suppress_spoiler = False simple_shuffle_dungeons(world, player) - old_man_entrances = list(Old_Man_Entrances) if not invFlag else list(Inverted_Old_Man_Entrances) + # shuffle dropdowns + scramble_holes(world, player) + + # list modification + lw_wdm_entrances = ['Old Man Cave (East)', 'Old Man House (Bottom)', 'Old Man House (Top)', + 'Death Mountain Return Cave (East)', 'Spectacle Rock Cave', + 'Spectacle Rock Cave Peak', 'Spectacle Rock Cave (Bottom)'] + lw_edm_entrances = ['Paradox Cave (Bottom)', 'Paradox Cave (Middle)', 'Paradox Cave (Top)', 'Spiral Cave', + 'Fairy Ascension Cave (Bottom)', 'Fairy Ascension Cave (Top)', 'Spiral Cave (Bottom)'] + ddm_entrances = ['Dark Death Mountain Fairy', 'Spike Cave'] + caves = list(Cave_Exits) three_exit_caves = list(Cave_Three_Exits) - single_doors = list(Single_Cave_Doors) - bomb_shop_doors = list(Bomb_Shop_Single_Cave_Doors) if not invFlag else list(Inverted_Bomb_Shop_Single_Cave_Doors) - blacksmith_doors = list(Blacksmith_Single_Cave_Doors) + ['Links House'] if not invFlag else [] - door_targets = list(Single_Cave_Targets) if not invFlag else list(Inverted_Single_Cave_Targets) - - # we shuffle all 2 entrance caves as pairs as a start + Two_Door_Caves_Directional = list() + Two_Door_Caves = [('Elder House (East)', 'Elder House (West)'), + ('Superbunny Cave (Bottom)', 'Superbunny Cave (Top)')] + if invFlag == (0x0a in world.owswaps[player][0] and world.owMixed[player]): + Two_Door_Caves_Directional.append(tuple({'Bumper Cave (Bottom)', 'Bumper Cave (Top)'})) + else: + Two_Door_Caves_Directional.append(tuple({'Old Man Cave (West)', 'Death Mountain Return Cave (West)'})) + if invFlag == (0x05 in world.owswaps[player][0] and world.owMixed[player]): + Two_Door_Caves_Directional.append(tuple({'Hookshot Cave', 'Hookshot Cave Back Entrance'})) + else: + Two_Door_Caves.append(tuple({'Hookshot Cave', 'Hookshot Cave Back Entrance'})) + if invFlag == (0x28 in world.owswaps[player][0] and world.owMixed[player]): + Two_Door_Caves.append(tuple({'Two Brothers House (East)', 'Two Brothers House (West)'})) + else: + Two_Door_Caves_Directional.append(tuple({'Two Brothers House (East)', 'Two Brothers House (West)'})) + + # shuffle all 2 entrance caves as pairs as a start # start with the ones that need to be directed - two_door_caves = list(Two_Door_Caves_Directional) if not invFlag else list(Inverted_Two_Door_Caves_Directional) + two_door_caves = list(Two_Door_Caves_Directional) random.shuffle(two_door_caves) random.shuffle(caves) while two_door_caves: @@ -133,8 +165,8 @@ def link_entrances(world, player): connect_two_way(world, entrance1, exit1, player) connect_two_way(world, entrance2, exit2, player) - # now the remaining pairs - two_door_caves = list(Two_Door_Caves) if not invFlag else list(Inverted_Two_Door_Caves) + # shuffle remaining 2 entrance cave pairs + two_door_caves = list(Two_Door_Caves) random.shuffle(two_door_caves) while two_door_caves: entrance1, entrance2 = two_door_caves.pop() @@ -142,567 +174,397 @@ def link_entrances(world, player): connect_two_way(world, entrance1, exit1, player) connect_two_way(world, entrance2, exit2, player) - # place links house - if world.mode[player] == 'standard' or not world.shufflelinks[player]: - links_house = 'Links House' if not invFlag else 'Big Bomb Shop' - else: - links_house_doors = [i for i in (LW_Single_Cave_Doors if not invFlag else DW_Single_Cave_Doors) if i not in isolated_entrances + ([] if not invFlag else Inverted_Dark_Sanctuary_Doors)] - links_house = random.choice(links_house_doors) - connect_two_way(world, links_house, 'Links House Exit', player) - - if links_house in bomb_shop_doors: - bomb_shop_doors.remove(links_house) - if links_house in blacksmith_doors: - blacksmith_doors.remove(links_house) - if links_house in old_man_entrances: - old_man_entrances.remove(links_house) - - if invFlag: - # place dark sanc - sanc_doors = [door for door in Inverted_Dark_Sanctuary_Doors if door in bomb_shop_doors] - sanc_door = random.choice(sanc_doors) - bomb_shop_doors.remove(sanc_door) - connect_entrance(world, sanc_door, 'Dark Sanctuary Hint', player) - world.get_entrance('Dark Sanctuary Hint Exit', player).connect(world.get_entrance(sanc_door, player).parent_region) - - lw_dm_entrances = ['Paradox Cave (Bottom)', 'Paradox Cave (Middle)', 'Paradox Cave (Top)', 'Old Man House (Bottom)', - 'Fairy Ascension Cave (Bottom)', 'Fairy Ascension Cave (Top)', 'Spiral Cave (Bottom)', 'Old Man Cave (East)', - 'Death Mountain Return Cave (East)', 'Spiral Cave', 'Old Man House (Top)', 'Spectacle Rock Cave', - 'Spectacle Rock Cave Peak', 'Spectacle Rock Cave (Bottom)'] - - # place old man, bumper cave bottom to DDM entrances not in east bottom - else: - # at this point only Light World death mountain entrances remain - # place old man, has limited options - lw_dm_entrances = ['Old Man Cave (West)', 'Old Man House (Bottom)', 'Death Mountain Return Cave (West)', 'Paradox Cave (Bottom)', 'Paradox Cave (Middle)', 'Paradox Cave (Top)', - 'Fairy Ascension Cave (Bottom)', 'Fairy Ascension Cave (Top)', 'Spiral Cave', 'Spiral Cave (Bottom)'] - - random.shuffle(old_man_entrances) - old_man_exit = old_man_entrances.pop() - if not invFlag: - lw_dm_entrances.extend(old_man_entrances) - random.shuffle(lw_dm_entrances) - old_man_entrance = lw_dm_entrances.pop() - connect_two_way(world, old_man_entrance if invFlag == (0x0a in world.owswaps[player][0] and world.owMixed[player]) else 'Bumper Cave (Bottom)', 'Old Man Cave Exit (West)', player) - connect_two_way(world, old_man_exit, 'Old Man Cave Exit (East)', player) - - if invFlag and old_man_exit == 'Spike Cave': - bomb_shop_doors.remove('Spike Cave') - bomb_shop_doors.extend(old_man_entrances) - - # add old man house to ensure it is always somewhere on light death mountain + # shuffle LW DM entrances caves.extend(list(Old_Man_House)) caves.extend(list(three_exit_caves)) - # connect rest - connect_caves(world, lw_dm_entrances, [], caves, player) + if invFlag == (0x18 in world.owswaps[player][0] and world.owMixed[player]) or invFlag == (0x03 in world.owswaps[player][0] and world.owMixed[player]): # ability to activate flute in LW + candidates = [e for e in lw_wdm_entrances if e != 'Old Man House (Bottom)'] + random.shuffle(candidates) + old_man_exit = candidates.pop() + lw_wdm_entrances.remove(old_man_exit) + connect_two_way(world, old_man_exit, 'Old Man Cave Exit (East)', player) - # scramble holes - scramble_holes(world, player) + if invFlag == (0x0a in world.owswaps[player][0] and world.owMixed[player]): + lw_wdm_entrances.extend(['Old Man Cave (West)', 'Death Mountain Return Cave (West)']) + else: + lw_wdm_entrances.extend(['Bumper Cave (Bottom)', 'Bumper Cave (Top)']) + + if 0x03 in world.owswaps[player][0] == 0x05 in world.owswaps[player][0]: # if WDM and EDM are in same world + candidates = lw_wdm_entrances + lw_edm_entrances + random.shuffle(candidates) + old_man_entrance = candidates.pop() + lw_wdm_entrances.remove(old_man_entrance) + if old_man_entrance in lw_wdm_entrances: + lw_wdm_entrances.remove(old_man_entrance) + elif old_man_entrance in lw_edm_entrances: + lw_edm_entrances.remove(old_man_entrance) + else: + random.shuffle(lw_wdm_entrances) + old_man_entrance = lw_wdm_entrances.pop() + connect_two_way(world, old_man_entrance, 'Old Man Cave Exit (West)', player) + else: + # force connection to DM + random.shuffle(ddm_entrances) + old_man_exit = ddm_entrances.pop() + connect_two_way(world, old_man_exit, 'Old Man Cave Exit (East)', player) + + # place old man, bumper cave bottom to DDM entrances not in east bottom + if invFlag == (0x0a in world.owswaps[player][0] and world.owMixed[player]): + connect_two_way(world, 'Old Man Cave (West)', 'Old Man Cave Exit (West)', player) + else: + connect_two_way(world, 'Bumper Cave (Bottom)', 'Old Man Cave Exit (West)', player) + # connect remaining LW DM entrances + if 0x03 in world.owswaps[player][0] == 0x05 in world.owswaps[player][0]: # if WDM and EDM are in same world + connect_caves(world, lw_wdm_entrances + lw_edm_entrances, [], caves, player) + else: + # place Old Man House in WDM if not swapped + if invFlag == (0x03 in world.owswaps[player][0] and world.owMixed[player]): + connect_caves(world, lw_wdm_entrances, [], list(Old_Man_House), player) + else: + connect_caves(world, lw_edm_entrances, [], list(Old_Man_House), player) + caves.remove(Old_Man_House[0]) + + i = 0 + c = 0 + while i != len(lw_wdm_entrances): + random.shuffle(caves) + i = 0 + c = 0 + while i < len(lw_wdm_entrances): + i += len(caves[c]) + c += 1 + + connect_caves(world, lw_wdm_entrances, [], caves[0:c], player) + connect_caves(world, lw_edm_entrances, [], caves[c:], player) + + if invFlag: + # place dark sanc + place_dark_sanc(world, sectors, player) + + # place links house + links_house = place_links_house(world, sectors, player) + # place blacksmith, has limited options - if invFlag: - blacksmith_doors = [door for door in blacksmith_doors[:]] - random.shuffle(blacksmith_doors) - blacksmith_hut = blacksmith_doors.pop() - connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player) - bomb_shop_doors.extend(blacksmith_doors) + place_blacksmith(world, links_house, player) + # junk fill inaccessible regions + # TODO: Should be obsolete someday when OWR rebalances the shuffle to prevent unreachable regions + junk_fill_inaccessible(world, player) + # place bomb shop, has limited options - if invFlag: - bomb_shop_doors = [door for door in bomb_shop_doors[:]] - random.shuffle(bomb_shop_doors) - bomb_shop = bomb_shop_doors.pop() + bomb_shop_doors = list(entrance_pool) + if world.logic[player] in ['noglitches', 'minorglitches'] or (invFlag != (0x1b in world.owswaps[player][0] and world.owMixed[player])): + bomb_shop_doors = [e for e in entrance_pool if e not in ['Pyramid Fairy']] + bomb_shop = random.choice(bomb_shop_doors) connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) - single_doors.extend(bomb_shop_doors) - + # place remaining doors - connect_doors(world, single_doors, door_targets, player) + connect_doors(world, list(entrance_pool), list(exit_pool), player) elif world.shuffle[player] == 'restricted': + suppress_spoiler = False simple_shuffle_dungeons(world, player) - if not invFlag: - lw_entrances = list(LW_Entrances + LW_Single_Cave_Doors + Old_Man_Entrances) - dw_entrances = list(DW_Entrances + DW_Single_Cave_Doors) - dw_must_exits = list(DW_Entrances_Must_Exit) - old_man_entrances = list(Old_Man_Entrances) - caves = list(Cave_Exits + Cave_Three_Exits) - single_doors = list(Single_Cave_Doors) - bomb_shop_doors = list(Bomb_Shop_Single_Cave_Doors + Bomb_Shop_Multi_Cave_Doors) - blacksmith_doors = list(Blacksmith_Single_Cave_Doors + Blacksmith_Multi_Cave_Doors) - door_targets = list(Single_Cave_Targets) - else: - lw_entrances = list(Inverted_LW_Entrances + LW_Single_Cave_Doors) - dw_entrances = list(Inverted_DW_Entrances + Inverted_DW_Single_Cave_Doors + Inverted_Old_Man_Entrances) - lw_must_exits = list(Inverted_LW_Entrances_Must_Exit) - old_man_entrances = list(Inverted_Old_Man_Entrances) - caves = list(Cave_Exits + Cave_Three_Exits + Old_Man_House) - single_doors = list(Single_Cave_Doors) - bomb_shop_doors = list(Inverted_Bomb_Shop_Single_Cave_Doors + Inverted_Bomb_Shop_Multi_Cave_Doors) - blacksmith_doors = list(Inverted_Blacksmith_Single_Cave_Doors + Blacksmith_Multi_Cave_Doors) - door_targets = list(Inverted_Single_Cave_Targets) - - # place links house - if world.mode[player] == 'standard' or not world.shufflelinks[player]: - links_house = 'Links House' if not invFlag else 'Big Bomb Shop' - else: - links_house_doors = [i for i in (lw_entrances if not invFlag else dw_entrances) if i not in isolated_entrances + ([] if not invFlag else Inverted_Dark_Sanctuary_Doors)] - links_house = random.choice(links_house_doors) - connect_two_way(world, links_house, 'Links House Exit', player) - if not invFlag: - if links_house in lw_entrances: - lw_entrances.remove(links_house) - else: - if links_house in dw_entrances: - dw_entrances.remove(links_house) + # shuffle holes + scramble_holes(world, player) # place dark sanc if invFlag: - sanc_doors = [door for door in Inverted_Dark_Sanctuary_Doors if door in dw_entrances] - sanc_door = random.choice(sanc_doors) - dw_entrances.remove(sanc_door) - connect_entrance(world, sanc_door, 'Dark Sanctuary Hint', player) - world.get_entrance('Dark Sanctuary Hint Exit', player).connect(world.get_entrance(sanc_door, player).parent_region) - - # in restricted, the only mandatory exits are in dark world (lw in inverted) - if not invFlag: - connect_mandatory_exits(world, dw_entrances, caves, dw_must_exits, player) - else: - connect_mandatory_exits(world, lw_entrances, caves, lw_must_exits, player) - - # place old man, has limited options - # exit has to come from specific set of doors, the entrance is free to move about - old_man_entrances = [door for door in old_man_entrances if door in (lw_entrances if not invFlag else dw_entrances)] - random.shuffle(old_man_entrances) - old_man_exit = old_man_entrances.pop() - connect_two_way(world, old_man_exit, 'Old Man Cave Exit (East)', player) - if not invFlag: - lw_entrances.remove(old_man_exit) - else: - dw_entrances.remove(old_man_exit) - + place_dark_sanc(world, sectors, player) + + # place links house + links_house = place_links_house(world, sectors, player) + # place blacksmith, has limited options - all_entrances = lw_entrances + dw_entrances - # cannot place it anywhere already taken (or that are otherwise not eligible for placement) - blacksmith_doors = [door for door in blacksmith_doors if door in all_entrances] - random.shuffle(blacksmith_doors) - blacksmith_hut = blacksmith_doors.pop() - connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player) - if blacksmith_hut in lw_entrances: - lw_entrances.remove(blacksmith_hut) - if blacksmith_hut in dw_entrances: - dw_entrances.remove(blacksmith_hut) - bomb_shop_doors.extend(blacksmith_doors) + place_blacksmith(world, links_house, player) + # determine pools + lw_entrances = list() + dw_entrances = list() + caves = list(Cave_Exits + Cave_Three_Exits + Old_Man_House) + for e in entrance_pool: + region = world.get_entrance(e, player).parent_region + if region.type == RegionType.LightWorld: + lw_entrances.append(e) + else: + dw_entrances.append(e) + + # place connectors in inaccessible regions + connect_inaccessible_regions(world, lw_entrances, dw_entrances, caves, player) + + # place old man, has limited options + place_old_man(world, lw_entrances if not invFlag else dw_entrances, player) + # place bomb shop, has limited options - all_entrances = lw_entrances + dw_entrances - # cannot place it anywhere already taken (or that are otherwise not eligible for placement) - bomb_shop_doors = [door for door in bomb_shop_doors if door in all_entrances] - random.shuffle(bomb_shop_doors) - bomb_shop = bomb_shop_doors.pop() + bomb_shop_doors = list(entrance_pool) + if world.logic[player] in ['noglitches', 'minorglitches'] or (invFlag != (0x1b in world.owswaps[player][0] and world.owMixed[player])): + bomb_shop_doors = [e for e in entrance_pool if e not in ['Pyramid Fairy']] + bomb_shop = random.choice(bomb_shop_doors) connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) - if bomb_shop in lw_entrances: - lw_entrances.remove(bomb_shop) - if bomb_shop in dw_entrances: - dw_entrances.remove(bomb_shop) - - # place the old man cave's entrance somewhere in the light world (dw for inverted) - if not invFlag: - random.shuffle(lw_entrances) - old_man_entrance = lw_entrances.pop() - else: - random.shuffle(dw_entrances) - old_man_entrance = dw_entrances.pop() - connect_two_way(world, old_man_entrance, 'Old Man Cave Exit (West)', player) - - if not invFlag: - # place Old Man House in Light World - connect_caves(world, lw_entrances, [], list(Old_Man_House), player) #for multiple seeds - - # now scramble the rest + + # shuffle connectors + lw_entrances = [e for e in lw_entrances if e in entrance_pool] + dw_entrances = [e for e in dw_entrances if e in entrance_pool] connect_caves(world, lw_entrances, dw_entrances, caves, player) - # scramble holes - scramble_holes(world, player) - # place remaining doors - doors = lw_entrances + dw_entrances - connect_doors(world, doors, door_targets, player) + connect_doors(world, list(entrance_pool), list(exit_pool), player) elif world.shuffle[player] == 'full': + suppress_spoiler = False skull_woods_shuffle(world, player) - if not invFlag: - lw_entrances = list(LW_Entrances + LW_Dungeon_Entrances + LW_Single_Cave_Doors + Old_Man_Entrances) - dw_entrances = list(DW_Entrances + DW_Dungeon_Entrances + DW_Single_Cave_Doors) - dw_must_exits = list(DW_Entrances_Must_Exit + DW_Dungeon_Entrances_Must_Exit) - lw_must_exits = list(LW_Dungeon_Entrances_Must_Exit) - old_man_entrances = list(Old_Man_Entrances + ['Tower of Hera']) - bomb_shop_doors = list(Bomb_Shop_Single_Cave_Doors + Bomb_Shop_Multi_Cave_Doors) - blacksmith_doors = list(Blacksmith_Single_Cave_Doors + Blacksmith_Multi_Cave_Doors) - door_targets = list(Single_Cave_Targets) - else: - lw_entrances = list(Inverted_LW_Entrances + Inverted_LW_Dungeon_Entrances + LW_Single_Cave_Doors) - dw_entrances = list(Inverted_DW_Entrances + Inverted_DW_Dungeon_Entrances + Inverted_DW_Single_Cave_Doors + Inverted_Old_Man_Entrances) - lw_must_exits = list(Inverted_LW_Dungeon_Entrances_Must_Exit + Inverted_LW_Entrances_Must_Exit) - old_man_entrances = list(Inverted_Old_Man_Entrances + Old_Man_Entrances + ['Ganons Tower', 'Tower of Hera']) - bomb_shop_doors = list(Inverted_Bomb_Shop_Single_Cave_Doors + Inverted_Bomb_Shop_Multi_Cave_Doors) - blacksmith_doors = list(Inverted_Blacksmith_Single_Cave_Doors + Blacksmith_Multi_Cave_Doors) - door_targets = list(Inverted_Single_Cave_Targets) - - # randomize which desert ledge door is a must-exit - if random.randint(0, 1) == 0: - lw_must_exits.append('Desert Palace Entrance (North)') - lw_entrances.append('Desert Palace Entrance (West)') - else: - lw_must_exits.append('Desert Palace Entrance (West)') - lw_entrances.append('Desert Palace Entrance (North)') - caves = list(Cave_Exits + Dungeon_Exits + Cave_Three_Exits) # don't need to consider three exit caves, have one exit caves to avoid parity issues - old_man_house = list(Old_Man_House) - + caves = list(Cave_Exits + Dungeon_Exits + Cave_Three_Exits + Old_Man_House) + if world.mode[player] == 'standard': - # must connect front of hyrule castle to do escape connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) + caves.append(tuple(random.sample(['Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)'], 2))) else: - lw_entrances.append('Hyrule Castle Entrance (South)') - if invFlag or world.doorShuffle[player] == 'vanilla': - caves.append(tuple(random.sample(['Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)'],3))) + caves.append(tuple(random.sample(['Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)'], 3))) if not world.shuffle_ganon: connect_two_way(world, 'Ganons Tower' if not invFlag else 'Agahnims Tower', 'Ganons Tower Exit', player) - hc_ledge_entrances = ['Hyrule Castle Entrance (West)', 'Hyrule Castle Entrance (East)'] else: - if not invFlag: - dw_entrances.append('Ganons Tower') - else: - lw_entrances.append('Agahnims Tower') caves.append('Ganons Tower Exit') - hc_ledge_entrances = ['Hyrule Castle Entrance (West)', 'Hyrule Castle Entrance (East)', 'Agahnims Tower'] + # place dark sanc if invFlag: - # shuffle aga door first. if it's on hc ledge, then one other hc ledge door has to be must_exit - all_entrances_aga = lw_entrances + dw_entrances - aga_doors = [i for i in all_entrances_aga] - random.shuffle(aga_doors) - aga_door = aga_doors.pop() - - if aga_door in hc_ledge_entrances: - lw_entrances.remove(aga_door) - hc_ledge_entrances.remove(aga_door) - - random.shuffle(hc_ledge_entrances) - hc_ledge_must_exit = hc_ledge_entrances.pop() - lw_entrances.remove(hc_ledge_must_exit) - lw_must_exits.append(hc_ledge_must_exit) - - if aga_door in lw_entrances: - lw_entrances.remove(aga_door) - elif aga_door in dw_entrances: - dw_entrances.remove(aga_door) - - connect_two_way(world, aga_door, 'Agahnims Tower Exit', player) - caves.remove('Agahnims Tower Exit') + place_dark_sanc(world, sectors, player, list(zip(*drop_connections + dropexit_connections))[0]) # place links house - if world.mode[player] == 'standard' or not world.shufflelinks[player]: - links_house = 'Links House' if not invFlag else 'Big Bomb Shop' - else: - links_house_doors = [i for i in (lw_entrances + lw_must_exits if not invFlag else dw_entrances) if i not in isolated_entrances + ([] if not invFlag else Inverted_Dark_Sanctuary_Doors)] - links_house = random.choice(links_house_doors) - connect_two_way(world, links_house, 'Links House Exit', player) - if not invFlag: - if links_house in lw_entrances: - lw_entrances.remove(links_house) - if links_house in lw_must_exits: - lw_must_exits.remove(links_house) - else: - if links_house in dw_entrances: - dw_entrances.remove(links_house) - - # place dark sanc - sanc_doors = [door for door in Inverted_Dark_Sanctuary_Doors if door in dw_entrances] - sanc_door = random.choice(sanc_doors) - dw_entrances.remove(sanc_door) - connect_entrance(world, sanc_door, 'Dark Sanctuary Hint', player) - world.get_entrance('Dark Sanctuary Hint Exit', player).connect(world.get_entrance(sanc_door, player).parent_region) - - # we randomize which world requirements we fulfill first so we get better dungeon distribution - # we also places the Old Man House at this time to make sure he can be connected to the desert one way - # no dw must exits in inverted, but we randomize whether cave is in light or dark world - if random.randint(0, 1) == 0: - caves += old_man_house - connect_mandatory_exits(world, lw_entrances, caves, lw_must_exits, player) - try: - caves.remove(old_man_house[0]) - except ValueError: - pass - else: #if the cave wasn't placed we get here - connect_caves(world, lw_entrances, [], old_man_house, player) - if not invFlag: - connect_mandatory_exits(world, dw_entrances, caves, dw_must_exits, player) - else: - if not invFlag: - connect_mandatory_exits(world, dw_entrances, caves, dw_must_exits, player) - caves += old_man_house - connect_mandatory_exits(world, lw_entrances, caves, lw_must_exits, player) - try: - caves.remove(old_man_house[0]) - except ValueError: - pass - else: # if the cave wasn't placed we get here - connect_caves(world, lw_entrances, [], old_man_house, player) - else: - connect_caves(world, dw_entrances, [], old_man_house, player) - connect_mandatory_exits(world, lw_entrances, caves, lw_must_exits, player) - - if world.mode[player] == 'standard': - # rest of hyrule castle must be in light world - connect_caves(world, lw_entrances, [], [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')], player) - # in full, Sanc must be in light world, so must all of HC if door shuffle is on - elif not invFlag and world.doorShuffle[player] != 'vanilla': - connect_caves(world, lw_entrances, [], [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)', 'Hyrule Castle Exit (South)')], player) + links_house = place_links_house(world, sectors, player, list(zip(*drop_connections + dropexit_connections))[0]) + # determine pools + lw_entrances = list() + dw_entrances = list() + for e in entrance_pool: + if e not in list(zip(*drop_connections + dropexit_connections))[0]: + region = world.get_entrance(e, player).parent_region + if region.type == RegionType.LightWorld: + lw_entrances.append(e) + else: + dw_entrances.append(e) + + # place connectors in inaccessible regions + connect_inaccessible_regions(world, lw_entrances, dw_entrances, caves, player, list(zip(*drop_connections + dropexit_connections))[0]) + # place old man, has limited options - # exit has to come from specific set of doors, the entrance is free to move about - if not invFlag: - old_man_entrances = [door for door in old_man_entrances if door in lw_entrances] - else: - old_man_entrances = [door for door in old_man_entrances if door in dw_entrances + lw_entrances] - random.shuffle(old_man_entrances) - old_man_exit = old_man_entrances.pop() - connect_two_way(world, old_man_exit, 'Old Man Cave Exit (East)', player) - if old_man_exit in dw_entrances: - dw_entrances.remove(old_man_exit) - old_man_world = 'dark' - elif invFlag or old_man_exit in lw_entrances: - lw_entrances.remove(old_man_exit) - old_man_world = 'light' - - # place blacksmith, has limited options - all_entrances = lw_entrances + dw_entrances - # cannot place it anywhere already taken (or that are otherwise not eligible for placement) - blacksmith_doors = [door for door in blacksmith_doors if door in all_entrances] - random.shuffle(blacksmith_doors) - blacksmith_hut = blacksmith_doors.pop() - connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player) - if blacksmith_hut in lw_entrances: - lw_entrances.remove(blacksmith_hut) - if blacksmith_hut in dw_entrances: - dw_entrances.remove(blacksmith_hut) - bomb_shop_doors.extend(blacksmith_doors) - + place_old_man(world, lw_entrances if not invFlag else dw_entrances, player, list(zip(*drop_connections + dropexit_connections))[0]) + # place bomb shop, has limited options - all_entrances = lw_entrances + dw_entrances - # cannot place it anywhere already taken (or that are otherwise not eligible for placement) - bomb_shop_doors = [door for door in bomb_shop_doors if door in all_entrances] - random.shuffle(bomb_shop_doors) - bomb_shop = bomb_shop_doors.pop() + bomb_shop_doors = [e for e in entrance_pool if e not in list(zip(*drop_connections + dropexit_connections))[0]] + if world.logic[player] in ['noglitches', 'minorglitches'] or (invFlag != (0x1b in world.owswaps[player][0] and world.owMixed[player])): + bomb_shop_doors = [e for e in bomb_shop_doors if e not in ['Pyramid Fairy']] + bomb_shop = random.choice(bomb_shop_doors) connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) - if bomb_shop in lw_entrances: - lw_entrances.remove(bomb_shop) - if bomb_shop in dw_entrances: - dw_entrances.remove(bomb_shop) - - # place the old man cave's entrance somewhere in the same world he'll exit from - if old_man_world == 'light': - random.shuffle(lw_entrances) - old_man_entrance = lw_entrances.pop() - elif old_man_world == 'dark': - random.shuffle(dw_entrances) - old_man_entrance = dw_entrances.pop() - connect_two_way(world, old_man_entrance, 'Old Man Cave Exit (West)', player) - - # now scramble the rest + + # shuffle connectors + lw_entrances = [e for e in lw_entrances if e in entrance_pool] + dw_entrances = [e for e in dw_entrances if e in entrance_pool] connect_caves(world, lw_entrances, dw_entrances, caves, player) - # scramble holes + # shuffle holes scramble_holes(world, player) + + # place blacksmith, has limited options + place_blacksmith(world, links_house, player) # place remaining doors - doors = lw_entrances + dw_entrances - connect_doors(world, doors, door_targets, player) - elif world.shuffle[player] == 'crossed': + connect_doors(world, list(entrance_pool), list(exit_pool), player) + elif world.shuffle[player] == 'lite': + for entrancename, exitname in default_connections + ([] if world.shopsanity[player] else default_shop_connections): + connect_logical(world, entrancename, exitname, player, False) + if invFlag: + world.get_entrance('Dark Sanctuary Hint Exit', player).connect(world.get_entrance('Dark Sanctuary Hint', player).parent_region) + + suppress_spoiler = False + + # shuffle dungeons skull_woods_shuffle(world, player) - if not invFlag: - entrances = list(LW_Entrances + LW_Dungeon_Entrances + LW_Single_Cave_Doors + Old_Man_Entrances + DW_Entrances + DW_Dungeon_Entrances + DW_Single_Cave_Doors) - must_exits = list(DW_Entrances_Must_Exit + DW_Dungeon_Entrances_Must_Exit + LW_Dungeon_Entrances_Must_Exit) - old_man_entrances = list(Old_Man_Entrances + ['Tower of Hera']) - bomb_shop_doors = list(Bomb_Shop_Single_Cave_Doors + Bomb_Shop_Multi_Cave_Doors) - blacksmith_doors = list(Blacksmith_Single_Cave_Doors + Blacksmith_Multi_Cave_Doors) - door_targets = list(Single_Cave_Targets) - else: - entrances = list(Inverted_LW_Entrances + Inverted_LW_Dungeon_Entrances + LW_Single_Cave_Doors + Inverted_Old_Man_Entrances + Inverted_DW_Entrances + Inverted_DW_Dungeon_Entrances + Inverted_DW_Single_Cave_Doors) - must_exits = list(Inverted_LW_Entrances_Must_Exit + Inverted_LW_Dungeon_Entrances_Must_Exit) - old_man_entrances = list(Inverted_Old_Man_Entrances + Old_Man_Entrances + ['Ganons Tower', 'Tower of Hera']) - bomb_shop_doors = list(Inverted_Bomb_Shop_Single_Cave_Doors + Inverted_Bomb_Shop_Multi_Cave_Doors) - blacksmith_doors = list(Inverted_Blacksmith_Single_Cave_Doors + Blacksmith_Multi_Cave_Doors) - door_targets = list(Inverted_Single_Cave_Targets) - caves = list(Cave_Exits + Dungeon_Exits + Cave_Three_Exits + Old_Man_House) # don't need to consider three exit caves, have one exit caves to avoid parity issues - - if invFlag: - # randomize which desert ledge door is a must-exit - if random.randint(0, 1) == 0: - must_exits.append('Desert Palace Entrance (North)') - entrances.append('Desert Palace Entrance (West)') - else: - must_exits.append('Desert Palace Entrance (West)') - entrances.append('Desert Palace Entrance (North)') + # build dungeon lists + lw_dungeons = LW_Dungeon_Exits.copy() + dw_dungeons = DW_Late_Dungeon_Exits.copy() if world.mode[player] == 'standard': - # must connect front of hyrule castle to do escape connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) + lw_dungeons.append(tuple(('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)'))) else: - caves.append(tuple(random.sample(['Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)'],3))) - entrances.append('Hyrule Castle Entrance (South)') + lw_dungeons.append(tuple(('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)', 'Hyrule Castle Exit (South)'))) if not world.shuffle_ganon: connect_two_way(world, 'Ganons Tower' if not invFlag else 'Agahnims Tower', 'Ganons Tower Exit', player) - hc_ledge_entrances = ['Hyrule Castle Entrance (West)', 'Hyrule Castle Entrance (East)'] else: - entrances.append('Ganons Tower' if not invFlag else 'Agahnims Tower') - caves.append('Ganons Tower Exit') - hc_ledge_entrances = ['Hyrule Castle Entrance (West)', 'Hyrule Castle Entrance (East)', 'Agahnims Tower'] - - if invFlag: - # shuffle aga door. if it's on hc ledge, then one other hc ledge door has to be must_exit - aga_choices = [x for x in entrances] - aga_door = random.choice(aga_choices) - - if aga_door in hc_ledge_entrances: - hc_ledge_entrances.remove(aga_door) - - random.shuffle(hc_ledge_entrances) - hc_ledge_must_exit = hc_ledge_entrances.pop() - entrances.remove(hc_ledge_must_exit) - must_exits.append(hc_ledge_must_exit) - - entrances.remove(aga_door) - connect_two_way(world, aga_door, 'Agahnims Tower Exit', player) - caves.remove('Agahnims Tower Exit') + dw_dungeons.append('Ganons Tower Exit') - # place links house - if world.mode[player] == 'standard' or not world.shufflelinks[player]: - links_house = 'Links House' if not invFlag else 'Big Bomb Shop' - else: - links_house_doors = [i for i in entrances + must_exits if i not in isolated_entrances + ([] if not invFlag else Inverted_Dark_Sanctuary_Doors)] - if not invFlag and world.doorShuffle[player] == 'crossed' and world.intensity[player] >= 3: - exclusions = DW_Entrances + DW_Dungeon_Entrances + DW_Single_Cave_Doors\ - + DW_Entrances_Must_Exit + DW_Dungeon_Entrances_Must_Exit + ['Ganons Tower'] - links_house_doors = [i for i in links_house_doors if i not in exclusions] - links_house = random.choice(list(links_house_doors)) - connect_two_way(world, links_house, 'Links House Exit', player) - if links_house in entrances: - entrances.remove(links_house) - elif links_house in must_exits: - must_exits.remove(links_house) - - if invFlag: - # place dark sanc - sanc_doors = [door for door in Inverted_Dark_Sanctuary_Doors if door in entrances] - sanc_door = random.choice(sanc_doors) - entrances.remove(sanc_door) - connect_entrance(world, sanc_door, 'Dark Sanctuary Hint', player) - world.get_entrance('Dark Sanctuary Hint Exit', player).connect(world.get_entrance(sanc_door, player).parent_region) - - #place must-exit caves - connect_mandatory_exits(world, entrances, caves, must_exits, player) - - if world.mode[player] == 'standard': - # rest of hyrule castle must be dealt with - connect_caves(world, entrances, [], [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')], player) - - # place old man, has limited options - # exit has to come from specific set of doors, the entrance is free to move about - old_man_entrances = [door for door in old_man_entrances if door in entrances] - random.shuffle(old_man_entrances) - old_man_exit = old_man_entrances.pop() - connect_two_way(world, old_man_exit, 'Old Man Cave Exit (East)', player) - entrances.remove(old_man_exit) - - # place blacksmith, has limited options - # cannot place it anywhere already taken (or that are otherwise not eligible for placement) - blacksmith_doors = [door for door in blacksmith_doors if door in entrances] - random.shuffle(blacksmith_doors) - blacksmith_hut = blacksmith_doors.pop() - connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player) - entrances.remove(blacksmith_hut) - if not invFlag: - bomb_shop_doors.extend(blacksmith_doors) - - # place bomb shop, has limited options - # cannot place it anywhere already taken (or that are otherwise not eligible for placement) - bomb_shop_doors = [door for door in bomb_shop_doors if door in entrances] - random.shuffle(bomb_shop_doors) - bomb_shop = bomb_shop_doors.pop() - connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) - entrances.remove(bomb_shop) - - # place the old man cave's entrance somewhere - random.shuffle(entrances) - old_man_entrance = entrances.pop() - connect_two_way(world, old_man_entrance, 'Old Man Cave Exit (West)', player) - - # now scramble the rest - connect_caves(world, entrances, [], caves, player) - - # scramble holes + unbias_dungeons(lw_dungeons) + unbias_dungeons(dw_dungeons) + + # shuffle dropdowns scramble_holes(world, player) + # place links house + links_house = place_links_house(world, sectors, player) + + # place blacksmith, has limited options + place_blacksmith(world, links_house, player) + + # determine pools + Cave_Base = list(Cave_Exits + Cave_Three_Exits) + lw_entrances = list() + dw_entrances = list() + for e in entrance_pool: + region = world.get_entrance(e, player).parent_region + if region.type == RegionType.LightWorld: + lw_entrances.append(e) + else: + dw_entrances.append(e) + + # place connectors in inaccessible regions + caves = Cave_Base + (dw_dungeons if not invFlag else lw_dungeons) + connector_entrances = [e for e in list(zip(*default_connector_connections + default_dungeon_connections + open_default_dungeon_connections))[0] if e in (dw_entrances if not invFlag else lw_entrances)] + connect_inaccessible_regions(world, [], connector_entrances, caves, player) + + caves = list(set(Cave_Base) & set(caves)) + (lw_dungeons if not invFlag else dw_dungeons) + connector_entrances = [e for e in list(zip(*default_connector_connections + default_dungeon_connections + open_default_dungeon_connections))[0] if e in (lw_entrances if not invFlag else dw_entrances)] + connect_inaccessible_regions(world, connector_entrances, [], caves, player) + + lw_dungeons = list(set(lw_dungeons) & set(caves)) + (Old_Man_House if not invFlag else []) + dw_dungeons = list(set(dw_dungeons) & set(caves)) + ([] if not invFlag else Old_Man_House) + caves = list(set(Cave_Base) & set(caves)) + DW_Mid_Dungeon_Exits + + # place old man, has limited options + lw_entrances = [e for e in lw_entrances if e in list(zip(*default_connector_connections + default_dungeon_connections + open_default_dungeon_connections))[0] and e in entrance_pool] + dw_entrances = [e for e in dw_entrances if e in list(zip(*default_connector_connections + default_dungeon_connections + open_default_dungeon_connections))[0] and e in entrance_pool] + place_old_man(world, lw_entrances if not invFlag else dw_entrances, player) + + # shuffle remaining connectors + lw_entrances = [e for e in lw_entrances if e in list(zip(*default_connector_connections + default_dungeon_connections + open_default_dungeon_connections))[0] and e in entrance_pool] + dw_entrances = [e for e in dw_entrances if e in list(zip(*default_connector_connections + default_dungeon_connections + open_default_dungeon_connections))[0] and e in entrance_pool] + connect_caves(world, lw_entrances, [], lw_dungeons, player) + connect_caves(world, [], dw_entrances, dw_dungeons, player) + connect_caves(world, lw_entrances, dw_entrances, caves, player) + + # place bomb shop, has limited options + bomb_shop_doors = list(entrance_pool) + if world.logic[player] in ['noglitches', 'minorglitches'] or (invFlag != (0x1b in world.owswaps[player][0] and world.owMixed[player])): + bomb_shop_doors = [e for e in entrance_pool if e not in ['Pyramid Fairy']] + bomb_shop = random.choice(bomb_shop_doors) + connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) + # place remaining doors - connect_doors(world, entrances, door_targets, player) + connect_doors(world, list(entrance_pool), list(exit_pool), player) + elif world.shuffle[player] == 'lean': + for entrancename, exitname in default_connections + ([] if world.shopsanity[player] else default_shop_connections): + connect_logical(world, entrancename, exitname, player, False) + if invFlag: + world.get_entrance('Dark Sanctuary Hint Exit', player).connect(world.get_entrance('Dark Sanctuary Hint', player).parent_region) + + suppress_spoiler = False + + # shuffle dungeons + skull_woods_shuffle(world, player) + + if world.mode[player] == 'standard': + connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) + Dungeon_Exits.append(tuple(('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)'))) + else: + Dungeon_Exits.append(tuple(('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)', 'Hyrule Castle Exit (South)'))) + + if not world.shuffle_ganon: + connect_two_way(world, 'Ganons Tower' if not invFlag else 'Agahnims Tower', 'Ganons Tower Exit', player) + else: + Dungeon_Exits.append('Ganons Tower Exit') + + caves = list(Cave_Exits + Dungeon_Exits + Cave_Three_Exits + Old_Man_House) + + # shuffle dropdowns + scramble_holes(world, player) + + # place links house + links_house = place_links_house(world, sectors, player) + + # place blacksmith, has limited options + place_blacksmith(world, links_house, player) + + # place connectors in inaccessible regions + connector_entrances = [e for e in list(zip(*default_connector_connections + default_dungeon_connections + open_default_dungeon_connections))[0] if e in entrance_pool] + connect_inaccessible_regions(world, connector_entrances, [], caves, player) + + # place old man, has limited options + connector_entrances = [e for e in connector_entrances if e in entrance_pool] + place_old_man(world, list(connector_entrances), player) + + # shuffle remaining connectors + connector_entrances = [e for e in connector_entrances if e in entrance_pool] + connect_caves(world, connector_entrances, [], caves, player) + + # place bomb shop, has limited options + bomb_shop_doors = list(entrance_pool) + if world.logic[player] in ['noglitches', 'minorglitches'] or (invFlag != (0x1b in world.owswaps[player][0] and world.owMixed[player])): + bomb_shop_doors = [e for e in entrance_pool if e not in ['Pyramid Fairy']] + bomb_shop = random.choice(bomb_shop_doors) + connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) + + # place remaining doors + connect_doors(world, list(entrance_pool), list(exit_pool), player) + elif world.shuffle[player] == 'crossed': + suppress_spoiler = False + skull_woods_shuffle(world, player) + + caves = list(Cave_Exits + Dungeon_Exits + Cave_Three_Exits + Old_Man_House) + + if world.mode[player] == 'standard': + connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) + caves.append(tuple(random.sample(['Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)'], 2))) + else: + caves.append(tuple(random.sample(['Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)'], 3))) + + if not world.shuffle_ganon: + connect_two_way(world, 'Ganons Tower' if not invFlag else 'Agahnims Tower', 'Ganons Tower Exit', player) + else: + caves.append('Ganons Tower Exit') + + # shuffle holes + scramble_holes(world, player) + + # place dark sanc + if invFlag: + place_dark_sanc(world, sectors, player) + + # place links house + links_house = place_links_house(world, sectors, player) + + # place blacksmith, has limited options + place_blacksmith(world, links_house, player) + + # place connectors in inaccessible regions + connect_inaccessible_regions(world, list(entrance_pool), [], caves, player) + + # place old man, has limited options + place_old_man(world, list(entrance_pool), player) + + # place bomb shop, has limited options + bomb_shop_doors = list(entrance_pool) + if world.logic[player] in ['noglitches', 'minorglitches'] or (invFlag != (0x1b in world.owswaps[player][0] and world.owMixed[player])): + bomb_shop_doors = [e for e in entrance_pool if e not in ['Pyramid Fairy']] + bomb_shop = random.choice(bomb_shop_doors) + connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) + + # shuffle connectors + connect_caves(world, list(entrance_pool), [], caves, player) + + # place remaining doors + connect_doors(world, list(entrance_pool), list(exit_pool), player) elif world.shuffle[player] == 'insanity': # beware ye who enter here - ignore_pool = True + suppress_spoiler = False - if not invFlag: - entrances_must_exits = DW_Entrances_Must_Exit + DW_Dungeon_Entrances_Must_Exit + LW_Dungeon_Entrances_Must_Exit + ['Skull Woods Second Section Door (West)'] + # list preparation + caves = Cave_Exits + Dungeon_Exits + Cave_Three_Exits + Old_Man_House + \ + ['Skull Woods First Section Exit', 'Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)', + 'Kakariko Well Exit', 'Bat Cave Exit', 'North Fairy Cave Exit', 'Lost Woods Hideout Exit', 'Lumberjack Tree Exit', 'Sanctuary Exit'] - doors = LW_Entrances + LW_Dungeon_Entrances + LW_Dungeon_Entrances_Must_Exit + ['Kakariko Well Cave', 'Bat Cave Cave', 'North Fairy Cave', 'Sanctuary', 'Lost Woods Hideout Stump', 'Lumberjack Tree Cave'] + Old_Man_Entrances +\ - DW_Entrances + DW_Dungeon_Entrances + DW_Entrances_Must_Exit + DW_Dungeon_Entrances_Must_Exit + ['Skull Woods First Section Door', 'Skull Woods Second Section Door (East)', 'Skull Woods Second Section Door (West)'] +\ - LW_Single_Cave_Doors + DW_Single_Cave_Doors - else: - entrances_must_exits = Inverted_LW_Entrances_Must_Exit + Inverted_LW_Dungeon_Entrances_Must_Exit - - doors = Inverted_LW_Entrances + Inverted_LW_Dungeon_Entrances + Inverted_LW_Entrances_Must_Exit + Inverted_LW_Dungeon_Entrances_Must_Exit + ['Kakariko Well Cave', 'Bat Cave Cave', 'North Fairy Cave', 'Sanctuary', 'Lost Woods Hideout Stump', 'Lumberjack Tree Cave', 'Hyrule Castle Secret Entrance Stairs'] + Inverted_Old_Man_Entrances +\ - Inverted_DW_Entrances + Inverted_DW_Dungeon_Entrances + ['Skull Woods First Section Door', 'Skull Woods Second Section Door (East)', 'Skull Woods Second Section Door (West)'] +\ - LW_Single_Cave_Doors + Inverted_DW_Single_Cave_Doors + ['Desert Palace Entrance (West)', 'Desert Palace Entrance (North)'] - - exit_pool = list(doors) - - if invFlag: - # randomize which desert ledge door is a must-exit - if random.randint(0, 1) == 0: - entrances_must_exits.append('Desert Palace Entrance (North)') - exit_pool.append('Desert Palace Entrance (West)') - else: - entrances_must_exits.append('Desert Palace Entrance (West)') - exit_pool.append('Desert Palace Entrance (North)') - - # TODO: there are other possible entrances we could support here by way of exiting from a connector, - # and rentering to find bomb shop. However appended list here is all those that we currently have - # bomb shop logic for. - # Specifically we could potentially add: 'Dark Death Mountain Ledge (East)' and doors associated with pits - if not invFlag: - bomb_shop_doors = list(Bomb_Shop_Single_Cave_Doors + Bomb_Shop_Multi_Cave_Doors+['Desert Palace Entrance (East)', 'Turtle Rock Isolated Ledge Entrance', 'Bumper Cave (Top)', 'Hookshot Cave Back Entrance']) - blacksmith_doors = list(Blacksmith_Single_Cave_Doors + Blacksmith_Multi_Cave_Doors) - door_targets = list(Single_Cave_Targets) - else: - bomb_shop_doors = list(Inverted_Bomb_Shop_Single_Cave_Doors + Inverted_Bomb_Shop_Multi_Cave_Doors + ['Turtle Rock Isolated Ledge Entrance', 'Hookshot Cave Back Entrance']) - blacksmith_doors = list(Inverted_Blacksmith_Single_Cave_Doors + Blacksmith_Multi_Cave_Doors) - door_targets = list(Inverted_Single_Cave_Targets) - - random.shuffle(doors) - - if not invFlag: - old_man_entrances = list(Old_Man_Entrances) + ['Tower of Hera'] - else: - old_man_entrances = list(Inverted_Old_Man_Entrances + Old_Man_Entrances) + ['Tower of Hera', 'Ganons Tower'] - - caves = Cave_Exits + Dungeon_Exits + Cave_Three_Exits + ['Old Man House Exit (Bottom)', 'Old Man House Exit (Top)', 'Skull Woods First Section Exit', 'Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)', - 'Kakariko Well Exit', 'Bat Cave Exit', 'North Fairy Cave Exit', 'Lost Woods Hideout Exit', 'Lumberjack Tree Exit', 'Sanctuary Exit'] - - - # shuffle up holes hole_entrances = ['Kakariko Well Drop', 'Bat Cave Drop', 'North Fairy Cave Drop', 'Lost Woods Hideout Drop', 'Lumberjack Tree Tree', 'Sanctuary Grave', 'Skull Woods First Section Hole (East)', 'Skull Woods First Section Hole (West)', 'Skull Woods First Section Hole (North)', 'Skull Woods Second Section Hole'] @@ -710,17 +572,15 @@ def link_entrances(world, player): 'Skull Left Drop', 'Skull Pinball', 'Skull Pot Circle'] if world.mode[player] == 'standard': - # cannot move uncle cave + connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) connect_entrance(world, 'Hyrule Castle Secret Entrance Drop', 'Hyrule Castle Secret Entrance', player) - connect_exit(world, 'Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance Stairs', player) - connect_entrance(world, 'Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Secret Entrance Exit', player) + connect_two_way(world, 'Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Secret Entrance Exit', player) + caves.append(('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')) else: hole_entrances.append('Hyrule Castle Secret Entrance Drop') hole_targets.append('Hyrule Castle Secret Entrance') caves.append('Hyrule Castle Secret Entrance Exit') - if not invFlag: - doors.append('Hyrule Castle Secret Entrance Stairs') - exit_pool.append('Hyrule Castle Secret Entrance Stairs') + caves.append(('Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')) if not world.shuffle_ganon: connect_two_way(world, 'Ganons Tower' if not invFlag else 'Agahnims Tower', 'Ganons Tower Exit', player) @@ -728,145 +588,77 @@ def link_entrances(world, player): connect_entrance(world, 'Pyramid Hole' if invFlag == (0x1b in world.owswaps[player][0] and world.owMixed[player]) else 'Inverted Pyramid Hole', 'Pyramid', player) else: caves.extend(['Ganons Tower Exit', 'Pyramid Exit']) + hole_entrances.append('Pyramid Hole' if invFlag == (0x1b in world.owswaps[player][0] and world.owMixed[player]) else 'Inverted Pyramid Hole') hole_targets.append('Pyramid') - exit_pool.extend(['Ganons Tower' if not invFlag else 'Agahnims Tower']) - doors.extend(['Ganons Tower' if not invFlag else 'Agahnims Tower']) - - if invFlag == (0x1b in world.owswaps[player][0] and world.owMixed[player]): - exit_pool.extend(['Pyramid Entrance']) - hole_entrances.append('Pyramid Hole') - entrances_must_exits.append('Pyramid Entrance') - doors.extend(['Pyramid Entrance']) - else: - exit_pool.extend(['Inverted Pyramid Entrance']) - hole_entrances.append('Inverted Pyramid Hole') - doors.extend(['Inverted Pyramid Entrance']) + # shuffle holes random.shuffle(hole_entrances) random.shuffle(hole_targets) - random.shuffle(exit_pool) - - # fill up holes for hole in hole_entrances: connect_entrance(world, hole, hole_targets.pop(), player) - # hyrule castle handling - if world.mode[player] == 'standard': - # must connect front of hyrule castle to do escape - connect_entrance(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) - connect_exit(world, 'Hyrule Castle Exit (South)', 'Hyrule Castle Entrance (South)', player) - caves.append(('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')) - else: - doors.append('Hyrule Castle Entrance (South)') - caves.append(('Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')) - if not invFlag: - exit_pool.append('Hyrule Castle Entrance (South)') + # place dark sanc + if invFlag: + place_dark_sanc(world, sectors, player) # place links house - if world.mode[player] == 'standard' or not world.shufflelinks[player]: - links_house = 'Links House' if not invFlag else 'Big Bomb Shop' - else: - links_house_doors = [i for i in doors if i not in isolated_entrances + ([] if not invFlag else Inverted_Dark_Sanctuary_Doors)] - if not invFlag and world.doorShuffle[player] == 'crossed' and world.intensity[player] >= 3: - exclusions = DW_Entrances + DW_Dungeon_Entrances + DW_Single_Cave_Doors \ - + DW_Entrances_Must_Exit + DW_Dungeon_Entrances_Must_Exit + ['Ganons Tower'] - links_house_doors = [i for i in links_house_doors if i not in exclusions] - links_house = random.choice(links_house_doors) - connect_two_way(world, links_house, 'Links House Exit', player) - exit_pool.remove(links_house) - doors.remove(links_house) + links_house = place_links_house(world, sectors, player) - if invFlag: - # place dark sanc - sanc_doors = [door for door in Inverted_Dark_Sanctuary_Doors if door in exit_pool] - sanc_door = random.choice(sanc_doors) - exit_pool.remove(sanc_door) - doors.remove(sanc_door) - connect_entrance(world, sanc_door, 'Dark Sanctuary Hint', player) - world.get_entrance('Dark Sanctuary Hint Exit', player).connect(world.get_entrance(sanc_door, player).parent_region) - - # now let's deal with mandatory reachable stuff - def extract_reachable_exit(cavelist): - random.shuffle(cavelist) - candidate = None - for cave in cavelist: - if isinstance(cave, tuple) and len(cave) > 1: - # special handling: TRock has two entries that we should consider entrance only - # ToDo this should be handled in a more sensible manner - if cave[0] in ['Turtle Rock Exit (Front)', 'Spectacle Rock Cave Exit (Peak)'] and len(cave) == 2: - continue - candidate = cave - break - if candidate is None: - raise RuntimeError('No suitable cave.') - cavelist.remove(candidate) - return candidate - - def connect_reachable_exit(entrance, caves, doors, exit_pool): - cave = extract_reachable_exit(caves) - - exit = cave[-1] - cave = cave[:-1] - connect_exit(world, exit, entrance, player) - connect_entrance(world, doors.pop(), exit, player) - # rest of cave now is forced to be in this world - exit_pool.remove(entrance) - caves.append(cave) - - # connect mandatory exits - for entrance in entrances_must_exits: - connect_reachable_exit(entrance, caves, doors, exit_pool) + # place blacksmith, place sanc exit first for additional blacksmith candidates + doors = list(entrance_pool) + random.shuffle(doors) + door = doors.pop() + connect_entrance(world, door, 'Sanctuary Exit', player, False) + doors = [e for e in doors if e not in entrance_exits] + door = doors.pop() + connect_exit(world, 'Sanctuary Exit', door, player, False) + caves.remove('Sanctuary Exit') + place_blacksmith(world, links_house, player) + # place connectors in inaccessible regions + connect_inaccessible_regions(world, list(entrance_pool), [], caves, player) + # place old man, has limited options - # exit has to come from specific set of doors, the entrance is free to move about - old_man_entrances = [entrance for entrance in old_man_entrances if entrance in exit_pool] - random.shuffle(old_man_entrances) - old_man_exit = old_man_entrances.pop() - exit_pool.remove(old_man_exit) - - connect_exit(world, 'Old Man Cave Exit (East)', old_man_exit, player) - connect_entrance(world, doors.pop(), 'Old Man Cave Exit (East)', player) + place_old_man(world, list(entrance_pool), player) caves.append('Old Man Cave Exit (West)') - # place blacksmith, has limited options - blacksmith_doors = [door for door in blacksmith_doors if door in doors] - random.shuffle(blacksmith_doors) - blacksmith_hut = blacksmith_doors.pop() - connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player) - exit_pool.remove(blacksmith_hut) - doors.remove(blacksmith_hut) - - # place dam and pyramid fairy, have limited options - bomb_shop_doors = [door for door in bomb_shop_doors if door in doors] + # place bomb shop, has limited options + bomb_shop_doors = list(entrance_pool) + if world.logic[player] in ['noglitches', 'minorglitches'] or (invFlag != (0x1b in world.owswaps[player][0] and world.owMixed[player])): + bomb_shop_doors = [e for e in entrance_pool if e not in ['Pyramid Fairy']] random.shuffle(bomb_shop_doors) bomb_shop = bomb_shop_doors.pop() connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) - exit_pool.remove(bomb_shop) - doors.remove(bomb_shop) - - # handle remaining caves + + # shuffle connectors + doors = list(entrance_pool) + exit_doors = [e for e in entrance_pool if e not in entrance_exits] + random.shuffle(doors) + random.shuffle(exit_doors) for cave in caves: if isinstance(cave, str): cave = (cave,) - for exit in cave: - connect_exit(world, exit, exit_pool.pop(), player) - connect_entrance(world, doors.pop(), exit, player) + connect_exit(world, exit, exit_doors.pop(), player, False) + connect_entrance(world, doors.pop(), exit, player, False) # place remaining doors - connect_doors(world, doors, door_targets, player) + connect_doors(world, list(entrance_pool), list(exit_pool), player) else: raise NotImplementedError('Shuffling not supported yet') # ensure Houlihan exits where Links House does # TODO: Plando should overrule this - for links_house in world.get_entrance('Links House Exit', player).connected_region.exits: - if links_house.connected_region and links_house.connected_region.name == 'Links House': - break - connect_exit(world, 'Chris Houlihan Room Exit', links_house.name, player) + if not links_house: + for links_house in world.get_entrance('Links House Exit', player).connected_region.exits: + if links_house.connected_region and links_house.connected_region.name == 'Links House': + links_house = links_house.name + break + connect_exit(world, 'Chris Houlihan Room Exit', links_house, player) + ignore_pool = True # check for swamp palace fix - if world.get_entrance('Dam', player).connected_region.name != 'Dam' or world.get_entrance('Swamp Palace', player).connected_region.name != 'Swamp Portal': + if not (world.get_entrance('Dam', player).connected_region.name in ['Dam', 'Swamp Portal'] and world.get_entrance('Swamp Palace', player).connected_region.name == ['Dam', 'Swamp Portal']): world.swamp_patch_required[player] = True # check for potion shop location @@ -881,6 +673,7 @@ def link_entrances(world, player): if world.get_entrance('Ganons Tower' if not invFlag else 'Agahnims Tower', player).connected_region.name != 'Ganons Tower Portal' if not invFlag else 'GT Lobby': world.ganonstower_vanilla[player] = False + def connect_custom(world, player): if hasattr(world, 'custom_entrances') and world.custom_entrances[player]: for exit_name, region_name in world.custom_entrances[player]: @@ -895,10 +688,7 @@ def connect_simple(world, exitname, regionname, player): def connect_logical(world, entrancename, exitname, player, isTwoWay = False): if not ignore_pool: - if entrancename not in entrance_pool: - x = 9 - if exitname not in exit_pool: - x = 9 + logging.getLogger('').debug('Connecting %s -> %s', entrancename, exitname) assert entrancename in entrance_pool, 'Entrance not in pool: ' + entrancename assert exitname in exit_pool, 'Exit not in pool: ' + exitname @@ -919,14 +709,12 @@ def connect_logical(world, entrancename, exitname, player, isTwoWay = False): exit_pool.remove(exitname) -def connect_entrance(world, entrancename, exitname, player): +def connect_entrance(world, entrancename, exitname, player, mark_two_way=True): if not ignore_pool: - if entrancename not in entrance_pool: - x = 9 - if exitname not in exit_pool: - x = 9 + logging.getLogger('').debug('Connecting %s -> %s', entrancename, exitname) assert entrancename in entrance_pool, 'Entrance not in pool: ' + entrancename - assert exitname in exit_pool, 'Exit not in pool: ' + exitname + if mark_two_way: + assert exitname in exit_pool, 'Exit not in pool: ' + exitname entrance = world.get_entrance(entrancename, player) # check if we got an entrance or a region to connect to @@ -948,18 +736,18 @@ def connect_entrance(world, entrancename, exitname, player): if not ignore_pool: entrance_pool.remove(entrancename) - exit_pool.remove(exitname) + if mark_two_way: + exit_pool.remove(exitname) - if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull']: + if not suppress_spoiler: world.spoiler.set_entrance(entrance.name, exit.name if exit is not None else region.name, 'entrance', player) -def connect_exit(world, exitname, entrancename, player): + +def connect_exit(world, exitname, entrancename, player, mark_two_way=True): if not (ignore_pool or exitname == 'Chris Houlihan Room Exit'): - if entrancename not in entrance_pool: - x = 9 - if exitname not in exit_pool: - x = 9 - assert entrancename in entrance_pool, 'Entrance not in pool: ' + entrancename + logging.getLogger('').debug('Connecting %s -> %s', exitname, entrancename) + if mark_two_way: + assert entrancename in entrance_pool, 'Entrance not in pool: ' + entrancename assert exitname in exit_pool, 'Exit not in pool: ' + exitname entrance = world.get_entrance(entrancename, player) @@ -972,19 +760,19 @@ def connect_exit(world, exitname, entrancename, player): exit.connect(entrance.parent_region, door_addresses[entrance.name][1], exit_ids[exit.name][1]) if not (ignore_pool or exitname == 'Chris Houlihan Room Exit'): - entrance_pool.remove(entrancename) + if mark_two_way: + entrance_pool.remove(entrancename) + elif world.shuffle[player] == 'insanity': + entrance_exits.append(entrancename) exit_pool.remove(exitname) - if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull']: + if not suppress_spoiler: world.spoiler.set_entrance(entrance.name, exit.name, 'exit', player) def connect_two_way(world, entrancename, exitname, player): if not ignore_pool: - if entrancename not in entrance_pool: - x = 9 - if exitname not in exit_pool: - x = 9 + logging.getLogger('').debug('Connecting %s <-> %s', entrancename, exitname) assert entrancename in entrance_pool, 'Entrance not in pool: ' + entrancename assert exitname in exit_pool, 'Exit not in pool: ' + exitname @@ -1003,67 +791,13 @@ def connect_two_way(world, entrancename, exitname, player): if not ignore_pool: entrance_pool.remove(entrancename) exit_pool.remove(exitname) + if world.shuffle[player] == 'insanity': + entrance_exits.append(entrancename) - if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull']: + if not suppress_spoiler: world.spoiler.set_entrance(entrance.name, exit.name, 'both', player) -def scramble_holes(world, player): - hole_entrances = [('Kakariko Well Cave', 'Kakariko Well Drop'), - ('Bat Cave Cave', 'Bat Cave Drop'), - ('North Fairy Cave', 'North Fairy Cave Drop'), - ('Lost Woods Hideout Stump', 'Lost Woods Hideout Drop'), - ('Lumberjack Tree Cave', 'Lumberjack Tree Tree'), - ('Sanctuary', 'Sanctuary Grave')] - - hole_targets = [('Kakariko Well Exit', 'Kakariko Well (top)'), - ('Bat Cave Exit', 'Bat Cave (right)'), - ('North Fairy Cave Exit', 'North Fairy Cave'), - ('Lost Woods Hideout Exit', 'Lost Woods Hideout (top)'), - ('Lumberjack Tree Exit', 'Lumberjack Tree (top)')] - - if world.mode[player] == 'standard': - # cannot move uncle cave - connect_two_way(world, 'Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Secret Entrance Exit', player) - connect_entrance(world, 'Hyrule Castle Secret Entrance Drop', 'Hyrule Castle Secret Entrance', player) - else: - hole_entrances.append(('Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Secret Entrance Drop')) - hole_targets.append(('Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance')) - - # do not shuffle sanctuary into pyramid hole unless shuffle is crossed - if world.shuffle[player] == 'crossed': - hole_targets.append(('Sanctuary Exit', 'Sewer Drop')) - - # determine pyramid hole - if not world.shuffle_ganon: - if (world.mode[player] == 'inverted') == (0x03 in world.owswaps[player][0] and world.owMixed[player]): - connect_two_way(world, 'Pyramid Entrance', 'Pyramid Exit', player) - connect_entrance(world, 'Pyramid Hole', 'Pyramid', player) - else: - connect_two_way(world, 'Inverted Pyramid Entrance', 'Pyramid Exit', player) - connect_entrance(world, 'Inverted Pyramid Hole', 'Pyramid', player) - else: - hole_targets.append(('Pyramid Exit', 'Pyramid')) - - random.shuffle(hole_targets) - exit, target = hole_targets.pop() - if (world.mode[player] == 'inverted') == (0x03 in world.owswaps[player][0] and world.owMixed[player]): - connect_two_way(world, 'Pyramid Entrance', exit, player) - connect_entrance(world, 'Pyramid Hole', target, player) - else: - connect_two_way(world, 'Inverted Pyramid Entrance', exit, player) - connect_entrance(world, 'Inverted Pyramid Hole', target, player) - - if world.shuffle[player] != 'crossed': - hole_targets.append(('Sanctuary Exit', 'Sewer Drop')) - - # shuffle the rest - random.shuffle(hole_targets) - for entrance, drop in hole_entrances: - exit, target = hole_targets.pop() - connect_two_way(world, entrance, exit, player) - connect_entrance(world, drop, target, player) - def connect_random(world, exitlist, targetlist, player, two_way=False): targetlist = list(targetlist) random.shuffle(targetlist) @@ -1075,75 +809,151 @@ def connect_random(world, exitlist, targetlist, player, two_way=False): connect_entrance(world, exit, target, player) -def connect_mandatory_exits(world, entrances, caves, must_be_exits, player): - +def connect_mandatory_exits(world, entrances, caves, must_be_exits, player, must_deplete_mustexits=True): # Keeps track of entrances that cannot be used to access each exit / cave - if world.mode[player] == 'inverted': - invalid_connections = Inverted_Must_Exit_Invalid_Connections.copy() - else: - invalid_connections = Must_Exit_Invalid_Connections.copy() invalid_cave_connections = defaultdict(set) - if world.logic[player] in ['owglitches', 'nologic']: - import OverworldGlitchRules - for entrance in OverworldGlitchRules.get_non_mandatory_exits(world.mode[player] == 'inverted'): - invalid_connections[entrance] = set() - if entrance in must_be_exits: - must_be_exits.remove(entrance) - entrances.append(entrance) + # if world.logic[player] in ['owglitches', 'nologic']: + # import OverworldGlitchRules + # for entrance in OverworldGlitchRules.get_non_mandatory_exits(world.mode[player] == 'inverted'): + # if entrance in must_be_exits: + # must_be_exits.remove(entrance) + # entrances.append(entrance) + + # for insanity use only + def extract_reachable_exit(cavelist): + candidate = None + for cave in cavelist: + if isinstance(cave, tuple) and len(cave) > 1: + # special handling: TRock has two entries that we should consider entrance only + # ToDo this should be handled in a more sensible manner + if cave[0] in ['Turtle Rock Exit (Front)', 'Spectacle Rock Cave Exit (Peak)'] and len(cave) == 2: + continue + candidate = cave + break + if candidate is None: + raise RuntimeError('No suitable cave.') + return candidate """This works inplace""" random.shuffle(entrances) random.shuffle(caves) - # Handle inverted Aga Tower - if it depends on connections, then so does Hyrule Castle Ledge - if world.mode[player] == 'inverted': - for entrance in invalid_connections: - if world.get_entrance(entrance, player).connected_region == world.get_region('Agahnims Tower Portal', player): - for exit in invalid_connections[entrance]: - invalid_connections[exit] = invalid_connections[exit].union({'Agahnims Tower', 'Hyrule Castle Entrance (West)', 'Hyrule Castle Entrance (East)'}) - break - + from DoorShuffle import find_inaccessible_regions + used_caves = [] required_entrances = 0 # Number of entrances reserved for used_caves - while must_be_exits: + skip_remaining = False + while must_be_exits and not skip_remaining: exit = must_be_exits.pop() + # find multi exit cave + # * this is a mess, but it ensures a loose assignment possibility when the cave/entrance pool is plentiful, + # * but can also find and prepare for solutions when the cave/entrance pool is limiting + # * however, this probably could be better implemented cave = None - for candidate in caves: - if not isinstance(candidate, str) and (candidate in used_caves or len(candidate) < len(entrances) - required_entrances - 1): - cave = candidate - break + if world.shuffle[player] == 'insanity': + cave = extract_reachable_exit(caves) + else: + if must_deplete_mustexits: + cave_surplus = sum(0 if isinstance(x, str) else len(x) - 1 for x in caves) - (len(must_be_exits) + 1) + if cave_surplus < 0: + raise RuntimeError('Not enough multi-entrance caves left to connect unreachable regions!') + if len(entrances) < len(must_be_exits) + 1: + raise RuntimeError('Not enough entrances left to connect unreachable regions!') + if cave_surplus > len(must_be_exits): + for candidate in caves: + if not isinstance(candidate, str) and (candidate in used_caves or len(candidate) < len(entrances) - required_entrances - 1): + cave = candidate + break + if len(must_be_exits) == 0: # if assigning last must exit + for candidate in caves: + if not isinstance(candidate, str) and (candidate in used_caves or len(candidate) <= len(entrances) - required_entrances - 1): + cave = candidate + break + if cave is None and cave_surplus <= 1: # if options are limited + # attempt to find use caves already used + for candidate in caves: + if not isinstance(candidate, str) and candidate in used_caves: + cave = candidate + break + if cave is None: + # attempt to find caves with exact number of exits + for candidate in caves: + if not isinstance(candidate, str) and (len(entrances) - required_entrances - 1) - len(candidate) == 0: + cave = candidate + break + if cave is None: + # attempt to find caves with one left over exit + for candidate in caves: + if not isinstance(candidate, str) and (len(entrances) - required_entrances - 1) - len(candidate) == 1: + cave = candidate + break + + if cave is None: + for candidate in caves: + if not isinstance(candidate, str) and (candidate in used_caves or len(candidate) < len(entrances) - required_entrances - 1): + cave = candidate + break + if cave is None and must_deplete_mustexits: + for candidate in caves: + if not isinstance(candidate, str) and (candidate in used_caves or len(candidate) <= len(entrances) - required_entrances - 1): + cave = candidate + break + + inaccessible_entrances = list() + for region_name in world.inaccessible_regions[player]: + region = world.get_region(region_name, player) + if region.type in [RegionType.LightWorld, RegionType.DarkWorld]: + for x in region.exits: + if not x.connected_region and x.name in entrance_pool: + inaccessible_entrances.append(x.name) if cave is None: - raise RuntimeError('No more caves left. Should not happen!') + if must_deplete_mustexits: + raise RuntimeError('No more caves left. Should not happen!') + else: + must_be_exits.append(exit) + skip_remaining = True + continue # all caves are sorted so that the last exit is always reachable - connect_two_way(world, exit, cave[-1], player) - if len(cave) == 2: - entrance = next(e for e in entrances[::-1] if e not in invalid_connections[exit] and e not in invalid_cave_connections[tuple(cave)]) + if world.shuffle[player] == 'insanity': + connect_exit(world, cave[-1], exit, player, False) + entrance = next(e for e in entrances[::-1] if e not in entrance_exits + inaccessible_entrances + list(invalid_cave_connections[tuple(cave)])) entrances.remove(entrance) - connect_two_way(world, entrance, cave[0], player) + connect_entrance(world, entrance, cave[-1], player, False) + else: + connect_two_way(world, exit, cave[-1], player) + + if len(cave) == 2: + entrance = next(e for e in entrances[::-1] if e not in inaccessible_entrances and e not in invalid_cave_connections[tuple(cave)]) + entrances.remove(entrance) + if world.shuffle[player] == 'insanity': + connect_entrance(world, entrance, cave[0], player, False) + entrance = next(e for e in entrances[::-1] if e not in entrance_exits + inaccessible_entrances + list(invalid_cave_connections[tuple(cave)])) + entrances.remove(entrance) + connect_exit(world, cave[0], entrance, player, False) + else: + connect_two_way(world, entrance, cave[0], player) if cave in used_caves: required_entrances -= 2 used_caves.remove(cave) - if entrance in invalid_connections: - for exit2 in invalid_connections[entrance]: - invalid_connections[exit2] = invalid_connections[exit2].union(invalid_connections[exit]).union(invalid_cave_connections[tuple(cave)]) - elif cave[-1] == 'Spectacle Rock Cave Exit': #Spectacle rock only has one exit + elif cave[-1] == 'Spectacle Rock Cave Exit': # Spectacle rock only has one exit cave_entrances = [] for cave_exit in cave[:-1]: - entrance = next(e for e in entrances[::-1] if e not in invalid_connections[exit]) + entrance = next(e for e in entrances[::-1] if e not in inaccessible_entrances) cave_entrances.append(entrance) entrances.remove(entrance) - connect_two_way(world,entrance,cave_exit, player) - if entrance not in invalid_connections: - invalid_connections[exit] = set() - if all(entrance in invalid_connections for entrance in cave_entrances): - new_invalid_connections = invalid_connections[cave_entrances[0]].intersection(invalid_connections[cave_entrances[1]]) - for exit2 in new_invalid_connections: - invalid_connections[exit2] = invalid_connections[exit2].union(invalid_connections[exit]) - else:#save for later so we can connect to multiple exits + if world.shuffle[player] == 'insanity': + connect_entrance(world, entrance, cave_exit, player, False) + entrance = next(e for e in entrances[::-1] if e not in entrance_exits + inaccessible_entrances) + cave_entrances.append(entrance) + entrances.remove(entrance) + connect_exit(world, cave_exit, entrance, player, False) + else: + connect_two_way(world, entrance, cave_exit, player) + else: # save for later so we can connect to multiple exits if cave in used_caves: required_entrances -= 1 used_caves.remove(cave) @@ -1152,15 +962,24 @@ def connect_mandatory_exits(world, entrances, caves, must_be_exits, player): caves.append(cave[0:-1]) random.shuffle(caves) used_caves.append(cave[0:-1]) - invalid_cave_connections[tuple(cave[0:-1])] = invalid_cave_connections[tuple(cave)].union(invalid_connections[exit]) + invalid_cave_connections[tuple(cave[0:-1])] = invalid_cave_connections[tuple(cave)].union(inaccessible_entrances).union(entrance_exits) caves.remove(cave) + + find_inaccessible_regions(world, player) + for cave in used_caves: - if cave in caves: #check if we placed multiple entrances from this 3 or 4 exit + if cave in caves: # check if we placed multiple entrances from this 3 or 4 exit for cave_exit in cave: - entrance = next(e for e in entrances[::-1] if e not in invalid_cave_connections[tuple(cave)]) + entrance = next(e for e in entrances[::-1] if e not in inaccessible_entrances and e not in invalid_cave_connections[tuple(cave)]) invalid_cave_connections[tuple(cave)] = set() entrances.remove(entrance) - connect_two_way(world, entrance, cave_exit, player) + if world.shuffle[player] == 'insanity': + connect_entrance(world, entrance, cave_exit, player, False) + entrance = next(e for e in entrances[::-1] if e not in entrance_exits + inaccessible_entrances + list(invalid_cave_connections[tuple(cave)])) + entrances.remove(entrance) + connect_exit(world, cave_exit, entrance, player, False) + else: + connect_two_way(world, entrance, cave_exit, player) caves.remove(cave) @@ -1203,6 +1022,90 @@ def connect_doors(world, doors, targets, player): targets[:] = targets[placing:] +def scramble_holes(world, player): + invFlag = world.mode[player] == 'inverted' + + hole_entrances = [('Kakariko Well Cave', 'Kakariko Well Drop'), + ('Bat Cave Cave', 'Bat Cave Drop'), + ('North Fairy Cave', 'North Fairy Cave Drop'), + ('Lost Woods Hideout Stump', 'Lost Woods Hideout Drop'), + ('Lumberjack Tree Cave', 'Lumberjack Tree Tree'), + ('Sanctuary', 'Sanctuary Grave')] + + hole_targets = [('Kakariko Well Exit', 'Kakariko Well (top)'), + ('Bat Cave Exit', 'Bat Cave (right)'), + ('North Fairy Cave Exit', 'North Fairy Cave'), + ('Lost Woods Hideout Exit', 'Lost Woods Hideout (top)'), + ('Lumberjack Tree Exit', 'Lumberjack Tree (top)')] + + # force uncle cave + if world.mode[player] == 'standard': + connect_two_way(world, 'Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Secret Entrance Exit', player) + connect_entrance(world, 'Hyrule Castle Secret Entrance Drop', 'Hyrule Castle Secret Entrance', player) + else: + hole_entrances.append(('Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Secret Entrance Drop')) + hole_targets.append(('Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance')) + + if world.shuffle_ganon: + hole_entrances.append(('Pyramid Entrance', 'Pyramid Hole') if invFlag == (0x1b in world.owswaps[player][0] and world.owMixed[player]) else ('Inverted Pyramid Entrance', 'Inverted Pyramid Hole')) + hole_targets.append(('Pyramid Exit', 'Pyramid')) + + # shuffle sanctuary hole in same world as other HC entrances + if world.shuffle[player] not in ['lean', 'crossed']: + drop_owid_map = { # owid, is_light_world + 'Lost Woods Hideout Stump': (0x00, True), + 'Lumberjack Tree Cave': (0x02, True), + 'Sanctuary': (0x13, True), + 'North Fairy Cave': (0x15, True), + 'Kakariko Well Cave': (0x18, True), + 'Hyrule Castle Secret Entrance Stairs': (0x1b, True), + 'Bat Cave Cave': (0x22, True), + 'Inverted Pyramid Entrance': (0x1b, True), + 'Pyramid Entrance': (0x5b, False) + } + + region = world.get_entrance('Hyrule Castle Exit (South)', player).parent_region + if len(region.entrances) > 0: + hc_in_lw = region.entrances[0].parent_region.type == (RegionType.LightWorld if not invFlag else RegionType.DarkWorld) + elif world.shuffle[player] == 'lite': + hc_in_lw = not invFlag + else: + # checks if drop candidates exist in LW + drop_owids = [ 0x00, 0x02, 0x13, 0x15, 0x18, 0x1b, 0x22 ] + hc_in_lw = any([invFlag == (owid in world.owswaps[player][0] and world.owMixed[player]) for owid in drop_owids]) + + candidate_drops = list() + for door, drop in hole_entrances: + if hc_in_lw == (drop_owid_map[door][1] == (invFlag == (drop_owid_map[door][0] in world.owswaps[player][0] and world.owMixed[player]))): + candidate_drops.append(tuple((door, drop))) + + random.shuffle(candidate_drops) + door, drop = candidate_drops.pop() + hole_entrances.remove((door, drop)) + connect_two_way(world, door, 'Sanctuary Exit', player) + connect_entrance(world, drop, 'Sewer Drop', player) + else: + hole_targets.append(('Sanctuary Exit', 'Sewer Drop')) + + # place pyramid hole + if not world.shuffle_ganon: + exit, target = ('Pyramid Exit', 'Pyramid') + if invFlag == (0x1b in world.owswaps[player][0] and world.owMixed[player]): + connect_two_way(world, 'Pyramid Entrance', exit, player) + connect_entrance(world, 'Pyramid Hole', target, player) + else: + connect_two_way(world, 'Inverted Pyramid Entrance', exit, player) + connect_entrance(world, 'Inverted Pyramid Hole', target, player) + + # shuffle the rest + random.shuffle(hole_entrances) + random.shuffle(hole_targets) + for entrance, drop in hole_entrances: + exit, target = hole_targets.pop() + connect_two_way(world, entrance, exit, player) + connect_entrance(world, drop, target, player) + + def skull_woods_shuffle(world, player): connect_random(world, ['Skull Woods First Section Hole (East)', 'Skull Woods First Section Hole (West)', 'Skull Woods First Section Hole (North)', 'Skull Woods Second Section Hole'], ['Skull Left Drop', 'Skull Pinball', 'Skull Pot Circle', 'Skull Back Drop'], player) @@ -1211,145 +1114,122 @@ def skull_woods_shuffle(world, player): def simple_shuffle_dungeons(world, player): + invFlag = world.mode[player] == 'inverted' + skull_woods_shuffle(world, player) dungeon_entrances = ['Eastern Palace', 'Tower of Hera', 'Thieves Town', 'Skull Woods Final Section', 'Palace of Darkness', 'Ice Palace', 'Misery Mire', 'Swamp Palace'] - dungeon_exits = ['Eastern Palace Exit', 'Tower of Hera Exit', 'Thieves Town Exit', 'Skull Woods Final Section Exit', 'Palace of Darkness Exit', 'Ice Palace Exit', 'Misery Mire Exit', 'Swamp Palace Exit'] + dungeon_exits = ['Eastern Palace Exit', 'Tower of Hera Exit', 'Agahnims Tower Exit', 'Thieves Town Exit', 'Skull Woods Final Section Exit', 'Palace of Darkness Exit', 'Ice Palace Exit', 'Misery Mire Exit', 'Swamp Palace Exit'] - if world.mode[player] != 'inverted': + if not invFlag: if not world.shuffle_ganon: connect_two_way(world, 'Ganons Tower', 'Ganons Tower Exit', player) else: dungeon_entrances.append('Ganons Tower') dungeon_exits.append('Ganons Tower Exit') + random.shuffle(dungeon_exits) + at_door = dungeon_exits.pop() else: - # TODO: Should we be ignoring world.shuffle_ganon?? dungeon_entrances.append('Ganons Tower') - dungeon_exits.append('Agahnims Tower Exit') + if not world.shuffle_ganon: + at_door = 'Ganons Tower Exit' + else: + dungeon_exits.append('Ganons Tower Exit') + random.shuffle(dungeon_exits) + at_door = dungeon_exits.pop() - # shuffle up single entrance dungeons + # shuffle single-entrance dungeons connect_random(world, dungeon_entrances, dungeon_exits, player, True) - # mix up 4 door dungeons - multi_dungeons = ['Desert', 'Turtle Rock'] - if world.mode[player] == 'open' or (world.mode[player] == 'inverted' and world.shuffle_ganon): - multi_dungeons.append('Hyrule Castle') - random.shuffle(multi_dungeons) - - dp_target = multi_dungeons[0] - tr_target = multi_dungeons[1] - if world.mode[player] not in ['open', 'inverted'] or (world.mode[player] == 'inverted' and world.shuffle_ganon is False): - # place hyrule castle as intended + # shuffle multi-entrance dungeons + multi_dungeons = ['Desert Palace', 'Turtle Rock'] + if world.mode[player] == 'standard' or (world.mode[player] == 'inverted' and not world.shuffle_ganon): hc_target = 'Hyrule Castle' else: - hc_target = multi_dungeons[2] - - # door shuffle should restrict hyrule castle to the light world due to sanc being limited to the LW - if world.doorShuffle[player] != 'vanilla' and hc_target == 'Turtle Rock': - swap_w_dp = random.choice([True, False]) - if swap_w_dp: - hc_target, dp_target = dp_target, hc_target + multi_dungeons.append('Hyrule Castle') + + dungeon_owid_map = { # owid, is_lw_dungeon + 'Hyrule Castle': (0x1b, True), + 'Desert Palace': (0x30, True), + 'Turtle Rock': (0x47, False) + } + + # checks if drop candidates exist in LW + drop_owids = [ 0x00, 0x02, 0x13, 0x15, 0x18, 0x1b, 0x22 ] + drops_in_light_world = any([invFlag == (owid in world.owswaps[player][0] and world.owMixed[player]) for owid in drop_owids]) + + # placing HC in guaranteed same-world as available dropdowns + if not drops_in_light_world or not invFlag: + candidate_dungeons = list() + for d in multi_dungeons: + if not drops_in_light_world and dungeon_owid_map[d][1] == (invFlag != (dungeon_owid_map[d][0] in world.owswaps[player][0] and world.owMixed[player])): + # only adding DW candidates + candidate_dungeons.append(d) + elif not invFlag and dungeon_owid_map[d][1] == (invFlag == (dungeon_owid_map[d][0] in world.owswaps[player][0] and world.owMixed[player])): + # only adding LW candidates + candidate_dungeons.append(d) + random.shuffle(candidate_dungeons) + hc_target = candidate_dungeons.pop() + multi_dungeons.remove(hc_target) else: - hc_target, tr_target = tr_target, hc_target + random.shuffle(multi_dungeons) + hc_target = multi_dungeons.pop() - # ToDo improve this? - - if world.mode[player] != 'inverted': - if hc_target == 'Hyrule Castle': - connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) - connect_two_way(world, 'Hyrule Castle Entrance (East)', 'Hyrule Castle Exit (East)', player) - connect_two_way(world, 'Hyrule Castle Entrance (West)', 'Hyrule Castle Exit (West)', player) - connect_two_way(world, 'Agahnims Tower', 'Agahnims Tower Exit', player) - elif hc_target == 'Desert': - connect_two_way(world, 'Desert Palace Entrance (South)', 'Hyrule Castle Exit (South)', player) - connect_two_way(world, 'Desert Palace Entrance (East)', 'Hyrule Castle Exit (East)', player) - connect_two_way(world, 'Desert Palace Entrance (West)', 'Hyrule Castle Exit (West)', player) - connect_two_way(world, 'Desert Palace Entrance (North)', 'Agahnims Tower Exit', player) - elif hc_target == 'Turtle Rock': - connect_two_way(world, 'Turtle Rock', 'Hyrule Castle Exit (South)', player) + dp_target = multi_dungeons.pop() + tr_target = multi_dungeons.pop() + + if hc_target == 'Hyrule Castle': + connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) + connect_two_way(world, 'Hyrule Castle Entrance (East)', 'Hyrule Castle Exit (East)', player) + connect_two_way(world, 'Hyrule Castle Entrance (West)', 'Hyrule Castle Exit (West)', player) + connect_two_way(world, 'Agahnims Tower', at_door, player) + elif hc_target == 'Desert Palace': + connect_two_way(world, 'Desert Palace Entrance (South)', 'Hyrule Castle Exit (South)', player) + connect_two_way(world, 'Desert Palace Entrance (East)', 'Hyrule Castle Exit (East)', player) + connect_two_way(world, 'Desert Palace Entrance (West)', 'Hyrule Castle Exit (West)', player) + connect_two_way(world, 'Desert Palace Entrance (North)', at_door, player) + elif hc_target == 'Turtle Rock': + connect_two_way(world, 'Turtle Rock', 'Hyrule Castle Exit (South)', player) + connect_two_way(world, 'Dark Death Mountain Ledge (West)', 'Hyrule Castle Exit (West)', player) + if invFlag == (0x45 in world.owswaps[player][0] and world.owMixed[player]): connect_two_way(world, 'Turtle Rock Isolated Ledge Entrance', 'Hyrule Castle Exit (East)', player) - connect_two_way(world, 'Dark Death Mountain Ledge (West)', 'Hyrule Castle Exit (West)', player) - connect_two_way(world, 'Dark Death Mountain Ledge (East)', 'Agahnims Tower Exit', player) - - if dp_target == 'Hyrule Castle': - connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Desert Palace Exit (South)', player) - connect_two_way(world, 'Hyrule Castle Entrance (East)', 'Desert Palace Exit (East)', player) - connect_two_way(world, 'Hyrule Castle Entrance (West)', 'Desert Palace Exit (West)', player) - connect_two_way(world, 'Agahnims Tower', 'Desert Palace Exit (North)', player) - elif dp_target == 'Desert': - connect_two_way(world, 'Desert Palace Entrance (South)', 'Desert Palace Exit (South)', player) - connect_two_way(world, 'Desert Palace Entrance (East)', 'Desert Palace Exit (East)', player) - connect_two_way(world, 'Desert Palace Entrance (West)', 'Desert Palace Exit (West)', player) - connect_two_way(world, 'Desert Palace Entrance (North)', 'Desert Palace Exit (North)', player) - elif dp_target == 'Turtle Rock': - connect_two_way(world, 'Turtle Rock', 'Desert Palace Exit (South)', player) - connect_two_way(world, 'Turtle Rock Isolated Ledge Entrance', 'Desert Palace Exit (East)', player) - connect_two_way(world, 'Dark Death Mountain Ledge (West)', 'Desert Palace Exit (West)', player) - connect_two_way(world, 'Dark Death Mountain Ledge (East)', 'Desert Palace Exit (North)', player) - - if tr_target == 'Hyrule Castle': - connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Turtle Rock Exit (Front)', player) - connect_two_way(world, 'Hyrule Castle Entrance (East)', 'Turtle Rock Ledge Exit (East)', player) - connect_two_way(world, 'Hyrule Castle Entrance (West)', 'Turtle Rock Ledge Exit (West)', player) - connect_two_way(world, 'Agahnims Tower', 'Turtle Rock Isolated Ledge Exit', player) - elif tr_target == 'Desert': - connect_two_way(world, 'Desert Palace Entrance (South)', 'Turtle Rock Exit (Front)', player) - connect_two_way(world, 'Desert Palace Entrance (North)', 'Turtle Rock Ledge Exit (East)', player) - connect_two_way(world, 'Desert Palace Entrance (West)', 'Turtle Rock Ledge Exit (West)', player) - connect_two_way(world, 'Desert Palace Entrance (East)', 'Turtle Rock Isolated Ledge Exit', player) - elif tr_target == 'Turtle Rock': - connect_two_way(world, 'Turtle Rock', 'Turtle Rock Exit (Front)', player) - connect_two_way(world, 'Turtle Rock Isolated Ledge Entrance', 'Turtle Rock Isolated Ledge Exit', player) - connect_two_way(world, 'Dark Death Mountain Ledge (West)', 'Turtle Rock Ledge Exit (West)', player) - connect_two_way(world, 'Dark Death Mountain Ledge (East)', 'Turtle Rock Ledge Exit (East)', player) - else: - if hc_target == 'Hyrule Castle': - connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) - connect_two_way(world, 'Hyrule Castle Entrance (East)', 'Hyrule Castle Exit (East)', player) - connect_two_way(world, 'Hyrule Castle Entrance (West)', 'Hyrule Castle Exit (West)', player) - connect_two_way(world, 'Agahnims Tower', 'Ganons Tower Exit', player) - elif hc_target == 'Desert': - connect_two_way(world, 'Desert Palace Entrance (South)', 'Hyrule Castle Exit (South)', player) - connect_two_way(world, 'Desert Palace Entrance (East)', 'Hyrule Castle Exit (East)', player) - connect_two_way(world, 'Desert Palace Entrance (West)', 'Hyrule Castle Exit (West)', player) - connect_two_way(world, 'Desert Palace Entrance (North)', 'Ganons Tower Exit', player) - elif hc_target == 'Turtle Rock': - connect_two_way(world, 'Turtle Rock', 'Hyrule Castle Exit (South)', player) - connect_two_way(world, 'Turtle Rock Isolated Ledge Entrance', 'Ganons Tower Exit', player) - connect_two_way(world, 'Dark Death Mountain Ledge (West)', 'Hyrule Castle Exit (West)', player) + connect_two_way(world, 'Dark Death Mountain Ledge (East)', at_door, player) + else: + connect_two_way(world, 'Turtle Rock Isolated Ledge Entrance', at_door, player) connect_two_way(world, 'Dark Death Mountain Ledge (East)', 'Hyrule Castle Exit (East)', player) + + if dp_target == 'Hyrule Castle': + connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Desert Palace Exit (South)', player) + connect_two_way(world, 'Hyrule Castle Entrance (East)', 'Desert Palace Exit (East)', player) + connect_two_way(world, 'Hyrule Castle Entrance (West)', 'Desert Palace Exit (West)', player) + connect_two_way(world, 'Agahnims Tower', 'Desert Palace Exit (North)', player) + elif dp_target == 'Desert Palace': + connect_two_way(world, 'Desert Palace Entrance (South)', 'Desert Palace Exit (South)', player) + connect_two_way(world, 'Desert Palace Entrance (East)', 'Desert Palace Exit (East)', player) + connect_two_way(world, 'Desert Palace Entrance (West)', 'Desert Palace Exit (West)', player) + connect_two_way(world, 'Desert Palace Entrance (North)', 'Desert Palace Exit (North)', player) + elif dp_target == 'Turtle Rock': + connect_two_way(world, 'Turtle Rock', 'Desert Palace Exit (South)', player) + connect_two_way(world, 'Turtle Rock Isolated Ledge Entrance', 'Desert Palace Exit (East)', player) + connect_two_way(world, 'Dark Death Mountain Ledge (West)', 'Desert Palace Exit (West)', player) + connect_two_way(world, 'Dark Death Mountain Ledge (East)', 'Desert Palace Exit (North)', player) + + if tr_target == 'Hyrule Castle': + connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Turtle Rock Exit (Front)', player) + connect_two_way(world, 'Hyrule Castle Entrance (East)', 'Turtle Rock Ledge Exit (East)', player) + connect_two_way(world, 'Hyrule Castle Entrance (West)', 'Turtle Rock Ledge Exit (West)', player) + connect_two_way(world, 'Agahnims Tower', 'Turtle Rock Isolated Ledge Exit', player) + elif tr_target == 'Desert Palace': + connect_two_way(world, 'Desert Palace Entrance (South)', 'Turtle Rock Exit (Front)', player) + connect_two_way(world, 'Desert Palace Entrance (North)', 'Turtle Rock Ledge Exit (East)', player) + connect_two_way(world, 'Desert Palace Entrance (West)', 'Turtle Rock Ledge Exit (West)', player) + connect_two_way(world, 'Desert Palace Entrance (East)', 'Turtle Rock Isolated Ledge Exit', player) + elif tr_target == 'Turtle Rock': + connect_two_way(world, 'Turtle Rock', 'Turtle Rock Exit (Front)', player) + connect_two_way(world, 'Turtle Rock Isolated Ledge Entrance', 'Turtle Rock Isolated Ledge Exit', player) + connect_two_way(world, 'Dark Death Mountain Ledge (West)', 'Turtle Rock Ledge Exit (West)', player) + connect_two_way(world, 'Dark Death Mountain Ledge (East)', 'Turtle Rock Ledge Exit (East)', player) - if dp_target == 'Hyrule Castle': - connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Desert Palace Exit (South)', player) - connect_two_way(world, 'Hyrule Castle Entrance (East)', 'Desert Palace Exit (East)', player) - connect_two_way(world, 'Hyrule Castle Entrance (West)', 'Desert Palace Exit (West)', player) - connect_two_way(world, 'Agahnims Tower', 'Desert Palace Exit (North)', player) - elif dp_target == 'Desert': - connect_two_way(world, 'Desert Palace Entrance (South)', 'Desert Palace Exit (South)', player) - connect_two_way(world, 'Desert Palace Entrance (East)', 'Desert Palace Exit (East)', player) - connect_two_way(world, 'Desert Palace Entrance (West)', 'Desert Palace Exit (West)', player) - connect_two_way(world, 'Desert Palace Entrance (North)', 'Desert Palace Exit (North)', player) - elif dp_target == 'Turtle Rock': - connect_two_way(world, 'Turtle Rock', 'Desert Palace Exit (South)', player) - connect_two_way(world, 'Turtle Rock Isolated Ledge Entrance', 'Desert Palace Exit (East)', player) - connect_two_way(world, 'Dark Death Mountain Ledge (West)', 'Desert Palace Exit (West)', player) - connect_two_way(world, 'Dark Death Mountain Ledge (East)', 'Desert Palace Exit (North)', player) - - if tr_target == 'Hyrule Castle': - connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Turtle Rock Exit (Front)', player) - connect_two_way(world, 'Hyrule Castle Entrance (East)', 'Turtle Rock Ledge Exit (East)', player) - connect_two_way(world, 'Hyrule Castle Entrance (West)', 'Turtle Rock Ledge Exit (West)', player) - connect_two_way(world, 'Agahnims Tower', 'Turtle Rock Isolated Ledge Exit', player) - elif tr_target == 'Desert': - connect_two_way(world, 'Desert Palace Entrance (South)', 'Turtle Rock Exit (Front)', player) - connect_two_way(world, 'Desert Palace Entrance (North)', 'Turtle Rock Ledge Exit (East)', player) - connect_two_way(world, 'Desert Palace Entrance (West)', 'Turtle Rock Ledge Exit (West)', player) - connect_two_way(world, 'Desert Palace Entrance (East)', 'Turtle Rock Isolated Ledge Exit', player) - elif tr_target == 'Turtle Rock': - connect_two_way(world, 'Turtle Rock', 'Turtle Rock Exit (Front)', player) - connect_two_way(world, 'Turtle Rock Isolated Ledge Entrance', 'Turtle Rock Isolated Ledge Exit', player) - connect_two_way(world, 'Dark Death Mountain Ledge (West)', 'Turtle Rock Ledge Exit (West)', player) - connect_two_way(world, 'Dark Death Mountain Ledge (East)', 'Turtle Rock Ledge Exit (East)', player) def full_shuffle_dungeons(world, Dungeon_Exits, player): invFlag = world.mode[player] == 'inverted' @@ -1357,82 +1237,315 @@ def full_shuffle_dungeons(world, Dungeon_Exits, player): skull_woods_shuffle(world, player) dungeon_exits = list(Dungeon_Exits) - lw_entrances = list(LW_Dungeon_Entrances) if not invFlag else list(Inverted_LW_Dungeon_Entrances_Must_Exit) - dw_entrances = list(DW_Dungeon_Entrances) if not invFlag else list(Inverted_DW_Dungeon_Entrances) - if world.mode[player] != 'standard': - lw_entrances.append('Hyrule Castle Entrance (South)') + if world.mode[player] == 'standard': + # must connect front of hyrule castle to do escape + connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) - if not invFlag: - if world.mode[player] == 'standard': - # must connect front of hyrule castle to do escape - connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) - elif world.doorShuffle[player] == 'vanilla': - dungeon_exits.append(('Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')) - else: - lw_dungeon_entrances_must_exit = list(Inverted_LW_Dungeon_Entrances_Must_Exit) - # randomize which desert ledge door is a must-exit - if random.randint(0, 1) == 0: - lw_dungeon_entrances_must_exit.append('Desert Palace Entrance (North)') - lw_entrances.append('Desert Palace Entrance (West)') - else: - lw_dungeon_entrances_must_exit.append('Desert Palace Entrance (West)') - lw_entrances.append('Desert Palace Entrance (North)') - dungeon_exits.append(('Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')) - if not world.shuffle_ganon: connect_two_way(world, 'Ganons Tower' if not invFlag else 'Agahnims Tower', 'Ganons Tower Exit', player) - hc_ledge_entrances = ['Hyrule Castle Entrance (West)', 'Hyrule Castle Entrance (East)'] else: - if not invFlag: - dw_entrances.append('Ganons Tower') - else: - lw_entrances.append('Agahnims Tower') dungeon_exits.append('Ganons Tower Exit') - hc_ledge_entrances = ['Hyrule Castle Entrance (West)', 'Hyrule Castle Entrance (East)', 'Agahnims Tower'] - if not invFlag: - if world.mode[player] == 'standard': - # rest of hyrule castle must be in light world, so it has to be the one connected to east exit of desert - hyrule_castle_exits = [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')] - connect_mandatory_exits(world, lw_entrances, hyrule_castle_exits, list(LW_Dungeon_Entrances_Must_Exit), player) - connect_caves(world, lw_entrances, [], hyrule_castle_exits, player) - elif world.doorShuffle[player] != 'vanilla': - # sanc is in light world, so must all of HC if door shuffle is on - connect_mandatory_exits(world, lw_entrances, - [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)', 'Hyrule Castle Exit (South)')], - list(LW_Dungeon_Entrances_Must_Exit), player) + # determine LW and DW entrances + # owid: (entrances, is_light_world) + dungeon_owid_map = {0x03: ({'Tower of Hera'}, True), + 0x1e: ({'Eastern Palace'}, True), + 0x1b: ({'Hyrule Castle Entrance (South)', + 'Hyrule Castle Entrance (West)', + 'Hyrule Castle Entrance (East)', + 'Agahnims Tower'}, True), + 0x30: ({'Desert Palace Entrance (South)', + 'Desert Palace Entrance (West)', + 'Desert Palace Entrance (East)', + 'Desert Palace Entrance (North)'}, True), + 0x40: ({'Skull Woods Final Section'}, False), + 0x43: ({'Ganons Tower'}, False), + 0x45: ({'Dark Death Mountain Ledge (West)', + 'Dark Death Mountain Ledge (East)', + 'Turtle Rock Isolated Ledge Entrance'}, False), + 0x47: ({'Turtle Rock'}, False), + 0x58: ({'Thieves Town'}, False), + 0x5e: ({'Palace of Darkness'}, False), + 0x70: ({'Misery Mire'}, False), + 0x75: ({'Ice Palace'}, False), + 0x7b: ({'Swamp Palace'}, False) + } + + lw_entrances = list() + dw_entrances = list() + for owid in dungeon_owid_map.keys(): + if dungeon_owid_map[owid][1] == (invFlag == (owid in world.owswaps[player][0] and world.owMixed[player])): + lw_entrances.extend([e for e in dungeon_owid_map[owid][0] if e in entrance_pool]) else: - connect_mandatory_exits(world, lw_entrances, dungeon_exits, list(LW_Dungeon_Entrances_Must_Exit), player) - connect_mandatory_exits(world, dw_entrances, dungeon_exits, list(DW_Dungeon_Entrances_Must_Exit), player) - else: - # shuffle aga door first. If it's on HC ledge, remaining HC ledge door must be must-exit - all_entrances_aga = lw_entrances + dw_entrances - aga_doors = [i for i in all_entrances_aga] - random.shuffle(aga_doors) - aga_door = aga_doors.pop() - - if aga_door in hc_ledge_entrances: - lw_entrances.remove(aga_door) - hc_ledge_entrances.remove(aga_door) - - random.shuffle(hc_ledge_entrances) - hc_ledge_must_exit = hc_ledge_entrances.pop() - lw_entrances.remove(hc_ledge_must_exit) - lw_dungeon_entrances_must_exit.append(hc_ledge_must_exit) - - if aga_door in lw_entrances: - lw_entrances.remove(aga_door) - elif aga_door in dw_entrances: - dw_entrances.remove(aga_door) - - connect_two_way(world, aga_door, 'Agahnims Tower Exit', player) - dungeon_exits.remove('Agahnims Tower Exit') - - connect_mandatory_exits(world, lw_entrances, dungeon_exits, lw_dungeon_entrances_must_exit, player) + dw_entrances.extend([e for e in dungeon_owid_map[owid][0] if e in entrance_pool]) + # determine must-exit entrances + from DoorShuffle import find_inaccessible_regions + find_inaccessible_regions(world, player) + + lw_must_exit = list() + dw_must_exit = list() + lw_related = list() + dw_related = list() + if invFlag == (0x45 in world.owswaps[player][0] and world.owMixed[player]): + dw_entrances.remove('Turtle Rock Isolated Ledge Entrance') + dw_must_exit.append('Turtle Rock Isolated Ledge Entrance') + if 'Dark Death Mountain Ledge' in world.inaccessible_regions[player]: + ledge = ['Dark Death Mountain Ledge (West)', 'Dark Death Mountain Ledge (East)'] + dw_entrances = [e for e in dw_entrances if e not in ledge] + random.shuffle(ledge) + dw_must_exit.append(ledge.pop()) + dw_related.extend(ledge) + if invFlag == (0x30 in world.owswaps[player][0] and world.owMixed[player]): + if 'Desert Palace Mouth' in world.inaccessible_regions[player]: + lw_entrances.remove('Desert Palace Entrance (East)') + lw_must_exit.append('Desert Palace Entrance (East)') + else: + dw_entrances.remove('Desert Palace Entrance (East)') + dw_must_exit.append('Desert Palace Entrance (East)') + if 'Desert Ledge' in world.inaccessible_regions[player]: + ledge = ['Desert Palace Entrance (West)', 'Desert Palace Entrance (North)'] + dw_entrances = [e for e in dw_entrances if e not in ledge] + random.shuffle(ledge) + dw_must_exit.append(ledge.pop()) + dw_related.extend(ledge) + if invFlag == (0x1b in world.owswaps[player][0] and world.owMixed[player]): + if 'Hyrule Castle Ledge' in world.inaccessible_regions[player]: + ledge = ['Hyrule Castle Entrance (West)', 'Hyrule Castle Entrance (East)', 'Agahnims Tower'] + lw_entrances = [e for e in lw_entrances if e not in ledge] + random.shuffle(ledge) + lw_must_exit.append(ledge.pop()) + lw_related.extend(ledge) + random.shuffle(lw_must_exit) + random.shuffle(dw_must_exit) + + # place HC first, needs to be same world as Sanc drop + hyrule_castle_exits = ('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)', 'Hyrule Castle Exit (South)') + hyrule_castle_exits = list([tuple(e for e in hyrule_castle_exits if e in exit_pool)]) + hyrule_castle_exits.extend([e for e in dungeon_exits if isinstance(e, str)]) + dungeon_exits = [e for e in dungeon_exits if not isinstance(e, str)] + if invFlag == (0x13 in world.owswaps[player][0] and world.owMixed[player]): + connect_mandatory_exits(world, lw_entrances, hyrule_castle_exits, lw_must_exit, player, False) + dungeon_exits.extend([e for e in hyrule_castle_exits if isinstance(e, str)]) + hyrule_castle_exits = [e for e in hyrule_castle_exits if not isinstance(e, str)] + connect_caves(world, lw_entrances, [], hyrule_castle_exits, player) + else: + connect_mandatory_exits(world, dw_entrances, hyrule_castle_exits, dw_must_exit, player, False) + dungeon_exits.extend([e for e in hyrule_castle_exits if isinstance(e, str)]) + hyrule_castle_exits = [e for e in hyrule_castle_exits if not isinstance(e, str)] + connect_caves(world, [], dw_entrances, hyrule_castle_exits, player) + + # connect any remaining must-exit entrances + dungeon_exits.extend(hyrule_castle_exits) + connect_mandatory_exits(world, lw_entrances, dungeon_exits, lw_must_exit, player) + connect_mandatory_exits(world, dw_entrances, dungeon_exits, dw_must_exit, player) + + # shuffle the remaining entrances + lw_entrances = lw_entrances + lw_related + dw_entrances = dw_entrances + dw_related connect_caves(world, lw_entrances, dw_entrances, dungeon_exits, player) + +def place_links_house(world, sectors, player, ignore_list=[]): + invFlag = world.mode[player] == 'inverted' + if world.mode[player] == 'standard' or not world.shufflelinks[player]: + links_house = 'Links House' if not invFlag else 'Big Bomb Shop' + else: + if invFlag: + for dark_sanc in world.get_entrance('Dark Sanctuary Hint Exit', player).connected_region.exits: + if dark_sanc.connected_region and dark_sanc.connected_region.name == 'Dark Sanctuary Hint': + dark_sanc = dark_sanc.name + break + + if invFlag and isinstance(dark_sanc, str): + links_house_doors = [i for i in get_distant_entrances(world, dark_sanc, sectors, player) if i in entrance_pool] + else: + links_house_doors = [i for i in get_starting_entrances(world, sectors, player, world.shuffle[player] != 'insanity') if i in entrance_pool] + if world.shuffle[player] in ['lite', 'lean']: + links_house_doors = [e for e in links_house_doors if e in list(zip(*(default_item_connections + (default_shop_connections if world.shopsanity[player] else []))))[0]] + + #TODO: Need to improve Links House placement to choose a better sector or eliminate entrances that are after ledge drops + links_house_doors = [e for e in links_house_doors if e not in ignore_list] + assert len(links_house_doors), 'No valid candidates to place Links House' + links_house = random.choice(links_house_doors) + connect_two_way(world, links_house, 'Links House Exit', player) + return links_house + + +def place_dark_sanc(world, sectors, player, ignore_list=[]): + if not world.shufflelinks[player]: + sanc_doors = [i for i in get_distant_entrances(world, 'Big Bomb Shop', sectors, player) if i in entrance_pool] + else: + sanc_doors = [i for i in get_starting_entrances(world, sectors, player, world.shuffle[player] != 'insanity') if i in entrance_pool] + if world.shuffle[player] in ['lite', 'lean']: + sanc_doors = [e for e in sanc_doors if e in list(zip(*(default_item_connections + (default_shop_connections if world.shopsanity[player] else []))))[0]] + + sanc_doors = [e for e in sanc_doors if e not in ignore_list] + assert len(sanc_doors), 'No valid candidates to place Dark Chapel' + sanc_door = random.choice(sanc_doors) + connect_entrance(world, sanc_door, 'Dark Sanctuary Hint', player) + world.get_entrance('Dark Sanctuary Hint Exit', player).connect(world.get_entrance(sanc_door, player).parent_region) + return sanc_door + + +def place_blacksmith(world, links_house, player): + invFlag = world.mode[player] == 'inverted' + + assumed_inventory = list() + region = world.get_region('Frog Prison', player) + if world.logic[player] in ['noglitches', 'minorglitches'] and region.type == (RegionType.DarkWorld if not invFlag else RegionType.LightWorld): + assumed_inventory.append('Titans Mitts') + + links_region = world.get_entrance(links_house, player).parent_region.name + blacksmith_doors = list(build_accessible_entrance_list(world, links_region, player, assumed_inventory, False, True, True)) + + if invFlag: + dark_sanc = world.get_entrance('Dark Sanctuary Hint Exit', player).connected_region.name + blacksmith_doors = list(set(blacksmith_doors + list(build_accessible_entrance_list(world, dark_sanc, player, assumed_inventory, False, True, True)))) + elif world.doorShuffle[player] == 'vanilla' or world.intensity[player] < 3: + sanc_region = world.get_entrance('Sanctuary Exit', player).connected_region.name + blacksmith_doors = list(set(blacksmith_doors + list(build_accessible_entrance_list(world, sanc_region, player, assumed_inventory, False, True, True)))) + if world.shuffle[player] in ['lite', 'lean']: + blacksmith_doors = [e for e in blacksmith_doors if e in list(zip(*(default_item_connections + (default_shop_connections if world.shopsanity[player] else []))))[0]] + + assert len(blacksmith_doors), 'No valid candidates to place Blacksmiths Hut' + blacksmith_hut = random.choice(blacksmith_doors) + connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player) + return blacksmith_hut + + +def place_old_man(world, pool, player, ignore_list=[]): + # exit has to come from specific set of doors, the entrance is free to move about + if (world.mode[player] == 'inverted') == (0x03 in world.owswaps[player][0] and world.owMixed[player]): + region_name = 'West Death Mountain (Top)' + else: + region_name = 'West Dark Death Mountain (Top)' + old_man_entrances = list(build_accessible_entrance_list(world, region_name, player, [], False, True, True, True)) + old_man_entrances = [e for e in old_man_entrances if e != 'Old Man House (Bottom)' and e not in ignore_list] + if world.shuffle[player] in ['lite', 'lean']: + old_man_entrances = [e for e in old_man_entrances if e in pool] + random.shuffle(old_man_entrances) + old_man_exit = None + while not old_man_exit: + old_man_exit = old_man_entrances.pop() + if 'West Death Mountain (Bottom)' not in build_accessible_region_list(world, world.get_entrance(old_man_exit, player).parent_region.name, player, True, True): + old_man_exit = None + + old_man_entrances = [e for e in pool if e in entrance_pool and e not in ignore_list and e not in entrance_exits + [old_man_exit]] + random.shuffle(old_man_entrances) + old_man_entrance = old_man_entrances.pop() + if world.shuffle[player] != 'insanity': + connect_two_way(world, old_man_exit, 'Old Man Cave Exit (East)', player) + connect_two_way(world, old_man_entrance, 'Old Man Cave Exit (West)', player) + else: + # skip assigning connections to West Entrance/Exit + connect_exit(world, 'Old Man Cave Exit (East)', old_man_exit, player, False) + connect_entrance(world, old_man_entrance, 'Old Man Cave Exit (East)', player, False) + + +def junk_fill_inaccessible(world, player): + from Main import copy_world + from DoorShuffle import find_inaccessible_regions + find_inaccessible_regions(world, player) + + for player in range(1, world.players + 1): + world.key_logic[player] = {} + base_world = copy_world(world) + base_world.override_bomb_check = True + world.key_logic = {} + + # remove regions that have a dungeon entrance + accessible_regions = list() + for region_name in world.inaccessible_regions[player]: + region = world.get_region(region_name, player) + for exit in region.exits: + if exit.connected_region and exit.connected_region.type == RegionType.Dungeon: + accessible_regions.append(region_name) + break + for region_name in accessible_regions.copy(): + accessible_regions = list(set(accessible_regions + list(build_accessible_region_list(base_world, region_name, player, False, True, False, False)))) + world.inaccessible_regions[player] = [r for r in world.inaccessible_regions[player] if r not in accessible_regions] + + # get inaccessible entrances + inaccessible_entrances = list() + for region_name in world.inaccessible_regions[player]: + region = world.get_region(region_name, player) + if region.type in [RegionType.LightWorld, RegionType.DarkWorld]: + for exit in region.exits: + if not exit.connected_region and exit.name in entrance_pool: + inaccessible_entrances.append(exit.name) + + junk_locations = [e for e in list(zip(*default_connections))[1] if e in exit_pool] + random.shuffle(junk_locations) + for entrance in inaccessible_entrances: + connect_entrance(world, entrance, junk_locations.pop(), player) + + +def connect_inaccessible_regions(world, lw_entrances, dw_entrances, caves, player, ignore_list=[]): + invFlag = world.mode[player] == 'inverted' + + random.shuffle(lw_entrances) + random.shuffle(dw_entrances) + + from DoorShuffle import find_inaccessible_regions + find_inaccessible_regions(world, player) + + # remove regions that have a dungeon entrance + accessible_regions = list() + for region_name in world.inaccessible_regions[player]: + region = world.get_region(region_name, player) + for exit in region.exits: + if exit.connected_region and exit.connected_region.type == RegionType.Dungeon: + accessible_regions.append(region_name) + break + for region_name in accessible_regions.copy(): + accessible_regions = list(set(accessible_regions + list(build_accessible_region_list(world, region_name, player, True, True, False, False)))) + world.inaccessible_regions[player] = [r for r in world.inaccessible_regions[player] if r not in accessible_regions] + + # split inaccessible into 2 lists for each world + inaccessible_regions = list(world.inaccessible_regions[player]) + must_exit_regions = list() + otherworld_must_exit_regions = list() + for region_name in inaccessible_regions.copy(): + region = world.get_region(region_name, player) + if region.type not in [RegionType.LightWorld, RegionType.DarkWorld] or not any((not exit.connected_region and exit.spot_type == 'Entrance') for exit in region.exits) \ + or (region_name == 'Pyramid Exit Ledge' and world.shuffle[player] != 'insanity' or invFlag != (0x1b in world.owswaps[player][0] and world.owMixed[player])): + inaccessible_regions.remove(region_name) + elif region.type == (RegionType.LightWorld if not invFlag else RegionType.DarkWorld): + must_exit_regions.append(region_name) + elif region.type == (RegionType.DarkWorld if not invFlag else RegionType.LightWorld): + otherworld_must_exit_regions.append(region_name) + + def connect_one(region_name, pool): + inaccessible_entrances = list() + region = world.get_region(region_name, player) + for exit in region.exits: + if not exit.connected_region and exit.name in [e for e in entrance_pool if e not in ignore_list] and (world.shuffle[player] not in ['lite', 'lean'] or exit.name in pool): + inaccessible_entrances.append(exit.name) + if len(inaccessible_entrances): + random.shuffle(inaccessible_entrances) + connect_mandatory_exits(world, pool, caves, [inaccessible_entrances.pop()], player) + connect_inaccessible_regions(world, lw_entrances, dw_entrances, caves, player, ignore_list) + + # connect one connector at a time to ensure multiple connectors aren't assigned to the same inaccessible set of regions + if world.shuffle[player] in ['lean', 'crossed', 'insanity']: + combined_must_exit_regions = list(must_exit_regions + otherworld_must_exit_regions) + if len(combined_must_exit_regions) > 0: + random.shuffle(combined_must_exit_regions) + connect_one(combined_must_exit_regions[0], [e for e in lw_entrances if e in entrance_pool]) + else: + pool = [e for e in dw_entrances if e in entrance_pool] + if len(otherworld_must_exit_regions) > 0 and len(pool): + random.shuffle(otherworld_must_exit_regions) + connect_one(otherworld_must_exit_regions[0], pool) + elif len(must_exit_regions) > 0: + pool = [e for e in lw_entrances if e in entrance_pool] + if len(pool): + random.shuffle(must_exit_regions) + connect_one(must_exit_regions[0], pool) + + def unbias_some_entrances(Dungeon_Exits, Cave_Exits, Old_Man_House, Cave_Three_Exits): def shuffle_lists_in_list(ls): for i, item in enumerate(ls): @@ -1473,673 +1586,284 @@ def unbias_some_entrances(Dungeon_Exits, Cave_Exits, Old_Man_House, Cave_Three_E tuplize_lists_in_list(Cave_Three_Exits) -LW_Dungeon_Entrances = ['Desert Palace Entrance (South)', - 'Desert Palace Entrance (West)', - 'Desert Palace Entrance (North)', - 'Eastern Palace', - 'Tower of Hera', - 'Hyrule Castle Entrance (West)', - 'Hyrule Castle Entrance (East)', - 'Agahnims Tower'] +def unbias_dungeons(Dungeon_Exits): + def shuffle_lists_in_list(ls): + for i, item in enumerate(ls): + if isinstance(item, list): + ls[i] = random.sample(item, len(item)) -LW_Dungeon_Entrances_Must_Exit = ['Desert Palace Entrance (East)'] + def tuplize_lists_in_list(ls): + for i, item in enumerate(ls): + if isinstance(item, list): + ls[i] = tuple(item) -DW_Dungeon_Entrances = ['Thieves Town', - 'Skull Woods Final Section', - 'Ice Palace', - 'Misery Mire', - 'Palace of Darkness', - 'Swamp Palace', - 'Turtle Rock', - 'Dark Death Mountain Ledge (West)'] + shuffle_lists_in_list(Dungeon_Exits) -DW_Dungeon_Entrances_Must_Exit = ['Dark Death Mountain Ledge (East)', - 'Turtle Rock Isolated Ledge Entrance'] + # TR fixup + for i, item in enumerate(Dungeon_Exits[-1]): + if 'Turtle Rock Ledge Exit (East)' == item: + if 0 != i: + Dungeon_Exits[-1][i] = Dungeon_Exits[-1][0] + Dungeon_Exits[-1][0] = 'Turtle Rock Ledge Exit (East)' + break -Dungeon_Exits_Base = [['Desert Palace Exit (South)', 'Desert Palace Exit (West)', 'Desert Palace Exit (East)'], - 'Desert Palace Exit (North)', - 'Eastern Palace Exit', - 'Tower of Hera Exit', - 'Thieves Town Exit', - 'Skull Woods Final Section Exit', - 'Ice Palace Exit', - 'Misery Mire Exit', - 'Palace of Darkness Exit', - 'Swamp Palace Exit', - 'Agahnims Tower Exit', - ['Turtle Rock Ledge Exit (East)', - 'Turtle Rock Exit (Front)', 'Turtle Rock Ledge Exit (West)', 'Turtle Rock Isolated Ledge Exit']] - -DW_Entrances_Must_Exit = ['Bumper Cave (Top)', 'Hookshot Cave Back Entrance'] - -Two_Door_Caves_Directional = [('Bumper Cave (Bottom)', 'Bumper Cave (Top)'), - ('Hookshot Cave', 'Hookshot Cave Back Entrance')] - -Two_Door_Caves = [('Elder House (East)', 'Elder House (West)'), - ('Two Brothers House (East)', 'Two Brothers House (West)'), - ('Superbunny Cave (Bottom)', 'Superbunny Cave (Top)')] - -Old_Man_Entrances = ['Old Man Cave (East)', - 'Old Man House (Top)', - 'Death Mountain Return Cave (East)', - 'Spectacle Rock Cave', - 'Spectacle Rock Cave Peak', - 'Spectacle Rock Cave (Bottom)'] - -Old_Man_House_Base = [['Old Man House Exit (Bottom)', 'Old Man House Exit (Top)']] - -Cave_Exits_Base = [['Elder House Exit (East)', 'Elder House Exit (West)'], - ['Two Brothers House Exit (East)', 'Two Brothers House Exit (West)'], - ['Death Mountain Return Cave Exit (West)', 'Death Mountain Return Cave Exit (East)'], - ['Fairy Ascension Cave Exit (Bottom)', 'Fairy Ascension Cave Exit (Top)'], - ['Bumper Cave Exit (Top)', 'Bumper Cave Exit (Bottom)'], - ['Hookshot Cave Back Exit', 'Hookshot Cave Front Exit'], - ['Superbunny Cave Exit (Bottom)', 'Superbunny Cave Exit (Top)'], - ['Spiral Cave Exit (Top)', 'Spiral Cave Exit']] + tuplize_lists_in_list(Dungeon_Exits) -Cave_Three_Exits_Base = [['Spectacle Rock Cave Exit (Peak)', 'Spectacle Rock Cave Exit (Top)', - 'Spectacle Rock Cave Exit'], - ['Paradox Cave Exit (Top)', 'Paradox Cave Exit (Middle)','Paradox Cave Exit (Bottom)']] +def build_sectors(world, player): + from Main import copy_world + from OWEdges import OWTileRegions + + # perform accessibility check on duplicate world + for player in range(1, world.players + 1): + world.key_logic[player] = {} + base_world = copy_world(world) + world.key_logic = {} + + # build lists of contiguous regions accessible with full inventory (excl portals/mirror/flute/entrances) + regions = list(OWTileRegions.copy().keys()) + sectors = list() + while(len(regions) > 0): + explored_regions = build_accessible_region_list(base_world, regions[0], player, False, False, False, False) + regions = [r for r in regions if r not in explored_regions] + unique_regions = [_ for i in range(len(sectors)) for _ in sectors[i]] + if (any(r in unique_regions for r in explored_regions)): + for s in range(len(sectors)): + if (any(r in sectors[s] for r in explored_regions)): + sectors[s] = set(list(sectors[s]) + list(explored_regions)) + break + else: + sectors.append(explored_regions) + + # remove water regions if Flippers not in starting inventory + if not any(map(lambda i: i.name == 'Flippers', world.precollected_items)): + for s in range(len(sectors)): + terrains = list() + for regionname in sectors[s]: + region = world.get_region(regionname, player) + if region.terrain == Terrain.Land: + terrains.append(regionname) + sectors[s] = terrains + + # within each group, split into contiguous regions accessible only with starting inventory + for s in range(len(sectors)): + regions = list(sectors[s]).copy() + sectors2 = list() + while(len(regions) > 0): + explored_regions = build_accessible_region_list(base_world, regions[0], player, False, False, True, False) + regions = [r for r in regions if r not in explored_regions] + unique_regions = [_ for i in range(len(sectors2)) for _ in sectors2[i]] + if (any(r in unique_regions for r in explored_regions)): + for s2 in range(len(sectors2)): + if (any(r in sectors2[s2] for r in explored_regions)): + sectors2[s2] = set(list(sectors2[s2]) + list(explored_regions)) + break + else: + sectors2.append(explored_regions) + sectors[s] = sectors2 + + return sectors -LW_Entrances = ['Elder House (East)', - 'Elder House (West)', - 'Two Brothers House (East)', - 'Two Brothers House (West)', - 'Old Man Cave (West)', - 'Old Man House (Bottom)', - 'Death Mountain Return Cave (West)', - 'Paradox Cave (Bottom)', - 'Paradox Cave (Middle)', - 'Paradox Cave (Top)', - 'Fairy Ascension Cave (Bottom)', - 'Fairy Ascension Cave (Top)', - 'Spiral Cave', - 'Spiral Cave (Bottom)'] +def build_accessible_region_list(world, start_region, player, build_copy_world=False, cross_world=False, region_rules=True, ignore_ledges = False): + from Main import copy_world + from Items import ItemFactory + + def explore_region(region_name, region=None): + explored_regions.add(region_name) + if not region: + region = base_world.get_region(region_name, player) + for exit in region.exits: + if exit.connected_region is not None: + if any(map(lambda i: i.name == 'Ocarina', base_world.precollected_items)) and exit.spot_type == 'Flute': + fluteregion = exit.connected_region + for flutespot in fluteregion.exits: + if flutespot.connected_region and flutespot.connected_region.name not in explored_regions: + explore_region(flutespot.connected_region.name, flutespot.connected_region) + elif exit.connected_region.name not in explored_regions \ + and (exit.connected_region.type == region.type or (cross_world and exit.connected_region.type in [RegionType.LightWorld, RegionType.DarkWorld])) \ + and (not region_rules or exit.access_rule(blank_state)) and (not ignore_ledges or exit.spot_type != 'Ledge'): + explore_region(exit.connected_region.name, exit.connected_region) + + if build_copy_world: + for player in range(1, world.players + 1): + world.key_logic[player] = {} + base_world = copy_world(world) + base_world.override_bomb_check = True + world.key_logic = {} + else: + base_world = world + + connect_simple(base_world, 'Links House S&Q', start_region, player) + blank_state = CollectionState(base_world) + if base_world.mode[player] == 'standard': + blank_state.collect(ItemFactory('Zelda Delivered', player), True) + explored_regions = set() + explore_region(start_region) -DW_Entrances = ['Bumper Cave (Bottom)', - 'Superbunny Cave (Top)', - 'Superbunny Cave (Bottom)', - 'Hookshot Cave'] + return explored_regions + -Bomb_Shop_Multi_Cave_Doors = ['Hyrule Castle Entrance (South)', - 'Misery Mire', - 'Thieves Town', - 'Bumper Cave (Bottom)', - 'Swamp Palace', - 'Hyrule Castle Secret Entrance Stairs', - 'Skull Woods First Section Door', - 'Skull Woods Second Section Door (East)', - 'Skull Woods Second Section Door (West)', - 'Skull Woods Final Section', - 'Ice Palace', - 'Turtle Rock', - 'Dark Death Mountain Ledge (West)', - 'Dark Death Mountain Ledge (East)', - 'Superbunny Cave (Top)', - 'Superbunny Cave (Bottom)', - 'Hookshot Cave', - 'Ganons Tower', - 'Desert Palace Entrance (South)', - 'Tower of Hera', - 'Two Brothers House (West)', - 'Old Man Cave (East)', - 'Old Man House (Bottom)', - 'Old Man House (Top)', - 'Death Mountain Return Cave (East)', - 'Death Mountain Return Cave (West)', - 'Spectacle Rock Cave Peak', - 'Spectacle Rock Cave', - 'Spectacle Rock Cave (Bottom)', - 'Paradox Cave (Bottom)', - 'Paradox Cave (Middle)', - 'Paradox Cave (Top)', - 'Fairy Ascension Cave (Bottom)', - 'Fairy Ascension Cave (Top)', - 'Spiral Cave', - 'Spiral Cave (Bottom)', - 'Palace of Darkness', - 'Hyrule Castle Entrance (West)', - 'Hyrule Castle Entrance (East)', - 'Agahnims Tower', - 'Desert Palace Entrance (West)', - 'Desert Palace Entrance (North)' - # all entrances below this line would be possible for blacksmith_hut - # if it were not for dwarf checking multi-entrance caves - ] +def build_accessible_entrance_list(world, start_region, player, assumed_inventory=[], cross_world=False, region_rules=True, exit_rules=True, include_one_ways=False): + from Main import copy_world + from Items import ItemFactory + + for player in range(1, world.players + 1): + world.key_logic[player] = {} + base_world = copy_world(world) + base_world.override_bomb_check = True + world.key_logic = {} + + connect_simple(base_world, 'Links House S&Q', start_region, player) + blank_state = CollectionState(base_world) + if base_world.mode[player] == 'standard': + blank_state.collect(ItemFactory('Zelda Delivered', player), True) + for item in assumed_inventory: + blank_state.collect(ItemFactory(item, player), True) -Blacksmith_Multi_Cave_Doors = ['Eastern Palace', - 'Elder House (East)', - 'Elder House (West)', - 'Two Brothers House (East)', - 'Old Man Cave (West)', - 'Sanctuary', - 'Lumberjack Tree Cave', - 'Lost Woods Hideout Stump', - 'North Fairy Cave', - 'Bat Cave Cave', - 'Kakariko Well Cave'] + explored_regions = list(build_accessible_region_list(base_world, start_region, player, False, cross_world, region_rules, False)) -LW_Single_Cave_Doors = ['Blinds Hideout', - 'Lake Hylia Fairy', - 'Light Hype Fairy', - 'Desert Fairy', - 'Chicken House', - 'Aginahs Cave', - 'Sahasrahlas Hut', - 'Cave Shop (Lake Hylia)', - 'Blacksmiths Hut', - 'Sick Kids House', - 'Lost Woods Gamble', - 'Fortune Teller (Light)', - 'Snitch Lady (East)', - 'Snitch Lady (West)', - 'Bush Covered House', - 'Tavern (Front)', - 'Light World Bomb Hut', - 'Kakariko Shop', - 'Mini Moldorm Cave', - 'Long Fairy Cave', - 'Good Bee Cave', - '20 Rupee Cave', - '50 Rupee Cave', - 'Ice Rod Cave', - 'Library', - 'Potion Shop', - 'Dam', - 'Lumberjack House', - 'Lake Hylia Fortune Teller', - 'Kakariko Gamble Game', - 'Waterfall of Wishing', - 'Capacity Upgrade', - 'Bonk Rock Cave', - 'Graveyard Cave', - 'Checkerboard Cave', - 'Cave 45', - 'Kings Grave', - 'Bonk Fairy (Light)', - 'Hookshot Fairy', - 'Mimic Cave', - 'Links House'] + if include_one_ways: + new_regions = list() + for region_name in explored_regions: + if region_name in one_way_ledges: + for ledge in one_way_ledges[region_name]: + if ledge not in explored_regions + new_regions: + new_regions.append(ledge) + explored_regions.extend(new_regions) + + entrances = set() + for region_name in explored_regions: + region = base_world.get_region(region_name, player) + for exit in region.exits: + if exit.name in entrance_pool and (not exit_rules or exit.access_rule(blank_state)): + entrances.add(exit.name) -Isolated_LH_Doors = ['Kings Grave', - 'Waterfall of Wishing', - 'Desert Palace Entrance (South)', - 'Desert Palace Entrance (North)', - 'Capacity Upgrade', - 'Ice Palace', - 'Skull Woods Final Section', - 'Dark World Hammer Peg Cave', - 'Turtle Rock Isolated Ledge Entrance'] + return entrances + -DW_Single_Cave_Doors = ['Bonk Fairy (Dark)', - 'Dark Sanctuary Hint', - 'Dark Lake Hylia Fairy', - 'C-Shaped House', - 'Big Bomb Shop', - 'Dark Death Mountain Fairy', - 'Dark Lake Hylia Shop', - 'Dark World Shop', - 'Red Shield Shop', - 'Mire Shed', - 'East Dark World Hint', - 'Dark Desert Hint', - 'Spike Cave', - 'Palace of Darkness Hint', - 'Dark Lake Hylia Ledge Spike Cave', - 'Cave Shop (Dark Death Mountain)', - 'Dark World Potion Shop', - 'Pyramid Fairy', - 'Archery Game', - 'Dark World Lumberjack Shop', - 'Hype Cave', - 'Brewery', - 'Dark Lake Hylia Ledge Hint', - 'Chest Game', - 'Dark Desert Fairy', - 'Dark Lake Hylia Ledge Fairy', - 'Fortune Teller (Dark)', - 'Dark World Hammer Peg Cave'] +def get_starting_entrances(world, sectors, player, force_starting_world=True): + invFlag = world.mode[player] == 'inverted' -Blacksmith_Single_Cave_Doors = ['Blinds Hideout', - 'Lake Hylia Fairy', - 'Light Hype Fairy', - 'Desert Fairy', - 'Chicken House', - 'Aginahs Cave', - 'Sahasrahlas Hut', - 'Cave Shop (Lake Hylia)', - 'Blacksmiths Hut', - 'Sick Kids House', - 'Lost Woods Gamble', - 'Fortune Teller (Light)', - 'Snitch Lady (East)', - 'Snitch Lady (West)', - 'Bush Covered House', - 'Tavern (Front)', - 'Light World Bomb Hut', - 'Kakariko Shop', - 'Mini Moldorm Cave', - 'Long Fairy Cave', - 'Good Bee Cave', - '20 Rupee Cave', - '50 Rupee Cave', - 'Ice Rod Cave', - 'Library', - 'Potion Shop', - 'Dam', - 'Lumberjack House', - 'Lake Hylia Fortune Teller', - 'Kakariko Gamble Game'] - -Bomb_Shop_Single_Cave_Doors = ['Waterfall of Wishing', - 'Capacity Upgrade', - 'Bonk Rock Cave', - 'Graveyard Cave', - 'Checkerboard Cave', - 'Cave 45', - 'Kings Grave', - 'Bonk Fairy (Light)', - 'Hookshot Fairy', - 'East Dark World Hint', - 'Palace of Darkness Hint', - 'Dark Lake Hylia Fairy', - 'Dark Lake Hylia Ledge Fairy', - 'Dark Lake Hylia Ledge Spike Cave', - 'Dark Lake Hylia Ledge Hint', - 'Hype Cave', - 'Bonk Fairy (Dark)', - 'Brewery', - 'C-Shaped House', - 'Chest Game', - 'Dark World Hammer Peg Cave', - 'Red Shield Shop', - 'Dark Sanctuary Hint', - 'Fortune Teller (Dark)', - 'Dark World Shop', - 'Dark World Lumberjack Shop', - 'Dark World Potion Shop', - 'Archery Game', - 'Mire Shed', - 'Dark Desert Hint', - 'Dark Desert Fairy', - 'Spike Cave', - 'Cave Shop (Dark Death Mountain)', - 'Dark Death Mountain Fairy', - 'Mimic Cave', - 'Big Bomb Shop', - 'Dark Lake Hylia Shop'] - -Single_Cave_Doors = ['Pyramid Fairy'] - -Single_Cave_Targets = ['Blinds Hideout', - 'Bonk Fairy (Light)', - 'Lake Hylia Healer Fairy', - 'Swamp Healer Fairy', - 'Desert Healer Fairy', - 'Kings Grave', - 'Chicken House', - 'Aginahs Cave', - 'Sahasrahlas Hut', - 'Cave Shop (Lake Hylia)', - 'Sick Kids House', - 'Lost Woods Gamble', - 'Fortune Teller (Light)', - 'Snitch Lady (East)', - 'Snitch Lady (West)', - 'Bush Covered House', - 'Tavern (Front)', - 'Light World Bomb Hut', - 'Kakariko Shop', - 'Cave 45', - 'Graveyard Cave', - 'Checkerboard Cave', - 'Mini Moldorm Cave', - 'Long Fairy Cave', - 'Good Bee Cave', - '20 Rupee Cave', - '50 Rupee Cave', - 'Ice Rod Cave', - 'Bonk Rock Cave', - 'Library', - 'Potion Shop', - 'Hookshot Fairy', - 'Waterfall of Wishing', - 'Capacity Upgrade', - 'Pyramid Fairy', - 'East Dark World Hint', - 'Palace of Darkness Hint', - 'Dark Lake Hylia Healer Fairy', - 'Dark Lake Hylia Ledge Healer Fairy', - 'Dark Lake Hylia Ledge Spike Cave', - 'Dark Lake Hylia Ledge Hint', - 'Hype Cave', - 'Bonk Fairy (Dark)', - 'Brewery', - 'C-Shaped House', - 'Chest Game', - 'Dark World Hammer Peg Cave', - 'Red Shield Shop', - 'Dark Sanctuary Hint', - 'Fortune Teller (Dark)', - 'Village of Outcasts Shop', - 'Dark Lake Hylia Shop', - 'Dark World Lumberjack Shop', - 'Archery Game', - 'Mire Shed', - 'Dark Desert Hint', - 'Dark Desert Healer Fairy', - 'Spike Cave', - 'Cave Shop (Dark Death Mountain)', - 'Dark Death Mountain Healer Fairy', - 'Mimic Cave', - 'Dark World Potion Shop', - 'Lumberjack House', - 'Lake Hylia Fortune Teller', - 'Kakariko Gamble Game', - 'Dam'] - -Inverted_LW_Dungeon_Entrances = ['Desert Palace Entrance (South)', - 'Eastern Palace', - 'Tower of Hera', - 'Hyrule Castle Entrance (West)', - 'Hyrule Castle Entrance (East)'] - -Inverted_DW_Dungeon_Entrances = ['Thieves Town', - 'Skull Woods Final Section', - 'Ice Palace', - 'Misery Mire', - 'Palace of Darkness', - 'Swamp Palace', - 'Turtle Rock', - 'Dark Death Mountain Ledge (West)', - 'Dark Death Mountain Ledge (East)', - 'Turtle Rock Isolated Ledge Entrance', - 'Ganons Tower'] - -Inverted_LW_Dungeon_Entrances_Must_Exit = ['Desert Palace Entrance (East)'] - -Inverted_LW_Entrances_Must_Exit = ['Death Mountain Return Cave (West)', - 'Two Brothers House (West)'] - -Inverted_Two_Door_Caves_Directional = [('Old Man Cave (West)', 'Death Mountain Return Cave (West)'), - ('Two Brothers House (East)', 'Two Brothers House (West)')] + # find largest walkable sector + sector = None + invalid_sectors = list() + entrances = list() + while not len(entrances): + while (sector is None): + sector = max(sectors, key=lambda x: len(x) - (0 if x not in invalid_sectors else 1000)) + if not ((world.owCrossed[player] == 'polar' and world.owMixed[player]) or world.owCrossed[player] not in ['none', 'polar']) \ + and world.get_region(next(iter(next(iter(sector)))), player).type != (RegionType.LightWorld if not invFlag else RegionType.DarkWorld): + invalid_sectors.append(sector) + sector = None + regions = max(sector, key=lambda x: len(x)) + + # get entrances from list of regions + entrances = list() + for region_name in regions: + if world.shuffle[player] == 'simple' and region_name in OWTileRegions and OWTileRegions[region_name] in [0x03, 0x05, 0x07]: + continue + region = world.get_region(region_name, player) + if not force_starting_world or region.type == (RegionType.LightWorld if not invFlag else RegionType.DarkWorld): + for exit in region.exits: + if not exit.connected_region and exit.spot_type == 'Entrance': + entrances.append(exit.name) + + invalid_sectors.append(sector) + sector = None + + return entrances -Inverted_Two_Door_Caves = [('Elder House (East)', 'Elder House (West)'), - ('Superbunny Cave (Bottom)', 'Superbunny Cave (Top)'), - ('Hookshot Cave', 'Hookshot Cave Back Entrance')] +def get_distant_entrances(world, start_entrance, sectors, player): + # get walkable sector in which initial entrance was placed + start_region = world.get_entrance(start_entrance, player).parent_region.name + regions = next(s for s in sectors if any(start_region in w for w in s)) + regions = next(w for w in regions if start_region in w) + + # eliminate regions surrounding the initial entrance until less than half of the candidate regions remain + explored_regions = list({start_region}) + was_progress = True + while was_progress and len(explored_regions) < len(regions) / 2: + was_progress = False + new_regions = list() + for region_name in explored_regions: + if region_name in one_way_ledges: + for ledge in one_way_ledges[region_name]: + if ledge not in explored_regions + new_regions: + new_regions.append(ledge) + was_progress = True + region = world.get_region(region_name, player) + for exit in region.exits: + if exit.connected_region and region.type == exit.connected_region.type and exit.connected_region.name in regions and exit.connected_region.name not in explored_regions + new_regions: + new_regions.append(exit.connected_region.name) + was_progress = True + explored_regions.extend(new_regions) + + # get entrances from remaining regions + candidates = list() + for region_name in [r for r in regions if r not in explored_regions]: + if OWTileRegions[region_name] in [0x03, 0x05, 0x07]: + continue + region = world.get_region(region_name, player) + for exit in region.exits: + if not exit.connected_region and exit.spot_type == 'Entrance': + candidates.append(exit.name) + + return candidates +def can_reach(world, entrance_name, region_name, player): + from Main import copy_world + from Items import ItemFactory + from DoorShuffle import find_inaccessible_regions + + for player in range(1, world.players + 1): + world.key_logic[player] = {} + base_world = copy_world(world) + base_world.override_bomb_check = True + world.key_logic = {} + + entrance = world.get_entrance(entrance_name, player) + connect_simple(base_world, 'Links House S&Q', entrance.parent_region.name, player) + blank_state = CollectionState(base_world) + if base_world.mode[player] == 'standard': + blank_state.collect(ItemFactory('Zelda Delivered', player), True) -Inverted_Old_Man_Entrances = ['Dark Death Mountain Fairy', - 'Spike Cave'] - -Inverted_LW_Entrances = ['Elder House (East)', - 'Elder House (West)', - 'Two Brothers House (East)', - 'Old Man Cave (East)', - 'Old Man Cave (West)', - 'Old Man House (Bottom)', - 'Old Man House (Top)', - 'Death Mountain Return Cave (East)', - 'Paradox Cave (Bottom)', - 'Paradox Cave (Middle)', - 'Paradox Cave (Top)', - 'Spectacle Rock Cave', - 'Spectacle Rock Cave Peak', - 'Spectacle Rock Cave (Bottom)', - 'Fairy Ascension Cave (Bottom)', - 'Fairy Ascension Cave (Top)', - 'Spiral Cave', - 'Spiral Cave (Bottom)'] + find_inaccessible_regions(world, player) + return region_name not in world.inaccessible_regions[player] -Inverted_DW_Entrances = ['Bumper Cave (Bottom)', - 'Superbunny Cave (Top)', - 'Superbunny Cave (Bottom)', - 'Hookshot Cave', - 'Hookshot Cave Back Entrance'] +LW_Dungeon_Exits = [('Desert Palace Exit (South)', 'Desert Palace Exit (West)', 'Desert Palace Exit (East)'), + 'Desert Palace Exit (North)', + 'Eastern Palace Exit', + 'Tower of Hera Exit', + 'Agahnims Tower Exit'] -Inverted_Bomb_Shop_Multi_Cave_Doors = ['Hyrule Castle Entrance (South)', - 'Misery Mire', - 'Thieves Town', - 'Bumper Cave (Bottom)', - 'Swamp Palace', - 'Hyrule Castle Secret Entrance Stairs', - 'Skull Woods First Section Door', - 'Skull Woods Second Section Door (East)', - 'Skull Woods Second Section Door (West)', - 'Skull Woods Final Section', - 'Ice Palace', - 'Turtle Rock', - 'Dark Death Mountain Ledge (West)', - 'Dark Death Mountain Ledge (East)', - 'Superbunny Cave (Top)', - 'Superbunny Cave (Bottom)', - 'Hookshot Cave', - 'Ganons Tower', - 'Desert Palace Entrance (South)', - 'Tower of Hera', - 'Two Brothers House (West)', - 'Old Man Cave (East)', - 'Old Man House (Bottom)', - 'Old Man House (Top)', - 'Death Mountain Return Cave (East)', - 'Death Mountain Return Cave (West)', - 'Spectacle Rock Cave Peak', - 'Paradox Cave (Bottom)', - 'Paradox Cave (Middle)', - 'Paradox Cave (Top)', - 'Fairy Ascension Cave (Bottom)', - 'Fairy Ascension Cave (Top)', - 'Spiral Cave', - 'Spiral Cave (Bottom)', - 'Palace of Darkness', - 'Hyrule Castle Entrance (West)', - 'Hyrule Castle Entrance (East)', - 'Agahnims Tower', - 'Desert Palace Entrance (West)', - 'Desert Palace Entrance (North)'] +DW_Late_Dungeon_Exits = ['Ice Palace Exit', + 'Misery Mire Exit', + ('Turtle Rock Ledge Exit (East)', 'Turtle Rock Exit (Front)', 'Turtle Rock Ledge Exit (West)', 'Turtle Rock Isolated Ledge Exit')] -Inverted_DW_Single_Cave_Doors = ['Bonk Fairy (Dark)', - 'Dark Sanctuary Hint', - 'Big Bomb Shop', - 'Dark Lake Hylia Fairy', - 'C-Shaped House', - 'Bumper Cave (Top)', - 'Dark Lake Hylia Shop', - 'Dark World Shop', - 'Red Shield Shop', - 'Mire Shed', - 'East Dark World Hint', - 'Dark Desert Hint', - 'Palace of Darkness Hint', - 'Dark Lake Hylia Ledge Spike Cave', - 'Cave Shop (Dark Death Mountain)', - 'Dark World Potion Shop', - 'Pyramid Fairy', - 'Archery Game', - 'Dark World Lumberjack Shop', - 'Hype Cave', - 'Brewery', - 'Dark Lake Hylia Ledge Hint', - 'Chest Game', - 'Dark Desert Fairy', - 'Dark Lake Hylia Ledge Fairy', - 'Fortune Teller (Dark)', - 'Dark World Hammer Peg Cave'] +DW_Mid_Dungeon_Exits = ['Thieves Town Exit', + 'Skull Woods Final Section Exit', + 'Palace of Darkness Exit', + 'Swamp Palace Exit'] +Cave_Exits_Base = [('Elder House Exit (East)', 'Elder House Exit (West)'), + ('Two Brothers House Exit (East)', 'Two Brothers House Exit (West)'), + ('Death Mountain Return Cave Exit (West)', 'Death Mountain Return Cave Exit (East)'), + ('Fairy Ascension Cave Exit (Bottom)', 'Fairy Ascension Cave Exit (Top)'), + ('Bumper Cave Exit (Top)', 'Bumper Cave Exit (Bottom)'), + ('Hookshot Cave Back Exit', 'Hookshot Cave Front Exit')] -Inverted_Bomb_Shop_Single_Cave_Doors = ['Waterfall of Wishing', - 'Capacity Upgrade', - 'Bonk Rock Cave', - 'Graveyard Cave', - 'Checkerboard Cave', - 'Cave 45', - 'Kings Grave', - 'Bonk Fairy (Light)', - 'Hookshot Fairy', - 'East Dark World Hint', - 'Palace of Darkness Hint', - 'Dark Lake Hylia Fairy', - 'Dark Lake Hylia Ledge Fairy', - 'Dark Lake Hylia Ledge Spike Cave', - 'Dark Lake Hylia Ledge Hint', - 'Hype Cave', - 'Bonk Fairy (Dark)', - 'Brewery', - 'C-Shaped House', - 'Chest Game', - 'Dark World Hammer Peg Cave', - 'Red Shield Shop', - 'Dark Sanctuary Hint', - 'Fortune Teller (Dark)', - 'Dark World Shop', - 'Dark World Lumberjack Shop', - 'Dark World Potion Shop', - 'Archery Game', - 'Mire Shed', - 'Dark Desert Hint', - 'Dark Desert Fairy', - 'Spike Cave', - 'Cave Shop (Dark Death Mountain)', - 'Bumper Cave (Top)', - 'Mimic Cave', - 'Dark Lake Hylia Shop', - 'Links House', - 'Big Bomb Shop'] +Cave_Exits_Directional = [('Superbunny Cave Exit (Bottom)', 'Superbunny Cave Exit (Top)'), + ('Spiral Cave Exit (Top)', 'Spiral Cave Exit')] -Inverted_Blacksmith_Single_Cave_Doors = ['Blinds Hideout', - 'Lake Hylia Fairy', - 'Light Hype Fairy', - 'Desert Fairy', - 'Chicken House', - 'Aginahs Cave', - 'Sahasrahlas Hut', - 'Cave Shop (Lake Hylia)', - 'Blacksmiths Hut', - 'Sick Kids House', - 'Lost Woods Gamble', - 'Fortune Teller (Light)', - 'Snitch Lady (East)', - 'Snitch Lady (West)', - 'Bush Covered House', - 'Tavern (Front)', - 'Light World Bomb Hut', - 'Kakariko Shop', - 'Mini Moldorm Cave', - 'Long Fairy Cave', - 'Good Bee Cave', - '20 Rupee Cave', - '50 Rupee Cave', - 'Ice Rod Cave', - 'Library', - 'Potion Shop', - 'Dam', - 'Lumberjack House', - 'Lake Hylia Fortune Teller', - 'Kakariko Gamble Game', - 'Links House'] +Cave_Three_Exits_Base = [('Spectacle Rock Cave Exit (Peak)', 'Spectacle Rock Cave Exit (Top)', 'Spectacle Rock Cave Exit'), + ('Paradox Cave Exit (Top)', 'Paradox Cave Exit (Middle)', 'Paradox Cave Exit (Bottom)')] -Inverted_Single_Cave_Targets = ['Blinds Hideout', - 'Bonk Fairy (Light)', - 'Lake Hylia Healer Fairy', - 'Swamp Healer Fairy', - 'Desert Healer Fairy', - 'Kings Grave', - 'Chicken House', - 'Aginahs Cave', - 'Sahasrahlas Hut', - 'Cave Shop (Lake Hylia)', - 'Sick Kids House', - 'Lost Woods Gamble', - 'Fortune Teller (Light)', - 'Snitch Lady (East)', - 'Snitch Lady (West)', - 'Bush Covered House', - 'Tavern (Front)', - 'Light World Bomb Hut', - 'Kakariko Shop', - 'Cave 45', - 'Graveyard Cave', - 'Checkerboard Cave', - 'Mini Moldorm Cave', - 'Long Fairy Cave', - 'Good Bee Cave', - '20 Rupee Cave', - '50 Rupee Cave', - 'Ice Rod Cave', - 'Bonk Rock Cave', - 'Library', - 'Potion Shop', - 'Hookshot Fairy', - 'Waterfall of Wishing', - 'Capacity Upgrade', - 'Pyramid Fairy', - 'East Dark World Hint', - 'Palace of Darkness Hint', - 'Dark Lake Hylia Healer Fairy', - 'Dark Lake Hylia Ledge Healer Fairy', - 'Dark Lake Hylia Ledge Spike Cave', - 'Dark Lake Hylia Ledge Hint', - 'Hype Cave', - 'Bonk Fairy (Dark)', - 'Brewery', - 'C-Shaped House', - 'Chest Game', - 'Dark World Hammer Peg Cave', - 'Red Shield Shop', - 'Fortune Teller (Dark)', - 'Village of Outcasts Shop', - 'Dark Lake Hylia Shop', - 'Dark World Lumberjack Shop', - 'Archery Game', - 'Mire Shed', - 'Dark Desert Hint', - 'Dark Desert Healer Fairy', - 'Spike Cave', - 'Cave Shop (Dark Death Mountain)', - 'Dark Death Mountain Healer Fairy', - 'Mimic Cave', - 'Dark World Potion Shop', - 'Lumberjack House', - 'Lake Hylia Fortune Teller', - 'Kakariko Gamble Game', - 'Dam'] +Old_Man_House_Base = [('Old Man House Exit (Bottom)', 'Old Man House Exit (Top)')] -# in inverted we put dark sanctuary in west dark world for now -Inverted_Dark_Sanctuary_Doors = ['Dark Sanctuary Hint', - 'Fortune Teller (Dark)', - 'Brewery', - 'C-Shaped House', - 'Chest Game', - 'Dark World Lumberjack Shop', - 'Red Shield Shop', - 'Bumper Cave (Bottom)', - 'Bumper Cave (Top)', - 'Thieves Town'] - -# Entrances that cannot be used to access a must_exit entrance - symmetrical to allow reverse lookups -Must_Exit_Invalid_Connections = defaultdict(set, { - 'Dark Death Mountain Ledge (East)': {'Dark Death Mountain Ledge (West)', 'Mimic Cave'}, - 'Dark Death Mountain Ledge (West)': {'Dark Death Mountain Ledge (East)', 'Mimic Cave'}, - 'Mimic Cave': {'Dark Death Mountain Ledge (West)', 'Dark Death Mountain Ledge (East)'}, - 'Bumper Cave (Top)': {'Death Mountain Return Cave (West)'}, - 'Death Mountain Return Cave (West)': {'Bumper Cave (Top)'}, - 'Skull Woods Second Section Door (West)': {'Skull Woods Final Section'}, - 'Skull Woods Final Section': {'Skull Woods Second Section Door (West)'}, -}) -Inverted_Must_Exit_Invalid_Connections = defaultdict(set, { - 'Bumper Cave (Top)': {'Death Mountain Return Cave (West)'}, - 'Death Mountain Return Cave (West)': {'Bumper Cave (Top)'}, - 'Desert Palace Entrance (North)': {'Desert Palace Entrance (West)'}, - 'Desert Palace Entrance (West)': {'Desert Palace Entrance (North)'}, - 'Agahnims Tower': {'Hyrule Castle Entrance (West)', 'Hyrule Castle Entrance (East)'}, - 'Hyrule Castle Entrance (West)': {'Hyrule Castle Entrance (East)', 'Agahnims Tower'}, - 'Hyrule Castle Entrance (East)': {'Hyrule Castle Entrance (West)', 'Agahnims Tower'}, -}) Entrance_Pool_Base = {'Links House', 'Desert Palace Entrance (South)', @@ -2401,7 +2125,7 @@ Exit_Pool_Base = {'Links House Exit', 'Chest Game', 'Dark World Hammer Peg Cave', 'Red Shield Shop', - 'Dark Sanctuary Hint Exit', + 'Dark Sanctuary Hint', 'Fortune Teller (Dark)', 'Archery Game', 'Mire Shed', @@ -2484,7 +2208,7 @@ default_connections = [('Lumberjack House', 'Lumberjack House'), ('Dark Lake Hylia Ledge Spike Cave', 'Dark Lake Hylia Ledge Spike Cave'), ('Dark Lake Hylia Ledge Hint', 'Dark Lake Hylia Ledge Hint'), ('Bonk Fairy (Dark)', 'Bonk Fairy (Dark)'), - ('Dark Sanctuary Hint', 'Dark Sanctuary Hint Exit'), + ('Dark Sanctuary Hint', 'Dark Sanctuary Hint'), ('Fortune Teller (Dark)', 'Fortune Teller (Dark)'), ('Archery Game', 'Archery Game'), ('Dark Desert Hint', 'Dark Desert Hint'), @@ -2644,6 +2368,53 @@ inverted_default_dungeon_connections = [('Ganons Tower', 'Agahnims Tower Exit'), ('Agahnims Tower', 'Ganons Tower Exit') ] +one_way_ledges = { + 'West Death Mountain (Bottom)': {'West Death Mountain (Top)', + 'Spectacle Rock Ledge'}, + 'East Death Mountain (Bottom)': {'East Death Mountain (Top East)', + 'Spiral Cave Ledge'}, + 'Fairy Ascension Plateau': {'Fairy Ascension Ledge'}, + 'Mountain Entry Area': {'Mountain Entry Ledge'}, + 'Sanctuary Area': {'Bonk Rock Ledge'}, + 'Graveyard Area': {'Graveyard Ledge'}, + 'Potion Shop Water': {'Potion Shop Area', + 'Potion Shop Northeast'}, + 'Zora Approach Water': {'Zora Approach Area'}, + 'Hyrule Castle Area': {'Hyrule Castle Ledge'}, + 'Wooden Bridge Water': {'Wooden Bridge Area', + 'Wooden Bridge Northeast'}, + 'Maze Race Area': {'Maze Race Ledge', + 'Maze Race Prize'}, + 'Flute Boy Approach Area': {'Cave 45 Ledge'}, + 'Desert Area': {'Desert Ledge', + 'Desert Palace Entrance (North) Spot', + 'Desert Checkerboard Ledge', + 'Desert Palace Mouth', + 'Desert Palace Stairs', + 'Bombos Tablet Ledge', + 'Desert Palace Teleporter Ledge'}, + 'Desert Pass Area': {'Desert Pass Ledge'}, + 'Lake Hylia Water': {'Lake Hylia South Shore', + 'Lake Hylia Island'}, + 'West Dark Death Mountain (Bottom)': {'West Dark Death Mountain (Top)'}, + 'West Dark Death Mountain (Top)': {'Dark Death Mountain Floating Island'}, + 'East Dark Death Mountain (Bottom)': {'East Dark Death Mountain (Top)'}, + 'Turtle Rock Area': {'Turtle Rock Ledge'}, + 'Bumper Cave Area': {'Bumper Cave Ledge'}, + 'Qirn Jump Water': {'Qirn Jump Area'}, + 'Dark Witch Water': {'Dark Witch Area', + 'Dark Witch Northeast'}, + 'Catfish Approach Water': {'Catfish Approach Area'}, + 'Pyramid Area': {'Pyramid Exit Ledge'}, + 'Broken Bridge Water': {'Broken Bridge West', + 'Broken Bridge Area', + 'Broken Bridge Northeast'}, + 'Misery Mire Area': {'Misery Mire Teleporter Ledge'}, + 'Ice Lake Water': {'Ice Lake Area', + 'Ice Lake Ledge (West)', + 'Ice Lake Ledge (East)'} +} + indirect_connections = { 'Turtle Rock Ledge': 'Turtle Rock', 'Big Bomb Shop': 'Pyramid Fairy', diff --git a/ItemList.py b/ItemList.py index 4dae9f08..25c75fb1 100644 --- a/ItemList.py +++ b/ItemList.py @@ -739,7 +739,7 @@ def balance_prices(world, player): def check_hints(world, player): - if world.shuffle[player] in ['simple', 'restricted', 'full', 'crossed', 'insanity']: + if world.shuffle[player] in ['simple', 'restricted', 'full', 'lite', 'lean', 'crossed', 'insanity']: for shop, location_list in shop_to_location_table.items(): if shop in ['Capacity Upgrade', 'Light World Death Mountain Shop', 'Potion Shop']: continue # near the queen, near potions, and near 7 chests are fine diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index 24023584..748c1549 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -171,7 +171,7 @@ class PlacementRule(object): if loc.item and loc.item.bigkey: bk_blocked = True break - else: + elif len(self.check_locations_w_bk) > self.needed_keys_w_bk: def loc_has_bk(l): return (big_key_loc is not None and big_key_loc == l) or (l.item and l.item.bigkey) diff --git a/Main.py b/Main.py index 009b1b42..c00a7151 100644 --- a/Main.py +++ b/Main.py @@ -30,7 +30,7 @@ from Fill import sell_potions, sell_keys, balance_multiworld_progression, balanc from ItemList import generate_itempool, difficulties, fill_prizes, customize_shops from Utils import output_path, parse_player_names -__version__ = '0.5.1.2-u' +__version__ = '0.5.1.4-u' from source.classes.BabelFish import BabelFish @@ -114,6 +114,7 @@ def main(args, seed=None, fish=None): world.ganon_item = {player: get_random_ganon_item(args.swords) if args.ganon_item[player] == 'random' else args.ganon_item[player] for player in range(1, world.players + 1)} world.ganon_item_orig = args.ganon_item.copy() world.owKeepSimilar = args.ow_keepsimilar.copy() + world.owWhirlpoolShuffle = args.ow_whirlpool.copy() world.owFluteShuffle = args.ow_fluteshuffle.copy() world.open_pyramid = args.openpyramid.copy() world.boss_shuffle = args.shufflebosses.copy() @@ -157,6 +158,15 @@ def main(args, seed=None, fish=None): world.player_names[player].append(name) logger.info('') + if world.owShuffle[1] != 'vanilla' or world.owCrossed[1] not in ['none', 'polar'] or world.owMixed[1] or str(world.seed).startswith('M'): + outfilebase = f'OR_{args.outputname if args.outputname else world.seed}' + else: + outfilebase = f'DR_{args.outputname if args.outputname else world.seed}' + + if args.create_spoiler and not args.jsonout: + logger.info(world.fish.translate("cli","cli","patching.spoiler")) + world.spoiler.meta_to_file(output_path('%s_Spoiler.txt' % outfilebase)) + for player in range(1, world.players + 1): world.difficulty_requirements[player] = difficulties[world.difficulty[player]] @@ -292,11 +302,6 @@ def main(args, seed=None, fish=None): customize_shops(world, player) balance_money_progression(world) - if world.owShuffle[1] != 'vanilla' or world.owCrossed[1] not in ['none', 'polar'] or world.owMixed[1] or str(world.seed).startswith('M'): - outfilebase = f'OR_{args.outputname if args.outputname else world.seed}' - else: - outfilebase = f'DR_{args.outputname if args.outputname else world.seed}' - rom_names = [] jsonout = {} enemized = False @@ -364,6 +369,10 @@ def main(args, seed=None, fish=None): with open(output_path('%s_multidata' % outfilebase), 'wb') as f: f.write(multidata) + if args.create_spoiler and not args.jsonout: + logger.info(world.fish.translate("cli","cli","patching.spoiler")) + world.spoiler.to_file(output_path('%s_Spoiler.txt' % outfilebase)) + if not args.skip_playthrough: logger.info(world.fish.translate("cli","cli","calc.playthrough")) create_playthrough(world) @@ -376,7 +385,7 @@ def main(args, seed=None, fish=None): with open(output_path('%s_Spoiler.json' % outfilebase), 'w') as outfile: outfile.write(world.spoiler.to_json()) else: - world.spoiler.to_file(output_path('%s_Spoiler.txt' % outfilebase)) + world.spoiler.playthru_to_file(output_path('%s_Spoiler.txt' % outfilebase)) YES = world.fish.translate("cli","cli","yes") NO = world.fish.translate("cli","cli","no") @@ -435,6 +444,7 @@ def copy_world(world): ret.ganon_item = world.ganon_item.copy() ret.ganon_item_orig = world.ganon_item_orig.copy() ret.owKeepSimilar = world.owKeepSimilar.copy() + ret.owWhirlpoolShuffle = world.owWhirlpoolShuffle.copy() ret.owFluteShuffle = world.owFluteShuffle.copy() ret.open_pyramid = world.open_pyramid.copy() ret.boss_shuffle = world.boss_shuffle.copy() @@ -542,7 +552,9 @@ def copy_world(world): connect_portal(portal, ret, player) ret.sanc_portal = world.sanc_portal + from OverworldShuffle import categorize_world_regions for player in range(1, world.players + 1): + categorize_world_regions(ret, player) set_rules(ret, player) return ret diff --git a/Mystery.py b/Mystery.py index 11526bc2..6adb7426 100644 --- a/Mystery.py +++ b/Mystery.py @@ -28,6 +28,7 @@ def main(): parser.add_argument('--names', default='') parser.add_argument('--teams', default=1, type=lambda value: max(int(value), 1)) parser.add_argument('--create_spoiler', action='store_true') + parser.add_argument('--no_race', action='store_true') parser.add_argument('--rom') parser.add_argument('--jsonout', action='store_true') parser.add_argument('--enemizercli') @@ -67,9 +68,10 @@ def main(): erargs.seed = seed erargs.names = args.names erargs.create_spoiler = args.create_spoiler - erargs.race = True + erargs.race = not args.no_race erargs.outputname = seedname - erargs.outputpath = args.outputpath + if args.outputpath: + erargs.outputpath = args.outputpath erargs.loglevel = args.loglevel if args.rom: @@ -124,7 +126,7 @@ def roll_settings(weights): if glitches_required not in ['none', 'no_logic']: print("Only NMG and No Logic supported") glitches_required = 'none' - ret.logic = {'none': 'noglitches', 'no_logic': 'nologic'}[glitches_required] + ret.logic = {'none': 'noglitches', 'owg': 'owglitches', 'no_logic': 'nologic'}[glitches_required] item_placement = get_choice('item_placement') # not supported in ER @@ -142,6 +144,7 @@ def roll_settings(weights): ret.ow_crossed = get_choice('overworld_crossed') 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' entrance_shuffle = get_choice('entrance_shuffle') @@ -171,6 +174,8 @@ def roll_settings(weights): }[goal] ret.openpyramid = goal == 'fast_ganon' if ret.shuffle in ['vanilla', 'dungeonsfull', 'dungeonssimple'] else False + ret.shuffleganon = get_choice('shuffleganon') == 'on' + ret.crystals_gt = get_choice('tower_open') ret.crystals_ganon = get_choice('ganon_open') diff --git a/OWEdges.py b/OWEdges.py index ebf1b15b..b9064281 100644 --- a/OWEdges.py +++ b/OWEdges.py @@ -969,7 +969,7 @@ OWTileRegions = bidict({ }) OWTileGroups = { - ("Woods", "Regular"): ( + ("Woods", "Regular", "None"): ( [ 0x00, 0x2d, 0x80 ], @@ -977,7 +977,7 @@ OWTileGroups = { 0x40, 0x6d ] ), - ("Lumberjack", "Regular"): ( + ("Lumberjack", "Regular", "None"): ( [ 0x02 ], @@ -985,7 +985,7 @@ OWTileGroups = { 0x42 ] ), - ("West Mountain", "Regular"): ( + ("West Mountain", "Regular", "None"): ( [ 0x03 ], @@ -993,7 +993,7 @@ OWTileGroups = { 0x43 ] ), - ("East Mountain", "Regular"): ( + ("East Mountain", "Regular", "None"): ( [ 0x05 ], @@ -1001,23 +1001,31 @@ OWTileGroups = { 0x45 ] ), - ("East Mountain", "Entrance"): ( + ("East Mountain", "Entrance", "None"): ( [ - 0x07, + 0x07 ], [ 0x47 ] ), - ("Lake", "Regular"): ( + ("Lake", "Regular", "Zora"): ( [ - 0x0f, 0x35, 0x81 + 0x0f, 0x81 ], [ - 0x4f, 0x75 + 0x4f ] ), - ("Mountain Entry", "Regular"): ( + ("Lake", "Regular", "Lake"): ( + [ + 0x35 + ], + [ + 0x75 + ] + ), + ("Mountain Entry", "Regular", "None"): ( [ 0x0a ], @@ -1025,7 +1033,7 @@ OWTileGroups = { 0x4a ] ), - ("Woods Pass", "Regular"): ( + ("Woods Pass", "Regular", "None"): ( [ 0x10 ], @@ -1033,7 +1041,7 @@ OWTileGroups = { 0x50 ] ), - ("Fortune", "Regular"): ( + ("Fortune", "Regular", "None"): ( [ 0x11 ], @@ -1041,15 +1049,39 @@ OWTileGroups = { 0x51 ] ), - ("Whirlpools", "Regular"): ( + ("Whirlpools", "Regular", "Pond"): ( [ - 0x12, 0x15, 0x33, 0x3f + 0x12 ], [ - 0x52, 0x55, 0x73, 0x7f + 0x52 ] ), - ("Castle", "Entrance"): ( + ("Whirlpools", "Regular", "Witch"): ( + [ + 0x15 + ], + [ + 0x55 + ] + ), + ("Whirlpools", "Regular", "CWhirlpool"): ( + [ + 0x33 + ], + [ + 0x73 + ] + ), + ("Whirlpools", "Regular", "Southeast"): ( + [ + 0x3f + ], + [ + 0x7f + ] + ), + ("Castle", "Entrance", "None"): ( [ 0x13, 0x14 ], @@ -1057,7 +1089,7 @@ OWTileGroups = { 0x53, 0x54 ] ), - ("Castle", "Regular"): ( + ("Castle", "Regular", "None"): ( [ 0x1a, 0x1b ], @@ -1065,7 +1097,7 @@ OWTileGroups = { 0x5a, 0x5b ] ), - ("Witch", "Regular"): ( + ("Witch", "Regular", "None"): ( [ 0x16 ], @@ -1073,7 +1105,7 @@ OWTileGroups = { 0x56 ] ), - ("Water Approach", "Regular"): ( + ("Water Approach", "Regular", "None"): ( [ 0x17 ], @@ -1081,7 +1113,7 @@ OWTileGroups = { 0x57 ] ), - ("Village", "Regular"): ( + ("Village", "Regular", "None"): ( [ 0x18 ], @@ -1089,7 +1121,7 @@ OWTileGroups = { 0x58 ] ), - ("Wooden Bridge", "Regular"): ( + ("Wooden Bridge", "Regular", "None"): ( [ 0x1d ], @@ -1097,7 +1129,7 @@ OWTileGroups = { 0x5d ] ), - ("Eastern", "Regular"): ( + ("Eastern", "Regular", "None"): ( [ 0x1e ], @@ -1105,7 +1137,7 @@ OWTileGroups = { 0x5e ] ), - ("Blacksmith", "Regular"): ( + ("Blacksmith", "Regular", "None"): ( [ 0x22 ], @@ -1113,7 +1145,7 @@ OWTileGroups = { 0x62 ] ), - ("Dunes", "Regular"): ( + ("Dunes", "Regular", "None"): ( [ 0x25 ], @@ -1121,7 +1153,7 @@ OWTileGroups = { 0x65 ] ), - ("Game", "Regular"): ( + ("Game", "Regular", "None"): ( [ 0x28, 0x29 ], @@ -1129,7 +1161,7 @@ OWTileGroups = { 0x68, 0x69 ] ), - ("Grove", "Regular"): ( + ("Grove", "Regular", "None"): ( [ 0x2a ], @@ -1137,7 +1169,7 @@ OWTileGroups = { 0x6a ] ), - ("Central Bonk Rocks", "Regular"): ( + ("Central Bonk Rocks", "Regular", "None"): ( [ 0x2b ], @@ -1145,7 +1177,7 @@ OWTileGroups = { 0x6b ] ), - # ("Links", "Regular"): ( + # ("Links", "Regular", "None"): ( # [ # 0x2c # ], @@ -1153,7 +1185,7 @@ OWTileGroups = { # 0x6c # ] # ), - ("Tree Line", "Regular"): ( + ("Tree Line", "Regular", "None"): ( [ 0x2e ], @@ -1161,7 +1193,7 @@ OWTileGroups = { 0x6e ] ), - ("Nook", "Regular"): ( + ("Nook", "Regular", "None"): ( [ 0x2f ], @@ -1169,7 +1201,7 @@ OWTileGroups = { 0x6f ] ), - ("Desert", "Regular"): ( + ("Desert", "Regular", "None"): ( [ 0x30, 0x3a ], @@ -1177,7 +1209,7 @@ OWTileGroups = { 0x70, 0x7a ] ), - ("Grove Approach", "Regular"): ( + ("Grove Approach", "Regular", "None"): ( [ 0x32 ], @@ -1185,7 +1217,7 @@ OWTileGroups = { 0x72 ] ), - ("Hype", "Regular"): ( + ("Hype", "Regular", "None"): ( [ 0x34 ], @@ -1193,7 +1225,7 @@ OWTileGroups = { 0x74 ] ), - ("Shopping Mall", "Regular"): ( + ("Shopping Mall", "Regular", "None"): ( [ 0x37 ], @@ -1201,7 +1233,7 @@ OWTileGroups = { 0x77 ] ), - ("Swamp", "Regular"): ( + ("Swamp", "Regular", "None"): ( [ 0x3b ], @@ -1209,7 +1241,7 @@ OWTileGroups = { 0x7b ] ), - ("South Pass", "Regular"): ( + ("South Pass", "Regular", "None"): ( [ 0x3c ], @@ -1353,4 +1385,460 @@ parallel_links = bidict({'Lost Woods SW': 'Skull Woods SW', 'Octoballoon NE': 'Bomber Corner NE', 'Octoballoon WC': 'Bomber Corner WC', 'Octoballoon WS': 'Bomber Corner WS' - }) \ No newline at end of file + }) + +OWExitTypes = { + 'Ledge': ['West Death Mountain Drop', + 'Spectacle Rock Drop', + 'East Death Mountain Spiral Ledge Drop', + 'East Death Mountain Fairy Ledge Drop', + 'East Death Mountain Mimic Ledge Drop', + 'Spiral Ledge Drop', + 'Mimic Ledge Drop', + 'Fairy Ascension Ledge Drop', + 'Fairy Ascension Plateau Ledge Drop', + 'TR Pegs Ledge Drop', + 'Mountain Entry Entrance Ledge Drop', + 'Mountain Entry Ledge Drop', + 'Zora Waterfall Water Drop', + 'Bonk Rock Ledge Drop', + 'Graveyard Ledge Drop', + 'River Bend Water Drop', + 'River Bend East Water Drop', + 'Potion Shop Water Drop', + 'Potion Shop Northeast Water Drop', + 'Zora Approach Bottom Ledge Drop', + 'Zora Approach Water Drop', + 'Zora Approach Ledge Drop', + 'Hyrule Castle Ledge Drop', + 'Hyrule Castle Ledge Courtyard Drop', + 'Wooden Bridge Water Drop', + 'Wooden Bridge Northeast Water Drop', + 'Sand Dunes Ledge Drop', + 'Stone Bridge East Ledge Drop', + 'Tree Line Ledge Drop', + 'Eastern Palace Ledge Drop', + 'Maze Race Ledge Drop', + 'Central Bonk Rocks Cliff Ledge Drop', + 'Links House Cliff Ledge Drop', + 'Stone Bridge Cliff Ledge Drop', + 'Lake Hylia Area Cliff Ledge Drop', + 'Lake Hylia Island FAWT Ledge Drop', + 'Stone Bridge EC Cliff Water Drop', + 'Tree Line WC Cliff Water Drop', + 'C Whirlpool Outer Cliff Ledge Drop', + 'C Whirlpool Cliff Ledge Drop', + 'South Teleporter Cliff Ledge Drop', + 'Statues Cliff Ledge Drop', + 'Desert Ledge Drop', + 'Checkerboard Ledge Drop', + 'Desert Mouth Drop', + 'Desert Teleporter Drop', + 'Desert Boss Cliff Ledge Drop', + 'Checkerboard Cliff Ledge Drop', + 'Suburb Cliff Ledge Drop', + 'Cave 45 Cliff Ledge Drop', + 'Desert C Whirlpool Cliff Ledge Drop', + 'Desert Pass Cliff Ledge Drop', + 'Desert Pass Southeast Cliff Ledge Drop', + 'Dam Cliff Ledge Drop', + 'Bombos Tablet Drop', + 'Cave 45 Ledge Drop', + 'Lake Hylia Water Drop', + 'Lake Hylia South Water Drop', + 'Lake Hylia Northeast Water Drop', + 'Lake Hylia Central Water Drop', + 'Lake Hylia Island Water Drop', + 'Desert Pass Ledge Drop', + 'Octoballoon Water Drop', + 'Octoballoon Waterfall Water Drop', + 'Dark Death Mountain Drop (West)', + 'Dark Death Mountain Drop (East)', + 'Floating Island Drop', + 'Turtle Rock Tail Ledge Drop', + 'Turtle Rock Ledge Drop', + 'Bumper Cave Ledge Drop', + 'Bumper Cave Entrance Drop', + 'Qirn Jump Water Drop', + 'Qirn Jump East Water Drop', + 'Dark Witch Water Drop', + 'Dark Witch Northeast Water Drop', + 'Catfish Approach Bottom Ledge Drop', + 'Catfish Approach Water Drop', + 'Catfish Approach Ledge Drop', + 'Shield Shop Fence (Outer) Ledge Drop', + 'Shield Shop Fence (Inner) Ledge Drop', + 'Pyramid Exit Ledge Drop', + 'Broken Bridge Water Drop', + 'Broken Bridge Northeast Water Drop', + 'Broken Bridge West Water Drop', + 'Dark Dunes Ledge Drop', + 'Hammer Bridge North Ledge Drop', + 'Dark Tree Line Ledge Drop', + 'Palace of Darkness Ledge Drop', + 'Dig Game To Ledge Drop', + 'Dig Game Ledge Drop', + 'Frog Ledge Drop', + 'Hammer Bridge Water Drop', + 'Dark Bonk Rocks Cliff Ledge Drop', + 'Bomb Shop Cliff Ledge Drop', + 'Hammer Bridge South Cliff Ledge Drop', + 'Ice Lake Area Cliff Ledge Drop', + 'Ice Palace Island FAWT Ledge Drop', + 'Hammer Bridge EC Cliff Water Drop', + 'Dark Tree Line WC Cliff Water Drop', + 'Dark C Whirlpool Outer Cliff Ledge Drop', + 'Dark C Whirlpool Cliff Ledge Drop', + 'Hype Cliff Ledge Drop', + 'Dark South Teleporter Cliff Ledge Drop', + 'Misery Mire Teleporter Ledge Drop', + 'Mire Cliff Ledge Drop', + 'Archery Game Cliff Ledge Drop', + 'Stumpy Approach Cliff Ledge Drop', + 'Mire C Whirlpool Cliff Ledge Drop', + 'Swamp Nook Cliff Ledge Drop', + 'Swamp Cliff Ledge Drop', + 'Ice Lake Water Drop', + 'Ice Lake Northeast Water Drop', + 'Ice Lake Southwest Water Drop', + 'Ice Lake Southeast Water Drop', + 'Bomber Corner Water Drop', + 'Bomber Corner Waterfall Water Drop' + ], + 'OWTerrain': ['Lost Woods Bush (West)', + 'Lost Woods Bush (East)', + 'Spectacle Rock Approach', + 'Spectacle Rock Leave', + 'DM Hammer Bridge (West)', + 'DM Hammer Bridge (East)', + 'Floating Island Bridge (East)', + 'Fairy Ascension Rocks (North)', + 'DM Broken Bridge (West)', + 'DM Broken Bridge (East)', + 'Fairy Ascension Rocks (South)', + 'Floating Island Bridge (West)', + 'TR Pegs Ledge Entry', + 'TR Pegs Ledge Leave', + 'Mountain Entry Entrance Rock (West)', + 'Mountain Entry Entrance Rock (East)', + 'Zora Waterfall Water Entry', + 'Waterfall of Wishing Cave Entry', + 'Zora Waterfall Landing', + 'Kings Grave Outer Rocks', + 'Graveyard Ladder (Bottom)', + 'Graveyard Ladder (Top)', + 'Kings Grave Inner Rocks', + 'River Bend West Pier', + 'River Bend East Pier', + 'Potion Shop Rock (South)', + 'Potion Shop Rock (North)', + 'Zora Approach Rocks (West)', + 'Zora Approach Rocks (East)', + 'Kakariko Southwest Bush (North)', + 'Kakariko Yard Bush (South)', + 'Kakariko Southwest Bush (South)', + 'Kakariko Yard Bush (North)', + 'Hyrule Castle Main Gate (South)', + 'Hyrule Castle Inner East Rock', + 'Hyrule Castle Southwest Bush (North)', + 'Hyrule Castle Southwest Bush (South)', + 'Hyrule Castle Courtyard Bush (South)', + 'Hyrule Castle Main Gate (North)', + 'Hyrule Castle Courtyard Bush (North)', + 'Hyrule Castle Outer East Rock', + 'Wooden Bridge Bush (South)', + 'Wooden Bridge Bush (North)', + 'Bat Cave Ledge Peg', + 'Bat Cave Ledge Peg (East)', + 'Maze Race Game', + 'Desert Palace Statue Move', + 'Checkerboard Ledge Approach', + 'Desert Ledge Outer Rocks', + 'Desert Ledge Inner Rocks', + 'Checkerboard Ledge Leave', + 'Flute Boy Bush (South)', + 'Cave 45 Inverted Approach', + 'Flute Boy Bush (North)', + 'Cave 45 Inverted Leave', + 'C Whirlpool Rock (Bottom)', + 'C Whirlpool Water Entry', + 'C Whirlpool Landing', + 'C Whirlpool Rock (Top)', + 'Statues Water Entry', + 'Statues Landing', + 'Lake Hylia Central Island Pier', + 'Lake Hylia Island Pier', + 'Lake Hylia West Pier', + 'Lake Hylia East Pier', + 'Desert Pass Ladder (South)', + 'Desert Pass Rocks (North)', + 'Desert Pass Rocks (South)', + 'Desert Pass Ladder (North)', + 'Octoballoon Pier', + 'Skull Woods Bush Rock (East)', + 'Skull Woods Bush Rock (West)', + 'Skull Woods Forgotten Bush (West)', + 'Skull Woods Forgotten Bush (East)', + 'GT Entry Approach', + 'Dark Death Mountain Ladder (North)', + 'GT Entry Leave', + 'Dark Death Mountain Ladder (South)', + 'Bumper Cave Entrance Rock', + 'Skull Woods Pass Bush Row (West)', + 'Skull Woods Pass Bush Row (East)', + 'Skull Woods Pass Rock (Top)', + 'Skull Woods Pass Rock (Bottom)', + 'Dark Graveyard Bush (South)', + 'Dark Graveyard Bush (North)', + 'Qirn Jump Pier', + 'Dark Witch Rock (South)', + 'Dark Witch Rock (North)', + 'Catfish Approach Rocks (West)', + 'Catfish Approach Rocks (East)', + 'Village of Outcasts Pegs', + 'Grassy Lawn Pegs', + 'Broken Bridge Hammer Rock (South)', + 'Broken Bridge Hammer Rock (North)', + 'Broken Bridge Hookshot Gap', + 'Peg Area Rocks (West)', + 'Peg Area Rocks (East)', + 'Frog Rock (Outer)', + 'Archery Game Rock (North)', + 'Frog Rock (Inner)', + 'Archery Game Rock (South)', + 'Hammer Bridge Pegs (North)', + 'Hammer Bridge Pegs (South)', + 'Hammer Bridge Pier', + 'Stumpy Approach Bush (South)', + 'Stumpy Approach Bush (North)', + 'Dark C Whirlpool Rock (Bottom)', + 'Dark C Whirlpool Water Entry', + 'Dark C Whirlpool Landing', + 'Dark C Whirlpool Rock (Top)', + 'Hype Cave Water Entry', + 'Hype Cave Landing', + 'Ice Lake Northeast Pier', + 'Ice Lake Moat Water Entry', + 'Ice Lake Northeast Pier Bomb Jump', + 'Ice Palace Approach', + 'Ice Palace Leave', + 'Bomber Corner Pier' + ], + 'Portal': ['West Death Mountain Teleporter', + 'East Death Mountain Teleporter', + 'TR Pegs Teleporter', + 'Kakariko Teleporter (Hammer)', + 'Kakariko Teleporter (Rock)', + 'Top of Pyramid', + 'Top of Pyramid (Inner)', + 'East Hyrule Teleporter', + 'Desert Teleporter', + 'South Hyrule Teleporter', + 'Lake Hylia Teleporter', + 'Dark Death Mountain Teleporter (West)', + 'Dark Death Mountain Teleporter (East)', + 'Turtle Rock Teleporter', + 'West Dark World Teleporter (Hammer)', + 'West Dark World Teleporter (Rock)', + 'Post Aga Inverted Teleporter', + 'East Dark World Teleporter', + 'Misery Mire Teleporter', + 'South Dark World Teleporter', + 'Ice Palace Teleporter' + ], + 'Whirlpool': ['Zora Whirlpool', + 'Kakariko Pond Whirlpool', + 'River Bend Whirlpool', + 'C Whirlpool', + 'Lake Hylia Whirlpool', + 'Octoballoon Whirlpool', + 'Qirn Jump Whirlpool', + 'Bomber Corner Whirlpool' + ], + 'Mirror': ['Skull Woods Back Mirror Spot', + 'Skull Woods Forgotten (West) Mirror Spot', + 'Skull Woods Forgotten (East) Mirror Spot', + 'Skull Woods Portal Entry Mirror Spot', + 'Skull Woods Forgotten (Middle) Mirror Spot', + 'Skull Woods Front Mirror Spot', + 'Dark Lumberjack Mirror Spot', + 'West Dark Death Mountain (Top) Mirror Spot', + 'Bubble Boy Mirror Spot', + 'West Dark Death Mountain (Bottom) Mirror Spot', + 'East Dark Death Mountain (Top West) Mirror Spot', + 'East Dark Death Mountain (Top East) Mirror Spot', + 'TR Ledge (West) Mirror Spot', + 'TR Ledge (East) Mirror Spot', + 'TR Isolated Mirror Spot', + 'East Dark Death Mountain (Bottom Plateau) Mirror Spot', + 'East Dark Death Mountain (Bottom Left) Mirror Spot', + 'East Dark Death Mountain (Bottom) Mirror Spot', + 'Dark Floating Island Mirror Spot', + 'Turtle Rock Mirror Spot', + 'Turtle Rock Ledge Mirror Spot', + 'Bumper Cave Area Mirror Spot', + 'Bumper Cave Entry Mirror Spot', + 'Bumper Cave Ledge Mirror Spot', + 'Catfish Mirror Spot', + 'Skull Woods Pass West Mirror Spot', + 'Skull Woods Pass East Top Mirror Spot', + 'Skull Woods Pass East Bottom Mirror Spot', + 'Outcast Fortune Mirror Spot', + 'Outcast Pond Mirror Spot', + 'Dark Chapel Mirror Spot', + 'Dark Chapel Ledge Mirror Spot', + 'Dark Graveyard Mirror Spot', + 'Dark Graveyard Ledge Mirror Spot', + 'Dark Graveyard Grave Mirror Spot', + 'Qirn Jump Mirror Spot', + 'Qirn Jump East Mirror Spot', + 'Dark Witch Mirror Spot', + 'Dark Witch Northeast Mirror Spot', + 'Catfish Approach Mirror Spot', + 'Catfish Approach Ledge Mirror Spot', + 'Village of Outcasts Mirror Spot', + 'Village of Outcasts Southwest Mirror Spot', + 'Hammer House Mirror Spot', + 'Shield Shop Mirror Spot', + 'Pyramid Mirror Spot', + 'Pyramid Pass Mirror Spot', + 'Pyramid Courtyard Mirror Spot', + 'Pyramid Uncle Mirror Spot', + 'Pyramid From Ledge Mirror Spot', + 'Pyramid Entry Mirror Spot', + 'Broken Bridge West Mirror Spot', + 'Broken Bridge East Mirror Spot', + 'Broken Bridge Northeast Mirror Spot', + 'Palace of Darkness Mirror Spot', + 'Hammer Pegs Mirror Spot', + 'Hammer Pegs Entry Mirror Spot', + 'Dark Dunes Mirror Spot', + 'Dig Game Mirror Spot', + 'Dig Game Ledge Mirror Spot', + 'Frog Mirror Spot', + 'Frog Prison Mirror Spot', + 'Archery Game Mirror Spot', + 'Stumpy Mirror Spot', + 'Stumpy Pass Mirror Spot', + 'Dark Bonk Rocks Mirror Spot', + 'Big Bomb Shop Mirror Spot', + 'Hammer Bridge North Mirror Spot', + 'Hammer Bridge South Mirror Spot', + 'Dark Hobo Mirror Spot', + 'Dark Tree Line Mirror Spot', + 'Darkness Nook Mirror Spot', + 'Misery Mire Mirror Spot', + 'Misery Mire Ledge Mirror Spot', + 'Misery Mire Blocked Mirror Spot', + 'Misery Mire Main Mirror Spot', + 'Stumpy Approach Mirror Spot', + 'Stumpy Bush Entry Mirror Spot', + 'Dark C Whirlpool Mirror Spot', + 'Dark C Whirlpool Outer Mirror Spot', + 'Hype Cave Mirror Spot', + 'Ice Lake Mirror Spot', + 'Ice Lake Southwest Mirror Spot', + 'Ice Lake Southeast Mirror Spot', + 'Ice Lake Northeast Mirror Spot', + 'Ice Palace Mirror Spot', + 'Shopping Mall Mirror Spot', + 'Swamp Nook Mirror Spot', + 'Swamp Nook Southeast Mirror Spot', + 'Swamp Nook Pegs Mirror Spot', + 'Swamp Mirror Spot', + 'Dark South Pass Mirror Spot', + 'Bomber Corner Mirror Spot', + 'Lost Woods East Mirror Spot', + 'Lost Woods Entry Mirror Spot', + 'Lost Woods Pedestal Mirror Spot', + 'Lost Woods Southwest Mirror Spot', + 'Lost Woods East (Forgotten) Mirror Spot', + 'Lost Woods West (Forgotten) Mirror Spot', + 'Lumberjack Mirror Spot', + 'West Death Mountain (Top) Mirror Spot', + 'Spectacle Rock Mirror Spot', + 'East Death Mountain (Top West) Mirror Spot', + 'East Death Mountain (Top East) Mirror Spot', + 'Fairy Ascension Mirror Spot', + 'Death Mountain Bridge Mirror Spot', + 'Spiral Cave Mirror Spot', + 'Mimic Cave Mirror Spot', + 'Isolated Ledge Mirror Spot', + 'Floating Island Mirror Spot', + 'TR Pegs Area Mirror Spot', + 'Mountain Entry Mirror Spot', + 'Mountain Entry Entrance Mirror Spot', + 'Mountain Entry Ledge Mirror Spot', + 'Zora Waterfall Mirror Spot', + 'Lost Woods Pass West Mirror Spot', + 'Lost Woods Pass East Top Mirror Spot', + 'Lost Woods Pass East Bottom Mirror Spot', + 'Kakariko Fortune Mirror Spot', + 'Kakariko Pond Mirror Spot', + 'Sanctuary Mirror Spot', + 'Bonk Rock Ledge Mirror Spot', + 'Graveyard Ledge Mirror Spot', + 'Kings Grave Mirror Spot', + 'River Bend Mirror Spot', + 'River Bend East Mirror Spot', + 'Potion Shop Mirror Spot', + 'Potion Shop Northeast Mirror Spot', + 'Zora Approach Mirror Spot', + 'Zora Approach Ledge Mirror Spot', + 'Kakariko Mirror Spot', + 'Kakariko Grass Mirror Spot', + 'Forgotton Forest Mirror Spot', + 'Forgotton Forest Fence Mirror Spot', + 'HC Ledge Mirror Spot', + 'HC Courtyard Mirror Spot', + 'HC Area Mirror Spot', + 'HC East Entry Mirror Spot', + 'HC Courtyard Left Mirror Spot', + 'HC Area South Mirror Spot', + 'Wooden Bridge Mirror Spot', + 'Wooden Bridge Northeast Mirror Spot', + 'Wooden Bridge West Mirror Spot', + 'Eastern Palace Mirror Spot', + 'Blacksmith Entry Mirror Spot', + 'Blacksmith Mirror Spot', + 'Bat Cave Ledge Mirror Spot', + 'Sand Dunes Mirror Spot', + 'Maze Race Mirror Spot', + 'Maze Race Ledge Mirror Spot', + 'Kakariko Suburb Mirror Spot', + 'Kakariko Suburb South Mirror Spot', + 'Flute Boy Mirror Spot', + 'Flute Boy Pass Mirror Spot', + 'Central Bonk Rocks Mirror Spot', + 'Links House Mirror Spot', + 'Stone Bridge Mirror Spot', + 'Stone Bridge South Mirror Spot', + 'Hobo Mirror Spot', + 'Tree Line Mirror Spot', + 'Eastern Nook Mirror Spot', + 'Desert Mirror Spot', + 'Desert Ledge Mirror Spot', + 'Checkerboard Mirror Spot', + 'DP Stairs Mirror Spot', + 'DP Entrance (North) Mirror Spot', + 'Bombos Tablet Ledge Mirror Spot', + 'Cave 45 Mirror Spot', + 'Flute Boy Entry Mirror Spot', + 'C Whirlpool Mirror Spot', + 'C Whirlpool Outer Mirror Spot', + 'Statues Mirror Spot', + 'Lake Hylia Mirror Spot', + 'Lake Hylia Northeast Mirror Spot', + 'South Shore Mirror Spot', + 'South Shore East Mirror Spot', + 'Lake Hylia Island Mirror Spot', + 'Lake Hylia Water Mirror Spot', + 'Lake Hylia Central Island Mirror Spot', + 'Ice Cave Mirror Spot', + 'Desert Pass Ledge Mirror Spot', + 'Desert Pass Mirror Spot', + 'Dam Mirror Spot', + 'South Pass Mirror Spot', + 'Octoballoon Mirror Spot' + ] +} \ No newline at end of file diff --git a/OverworldGlitchRules.py b/OverworldGlitchRules.py index fcb9a18d..40c1efb9 100644 --- a/OverworldGlitchRules.py +++ b/OverworldGlitchRules.py @@ -152,8 +152,8 @@ def get_boots_clip_exits_lw(inverted = False): yield ('TR Pegs Ledge Clip', 'Death Mountain TR Pegs', 'Death Mountain TR Pegs Ledge') yield ('TR Pegs To EDM Clip', 'Death Mountain TR Pegs', 'East Death Mountain (Top East)') yield ('Zora DMD Clip', 'Death Mountain TR Pegs', 'Zora Waterfall Area') - yield ('Mountain Entry To Ledge Clip', 'Mountain Entry Area', 'Death Mountain Return Ledge') - yield ('Mountain Ledge Drop Clip', 'Death Mountain Return Ledge', 'Death Mountain Entrance') + yield ('Mountain Entry To Ledge Clip', 'Mountain Entry Area', 'Mountain Entry Ledge') + yield ('Mountain Ledge Drop Clip', 'Mountain Entry Ledge', 'Mountain Entry Entrance') yield ('Mountain Entry To Pond Clip', 'Mountain Entry Area', 'Kakariko Pond Area') yield ('Zora Waterfall Ledge Clip', 'Zora Waterfall Area', 'Zora Approach Area') @@ -225,12 +225,12 @@ def get_boots_clip_exits_dw(inverted): yield ('DDM Glitched Bridge Clip', 'West Dark Death Mountain (Bottom)', 'East Dark Death Mountain (Top)') yield ('Chapel DMD Clip', 'West Dark Death Mountain (Bottom)', 'Dark Chapel Area') yield ('Dark Graveyard DMD Clip', 'West Dark Death Mountain (Bottom)', 'Dark Graveyard Area') - yield ('EDDM West Dropdown Clip', 'East Dark Death Mountain (Top)', 'East Dark Death Mountain (West Lip)') + yield ('EDDM West Dropdown Clip', 'East Dark Death Mountain (Top)', 'East Dark Death Mountain (Bottom Left)') yield ('EDDM To WDDM Clip', 'East Dark Death Mountain (Top)', 'West Dark Death Mountain (Top)') yield ('TR Bridge Clip', 'East Dark Death Mountain (Top)', 'Dark Death Mountain Ledge') yield ('Dark Witch DMD FAWT Clip', 'East Dark Death Mountain (Bottom)', 'Dark Witch Area') - yield ('Qirn Jump DMD Clip', 'East Dark Death Mountain (West Lip)', 'Qirn Jump Area') - yield ('WDDM To EDDM Clip', 'East Dark Death Mountain (West Lip)', 'East Dark Death Mountain (Bottom)') + yield ('Qirn Jump DMD Clip', 'East Dark Death Mountain (Bottom Left)', 'Qirn Jump Area') + yield ('WDDM To EDDM Clip', 'East Dark Death Mountain (Bottom Left)', 'East Dark Death Mountain (Bottom)') #yield ('DW Floating Island Clip', 'East Dark Death Mountain (Bottom)', 'Dark Death Mountain Floating Island') #cannot guarantee camera correction yield ('TR To EDDM Clip', 'Turtle Rock Area', 'East Dark Death Mountain (Top)') yield ('Catfish DMD Clip', 'Turtle Rock Area', 'Catfish Area') @@ -312,8 +312,8 @@ def get_mirror_clip_spots_dw(): """ Out of bounds transitions using the mirror """ - yield ('Qirn Jump Bunny DMD Clip', 'East Dark Death Mountain (West Lip)', 'Qirn Jump Area') - yield ('EDDM Mirror Clip', 'East Dark Death Mountain (West Lip)', 'East Dark Death Mountain (Bottom)') + yield ('Qirn Jump Bunny DMD Clip', 'East Dark Death Mountain (Bottom Left)', 'Qirn Jump Area') + yield ('EDDM Mirror Clip', 'East Dark Death Mountain (Bottom Left)', 'East Dark Death Mountain (Bottom)') yield ('Desert East Mirror Clip', 'Misery Mire Area', 'Desert Palace Mouth') diff --git a/OverworldShuffle.py b/OverworldShuffle.py index 5ad3a08e..8207d66b 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -1,8 +1,9 @@ import RaceRandom as random, logging, copy from BaseClasses import OWEdge, WorldType, RegionType, Direction, Terrain, PolSlot, Entrance -from OWEdges import OWTileRegions, OWTileGroups, OWEdgeGroups, OpenStd, parallel_links, IsParallel +from Regions import mark_dark_world_regions, mark_light_world_regions +from OWEdges import OWTileRegions, OWTileGroups, OWEdgeGroups, OWExitTypes, OpenStd, parallel_links, IsParallel -__version__ = '0.1.9.4-u' +__version__ = '0.2.1.2-u' def link_overworld(world, player): # setup mandatory connections @@ -146,6 +147,8 @@ def link_overworld(world, player): for (exitname, regionname) in ow_connections[owid][1]: connect_simple(world, exitname, regionname, player) + categorize_world_regions(world, player) + # crossed shuffle logging.getLogger('').debug('Crossing overworld edges') if world.owCrossed[player] in ['grouped', 'limited', 'chaos']: @@ -169,7 +172,7 @@ def link_overworld(world, player): if world.owCrossed[player] == 'chaos' and random.randint(0, 1): crossed_edges.append(edge) elif world.owCrossed[player] == 'limited': - crossed_candidates.append(edge) + crossed_candidates.append([edge]) if world.owCrossed[player] == 'limited': random.shuffle(crossed_candidates) for edge_set in crossed_candidates[:9]: @@ -184,6 +187,44 @@ def link_overworld(world, player): trimmed_groups = performSwap(trimmed_groups, crossed_edges) assert len(crossed_edges) == 0, 'Not all edges were crossed successfully: ' + ', '.join(crossed_edges) + # whirlpool shuffle + logging.getLogger('').debug('Shuffling whirlpools') + + if not world.owWhirlpoolShuffle[player]: + for (_, from_whirlpool, from_region), (_, to_whirlpool, to_region) in default_whirlpool_connections: + connect_simple(world, from_whirlpool, to_region, player) + connect_simple(world, to_whirlpool, from_region, player) + else: + whirlpool_candidates = [[],[]] + for (from_owid, from_whirlpool, from_region), (to_owid, to_whirlpool, to_region) in default_whirlpool_connections: + if world.owCrossed[player] != 'none': + whirlpool_candidates[0].append(tuple((from_owid, from_whirlpool, from_region))) + whirlpool_candidates[0].append(tuple((to_owid, to_whirlpool, to_region))) + else: + if world.get_region(from_region, player).type == RegionType.LightWorld: + whirlpool_candidates[0].append(tuple((from_owid, from_whirlpool, from_region))) + else: + whirlpool_candidates[1].append(tuple((from_owid, from_whirlpool, from_region))) + + if world.get_region(to_region, player).type == RegionType.LightWorld: + whirlpool_candidates[0].append(tuple((to_owid, to_whirlpool, to_region))) + else: + whirlpool_candidates[1].append(tuple((to_owid, to_whirlpool, to_region))) + + # shuffle happens here + world.owwhirlpools[player] = [None] * 8 + whirlpool_map = [ 0x35, 0x0f, 0x15, 0x33, 0x12, 0x3f, 0x55, 0x7f ] + for whirlpools in whirlpool_candidates: + random.shuffle(whirlpools) + while len(whirlpools): + from_owid, from_whirlpool, from_region = whirlpools.pop() + to_owid, to_whirlpool, to_region = whirlpools.pop() + connect_simple(world, from_whirlpool, to_region, player) + connect_simple(world, to_whirlpool, from_region, player) + world.owwhirlpools[player][next(i for i, v in enumerate(whirlpool_map) if v == to_owid)] = from_owid + world.owwhirlpools[player][next(i for i, v in enumerate(whirlpool_map) if v == from_owid)] = to_owid + world.spoiler.set_overworld(from_whirlpool, to_whirlpool, 'both', player) + # layout shuffle logging.getLogger('').debug('Shuffling overworld layout') connected_edges = [] @@ -253,6 +294,8 @@ def link_overworld(world, player): logging.getLogger('').warning("Edge '%s' could not find a valid connection" % back_set[0]) assert len(connected_edges) == len(default_connections) * 2, connected_edges + # TODO: Reshuffle some areas if impossible to reach, exception if non-dungeon ER enabled or if area is LW with no portal and flute shuffle is enabled + # flute shuffle def connect_flutes(flute_destinations): for o in range(0, len(flute_destinations)): @@ -276,7 +319,7 @@ def link_overworld(world, player): region = world.get_region(regionname, player) for exit in region.exits: if exit.connected_region is not None and exit.connected_region.type in [RegionType.LightWorld, RegionType.DarkWorld] and exit.connected_region.name not in new_ignored: - if OWTileRegions[exit.connected_region.name] in [base_owid, owid] or OWTileRegions[regionname] == base_owid: + if exit.connected_region.name in OWTileRegions and (OWTileRegions[exit.connected_region.name] in [base_owid, owid] or OWTileRegions[regionname] == base_owid): new_ignored.add(exit.connected_region.name) getIgnored(exit.connected_region.name, base_owid, OWTileRegions[exit.connected_region.name]) @@ -382,22 +425,33 @@ def connect_two_way(world, edgename1, edgename2, player, connected_edges=None): def shuffle_tiles(world, groups, result_list, player): swapped_edges = list() + valid_whirlpool_parity = False - # tile shuffle happens here - removed = list() - for group in groups.keys(): - if random.randint(0, 1): - removed.append(group) - - # save shuffled tiles to list - for group in groups.keys(): - if group not in removed: - (owids, lw_regions, dw_regions) = groups[group] - (exist_owids, exist_lw_regions, exist_dw_regions) = result_list - exist_owids.extend(owids) - exist_lw_regions.extend(lw_regions) - exist_dw_regions.extend(dw_regions) - result_list = [exist_owids, exist_lw_regions, exist_dw_regions] + while not valid_whirlpool_parity: + # tile shuffle happens here + removed = list() + for group in groups.keys(): + # if group[0] in ['Links', 'Central Bonk Rocks', 'Castle']: # TODO: Standard + Inverted + if random.randint(0, 1): + removed.append(group) + + # save shuffled tiles to list + new_results = [[],[],[]] + for group in groups.keys(): + if group not in removed: + (owids, lw_regions, dw_regions) = groups[group] + (exist_owids, exist_lw_regions, exist_dw_regions) = new_results + exist_owids.extend(owids) + exist_lw_regions.extend(lw_regions) + exist_dw_regions.extend(dw_regions) + + # check whirlpool parity + valid_whirlpool_parity = world.owCrossed[player] != 'none' or len(set(new_results[0]) & set({0x0f, 0x12, 0x15, 0x33, 0x35, 0x3f, 0x55, 0x7f})) % 2 == 0 + + (exist_owids, exist_lw_regions, exist_dw_regions) = result_list + exist_owids.extend(new_results[0]) + exist_lw_regions.extend(new_results[1]) + exist_dw_regions.extend(new_results[2]) # replace LW edges with DW ignore_list = list() #TODO: Remove ignore_list when special OW areas are included in pool @@ -421,34 +475,62 @@ def shuffle_tiles(world, groups, result_list, player): def reorganize_tile_groups(world, player): groups = {} - for (name, groupType) in OWTileGroups.keys(): - if world.mode[player] != 'standard' or name not in ['Castle', 'Links', 'Central Bonk Rocks']: - if world.shuffle[player] in ['vanilla', 'simple', 'dungeonssimple']: - groups[(name,)] = ([], [], []) + for (name, groupType, whirlpoolGroup) in OWTileGroups.keys(): + if world.mode[player] != 'standard' or name not in ['Castle', 'Links', 'Central Bonk Rocks'] \ + or (world.mode[player] == 'standard' and world.shuffle[player] in ['lean', 'crossed', 'insanity'] and name == 'Castle' and groupType == 'Entrance'): + if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'simple', 'restricted']: + if world.owWhirlpoolShuffle[player] or world.owCrossed[player] != 'none': + groups[(name, whirlpoolGroup)] = ([], [], []) + else: + groups[(name,)] = ([], [], []) else: - groups[(name, groupType)] = ([], [], []) + if world.owWhirlpoolShuffle[player] or world.owCrossed[player] != 'none': + groups[(name, groupType, whirlpoolGroup)] = ([], [], []) + else: + groups[(name, groupType)] = ([], [], []) - for (name, groupType) in OWTileGroups.keys(): - if world.mode[player] != 'standard' or name not in ['Castle', 'Links', 'Central Bonk Rocks']: - (lw_owids, dw_owids) = OWTileGroups[(name, groupType,)] - if world.shuffle[player] in ['vanilla', 'simple', 'dungeonssimple']: - (exist_owids, exist_lw_regions, exist_dw_regions) = groups[(name,)] - exist_owids.extend(lw_owids) - exist_owids.extend(dw_owids) - for owid in lw_owids: - exist_lw_regions.extend(OWTileRegions.inverse[owid]) - for owid in dw_owids: - exist_dw_regions.extend(OWTileRegions.inverse[owid]) - groups[(name,)] = (exist_owids, exist_lw_regions, exist_dw_regions) + for (name, groupType, whirlpoolGroup) in OWTileGroups.keys(): + if world.mode[player] != 'standard' or name not in ['Castle', 'Links', 'Central Bonk Rocks'] \ + or (world.mode[player] == 'standard' and world.shuffle[player] in ['lean', 'crossed', 'insanity'] and name == 'Castle' and groupType == 'Entrance'): + (lw_owids, dw_owids) = OWTileGroups[(name, groupType, whirlpoolGroup)] + if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'simple', 'restricted']: + if world.owWhirlpoolShuffle[player] or world.owCrossed[player] != 'none': + (exist_owids, exist_lw_regions, exist_dw_regions) = groups[(name, whirlpoolGroup)] + exist_owids.extend(lw_owids) + exist_owids.extend(dw_owids) + for owid in lw_owids: + exist_lw_regions.extend(OWTileRegions.inverse[owid]) + for owid in dw_owids: + exist_dw_regions.extend(OWTileRegions.inverse[owid]) + groups[(name, whirlpoolGroup)] = (exist_owids, exist_lw_regions, exist_dw_regions) + else: + (exist_owids, exist_lw_regions, exist_dw_regions) = groups[(name,)] + exist_owids.extend(lw_owids) + exist_owids.extend(dw_owids) + for owid in lw_owids: + exist_lw_regions.extend(OWTileRegions.inverse[owid]) + for owid in dw_owids: + exist_dw_regions.extend(OWTileRegions.inverse[owid]) + groups[(name,)] = (exist_owids, exist_lw_regions, exist_dw_regions) else: - (exist_owids, exist_lw_regions, exist_dw_regions) = groups[(name, groupType)] - exist_owids.extend(lw_owids) - exist_owids.extend(dw_owids) - for owid in lw_owids: - exist_lw_regions.extend(OWTileRegions.inverse[owid]) - for owid in dw_owids: - exist_dw_regions.extend(OWTileRegions.inverse[owid]) - groups[(name, groupType)] = (exist_owids, exist_lw_regions, exist_dw_regions) + if world.owWhirlpoolShuffle[player] or world.owCrossed[player] != 'none': + (exist_owids, exist_lw_regions, exist_dw_regions) = groups[(name, groupType, whirlpoolGroup)] + exist_owids.extend(lw_owids) + exist_owids.extend(dw_owids) + for owid in lw_owids: + exist_lw_regions.extend(OWTileRegions.inverse[owid]) + for owid in dw_owids: + exist_dw_regions.extend(OWTileRegions.inverse[owid]) + groups[(name, groupType, whirlpoolGroup)] = (exist_owids, exist_lw_regions, exist_dw_regions) + else: + (exist_owids, exist_lw_regions, exist_dw_regions) = groups[(name, groupType)] + exist_owids.extend(lw_owids) + exist_owids.extend(dw_owids) + for owid in lw_owids: + exist_lw_regions.extend(OWTileRegions.inverse[owid]) + for owid in dw_owids: + exist_dw_regions.extend(OWTileRegions.inverse[owid]) + groups[(name, groupType)] = (exist_owids, exist_lw_regions, exist_dw_regions) return groups def remove_reserved(world, groupedlist, connected_edges, player): @@ -646,11 +728,20 @@ def create_flute_exits(world, player): and (region.name not in world.owswaps[player][1] or region.name in world.owswaps[player][2])): exitname = 'Flute From ' + region.name exit = Entrance(region.player, exitname, region) + exit.spot_type = 'Flute' exit.access_rule = lambda state: state.can_flute(player) exit.connect(world.get_region('Flute Sky', player)) region.exits.append(exit) world.initialize_regions() +def categorize_world_regions(world, player): + for type in OWExitTypes: + for exitname in OWExitTypes[type]: + world.get_entrance(exitname, player).spot_type = type + + mark_light_world_regions(world, player) + mark_dark_world_regions(world, player) + def update_world_regions(world, player): if world.owMixed[player]: for name in world.owswaps[player][1]: @@ -658,6 +749,52 @@ def update_world_regions(world, player): for name in world.owswaps[player][2]: world.get_region(name, player).type = RegionType.LightWorld +def can_reach_smith(world, player): + from Items import ItemFactory + from BaseClasses import CollectionState + + invFlag = world.mode[player] == 'inverted' + + def explore_region(region_name, region=None): + nonlocal found + explored_regions.add(region_name) + if not found: + if not region: + region = world.get_region(region_name, player) + for exit in region.exits: + if not found and exit.connected_region is not None: + if any(map(lambda i: i.name == 'Ocarina', world.precollected_items)) and exit.spot_type == 'Flute': + fluteregion = exit.connected_region + for flutespot in fluteregion.exits: + if flutespot.connected_region and flutespot.connected_region.name not in explored_regions: + explore_region(flutespot.connected_region.name, flutespot.connected_region) + elif exit.connected_region.name not in explored_regions \ + and exit.connected_region.type in [RegionType.LightWorld, RegionType.DarkWorld] \ + and exit.access_rule(blank_state): + explore_region(exit.connected_region.name, exit.connected_region) + elif exit.name == 'Sanctuary S': + sanc_region = exit.connected_region + if len(sanc_region.exits) and sanc_region.exits[0].name == 'Sanctuary Exit': + explore_region(sanc_region.exits[0].connected_region.name, sanc_region.exits[0].connected_region) + elif exit.connected_region.name == 'Blacksmiths Hut' and exit.access_rule(blank_state): + found = True + + blank_state = CollectionState(world) + if world.mode[player] == 'standard': + blank_state.collect(ItemFactory('Zelda Delivered', player), True) + if world.logic[player] in ['noglitches', 'minorglitches'] and world.get_region('Frog Prison', player).type == (RegionType.DarkWorld if not invFlag else RegionType.LightWorld): + blank_state.collect(ItemFactory('Titans Mitts', player), True) + + found = False + explored_regions = set() + explore_region('Links House') + if not found: + if not invFlag: + explore_region('Sanctuary') + else: + explore_region('Dark Sanctuary Hint') + return found + test_connections = [ #('Links House ES', 'Octoballoon WS'), #('Links House NE', 'Lost Woods Pass SW') @@ -671,17 +808,7 @@ temporary_mandatory_connections = [ ] # these are connections that cannot be shuffled and always exist. They link together separate parts of the world we need to divide into regions -mandatory_connections = [# Whirlpool Connections - ('C Whirlpool', 'River Bend Water'), - ('River Bend Whirlpool', 'C Whirlpool Water'), - ('Lake Hylia Whirlpool', 'Zora Waterfall Water'), - ('Zora Whirlpool', 'Lake Hylia Water'), - ('Kakariko Pond Whirlpool', 'Octoballoon Water'), - ('Octoballoon Whirlpool', 'Kakariko Pond Area'), - ('Qirn Jump Whirlpool', 'Bomber Corner Water'), - ('Bomber Corner Whirlpool', 'Qirn Jump Water'), - - # Intra-tile OW Connections +mandatory_connections = [# Intra-tile OW Connections ('Lost Woods Bush (West)', 'Lost Woods East Area'), #pearl ('Lost Woods Bush (East)', 'Lost Woods West Area'), #pearl ('West Death Mountain Drop', 'West Death Mountain (Bottom)'), @@ -744,6 +871,7 @@ mandatory_connections = [# Whirlpool Connections ('Wooden Bridge Water Drop', 'Wooden Bridge Water'), #flippers ('Wooden Bridge Northeast Water Drop', 'Wooden Bridge Water'), #flippers ('Bat Cave Ledge Peg', 'Bat Cave Ledge'), #hammer + ('Bat Cave Ledge Peg (East)', 'Blacksmith Area'), #hammer ('Maze Race Game', 'Maze Race Prize'), #pearl ('Maze Race Ledge Drop', 'Maze Race Area'), ('Desert Palace Statue Move', 'Desert Palace Stairs'), #book @@ -813,7 +941,7 @@ mandatory_connections = [# Whirlpool Connections ('Grassy Lawn Pegs', 'Village of Outcasts Area'), #hammer ('Shield Shop Fence (Outer) Ledge Drop', 'Shield Shop Fence'), ('Shield Shop Fence (Inner) Ledge Drop', 'Shield Shop Area'), - ('Pyramid Exit Ledge Drop', 'Pyramid Area'), #hammer(inverted) + ('Pyramid Exit Ledge Drop', 'Pyramid Area'), ('Broken Bridge Hammer Rock (South)', 'Broken Bridge Northeast'), #hammer/glove ('Broken Bridge Hammer Rock (North)', 'Broken Bridge Area'), #hammer/glove ('Broken Bridge Hookshot Gap', 'Broken Bridge West'), #hookshot @@ -904,6 +1032,13 @@ mandatory_connections = [# Whirlpool Connections ('Dark Tree Line WC Cliff Water Drop', 'Dark Tree Line Water') #fake flipper ] +default_whirlpool_connections = [ + ((0x33, 'C Whirlpool', 'C Whirlpool Water'), (0x15, 'River Bend Whirlpool', 'River Bend Water')), + ((0x35, 'Lake Hylia Whirlpool', 'Lake Hylia Water'), (0x0f, 'Zora Whirlpool', 'Zora Waterfall Water')), + ((0x12, 'Kakariko Pond Whirlpool', 'Kakariko Pond Area'), (0x3f, 'Octoballoon Whirlpool', 'Octoballoon Water')), + ((0x55, 'Qirn Jump Whirlpool', 'Qirn Jump Water'), (0x7f, 'Bomber Corner Whirlpool', 'Bomber Corner Water')) +] + default_flute_connections = [ 0x0b, 0x16, 0x18, 0x2c, 0x2f, 0x38, 0x3b, 0x3f ] @@ -1071,6 +1206,7 @@ ow_connections = { ('HC Ledge Mirror Spot', 'Hyrule Castle Ledge'), ('HC Courtyard Mirror Spot', 'Hyrule Castle Courtyard'), ('HC Area Mirror Spot', 'Hyrule Castle Area'), + ('HC Courtyard Left Mirror Spot', 'Hyrule Castle Courtyard'), ('HC Area South Mirror Spot', 'Hyrule Castle Area'), ('HC East Entry Mirror Spot', 'Hyrule Castle East Entry'), ('Top of Pyramid', 'Pyramid Area'), @@ -1439,9 +1575,10 @@ flute_data = { 0x2e: (['Tree Line Area', 'Dark Tree Line Area'], 0x2e, 0x0100, 0x0a1a, 0x0c00, 0x0a78, 0x0c30, 0x0a87, 0x0c7d, 0x0006, 0x0000, 0x0a78, 0x0c58), 0x2f: (['Eastern Nook Area', 'Palace of Darkness Nook Area'], 0x2f, 0x0798, 0x0afa, 0x0eb2, 0x0b58, 0x0f30, 0x0b67, 0x0f37, 0xfff6, 0x000e, 0x0b50, 0x0f30), 0x38: (['Desert Palace Teleporter Ledge', 'Misery Mire Teleporter Ledge'], 0x30, 0x1880, 0x0f1e, 0x0000, 0x0fa8, 0x0078, 0x0f8d, 0x008d, 0x0000, 0x0000, 0x0fb0, 0x0070), - 0x32: (['Flute Boy Approach Area', 'Stumpy Approach Area'], 0x32, 0x03a0, 0x0c6c, 0x0500, 0x0cd0, 0x05a8, 0x0cdb, 0x0585, 0x0002, 0x0000, 0x0cd6, 0x05a8), + 0x32: (['Flute Boy Approach Area', 'Stumpy Approach Area'], 0x32, 0x03a0, 0x0c6c, 0x0500, 0x0cd0, 0x05a8, 0x0cdb, 0x0585, 0x0002, 0x0000, 0x0cd6, 0x0568), 0x33: (['C Whirlpool Outer Area', 'Dark C Whirlpool Outer Area'], 0x33, 0x0180, 0x0c20, 0x0600, 0x0c80, 0x0628, 0x0c8f, 0x067d, 0x0000, 0x0000, 0x0c80, 0x0628), 0x34: (['Statues Area', 'Hype Cave Area'], 0x34, 0x088e, 0x0d00, 0x0866, 0x0d60, 0x08d8, 0x0d6f, 0x08e3, 0x0000, 0x000a, 0x0d60, 0x08d8), + #0x35: (['Lake Hylia Area', 'Ice Lake Area'], 0x35, 0x0d00, 0x0da6, 0x0a06, 0x0e08, 0x0a80, 0x0e13, 0x0a8b, 0xfffa, 0xfffa, 0x0d88, 0x0a88), 0x3e: (['Lake Hylia South Shore', 'Ice Lake Ledge (East)'], 0x35, 0x1860, 0x0f1e, 0x0d00, 0x0f98, 0x0da8, 0x0f8b, 0x0d85, 0x0000, 0x0000, 0x0f90, 0x0da4), 0x37: (['Ice Cave Area', 'Shopping Mall Area'], 0x37, 0x0786, 0x0cf6, 0x0e2e, 0x0d58, 0x0ea0, 0x0d63, 0x0eab, 0x000a, 0x0002, 0x0d48, 0x0ed0), 0x3a: (['Desert Pass Area', 'Swamp Nook Area'], 0x3a, 0x001a, 0x0e08, 0x04c6, 0x0e70, 0x0540, 0x0e7d, 0x054b, 0x0006, 0x000a, 0x0e70, 0x0540), diff --git a/README.md b/README.md index 221c5987..e09e2069 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,8 @@ Every transition independently is a candidate to be chosen as a cross-world conn Note: Only parallel connections (a connection that also exists in the opposite world) are considered for cross-world connections, which means that the same connection in the opposite world will also connect cross-world. +Note: If Whirlpool Shuffle is enabled, those connections can be cross-world but do not count towards the 9 transitions that are crossed. + Motive: Why 9 connections? To imitate the effect of the 9 standard portals that exist. ### Chaos @@ -111,6 +113,10 @@ OW tiles are randomly chosen to become a part of the opposite world. When on the Note: Tiles are put into groups that must be shuffled together when certain settings are enabled. For instance, if ER is disabled, then any tiles that have a connector cave that leads to another tile, those tiles must swap together; (an exception to this is the Old Man Rescue cave which has been modified similar to how Inverted modifies it, Old Man Rescue is ALWAYS accessible from the Light World) +## Whirlpool Shuffle (--ow_whirlpool) + +When enabled, the whirlpool connections are shuffled. If Crossed OW is enabled, the whirlpools can also be cross-world as well. For Limited Crossed OW, this doesn't count towards the limited number of crossed edge transitions. + ## Flute Shuffle (--ow_fluteshuffle) When enabled, new flute spots are generated and gives the player the option to cancel out of the flute menu by pressing X. @@ -129,6 +135,42 @@ New flute spots are chosen at random, with restrictions that limit the promixity New flute spots are chosen at random with minimum bias. +## New Entrance Shuffle Options (--shuffle) + +### Lite + +This mode is intended to be a beginner-friendly introduction to playing ER. It focuses on reducing low% world traversal in late-game dungeons while reducing the number of entrances needing to be checked. + +This mode groups entrances into types and shuffles them freely within those groups. +- Dungeons and Connectors (Multi-Entrance Caves) +- Item Locations (Single-Entrance Caves with an item, includes Potion Shop and Red Bomb Shop, includes Shops only if Shopsanity is enabled) +- Dropdowns and their associated exits (Skull Woods dropdowns are handled the same as in Crossed) +- Non-item locations (junk locations) all remain vanilla + +Lite mode shuffles all connectors same-world, to limit bunny traversal. And to prevent Low% enemy and boss combat, some dungeons are confined to specific worlds. + +The following dungeons are guaranteed to be in the Light World: +- Hyrule Castle +- Eastern Palace +- Desert Palace +- Tower of Hera +- Agahnim's Tower + +The following are guaranteed to be in the Dark World: +- Ice Palace +- Misery Mire +- Turtle Rock +- Ganon's Tower + +### Lean + +This mode is intended to be a more refined and more competitive format to Crossed ER. It focuses on reducing the number of entrances needing to be checked, while giving the player unique routing options based on the entrance pools defined below, as opposed to mindlessly checking all the remaining entrances. The Dungeons/Connectors can connect cross-world. + +This mode groups entrances into types and shuffles them freely within those groups. +- Dungeons and Connectors (Multi-Entrance Caves) +- Item Locations (Single-Entrance Caves with an item, includes Potion Shop and Red Bomb Shop, includes Shops only if Shopsanity is enabled) +- Dropdowns and their associated exits (Skull Woods dropdowns are handled the same as in Crossed) +- Non-item locations (junk locations) all remain vanilla ## Ganon Vulnerability Item (--ganon_item) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 55a34c3f..7098107e 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -15,6 +15,14 @@ CLI: ```--bombbag``` # Bug Fixes and Notes. +* 0.5.1.4 + * Revert quadrant glitch fix for baserom + * Fix for inverted +* 0.5.1.3 + * Certain lobbies forbidden in standard when rupee bow is enabled + * PoD EG disarmed when mirroring (except in nologic) + * Fixed issue with key logic + * Updated baserom * 0.5.1.2 * Allowed Blind's Cell to be shuffled anywhere if Blind is not the boss of Thieves Town * Remove unique annotation from a FastEnum that was causing problems diff --git a/Regions.py b/Regions.py index ab494e12..26b3aeb7 100644 --- a/Regions.py +++ b/Regions.py @@ -70,7 +70,7 @@ def create_regions(world, player): create_lw_region(player, 'Eastern Palace Area', None, ['Sahasrahlas Hut', 'Eastern Palace', 'Palace of Darkness Mirror Spot', 'Eastern Palace SW', 'Eastern Palace SE']), create_lw_region(player, 'Eastern Cliff', None, ['Sand Dunes Ledge Drop', 'Stone Bridge East Ledge Drop', 'Tree Line Ledge Drop', 'Eastern Palace Ledge Drop']), create_lw_region(player, 'Blacksmith Area', None, ['Blacksmiths Hut', 'Bat Cave Cave', 'Bat Cave Ledge Peg', 'Hammer Pegs Mirror Spot', 'Hammer Pegs Entry Mirror Spot', 'Blacksmith WS']), - create_lw_region(player, 'Bat Cave Ledge', None, ['Bat Cave Drop']), + create_lw_region(player, 'Bat Cave Ledge', None, ['Bat Cave Ledge Peg (East)', 'Bat Cave Drop']), create_lw_region(player, 'Sand Dunes Area', None, ['Dark Dunes Mirror Spot', 'Sand Dunes NW', 'Sand Dunes WN', 'Sand Dunes SC']), create_lw_region(player, 'Maze Race Area', None, ['Dig Game Mirror Spot', 'Maze Race ES']), create_lw_region(player, 'Maze Race Ledge', None, ['Two Brothers House (West)', 'Maze Race Game', 'Dig Game Ledge Mirror Spot']), @@ -82,7 +82,7 @@ def create_regions(world, player): create_lw_region(player, 'Links House Area', None, ['Links House', 'Big Bomb Shop Mirror Spot', 'Links House NE', 'Links House WN', 'Links House WC', 'Links House WS', 'Links House SC', 'Links House ES']), create_lw_region(player, 'Stone Bridge Area', None, ['Hammer Bridge North Mirror Spot', 'Hammer Bridge South Mirror Spot', 'Stone Bridge NC', 'Stone Bridge EN', 'Stone Bridge WS', 'Stone Bridge SC']), create_lw_region(player, 'Stone Bridge Water', None, ['Dark Hobo Mirror Spot', 'Stone Bridge WC', 'Stone Bridge EC'], Terrain.Water), - create_lw_region(player, 'Hobo Bridge', ['Hobo'], ['Hobo EC']), + create_lw_region(player, 'Hobo Bridge', ['Hobo'], ['Hobo EC'], Terrain.Water), create_lw_region(player, 'Central Cliffs', None, ['Central Bonk Rocks Cliff Ledge Drop', 'Links House Cliff Ledge Drop', 'Stone Bridge Cliff Ledge Drop', 'Lake Hylia Area Cliff Ledge Drop', 'Lake Hylia Island FAWT Ledge Drop', 'Stone Bridge EC Cliff Water Drop', 'Tree Line WC Cliff Water Drop', 'C Whirlpool Outer Cliff Ledge Drop', 'C Whirlpool Cliff Ledge Drop', 'South Teleporter Cliff Ledge Drop', 'Statues Cliff Ledge Drop']), create_lw_region(player, 'Tree Line Area', None, ['Lake Hylia Fairy', 'Dark Tree Line Mirror Spot', 'Tree Line WN', 'Tree Line NW', 'Tree Line SE']), create_lw_region(player, 'Tree Line Water', None, ['Tree Line WC', 'Tree Line SC'], Terrain.Water), @@ -164,7 +164,7 @@ def create_regions(world, player): create_dw_region(player, 'Shield Shop Area', None, ['Shield Shop Fence (Outer) Ledge Drop', 'Forgotton Forest Mirror Spot', 'Shield Shop NW', 'Shield Shop NE']), create_dw_region(player, 'Shield Shop Fence', None, ['Shield Shop Fence (Inner) Ledge Drop', 'Red Shield Shop', 'Forgotton Forest Fence Mirror Spot']), create_dw_region(player, 'Pyramid Area', ['Pyramid'], ['Pyramid Fairy', 'Pyramid Hole', 'HC Ledge Mirror Spot', 'HC Courtyard Mirror Spot', 'HC Area Mirror Spot', 'HC East Entry Mirror Spot', 'Pyramid ES']), - create_dw_region(player, 'Pyramid Exit Ledge', None, ['Pyramid Exit Ledge Drop', 'Pyramid Entrance']), + create_dw_region(player, 'Pyramid Exit Ledge', None, ['Pyramid Exit Ledge Drop', 'HC Courtyard Left Mirror Spot', 'Pyramid Entrance']), create_dw_region(player, 'Pyramid Pass', None, ['Post Aga Inverted Teleporter', 'HC Area South Mirror Spot', 'Pyramid SW', 'Pyramid SE']), create_dw_region(player, 'Broken Bridge Area', None, ['Broken Bridge Hammer Rock (South)', 'Broken Bridge Water Drop', 'Wooden Bridge Mirror Spot', 'Broken Bridge SW']), create_dw_region(player, 'Broken Bridge Northeast', None, ['Broken Bridge Hammer Rock (North)', 'Broken Bridge Hookshot Gap', 'Broken Bridge Northeast Water Drop', 'Wooden Bridge Northeast Mirror Spot', 'Broken Bridge NE']), diff --git a/Rom.py b/Rom.py index e04785d9..50b2f8b4 100644 --- a/Rom.py +++ b/Rom.py @@ -33,7 +33,7 @@ from source.classes.SFX import randomize_sfx JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '3a88011d09104da542cb1204f981f0f0' +RANDOMIZERBASEHASH = '0a03e2d02ede95a7511fad81df24eea9' class JsonRom(object): @@ -659,10 +659,15 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): rom.write_byte(snes_to_pc(0x0AB793 + o), data[11] & 0xff) # Y low byte rom.write_byte(snes_to_pc(0x0AB79B + o), data[11] // 0x100) # Y high byte + # patch whirlpools + if world.owWhirlpoolShuffle[player]: + owFlags |= 0x01 + write_int16s(rom, snes_to_pc(0x02EA5C), world.owwhirlpools[player]) + # patch overworld edges inverted_buffer = [0] * 0x82 + owMode = 0 if world.owShuffle[player] != 'vanilla' or world.owCrossed[player] not in ['none', 'polar'] or world.owMixed[player]: - owMode = 0 if world.owShuffle[player] == 'parallel': owMode = 1 elif world.owShuffle[player] == 'full': @@ -676,12 +681,6 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): if world.owMixed[player]: owMode |= 0x400 - write_int16(rom, 0x150002, owMode) - - write_int16(rom, 0x150004, owFlags) - - rom.write_byte(0x18004C, 0x01) # patch for allowing Frogsmith to enter multi-entrance caves - # patches map data specific for OW Shuffle #inverted_buffer[0x03] = inverted_buffer[0x03] | 0x2 # convenient portal on WDM inverted_buffer[0x1A] = inverted_buffer[0x1A] | 0x2 # rocks added to prevent OWG hardlock @@ -707,6 +706,12 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): write_int16(rom, edge.getAddress() + 0x0a, edge.vramLoc) write_int16(rom, edge.getAddress() + 0x0e, edge.getTarget()) + write_int16(rom, 0x150002, owMode) + write_int16(rom, 0x150004, owFlags) + + from OverworldShuffle import can_reach_smith + if not can_reach_smith(world, player): + rom.write_byte(0x18005d, 0x01) # patch for deleting smith on S+Q # patch entrance/exits/holes for region in world.regions: @@ -786,6 +791,8 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): dr_flags |= DROptions.OriginalPalettes if world.experimental[player]: dr_flags |= DROptions.DarkWorld_Spawns + if world.logic[player] != 'nologic': + dr_flags |= DROptions.Fix_EG # fix hc big key problems (map and compass too) @@ -1706,7 +1713,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): rom.write_bytes(0x02F539, [0xEA, 0xEA, 0xEA, 0xEA, 0xEA] if world.powder_patch_required[player] else [0xAD, 0xBF, 0x0A, 0xF0, 0x4F]) # allow smith into multi-entrance caves in appropriate shuffles - if world.shuffle[player] in ['restricted', 'full', 'crossed', 'insanity'] or (world.shuffle[player] == 'simple' and world.mode[player] == 'inverted'): + if world.shuffle[player] in ['restricted', 'full', 'lite', 'lean', 'crossed', 'insanity'] or (world.shuffle[player] == 'simple' and world.mode[player] == 'inverted'): rom.write_byte(0x18004C, 0x01) # set correct flag for hera basement item @@ -2609,7 +2616,8 @@ def set_inverted_mode(world, player, rom, inverted_buffer): write_int16(rom, 0x15AEE + 2*0x25, 0x000C) if (world.mode[player] == 'inverted') != (0x03 in world.owswaps[player][0] and world.owMixed[player]): - if world.shuffle[player] in ['vanilla', 'dungeonsfull', 'dungeonssimple']: + if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull'] \ + or (world.shuffle[player] == 'simple' and (world.mode[player] == 'inverted' != (0x05 in world.owswaps[player][0] and world.owMixed[player]))): rom.write_bytes(snes_to_pc(0x308350), [0x00, 0x00, 0x01]) # mountain cave starts on OW write_int16(rom, snes_to_pc(0x02D8DE), 0x00F1) # change mountain cave spawn point to just outside old man cave @@ -2630,9 +2638,10 @@ def set_inverted_mode(world, player, rom, inverted_buffer): write_int16(rom, snes_to_pc(0x02D9B0), 0x0007) rom.write_byte(snes_to_pc(0x02D9B8), 0x12) - rom.write_bytes(0x180247, [0x00, 0x5A, 0x00, 0x00, 0x00, 0x00, 0x00]) #indicates the overworld door being used for the single entrance spawn point + rom.write_bytes(0x180247, [0x00, 0x5A, 0x00, 0x00, 0x00, 0x00, 0x00]) # indicates the overworld door being used for the single entrance spawn point if (world.mode[player] == 'inverted') != (0x05 in world.owswaps[player][0] and world.owMixed[player]): rom.write_bytes(snes_to_pc(0x1BC655), [0x4A, 0x1D, 0x82]) # add warp under rock + rom.write_byte(snes_to_pc(0x1BC428), 0x00) # remove secret portal if (world.mode[player] == 'inverted') != (0x07 in world.owswaps[player][0] and world.owMixed[player]): rom.write_bytes(snes_to_pc(0x1BC387), [0xDD, 0xD1]) # add warps under rocks rom.write_bytes(snes_to_pc(0x1BD1DD), [0xA4, 0x06, 0x82, 0x9E, 0x06, 0x82, 0xFF, 0xFF]) # add warps under rocks @@ -2642,6 +2651,7 @@ def set_inverted_mode(world, player, rom, inverted_buffer): world.fix_trock_doors[player] = True if (world.mode[player] == 'inverted') != (0x10 in world.owswaps[player][0] and world.owMixed[player]): rom.write_bytes(snes_to_pc(0x1BC67A), [0x2E, 0x0B, 0x82]) # add warp under rock + rom.write_byte(snes_to_pc(0x1BC43A), 0x00) # remove secret portal if (world.mode[player] == 'inverted') != (0x1B in world.owswaps[player][0] and world.owMixed[player]): write_int16(rom, 0x15AEE + 2 * 0x06, 0x0020) # post aga hyrule castle spawn rom.write_byte(0x15B8C + 0x06, 0x1B) @@ -2730,6 +2740,23 @@ def set_inverted_mode(world, player, rom, inverted_buffer): write_int16(rom, 0xDB96F + 2 * 0x35, 0x001B) # move pyramid exit door write_int16(rom, 0xDBA71 + 2 * 0x35, 0x011C) + + if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull']: + rom.write_byte(0xDBB73 + 0x35, 0x36) # move pyramid exit door + + write_int16(rom, 0x15AEE + 2 * 0x37, 0x0010) # pyramid exit to new hc area + rom.write_byte(0x15B8C + 0x37, 0x1B) + write_int16(rom, 0x15BDB + 2 * 0x37, 0x000E) + write_int16(rom, 0x15C79 + 2 * 0x37, 0x0600) + write_int16(rom, 0x15D17 + 2 * 0x37, 0x0676) + write_int16(rom, 0x15DB5 + 2 * 0x37, 0x0604) + write_int16(rom, 0x15E53 + 2 * 0x37, 0x06E8) + write_int16(rom, 0x15EF1 + 2 * 0x37, 0x066D) + write_int16(rom, 0x15F8F + 2 * 0x37, 0x06F3) + rom.write_byte(0x1602D + 0x37, 0x00) + rom.write_byte(0x1607C + 0x37, 0x0A) + write_int16(rom, 0x160CB + 2 * 0x37, 0x0000) + write_int16(rom, 0x16169 + 2 * 0x37, 0x811C) if (world.mode[player] == 'inverted') != (0x29 in world.owswaps[player][0] and world.owMixed[player]): rom.write_bytes(snes_to_pc(0x06B2AB), [0xF0, 0xE1, 0x05]) # frog pickup on contact if (world.mode[player] == 'inverted') != (0x2C in world.owswaps[player][0] and world.owMixed[player]): @@ -2739,13 +2766,17 @@ def set_inverted_mode(world, player, rom, inverted_buffer): rom.write_byte(0xDBB73 + 0x52, 0x01) if (world.mode[player] == 'inverted') != (0x2F in world.owswaps[player][0] and world.owMixed[player]): rom.write_bytes(snes_to_pc(0x1BC80D), [0xB2, 0x0B, 0x82]) # add warp under rock + rom.write_byte(snes_to_pc(0x1BC590), 0x00) # remove secret portal if (world.mode[player] == 'inverted') != (0x30 in world.owswaps[player][0] and world.owMixed[player]): rom.write_bytes(snes_to_pc(0x1BC81E), [0x94, 0x1D, 0x82]) # add warp under rock + rom.write_byte(snes_to_pc(0x1BC5A1), 0x00) # remove secret portal if (world.mode[player] == 'inverted') != (0x33 in world.owswaps[player][0] and world.owMixed[player]): rom.write_bytes(snes_to_pc(0x1BC3DF), [0xD8, 0xD1]) # add warp under rock rom.write_bytes(snes_to_pc(0x1BD1D8), [0xA8, 0x02, 0x82, 0xFF, 0xFF]) # add warp under rock + rom.write_byte(snes_to_pc(0x1BC5B1), 0x00) # remove secret portal if (world.mode[player] == 'inverted') != (0x35 in world.owswaps[player][0] and world.owMixed[player]): rom.write_bytes(snes_to_pc(0x1BC85A), [0x50, 0x0F, 0x82]) # add warp under rock + rom.write_byte(snes_to_pc(0x1BC5C7), 0x00) # remove secret portal # apply inverted map changes for b in range(0x00, len(inverted_buffer)): diff --git a/Rules.py b/Rules.py index 394adc39..86eff18f 100644 --- a/Rules.py +++ b/Rules.py @@ -24,7 +24,8 @@ def set_rules(world, player): ow_bunny_rules(world, player) if world.mode[player] == 'standard': - standard_rules(world, player) + if world.get_region('Big Bomb Shop', player).entrances: # just some location that is placed late in the ER algorithm, prevent standard rules from applying when trying to search reachability in the overworld + standard_rules(world, player) elif world.mode[player] == 'open' or world.mode[player] == 'inverted': open_rules(world, player) else: @@ -106,14 +107,15 @@ def mirrorless_path_to_castle_courtyard(world, player): queue = collections.deque([(start.connected_region, [])]) while queue: (current, path) = queue.popleft() - for entrance in current.exits: - if entrance.connected_region not in seen: - new_path = path + [entrance.access_rule] - if entrance.connected_region == target: - return new_path - else: - queue.append((entrance.connected_region, new_path)) - seen.add(entrance.connected_region) + if current: + for entrance in current.exits: + if entrance.connected_region not in seen: + new_path = path + [entrance.access_rule] + if entrance.connected_region == target: + return new_path + else: + queue.append((entrance.connected_region, new_path)) + seen.add(entrance.connected_region) def set_rule(spot, rule): spot.access_rule = rule @@ -340,7 +342,12 @@ def global_rules(world, player): set_rule(world.get_entrance('Mire Lobby Gap', player), lambda state: state.has_Boots(player) or state.has('Hookshot', player)) set_rule(world.get_entrance('Mire Post-Gap Gap', player), lambda state: state.has_Boots(player) or state.has('Hookshot', player)) set_rule(world.get_entrance('Mire Falling Bridge WN', player), lambda state: state.has_Boots(player) or state.has('Hookshot', player)) # this is due to the fact the the door opposite is blocked - set_rule(world.get_entrance('Mire 2 NE', player), lambda state: state.bomb_mode_check(player, 1) and (state.has_real_sword(player) or state.has('Fire Rod', player) or state.has('Ice Rod', player) or state.has('Hammer', player) or state.has('Cane of Somaria', player) or state.can_shoot_arrows(player) or state.has_bomb_level(player, 1))) # need to defeat wizzrobes, bombs don't work ... + set_rule(world.get_entrance('Mire 2 NE', player), lambda state: state.bomb_mode_check(player, 1) and + (state.has_real_sword(player) or + (state.has('Fire Rod', player) and (state.can_use_bombs(player) or state.can_extend_magic(player, 9))) or # 9 fr shots or 8 with some bombs + (state.has('Ice Rod', player) and state.can_use_bombs(player)) or # freeze popo and throw, bomb to finish + state.has('Hammer', player) or state.has('Cane of Somaria', player) or state.can_shoot_arrows(player) or state.has_bomb_level(player, 1))) # need to defeat wizzrobes, bombs don't work ... + # byrna could work with sufficient magic set_rule(world.get_location('Misery Mire - Spike Chest', player), lambda state: (state.world.can_take_damage and state.has_hearts(player, 4)) or state.has('Cane of Byrna', player) or state.has('Cape', player)) set_rule(world.get_entrance('Mire Left Bridge Hook Path', player), lambda state: state.has('Hookshot', player)) set_rule(world.get_entrance('Mire Tile Room NW', player), lambda state: state.has_fire_source(player)) @@ -771,6 +778,7 @@ def default_rules(world, player): set_rule(world.get_entrance('Hyrule Castle Inner East Rock', player), lambda state: state.can_lift_rocks(player)) set_rule(world.get_entrance('Hyrule Castle Outer East Rock', player), lambda state: state.can_lift_rocks(player)) set_rule(world.get_entrance('Bat Cave Ledge Peg', player), lambda state: state.has('Hammer', player)) + set_rule(world.get_entrance('Bat Cave Ledge Peg (East)', player), lambda state: state.has('Hammer', player)) set_rule(world.get_entrance('Desert Palace Statue Move', player), lambda state: state.has('Book of Mudora', player)) set_rule(world.get_entrance('Desert Ledge Outer Rocks', player), lambda state: state.can_lift_rocks(player)) set_rule(world.get_entrance('Desert Ledge Inner Rocks', player), lambda state: state.can_lift_rocks(player)) @@ -857,7 +865,7 @@ def ow_rules(world, player): if world.mode[player] != 'inverted': set_rule(world.get_entrance('Agahnims Tower', player), lambda state: state.has('Cape', player) or state.has_sword(player, 2) or state.has('Beat Agahnim 1', player)) # barrier gets removed after killing agahnim, relevant for entrance shuffle set_rule(world.get_entrance('GT Entry Approach', player), lambda state: state.has_crystals(world.crystals_needed_for_gt[player], player)) - set_rule(world.get_entrance('GT Entry Leave', player), lambda state: state.has_crystals(world.crystals_needed_for_gt[player], player) or state.world.shuffle[player] in ('restricted', 'full', 'crossed', 'insanity')) + set_rule(world.get_entrance('GT Entry Leave', player), lambda state: state.has_crystals(world.crystals_needed_for_gt[player], player) or state.world.shuffle[player] in ('restricted', 'full', 'lite', 'lean', 'crossed', 'insanity')) else: set_rule(world.get_entrance('Agahnims Tower', player), lambda state: state.has_crystals(world.crystals_needed_for_gt[player], player)) @@ -1017,6 +1025,7 @@ def ow_rules(world, player): set_rule(world.get_entrance('HC Ledge Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('HC Courtyard Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('HC East Entry Mirror Spot', player), lambda state: state.has_Mirror(player)) + set_rule(world.get_entrance('HC Courtyard Left Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('HC Area South Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Top of Pyramid', player), lambda state: state.has('Beat Agahnim 1', player)) set_rule(world.get_entrance('Top of Pyramid (Inner)', player), lambda state: state.has('Beat Agahnim 1', player)) @@ -1263,6 +1272,7 @@ def ow_bunny_rules(world, player): add_bunny_rule(world.get_entrance('Wooden Bridge Bush (North)', player), player) add_bunny_rule(world.get_entrance('Wooden Bridge Bush (South)', player), player) add_bunny_rule(world.get_entrance('Bat Cave Ledge Peg', player), player) + add_bunny_rule(world.get_entrance('Bat Cave Ledge Peg (East)', player), player) add_bunny_rule(world.get_entrance('Desert Ledge Outer Rocks', player), player) add_bunny_rule(world.get_entrance('Desert Ledge Inner Rocks', player), player) add_bunny_rule(world.get_entrance('Flute Boy Bush (North)', player), player) @@ -1356,7 +1366,8 @@ def no_glitches_rules(world, player): # add_rule(world.get_location(location, player), lambda state: state.has('Hookshot', player)) set_rule(world.get_entrance('Paradox Cave Push Block Reverse', player), lambda state: False) # no glitches does not require block override forbid_bomb_jump_requirements(world, player) - add_conditional_lamps(world, player) + if world.get_region('Big Bomb Shop', player).entrances: # just some location that is placed late in the ER algorithm, prevent underworld rules from applying when trying to search reachability in the overworld + add_conditional_lamps(world, player) def fake_flipper_rules(world, player): @@ -1895,7 +1906,7 @@ def set_big_bomb_rules(world, player): add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Flippers', player) or state.can_flute(player))) #TODO: Fix red bomb rules, artifically adding a bunch of rules to help reduce unbeatable seeds in OW shuffle - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: False) + set_rule(world.get_entrance('Pyramid Fairy', player), lambda state: False) #add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.can_reach('Pyramid Area', 'Region', player)) #add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.can_lift_heavy_rocks(player) and state.has('Flippers', player) and state.can_flute(player) and state.has('Hammer', player) and state.has('Hookshot', player) and state.has_Pearl(player) and state.has_Mirror(player))) @@ -2092,8 +2103,8 @@ def set_inverted_big_bomb_rules(world, player): else: raise Exception('No logic found for routing from %s to the pyramid.' % bombshop_entrance.name) - if world.owShuffle[player] != 'vanilla' or world.owMixed[player] or world.owCrossed[player] != 'none': - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: False) #temp disable progression until routing to Pyramid get be guaranteed + if world.owShuffle[player] != 'vanilla' or world.owMixed[player] or world.owCrossed[player] not in ['none', 'polar']: + set_rule(world.get_entrance('Pyramid Fairy', player), lambda state: False) #temp disable progression until routing to Pyramid get be guaranteed def set_bunny_rules(world, player, inverted): @@ -2232,7 +2243,7 @@ def set_bunny_rules(world, player, inverted): for ent_name in bunny_impassible_doors: bunny_exit = world.get_entrance(ent_name, player) - if is_bunny(bunny_exit.parent_region): + if bunny_exit.connected_region and is_bunny(bunny_exit.parent_region): add_rule(bunny_exit, get_rule_to_add(bunny_exit.parent_region)) doors_to_check = [x for x in world.doors if x.player == player and x not in bunny_impassible_doors] @@ -2369,7 +2380,11 @@ def add_key_logic_rules(world, player): key_logic = world.key_logic[player] for d_name, d_logic in key_logic.items(): for door_name, rule in d_logic.door_rules.items(): - add_rule(world.get_entrance(door_name, player), eval_small_key_door(door_name, d_name, player)) + door_entrance = world.get_entrance(door_name, player) + add_rule(door_entrance, eval_small_key_door(door_name, d_name, player)) + if door_entrance.door.dependents: + for dep in door_entrance.door.dependents: + add_rule(dep.entrance, eval_small_key_door(door_name, d_name, player)) for location in d_logic.bk_restricted: if not location.forced_item: forbid_item(location, d_logic.bk_name, player) diff --git a/Utils.py b/Utils.py index 0d259936..8a4eabcf 100644 --- a/Utils.py +++ b/Utils.py @@ -351,16 +351,6 @@ def update_deprecated_args(args): if "create_rom" in argVars: args.suppress_rom = not args.create_rom in truthy - # Shuffle Ganon defaults to TRUE - # Don't do: Yes - # Do: No - if "no_shuffleganon" in argVars: - args.shuffleganon = not args.no_shuffleganon in truthy - # Don't do: No - # Do: Yes - if "shuffleganon" in argVars: - args.no_shuffleganon = not args.shuffleganon in truthy - # Playthrough defaults to TRUE # Don't do: Yes # Do: No diff --git a/asm/drhooks.asm b/asm/drhooks.asm index 86ae1ff1..1d3b485b 100644 --- a/asm/drhooks.asm +++ b/asm/drhooks.asm @@ -125,6 +125,11 @@ org $07a955 ; <- Bank07.asm : around 6564 (JP is a bit different) (STZ $05FC : S jsl BlockEraseFix nop #2 +org $02A0A8 +Mirror_SaveRoomData: +org $07A95B ; < bank_07.asm ; #_07A95B: JSL Mirror_SaveRoomData +jsl EGFixOnMirror + org $02b82a jsl FixShopCode diff --git a/asm/normal.asm b/asm/normal.asm index aabb24de..3bdf9622 100644 --- a/asm/normal.asm +++ b/asm/normal.asm @@ -73,8 +73,9 @@ TrapDoorFixer: rts Cleanup: - stz $047a - inc $11 + lda.l DRFlags : and #$10 : beq + + stz $047a + + inc $11 lda $ef rts diff --git a/asm/overrides.asm b/asm/overrides.asm index a041ae30..91029a8a 100644 --- a/asm/overrides.asm +++ b/asm/overrides.asm @@ -47,6 +47,12 @@ MirrorCheckOverride: rtl + lda.l DRScroll : rtl +EGFixOnMirror: + lda.l DRFlags : and #$10 : beq + + stz $047a + + jsl Mirror_SaveRoomData + rtl + BlockEraseFix: lda $7ef353 : and #$02 : beq + stz $05fc : stz $05fd diff --git a/asm/owrando.asm b/asm/owrando.asm index 8082d38f..d4a5ebd1 100644 --- a/asm/owrando.asm +++ b/asm/owrando.asm @@ -14,6 +14,10 @@ jsl OWEdgeTransition : nop #4 ;LDA $02A4E3,X : ORA $7EF3CA ;org $02e238 ;LDX #$9E : - DEX : DEX : CMP $DAEE,X : BNE - ;jsl OWSpecialTransition : nop #5 +; whirlpool shuffle cross world change +org $02b3bd +jsl OWWhirlpoolUpdate ;JSL $02EA6C + ; flute menu cancel org $0ab7af ;LDA $F2 : ORA $F0 : AND #$C0 jml OWFluteCancel2 : nop @@ -123,6 +127,14 @@ OWWorldCheck16: plx : and.w #$00ff : rtl } +OWWhirlpoolUpdate: +{ + jsl $02ea6c ; what we wrote over + lda.l OWFlags : and #$01 : beq + + ldx $8a : jsr OWWorldUpdate + + rtl +} + OWFluteCancel: { lda.l OWFlags+1 : and #$01 : bne + @@ -341,32 +353,39 @@ OWNewDestination: sep #$30 : lda OWOppSlotOffset,y : !add $04 : asl : and #$7f : sta $700 ; crossed OW shuffle - LDA.l OWMode+1 : AND.b #!FLAG_OW_CROSSED : beq .return - ldx $05 : lda.l OWTileWorldAssoc,x : cmp.l $7ef3ca : beq .return - sta.l $7ef3ca ; change world - lda #$38 : sta $012f ; play sfx - #$3b is an alternative - - ; toggle bunny mode - + lda $7ef357 : bne .nobunny - lda.l InvertedMode : bne .inverted - lda $7ef3ca : and.b #$40 : bra + - .inverted lda $7ef3ca : and.b #$40 : eor #$40 - + cmp #$40 : bne .nobunny - ; turn into bunny - lda $5d : cmp #$04 : beq + ; if swimming, continue - lda #$17 : sta $5d - + lda #$01 : sta $02e0 : sta $56 - bra .return - - .nobunny - lda $5d : cmp #$17 : bne + ; retain current state unless bunny - stz $5d - + stz $02e0 : stz $56 + lda.l OWMode+1 : and.b #!FLAG_OW_CROSSED : beq .return + ldx $05 : jsr OWWorldUpdate .return lda $05 : sta $8a rep #$30 : rts } +OWWorldUpdate: ; x = owid of destination screen +{ + lda.l OWTileWorldAssoc,x : cmp.l $7ef3ca : beq .return + sta.l $7ef3ca ; change world + lda #$38 : sta $012f ; play sfx - #$3b is an alternative + + ; toggle bunny mode + + lda $7ef357 : bne .nobunny + lda.l InvertedMode : bne .inverted + lda $7ef3ca : and.b #$40 : bra + + .inverted lda $7ef3ca : and.b #$40 : eor #$40 + + cmp #$40 : bne .nobunny + ; turn into bunny + lda $5d : cmp #$04 : beq + ; if swimming, continue + lda #$17 : sta $5d + + lda #$01 : sta $02e0 : sta $56 + bra .return + + .nobunny + lda $5d : cmp #$17 : bne + ; retain current state unless bunny + stz $5d + + stz $02e0 : stz $56 + + .return + rts +} OWSpecialTransition: { LDX #$9E @@ -531,7 +550,7 @@ OWNorthEdges: ; Min Max Width Mid OW Slot/OWID VRAM *FREE* Dest Index dw $00a0, $00a0, $0000, $00a0, $0000, $0000, $0000, $0040 ;Lost Woods dw $0458, $0540, $00e8, $04cc, $0a0a, $0000, $0000, $0000 -dw $0f70, $0f90, $0020, $0f80, $0f0f, $0000, $0000, $0041 +dw $0f38, $0f60, $0028, $0f4c, $0f0f, $0000, $0000, $0041 dw $0058, $0058, $0000, $0058, $1010, $0000, $0000, $0001 dw $0178, $0178, $0000, $0178, $1010, $0000, $0000, $0002 dw $0388, $0388, $0000, $0388, $1111, $0000, $0000, $0003 diff --git a/data/base2current.bps b/data/base2current.bps index 36f2eddd..9852386c 100644 Binary files a/data/base2current.bps and b/data/base2current.bps differ diff --git a/mystery_example.yml b/mystery_example.yml index 9d1d1501..3ac5950b 100644 --- a/mystery_example.yml +++ b/mystery_example.yml @@ -15,6 +15,9 @@ overworld_swap: on: 1 off: 1 + whirlpool_shuffle: + on: 1 + off: 1 flute_shuffle: vanilla: 0 balanced: 1 @@ -46,8 +49,15 @@ simple: 2 restricted: 2 full: 2 + lite: 2 crossed: 3 insanity: 1 + shufflelinks: + on: 1 + off: 1 + shuffleganon: + on: 1 + off: 1 world_state: standard: 1 open: 1 @@ -81,6 +91,7 @@ off: 1 glitches_required: none: 1 + owg: 0 no_logic: 0 accessibility: items: 1 diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index 5a956c8a..b6c56165 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -136,6 +136,10 @@ "action": "store_true", "type": "bool" }, + "ow_whirlpool": { + "action": "store_true", + "type": "bool" + }, "ow_fluteshuffle": { "choices": [ "vanilla", @@ -149,6 +153,8 @@ "simple", "restricted", "full", + "lite", + "lean", "crossed", "insanity", "dungeonsfull", diff --git a/resources/app/cli/lang/en.json b/resources/app/cli/lang/en.json index 87f7136b..ba6e7ef0 100644 --- a/resources/app/cli/lang/en.json +++ b/resources/app/cli/lang/en.json @@ -178,14 +178,18 @@ ], "shuffle": [ "Select Entrance Shuffling Algorithm. (default: %(default)s)", - "Full: Mix cave and dungeon entrances freely while limiting", - " multi-entrance caves to one world.", "Simple: Shuffle Dungeon Entrances/Exits between each other", " and keep all 4-entrance dungeons confined to one", " location. All caves outside of death mountain are", " shuffled in pairs and matched by original type.", "Restricted: Use Dungeons shuffling from Simple but freely", " connect remaining entrances.", + "Full: Mix cave and dungeon entrances freely while limiting", + " multi-entrance caves to one world.", + "Lite: Entrances are put into separate pools based on their", + " vanilla location. Dungeons, dropdowns, connector caves,", + " and locations that have an item are shuffled from", + " individual pools. Non-item locations remain vanilla.", "Crossed: Mix cave and dungeon entrances freely while allowing", " caves to cross between worlds.", "Insanity: Decouple entrances and exits from each other and", @@ -222,6 +226,9 @@ "ow_mixed": [ "Overworld tiles are randomly chosen to become part of the opposite world." ], + "ow_whirlpool": [ + "Whirlpools will be shuffled and paired together." + ], "ow_fluteshuffle": [ "This randomizes the flute spot destinations.", "Vanilla: All flute spots remain unchanged.", diff --git a/resources/app/gui/lang/en.json b/resources/app/gui/lang/en.json index bfcea394..0adb21ce 100644 --- a/resources/app/gui/lang/en.json +++ b/resources/app/gui/lang/en.json @@ -123,9 +123,13 @@ "randomizer.overworld.crossed.grouped": "Grouped", "randomizer.overworld.crossed.limited": "Limited", "randomizer.overworld.crossed.chaos": "Chaos", + "randomizer.overworld.keepsimilar": "Keep Similar Edges Together", + "randomizer.overworld.mixed": "Tile Swap (Mixed)", + "randomizer.overworld.whirlpool": "Whirlpool Shuffle", + "randomizer.overworld.overworldflute": "Flute Shuffle", "randomizer.overworld.overworldflute.vanilla": "Vanilla", "randomizer.overworld.overworldflute.balanced": "Balanced", @@ -140,6 +144,8 @@ "randomizer.entrance.entranceshuffle.simple": "Simple", "randomizer.entrance.entranceshuffle.restricted": "Restricted", "randomizer.entrance.entranceshuffle.full": "Full", + "randomizer.entrance.entranceshuffle.lite": "Lite", + "randomizer.entrance.entranceshuffle.lean": "Lean", "randomizer.entrance.entranceshuffle.crossed": "Crossed", "randomizer.entrance.entranceshuffle.insanity": "Insanity", "randomizer.entrance.entranceshuffle.restricted_legacy": "Restricted (Legacy)", diff --git a/resources/app/gui/randomize/entrando/widgets.json b/resources/app/gui/randomize/entrando/widgets.json index ffeeb976..ff30506b 100644 --- a/resources/app/gui/randomize/entrando/widgets.json +++ b/resources/app/gui/randomize/entrando/widgets.json @@ -10,6 +10,8 @@ "simple", "restricted", "full", + "lite", + "lean", "crossed", "insanity", "dungeonsfull", diff --git a/resources/app/gui/randomize/overworld/widgets.json b/resources/app/gui/randomize/overworld/widgets.json index faf4a100..f6751bc2 100644 --- a/resources/app/gui/randomize/overworld/widgets.json +++ b/resources/app/gui/randomize/overworld/widgets.json @@ -24,6 +24,10 @@ "type": "checkbox", "default": true }, + "whirlpool": { + "type": "checkbox", + "default": false + }, "overworldflute": { "type": "selectbox", "default": "vanilla", diff --git a/source/classes/constants.py b/source/classes/constants.py index 1e2320b9..f405a6c0 100644 --- a/source/classes/constants.py +++ b/source/classes/constants.py @@ -80,6 +80,7 @@ SETTINGSTOPROCESS = { "crossed": "ow_crossed", "keepsimilar": "ow_keepsimilar", "mixed": "ow_mixed", + "whirlpool": "ow_whirlpool", "overworldflute": "ow_fluteshuffle" }, "entrance": { diff --git a/source/gui/randomize/overworld.py b/source/gui/randomize/overworld.py index 77be948a..3cc3eb36 100644 --- a/source/gui/randomize/overworld.py +++ b/source/gui/randomize/overworld.py @@ -33,7 +33,7 @@ def overworld_page(parent): packAttrs = {"side":LEFT, "pady":(18,0)} elif key == "overworldflute": packAttrs["pady"] = (20,0) - elif key == "mixed": + elif key in ["whirlpool", "mixed"]: packAttrs = {"anchor":W, "padx":(79,0)} self.widgets[key].pack(packAttrs)