diff --git a/BaseClasses.py b/BaseClasses.py index d9952095..ca44b0a3 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -510,6 +510,7 @@ class CollectionState(object): self.world = parent if not skip_init: self.prog_items = Counter() + self.forced_keys = Counter() self.reachable_regions = {player: dict() for player in range(1, parent.players + 1)} self.blocked_connections = {player: dict() for player in range(1, parent.players + 1)} self.events = [] @@ -542,12 +543,13 @@ class CollectionState(object): queue = deque(self.blocked_connections[player].items()) self.traverse_world(queue, rrp, bc, player) - unresolved_events = [x for y in self.reachable_regions[player] for x in y.locations - if x.event and x.item and (x.item.smallkey or x.item.bigkey or x.item.advancement) - and x not in self.locations_checked and x.can_reach(self)] - unresolved_events = self._do_not_flood_the_keys(unresolved_events) - if len(unresolved_events) == 0: - self.check_key_doors_in_dungeons(rrp, player) + if self.world.key_logic_algorithm[player] == 'default': + unresolved_events = [x for y in self.reachable_regions[player] for x in y.locations + if x.event and x.item and (x.item.smallkey or x.item.bigkey or x.item.advancement) + and x not in self.locations_checked and x.can_reach(self)] + unresolved_events = self._do_not_flood_the_keys(unresolved_events) + if len(unresolved_events) == 0: + self.check_key_doors_in_dungeons(rrp, player) def traverse_world(self, queue, rrp, bc, player): # run BFS on all connections, and keep track of those blocked by missing items @@ -639,6 +641,7 @@ class CollectionState(object): def check_key_doors_in_dungeons(self, rrp, player): for dungeon_name, checklist in self.dungeons_to_check[player].items(): + # todo: optimization idea - abort exploration if there are unresolved events now if self.apply_dungeon_exploration(rrp, player, dungeon_name, checklist): continue init_door_candidates = self.should_explore_child_state(self, dungeon_name, player) @@ -838,6 +841,7 @@ class CollectionState(object): def copy(self): ret = CollectionState(self.world, skip_init=True) ret.prog_items = self.prog_items.copy() + ret.forced_keys = self.forced_keys.copy() ret.reachable_regions = {player: copy.copy(self.reachable_regions[player]) for player in range(1, self.world.players + 1)} ret.blocked_connections = {player: copy.copy(self.blocked_connections[player]) for player in range(1, self.world.players + 1)} ret.events = copy.copy(self.events) @@ -1045,6 +1049,14 @@ class CollectionState(object): return (item, player) in self.prog_items return self.prog_items[item, player] >= count + def has_sm_key_strict(self, item, player, count=1): + if self.world.keyshuffle[player] == 'universal': + if self.world.mode[player] == 'standard' and self.world.doorShuffle[player] == 'vanilla' and item == 'Small Key (Escape)': + return True # Cannot access the shop until escape is finished. This is safe because the key is manually placed in make_custom_item_pool + return self.can_buy_unlimited('Small Key (Universal)', player) + obtained = self.prog_items[item, player] - self.forced_keys[item, player] + return obtained >= count + def can_buy_unlimited(self, item, player): for shop in self.world.shops[player]: if shop.region.player == player and shop.has_unlimited(item) and shop.region.can_reach(self): @@ -1241,6 +1253,8 @@ class CollectionState(object): def collect(self, item, event=False, location=None): if location: self.locations_checked.add(location) + if item and item.smallkey and location.forced_item is not None: + self.forced_keys[item.name, item.player] += 1 if not item: return changed = False @@ -2949,7 +2963,7 @@ bow_mode = {'progressive': 0, 'silvers': 1, 'retro': 2, 'retro_silvers': 3} # byte 12: POOT TKKK (pseudoboots, overworld_map, trap_door_mode, key_logic_algo) overworld_map_mode = {'default': 0, 'compass': 1, 'map': 2} trap_door_mode = {'vanilla': 0, 'boss': 1, 'oneway': 2} -key_logic_algo = {'loose': 0, 'default': 1, 'partial': 2, 'strict': 4} +key_logic_algo = {'default': 0, 'partial': 1, 'strict': 2} # sfx_shuffle and other adjust items does not affect settings code diff --git a/Fill.py b/Fill.py index 239d8e8e..4859ebc3 100644 --- a/Fill.py +++ b/Fill.py @@ -105,6 +105,8 @@ def fill_restrictive(world, base_state, locations, itempool, key_pool=None, sing spot_to_fill = None item_locations = filter_locations(item_to_place, locations, world, vanilla) + verify(item_to_place, item_locations, maximum_exploration_state, single_player_placement, + perform_access_check, key_pool, world) for location in item_locations: spot_to_fill = verify_spot_to_fill(location, item_to_place, maximum_exploration_state, single_player_placement, perform_access_check, key_pool, world) @@ -128,9 +130,6 @@ def fill_restrictive(world, base_state, locations, itempool, key_pool=None, sing raise FillError('No more spots to place %s' % item_to_place) world.push_item(spot_to_fill, item_to_place, False) - if item_to_place.smallkey: - with suppress(ValueError): - key_pool.remove(item_to_place) track_outside_keys(item_to_place, spot_to_fill, world) track_dungeon_items(item_to_place, spot_to_fill, world) locations.remove(spot_to_fill) @@ -144,6 +143,9 @@ def verify_spot_to_fill(location, item_to_place, max_exp_state, single_player_pl if item_to_place.smallkey or item_to_place.bigkey: # a better test to see if a key can go there location.item = item_to_place location.event = True + if item_to_place.smallkey: + with suppress(ValueError): + key_pool.remove(item_to_place) test_state = max_exp_state.copy() test_state.stale[item_to_place.player] = True else: @@ -157,6 +159,8 @@ def verify_spot_to_fill(location, item_to_place, max_exp_state, single_player_pl if item_to_place.smallkey or item_to_place.bigkey: location.item = None location.event = False + if item_to_place.smallkey: + key_pool.append(item_to_place) return None @@ -394,11 +398,7 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None random.shuffle(fill_locations) random.shuffle(world.itempool) - if world.item_pool_config.preferred: - pref = list(world.item_pool_config.preferred.keys()) - pref_len = len(pref) - world.itempool.sort(key=lambda i: pref_len - pref.index((i.name, i.player)) - if (i.name, i.player) in world.item_pool_config.preferred else 0) + config_sort(world) progitempool = [item for item in world.itempool if item.advancement] prioitempool = [item for item in world.itempool if not item.advancement and item.priority] restitempool = [item for item in world.itempool if not item.advancement and not item.priority] @@ -496,6 +496,20 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None ensure_good_pots(world) +def config_sort(world): + if world.item_pool_config.verify: + config_sort_helper(world, world.item_pool_config.verify) + elif world.item_pool_config.preferred: + config_sort_helper(world, world.item_pool_config.preferred) + + +def config_sort_helper(world, sort_dict): + pref = list(sort_dict.keys()) + pref_len = len(pref) + world.itempool.sort(key=lambda i: pref_len - pref.index((i.name, i.player)) + if (i.name, i.player) in sort_dict else 0) + + def calc_trash_locations(world, player): total_count, gt_count = 0, 0 for loc in world.get_locations(): @@ -667,6 +681,40 @@ def sell_keys(world, player): world.itempool.remove(universal_key) +def verify(item_to_place, item_locations, state, spp, pac, key_pool, world): + if world.item_pool_config.verify: + logger = logging.getLogger('') + item_name = 'Bottle' if item_to_place.name.startswith('Bottle') else item_to_place.name + item_player = item_to_place.player + config = world.item_pool_config + if (item_name, item_player) in config.verify: + tests = config.verify[(item_name, item_player)] + issues = [] + for location in item_locations: + if location.name in tests: + expected = tests[location.name] + spot = verify_spot_to_fill(location, item_to_place, state, spp, pac, key_pool, world) + if spot and (item_to_place.smallkey or item_to_place.bigkey): + location.item = None + location.event = False + if item_to_place.smallkey: + key_pool.append(item_to_place) + if (expected and spot) or (not expected and spot is None): + logger.debug(f'Placing {item_name} ({item_player}) at {location.name} was {expected}') + config.verify_count += 1 + if config.verify_count >= config.verify_target: + exit() + else: + issues.append((item_name, item_player, location.name, expected)) + if len(issues) > 0: + for name, player, loc, expected in issues: + if expected: + logger.error(f'Could not place {name} ({player}) at {loc}') + else: + logger.error(f'{name} ({player}) should not be allowed at {loc}') + raise Exception(f'Test failed placing {name}') + + def balance_multiworld_progression(world): state = CollectionState(world) checked_locations = set() diff --git a/ItemList.py b/ItemList.py index cac01f3f..b708b284 100644 --- a/ItemList.py +++ b/ItemList.py @@ -1431,6 +1431,13 @@ def fill_specific_items(world): item_player = player if len(item_parts) < 2 else int(item_parts[1]) item_name = item_parts[0] world.item_pool_config.preferred[(item_name, item_player)] = placement['locations'] + elif placement['type'] == 'Verification': + item = placement['item'] + item_parts = item.split('#') + item_player = player if len(item_parts) < 2 else int(item_parts[1]) + item_name = item_parts[0] + world.item_pool_config.verify[(item_name, item_player)] = placement['locations'] + world.item_pool_config.verify_target += len(placement['locations']) def get_item_and_event_flag(item, world, player, dungeon_pool, prize_set, prize_pool): diff --git a/README.md b/README.md index 2cd2b881..c7fd99bd 100644 --- a/README.md +++ b/README.md @@ -158,12 +158,11 @@ CLI: `--trap_door_mode [vanilla|boss|oneway]` Determines how small key door logic works. -* Loose: Skips placement rules checks. Currently, experimental to see what kinds of problems can arise. * Default: Current key logic. Assumes worse case usage, placement checks, but assumes you can't get to a chest until you have sufficient keys. (May assume items are unreachable) * Partial Protection: Assumes you always have full inventory and worse case usage. This should account for dark room and bunny revival glitches. * Strict: For those would like to glitch and be protected from yourselves. Small keys door require all small keys to be available to be in logic. -CLI: `--key_logic [loose|default|partial|strict]` +CLI: `--key_logic [default|partial|strict]` ### Decouple Doors diff --git a/Rules.py b/Rules.py index a550a9d4..ed6367dd 100644 --- a/Rules.py +++ b/Rules.py @@ -2076,13 +2076,16 @@ bunny_impassible_doors = { def add_key_logic_rules(world, player): key_logic = world.key_logic[player] + eval_func = eval_small_key_door + if world.key_logic_algorithm[player] == 'strict' and world.keyshuffle[player] == 'wild': + eval_func = eval_small_key_door_strict for d_name, d_logic in key_logic.items(): for door_name, rule in d_logic.door_rules.items(): door_entrance = world.get_entrance(door_name, player) - add_rule(door_entrance, eval_small_key_door(door_name, d_name, player)) + add_rule(door_entrance, eval_func(door_name, d_name, player)) if door_entrance.door.dependents: for dep in door_entrance.door.dependents: - add_rule(dep.entrance, eval_small_key_door(door_name, d_name, player)) + add_rule(dep.entrance, eval_func(door_name, d_name, player)) for location in d_logic.bk_restricted: if not location.forced_item: forbid_item(location, d_logic.bk_name, player) @@ -2129,10 +2132,24 @@ def eval_small_key_door_main(state, door_name, dungeon, player): return door_openable +def eval_small_key_door_strict_main(state, door_name, dungeon, player): + if state.is_door_open(door_name, player): + return True + key_layout = state.world.key_layout[player][dungeon] + number = key_layout.max_chests + if number <= 0: + return True + return state.has_sm_key_strict(key_layout.key_logic.small_key_name, player, number) + + def eval_small_key_door(door_name, dungeon, player): return lambda state: eval_small_key_door_main(state, door_name, dungeon, player) +def eval_small_key_door_strict(door_name, dungeon, player): + return lambda state: eval_small_key_door_strict_main(state, door_name, dungeon, player) + + def allow_big_key_in_big_chest(bk_name, player): return lambda state, item: item.name == bk_name and item.player == player diff --git a/mystery_example.yml b/mystery_example.yml index dc1a21a7..47d44e00 100644 --- a/mystery_example.yml +++ b/mystery_example.yml @@ -27,7 +27,6 @@ boss: 0 oneway: 0 key_logic_algorithm: - loose: 0 default: 1 partial: 0 strict: 0 diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index 0926a8c9..53eb2f45 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -202,7 +202,6 @@ }, "key_logic_algorithm": { "choices": [ - "loose", "default", "partial", "strict" diff --git a/resources/app/cli/lang/en.json b/resources/app/cli/lang/en.json index 8e574ee0..d27e69ab 100644 --- a/resources/app/cli/lang/en.json +++ b/resources/app/cli/lang/en.json @@ -247,7 +247,6 @@ ], "key_logic_algorithm": [ "Key Logic Algorithm (default: %(default)s)", - "loose: Allow more randomization", "default: Balance between safety and randomization", "partial: Partial protection when using certain minor glitches", "strict: Ensure small keys are available" diff --git a/resources/app/gui/lang/en.json b/resources/app/gui/lang/en.json index 3135572e..54cd4965 100644 --- a/resources/app/gui/lang/en.json +++ b/resources/app/gui/lang/en.json @@ -97,7 +97,6 @@ "randomizer.dungeon.trap_door_mode.oneway": "Remove Annoying Traps", "randomizer.dungeon.key_logic_algorithm": "Key Logic Algorithm", - "randomizer.dungeon.key_logic_algorithm.loose": "Loose", "randomizer.dungeon.key_logic_algorithm.default": "Default", "randomizer.dungeon.key_logic_algorithm.partial": "Partial Protection", "randomizer.dungeon.key_logic_algorithm.strict": "Strict", diff --git a/resources/app/gui/randomize/dungeon/widgets.json b/resources/app/gui/randomize/dungeon/widgets.json index cecfeeff..d442ae21 100644 --- a/resources/app/gui/randomize/dungeon/widgets.json +++ b/resources/app/gui/randomize/dungeon/widgets.json @@ -57,7 +57,6 @@ "type": "selectbox", "default": "default", "options": [ - "loose", "default", "partial", "strict" diff --git a/source/item/FillUtil.py b/source/item/FillUtil.py index 07f36ac8..8c85bfdd 100644 --- a/source/item/FillUtil.py +++ b/source/item/FillUtil.py @@ -20,6 +20,9 @@ class ItemPoolConfig(object): self.reserved_locations = defaultdict(set) self.restricted = {} self.preferred = {} + self.verify = {} + self.verify_count = 0 + self.verify_target = 0 self.recorded_choices = [] @@ -435,6 +438,9 @@ def filter_locations(item_to_place, locations, world, vanilla_skip=False, potion if (item_name, item_to_place.player) in config.preferred: locs = config.preferred[(item_name, item_to_place.player)] return sorted(locations, key=lambda l: 0 if l.name in locs else 1) + if (item_name, item_to_place.player) in config.verify: + locs = config.verify[(item_name, item_to_place.player)].keys() + return sorted(locations, key=lambda l: 0 if l.name in locs else 1) return locations diff --git a/test/NewTestSuite.py b/test/NewTestSuite.py new file mode 100644 index 00000000..aca4e796 --- /dev/null +++ b/test/NewTestSuite.py @@ -0,0 +1,126 @@ +import fnmatch +import os +import subprocess +import sys +import multiprocessing +import concurrent.futures +import argparse +from collections import OrderedDict + +cpu_threads = multiprocessing.cpu_count() +py_version = f"{sys.version_info.major}.{sys.version_info.minor}" + + +def main(args=None): + successes = [] + errors = [] + task_mapping = [] + tests = OrderedDict() + + successes.append(f"Testing DR (NewTestSuite)") + print(successes[0]) + + # max_attempts = args.count + pool = concurrent.futures.ThreadPoolExecutor(max_workers=cpu_threads) + dead_or_alive = 0 + alive = 0 + + def test(test_name: str, command: str, test_file: str): + tests[test_name] = [command] + + base_command = f"python3.8 DungeonRandomizer.py --suppress_rom --suppress_spoiler" + + def gen_seed(): + task_command = base_command + " " + command + return subprocess.run(task_command, capture_output=True, shell=True, text=True) + + task = pool.submit(gen_seed) + task.success = False + task.name = test_name + task.test_file = test_file + task.cmd = base_command + " " + command + task_mapping.append(task) + + for test_suite, test_files in args.test_suite.items(): + for test_file in test_files: + test(test_suite, f'--customizer {os.path.join(test_suite, test_file)}', test_file) + + from tqdm import tqdm + with tqdm(concurrent.futures.as_completed(task_mapping), + total=len(task_mapping), unit="seed(s)", + desc=f"Success rate: 0.00%") as progressbar: + for task in progressbar: + dead_or_alive += 1 + try: + result = task.result() + if result.returncode: + errors.append([task.name + ' ' + task.test_file, task.cmd, result.stderr]) + else: + alive += 1 + task.success = True + except Exception as e: + raise e + + progressbar.set_description(f"Success rate: {(alive/dead_or_alive)*100:.2f}% - {task.name}") + + def get_results(testname: str): + result = "" + dead_or_alive = [task.success for task in task_mapping if task.name == testname] + alive = [x for x in dead_or_alive if x] + success = f"{testname} Rate: {(len(alive) / len(dead_or_alive)) * 100:.2f}%" + successes.append(success) + print(success) + result += f"{(len(alive)/len(dead_or_alive))*100:.2f}%\t" + return result.strip() + + results = [] + for t in tests.keys(): + results.append(get_results(t)) + + for result in results: + print(result) + successes.append(result) + + return successes, errors + + +if __name__ == "__main__": + successes = [] + + parser = argparse.ArgumentParser(add_help=False) + # parser.add_argument('--count', default=0, type=lambda value: max(int(value), 0)) + parser.add_argument('--cpu_threads', default=cpu_threads, type=lambda value: max(int(value), 1)) + parser.add_argument('--help', default=False, action='store_true') + + args = parser.parse_args() + + if args.help: + parser.print_help() + exit(0) + + cpu_threads = args.cpu_threads + + test_suites = {} + # not sure if it supports subdirectories properly yet + for root, dirnames, filenames in os.walk('test/suite'): + test_suites[root] = fnmatch.filter(filenames, '*.yaml') + + args = argparse.Namespace() + args.test_suite = test_suites + s, errors = main(args=args) + if successes: + successes += [""] * 2 + successes += s + print() + + if errors: + with open(f"new-test-suite-errors.txt", 'w') as stream: + for error in errors: + stream.write(error[0] + "\n") + stream.write(error[1] + "\n") + stream.write(error[2] + "\n\n") + + with open("new-test-suite-success.txt", "w") as stream: + stream.write(str.join("\n", successes)) + + input("Press enter to continue") diff --git a/test/suite/default_key_logic.yaml b/test/suite/default_key_logic.yaml new file mode 100644 index 00000000..e9f3e94b --- /dev/null +++ b/test/suite/default_key_logic.yaml @@ -0,0 +1,44 @@ +# Possible improvements: account for items that are possibly in logic +# Example: Mire Big Key in harmless means all 6 mire smalls required for fire-locked side, +# if you have access to harmless via: +# 2 pod smalls + bow, hammer or 3 pod small +meta: + players: 1 +settings: + 1: + key_logic_algorithm: default + keysanity: True + crystals_needed_for_gt: 0 # to skip trash fill +placements: + 1: + Hobo: Big Key (Misery Mire) + Waterfall Fairy - Left: Small Key (Misery Mire) + Waterfall Fairy - Right: Small Key (Misery Mire) + Palace of Darkness - Big Chest: Hammer +advanced_placements: + 1: + # Contrast with partial_2 + - type: Verification + item: Flippers + locations: + Misery Mire - Map Chest: True + Misery Mire - Main Lobby: True + Misery Mire - Bridge Chest: True + Misery Mire - Spike Chest: True + Misery Mire - Compass Chest: False + Misery Mire - Big Key Chest: False + Misery Mire - Boss: False + # Contrast with partial_3 + - type: Verification + item: Big Key (Ganons Tower) + locations: + Ganons Tower - Big Key Chest: True + Ganons Tower - Big Key Room - Left: True + Ganons Tower - Big Key Room - Right: True + Ganons Tower - Bob's Chest: True + # Normal logic doesn't allow this placement + # unless hammer is placed before it - no algorithm does this in non-keysanity, but possible in keysanity + - type: Verification + item: Small Key (Palace of Darkness) + locations: + Palace of Darkness - Dark Maze - Bottom: True \ No newline at end of file diff --git a/test/suite/partial_key_logic.yaml b/test/suite/partial_key_logic.yaml new file mode 100644 index 00000000..b5e951ed --- /dev/null +++ b/test/suite/partial_key_logic.yaml @@ -0,0 +1,24 @@ +# Even though Lamp is Flipper-locked, this logic considers that a key could be wasted in the dark in mire +# Only fire locked mire is off limits +meta: + players: 1 +settings: + 1: + key_logic_algorithm: partial + keysanity: true +placements: + 1: + Hobo: Lamp + Waterfall Fairy - Left: Small Key (Misery Mire) +advanced_placements: + 1: + - type: Verification + item: Flippers + locations: + Misery Mire - Map Chest: True + Misery Mire - Main Lobby: True + Misery Mire - Bridge Chest: True + Misery Mire - Spike Chest: True + Misery Mire - Compass Chest: False + Misery Mire - Big Key Chest: False + Misery Mire - Boss: False \ No newline at end of file diff --git a/test/suite/partial_key_logic_2.yaml b/test/suite/partial_key_logic_2.yaml new file mode 100644 index 00000000..a6f676e6 --- /dev/null +++ b/test/suite/partial_key_logic_2.yaml @@ -0,0 +1,26 @@ +# For contrast with default logic +# This logic is not yet smart enough to allow the crystal blocked chests with two keys (Spike Pot and one other) +meta: + players: 1 +settings: + 1: + key_logic_algorithm: partial + keysanity: True +placements: + 1: + Hobo: Lamp + Waterfall Fairy - Left: Small Key (Misery Mire) + Waterfall Fairy - Right: Small Key (Misery Mire) + Swamp Palace - Entrance: Big Key (Misery Mire) +advanced_placements: + 1: + - type: Verification + item: Flippers + locations: + Misery Mire - Map Chest: False + Misery Mire - Main Lobby: False + Misery Mire - Bridge Chest: True + Misery Mire - Spike Chest: True + Misery Mire - Compass Chest: False + Misery Mire - Big Key Chest: False + Misery Mire - Boss: False \ No newline at end of file diff --git a/test/suite/partial_key_logic_3.yaml b/test/suite/partial_key_logic_3.yaml new file mode 100644 index 00000000..774e4830 --- /dev/null +++ b/test/suite/partial_key_logic_3.yaml @@ -0,0 +1,160 @@ +# For contrast with default logic +# Examples of valid big key placement that doesn't work with pure worst case scenarios +# Basically chests that are obtainable two ways+ of spending keys +# (Possible fix: access to the extra door grants access to the mini helma key too) +meta: + players: 1 +settings: + 1: + key_logic_algorithm: partial + crystals_needed_for_gt: 0 # to skip trash fill +advanced_placements: + 1: + - type: Verification + item: Big Key (Desert Palace) + locations: + Desert Palace - Big Chest: False + Desert Palace - Big Key Chest: True + Desert Palace - Boss: False + Desert Palace - Compass Chest: True + Desert Palace - Map Chest: True + Desert Palace - Torch: True + - type: Verification + item: Big Key (Eastern Palace) + locations: + Eastern Palace - Big Chest: False + Eastern Palace - Big Key Chest: True + Eastern Palace - Boss: False + Eastern Palace - Cannonball Chest: True + Eastern Palace - Compass Chest: True + Eastern Palace - Map Chest: True + - type: Verification + item: Big Key (Ganons Tower) + locations: + # These four require not wasting keys upstairs because the big key is down here + Ganons Tower - Big Key Chest: False + Ganons Tower - Big Key Room - Left: False + Ganons Tower - Big Key Room - Right: False + Ganons Tower - Bob's Chest: False + # These are normal + Ganons Tower - Big Chest: False + Ganons Tower - Bob's Torch: True + Ganons Tower - Compass Room - Bottom Left: True + Ganons Tower - Compass Room - Bottom Right: True + Ganons Tower - Compass Room - Top Left: True + Ganons Tower - Compass Room - Top Right: True + Ganons Tower - DMs Room - Bottom Left: True + Ganons Tower - DMs Room - Bottom Right: True + Ganons Tower - DMs Room - Top Left: True + Ganons Tower - DMs Room - Top Right: True + Ganons Tower - Firesnake Room: True + Ganons Tower - Hope Room - Left: True + Ganons Tower - Hope Room - Right: True + Ganons Tower - Map Chest: True + Ganons Tower - Mini Helmasaur Room - Left: False + Ganons Tower - Mini Helmasaur Room - Right: False + Ganons Tower - Pre-Moldorm Chest: False + Ganons Tower - Randomizer Room - Bottom Left: True + Ganons Tower - Randomizer Room - Bottom Right: True + Ganons Tower - Randomizer Room - Top Left: True + Ganons Tower - Randomizer Room - Top Right: True + Ganons Tower - Tile Room: True + Ganons Tower - Validation Chest: False + - type: Verification + item: Big Key (Ice Palace) + locations: + Ice Palace - Big Chest: False + Ice Palace - Big Key Chest: True + Ice Palace - Boss: False + Ice Palace - Compass Chest: True + Ice Palace - Freezor Chest: True + Ice Palace - Iced T Room: True + Ice Palace - Map Chest: True + Ice Palace - Spike Room: True + - type: Verification + item: Big Key (Misery Mire) + locations: + Misery Mire - Big Chest: False + Misery Mire - Big Key Chest: True + Misery Mire - Boss: False + Misery Mire - Bridge Chest: True + Misery Mire - Compass Chest: True + Misery Mire - Main Lobby: True + Misery Mire - Map Chest: True + Misery Mire - Spike Chest: True + - type: Verification + item: Big Key (Palace of Darkness) + locations: + Palace of Darkness - Big Chest: False + Palace of Darkness - Big Key Chest: True + Palace of Darkness - Boss: False + Palace of Darkness - Compass Chest: True + Palace of Darkness - Dark Basement - Left: True + Palace of Darkness - Dark Basement - Right: True + Palace of Darkness - Dark Maze - Bottom: True + Palace of Darkness - Dark Maze - Top: True + Palace of Darkness - Harmless Hellway: True + Palace of Darkness - Map Chest: True + Palace of Darkness - Shooter Room: True + Palace of Darkness - Stalfos Basement: True + Palace of Darkness - The Arena - Bridge: True + Palace of Darkness - The Arena - Ledge: True + - type: Verification + item: Big Key (Skull Woods) + locations: + Skull Woods - Big Chest: True + Skull Woods - Big Key Chest: True + Skull Woods - Boss: True + Skull Woods - Bridge Room: True + Skull Woods - Compass Chest: True + Skull Woods - Map Chest: True + Skull Woods - Pinball Room: True + Skull Woods - Pot Prison: True + - type: Verification + item: Big Key (Swamp Palace) + locations: + Swamp Palace - Big Chest: True + Swamp Palace - Big Key Chest: True + Swamp Palace - Boss: True + Swamp Palace - Compass Chest: True + Swamp Palace - Entrance: False + Swamp Palace - Flooded Room - Left: True + Swamp Palace - Flooded Room - Right: True + Swamp Palace - Map Chest: True + Swamp Palace - Waterfall Room: True + Swamp Palace - West Chest: True + - type: Verification + item: Big Key (Thieves Town) + locations: + Thieves' Town - Ambush Chest: True + Thieves' Town - Attic: False + Thieves' Town - Big Chest: False + Thieves' Town - Big Key Chest: True + Thieves' Town - Blind's Cell: False + Thieves' Town - Boss: False + Thieves' Town - Compass Chest: True + Thieves' Town - Map Chest: True + - type: Verification + item: Big Key (Tower of Hera) + locations: + Tower of Hera - Basement Cage: True + Tower of Hera - Big Chest: False + Tower of Hera - Big Key Chest: True + Tower of Hera - Boss: False + Tower of Hera - Compass Chest: False + Tower of Hera - Map Chest: True + - type: Verification + item: Big Key (Turtle Rock) + locations: + Turtle Rock - Big Chest: False + Turtle Rock - Big Key Chest: True + Turtle Rock - Boss: False + Turtle Rock - Chain Chomps: True + Turtle Rock - Compass Chest: True + Turtle Rock - Crystaroller Room: False + Turtle Rock - Eye Bridge - Bottom Left: False + Turtle Rock - Eye Bridge - Bottom Right: False + Turtle Rock - Eye Bridge - Top Left: False + Turtle Rock - Eye Bridge - Top Right: False + Turtle Rock - Roller Room - Left: True + Turtle Rock - Roller Room - Right: True \ No newline at end of file diff --git a/test/suite/strict_key_logic.yaml b/test/suite/strict_key_logic.yaml new file mode 100644 index 00000000..89eb11c0 --- /dev/null +++ b/test/suite/strict_key_logic.yaml @@ -0,0 +1,22 @@ +meta: + players: 1 +settings: + 1: + key_logic_algorithm: strict + keysanity: true +placements: + 1: + Hobo: Big Key (Misery Mire) + Waterfall Fairy - Left: Small Key (Misery Mire) +advanced_placements: + 1: + - type: Verification + item: Flippers + locations: + Misery Mire - Map Chest: False + Misery Mire - Main Lobby: False + Misery Mire - Bridge Chest: True + Misery Mire - Spike Chest: True + Misery Mire - Compass Chest: False + Misery Mire - Big Key Chest: False + Misery Mire - Boss: False \ No newline at end of file