diff --git a/BaseClasses.py b/BaseClasses.py index 23e8cc9d..e37e412e 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -35,6 +35,8 @@ class World(object): self.doorShuffle = doorShuffle.copy() self.intensity = {} self.door_type_mode = {} + self.trap_door_mode = {} + self.key_logic_algorithm = {} self.logic = logic.copy() self.mode = mode.copy() self.swords = swords.copy() @@ -162,6 +164,8 @@ class World(object): set_player_attr('pot_pool', {}) set_player_attr('decoupledoors', False) set_player_attr('door_type_mode', 'original') + set_player_attr('trap_door_mode', 'optional') + set_player_attr('key_logic_algorithm', 'default') set_player_attr('shopsanity', False) set_player_attr('mixed_travel', 'prevent') @@ -585,6 +589,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 = [] @@ -617,12 +622,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 @@ -706,6 +712,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) @@ -905,6 +912,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) @@ -1112,6 +1120,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): @@ -1341,6 +1357,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 @@ -1533,10 +1551,10 @@ class Region(object): or (item.bigkey and not self.world.bigkeyshuffle[item.player]) or (item.map and not self.world.mapshuffle[item.player]) or (item.compass and not self.world.compassshuffle[item.player])) - sewer_hack = self.world.mode[item.player] == 'standard' and item.name == 'Small Key (Escape)' - if sewer_hack or inside_dungeon_item: + # not all small keys to escape must be in escape + # sewer_hack = self.world.mode[item.player] == 'standard' and item.name == 'Small Key (Escape)' + if inside_dungeon_item: return self.dungeon and self.dungeon.is_dungeon_item(item) and item.player == self.player - return True def can_cause_bunny(self, player): @@ -2168,6 +2186,9 @@ class Door(object): return world.get_room(self.roomIndex, self.player).kind(self) return None + def dungeon_name(self): + return self.entrance.parent_region.dungeon.name if self.entrance.parent_region.dungeon else 'Cave' + def __eq__(self, other): return isinstance(other, self.__class__) and self.name == other.name @@ -2846,6 +2867,8 @@ class Spoiler(object): 'door_shuffle': self.world.doorShuffle, 'intensity': self.world.intensity, 'door_type_mode': self.world.door_type_mode, + 'trap_door_mode': self.world.trap_door_mode, + 'key_logic': self.world.key_logic_algorithm, 'decoupledoors': self.world.decoupledoors, 'dungeon_counters': self.world.dungeon_counters, 'item_pool': self.world.difficulty, @@ -3070,6 +3093,8 @@ class Spoiler(object): if self.metadata['door_shuffle'][player] != 'vanilla': outfile.write('Intensity:'.ljust(line_width) + '%s\n' % self.metadata['intensity'][player]) outfile.write('Door Type Mode:'.ljust(line_width) + '%s\n' % self.metadata['door_type_mode'][player]) + outfile.write('Trap Door Mode:'.ljust(line_width) + '%s\n' % self.metadata['trap_door_mode'][player]) + outfile.write('Key Logic Algorithm:'.ljust(line_width) + '%s\n' % self.metadata['key_logic'][player]) outfile.write('Decouple Doors:'.ljust(line_width) + '%s\n' % yn(self.metadata['decoupledoors'][player])) outfile.write('Experimental:'.ljust(line_width) + '%s\n' % yn(self.metadata['experimental'][player])) outfile.write('Dungeon Counters:'.ljust(line_width) + '%s\n' % self.metadata['dungeon_counters'][player]) @@ -3408,15 +3433,19 @@ orcrossed_mode = {"none": 0, "polar": 1, "grouped": 2, "limited": 3, "chaos": 4} # byte 12: KMB? FF?? (keep similar, mixed/tile flip, bonk drops, flute spots) flutespot_mode = {"vanilla": 0, "balanced": 1, "random": 2} -# byte 13: FBBB, TTSS (flute_mode, bow_mode, take_any, small_key_mode) +# byte 13: FBBB TTSS (flute_mode, bow_mode, take_any, small_key_mode) flute_mode = {'normal': 0, 'active': 1} keyshuffle_mode = {'none': 0, 'wild': 1, 'universal': 2} # reserved 8 modes? take_any_mode = {'none': 0, 'random': 1, 'fixed': 2} bow_mode = {'progressive': 0, 'silvers': 1, 'retro': 2, 'retro_silvers': 3} # additions -# psuedoboots does not effect code -# sfx_shuffle and other adjust items does not effect settings code +# byte 14: 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, 'optional': 1, 'boss': 2, 'oneway': 3} +key_logic_algo = {'default': 0, 'partial': 1, 'strict': 2} + +# sfx_shuffle and other adjust items does not affect settings code # Bump this when making changes that are not backwards compatible (nearly all of them) settings_version = 1 @@ -3466,7 +3495,10 @@ class Settings(object): | (0x20 if w.shuffle_bonk_drops[p] else 0) | (flutespot_mode[w.owFluteShuffle[p]] << 4), (flute_mode[w.flute_mode[p]] << 7 | bow_mode[w.bow_mode[p]] << 4 - | take_any_mode[w.take_any[p]] << 2 | keyshuffle_mode[w.keyshuffle[p]]) + | take_any_mode[w.take_any[p]] << 2 | keyshuffle_mode[w.keyshuffle[p]]), + + ((0x80 if w.pseudoboots[p] else 0) | overworld_map_mode[w.overworld_map[p]] << 6 + | trap_door_mode[w.trap_door_mode[p]] << 4 | key_logic_algo[w.key_logic_algorithm[p]]), ]) return base64.b64encode(code, "+-".encode()).decode() @@ -3547,6 +3579,12 @@ class Settings(object): args.take_any[p] = r(take_any_mode)[(settings[13] & 0xC) >> 2] args.keyshuffle[p] = r(keyshuffle_mode)[settings[13] & 0x3] + if len(settings) > 14: + args.pseudoboots[p] = True if settings[14] & 0x80 else False + args.overworld_map[p] = r(overworld_map_mode)[(settings[14] & 0x60) >> 6] + args.trap_door_mode[p] = r(trap_door_mode)[(settings[14] & 0x14) >> 4] + args.key_logic_algorithm[p] = r(key_logic_algo)[settings[14] & 0x07] + class KeyRuleType(FastEnum): WorstCase = 0 diff --git a/CLI.py b/CLI.py index 5fea49d3..ad1b157c 100644 --- a/CLI.py +++ b/CLI.py @@ -141,7 +141,8 @@ def parse_cli(argv, no_defaults=False): 'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor', 'heartbeep', 'remote_items', 'shopsanity', 'dropshuffle', 'pottery', 'keydropshuffle', 'mixed_travel', 'standardize_palettes', 'code', 'reduce_flashing', 'shuffle_sfx', - 'msu_resume', 'collection_rate', 'colorizepots', 'decoupledoors', 'door_type_mode', 'bonk_drops']: + 'msu_resume', 'collection_rate', 'colorizepots', 'decoupledoors', 'door_type_mode', + 'bonk_drops', 'trap_door_mode', 'key_logic_algorithm']: value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name) if player == 1: setattr(ret, name, {1: value}) @@ -223,6 +224,8 @@ def parse_settings(): "door_shuffle": "vanilla", "intensity": 3, "door_type_mode": "original", + "trap_door_mode": "optional", + "key_logic_algorithm": "default", "decoupledoors": False, "experimental": False, "dungeon_counters": "default", diff --git a/DoorShuffle.py b/DoorShuffle.py index f52365f3..de9a398a 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -226,22 +226,38 @@ def vanilla_key_logic(world, player): add_inaccessible_doors(world, player) entrances_map, potentials, connections = determine_entrance_list(world, player) - for builder in builders: + enabled_entrances = world.enabled_entrances[player] = {} + builder_queue = deque(builders) + last_key, loops = None, 0 + while len(builder_queue) > 0: + builder = builder_queue.popleft() origin_list = entrances_map[builder.name] - start_regions = convert_regions(origin_list, world, player) - doors = convert_key_doors(default_small_key_doors[builder.name], world, player) - key_layout = build_key_layout(builder, start_regions, doors, {}, world, player) - valid = validate_key_layout(key_layout, world, player) - if not valid: - logging.getLogger('').info('Vanilla key layout not valid %s', builder.name) - builder.key_door_proposal = doors - if player not in world.key_logic.keys(): - world.key_logic[player] = {} - analyze_dungeon(key_layout, world, player) - world.key_logic[player][builder.name] = key_layout.key_logic - world.key_layout[player][builder.name] = key_layout - log_key_logic(builder.name, key_layout.key_logic) - # if world.shuffle[player] == 'vanilla' and world.owShuffle[player] == 'vanilla' and world.owCrossed[player] == 'none' and not world.owMixed[player] and world.accessibility[player] == 'items' and not world.retro[player] and not world.keydropshuffle[player]: + find_enabled_origins(builder.sectors, enabled_entrances, origin_list, entrances_map, builder.name) + if len(origin_list) <= 0: + if last_key == builder.name or loops > 1000: + origin_name = (world.get_region(origin_list[0], player).entrances[0].parent_region.name + if len(origin_list) > 0 else 'no origin') + raise GenerationException(f'Infinite loop detected for "{builder.name}" located at {origin_name}') + builder_queue.append(builder) + last_key = builder.name + loops += 1 + else: + find_new_entrances(builder.master_sector, entrances_map, connections, potentials, + enabled_entrances, world, player) + start_regions = convert_regions(origin_list, world, player) + doors = convert_key_doors(default_small_key_doors[builder.name], world, player) + key_layout = build_key_layout(builder, start_regions, doors, {}, world, player) + valid = validate_key_layout(key_layout, world, player) + if not valid: + logging.getLogger('').info('Vanilla key layout not valid %s', builder.name) + builder.key_door_proposal = doors + if player not in world.key_logic.keys(): + world.key_logic[player] = {} + analyze_dungeon(key_layout, world, player) + world.key_logic[player][builder.name] = key_layout.key_logic + world.key_layout[player][builder.name] = key_layout + log_key_logic(builder.name, key_layout.key_logic) + # if world.shuffle[player] == 'vanilla' and world.accessibility[player] == 'items' and not world.retro[player] and not world.keydropshuffle[player]: # validate_vanilla_key_logic(world, player) @@ -1745,12 +1761,12 @@ class DoorTypePool: self.tricky += counts[6] def chaos_shuffle(self, counts): - weights = [1, 2, 4, 3, 2, 1] + weights = [1, 2, 4, 3, 2] return [random.choices(self.get_choices(counts[i]), weights=weights)[0] for i, c in enumerate(counts)] @staticmethod def get_choices(number): - return [max(number+i, 0) for i in range(-1, 5)] + return [max(number+i, 0) for i in range(-1, 4)] class BuilderDoorCandidates: @@ -1782,58 +1798,68 @@ def shuffle_door_types(door_type_pools, paths, world, player): def shuffle_trap_doors(door_type_pools, paths, start_regions_map, world, player): used_doors = set() for pool, door_type_pool in door_type_pools: - ttl = 0 - suggestion_map, trap_map, flex_map = {}, {}, {} - remaining = door_type_pool.traps - if player in world.custom_door_types: - custom_trap_doors = world.custom_door_types[player]['Trap Door'] - else: - custom_trap_doors = defaultdict(list) + if world.trap_door_mode[player] != 'oneway': + ttl = 0 + suggestion_map, trap_map, flex_map = {}, {}, {} + remaining = door_type_pool.traps + if player in world.custom_door_types and 'Trap Door' in world.custom_door_types[player]: + custom_trap_doors = world.custom_door_types[player]['Trap Door'] + else: + custom_trap_doors = defaultdict(list) - for dungeon in pool: - builder = world.dungeon_layouts[player][dungeon] - find_trappable_candidates(builder, world, player) - if custom_trap_doors[dungeon]: - builder.candidates.trap = filter_key_door_pool(builder.candidates.trap, custom_trap_doors[dungeon]) - remaining -= len(custom_trap_doors[dungeon]) - ttl += len(builder.candidates.trap) - if ttl == 0: - continue - for dungeon in pool: - builder = world.dungeon_layouts[player][dungeon] - proportion = len(builder.candidates.trap) - calc = int(round(proportion * door_type_pool.traps/ttl)) - suggested = min(proportion, calc) - remaining -= suggested - suggestion_map[dungeon] = suggested - flex_map[dungeon] = (proportion - suggested) if suggested < proportion else 0 - for dungeon in pool: - builder = world.dungeon_layouts[player][dungeon] - valid_traps, trap_number = find_valid_trap_combination(builder, suggestion_map[dungeon], - start_regions_map[dungeon], paths, world, player, - drop=True) - trap_map[dungeon] = valid_traps - if trap_number < suggestion_map[dungeon]: - flex_map[dungeon] = 0 - remaining += suggestion_map[dungeon] - trap_number - suggestion_map[dungeon] = trap_number - builder_order = [x for x in pool if flex_map[x] > 0] - random.shuffle(builder_order) - queue = deque(builder_order) - while len(queue) > 0 and remaining > 0: - dungeon = queue.popleft() - builder = world.dungeon_layouts[player][dungeon] - increased = suggestion_map[dungeon] + 1 - valid_traps, trap_number = find_valid_trap_combination(builder, increased, start_regions_map[dungeon], - paths, world, player) - if valid_traps: + for dungeon in pool: + builder = world.dungeon_layouts[player][dungeon] + if 'Mire Warping Pool' in builder.master_sector.region_set(): + custom_trap_doors[dungeon].append(world.get_door('Mire Warping Pool ES', player)) + world.custom_door_types[player]['Trap Door'] = custom_trap_doors + find_trappable_candidates(builder, world, player) + if custom_trap_doors[dungeon]: + builder.candidates.trap = filter_key_door_pool(builder.candidates.trap, custom_trap_doors[dungeon]) + remaining -= len(custom_trap_doors[dungeon]) + ttl += len(builder.candidates.trap) + if ttl == 0: + continue + for dungeon in pool: + builder = world.dungeon_layouts[player][dungeon] + proportion = len(builder.candidates.trap) + calc = int(round(proportion * door_type_pool.traps/ttl)) + suggested = min(proportion, calc) + remaining -= suggested + suggestion_map[dungeon] = suggested + flex_map[dungeon] = (proportion - suggested) if suggested < proportion else 0 + for dungeon in pool: + builder = world.dungeon_layouts[player][dungeon] + valid_traps, trap_number = find_valid_trap_combination(builder, suggestion_map[dungeon], + start_regions_map[dungeon], paths, world, player, + drop=True) trap_map[dungeon] = valid_traps - remaining -= 1 - suggestion_map[dungeon] = increased - flex_map[dungeon] -= 1 - if flex_map[dungeon] > 0: - queue.append(dungeon) - # time to re-assign + if trap_number < suggestion_map[dungeon]: + flex_map[dungeon] = 0 + remaining += suggestion_map[dungeon] - trap_number + suggestion_map[dungeon] = trap_number + builder_order = [x for x in pool if flex_map[x] > 0] + random.shuffle(builder_order) + queue = deque(builder_order) + while len(queue) > 0 and remaining > 0: + dungeon = queue.popleft() + builder = world.dungeon_layouts[player][dungeon] + increased = suggestion_map[dungeon] + 1 + valid_traps, trap_number = find_valid_trap_combination(builder, increased, start_regions_map[dungeon], + paths, world, player) + if valid_traps: + trap_map[dungeon] = valid_traps + remaining -= 1 + suggestion_map[dungeon] = increased + flex_map[dungeon] -= 1 + if flex_map[dungeon] > 0: + queue.append(dungeon) + # time to re-assign + else: + trap_map = {dungeon: [] for dungeon in pool} + for dungeon in pool: + builder = world.dungeon_layouts[player][dungeon] + if 'Mire Warping Pool' in builder.master_sector.region_set(): + trap_map[dungeon].append(world.get_door('Mire Warping Pool ES', player)) reassign_trap_doors(trap_map, world, player) for name, traps in trap_map.items(): used_doors.update(traps) @@ -1845,7 +1871,7 @@ def shuffle_big_key_doors(door_type_pools, used_doors, start_regions_map, world, ttl = 0 suggestion_map, bk_map, flex_map = {}, {}, {} remaining = door_type_pool.bigs - if player in world.custom_door_types: + if player in world.custom_door_types and 'Big Key Door' in world.custom_door_types[player]: custom_bk_doors = world.custom_door_types[player]['Big Key Door'] else: custom_bk_doors = defaultdict(list) @@ -1907,7 +1933,7 @@ def shuffle_small_key_doors(door_type_pools, used_doors, start_regions_map, worl suggestion_map, small_map, flex_map = {}, {}, {} remaining = door_type_pool.smalls total_keys = remaining - if player in world.custom_door_types: + if player in world.custom_door_types and 'Key Door' in world.custom_door_types[player]: custom_key_doors = world.custom_door_types[player]['Key Door'] else: custom_key_doors = defaultdict(list) @@ -2007,7 +2033,7 @@ def shuffle_bomb_dash_doors(door_type_pools, used_doors, start_regions_map, worl remaining_bomb = door_type_pool.bombable remaining_dash = door_type_pool.dashable - if player in world.custom_door_types: + if player in world.custom_door_types and 'Bomb Door' in world.custom_door_types[player]: custom_bomb_doors = world.custom_door_types[player]['Bomb Door'] custom_dash_doors = world.custom_door_types[player]['Dash Door'] else: @@ -2139,14 +2165,14 @@ def find_trappable_candidates(builder, world, player): for ext in world.get_region(r, player).exits: if ext.door: d = ext.door - if d.blocked and d.trapFlag != 0 and 'Boss' not in d.name and 'Agahnim' not in d.name: + if d.blocked and d.trapFlag != 0 and exclude_boss_traps(d): builder.candidates.trap.append(d) def find_valid_trap_combination(builder, suggested, start_regions, paths, world, player, drop=True): trap_door_pool = builder.candidates.trap trap_doors_needed = suggested - if player in world.custom_door_types: + if player in world.custom_door_types and 'Trap Door' in world.custom_door_types[player]: custom_trap_doors = world.custom_door_types[player]['Trap Door'][builder.name] else: custom_trap_doors = [] @@ -2282,7 +2308,7 @@ def reassign_trap_doors(trap_map, world, player): logger = logging.getLogger('') for name, traps in trap_map.items(): builder = world.dungeon_layouts[player][name] - queue = deque(find_current_trap_doors(builder)) + queue = deque(find_current_trap_doors(builder, world, player)) while len(queue) > 0: d = queue.pop() if d.type is DoorType.Interior and d not in traps: @@ -2301,16 +2327,21 @@ def reassign_trap_doors(trap_map, world, player): d.blocked = False for d in traps: change_door_to_trap(d, world, player) - world.spoiler.set_door_type(d.name, 'Trap Door', player) - logger.debug('Trap Door: %s', d.name) + world.spoiler.set_door_type(f'{d.name} ({d.dungeon_name()})', 'Trap Door', player) + logger.debug(f'Trap Door: {d.name} ({d.dungeon_name()})') -def find_current_trap_doors(builder): +def exclude_boss_traps(d): + return ' Boss ' not in d.name and ' Agahnim ' not in d.name and d.name not in ['Skull Spike Corner SW'] + + +def find_current_trap_doors(builder, world, player): + checker = exclude_boss_traps if world.trap_door_mode[player] in ['vanilla', 'optional'] else (lambda x: True) current_doors = [] for region in builder.master_sector.regions: for ext in region.exits: d = ext.door - if d and d.blocked and d.trapFlag != 0: # could exclude removing boss doors here + if d and d.blocked and d.trapFlag != 0 and checker(d): current_doors.append(d) return current_doors @@ -2425,7 +2456,7 @@ def find_big_key_door_candidates(region, checked, used, world, player): def find_valid_bk_combination(builder, suggested, start_regions, world, player, drop=True): bk_door_pool = builder.candidates.big bk_doors_needed = suggested - if player in world.custom_door_types: + if player in world.custom_door_types and 'Big Key Door' in world.custom_door_types[player]: custom_bk_doors = world.custom_door_types[player]['Big Key Door'][builder.name] else: custom_bk_doors = [] @@ -2494,12 +2525,14 @@ def reassign_big_key_doors(bk_map, world, player): if d1.type is DoorType.Interior: change_door_to_big_key(d1, world, player) d2.bigKey = True # ensure flag is set + if d2.smallKey: + d2.smallKey = False else: world.paired_doors[player].append(PairedDoor(d1.name, d2.name)) change_door_to_big_key(d1, world, player) change_door_to_big_key(d2, world, player) - world.spoiler.set_door_type(d1.name+' <-> '+d2.name, 'Big Key Door', player) - logger.debug(f'Big Key Door: {d1.name} <-> {d2.name}') + world.spoiler.set_door_type(f'{d1.name} <-> {d2.name} ({d1.dungeon_name()})', 'Big Key Door', player) + logger.debug(f'Big Key Door: {d1.name} <-> {d2.name} ({d1.dungeon_name()})') else: d = obj if d.type is DoorType.Interior: @@ -2516,12 +2549,14 @@ def reassign_big_key_doors(bk_map, world, player): if stateful_door(d.dest, dest_room.kind(d.dest)): change_door_to_big_key(d.dest, world, player) add_pair(d, d.dest, world, player) - world.spoiler.set_door_type(d.name, 'Big Key Door', player) - logger.debug(f'Big Key Door: {d.name}') + world.spoiler.set_door_type(f'{d.name} ({d.dungeon_name()})', 'Big Key Door', player) + logger.debug(f'Big Key Door: {d.name} ({d.dungeon_name()})') def change_door_to_big_key(d, world, player): d.bigKey = True + if d.smallKey: + d.smallKey = False room = world.get_room(d.roomIndex, player) if room.doorList[d.doorListPos][1] != DoorKind.BigKey: verify_door_list_pos(d, room, world, player) @@ -2565,7 +2600,7 @@ def find_valid_combination(builder, target, start_regions, world, player, drop_k logger = logging.getLogger('') key_door_pool = list(builder.candidates.small) key_doors_needed = target - if player in world.custom_door_types: + if player in world.custom_door_types and 'Key Door' in world.custom_door_types[player]: custom_key_doors = world.custom_door_types[player]['Key Door'][builder.name] else: custom_key_doors = [] @@ -2693,7 +2728,7 @@ def find_valid_bd_combination(builder, suggested, world, player): bd_door_pool = builder.candidates.bomb_dash bomb_doors_needed, dash_doors_needed = suggested ttl_needed = bomb_doors_needed + dash_doors_needed - if player in world.custom_door_types: + if player in world.custom_door_types and 'Bomb Door' in world.custom_door_types[player]: custom_bomb_doors = world.custom_door_types[player]['Bomb Door'][builder.name] custom_dash_doors = world.custom_door_types[player]['Dash Door'][builder.name] else: @@ -2769,7 +2804,7 @@ def do_bombable_dashable(proposal, kind, world, player): change_door_to_kind(d1, kind, world, player) change_door_to_kind(d2, kind, world, player) spoiler_type = 'Bomb Door' if kind == DoorKind.Bombable else 'Dash Door' - world.spoiler.set_door_type(d1.name+' <-> '+d2.name, spoiler_type, player) + world.spoiler.set_door_type(f'{d1.name} <-> {d2.name} ({d1.dungeon_name()})', spoiler_type, player) else: d = obj if d.type is DoorType.Interior: @@ -2783,7 +2818,7 @@ def do_bombable_dashable(proposal, kind, world, player): change_door_to_kind(d.dest, kind, world, player) add_pair(d, d.dest, world, player) spoiler_type = 'Bomb Door' if kind == DoorKind.Bombable else 'Dash Door' - world.spoiler.set_door_type(d.name, spoiler_type, player) + world.spoiler.set_door_type(f'{d.name} ({d.dungeon_name()})', spoiler_type, player) def find_current_bd_doors(builder, world): @@ -2986,8 +3021,8 @@ def reassign_key_doors(small_map, world, player): world.paired_doors[player].append(PairedDoor(d1.name, d2.name)) change_door_to_small_key(d1, world, player) change_door_to_small_key(d2, world, player) - world.spoiler.set_door_type(d1.name+' <-> '+d2.name, 'Key Door', player) - logger.debug('Key Door: %s', d1.name+' <-> '+d2.name) + world.spoiler.set_door_type(f'{d1.name} <-> {d2.name} ({d1.dungeon_name()})', 'Key Door', player) + logger.debug(f'Key Door: {d1.name} <-> {d2.name} ({d1.dungeon_name()})') else: d = obj if d.type is DoorType.Interior: @@ -3003,8 +3038,8 @@ def reassign_key_doors(small_map, world, player): if stateful_door(d.dest, dest_room.kind(d.dest)): change_door_to_small_key(d.dest, world, player) add_pair(d, d.dest, world, player) - world.spoiler.set_door_type(d.name, 'Key Door', player) - logger.debug('Key Door: %s', d.name) + world.spoiler.set_door_type(f'{d.name} ({d.dungeon_name()})', 'Key Door', player) + logger.debug(f'Key Door: {d.name} ({d.dungeon_name()})') def change_door_to_small_key(d, world, player): @@ -3194,7 +3229,7 @@ def change_pair_type(door, new_type, world, player): room_b.change(door.dest.doorListPos, new_type) add_pair(door, door.dest, world, player) spoiler_type = 'Bomb Door' if new_type == DoorKind.Bombable else 'Dash Door' - world.spoiler.set_door_type(door.name + ' <-> ' + door.dest.name, spoiler_type, player) + world.spoiler.set_door_type(f'{door.name} <-> {door.dest.name} ({door.dungeon_name()})', spoiler_type, player) def remove_pair_type_if_present(door, world, player): @@ -4550,7 +4585,7 @@ door_type_counts = { 'Swamp Palace': (6, 0, 0, 2, 0, 0, 0), 'Palace of Darkness': (6, 1, 1, 3, 2, 0, 0), 'Misery Mire': (6, 3, 5, 2, 0, 0, 0), - 'Skull Woods': (5, 0, 2, 2, 0, 1, 0), + 'Skull Woods': (5, 0, 1, 2, 0, 1, 0), 'Ice Palace': (6, 1, 3, 0, 0, 0, 0), 'Tower of Hera': (1, 1, 0, 0, 0, 0, 0), 'Thieves Town': (3, 1, 2, 1, 1, 0, 0), diff --git a/DungeonGenerator.py b/DungeonGenerator.py index 2140520e..9be244f8 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -772,6 +772,13 @@ def connect_simple_door(exit_door, region): special_big_key_doors = ['Hyrule Dungeon Cellblock Door', "Thieves Blind's Cell Door"] +std_special_big_key_doors = ['Hyrule Castle Throne Room Tapestry'] + special_big_key_doors + + +def get_special_big_key_doors(world, player): + if world.mode[player] == 'standard': + return std_special_big_key_doors + return special_big_key_doors class ExplorationState(object): @@ -1002,7 +1009,8 @@ class ExplorationState(object): if self.can_traverse(door): if door.controller: door = door.controller - if (door in big_key_door_proposal or door.name in special_big_key_doors) and not self.big_key_opened: + if (door in big_key_door_proposal + or door.name in get_special_big_key_doors(world, player)) and not self.big_key_opened: if not self.in_door_list(door, self.big_doors): self.append_door_to_list(door, self.big_doors) elif door.req_event is not None and door.req_event not in self.events: @@ -3509,6 +3517,8 @@ def identify_branching_issues(dungeon_map, builder_info): unconnected_builders[name] = builder for hook, door_list in unreached_doors.items(): builder.unfulfilled[hook] += len(door_list) + elif package: + builder.throne_door, builder.throne_sector, builder.chosen_lobby = package return unconnected_builders @@ -3554,7 +3564,8 @@ def check_for_valid_layout(builder, sector_list, builder_info): split_list['Sewers'].remove(temp_builder.throne_door.entrance.parent_region.name) builder.exception_list = list(sector_list) return True, {}, package - except (GenerationException, NeutralizingException, OtherGenException): + except (GenerationException, NeutralizingException, OtherGenException) as e: + logging.getLogger('').info(f'Bailing on this layout for {builder.name}', exc_info=1) builder.split_dungeon_map = None builder.valid_proposal = None if temp_builder.name == 'Hyrule Castle' and temp_builder.throne_door: diff --git a/Fill.py b/Fill.py index 9648ca62..4507ddeb 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) @@ -143,21 +142,29 @@ def verify_spot_to_fill(location, item_to_place, max_exp_state, single_player_pl key_pool, world): 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: test_state = max_exp_state if not single_player_placement or location.player == item_to_place.player: + test_state.sweep_for_events() if location.can_fill(test_state, item_to_place, perform_access_check): - if valid_key_placement(item_to_place, location, key_pool, world): + if valid_key_placement(item_to_place, location, key_pool, test_state, world): if item_to_place.crystal or valid_dungeon_placement(item_to_place, location, world): return location 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 -def valid_key_placement(item, location, key_pool, world): +def valid_key_placement(item, location, key_pool, collection_state, world): if not valid_reserved_placement(item, location, world): return False if ((not item.smallkey and not item.bigkey) or item.player != location.player @@ -174,7 +181,15 @@ def valid_key_placement(item, location, key_pool, world): prize_loc = world.get_location(key_logic.prize_location, location.player) cr_count = world.crystals_needed_for_gt[location.player] wild_keys = world.keyshuffle[item.player] != 'none' - return key_logic.check_placement(unplaced_keys, wild_keys, location if item.bigkey else None, prize_loc, cr_count) + if wild_keys: + reached_keys = {x for x in collection_state.locations_checked + if x.item and x.item.name == key_logic.small_key_name and x.item.player == item.player} + else: + reached_keys = set() # will be calculated using key logic in a moment + self_locking_keys = sum(1 for d, rule in key_logic.door_rules.items() if rule.allow_small + and rule.small_location.item and rule.small_location.item.name == key_logic.small_key_name) + return key_logic.check_placement(unplaced_keys, wild_keys, reached_keys, self_locking_keys, + location if item.bigkey else None, prize_loc, cr_count) else: return not item.is_inside_dungeon_item(world) @@ -205,6 +220,7 @@ def track_outside_keys(item, location, world): if loc_dungeon and loc_dungeon.name == item_dungeon: return # this is an inside key world.key_logic[item.player][item_dungeon].outside_keys += 1 + world.key_logic[item.player][item_dungeon].outside_keys_locations.add(location) def track_dungeon_items(item, location, world): @@ -345,7 +361,9 @@ def find_spot_for_item(item_to_place, locations, world, base_state, pool, test_state = maximum_exploration_state if (not single_player_placement or location.player == item_to_place.player) \ and location.can_fill(test_state, item_to_place, perform_access_check) \ - and valid_key_placement(item_to_place, location, pool if (keys_in_itempool and keys_in_itempool[item_to_place.player]) else world.itempool, world): + and valid_key_placement(item_to_place, location, + pool if (keys_in_itempool and keys_in_itempool[item_to_place.player]) else world.itempool, + test_state, world): return location if item_to_place.smallkey or item_to_place.bigkey: location.item = old_item @@ -386,11 +404,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] @@ -430,10 +444,16 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None random.shuffle(fill_locations) fill_locations.reverse() - # Make sure the escape small key is placed first in standard with key shuffle to prevent running out of spots - # todo: crossed - progitempool.sort(key=lambda item: 1 if item.name == 'Small Key (Escape)' - and world.keyshuffle[item.player] != 'none' and world.mode[item.player] == 'standard' else 0) + # Make sure the escape keys ire placed first in standard to prevent running out of spots + def std_item_sort(item): + if world.mode[item.player] == 'standard': + if item.name == 'Small Key (Escape)': + return 1 + if item.name == 'Big Key (Escape)': + return 2 + return 0 + + progitempool.sort(key=std_item_sort) key_pool = [x for x in progitempool if x.smallkey] # sort maps and compasses to the back -- this may not be viable in equitable & ambrosia @@ -482,6 +502,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(): @@ -656,6 +690,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 45007237..9a0eec7b 100644 --- a/ItemList.py +++ b/ItemList.py @@ -1589,6 +1589,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/KeyDoorShuffle.py b/KeyDoorShuffle.py index bb1eaaa6..2cbda4be 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -5,7 +5,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_table -from DungeonGenerator import ExplorationState, special_big_key_doors, count_locations_exclude_big_chest, prize_or_event +from DungeonGenerator import ExplorationState, get_special_big_key_doors, count_locations_exclude_big_chest, prize_or_event from DungeonGenerator import reserved_location, blind_boss_unavail @@ -59,13 +59,16 @@ class KeyLogic(object): self.placement_rules = [] self.location_rules = {} self.outside_keys = 0 + self.outside_keys_locations = set() self.dungeon = dungeon_name self.sm_doors = {} self.prize_location = None - def check_placement(self, unplaced_keys, wild_keys, big_key_loc=None, prize_loc=None, cr_count=7): + def check_placement(self, unplaced_keys, wild_keys, reached_keys, self_locking_keys, + big_key_loc=None, prize_loc=None, cr_count=7): for rule in self.placement_rules: - if not rule.is_satisfiable(self.outside_keys, wild_keys, unplaced_keys, big_key_loc, prize_loc, cr_count): + if not rule.is_satisfiable(self.outside_keys_locations, wild_keys, reached_keys, self_locking_keys, + unplaced_keys, big_key_loc, prize_loc, cr_count): return False if big_key_loc: for rule_a, rule_b in itertools.combinations(self.placement_rules, 2): @@ -159,7 +162,8 @@ class PlacementRule(object): left -= rule_needed return False - def is_satisfiable(self, outside_keys, wild_keys, unplaced_keys, big_key_loc, prize_location, cr_count): + def is_satisfiable(self, outside_keys_locations, wild_keys, reached_keys, self_locking_keys, unplaced_keys, + big_key_loc, prize_location, cr_count): if self.prize_relevance and prize_location: if self.prize_relevance == 'BigBomb': if prize_location.item.name not in ['Crystal 5', 'Crystal 6']: @@ -186,10 +190,11 @@ class PlacementRule(object): check_locations = self.check_locations_wo_bk if bk_blocked else self.check_locations_w_bk if not bk_blocked and check_locations is None: return True - available_keys = outside_keys + available_keys = len(outside_keys_locations) # todo: sometimes we need an extra empty chest to accomodate the big key too # dungeon bias seed 563518200 for example threshold = self.needed_keys_wo_bk if bk_blocked else self.needed_keys_w_bk + threshold -= self_locking_keys if not wild_keys: empty_chests = 0 for loc in check_locations: @@ -200,7 +205,8 @@ class PlacementRule(object): place_able_keys = min(empty_chests, unplaced_keys) available_keys += place_able_keys else: - available_keys += unplaced_keys + available_keys += len(reached_keys.difference(outside_keys_locations)) # already placed small keys + available_keys += unplaced_keys # small keys not yet placed return available_keys >= threshold @@ -1002,7 +1008,7 @@ def find_worst_counter_wo_bk(small_key_num, accessible_set, door, odd_ctr, key_c def open_a_door(door, child_state, flat_proposal, world, player): - if door.bigKey or door.name in special_big_key_doors: + if door.bigKey or door.name in get_special_big_key_doors(world, player): child_state.big_key_opened = True child_state.avail_doors.extend(child_state.big_doors) child_state.opened_doors.extend(set([d.door for d in child_state.big_doors])) @@ -1485,7 +1491,8 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa else: 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_small_locations = cnt_avail_small_locations(ttl_locations, ttl_small_key_only, state, + key_layout, world, player) available_big_locations = cnt_avail_big_locations(ttl_locations, state, world, player) if invalid_self_locking_key(key_layout, state, prev_state, prev_avail, world, player): return False @@ -1615,18 +1622,24 @@ def determine_prize_lock(key_layout, world, player): key_layout.prize_can_lock = True -def cnt_avail_small_locations(free_locations, key_only, state, world, player): - if world.keyshuffle[player] == 'none': +def cnt_avail_small_locations(free_locations, key_only, state, key_layout, world, player): + std_flag = world.mode[player] == 'standard' and key_layout.sector.name == 'Hyrule Castle' + if world.keyshuffle[player] == 'none' or std_flag: bk_adj = 1 if state.big_key_opened and not state.big_key_special else 0 - avail_chest_keys = min(free_locations - bk_adj, state.key_locations - key_only) + # this is the secret passage, could expand to Uncle/Links House with appropriate logic + std_adj = 1 if std_flag and world.keyshuffle[player] != 'none' else 0 + avail_chest_keys = min(free_locations + std_adj - bk_adj, state.key_locations - key_only) return max(0, avail_chest_keys + key_only - state.used_smalls) return state.key_locations - state.used_smalls def cnt_avail_small_locations_by_ctr(free_locations, counter, layout, world, player): - if world.keyshuffle[player] == 'none': + std_flag = world.mode[player] == 'standard' and layout.sector.name == 'Hyrule Castle' + if world.keyshuffle[player] == 'none' or std_flag: bk_adj = 1 if counter.big_key_opened and not layout.big_key_special else 0 - avail_chest_keys = min(free_locations - bk_adj, layout.max_chests) + # this is the secret passage, could expand to Uncle/Links House with appropriate logic + std_adj = 1 if std_flag and world.keyshuffle[player] != 'none' else 0 + avail_chest_keys = min(free_locations + std_adj - bk_adj, layout.max_chests) return max(0, avail_chest_keys + len(counter.key_only_locations) - counter.used_keys) return layout.max_chests + len(counter.key_only_locations) - counter.used_keys @@ -1683,10 +1696,10 @@ def create_key_counters(key_layout, world, player): if door.dest in flat_proposal and door.type != DoorType.SpiralStairs: key_layout.found_doors.add(door.dest) child_state = parent_state.copy() - if door.bigKey or door.name in special_big_key_doors: + if door.bigKey or door.name in get_special_big_key_doors(world, player): key_layout.key_logic.bk_doors.add(door) # open the door, if possible - if can_open_door(door, child_state, world, player): + if can_open_door(door, child_state, key_layout, world, player): open_a_door(door, child_state, flat_proposal, world, player) expand_key_state(child_state, flat_proposal, world, player) code = state_id(child_state, key_layout.flat_prop) @@ -1707,14 +1720,15 @@ def find_outside_connection(region): return None, None -def can_open_door(door, state, world, player): +def can_open_door(door, state, key_layout, world, player): if state.big_key_opened: ttl_locations = count_free_locations(state, world, player) else: 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) + available_small_locations = cnt_avail_small_locations(ttl_locations, ttl_small_key_only, state, + key_layout, world, player) return available_small_locations > 0 elif door.bigKey: available_big_locations = cnt_avail_big_locations(ttl_locations, state, world, player) diff --git a/Main.py b/Main.py index 959afc01..c05828fc 100644 --- a/Main.py +++ b/Main.py @@ -35,7 +35,7 @@ from source.overworld.EntranceShuffle2 import link_entrances_new from source.tools.BPS import create_bps_from_data from source.classes.CustomSettings import CustomSettings -__version__ = '1.2.0.7-u' +__version__ = '1.2.0.8-u' from source.classes.BabelFish import BabelFish @@ -116,6 +116,8 @@ def main(args, seed=None, fish=None): world.beemizer = args.beemizer.copy() world.intensity = {player: random.randint(1, 3) if args.intensity[player] == 'random' else int(args.intensity[player]) for player in range(1, world.players + 1)} world.door_type_mode = args.door_type_mode.copy() + world.trap_door_mode = args.trap_door_mode.copy() + world.key_logic_algorithm = args.key_logic_algorithm.copy() world.decoupledoors = args.decoupledoors.copy() world.experimental = args.experimental.copy() world.dungeon_counters = args.dungeon_counters.copy() diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 597cdc2f..0328487c 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -108,6 +108,15 @@ These are now independent of retro mode and have three options: None, Random, an * Bonk Fairy (Dark) # Bug Fixes and Notes +* 1.2.0.8-u + * New Features: trap_door_mode and key_logic_algorithm + * Change S&Q in door shuffle + standard during escape to spawn as Uncle + * Fix for vanilla doors + certain ER modes + * Fix for unintentional decoupled door in standard + * Fix a problem with BK doors being one-sided + * Change to how wilds keys are placed in standard, better randomization + * Removed a Triforce text + * Fix for Desert Tiles 1 key door * 1.2.0.7-u * Fix for some misery mire key logic * Minor standard generation fix diff --git a/Rom.py b/Rom.py index 03d10639..5041f030 100644 --- a/Rom.py +++ b/Rom.py @@ -38,7 +38,7 @@ from source.dungeon.RoomList import Room0127 JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '45aa34e724750862234f9a4b09caa3d6' +RANDOMIZERBASEHASH = '1694ba41bf6ab7086f05e914e8d08433' class JsonRom(object): diff --git a/RoomData.py b/RoomData.py index 85c3b1fd..d8d3a82a 100644 --- a/RoomData.py +++ b/RoomData.py @@ -103,7 +103,7 @@ def create_rooms(world, player): Room(player, 0x60, 0x51309).door(Position.NorthE2, DoorKind.NormalLow2).door(Position.East2, DoorKind.NormalLow2).door(Position.East2, DoorKind.ToggleFlag).door(Position.EastN, DoorKind.Normal).door(Position.SouthE, DoorKind.Normal).door(Position.SouthE, DoorKind.IncognitoEntrance), Room(player, 0x61, 0x51454).door(Position.West2, DoorKind.NormalLow).door(Position.West2, DoorKind.ToggleFlag).door(Position.East2, DoorKind.NormalLow).door(Position.East2, DoorKind.ToggleFlag).door(Position.South2, DoorKind.NormalLow).door(Position.South2, DoorKind.IncognitoEntrance).door(Position.WestN, DoorKind.Normal), Room(player, 0x62, 0x51577).door(Position.West2, DoorKind.NormalLow2).door(Position.West2, DoorKind.ToggleFlag).door(Position.NorthW2, DoorKind.NormalLow2).door(Position.North, DoorKind.Normal).door(Position.SouthW, DoorKind.Normal).door(Position.SouthW, DoorKind.IncognitoEntrance), - Room(player, 0x63, 0xf88ed).door(Position.NorthE, DoorKind.StairKey).door(Position.InteriorW, DoorKind.TrapTriggerable).door(Position.SouthW, DoorKind.DungeonEntrance), # looked like a huge typo - I had to guess on StairKey + Room(player, 0x63, 0xf88ed).door(Position.NorthW, DoorKind.StairKey).door(Position.InteriorW, DoorKind.TrapTriggerable).door(Position.SouthW, DoorKind.DungeonEntrance), # looked like a huge typo - I had to guess on StairKey Room(player, 0x64, 0xfda53).door(Position.InteriorS, DoorKind.Trap2), Room(player, 0x65, 0xfdac5).door(Position.InteriorS, DoorKind.Normal), Room(player, 0x66, 0xfa01b).door(Position.InteriorE2, DoorKind.Waterfall).door(Position.SouthW2, DoorKind.NormalLow2).door(Position.SouthW2, DoorKind.ToggleFlag).door(Position.InteriorW2, DoorKind.NormalLow2), diff --git a/Rules.py b/Rules.py index 3fe7e43e..11c8df16 100644 --- a/Rules.py +++ b/Rules.py @@ -1459,7 +1459,7 @@ def standard_rules(world, player): # zelda should be saved before agahnim is in play add_rule(world.get_location('Agahnim 1', player), lambda state: state.has('Zelda Delivered', player)) - # too restrictive for crossed? + # uncle can't have keys generally because unplaced items aren't used here def uncle_item_rule(item): copy_state = CollectionState(world) copy_state.collect(item) @@ -1817,13 +1817,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) @@ -1870,10 +1873,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/Text.py b/Text.py index 13317afb..5d023818 100644 --- a/Text.py +++ b/Text.py @@ -91,7 +91,6 @@ Triforce_texts = [ 'Who stole the fourth triangle?', 'Trifource?\nMore Like Tritrice, am I right?' '\n Well Done!', - 'You just wasted 2 hours of your life.', 'This was meant to be a trapezoid', # these ones are from web randomizer "\n G G", diff --git a/data/base2current.bps b/data/base2current.bps index 7daea3d4..d629c08c 100644 Binary files a/data/base2current.bps and b/data/base2current.bps differ diff --git a/mystery_example.yml b/mystery_example.yml index fdb292bf..a83cf5f5 100644 --- a/mystery_example.yml +++ b/mystery_example.yml @@ -41,6 +41,14 @@ big: 2 all: 1 chaos: 1 + trap_door_mode: + vanilla: 1 + boss: 0 + oneway: 0 + key_logic_algorithm: + default: 1 + partial: 0 + strict: 0 decoupledoors: off dropshuffle: on: 1 diff --git a/mystery_testsuite.yml b/mystery_testsuite.yml index aca535c3..f919b7cc 100644 --- a/mystery_testsuite.yml +++ b/mystery_testsuite.yml @@ -7,12 +7,30 @@ algorithm: district: 1 door_shuffle: vanilla: 1 - basic: 2 + basic: 1 + partitioned: 1 crossed: 3 # crossed yield more errors so is preferred intensity: 1: 1 2: 1 3: 2 # intensity 3 usually yield more errors +door_type_mode: + original: 2 + big: 2 + all: 1 + chaos: 1 +trap_door_mode: + vanilla: 3 # more errors + optional: 1 + boss: 1 + oneway: 1 +key_logic_algorithm: + default: 1 + partial: 0 + strict: 0 +decoupledoors: + off: 9 # more strict + on: 1 dropshuffle: on: 1 off: 1 diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index 97915be7..9f0d2d77 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -239,6 +239,21 @@ "chaos" ] }, + "trap_door_mode": { + "choices": [ + "vanilla", + "optional", + "boss", + "oneway" + ] + }, + "key_logic_algorithm": { + "choices": [ + "default", + "partial", + "strict" + ] + }, "decoupledoors": { "action": "store_true", "type": "bool" diff --git a/resources/app/cli/lang/en.json b/resources/app/cli/lang/en.json index 73e35b83..f76bf61b 100644 --- a/resources/app/cli/lang/en.json +++ b/resources/app/cli/lang/en.json @@ -284,6 +284,19 @@ "all: Adds traps doors (and any future supported door types)", "chaos: Increases the number of door types in all dungeon pools" ], + "trap_door_mode" : [ + "Trap Door Removal (default: %(default)s)", + "vanilla: No trap door removal", + "optional: Trap doors removed if blocking", + "boss: Also remove boss traps", + "oneway: Remove all annoying trap doors" + ], + "key_logic_algorithm": [ + "Key Logic Algorithm (default: %(default)s)", + "default: Balance between safety and randomization", + "partial: Partial protection when using certain minor glitches", + "strict: Ensure small keys are available" + ], "decoupledoors" : [ "Door entrances and exits are decoupled" ], "experimental": [ "Enable experimental features. (default: %(default)s)" ], "dungeon_counters": [ "Enable dungeon chest counters. (default: %(default)s)" ], diff --git a/resources/app/gui/lang/en.json b/resources/app/gui/lang/en.json index c6bc21b6..5598f292 100644 --- a/resources/app/gui/lang/en.json +++ b/resources/app/gui/lang/en.json @@ -91,6 +91,17 @@ "randomizer.dungeon.door_type_mode.all": "Adds Trap Doors", "randomizer.dungeon.door_type_mode.chaos": "Increases all door types", + "randomizer.dungeon.trap_door_mode": "Trap Door Removal", + "randomizer.dungeon.trap_door_mode.vanilla": "No Removal", + "randomizer.dungeon.trap_door_mode.optional": "Removed If Blocking Path", + "randomizer.dungeon.trap_door_mode.boss": "Also Remove Boss Traps", + "randomizer.dungeon.trap_door_mode.oneway": "Remove All Annoying Traps", + + "randomizer.dungeon.key_logic_algorithm": "Key Logic Algorithm", + "randomizer.dungeon.key_logic_algorithm.default": "Default", + "randomizer.dungeon.key_logic_algorithm.partial": "Partial Protection", + "randomizer.dungeon.key_logic_algorithm.strict": "Strict", + "randomizer.dungeon.experimental": "Enable Experimental Features", "randomizer.dungeon.dungeon_counters": "Dungeon Chest Counters", diff --git a/resources/app/gui/randomize/dungeon/widgets.json b/resources/app/gui/randomize/dungeon/widgets.json index 88fe3d53..e0cd2bdd 100644 --- a/resources/app/gui/randomize/dungeon/widgets.json +++ b/resources/app/gui/randomize/dungeon/widgets.json @@ -33,7 +33,7 @@ }, "door_type_mode": { "type": "selectbox", - "default": "basic", + "default": "original", "options": [ "original", "big", @@ -44,6 +44,28 @@ "width": 45 } }, + "trap_door_mode": { + "type": "selectbox", + "default": "optional", + "options": [ + "vanilla", + "optional", + "boss", + "oneway" + ], + "config": { + "width": 30 + } + }, + "key_logic_algorithm": { + "type": "selectbox", + "default": "default", + "options": [ + "default", + "partial", + "strict" + ] + }, "decoupledoors": { "type": "checkbox" }, "keydropshuffle": { "type": "checkbox" }, "pottery": { diff --git a/source/classes/CustomSettings.py b/source/classes/CustomSettings.py index af6e47ff..fd15253f 100644 --- a/source/classes/CustomSettings.py +++ b/source/classes/CustomSettings.py @@ -109,6 +109,8 @@ class CustomSettings(object): args.standardize_palettes[p]) args.intensity[p] = get_setting(settings['intensity'], args.intensity[p]) args.door_type_mode[p] = get_setting(settings['door_type_mode'], args.door_type_mode[p]) + args.trap_door_mode[p] = get_setting(settings['trap_door_mode'], args.trap_door_mode[p]) + args.key_logic_algorithm[p] = get_setting(settings['key_logic_algorithm'], args.key_logic_algorithm[p]) args.decoupledoors[p] = get_setting(settings['decoupledoors'], args.decoupledoors[p]) args.dungeon_counters[p] = get_setting(settings['dungeon_counters'], args.dungeon_counters[p]) args.crystals_gt[p] = get_setting(settings['crystals_gt'], args.crystals_gt[p]) @@ -219,6 +221,8 @@ class CustomSettings(object): settings_dict[p]['door_shuffle'] = world.doorShuffle[p] settings_dict[p]['intensity'] = world.intensity[p] settings_dict[p]['door_type_mode'] = world.door_type_mode[p] + settings_dict[p]['trap_door_mode'] = world.trap_door_mode[p] + settings_dict[p]['key_logic_algorithm'] = world.key_logic_algorithm[p] settings_dict[p]['decoupledoors'] = world.decoupledoors[p] settings_dict[p]['logic'] = world.logic[p] settings_dict[p]['mode'] = world.mode[p] diff --git a/source/classes/constants.py b/source/classes/constants.py index b6a05d77..e816f61b 100644 --- a/source/classes/constants.py +++ b/source/classes/constants.py @@ -109,6 +109,8 @@ SETTINGSTOPROCESS = { "dungeondoorshuffle": "door_shuffle", "dungeonintensity": "intensity", "door_type_mode": "door_type_mode", + "trap_door_mode": "trap_door_mode", + "key_logic_algorithm": "key_logic_algorithm", "decoupledoors": "decoupledoors", "keydropshuffle": "keydropshuffle", "dropshuffle": "dropshuffle", diff --git a/source/dungeon/DungeonStitcher.py b/source/dungeon/DungeonStitcher.py index d0b21d89..1eac3fb1 100644 --- a/source/dungeon/DungeonStitcher.py +++ b/source/dungeon/DungeonStitcher.py @@ -320,7 +320,10 @@ def determine_paths_for_dungeon(world, player, all_regions, name): paths.append('Hyrule Dungeon Cellblock') paths.append(('Hyrule Dungeon Cellblock', 'Hyrule Castle Throne Room')) entrance = next(x for x in world.dungeon_portals[player] if x.name == 'Hyrule Castle South') + # todo: in non-er, we can use the other portals too paths.append(('Hyrule Dungeon Cellblock', entrance.door.entrance.parent_region.name)) + paths.append(('Hyrule Castle Throne Room', [entrance.door.entrance.parent_region.name, + 'Hyrule Dungeon Cellblock'])) if world.doorShuffle[player] in ['basic'] and name == 'Thieves Town': paths.append('Thieves Attic Window') elif 'Thieves Attic Window' in all_r_names: @@ -434,6 +437,13 @@ def connect_simple_door(exit_door, region): special_big_key_doors = ['Hyrule Dungeon Cellblock Door', "Thieves Blind's Cell Door"] +std_special_big_key_doors = ['Hyrule Castle Throne Room Tapestry'] + special_big_key_doors + + +def get_special_big_key_doors(world, player): + if world.mode[player] == 'standard': + return std_special_big_key_doors + return special_big_key_doors class ExplorationState(object): @@ -621,6 +631,7 @@ class ExplorationState(object): elif not self.in_door_list(door, self.avail_doors): self.append_door_to_list(door, self.avail_doors, flag) + # same as above but traps are ignored, and flag is not used def add_all_doors_check_proposed_2(self, region, proposed_map, valid_doors, world, player): for door in get_doors(world, region, player): if door in proposed_map and door.name in valid_doors: @@ -641,6 +652,27 @@ class ExplorationState(object): elif not self.in_door_list(door, self.avail_doors): self.append_door_to_list(door, self.avail_doors) + # same as above but traps are checked for + def add_all_doors_check_proposed_3(self, region, proposed_map, valid_doors, world, player): + for door in get_doors(world, region, player): + if door in proposed_map and door.name in valid_doors: + self.visited_doors.add(door) + if self.can_traverse(door): + if door.controller is not None: + door = door.controller + if door.dest is None and door not in proposed_map.keys() and door.name in valid_doors: + if not self.in_door_list_ic(door, self.unattached_doors): + self.append_door_to_list(door, self.unattached_doors) + else: + other = self.find_door_in_list(door, self.unattached_doors) + if self.crystal != other.crystal: + other.crystal = CrystalBarrier.Either + elif door.req_event is not None and door.req_event not in self.events and not self.in_door_list(door, + self.event_doors): + self.append_door_to_list(door, self.event_doors) + elif not self.in_door_list(door, self.avail_doors): + self.append_door_to_list(door, self.avail_doors) + def add_all_doors_check_proposed_traps(self, region, proposed_traps, world, player): for door in get_doors(world, region, player): if self.can_traverse_ignore_traps(door) and door not in proposed_traps: @@ -674,7 +706,7 @@ class ExplorationState(object): if door in key_door_proposal and door not in self.opened_doors: if not self.in_door_list(door, self.small_doors): self.append_door_to_list(door, self.small_doors) - elif (door.bigKey or door.name in special_big_key_doors) and not self.big_key_opened: + elif (door.bigKey or door.name in get_special_big_key_doors(world, player)) and not self.big_key_opened: if not self.in_door_list(door, self.big_doors): self.append_door_to_list(door, self.big_doors) elif door.req_event is not None and door.req_event not in self.events: @@ -827,7 +859,10 @@ def extend_reachable_state_lenient(search_regions, state, proposed_map, all_regi local_state = state.copy() for region in search_regions: local_state.visit_region(region) - local_state.add_all_doors_check_proposed_2(region, proposed_map, valid_doors, world, player) + if world.trap_door_mode[player] == 'vanilla': + local_state.add_all_doors_check_proposed_3(region, proposed_map, valid_doors, world, player) + else: + local_state.add_all_doors_check_proposed_2(region, proposed_map, valid_doors, world, player) while len(local_state.avail_doors) > 0: explorable_door = local_state.next_avail_door() if explorable_door.door in proposed_map: @@ -838,7 +873,10 @@ def extend_reachable_state_lenient(search_regions, state, proposed_map, all_regi if (valid_region_to_explore_in_regions(connect_region, all_regions, world, player) and not local_state.visited(connect_region)): local_state.visit_region(connect_region) - local_state.add_all_doors_check_proposed_2(connect_region, proposed_map, valid_doors, world, player) + if world.trap_door_mode[player] == 'vanilla': + local_state.add_all_doors_check_proposed_3(connect_region, proposed_map, valid_doors, world, player) + else: + local_state.add_all_doors_check_proposed_2(connect_region, proposed_map, valid_doors, world, player) return local_state diff --git a/source/item/FillUtil.py b/source/item/FillUtil.py index dba7cb57..a0e8a01a 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/source/meta/check_requirements.py b/source/meta/check_requirements.py index 680dfe8f..6976f707 100644 --- a/source/meta/check_requirements.py +++ b/source/meta/check_requirements.py @@ -1,6 +1,4 @@ import importlib.util -import webbrowser -from tkinter import Tk, Label, Button, Frame def check_requirements(console=False): @@ -26,6 +24,9 @@ def check_requirements(console=False): logger.error('See the step about "Installing Platform-specific dependencies":') logger.error('https://github.com/aerinon/ALttPDoorRandomizer/blob/DoorDev/docs/BUILDING.md') else: + import webbrowser + from tkinter import Tk, Label, Button, Frame + master = Tk() master.title('Error') frame = Frame(master) diff --git a/source/overworld/EntranceShuffle2.py b/source/overworld/EntranceShuffle2.py index 5b5526a9..9b4b9464 100644 --- a/source/overworld/EntranceShuffle2.py +++ b/source/overworld/EntranceShuffle2.py @@ -304,7 +304,8 @@ def do_main_shuffle(entrances, exits, avail, mode_def): unused_entrances = set() if not cross_world: lw_entrances, dw_entrances = [], [] - for x in rem_entrances: + left = sorted(rem_entrances) + for x in left: if bonk_fairy_exception(x): lw_entrances.append(x) if x in LW_Entrances else dw_entrances.append(x) do_same_world_connectors(lw_entrances, dw_entrances, multi_exit_caves, avail) diff --git a/source/tools/MysteryUtils.py b/source/tools/MysteryUtils.py index 1ebc467d..e5c215c6 100644 --- a/source/tools/MysteryUtils.py +++ b/source/tools/MysteryUtils.py @@ -95,6 +95,8 @@ def roll_settings(weights): ret.door_shuffle = door_shuffle if door_shuffle != 'none' else 'vanilla' ret.intensity = get_choice('intensity') ret.door_type_mode = get_choice('door_type_mode') + ret.trap_door_mode = get_choice('trap_door_mode') + ret.key_logic_algorithm = get_choice('key_logic_algorithm') ret.decoupledoors = get_choice('decoupledoors') == 'on' ret.experimental = get_choice('experimental') == 'on' ret.collection_rate = get_choice('collection_rate') == 'on' 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