From 7f3a373b68b3c7987233e0c91e7f67d875bc3d67 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Wed, 27 Oct 2021 21:59:34 -0500 Subject: [PATCH 1/9] Fixed output path in Mystery to use the saved settings if not specified on CLI --- Mystery.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Mystery.py b/Mystery.py index 8f1b5158..e1f91164 100644 --- a/Mystery.py +++ b/Mystery.py @@ -63,7 +63,8 @@ def main(): erargs.create_spoiler = args.create_spoiler erargs.race = True erargs.outputname = seedname - erargs.outputpath = args.outputpath + if args.outputpath: + erargs.outputpath = args.outputpath erargs.loglevel = args.loglevel if args.rom: From d22d421527fae605907ebe3fbc2e19dd758f7782 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Wed, 27 Oct 2021 22:01:02 -0500 Subject: [PATCH 2/9] Fixed issue with Links House candidate list in ER is blank --- EntranceShuffle.py | 47 ++++++++++++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/EntranceShuffle.py b/EntranceShuffle.py index 949e21b8..8f5f7a08 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -1364,9 +1364,10 @@ def place_links_house(world, sectors, player): 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) if i in entrance_pool] + 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]] + links_house = random.choice(links_house_doors) connect_two_way(world, links_house, 'Links House Exit', player) return links_house @@ -1376,7 +1377,7 @@ def place_dark_sanc(world, sectors, player): 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) if i in entrance_pool] + 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_door = random.choice(sanc_doors) @@ -1735,30 +1736,36 @@ def build_accessible_entrance_list(world, start_region, player, assumed_inventor return entrances -def get_starting_entrances(world, sectors, player): +def get_starting_entrances(world, sectors, player, force_starting_world=True): invFlag = world.mode[player] == 'inverted' # find largest walkable sector sector = None invalid_sectors = list() - 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 [r for r in regions if r ]: - 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 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) + 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 From deb30151057898279f68b445e89b21640199de9e Mon Sep 17 00:00:00 2001 From: codemann8 Date: Wed, 27 Oct 2021 22:02:21 -0500 Subject: [PATCH 3/9] Fixed issue with Links House candidate list in ER is blank --- EntranceShuffle.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/EntranceShuffle.py b/EntranceShuffle.py index 8f5f7a08..3fd0c278 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -1361,10 +1361,11 @@ def place_links_house(world, sectors, player): 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] + 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]] @@ -1377,7 +1378,7 @@ def place_dark_sanc(world, sectors, player): 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] + 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_door = random.choice(sanc_doors) From a5ceb2f61c19f720d43f0976774a65c3c10837f0 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Wed, 27 Oct 2021 22:33:15 -0500 Subject: [PATCH 4/9] Moving shuffling dropdowns to later in modes where HC isn't placed early This fixes unwanted cross-world scenarios --- EntranceShuffle.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/EntranceShuffle.py b/EntranceShuffle.py index 3fd0c278..83a7faea 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -329,9 +329,6 @@ def link_entrances(world, player): else: caves.append('Ganons Tower Exit') - # shuffle holes - scramble_holes(world, player) - # place dark sanc if invFlag: place_dark_sanc(world, sectors, player) @@ -371,6 +368,9 @@ def link_entrances(world, player): dw_entrances = [e for e in dw_entrances if e in entrance_pool] connect_caves(world, lw_entrances, dw_entrances, caves, player) + # shuffle holes + scramble_holes(world, player) + # place remaining doors connect_doors(world, list(entrance_pool), list(exit_pool), player) elif world.shuffle[player] == 'lite': @@ -467,9 +467,6 @@ def link_entrances(world, player): # shuffle dungeons skull_woods_shuffle(world, player) - # shuffle dropdowns - scramble_holes(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)'))) @@ -509,6 +506,9 @@ def link_entrances(world, player): bomb_shop = bomb_shop_doors.pop() connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) + # shuffle dropdowns + scramble_holes(world, player) + # place remaining doors connect_doors(world, list(entrance_pool), list(exit_pool), player) elif world.shuffle[player] == 'crossed': From a965e854a7f806c782d4ce125cd3c115daa1a7ae Mon Sep 17 00:00:00 2001 From: codemann8 Date: Thu, 28 Oct 2021 00:21:46 -0500 Subject: [PATCH 5/9] Changed spoiler output to occur in stages rather than once at the end --- BaseClasses.py | 134 +++++++++++++++++++++++++++---------------------- Main.py | 20 +++++--- 2 files changed, 88 insertions(+), 66 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 49310325..a2bf19b5 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -2600,6 +2600,56 @@ 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, + '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: @@ -2610,8 +2660,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() @@ -2682,54 +2730,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_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, - '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 to_json(self): + self.parse_meta() self.parse_data() out = OrderedDict() out['Overworld'] = list(self.overworlds.values()) @@ -2751,8 +2753,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)) @@ -2764,9 +2766,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]) @@ -2812,14 +2811,28 @@ class Spoiler(object): 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) - if self.world.crystals_gt_orig[player] == 'random': - outfile.write('Crystals Required for GT:'.ljust(line_width) + '%s\n' % (str(self.metadata['gt_crystals'][player]))) - if self.world.crystals_ganon_orig[player] == 'random': - outfile.write('Crystals Required for Ganon:'.ljust(line_width) + '%s\n' % (str(self.metadata['ganon_crystals'][player]))) + 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; @@ -2868,6 +2881,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') @@ -2896,7 +2911,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' diff --git a/Main.py b/Main.py index 9827efb1..b5740bf4 100644 --- a/Main.py +++ b/Main.py @@ -131,6 +131,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]] @@ -266,11 +275,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 @@ -338,6 +342,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) @@ -350,7 +358,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") From 3cd8f4a741e8035824bbf99912643acad4ac3733 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Thu, 28 Oct 2021 01:01:53 -0500 Subject: [PATCH 6/9] Adding no_race CLI option for Mystery --- Mystery.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Mystery.py b/Mystery.py index e1f91164..711c88db 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('--enemizercli') parser.add_argument('--outputpath') @@ -61,7 +62,7 @@ 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 if args.outputpath: erargs.outputpath = args.outputpath From 4b0d8fe762f6cb0cc97cd7a9b16cf33b2f81be51 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Thu, 28 Oct 2021 12:55:12 -0500 Subject: [PATCH 7/9] Fixed infinite loop issue and better error handling in ER --- EntranceShuffle.py | 55 ++++++++++++++++++++++++++-------------------- 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/EntranceShuffle.py b/EntranceShuffle.py index 83a7faea..129903eb 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -335,9 +335,6 @@ def link_entrances(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 lw_entrances = list() @@ -370,6 +367,9 @@ def link_entrances(world, player): # shuffle holes scramble_holes(world, player) + + # place blacksmith, has limited options + place_blacksmith(world, links_house, player) # place remaining doors connect_doors(world, list(entrance_pool), list(exit_pool), player) @@ -384,6 +384,9 @@ def link_entrances(world, player): # shuffle dungeons skull_woods_shuffle(world, player) + # shuffle dropdowns + scramble_holes(world, player) + # build dungeon lists lw_dungeons = LW_Dungeon_Exits.copy() dw_dungeons = DW_Late_Dungeon_Exits.copy() @@ -402,9 +405,6 @@ def link_entrances(world, player): 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) @@ -423,15 +423,16 @@ def link_entrances(world, player): dw_entrances.append(e) # place connectors in inaccessible regions - caves = Cave_Base + lw_dungeons + Cave_Base - connector_entrances = [e for e in list(zip(*default_connector_connections + default_dungeon_connections + open_default_dungeon_connections))[0] if e in lw_entrances] - connect_inaccessible_regions(world, connector_entrances, [], caves, player) - lw_dungeons = list(set(lw_dungeons) & set(caves)) + Old_Man_House - - caves = list(set(Cave_Base) & set(caves)) + dw_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] + 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) - dw_dungeons = list(set(dw_dungeons) & set(caves)) + + 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 @@ -480,6 +481,9 @@ def link_entrances(world, player): 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) @@ -506,9 +510,6 @@ def link_entrances(world, player): bomb_shop = bomb_shop_doors.pop() connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) - # shuffle dropdowns - scramble_holes(world, player) - # place remaining doors connect_doors(world, list(entrance_pool), list(exit_pool), player) elif world.shuffle[player] == 'crossed': @@ -1072,7 +1073,7 @@ def scramble_holes(world, player): 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 = True + hc_in_lw = not invFlag else: # checks if drop candidates exist in LW drop_owids = [ 0x00, 0x02, 0x13, 0x15, 0x18, 0x1b, 0x22 ] @@ -1369,6 +1370,7 @@ def place_links_house(world, sectors, player): 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]] + 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 @@ -1381,6 +1383,8 @@ def place_dark_sanc(world, sectors, player): 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]] + + 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) @@ -1407,8 +1411,8 @@ def place_blacksmith(world, links_house, player): 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]] - random.shuffle(blacksmith_doors) - blacksmith_hut = blacksmith_doors.pop() + 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 @@ -1533,12 +1537,15 @@ def connect_inaccessible_regions(world, lw_entrances, dw_entrances, caves, playe 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: - if len(otherworld_must_exit_regions) > 0: + 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], [e for e in (dw_entrances if not invFlag else lw_entrances) if e in entrance_pool]) + connect_one(otherworld_must_exit_regions[0], pool) elif len(must_exit_regions) > 0: - random.shuffle(must_exit_regions) - connect_one(must_exit_regions[0], [e for e in (lw_entrances if not invFlag else dw_entrances) if e in entrance_pool]) + 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): From ebd301e4e53b08dd23ccc31835905acce4b62704 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Thu, 28 Oct 2021 13:55:48 -0500 Subject: [PATCH 8/9] Fixed various issue involving dropdown entrances getting chosen for non-dropdowns --- EntranceShuffle.py | 68 ++++++++++++++++++++++------------------------ 1 file changed, 33 insertions(+), 35 deletions(-) diff --git a/EntranceShuffle.py b/EntranceShuffle.py index 129903eb..e845587f 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -257,8 +257,7 @@ def link_entrances(world, player): 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() + bomb_shop = random.choice(bomb_shop_doors) connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) # place remaining doors @@ -301,8 +300,7 @@ def link_entrances(world, player): 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() + bomb_shop = random.choice(bomb_shop_doors) connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) # shuffle connectors @@ -331,33 +329,33 @@ def link_entrances(world, player): # place dark sanc if invFlag: - place_dark_sanc(world, sectors, player) + place_dark_sanc(world, sectors, player, list(zip(*drop_connections + dropexit_connections))[0]) # place links house - links_house = place_links_house(world, sectors, 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: - region = world.get_entrance(e, player).parent_region - if region.type == RegionType.LightWorld: - lw_entrances.append(e) - else: - dw_entrances.append(e) + 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) + connect_inaccessible_regions(world, lw_entrances, dw_entrances, caves, player, list(zip(*drop_connections + dropexit_connections))[0]) # place old man, has limited options - place_old_man(world, lw_entrances if not invFlag else dw_entrances, player) + 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 - bomb_shop_doors = list(entrance_pool) + 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 entrance_pool if e not in ['Pyramid Fairy']] - random.shuffle(bomb_shop_doors) - bomb_shop = bomb_shop_doors.pop() + 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) # shuffle connectors @@ -384,9 +382,6 @@ def link_entrances(world, player): # shuffle dungeons skull_woods_shuffle(world, player) - # shuffle dropdowns - scramble_holes(world, player) - # build dungeon lists lw_dungeons = LW_Dungeon_Exits.copy() dw_dungeons = DW_Late_Dungeon_Exits.copy() @@ -405,6 +400,9 @@ def link_entrances(world, player): 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) @@ -451,8 +449,7 @@ def link_entrances(world, player): 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() + bomb_shop = random.choice(bomb_shop_doors) connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) # place remaining doors @@ -506,8 +503,7 @@ def link_entrances(world, player): 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() + bomb_shop = random.choice(bomb_shop_doors) connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) # place remaining doors @@ -552,8 +548,7 @@ def link_entrances(world, player): 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() + bomb_shop = random.choice(bomb_shop_doors) connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) # shuffle connectors @@ -1352,7 +1347,7 @@ def full_shuffle_dungeons(world, Dungeon_Exits, player): connect_caves(world, lw_entrances, dw_entrances, dungeon_exits, player) -def place_links_house(world, sectors, 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' @@ -1370,13 +1365,15 @@ def place_links_house(world, sectors, player): 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): +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: @@ -1384,6 +1381,7 @@ def place_dark_sanc(world, sectors, player): 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) @@ -1417,14 +1415,14 @@ def place_blacksmith(world, links_house, player): return blacksmith_hut -def place_old_man(world, pool, player): +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)'] + 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) @@ -1434,7 +1432,7 @@ def place_old_man(world, pool, player): 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 entrance_exits + [old_man_exit]] + 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': @@ -1484,7 +1482,7 @@ def junk_fill_inaccessible(world, player): connect_entrance(world, entrance, junk_locations.pop(), player) -def connect_inaccessible_regions(world, lw_entrances, dw_entrances, caves, player): +def connect_inaccessible_regions(world, lw_entrances, dw_entrances, caves, player, ignore_list=[]): invFlag = world.mode[player] == 'inverted' random.shuffle(lw_entrances) @@ -1512,7 +1510,7 @@ def connect_inaccessible_regions(world, lw_entrances, dw_entrances, caves, playe 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 invFlag != (0x1b in world.owswaps[player][0] and world.owMixed[player])): + 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) @@ -1523,12 +1521,12 @@ def connect_inaccessible_regions(world, lw_entrances, dw_entrances, caves, playe inaccessible_entrances = list() region = world.get_region(region_name, player) for exit in region.exits: - if not exit.connected_region and exit.name in entrance_pool and (world.shuffle[player] not in ['lite', 'lean'] or exit.name in pool): + 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) + 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']: From d336c1814436bf9bd220ebf94d6305804af33ddc Mon Sep 17 00:00:00 2001 From: codemann8 Date: Thu, 28 Oct 2021 14:01:28 -0500 Subject: [PATCH 9/9] Version bump 0.2.1.1 --- CHANGELOG.md | 6 ++++++ OverworldShuffle.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f76fb7c7..7bc7474a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +### 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 diff --git a/OverworldShuffle.py b/OverworldShuffle.py index 6e3f52f2..449af86d 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -3,7 +3,7 @@ from BaseClasses import OWEdge, WorldType, RegionType, Direction, Terrain, PolSl from Regions import mark_dark_world_regions, mark_light_world_regions from OWEdges import OWTileRegions, OWTileGroups, OWEdgeGroups, OWExitTypes, OpenStd, parallel_links, IsParallel -__version__ = '0.2.1.0-u' +__version__ = '0.2.1.1-u' def link_overworld(world, player): # setup mandatory connections