From f259e8bdc8f3e248e9a912501a640ffd3024a7c0 Mon Sep 17 00:00:00 2001 From: aerinon Date: Fri, 20 Aug 2021 14:32:54 -0600 Subject: [PATCH 1/6] Boss item restriction logic added Reserved location logic started --- BaseClasses.py | 39 ++++++++++--- CLI.py | 3 +- DungeonGenerator.py | 56 +++++++++++++------ Dungeons.py | 8 +-- Fill.py | 15 +++-- KeyDoorShuffle.py | 55 ++++++++---------- Main.py | 4 ++ Mystery.py | 1 + Rules.py | 18 ++++++ resources/app/cli/args.json | 7 +++ resources/app/cli/lang/en.json | 7 +++ resources/app/gui/lang/en.json | 4 ++ resources/app/gui/randomize/item/widgets.json | 9 +++ source/classes/constants.py | 3 +- source/item/FillUtil.py | 20 +++++++ 15 files changed, 181 insertions(+), 68 deletions(-) create mode 100644 source/item/FillUtil.py diff --git a/BaseClasses.py b/BaseClasses.py index b9b3dd1c..3a2dc49b 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -114,6 +114,7 @@ class World(object): set_player_attr('compassshuffle', False) set_player_attr('keyshuffle', False) set_player_attr('bigkeyshuffle', False) + set_player_attr('restrict_boss_items', 'none') set_player_attr('bombbag', False) set_player_attr('difficulty_requirements', None) set_player_attr('boss_shuffle', 'none') @@ -324,7 +325,7 @@ class World(object): elif item.name.startswith('Bottle'): if ret.bottle_count(item.player) < self.difficulty_requirements[item.player].progressive_bottle_limit: ret.prog_items[item.name, item.player] += 1 - elif item.advancement or item.smallkey or item.bigkey: + elif item.advancement or item.smallkey or item.bigkey or item.compass or item.map: ret.prog_items[item.name, item.player] += 1 for item in self.itempool: @@ -339,6 +340,8 @@ class World(object): key_list += [dungeon.big_key.name] if len(dungeon.small_keys) > 0: key_list += [x.name for x in dungeon.small_keys] + # map/compass may be required now + key_list += [x.name for x in dungeon.dungeon_items] from Items import ItemFactory for item in ItemFactory(key_list, p): soft_collect(item) @@ -2178,6 +2181,12 @@ class Item(object): item_dungeon = 'Hyrule Castle' return item_dungeon + def is_inside_dungeon_item(self, world): + return ((self.smallkey and not world.keyshuffle[self.player]) + or (self.bigkey and not world.bigkeyshuffle[self.player]) + or (self.compass and not world.compassshuffle[self.player]) + or (self.map and not world.mapshuffle[self.player])) + def __str__(self): return str(self.__unicode__()) @@ -2388,6 +2397,7 @@ class Spoiler(object): 'weapons': self.world.swords, 'goal': self.world.goal, 'shuffle': self.world.shuffle, + 'linkshuffle': self.world.shufflelinks, 'door_shuffle': self.world.doorShuffle, 'intensity': self.world.intensity, 'item_pool': self.world.difficulty, @@ -2396,6 +2406,7 @@ class Spoiler(object): 'ganon_crystals': self.world.crystals_needed_for_ganon, 'open_pyramid': self.world.open_pyramid, 'accessibility': self.world.accessibility, + 'restricted_boss_items': self.world.restrict_boss_items, 'hints': self.world.hints, 'mapshuffle': self.world.mapshuffle, 'compassshuffle': self.world.compassshuffle, @@ -2411,6 +2422,7 @@ class Spoiler(object): 'experimental': self.world.experimental, 'keydropshuffle': self.world.keydropshuffle, 'shopsanity': self.world.shopsanity, + 'psuedoboots': self.world.pseudoboots, '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)} @@ -2438,6 +2450,9 @@ class Spoiler(object): return json.dumps(out) def to_file(self, filename): + def yn(flag): + return 'Yes' if flag else 'No' + self.parse_data() with open(filename, 'w') as outfile: outfile.write('ALttP Entrance Randomizer Version %s - Seed: %s\n\n' % (self.metadata['version'], self.world.seed)) @@ -2462,6 +2477,7 @@ class Spoiler(object): outfile.write('Difficulty: %s\n' % self.metadata['item_pool'][player]) outfile.write('Item Functionality: %s\n' % self.metadata['item_functionality'][player]) outfile.write('Entrance Shuffle: %s\n' % self.metadata['shuffle'][player]) + outfile.write(f"Links House Shuffled: {self.metadata['linkshuffle'][player]}\n") outfile.write('Door Shuffle: %s\n' % self.metadata['door_shuffle'][player]) outfile.write('Intensity: %s\n' % self.metadata['intensity'][player]) addition = ' (Random)' if self.world.crystals_gt_orig[player] == 'random' else '' @@ -2470,6 +2486,7 @@ class Spoiler(object): outfile.write('Crystals required for Ganon: %s\n' % (str(self.metadata['ganon_crystals'][player]) + addition)) outfile.write('Pyramid hole pre-opened: %s\n' % ('Yes' if self.metadata['open_pyramid'][player] else 'No')) outfile.write('Accessibility: %s\n' % self.metadata['accessibility'][player]) + outfile.write(f"Restricted Boss Items: {self.metadata['restricted_boss_items'][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('Small Key shuffle: %s\n' % ('Yes' if self.metadata['keyshuffle'][player] else 'No')) @@ -2478,12 +2495,13 @@ class Spoiler(object): 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]) - outfile.write('Pot shuffle: %s\n' % ('Yes' if self.metadata['potshuffle'][player] else 'No')) - outfile.write('Hints: %s\n' % ('Yes' if self.metadata['hints'][player] else 'No')) - outfile.write('Experimental: %s\n' % ('Yes' if self.metadata['experimental'][player] else 'No')) - outfile.write('Key Drops shuffled: %s\n' % ('Yes' if self.metadata['keydropshuffle'][player] else 'No')) - outfile.write(f"Shopsanity: {'Yes' if self.metadata['shopsanity'][player] else 'No'}\n") - outfile.write('Bombbag: %s\n' % ('Yes' if self.metadata['bombbag'][player] else 'No')) + outfile.write(f"Pot shuffle: {yn(self.metadata['potshuffle'][player])}\n") + outfile.write(f"Hints: {yn(self.metadata['hints'][player])}\n") + outfile.write(f"Experimental: {yn(self.metadata['experimental'][player])}\n") + outfile.write(f"Key Drops shuffled: {yn(self.metadata['keydropshuffle'][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['bombbag'][player])}\n") if self.doors: outfile.write('\n\nDoors:\n\n') outfile.write('\n'.join( @@ -2665,6 +2683,12 @@ enemy_mode = {"none": 0, "shuffled": 1, "random": 2, "chaos": 2, "legacy": 3} e_health = {"default": 0, "easy": 1, "normal": 2, "hard": 3, "expert": 4} e_dmg = {"default": 0, "shuffled": 1, "random": 2} +# additions +# shuffle links: 1 bit +# restrict_boss_mode: 2 bits +# psuedoboots does not effect code +# sfx_shuffle and other adjust items does not effect settings code + class Settings(object): @staticmethod @@ -2738,7 +2762,6 @@ class Settings(object): args.shufflepots[p] = True if settings[7] & 0x4 else False -@unique class KeyRuleType(FastEnum): WorstCase = 0 AllowSmall = 1 diff --git a/CLI.py b/CLI.py index bb9ab0a2..bb0af91b 100644 --- a/CLI.py +++ b/CLI.py @@ -96,7 +96,7 @@ def parse_cli(argv, no_defaults=False): for name in ['logic', 'mode', 'swords', 'goal', 'difficulty', 'item_functionality', 'shuffle', 'door_shuffle', 'intensity', 'crystals_ganon', 'crystals_gt', 'openpyramid', 'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory', - 'bombbag', + 'bombbag', 'restrict_boss_items', 'triforce_pool_min', 'triforce_pool_max', 'triforce_goal_min', 'triforce_goal_max', 'triforce_min_difference', 'triforce_goal', 'triforce_pool', 'shufflelinks', 'pseudoboots', 'retro', 'accessibility', 'hints', 'beemizer', 'experimental', 'dungeon_counters', @@ -140,6 +140,7 @@ def parse_settings(): "progressive": "on", "accessibility": "items", "algorithm": "balanced", + "restrict_boss_items": "none", # Shuffle Ganon defaults to TRUE "openpyramid": False, diff --git a/DungeonGenerator.py b/DungeonGenerator.py index 948115f3..4be27692 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -226,7 +226,8 @@ def gen_dungeon_info(name, available_sectors, entrance_regions, all_regions, pro return name == 'Skull Woods 2' and d.name == 'Skull Pinball WS' original_state = extend_reachable_state_improved(entrance_regions, start, proposed_map, all_regions, valid_doors, bk_flag, world, player, exception) - dungeon['Origin'] = create_graph_piece_from_state(None, original_state, original_state, proposed_map, exception) + dungeon['Origin'] = create_graph_piece_from_state(None, original_state, original_state, proposed_map, exception, + world, player) either_crystal = True # if all hooks from the origin are either, explore all bits with either for hook, crystal in dungeon['Origin'].hooks.items(): if crystal != CrystalBarrier.Either: @@ -247,7 +248,7 @@ def gen_dungeon_info(name, available_sectors, entrance_regions, all_regions, pro o_state = extend_reachable_state_improved([parent], init_state, proposed_map, all_regions, valid_doors, bk_flag, world, player, exception) o_state_cache[door.name] = o_state - piece = create_graph_piece_from_state(door, o_state, o_state, proposed_map, exception) + piece = create_graph_piece_from_state(door, o_state, o_state, proposed_map, exception, world, player) dungeon[door.name] = piece check_blue_states(hanger_set, dungeon, o_state_cache, proposed_map, all_regions, valid_doors, group_flags, door_map, world, player, exception) @@ -347,7 +348,7 @@ def explore_blue_state(door, dungeon, o_state, proposed_map, all_regions, valid_ blue_start.big_key_special = o_state.big_key_special b_state = extend_reachable_state_improved([parent], blue_start, proposed_map, all_regions, valid_doors, bk_flag, world, player, exception) - dungeon[door.name] = create_graph_piece_from_state(door, o_state, b_state, proposed_map, exception) + dungeon[door.name] = create_graph_piece_from_state(door, o_state, b_state, proposed_map, exception, world, player) def make_a_choice(dungeon, hangers, avail_hooks, prev_choices, name): @@ -639,7 +640,7 @@ def stonewall_valid(stonewall): return True -def create_graph_piece_from_state(door, o_state, b_state, proposed_map, exception): +def create_graph_piece_from_state(door, o_state, b_state, proposed_map, exception, world, player): # todo: info about dungeon events - not sure about that graph_piece = GraphPiece() all_unattached = {} @@ -671,16 +672,14 @@ def create_graph_piece_from_state(door, o_state, b_state, proposed_map, exceptio graph_piece.visited_regions.update(o_state.visited_orange) graph_piece.visited_regions.update(b_state.visited_blue) graph_piece.visited_regions.update(b_state.visited_orange) - graph_piece.possible_bk_locations.update(filter_for_potential_bk_locations(o_state.bk_found)) - graph_piece.possible_bk_locations.update(filter_for_potential_bk_locations(b_state.bk_found)) + graph_piece.possible_bk_locations.update(filter_for_potential_bk_locations(o_state.bk_found, world, player)) + graph_piece.possible_bk_locations.update(filter_for_potential_bk_locations(b_state.bk_found, world, player)) graph_piece.pinball_used = o_state.pinball_used or b_state.pinball_used return graph_piece -def filter_for_potential_bk_locations(locations): - return [x for x in locations if - '- Big Chest' not in x.name and '- Prize' not in x.name and x.name not in dungeon_events - and not x.forced_item and x.name not in ['Agahnim 1', 'Agahnim 2']] +def filter_for_potential_bk_locations(locations, world, player): + return count_locations_exclude_big_chest(locations, world, player) type_map = { @@ -1023,12 +1022,8 @@ class ExplorationState(object): return self.crystal == CrystalBarrier.Either or door.crystal == self.crystal return True - def count_locations_exclude_specials(self): - cnt = 0 - for loc in self.found_locations: - if '- Big Chest' not in loc.name and '- Prize' not in loc.name and loc.name not in dungeon_events and not loc.forced_item: - cnt += 1 - return cnt + def count_locations_exclude_specials(self, world, player): + return count_locations_exclude_big_chest(self.found_locations, world, player) def validate(self, door, region, world, player): return self.can_traverse(door) and not self.visited(region) and valid_region_to_explore(region, self.dungeon, @@ -1069,6 +1064,32 @@ class ExplorationState(object): return 2 +def count_locations_exclude_big_chest(locations, world, player): + cnt = 0 + for loc in locations: + if ('- Big Chest' not in loc.name and not loc.forced_item and not reserved_location(loc, world, player) + and not prize_or_event(loc) and not blind_boss_unavail(loc, locations, world, player)): + cnt += 1 + return cnt + + +def prize_or_event(loc): + return loc.name in dungeon_events or '- Prize' in loc.name or loc.name in ['Agahnim 1', 'Agahnim 2'] + + +def reserved_location(loc, world, player): + return loc.name in world.item_pool_config.reserved_locations[player] + + +def blind_boss_unavail(loc, locations, world, player): + if loc.name == "Thieves' Town - Boss": + return (loc.parent_region.dungeon.boss.name == 'Blind' and + (not any(x for x in locations if x.name == 'Suspicious Maiden') or + (world.get_region('Thieves Attic Window', player).dungeon.name == 'Thieves Town' and + not any(x for x in locations if x.name == 'Attic Cracked Floor')))) + return False + + class ExplorableDoor(object): def __init__(self, door, crystal, flag): @@ -1092,7 +1113,8 @@ def extend_reachable_state_improved(search_regions, state, proposed_map, all_reg explorable_door = local_state.next_avail_door() if explorable_door.door.bigKey: if bk_flag: - big_not_found = not special_big_key_found(local_state) if local_state.big_key_special else local_state.count_locations_exclude_specials() == 0 + big_not_found = (not special_big_key_found(local_state) if local_state.big_key_special + else local_state.count_locations_exclude_specials(world, player) == 0) if big_not_found: continue # we can't open this door if explorable_door.door in proposed_map: diff --git a/Dungeons.py b/Dungeons.py index 73f53794..a37ce3d4 100644 --- a/Dungeons.py +++ b/Dungeons.py @@ -117,6 +117,7 @@ def fill_dungeons(world): def get_dungeon_item_pool(world): return [item for dungeon in world.dungeons for item in dungeon.all_items] + def fill_dungeons_restrictive(world, shuffled_locations): all_state_base = world.get_all_state() @@ -137,10 +138,7 @@ def fill_dungeons_restrictive(world, shuffled_locations): elif (item.map and world.mapshuffle[item.player]) or (item.compass and world.compassshuffle[item.player]): item.priority = True - dungeon_items = [item for item in get_dungeon_item_pool(world) if ((item.smallkey and not world.keyshuffle[item.player]) - or (item.bigkey and not world.bigkeyshuffle[item.player]) - or (item.map and not world.mapshuffle[item.player]) - or (item.compass and not world.compassshuffle[item.player]))] + dungeon_items = [item for item in get_dungeon_item_pool(world) if item.is_inside_dungeon_item(world)] # sort in the order Big Key, Small Key, Other before placing dungeon items sort_order = {"BigKey": 3, "SmallKey": 2} @@ -414,7 +412,7 @@ dungeon_prize = { 'Palace of Darkness': 'Palace of Darkness - Prize', 'Swamp Palace': 'Swamp Palace - Prize', 'Skull Woods': 'Skull Woods - Prize', - 'Thieves Town': 'Thieves Town - Prize', + 'Thieves Town': "Thieves' Town - Prize", 'Ice Palace': 'Ice Palace - Prize', 'Misery Mire': 'Misery Mire - Prize', 'Turtle Rock': 'Turtle Rock - Prize', diff --git a/Fill.py b/Fill.py index c42b6251..67476fbf 100644 --- a/Fill.py +++ b/Fill.py @@ -237,7 +237,10 @@ def fill_restrictive(world, base_state, locations, itempool, keys_in_itempool = def valid_key_placement(item, location, itempool, world): - if (not item.smallkey and not item.bigkey) or item.player != location.player or world.retro[item.player] or world.logic[item.player] == 'nologic': + if not valid_reserved_placement(item, location, world): + return False + if ((not item.smallkey and not item.bigkey) or item.player != location.player + or world.retro[item.player] or world.logic[item.player] == 'nologic'): return True dungeon = location.parent_region.dungeon if dungeon: @@ -247,9 +250,13 @@ def valid_key_placement(item, location, itempool, world): unplaced_keys = len([x for x in itempool if x.name == key_logic.small_key_name and x.player == item.player]) return key_logic.check_placement(unplaced_keys, location if item.bigkey else None) else: - inside_dungeon_item = ((item.smallkey and not world.keyshuffle[item.player]) - or (item.bigkey and not world.bigkeyshuffle[item.player])) - return not inside_dungeon_item + return item.is_inside_dungeon_item(world) + + +def valid_reserved_placement(item, location, world): + if item.player == location.player and item.is_inside_dungeon_item(world): + return location.name not in world.item_pool_config.reserved_locations[location.player] + return True def track_outside_keys(item, location, world): diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index cf18f4f4..c5c666e4 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -5,7 +5,8 @@ from collections import defaultdict, deque from BaseClasses import DoorType, dungeon_keys, KeyRuleType, RegionType from Regions import dungeon_events from Dungeons import dungeon_keys, dungeon_bigs, dungeon_prize -from DungeonGenerator import ExplorationState, special_big_key_doors +from DungeonGenerator import ExplorationState, special_big_key_doors, count_locations_exclude_big_chest, prize_or_event +from DungeonGenerator import reserved_location, blind_boss_unavail class KeyLayout(object): @@ -1078,40 +1079,30 @@ def location_is_bk_locked(loc, key_logic): return loc in key_logic.bk_chests or loc in key_logic.bk_locked -def prize_or_event(loc): - return loc.name in dungeon_events or '- Prize' in loc.name or loc.name in ['Agahnim 1', 'Agahnim 2'] - - -def boss_unavail(loc, world, player): - # todo: ambrosia - # return world.bossdrops[player] == 'ambrosia' and "- Boss" in loc.name - return False - - -def blind_boss_unavail(loc, state, world, player): - if loc.name == "Thieves' Town - Boss": - # todo: check attic - return (loc.parent_region.dungeon.boss.name == 'Blind' and - (not any(x for x in state.found_locations if x.name == 'Suspicious Maiden') or - (world.get_region('Thieves Attic Window', player).dungeon.name == 'Thieves Town' and - not any(x for x in state.found_locations if x.name == 'Attic Cracked Floor')))) - return False +# todo: verfiy this code is defunct +# def prize_or_event(loc): +# return loc.name in dungeon_events or '- Prize' in loc.name or loc.name in ['Agahnim 1', 'Agahnim 2'] +# +# +# def reserved_location(loc, world, player): +# return loc in world.item_pool.config.reserved_locations[player] +# +# +# def blind_boss_unavail(loc, state, world, player): +# if loc.name == "Thieves' Town - Boss": +# return (loc.parent_region.dungeon.boss.name == 'Blind' and +# (not any(x for x in state.found_locations if x.name == 'Suspicious Maiden') or +# (world.get_region('Thieves Attic Window', player).dungeon.name == 'Thieves Town' and +# not any(x for x in state.found_locations if x.name == 'Attic Cracked Floor')))) +# return False +# counts free locations for keys - hence why reserved locations don't count def count_free_locations(state, world, player): cnt = 0 for loc in state.found_locations: - if (not prize_or_event(loc) and not loc.forced_item and not boss_unavail(loc, world, player) - and not blind_boss_unavail(loc, state, world, player)): - cnt += 1 - return cnt - - -def count_locations_exclude_big_chest(state, world, player): - cnt = 0 - for loc in state.found_locations: - if ('- Big Chest' not in loc.name and not loc.forced_item and not boss_unavail(loc, world, player) - and not prize_or_event(loc) and not blind_boss_unavail(loc, state, world, player)): + if (not prize_or_event(loc) and not loc.forced_item and not reserved_location(loc, world, player) + and not blind_boss_unavail(loc, state.found_locations, world, player)): cnt += 1 return cnt @@ -1407,7 +1398,7 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa if state.big_key_opened: ttl_locations = count_free_locations(state, world, player) else: - ttl_locations = count_locations_exclude_big_chest(state, world, player) + ttl_locations = count_locations_exclude_big_chest(state.found_locations, world, player) ttl_small_key_only = count_small_key_only_locations(state) available_small_locations = cnt_avail_small_locations(ttl_locations, ttl_small_key_only, state, world, player) available_big_locations = cnt_avail_big_locations(ttl_locations, state, world, player) @@ -1596,7 +1587,7 @@ def can_open_door(door, state, world, player): if state.big_key_opened: ttl_locations = count_free_locations(state, world, player) else: - ttl_locations = count_locations_exclude_big_chest(state, world, player) + ttl_locations = count_locations_exclude_big_chest(state.found_locations, world, player) if door.smallKey: ttl_small_key_only = count_small_key_only_locations(state) available_small_locations = cnt_avail_small_locations(ttl_locations, ttl_small_key_only, state, world, player) diff --git a/Main.py b/Main.py index ee9e83fb..f6a94ce9 100644 --- a/Main.py +++ b/Main.py @@ -28,6 +28,8 @@ 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 +from source.item.FillUtil import create_item_pool_config + __version__ = '0.5.1.0-u' from source.classes.BabelFish import BabelFish @@ -103,6 +105,7 @@ def main(args, seed=None, fish=None): world.treasure_hunt_total = args.triforce_pool.copy() world.shufflelinks = args.shufflelinks.copy() world.pseudoboots = args.pseudoboots.copy() + world.restrict_boss_items = args.restrict_boss_items.copy() world.rom_seeds = {player: random.randint(0, 999999999) for player in range(1, world.players + 1)} @@ -146,6 +149,7 @@ def main(args, seed=None, fish=None): create_rooms(world, player) create_dungeons(world, player) adjust_locations(world, player) + create_item_pool_config(world) if any(world.potshuffle.values()): logger.info(world.fish.translate("cli", "cli", "shuffling.pots")) diff --git a/Mystery.py b/Mystery.py index 73644500..a53fb514 100644 --- a/Mystery.py +++ b/Mystery.py @@ -132,6 +132,7 @@ def roll_settings(weights): ret.bigkeyshuffle = get_choice('bigkey_shuffle') == 'on' if 'bigkey_shuffle' in weights else dungeon_items in ['full'] ret.accessibility = get_choice('accessibility') + ret.restrict_boss_items = get_choice('restrict_boss_items') entrance_shuffle = get_choice('entrance_shuffle') ret.shuffle = entrance_shuffle if entrance_shuffle != 'none' else 'vanilla' diff --git a/Rules.py b/Rules.py index 28d627da..f06328c5 100644 --- a/Rules.py +++ b/Rules.py @@ -4,6 +4,7 @@ from collections import deque import OverworldGlitchRules from BaseClasses import CollectionState, RegionType, DoorType, Entrance, CrystalBarrier, KeyRuleType +from Dungeons import dungeon_regions, dungeon_prize from RoomData import DoorKind from OverworldGlitchRules import overworld_glitches_rules @@ -553,11 +554,28 @@ def global_rules(world, player): add_key_logic_rules(world, player) # End of door rando rules. + if world.restrict_boss_items[player] != 'none': + def add_mc_rule(l): + boss_location = world.get_location(l, player) + d_name = boss_location.parent_region.dungeon.name + compass_name = f'Compass ({d_name})' + map_name = f'Map ({d_name})' + add_rule(boss_location, lambda state: state.has(compass_name, player) and state.has(map_name, player)) + + for dungeon in dungeon_prize.keys(): + d_name = "Thieves' Town" if dungeon.startswith('Thieves') else dungeon + for loc in [dungeon_prize[dungeon], f'{d_name} - Boss']: + add_mc_rule(loc) + if world.doorShuffle[player] == 'crossed': + add_mc_rule('Agahnim 1') + add_mc_rule('Agahnim 2') + add_rule(world.get_location('Sunken Treasure', player), lambda state: state.has('Open Floodgate', player)) set_rule(world.get_location('Ganon', player), lambda state: state.has_beam_sword(player) and state.has_fire_source(player) and state.has_crystals(world.crystals_needed_for_ganon[player], player) and (state.has('Tempered Sword', player) or state.has('Golden Sword', player) or (state.has('Silver Arrows', player) and state.can_shoot_arrows(player)) or state.has('Lamp', player) or state.can_extend_magic(player, 12))) # need to light torch a sufficient amount of times set_rule(world.get_entrance('Ganon Drop', player), lambda state: state.has_beam_sword(player)) # need to damage ganon to get tiles to drop + def bomb_rules(world, player): bonkable_doors = ['Two Brothers House Exit (West)', 'Two Brothers House Exit (East)'] # Technically this is incorrectly defined, but functionally the same as what is intended. bombable_doors = ['Ice Rod Cave', 'Light World Bomb Hut', 'Light World Death Mountain Shop', 'Mini Moldorm Cave', diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index a0113222..da496104 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -254,6 +254,13 @@ "none" ] }, + "restrict_boss_items": { + "choices": [ + "none", + "mapcompass", + "dungeon" + ] + }, "hints": { "action": "store_true", "type": "bool" diff --git a/resources/app/cli/lang/en.json b/resources/app/cli/lang/en.json index 67efab1d..13c99b2a 100644 --- a/resources/app/cli/lang/en.json +++ b/resources/app/cli/lang/en.json @@ -276,6 +276,13 @@ "Locations: You will be able to reach every location in the game.", "None: You will be able to reach enough locations to beat the game." ], + "restrict_boss_items": [ + "Select which dungeon are not allowed on bosses (default: %(default)s)", + "None: All items allowed", + "Mapcompass: Map and Compass are required before you defeat the boss.", + "Dungeon: Same as above and keys too cannot be on the boss. Small key shuffle", + " and big key shuffle override this behavior" + ], "hints": [ "Make telepathic tiles and storytellers give helpful hints. (default: %(default)s)" ], "shuffleganon": [ "Include the Ganon's Tower and Pyramid Hole in the", diff --git a/resources/app/gui/lang/en.json b/resources/app/gui/lang/en.json index c4cd8a11..ae26b0dd 100644 --- a/resources/app/gui/lang/en.json +++ b/resources/app/gui/lang/en.json @@ -287,6 +287,10 @@ "randomizer.item.sortingalgo.vt26": "VT8.26", "randomizer.item.sortingalgo.balanced": "Balanced", + "randomizer.item.restrict_boss_items": "Forbidden Boss Items", + "randomizer.item.restrict_boss_items.none": "None", + "randomizer.item.restrict_boss_items.mapcompass": "Map & Compass", + "randomizer.item.restrict_boss_items.dungeon": "Map & Compass & Keys", "bottom.content.worlds": "Worlds", "bottom.content.names": "Player names", diff --git a/resources/app/gui/randomize/item/widgets.json b/resources/app/gui/randomize/item/widgets.json index 038e668c..89cacb00 100644 --- a/resources/app/gui/randomize/item/widgets.json +++ b/resources/app/gui/randomize/item/widgets.json @@ -124,6 +124,15 @@ "vt26", "balanced" ] + }, + "restrict_boss_items": { + "type": "selectbox", + "default": "none", + "options": [ + "none", + "mapcompass", + "dungeon" + ] } } } diff --git a/source/classes/constants.py b/source/classes/constants.py index b184643b..64b01520 100644 --- a/source/classes/constants.py +++ b/source/classes/constants.py @@ -72,7 +72,8 @@ SETTINGSTOPROCESS = { "progressives": "progressive", "accessibility": "accessibility", "sortingalgo": "algorithm", - "beemizer": "beemizer" + "beemizer": "beemizer", + "restrict_boss_items": "restrict_boss_items" }, "entrance": { "openpyramid": "openpyramid", diff --git a/source/item/FillUtil.py b/source/item/FillUtil.py new file mode 100644 index 00000000..958c6b67 --- /dev/null +++ b/source/item/FillUtil.py @@ -0,0 +1,20 @@ +from collections import defaultdict + +from Dungeons import dungeon_prize + +class ItemPoolConfig(object): + + def __init__(self): + self.reserved_locations = defaultdict(set) + + +def create_item_pool_config(world): + config = ItemPoolConfig() + if world.algorithm in ['balanced']: + for player in range(1, world.players+1): + if world.restrict_boss_items[player]: + for dungeon in dungeon_prize: + if dungeon.startswith('Thieves'): + dungeon = "Thieves' Town" + config.reserved_locations[player].add(f'{dungeon} - Boss') + world.item_pool_config = config From 5c835dc243ebb9fde6faf9d1efa8424fa165fa13 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 26 Aug 2021 15:21:10 -0600 Subject: [PATCH 2/6] Fix can_beat_game error Add start_region awareness to door finder combinations Added dungeon table --- BaseClasses.py | 2 -- DoorShuffle.py | 8 ++++++++ Dungeons.py | 45 ++++++++++++++++++++++++++++++++------------- KeyDoorShuffle.py | 10 +++++----- 4 files changed, 45 insertions(+), 20 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 3a2dc49b..b35e53da 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -441,8 +441,6 @@ class World(object): return True state = starting_state.copy() else: - if self.has_beaten_game(self.state): - return True state = CollectionState(self) if self.has_beaten_game(state): diff --git a/DoorShuffle.py b/DoorShuffle.py index 3ed21895..ca377c65 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -1456,6 +1456,14 @@ def find_valid_combination(builder, start_regions, world, player, drop_keys=True random.shuffle(sample_list) proposal = kth_combination(sample_list[itr], builder.candidates, builder.key_doors_num) + # eliminate start region if portal marked as destination + excluded = {} + for region in start_regions: + portal = next((x for x in world.dungeon_portals[player] if x.door.entrance.parent_region == region), None) + if portal and portal.destination: + excluded[region] = None + start_regions = [x for x in start_regions if x not in excluded.keys()] + key_layout = build_key_layout(builder, start_regions, proposal, world, player) while not validate_key_layout(key_layout, world, player): itr += 1 diff --git a/Dungeons.py b/Dungeons.py index a37ce3d4..2edba0d4 100644 --- a/Dungeons.py +++ b/Dungeons.py @@ -373,6 +373,38 @@ flexible_starts = { 'Skull Woods': ['Skull Left Drop', 'Skull Pinball'] } + +class DungeonInfo: + + def __init__(self, free, keys, bk, map, compass, bk_drop, drops, prize=None): + # todo reduce static maps ideas: prize, bk_name, sm_name, cmp_name, map_name): + self.free_items = free + self.key_num = keys + self.bk_present = bk + self.map_present = map + self.compass_present = compass + self.bk_drops = bk_drop + self.key_drops = drops + self.prize = prize + + +dungeon_table = { + 'Hyrule Castle': DungeonInfo(6, 1, False, True, False, True, 3, None), + 'Eastern Palace': DungeonInfo(3, 0, True, True, True, False, 2, 'Eastern Palace - Prize'), + 'Desert Palace': DungeonInfo(2, 1, True, True, True, False, 3, 'Desert Palace - Prize'), + 'Tower of Hera': DungeonInfo(2, 1, True, True, True, False, 0, 'Tower of Hera - Prize'), + 'Agahnims Tower': DungeonInfo(0, 2, False, False, False, False, 2, None), + 'Palace of Darkness': DungeonInfo(5, 6, True, True, True, False, 0, 'Palace of Darkness - Prize'), + 'Swamp Palace': DungeonInfo(6, 1, True, True, True, False, 5, 'Swamp Palace - Prize'), + 'Skull Woods': DungeonInfo(2, 3, True, True, True, False, 2, 'Skull Woods - Prize'), + 'Thieves Town': DungeonInfo(4, 1, True, True, True, False, 2, "Thieves' Town - Prize"), + 'Ice Palace': DungeonInfo(3, 2, True, True, True, False, 4, 'Ice Palace - Prize'), + 'Misery Mire': DungeonInfo(2, 3, True, True, True, False, 3, 'Misery Mire - Prize'), + 'Turtle Rock': DungeonInfo(5, 4, True, True, True, False, 2, 'Turtle Rock - Prize'), + 'Ganons Tower': DungeonInfo(20, 4, True, True, True, False, 4, None), +} + + dungeon_keys = { 'Hyrule Castle': 'Small Key (Escape)', 'Eastern Palace': 'Small Key (Eastern Palace)', @@ -405,19 +437,6 @@ dungeon_bigs = { 'Ganons Tower': 'Big Key (Ganons Tower)' } -dungeon_prize = { - 'Eastern Palace': 'Eastern Palace - Prize', - 'Desert Palace': 'Desert Palace - Prize', - 'Tower of Hera': 'Tower of Hera - Prize', - 'Palace of Darkness': 'Palace of Darkness - Prize', - 'Swamp Palace': 'Swamp Palace - Prize', - 'Skull Woods': 'Skull Woods - Prize', - 'Thieves Town': "Thieves' Town - Prize", - 'Ice Palace': 'Ice Palace - Prize', - 'Misery Mire': 'Misery Mire - Prize', - 'Turtle Rock': 'Turtle Rock - Prize', -} - dungeon_hints = { 'Hyrule Castle': 'in Hyrule Castle', 'Eastern Palace': 'in Eastern Palace', diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index c5c666e4..5c65dbe6 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -4,7 +4,7 @@ from collections import defaultdict, deque from BaseClasses import DoorType, dungeon_keys, KeyRuleType, RegionType from Regions import dungeon_events -from Dungeons import dungeon_keys, dungeon_bigs, dungeon_prize +from Dungeons import dungeon_keys, dungeon_bigs, dungeon_table from DungeonGenerator import ExplorationState, special_big_key_doors, count_locations_exclude_big_chest, prize_or_event from DungeonGenerator import reserved_location, blind_boss_unavail @@ -1378,7 +1378,7 @@ def validate_key_layout(key_layout, world, player): dungeon_entrance, portal_door = find_outside_connection(region) if (len(key_layout.start_regions) > 1 and dungeon_entrance and dungeon_entrance.name in ['Ganons Tower', 'Inverted Ganons Tower', 'Pyramid Fairy'] - and key_layout.key_logic.dungeon in dungeon_prize): + and dungeon_table[key_layout.key_logic.dungeon].prize): state.append_door_to_list(portal_door, state.prize_doors) state.prize_door_set[portal_door] = dungeon_entrance key_layout.prize_relevant = True @@ -1541,7 +1541,7 @@ def create_key_counters(key_layout, world, player): dungeon_entrance, portal_door = find_outside_connection(region) if (len(key_layout.start_regions) > 1 and dungeon_entrance and dungeon_entrance.name in ['Ganons Tower', 'Inverted Ganons Tower', 'Pyramid Fairy'] - and key_layout.key_logic.dungeon in dungeon_prize): + and dungeon_table[key_layout.key_logic.dungeon].prize): state.append_door_to_list(portal_door, state.prize_doors) state.prize_door_set[portal_door] = dungeon_entrance key_layout.prize_relevant = True @@ -1966,8 +1966,8 @@ def validate_key_placement(key_layout, world, player): len(counter.key_only_locations) + keys_outside if key_layout.prize_relevant: found_prize = any(x for x in counter.important_locations if '- Prize' in x.name) - if not found_prize and key_layout.sector.name in dungeon_prize: - prize_loc = world.get_location(dungeon_prize[key_layout.sector.name], player) + if not found_prize and dungeon_table[key_layout.sector.name].prize: + prize_loc = world.get_location(dungeon_table[key_layout.sector.name].prize, player) # todo: pyramid fairy only care about crystals 5 & 6 found_prize = 'Crystal' not in prize_loc.item.name else: From 4e8a8d28406a1fd59737cd2dcc5b69ad95e27054 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 26 Aug 2021 15:25:29 -0600 Subject: [PATCH 3/6] Compass/Map can be progressive Fixed filter_for_potential_bk_locations Changed rules to use dungeon_table --- BaseClasses.py | 8 ++++---- DungeonGenerator.py | 5 +++-- Rules.py | 11 ++++++----- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index b35e53da..67f2f93d 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -852,7 +852,7 @@ class CollectionState(object): reduced = Counter() for item, cnt in self.prog_items.items(): item_name, item_player = item - if item_player == player and self.check_if_progressive(item_name): + if item_player == player and self.check_if_progressive(item_name, player): if item_name.startswith('Bottle'): # I think magic requirements can require multiple bottles bottle_count += cnt elif item_name in ['Boss Heart Container', 'Sanctuary Heart Container', 'Piece of Heart']: @@ -868,8 +868,7 @@ class CollectionState(object): reduced[('Heart Container', player)] = 1 return frozenset(reduced.items()) - @staticmethod - def check_if_progressive(item_name): + def check_if_progressive(self, item_name, player): return (item_name in ['Bow', 'Progressive Bow', 'Progressive Bow (Alt)', 'Book of Mudora', 'Hammer', 'Hookshot', 'Magic Mirror', 'Ocarina', 'Pegasus Boots', 'Power Glove', 'Cape', 'Mushroom', 'Shovel', @@ -881,7 +880,8 @@ class CollectionState(object): 'Mirror Shield', 'Progressive Shield', 'Bug Catching Net', 'Cane of Byrna', 'Boss Heart Container', 'Sanctuary Heart Container', 'Piece of Heart', 'Magic Upgrade (1/2)', 'Magic Upgrade (1/4)'] - or item_name.startswith(('Bottle', 'Small Key', 'Big Key'))) + or item_name.startswith(('Bottle', 'Small Key', 'Big Key')) + or (self.world.restrict_boss_items[player] != 'none' and item_name.startswith(('Map', 'Compass')))) def can_reach(self, spot, resolution_hint=None, player=None): try: diff --git a/DungeonGenerator.py b/DungeonGenerator.py index 4be27692..a2c3df04 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -679,7 +679,8 @@ def create_graph_piece_from_state(door, o_state, b_state, proposed_map, exceptio def filter_for_potential_bk_locations(locations, world, player): - return count_locations_exclude_big_chest(locations, world, player) + return [x for x in locations if '- Big Chest' not in x.name and not not reserved_location(x, world, player) and + not x.forced_item and not prize_or_event(x) and not blind_boss_unavail(x, locations, world, player)] type_map = { @@ -1078,7 +1079,7 @@ def prize_or_event(loc): def reserved_location(loc, world, player): - return loc.name in world.item_pool_config.reserved_locations[player] + return hasattr(world, 'item_pool_config') and loc.name in world.item_pool_config.reserved_locations[player] def blind_boss_unavail(loc, locations, world, player): diff --git a/Rules.py b/Rules.py index f06328c5..dec41c79 100644 --- a/Rules.py +++ b/Rules.py @@ -4,7 +4,7 @@ from collections import deque import OverworldGlitchRules from BaseClasses import CollectionState, RegionType, DoorType, Entrance, CrystalBarrier, KeyRuleType -from Dungeons import dungeon_regions, dungeon_prize +from Dungeons import dungeon_table from RoomData import DoorKind from OverworldGlitchRules import overworld_glitches_rules @@ -562,10 +562,11 @@ def global_rules(world, player): map_name = f'Map ({d_name})' add_rule(boss_location, lambda state: state.has(compass_name, player) and state.has(map_name, player)) - for dungeon in dungeon_prize.keys(): - d_name = "Thieves' Town" if dungeon.startswith('Thieves') else dungeon - for loc in [dungeon_prize[dungeon], f'{d_name} - Boss']: - add_mc_rule(loc) + for dungeon, info in dungeon_table.items(): + if info.prize: + d_name = "Thieves' Town" if dungeon.startswith('Thieves') else dungeon + for loc in [info.prize, f'{d_name} - Boss']: + add_mc_rule(loc) if world.doorShuffle[player] == 'crossed': add_mc_rule('Agahnim 1') add_mc_rule('Agahnim 2') From 23352c3bf7ad1098969d4209a41b339349399ed4 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 26 Aug 2021 15:36:12 -0600 Subject: [PATCH 4/6] Correct promotion of map and compass to advancement to add that logic --- source/item/FillUtil.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/source/item/FillUtil.py b/source/item/FillUtil.py index 958c6b67..6aa2f5cf 100644 --- a/source/item/FillUtil.py +++ b/source/item/FillUtil.py @@ -1,6 +1,6 @@ from collections import defaultdict -from Dungeons import dungeon_prize +from Dungeons import dungeon_table class ItemPoolConfig(object): @@ -9,12 +9,17 @@ class ItemPoolConfig(object): def create_item_pool_config(world): - config = ItemPoolConfig() - if world.algorithm in ['balanced']: - for player in range(1, world.players+1): - if world.restrict_boss_items[player]: - for dungeon in dungeon_prize: - if dungeon.startswith('Thieves'): - dungeon = "Thieves' Town" - config.reserved_locations[player].add(f'{dungeon} - Boss') - world.item_pool_config = config + world.item_pool_config = config = ItemPoolConfig() + player_set = set() + for player in range(1, world.players+1): + if world.restrict_boss_items[player] != 'none': + player_set.add(player) + if world.restrict_boss_items[player] == 'dungeon': + for dungeon, info in dungeon_table.items(): + if info.prize: + d_name = "Thieves' Town" if dungeon.startswith('Thieves') else dungeon + config.reserved_locations[player].add(f'{d_name} - Boss') + for dungeon in world.dungeons: + for item in dungeon.all_items: + if item.map or item.compass: + item.advancement = True From a9b872b88d32cde9670a1d579b9da52d10d2add2 Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 14 Sep 2021 15:00:55 -0600 Subject: [PATCH 5/6] Ambrosia logic fixes --- DungeonGenerator.py | 2 +- Fill.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/DungeonGenerator.py b/DungeonGenerator.py index 20352b37..cb834877 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -680,7 +680,7 @@ def create_graph_piece_from_state(door, o_state, b_state, proposed_map, exceptio def filter_for_potential_bk_locations(locations, world, player): - return [x for x in locations if '- Big Chest' not in x.name and not not reserved_location(x, world, player) and + return [x for x in locations if '- Big Chest' not in x.name and not reserved_location(x, world, player) and not x.forced_item and not prize_or_event(x) and not blind_boss_unavail(x, locations, world, player)] diff --git a/Fill.py b/Fill.py index 447e6912..59a98cea 100644 --- a/Fill.py +++ b/Fill.py @@ -254,7 +254,7 @@ def valid_key_placement(item, location, itempool, world): cr_count = world.crystals_needed_for_gt[location.player] return key_logic.check_placement(unplaced_keys, location if item.bigkey else None, prize_loc, cr_count) else: - return item.is_inside_dungeon_item(world) + return not item.is_inside_dungeon_item(world) # todo: big deal for ambrosia to fix this def valid_reserved_placement(item, location, world): From c64d499bab652d847990529e0f41cd5aad22dba4 Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 14 Sep 2021 15:03:51 -0600 Subject: [PATCH 6/6] Maps/compasses should not be advancement items if the restriction is none --- source/item/FillUtil.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/source/item/FillUtil.py b/source/item/FillUtil.py index 6aa2f5cf..0a1e54f7 100644 --- a/source/item/FillUtil.py +++ b/source/item/FillUtil.py @@ -20,6 +20,7 @@ def create_item_pool_config(world): d_name = "Thieves' Town" if dungeon.startswith('Thieves') else dungeon config.reserved_locations[player].add(f'{d_name} - Boss') for dungeon in world.dungeons: - for item in dungeon.all_items: - if item.map or item.compass: - item.advancement = True + if world.restrict_boss_items[dungeon.player] != 'none': + for item in dungeon.all_items: + if item.map or item.compass: + item.advancement = True