From 2694efeb9f41846a7fde4f6860cfcf594c7c804c Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 14 May 2024 14:08:33 -0600 Subject: [PATCH] fix: issues with major_only and too mnay major items chore: reformatted spoiler, moved crystal reqs to requirements --- BaseClasses.py | 51 ++++++++++++++++++++++++++--------------- Main.py | 3 ++- README.md | 8 +++++-- RELEASENOTES.md | 2 ++ source/item/FillUtil.py | 27 ++++++++++++++++++++++ 5 files changed, 69 insertions(+), 22 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index a78a7bc2..9cb3e2a4 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -2753,49 +2753,64 @@ class Spoiler(object): if self.metadata['goal'][player] in ['triforcehunt', 'trinity', 'ganonhunt']: outfile.write('Triforce Pieces Required: %s\n' % self.metadata['triforcegoal'][player]) outfile.write('Triforce Pieces Total: %s\n' % self.metadata['triforcepool'][player]) - outfile.write('Crystals required for GT: %s\n' % (str(self.world.crystals_gt_orig[player]))) - outfile.write('Crystals required for Ganon: %s\n' % (str(self.world.crystals_ganon_orig[player]))) + outfile.write('\n') + + # Item Settings outfile.write('Accessibility: %s\n' % self.metadata['accessibility'][player]) outfile.write(f"Restricted Boss Items: {self.metadata['restricted_boss_items'][player]}\n") outfile.write('Difficulty: %s\n' % self.metadata['item_pool'][player]) outfile.write('Item Functionality: %s\n' % self.metadata['item_functionality'][player]) outfile.write(f"Flute Mode: {self.metadata['flute_mode'][player]}\n") outfile.write(f"Bow Mode: {self.metadata['bow_mode'][player]}\n") - outfile.write(f"Shopsanity: {yn(self.metadata['shopsanity'][player])}\n") outfile.write(f"Bombbag: {yn(self.metadata['bombbag'][player])}\n") outfile.write(f"Pseudoboots: {yn(self.metadata['pseudoboots'][player])}\n") + outfile.write('\n') + + # Item Pool Settings + outfile.write(f"Shopsanity: {yn(self.metadata['shopsanity'][player])}\n") + outfile.write(f"Pottery Mode: {self.metadata['pottery'][player]}\n") + outfile.write(f"Pot Shuffle (Legacy): {yn(self.metadata['potshuffle'][player])}\n") + outfile.write(f"Enemy Drop Shuffle: {self.metadata['dropshuffle'][player]}\n") + outfile.write(f"Take Any Caves: {self.metadata['take_any'][player]}\n") + outfile.write('\n') + + # Entrances outfile.write('Entrance Shuffle: %s\n' % self.metadata['shuffle'][player]) if self.metadata['shuffle'][player] != 'vanilla': outfile.write(f"Link's House Shuffled: {yn(self.metadata['shufflelinks'][player])}\n") outfile.write(f"Back of Tavern Shuffled: {yn(self.metadata['shuffletavern'][player])}\n") outfile.write(f"GT/Ganon Shuffled: {yn(self.metadata['shuffleganon'])}\n") outfile.write(f"Overworld Map: {self.metadata['overworld_map'][player]}\n") - outfile.write(f"Take Any Caves: {self.metadata['take_any'][player]}\n") - if self.metadata['goal'][player] != 'trinity': - outfile.write('Pyramid hole pre-opened: %s\n' % (self.metadata['open_pyramid'][player])) + outfile.write('Pyramid hole pre-opened: %s\n' % (self.metadata['open_pyramid'][player])) + outfile.write('\n') + + # Dungeons + outfile.write('Map shuffle: %s\n' % ('Yes' if self.metadata['mapshuffle'][player] else 'No')) + outfile.write('Compass shuffle: %s\n' % ('Yes' if self.metadata['compassshuffle'][player] else 'No')) + outfile.write(f"Small Key shuffle: {self.metadata['keyshuffle'][player]}\n") + outfile.write('Big Key shuffle: %s\n' % ('Yes' if self.metadata['bigkeyshuffle'][player] else 'No')) + outfile.write(f"Key Logic Algorithm:' {self.metadata['key_logic'][player]}\n") outfile.write('Door Shuffle: %s\n' % self.metadata['door_shuffle'][player]) if self.metadata['door_shuffle'][player] != 'vanilla': outfile.write(f"Intensity: {self.metadata['intensity'][player]}\n") outfile.write(f"Door Type Mode: {self.metadata['door_type_mode'][player]}\n") outfile.write(f"Trap Door Mode: {self.metadata['trap_door_mode'][player]}\n") - outfile.write(f"Key Logic Algorithm: {self.metadata['key_logic'][player]}\n") outfile.write(f"Decouple Doors: {yn(self.metadata['decoupledoors'][player])}\n") outfile.write(f"Spiral Stairs can self-loop: {yn(self.metadata['door_self_loops'][player])}\n") - outfile.write(f"Experimental: {yn(self.metadata['experimental'][player])}\n") + outfile.write(f"Experimental: {yn(self.metadata['experimental'][player])}\n") outfile.write(f"Dungeon Counters: {self.metadata['dungeon_counters'][player]}\n") - outfile.write(f"Drop Shuffle: {self.metadata['dropshuffle'][player]}\n") - outfile.write(f"Pottery Mode: {self.metadata['pottery'][player]}\n") - outfile.write(f"Pot Shuffle (Legacy): {yn(self.metadata['potshuffle'][player])}\n") - outfile.write('Map shuffle: %s\n' % ('Yes' if self.metadata['mapshuffle'][player] else 'No')) - outfile.write('Compass shuffle: %s\n' % ('Yes' if self.metadata['compassshuffle'][player] else 'No')) - outfile.write(f"Small Key shuffle: {self.metadata['keyshuffle'][player]}\n") - outfile.write('Big Key shuffle: %s\n' % ('Yes' if self.metadata['bigkeyshuffle'][player] else 'No')) + outfile.write('\n') + + # Enemizer outfile.write('Boss shuffle: %s\n' % self.metadata['boss_shuffle'][player]) outfile.write('Enemy shuffle: %s\n' % self.metadata['enemy_shuffle'][player]) outfile.write('Enemy health: %s\n' % self.metadata['enemy_health'][player]) outfile.write('Enemy damage: %s\n' % self.metadata['enemy_damage'][player]) if self.metadata['enemy_shuffle'][player] != 'none': outfile.write(f"Enemy logic: {self.metadata['any_enemy_logic'][player]}\n") + outfile.write('\n') + + # Misc outfile.write(f"Hints: {yn(self.metadata['hints'][player])}\n") outfile.write('Race: %s\n' % ('Yes' if self.world.settings.world_rep['meta']['race'] else 'No')) @@ -2842,10 +2857,8 @@ class Spoiler(object): outfile.write(f'{dungeon}: {medallion} Medallion\n') 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]))) + outfile.write(str('Crystals Required for GT' + player_name + ':').ljust(line_width) + '%s\n' % (str(self.metadata['gt_crystals'][player]))) + outfile.write(str('Crystals Required for Ganon' + player_name + ':').ljust(line_width) + '%s\n' % (str(self.metadata['ganon_crystals'][player]))) if 'misc' in self.settings: outfile.write('\n\nBottle Refills:\n\n') diff --git a/Main.py b/Main.py index 93f5b048..983d4673 100644 --- a/Main.py +++ b/Main.py @@ -30,7 +30,7 @@ from ItemList import generate_itempool, difficulties, fill_prizes, customize_sho from UnderworldGlitchRules import create_hybridmajor_connections, create_hybridmajor_connectors from Utils import output_path, parse_player_names -from source.item.FillUtil import create_item_pool_config, massage_item_pool, district_item_pool_config +from source.item.FillUtil import create_item_pool_config, massage_item_pool, district_item_pool_config, verify_item_pool_config from source.overworld.EntranceShuffle2 import link_entrances_new from source.tools.BPS import create_bps_from_data from source.classes.CustomSettings import CustomSettings @@ -274,6 +274,7 @@ def main(args, seed=None, fish=None): for player in range(1, world.players + 1): generate_itempool(world, player) + verify_item_pool_config(world) logger.info(world.fish.translate("cli","cli","calc.access.rules")) for player in range(1, world.players + 1): diff --git a/README.md b/README.md index fd71ed19..a70372d5 100644 --- a/README.md +++ b/README.md @@ -449,7 +449,11 @@ This fill attempts to place all items in their vanilla locations when possible. This fill attempts to place major items in major locations. Major locations are where the major items are found in the vanilla game. This includes the spot next to Uncle in the Sewers, and the Boomerang chest in Hyrule Castle. -This location pool is expanded to where dungeon items are locations if those dungeon items are shuffled. The Capacity Fairy locations are included if Shopsanity is on. If retro is enabled in addition to shopsanity, then the Old Man Sword Cave and one location in each retro cave is included. Key drop locations can be included if small or big key shuffle is on. This gives a very good balance between overworld and underworld locations though the dungeons ones will be on bosses and in big chests generally. Seeds do become more linear but usually easier to figure out. +The location pool expands to where dungeon items are located if those dungeon items are shuffled. The Capacity Fairy locations are included if Shopsanity is on. If retro is enabled in addition to shopsanity, then the major locations will include the Old Man Sword Cave and one location in each retro cave. When the various enemy and pots keys are in the location pool, then those are included if small or big key shuffle is on. + +THe location pool will be expanded to include visible heart pieces locations if the number of major items exceeds the number of locations that are considered major. This mainly affects Trinity goal, Triforce Pieces hunts, Bomb Bag shuffle, and other settings that may not have perfect 1-to-1 correspondence with major locations and items. + +This algorithm generally gives a good balance between overworld and underworld locations. Seeds do become more linear but usually easier to figure out. #### Dungeon Restriction @@ -491,7 +495,7 @@ In multiworld, the districts chosen apply to all players. #### New Hints -Based on the district algorithm above (whether it is enabled or not,) new hints can appear about that district or dungeon. For each district and dungeon, it is evaluated whether it contains vital items and how many. If it has not any vital item, items then it moves onto useful items. Useful items are generally safeties or convenience items: shields, mails, half magic, bottles, medallions that aren't required, etc. If it contains none of those and is an overworld district, then it checks for a couple more things. First, if dungeons are shuffled, it looks to see if any are in the district, if so, one of those dungeons is picked for the hint. Then, if connectors are shuffled, it checks to see if you can get to unique region through a connector in that district. If none of the above apply, the district or dungeon is considered completely foolish. +Based on the district algorithm above (whether it is enabled or not), new hints can appear about that district or dungeon. For each district and dungeon, it is evaluated whether it contains vital items and how many. If it has not any vital item, items then it moves onto useful items. Useful items are generally safeties or convenience items: shields, mails, half magic, bottles, medallions that aren't required, etc. If it contains none of those and is an overworld district, then it checks for a couple more things. First, if dungeons are shuffled, it looks to see if any are in the district, if so, one of those dungeons is picked for the hint. Then, if connectors are shuffled, it checks to see if you can get to unique region through a connector in that district. If none of the above apply, the district or dungeon is considered completely foolish. ### Forbidden Boss Items diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 22048ed7..e1a5a605 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -148,6 +148,8 @@ These are now independent of retro mode and have three options: None, Random, an * Packaged build of unstable now available * Customizer: New PreferredLocationGroup for putting a set of items in a set of locations. See customizer docs. * Customizer: Fixed an issue with starting with `Ocarina` and flute_mode is active + * Spoiler: Some reformatting. Crystal req. for GT/Ganon moved to requirements section so randomized requirements don't show up in the meta section + * Algorithm: Major_Only. Supports up to 16 extra locations (the visible heart pieces) for when major item count exceeds major location count. Examples: Triforce Hunt, Trinity (Triforce on Ped), Bombbag shuffle * Fix: HC Big Key drop doesn't count on Basic Doors * Fix: Small Key for this dungeon in Hera Basement doesn't count twice for the key counter * Fix: All cross-dungeon modes with restrict boss items should require map/compass for the boss diff --git a/source/item/FillUtil.py b/source/item/FillUtil.py index 0e1db73c..db5a1a4f 100644 --- a/source/item/FillUtil.py +++ b/source/item/FillUtil.py @@ -263,6 +263,24 @@ def previously_reserved(location, world, player): return False +def verify_item_pool_config(world): + if world.algorithm == 'major_only': + major_pool = defaultdict(list) + for item in world.itempool: + if item.name in world.item_pool_config.item_pool[item.player]: + major_pool[item.player].append(item) + for player in major_pool: + available_locations = [world.get_location(l, player) for l in world.item_pool_config.location_groups[0].locations] + available_locations = [l for l in available_locations if l.item is None] + if len(available_locations) < len(major_pool[player]): + if len(major_pool[player]) - len(available_locations) <= len(mode_grouping['Heart Pieces Visible']): + logging.getLogger('').warning('Expanding location pool for extra major items') + world.item_pool_config.location_groups[1].locations = set(mode_grouping['Heart Pieces Visible']) + else: + raise Exception(f'Major only: there are only {len(available_locations)} locations' + f' for {len(major_pool[player])} major items for player {player}. Cannot generate.') + + def massage_item_pool(world): player_pool = defaultdict(list) for item in world.itempool: @@ -413,6 +431,9 @@ def filter_locations(item_to_place, locations, world, vanilla_skip=False, potion if item_to_place.name in config.item_pool[item_to_place.player]: restricted = config.location_groups[0].locations filtered = [l for l in locations if l.name in restricted] + if len(filtered) == 0 and len(config.location_groups[1].locations) > 0: + restricted = config.location_groups[1].locations + filtered = [l for l in locations if l.name in restricted] return filtered if world.algorithm == 'district': config = world.item_pool_config @@ -689,6 +710,12 @@ mode_grouping = { 'Graveyard Cave', 'Kakariko Well - Top', "Blind's Hideout - Top", 'Bonk Rock Cave', "Aginah's Cave", 'Chest Game', 'Digging Game', 'Mire Shed - Left', 'Mimic Cave' ], + 'Heart Pieces Visible': [ + 'Bumper Cave Ledge', 'Desert Ledge', 'Lake Hylia Island', 'Floating Island', # visible on OW + 'Maze Race', 'Pyramid', "Zora's Ledge", 'Sunken Treasure', 'Spectacle Rock', + 'Lumberjack Tree', 'Spectacle Rock Cave', 'Lost Woods Hideout', 'Checkerboard Cave', + 'Peg Cave', 'Cave 45', 'Graveyard Cave' + ], 'Big Keys': [ 'Eastern Palace - Big Key Chest', 'Ganons Tower - Big Key Chest', 'Desert Palace - Big Key Chest', 'Tower of Hera - Big Key Chest', 'Palace of Darkness - Big Key Chest',