fix: issues with major_only and too mnay major items

chore: reformatted spoiler, moved crystal reqs to requirements
This commit is contained in:
aerinon
2024-05-14 14:08:33 -06:00
parent a15a12d240
commit 2694efeb9f
5 changed files with 69 additions and 22 deletions

View File

@@ -2753,49 +2753,64 @@ class Spoiler(object):
if self.metadata['goal'][player] in ['triforcehunt', 'trinity', 'ganonhunt']: if self.metadata['goal'][player] in ['triforcehunt', 'trinity', 'ganonhunt']:
outfile.write('Triforce Pieces Required: %s\n' % self.metadata['triforcegoal'][player]) outfile.write('Triforce Pieces Required: %s\n' % self.metadata['triforcegoal'][player])
outfile.write('Triforce Pieces Total: %s\n' % self.metadata['triforcepool'][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('\n')
outfile.write('Crystals required for Ganon: %s\n' % (str(self.world.crystals_ganon_orig[player])))
# Item Settings
outfile.write('Accessibility: %s\n' % self.metadata['accessibility'][player]) outfile.write('Accessibility: %s\n' % self.metadata['accessibility'][player])
outfile.write(f"Restricted Boss Items: {self.metadata['restricted_boss_items'][player]}\n") 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('Difficulty: %s\n' % self.metadata['item_pool'][player])
outfile.write('Item Functionality: %s\n' % self.metadata['item_functionality'][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"Flute Mode: {self.metadata['flute_mode'][player]}\n")
outfile.write(f"Bow Mode: {self.metadata['bow_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"Bombbag: {yn(self.metadata['bombbag'][player])}\n")
outfile.write(f"Pseudoboots: {yn(self.metadata['pseudoboots'][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]) outfile.write('Entrance Shuffle: %s\n' % self.metadata['shuffle'][player])
if self.metadata['shuffle'][player] != 'vanilla': if self.metadata['shuffle'][player] != 'vanilla':
outfile.write(f"Link's House Shuffled: {yn(self.metadata['shufflelinks'][player])}\n") 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"Back of Tavern Shuffled: {yn(self.metadata['shuffletavern'][player])}\n")
outfile.write(f"GT/Ganon Shuffled: {yn(self.metadata['shuffleganon'])}\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"Overworld Map: {self.metadata['overworld_map'][player]}\n")
outfile.write(f"Take Any Caves: {self.metadata['take_any'][player]}\n") outfile.write('Pyramid hole pre-opened: %s\n' % (self.metadata['open_pyramid'][player]))
if self.metadata['goal'][player] != 'trinity': outfile.write('\n')
outfile.write('Pyramid hole pre-opened: %s\n' % (self.metadata['open_pyramid'][player]))
# 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]) outfile.write('Door Shuffle: %s\n' % self.metadata['door_shuffle'][player])
if self.metadata['door_shuffle'][player] != 'vanilla': if self.metadata['door_shuffle'][player] != 'vanilla':
outfile.write(f"Intensity: {self.metadata['intensity'][player]}\n") 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"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"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"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"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"Dungeon Counters: {self.metadata['dungeon_counters'][player]}\n")
outfile.write(f"Drop Shuffle: {self.metadata['dropshuffle'][player]}\n") outfile.write('\n')
outfile.write(f"Pottery Mode: {self.metadata['pottery'][player]}\n")
outfile.write(f"Pot Shuffle (Legacy): {yn(self.metadata['potshuffle'][player])}\n") # Enemizer
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('Boss shuffle: %s\n' % self.metadata['boss_shuffle'][player]) 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 shuffle: %s\n' % self.metadata['enemy_shuffle'][player])
outfile.write('Enemy health: %s\n' % self.metadata['enemy_health'][player]) outfile.write('Enemy health: %s\n' % self.metadata['enemy_health'][player])
outfile.write('Enemy damage: %s\n' % self.metadata['enemy_damage'][player]) outfile.write('Enemy damage: %s\n' % self.metadata['enemy_damage'][player])
if self.metadata['enemy_shuffle'][player] != 'none': if self.metadata['enemy_shuffle'][player] != 'none':
outfile.write(f"Enemy logic: {self.metadata['any_enemy_logic'][player]}\n") 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(f"Hints: {yn(self.metadata['hints'][player])}\n")
outfile.write('Race: %s\n' % ('Yes' if self.world.settings.world_rep['meta']['race'] else 'No')) 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') outfile.write(f'{dungeon}: {medallion} Medallion\n')
for player in range(1, self.world.players + 1): for player in range(1, self.world.players + 1):
player_name = '' if self.world.players == 1 else str(' (Player ' + str(player) + ')') 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])))
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 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 'misc' in self.settings: if 'misc' in self.settings:
outfile.write('\n\nBottle Refills:\n\n') outfile.write('\n\nBottle Refills:\n\n')

View File

@@ -30,7 +30,7 @@ from ItemList import generate_itempool, difficulties, fill_prizes, customize_sho
from UnderworldGlitchRules import create_hybridmajor_connections, create_hybridmajor_connectors from UnderworldGlitchRules import create_hybridmajor_connections, create_hybridmajor_connectors
from Utils import output_path, parse_player_names 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.overworld.EntranceShuffle2 import link_entrances_new
from source.tools.BPS import create_bps_from_data from source.tools.BPS import create_bps_from_data
from source.classes.CustomSettings import CustomSettings 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): for player in range(1, world.players + 1):
generate_itempool(world, player) generate_itempool(world, player)
verify_item_pool_config(world)
logger.info(world.fish.translate("cli","cli","calc.access.rules")) logger.info(world.fish.translate("cli","cli","calc.access.rules"))
for player in range(1, world.players + 1): for player in range(1, world.players + 1):

View File

@@ -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 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 #### Dungeon Restriction
@@ -491,7 +495,7 @@ In multiworld, the districts chosen apply to all players.
#### New Hints #### 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 ### Forbidden Boss Items

View File

@@ -148,6 +148,8 @@ These are now independent of retro mode and have three options: None, Random, an
* Packaged build of unstable now available * Packaged build of unstable now available
* Customizer: New PreferredLocationGroup for putting a set of items in a set of locations. See customizer docs. * 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 * 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: 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: 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 * Fix: All cross-dungeon modes with restrict boss items should require map/compass for the boss

View File

@@ -263,6 +263,24 @@ def previously_reserved(location, world, player):
return False 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): def massage_item_pool(world):
player_pool = defaultdict(list) player_pool = defaultdict(list)
for item in world.itempool: 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]: if item_to_place.name in config.item_pool[item_to_place.player]:
restricted = config.location_groups[0].locations restricted = config.location_groups[0].locations
filtered = [l for l in locations if l.name in restricted] 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 return filtered
if world.algorithm == 'district': if world.algorithm == 'district':
config = world.item_pool_config 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", 'Graveyard Cave', 'Kakariko Well - Top', "Blind's Hideout - Top", 'Bonk Rock Cave', "Aginah's Cave",
'Chest Game', 'Digging Game', 'Mire Shed - Left', 'Mimic 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': [ 'Big Keys': [
'Eastern Palace - Big Key Chest', 'Ganons Tower - Big Key Chest', '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', 'Desert Palace - Big Key Chest', 'Tower of Hera - Big Key Chest', 'Palace of Darkness - Big Key Chest',