diff --git a/BaseClasses.py b/BaseClasses.py index 8c7957d8..39286e37 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -28,6 +28,7 @@ class World(object): self.shuffle = shuffle.copy() self.doorShuffle = doorShuffle.copy() self.intensity = {} + self.door_type_mode = {} self.logic = logic.copy() self.mode = mode.copy() self.swords = swords.copy() @@ -143,6 +144,7 @@ class World(object): set_player_attr('colorizepots', False) set_player_attr('pot_pool', {}) set_player_attr('decoupledoors', False) + set_player_attr('door_type_mode', 'original') set_player_attr('shopsanity', False) set_player_attr('mixed_travel', 'prevent') @@ -1872,7 +1874,6 @@ class Sector(object): self.item_logic = set() self.chest_location_set = set() - def region_set(self): if self.r_name_set is None: self.r_name_set = dict.fromkeys(map(lambda r: r.name, self.regions)) @@ -2154,7 +2155,7 @@ class Location(object): def gen_name(self): name = self.name world = self.parent_region.world if self.parent_region and self.parent_region.world else None - if self.parent_region.dungeon and world and world.doorShuffle[self.player] == 'crossed': + if self.parent_region.dungeon and world and world.doorShuffle[self.player] not in ['basic', 'vanilla']: name += f' @ {self.parent_region.dungeon.name}' if world and world.players > 1: name += f' ({world.get_player_names(self.player)})' @@ -2377,6 +2378,8 @@ class Spoiler(object): 'overworld_map': self.world.overworld_map, 'door_shuffle': self.world.doorShuffle, 'intensity': self.world.intensity, + 'door_type_mode': self.world.door_type_mode, + 'decoupledoors': self.world.decoupledoors, 'dungeon_counters': self.world.dungeon_counters, 'item_pool': self.world.difficulty, 'item_functionality': self.world.difficulty_adjustments, @@ -2578,6 +2581,7 @@ class Spoiler(object): outfile.write('Door Shuffle: %s\n' % self.metadata['door_shuffle'][player]) if self.metadata['door_shuffle'][player] != 'vanilla': outfile.write(f"Intensity: {self.metadata['intensity'][player]}\n") + outfile.write(f"Door Type Mode: {self.metadata['door_type_mode'][player]}\n") outfile.write(f"Decouple Doors: {yn(self.metadata['decoupledoors'][player])}\n") outfile.write(f"Experimental: {yn(self.metadata['experimental'][player])}\n") outfile.write(f"Dungeon Counters: {self.metadata['dungeon_counters'][player]}\n") @@ -2815,7 +2819,7 @@ class Pot(object): # byte 0: DDDE EEEE (DR, ER) -dr_mode = {"basic": 1, "crossed": 2, "vanilla": 0} +dr_mode = {"basic": 1, "crossed": 2, "vanilla": 0, "partitioned": 3} er_mode = {"vanilla": 0, "simple": 1, "restricted": 2, "full": 3, "crossed": 4, "insanity": 5, 'lite': 8, 'lean': 9, "dungeonsfull": 7, "dungeonssimple": 6} @@ -2845,7 +2849,8 @@ counter_mode = {"default": 0, "off": 1, "on": 2, "pickup": 3} # byte 6: CCCC CPAA (crystals ganon, pyramid, access access_mode = {"items": 0, "locations": 1, "none": 2} -# byte 7: BSMC ??EE (big, small, maps, compass, bosses, enemies) +# byte 7: BSMC DDEE (big, small, maps, compass, door_type, enemies) +door_type_mode = {'original': 0, 'big': 1, 'all': 2, 'chaos': 3} enemy_mode = {"none": 0, "shuffled": 1, "chaos": 2, "random": 2, "legacy": 3} # byte 8: HHHD DPBS (enemy_health, enemy_dmg, potshuffle, bomb logic, shuffle links) @@ -2898,7 +2903,7 @@ class Settings(object): (0x80 if w.bigkeyshuffle[p] else 0) | (0x40 if w.keyshuffle[p] else 0) | (0x20 if w.mapshuffle[p] else 0) | (0x10 if w.compassshuffle[p] else 0) - | (enemy_mode[w.enemy_shuffle[p]]), + | (door_type_mode[w.door_type_mode[p]] << 2) | (enemy_mode[w.enemy_shuffle[p]]), (e_health[w.enemy_health[p]] << 5) | (e_dmg[w.enemy_damage[p]] << 3) | (0x4 if w.potshuffle[p] else 0) | (0x2 if w.bombbag[p] else 0) | (1 if w.shufflelinks[p] else 0), @@ -2955,7 +2960,7 @@ class Settings(object): args.keyshuffle[p] = True if settings[7] & 0x40 else False args.mapshuffle[p] = True if settings[7] & 0x20 else False args.compassshuffle[p] = True if settings[7] & 0x10 else False - # args.shufflebosses[p] = r(boss_mode)[(settings[7] & 0xc) >> 2] + args.door_type_mode[p] = r(door_type_mode)[(settings[7] & 0xc) >> 2] args.shuffleenemies[p] = r(enemy_mode)[settings[7] & 0x3] args.enemy_health[p] = r(e_health)[(settings[8] & 0xE0) >> 5] diff --git a/CLI.py b/CLI.py index 77b020b4..2e01e4bc 100644 --- a/CLI.py +++ b/CLI.py @@ -125,7 +125,7 @@ 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']: + 'msu_resume', 'collection_rate', 'colorizepots', 'decoupledoors', 'door_type_mode']: value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name) if player == 1: setattr(ret, name, {1: value}) @@ -192,6 +192,7 @@ def parse_settings(): "keysanity": False, "door_shuffle": "basic", "intensity": 2, + 'door_type_mode': 'original', 'decoupledoors': False, "experimental": False, "dungeon_counters": "default", diff --git a/DoorShuffle.py b/DoorShuffle.py index b8c8abbe..42d32c24 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -4,6 +4,7 @@ import logging import time from enum import unique, Flag from typing import DefaultDict, Dict, List +from itertools import chain from BaseClasses import RegionType, Region, Door, DoorType, Direction, Sector, CrystalBarrier, DungeonInfo, dungeon_keys from BaseClasses import PotFlags, LocationType, Direction @@ -18,8 +19,9 @@ from source.dungeon.DungeonStitcher import ExplorationState as ExplorationState2 from DungeonGenerator import ExplorationState, convert_regions, pre_validate, determine_required_paths, drop_entrances from DungeonGenerator import create_dungeon_builders, split_dungeon_builder, simple_dungeon_builder, default_dungeon_entrances from DungeonGenerator import dungeon_portals, dungeon_drops, connect_doors -from DungeonGenerator import valid_region_to_explore as valid_region_to_explore_lim +from DungeonGenerator import valid_region_to_explore from KeyDoorShuffle import analyze_dungeon, build_key_layout, validate_key_layout, determine_prize_lock +from KeyDoorShuffle import validate_bk_layout, check_bk_special from Utils import ncr, kth_combination @@ -87,7 +89,7 @@ def link_doors_prep(world, player): find_inaccessible_regions(world, player) - if world.intensity[player] >= 3 and world.doorShuffle[player] in ['basic', 'crossed']: + if world.intensity[player] >= 3 and world.doorShuffle[player] != 'vanilla': choose_portals(world, player) else: if world.shuffle[player] == 'vanilla': @@ -128,14 +130,21 @@ def link_doors_prep(world, player): def link_doors_main(world, player): + pool = None if world.doorShuffle[player] == 'basic': - within_dungeon(world, player) + pool = [([name], regions) for name, regions in dungeon_regions.items()] + elif world.doorShuffle[player] == 'partitioned': + groups = [['Hyrule Castle', 'Eastern Palace', 'Desert Palace', 'Tower of Hera', 'Agahnims Tower'], + ['Palace of Darkness', 'Swamp Palace', 'Skull Woods', 'Thieves Town'], + ['Ice Palace', 'Misery Mire', 'Turtle Rock', 'Ganons Tower']] + pool = [(group, list(chain.from_iterable([dungeon_regions[d] for d in group]))) for group in groups] elif world.doorShuffle[player] == 'crossed': - cross_dungeon(world, player) + pool = [list(dungeon_regions.keys()), sum(r for r in dungeon_regions.values())] elif world.doorShuffle[player] != 'vanilla': logging.getLogger('').error('Invalid door shuffle setting: %s' % world.doorShuffle[player]) raise Exception('Invalid door shuffle setting: %s' % world.doorShuffle[player]) - + if pool: + main_dungeon_pool(pool, world, player) if world.doorShuffle[player] != 'vanilla': create_door_spoiler(world, player) @@ -395,8 +404,20 @@ def pair_existing_key_doors(world, player, door_a, door_b): def choose_portals(world, player): - if world.doorShuffle[player] in ['basic', 'crossed']: - cross_flag = world.doorShuffle[player] == 'crossed' + if world.doorShuffle[player] != ['vanilla']: + shuffle_flag = world.doorShuffle[player] != 'basic' + allowed = {} + if world.doorShuffle[player] == 'basic': + allowed = {name: {name} for name in dungeon_regions} + elif world.doorShuffle[player] == 'partitioned': + groups = [['Hyrule Castle', 'Eastern Palace', 'Desert Palace', 'Tower of Hera', 'Agahnims Tower'], + ['Palace of Darkness', 'Swamp Palace', 'Skull Woods', 'Thieves Town'], + ['Ice Palace', 'Misery Mire', 'Turtle Rock', 'Ganons Tower']] + allowed = {name: set(group) for group in groups for name in group} + elif world.doorShuffle[player] == 'crossed': + all_dungeons = set(dungeon_regions.keys()) + allowed = {name: all_dungeons for name in dungeon_regions} + # key drops allow the big key in the right place in Desert Tiles 2 bk_shuffle = world.bigkeyshuffle[player] or world.dropshuffle[player] std_flag = world.mode[player] == 'standard' @@ -443,7 +464,7 @@ def choose_portals(world, player): custom = customizer_portals(master_door_list, world, player) - if cross_flag: + if shuffle_flag: random.shuffle(shuffled_info) for dungeon, info in shuffled_info: outstanding_portals = list(dungeon_portals[dungeon]) @@ -457,15 +478,15 @@ def choose_portals(world, player): info.required_passage[target_region] = [x for x in possible_portals if x != sanc.name] info.required_passage = {x: y for x, y in info.required_passage.items() if len(y) > 0} for target_region, possible_portals in info.required_passage.items(): - candidates = find_portal_candidates(master_door_list, dungeon, custom, need_passage=True, - crossed=cross_flag, bk_shuffle=bk_shuffle, rupee_bow=rupee_bow_flag) + candidates = find_portal_candidates(master_door_list, dungeon, custom, allowed, need_passage=True, + bk_shuffle=bk_shuffle, rupee_bow=rupee_bow_flag) choice, portal = assign_portal(candidates, possible_portals, custom, world, player) portal.destination = True clean_up_portal_assignment(portal_assignment, dungeon, portal, master_door_list, outstanding_portals) dead_end_choices = info.total - 1 - len(portal_assignment[dungeon]) for i in range(0, dead_end_choices): - candidates = find_portal_candidates(master_door_list, dungeon, custom, dead_end_allowed=True, - crossed=cross_flag, bk_shuffle=bk_shuffle, rupee_bow=rupee_bow_flag) + candidates = find_portal_candidates(master_door_list, dungeon, custom, allowed, dead_end_allowed=True, + bk_shuffle=bk_shuffle, rupee_bow=rupee_bow_flag) possible_portals = outstanding_portals if not info.sole_entrance else [x for x in outstanding_portals if x != info.sole_entrance] choice, portal = assign_portal(candidates, possible_portals, custom, world, player) if choice.deadEnd: @@ -476,7 +497,7 @@ def choose_portals(world, player): clean_up_portal_assignment(portal_assignment, dungeon, portal, master_door_list, outstanding_portals) the_rest = info.total - len(portal_assignment[dungeon]) for i in range(0, the_rest): - candidates = find_portal_candidates(master_door_list, dungeon, custom, crossed=cross_flag, + candidates = find_portal_candidates(master_door_list, dungeon, custom, allowed, bk_shuffle=bk_shuffle, standard=hc_flag, rupee_bow=rupee_bow_flag) choice, portal = assign_portal(candidates, outstanding_portals, custom, world, player) clean_up_portal_assignment(portal_assignment, dungeon, portal, master_door_list, outstanding_portals) @@ -620,7 +641,7 @@ def disconnect_portal(portal, world, player): chosen_door.entranceFlag = False -def find_portal_candidates(door_list, dungeon, custom, need_passage=False, dead_end_allowed=False, crossed=False, +def find_portal_candidates(door_list, dungeon, custom, allowed, need_passage=False, dead_end_allowed=False, bk_shuffle=False, standard=False, rupee_bow=False): custom_portals, assigned_doors = custom if assigned_doors: @@ -628,10 +649,8 @@ def find_portal_candidates(door_list, dungeon, custom, need_passage=False, dead_ else: ret = door_list ret = [x for x in ret if bk_shuffle or not x.bk_shuffle_req] - if crossed: - ret = [x for x in ret if not x.dungeonLink or x.dungeonLink == dungeon or x.dungeonLink.startswith('link')] - else: - ret = [x for x in ret if x.entrance.parent_region.dungeon.name == dungeon] + ret = [x for x in ret if not x.dungeonLink or x.dungeonLink == dungeon or x.dungeonLink.startswith('link')] + ret = [x for x in ret if x.entrance.parent_region.dungeon.name in allowed[dungeon]] if need_passage: ret = [x for x in ret if x.passage] if not dead_end_allowed: @@ -789,7 +808,7 @@ def main_dungeon_pool(dungeon_pool, world, player): # todo: which dungeon to create dungeon_builders.update(create_dungeon_builders(sector_pool, connections_tuple, world, player, pool, entrances, splits)) - door_type_pools.append((pool, DoorTypePool(sector_pool, world, player))) + door_type_pools.append((pool, DoorTypePool(pool, world, player))) update_forced_keys(dungeon_builders, entrances_map, world, player) recombinant_builders = {} @@ -816,8 +835,60 @@ def main_dungeon_pool(dungeon_pool, world, player): target_items += 19 # 19 pot keys d_items = target_items - all_dungeon_items_cnt world.pool_adjustment[player] = d_items + # todo: remove unused pairs if not world.decoupledoors[player]: smooth_door_pairs(world, player) + cross_dungeon_clean_up(world, player) + + +def cross_dungeon_clean_up(world, player): + # Re-assign dungeon bosses + gt = world.get_dungeon('Ganons Tower', player) + for name, builder in world.dungeon_layouts[player].items(): + reassign_boss('GT Ice Armos', 'bottom', builder, gt, world, player) + reassign_boss('GT Lanmolas 2', 'middle', builder, gt, world, player) + reassign_boss('GT Moldorm', 'top', builder, gt, world, player) + + sanctuary = world.get_region('Sanctuary', player) + d_name = sanctuary.dungeon.name + if d_name != 'Hyrule Castle': + possible_portals = [] + for portal_name in dungeon_portals[d_name]: + portal = world.get_portal(portal_name, player) + if portal.door.name == 'Sanctuary S': + possible_portals.clear() + possible_portals.append(portal) + break + if not portal.destination and not portal.deadEnd: + possible_portals.append(portal) + if len(possible_portals) == 1: + world.sanc_portal[player] = possible_portals[0] + else: + reachable_portals = [] + for portal in possible_portals: + start_area = portal.door.entrance.parent_region + state = ExplorationState(dungeon=d_name) + state.visit_region(start_area) + state.add_all_doors_check_unattached(start_area, world, player) + explore_state(state, world, player) + if state.visited_at_all(sanctuary): + reachable_portals.append(portal) + world.sanc_portal[player] = random.choice(reachable_portals) + if world.intensity[player] >= 3: + if player in world.sanc_portal: + portal = world.sanc_portal[player] + else: + portal = world.get_portal('Sanctuary', player) + target = portal.door.entrance.parent_region + connect_simple_door(world, 'Sanctuary Mirror Route', target, player) + + check_entrance_fixes(world, player) + + if world.standardize_palettes[player] == 'standardize' and world.doorShuffle[player] != 'basic': + palette_assignment(world, player) + + refine_hints(world.dungeon_layouts[player]) + refine_boss_exits(world, player) def update_forced_keys(dungeon_builders, entrances_map, world, player): @@ -1629,14 +1700,6 @@ def combine_layouts(recombinant_builders, dungeon_builders, entrances_map, world dungeon_builders[recombine.name] = recombine -# todo: this allows cross-dungeon exploring via HC Ledge or Inaccessible Regions -# todo: @deprecated -def valid_region_to_explore(region, world, player): - return region and (region.type == RegionType.Dungeon - or region.name in world.inaccessible_regions[player] - or (region.name == 'Hyrule Castle Ledge' and world.mode[player] == 'standard')) - - def setup_custom_door_types(world, player): if not hasattr(world, 'custom_door_types'): world.custom_door_types = defaultdict(dict) @@ -1668,58 +1731,30 @@ def setup_custom_door_types(world, player): class DoorTypePool: - def __init__(self, sectors, world, player): + def __init__(self, pool, world, player): self.smalls = 0 self.bombable = 0 self.dashable = 0 self.bigs = 0 self.traps = 0 - # self.tricky = 0 - # self.hidden = 0 + self.tricky = 0 + self.hidden = 0 # todo: custom pools? - self.count_via_sectors(sectors, world, player) + for dungeon in pool: + counts = door_type_counts[dungeon] + if world.door_type_mode[player] == 'chaos': + counts = self.chaos_shuffle(counts) + self.smalls += counts[0] + self.bigs += counts[1] + self.traps += counts[2] + self.bombable += counts[3] + self.dashable += counts[4] + self.hidden += counts[5] + self.tricky += counts[6] - def count_via_sectors(self, sectors, world, player): - skips = set() - for sector in sectors: - for region in sector.regions: - for ext in region.exits: - if ext.door: - d = ext.door - if d.name not in skips and d.type in [DoorType.Normal, DoorType.Interior]: - if d.smallKey: - self.smalls += 1 - elif d.bigKey: - self.bigs += 1 - elif d.blocked and d.trapFlag and 'Boss' not in d.name and 'Agahnim' not in d.name: - self.traps += 1 - elif d.name == 'TR Compass Room NW': - self.tricky += 1 - elif d.name in ['Skull Vines NW', 'Tower Altar NW']: - self.hidden += 1 - else: - kind = world.get_room(d.roomIndex, player).kind(d) - if kind == DoorKind.Bombable: - self.bombable += 1 - elif kind == DoorKind.Dashable: - self.dashable += 1 - if d.type == DoorType.Interior: - skips.add(d.dest.name) ## lookup a different way for interior door shuffle - elif d.type == DoorType.Normal: - for dp in world.paired_doors[player]: - if d.name == dp.door_a or d.name == dp.door_b: - skips.add(dp.door_b if d.name == dp.door_a else dp.door_a) - break - - def chaos_shuffle(self): + def chaos_shuffle(self, counts): weights = [1, 2, 4, 3, 2, 1] - self.smalls = random.choices(self.get_choices(self.smalls), weights=weights) - self.bombable = random.choices(self.get_choices(self.bombable), weights=weights) - self.dashable = random.choices(self.get_choices(self.dashable), weights=weights) - self.bigs = random.choices(self.get_choices(self.bigs), weights=weights) - self.traps = random.choices(self.get_choices(self.traps), weights=weights) - # self.tricky = random.choices(self.get_choices(self.tricky), weights=weights) - # self.hidden = random.choices(self.get_choices(self.hidden), weights=weights) + return [random.choices(self.get_choices(counts[i]), weights=weights) for i, c in enumerate(counts)] @staticmethod def get_choices(number): @@ -1728,24 +1763,24 @@ class DoorTypePool: class BuilderDoorCandidates: def __init__(self): - self.small = set() - self.big = set() - self.trap = set() - self.bombable = set() - self.dashable = set() - - self.checked = set() + self.small = [] + self.big = [] + self.trap = [] + self.bombable = [] + self.dashable = [] def shuffle_door_types(door_type_pools, paths, world, player): start_regions_map = {} - for name, builder in world.dungeon_layouts[player]: + for name, builder in world.dungeon_layouts[player].items(): start_regions = convert_regions(builder.path_entrances, world, player) start_regions_map[name] = start_regions + builder.candidates = BuilderDoorCandidates() used_doors = shuffle_door_traps(door_type_pools, paths, start_regions_map, world, player) # big keys + used_doors = shuffle_big_key_doors(door_type_pools, used_doors, start_regions_map, world, player) # small keys - + used_doors = shuffle_small_key_doors(door_type_pools, used_doors, start_regions_map, world, player) # bombable / dashable # tricky / hidden @@ -1780,13 +1815,15 @@ def shuffle_door_traps(door_type_pools, paths, start_regions_map, world, player) 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) + 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 + 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() @@ -1820,15 +1857,15 @@ def shuffle_big_key_doors(door_type_pools, used_doors, start_regions_map, world, for dungeon in pool: builder = world.dungeon_layouts[player][dungeon] - find_big_key_candidates(builder, used_doors, world, player) + find_big_key_candidates(builder, start_regions_map[dungeon], used_doors, world, player) if custom_bk_doors[dungeon]: - builder.candidates.trap = filter_key_door_pool(builder.candidates.big, custom_bk_doors[dungeon]) + builder.candidates.big = filter_key_door_pool(builder.candidates.big, custom_bk_doors[dungeon]) remaining -= len(custom_bk_doors[dungeon]) ttl += len(builder.candidates.big) for dungeon in pool: builder = world.dungeon_layouts[player][dungeon] proportion = len(builder.candidates.big) - calc = int(round(proportion * door_type_pool.big/ttl)) + calc = int(round(proportion * door_type_pool.bigs/ttl)) suggested = min(proportion, calc) remaining -= suggested suggestion_map[dungeon] = suggested @@ -1836,20 +1873,21 @@ def shuffle_big_key_doors(door_type_pools, used_doors, start_regions_map, world, for dungeon in pool: builder = world.dungeon_layouts[player][dungeon] valid_doors, bk_number = find_valid_bk_combination(builder, suggestion_map[dungeon], - start_regions_map[dungeon], world, player) + start_regions_map[dungeon], world, player, True) bk_map[dungeon] = valid_doors if bk_number < suggestion_map[dungeon]: flex_map[dungeon] = 0 remaining += suggestion_map[dungeon] - bk_number - suggestion_map[dungeon] = bk_number + suggestion_map[dungeon] = bk_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_doors, bk_number = find_valid_bk_combination(builder, increased, start_regions_map[dungeon], - paths, world, player) + world, player) if valid_doors: bk_map[dungeon] = valid_doors remaining -= 1 @@ -1859,6 +1897,93 @@ def shuffle_big_key_doors(door_type_pools, used_doors, start_regions_map, world, queue.append(dungeon) # time to re-assign reassign_big_key_doors(bk_map, world, player) + for name, big_list in bk_map.items(): + used_doors.update(flatten_pair_list(big_list)) + return used_doors + + +def shuffle_small_key_doors(door_type_pools, used_doors, start_regions_map, world, player): + for pool, door_type_pool in door_type_pools: + ttl = 0 + suggestion_map, small_map, flex_map = {}, {}, {} + remaining = door_type_pool.smalls + total_keys = remaining + if player in world.custom_door_types: + custom_key_doors = world.custom_door_types[player]['Key Door'] + else: + custom_key_doors = defaultdict(list) + + for dungeon in pool: + builder = world.dungeon_layouts[player][dungeon] + find_small_key_door_candidates(builder, start_regions_map[dungeon], used_doors, world, player) + if custom_key_doors[dungeon]: + builder.candidates.small = filter_key_door_pool(builder.candidates.small, custom_key_doors[dungeon]) + remaining -= len(custom_key_doors[dungeon]) + builder.key_doors_num = max(0, len(builder.candidates.small) - builder.key_drop_cnt) + total_keys -= builder.key_drop_cnt + ttl += builder.key_doors_num + for dungeon in pool: + builder = world.dungeon_layouts[player][dungeon] + calculated = int(round(builder.key_doors_num*total_keys/ttl)) + max_keys = max(0, builder.location_cnt - calc_used_dungeon_items(builder, world, player)) + cand_len = max(0, len(builder.candidates.small) - builder.key_drop_cnt) + limit = min(max_keys, cand_len) + suggested = min(calculated, limit) + combo_size = ncr(len(builder.candidates.small), suggested + builder.key_drop_cnt) + while combo_size > 500000 and suggested > 0: + suggested -= 1 + combo_size = ncr(len(builder.candidates.small), suggested + builder.key_drop_cnt) + suggestion_map[dungeon] = builder.key_doors_num = suggested + builder.key_drop_cnt + builder.combo_size = combo_size + flex_map[dungeon] = (limit - suggested) if suggested < limit else 0 + for dungeon in pool: + builder = world.dungeon_layouts[player][dungeon] + valid_doors, small_number = find_valid_combination(builder, suggestion_map[dungeon], + start_regions_map[dungeon], world, player) + small_map[dungeon] = valid_doors + actual_chest_keys = small_number - builder.key_drop_cnt + if actual_chest_keys < suggestion_map[dungeon]: + flex_map[dungeon] = 0 + remaining += suggestion_map[dungeon] - actual_chest_keys + suggestion_map[dungeon] = small_number + builder_order = [world.dungeon_layouts[player][x] for x in pool if flex_map[x] > 0] + builder_order.sort(key=lambda b: b.combo_size) + queue = deque(builder_order) + while len(queue) > 0 and remaining > 0: + builder = queue.popleft() + dungeon = builder.name + increased = suggestion_map[dungeon] + 1 + builder.key_doors_num = increased + valid_doors, small_number = find_valid_combination(builder, increased, start_regions_map[dungeon], + world, player) + if valid_doors: + small_map[dungeon] = valid_doors + remaining -= 1 + suggestion_map[dungeon] = increased + flex_map[dungeon] -= 1 + if flex_map[dungeon] > 0: + builder.combo_size = ncr(len(builder.candidates.small), builder.key_doors_num) + queue.append(builder) + queue = deque(sorted(queue, key=lambda b: b.combo_size)) + else: + builder.key_doors_num -= 1 + # time to re-assign + reassign_key_doors(small_map, world, player) + for dungeon_name in pool: + if not world.retro[player]: + builder = world.dungeon_layouts[player][dungeon_name] + log_key_logic(builder.name, world.key_logic[player][builder.name]) + if world.doorShuffle[player] != 'basic': + actual_chest_keys = max(builder.key_doors_num - builder.key_drop_cnt, 0) + dungeon = world.get_dungeon(dungeon_name, player) + if actual_chest_keys == 0: + dungeon.small_keys = [] + else: + dungeon.small_keys = [ItemFactory(dungeon_keys[dungeon_name], player)] * actual_chest_keys + + for name, small_list in small_map.items(): + used_doors.update(flatten_pair_list(small_list)) + return used_doors def shuffle_key_doors(builder, world, player): @@ -1900,9 +2025,9 @@ def find_current_key_doors(builder): def find_trappable_candidates(builder, world, player): - if world.door_type_mode[player] != 'original': # all, chaos + if world.door_type_mode[player] not in ['original', 'big']: # all, chaos r_set = builder.master_sector.region_set() - filtered_doors = [ext.door for r in r_set for ext in r.exits + filtered_doors = [ext.door for r in r_set for ext in world.get_region(r, player).exits if ext.door and ext.door.type in [DoorType.Interior, DoorType.Normal]] for d in filtered_doors: # I only support the first 3 due to the trapFlag right now @@ -1913,14 +2038,14 @@ def find_trappable_candidates(builder, world, player): if (kind in [DoorKind.Normal, DoorKind.SmallKey, DoorKind.Bombable, DoorKind.Dashable, DoorKind.BigKey] or (d.blocked and d.trapFlag != 0 and 'Boss' not in d.name and 'Agahnim' not in d.name) - or (kind == DoorKind.TrapTriggerable and d.direction in [Direction.North, Direction.East]) - or (kind == DoorKind.Trap2 and d.direction in [Direction.South, Direction.West])): - builder.candidates.trap.add(d) + or (kind == DoorKind.TrapTriggerable and d.direction in [Direction.South, Direction.East]) + or (kind == DoorKind.Trap2 and d.direction in [Direction.North, Direction.West])): + builder.candidates.trap.append(d) elif d.type == DoorType.Normal: if (kind in [DoorKind.Normal, DoorKind.SmallKey, DoorKind.Bombable, DoorKind.Dashable, DoorKind.BigKey] or (d.blocked and d.trapFlag != 0 and 'Boss' not in d.name and 'Agahnim' not in d.name)): - builder.candidates.trap.add(d) + builder.candidates.trap.append(d) else: r_set = builder.master_sector.region_set() for r in r_set: @@ -1928,7 +2053,7 @@ def find_trappable_candidates(builder, world, player): 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: - builder.candidates.trap.add(d) + builder.candidates.trap.append(d) def find_valid_trap_combination(builder, suggested, start_regions, paths, world, player, drop=True): @@ -1965,6 +2090,7 @@ def find_valid_trap_combination(builder, suggested, start_regions, paths, world, itr = 0 proposal = kth_combination(sample_list[itr], trap_door_pool, trap_doors_needed) proposal.extend(custom_trap_doors) + builder.trap_door_proposal = proposal return proposal, trap_doors_needed @@ -1982,21 +2108,40 @@ def filter_start_regions(builder, start_regions, world, player): def validate_trap_layout(proposal, builder, start_regions, paths, world, player): - return check_required_paths_with_traps(paths, proposal, builder.name, start_regions, world, player) + flag, state = check_required_paths_with_traps(paths, proposal, builder.name, start_regions, world, player) + if not flag: + return False + bk_special_loc = find_bk_special_location(builder, world, player) + if bk_special_loc: + if not state.found_forced_bk(): + return False + if world.accessibility[player] != 'beatable': + all_locations = [l for r in builder.master_sector.region_set() for l in world.get_region(r, player).locations] + if any(l not in state.found_locations for l in all_locations): + return False + return True +def find_bk_special_location(builder, world, player): + for r_name in builder.master_sector.region_set(): + region = world.get_region(r_name, player) + for loc in region.locations: + if loc.forced_big_key(): + return loc + return None + def check_required_paths_with_traps(paths, proposal, dungeon_name, start_regions, world, player): + cached_initial_state = None if len(paths[dungeon_name]) > 0: - states_to_explore = {} common_starts = tuple(start_regions) + states_to_explore = {common_starts: ([], 'all')} for path in paths[dungeon_name]: if type(path) is tuple: states_to_explore[tuple([path[0]])] = (path[1], 'any') else: - if common_starts not in states_to_explore: - states_to_explore[common_starts] = ([], 'all') + # if common_starts not in states_to_explore: + # states_to_explore[common_starts] = ([], 'all') states_to_explore[common_starts][0].append(path) - cached_initial_state = None for start_regs, info in states_to_explore.items(): dest_regs, path_type = info if type(dest_regs) is not list: @@ -2005,12 +2150,14 @@ def check_required_paths_with_traps(paths, proposal, dungeon_name, start_regions start_regions = convert_regions(start_regs, world, player) initial = start_regs == common_starts if not initial or cached_initial_state is None: + if cached_initial_state and any(not cached_initial_state.visited_at_all(r) for r in start_regions): + return False, None # can't start processing the initial state because start regs aren't reachable init = determine_init_crystal(initial, cached_initial_state, start_regions) state = ExplorationState2(init, dungeon_name) for region in start_regions: state.visit_region(region) state.add_all_doors_check_proposed_traps(region, proposal, world, player) - explore_state_proposed_traps(state, world, player) + explore_state_proposed_traps(state, proposal, world, player) if initial and cached_initial_state is None: cached_initial_state = state else: @@ -2020,8 +2167,8 @@ def check_required_paths_with_traps(paths, proposal, dungeon_name, start_regions else: valid, bad_region = check_if_all_regions_visited(state, check_paths) if not valid: - return False - return True + return False, None + return True, cached_initial_state def reassign_trap_doors(trap_map, world, player): @@ -2041,13 +2188,14 @@ def reassign_trap_doors(trap_map, world, player): elif kind in [DoorKind.Trap2, DoorKind.TrapTriggerable]: room.change(d.doorListPos, DoorKind.Normal) d.blocked = False + connect_one_way(world, d.name, d.dest.name, player) elif d.type is DoorType.Normal and d not in traps: world.get_room(d.roomIndex, player).change(d.doorListPos, DoorKind.Normal) 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('Key Door: %s', d.name) + logger.debug('Trap Door: %s', d.name) def find_current_trap_doors(builder): @@ -2078,34 +2226,37 @@ def change_door_to_trap(d, world, player): verify_door_list_pos(d, room, world, player, pos=3) d.trapFlag = {0: 0x4, 1: 0x2, 2: 0x1}[d.doorListPos] room.change(d.doorListPos, new_kind) + if d.entrance.connected_region is not None: + d.entrance.connected_region.entrances.remove(d.entrance) + d.entrance.connected_region = None elif d.type is DoorType.Normal: d.blocked = True verify_door_list_pos(d, room, world, player, pos=3) d.trapFlag = {0: 0x4, 1: 0x2, 2: 0x1}[d.doorListPos] room.change(d.doorListPos, DoorKind.Trap) + if d.entrance.connected_region is not None: + d.entrance.connected_region.entrances.remove(d.entrance) + d.entrance.connected_region = None -def find_big_key_candidates(builder, used, world, player): - if world.door_type_mode[player] != 'original': # all, chaos - r_set = builder.master_sector.region_set() - filtered_doors = [ext.door for r in r_set for ext in r.exits - if ext.door and ext.door.type in [DoorType.Interior, DoorType.Normal]] - for d in filtered_doors: - if 0 <= d.doorListPos < 4 and not d.entranceFlag: - room = world.get_room(d.roomIndex, player) - kind = room.kind(d) - if d.type in [DoorType.Interior, DoorType.Normal]: - if (kind in [DoorKind.Normal, DoorKind.SmallKey, DoorKind.Bombable, DoorKind.Dashable, - DoorKind.BigKey] and d not in used) - or (d.blocked and d.trapFlag != 0 and 'Boss' not in d.name and 'Agahnim' not in d.name) - or (kind == DoorKind.TrapTriggerable and d.direction in [Direction.North, Direction.East]) - or (kind == DoorKind.Trap2 and d.direction in [Direction.South, Direction.West])): - builder.candidates.big.add(d) - elif d.type == DoorType.Normal: - if (kind in [DoorKind.Normal, DoorKind.SmallKey, DoorKind.Bombable, DoorKind.Dashable, - DoorKind.BigKey] - or (d.blocked and d.trapFlag != 0 and 'Boss' not in d.name and 'Agahnim' not in d.name)): - builder.candidates.trap.add(d) +def find_big_key_candidates(builder, start_regions, used, world, player): + if world.door_type_mode[player] != 'original': # big, all, chaos + # traverse dungeon and find candidates + candidates = [] + checked_doors = set() + for region in start_regions: + possible, checked = find_big_key_door_candidates(region, checked_doors, used, world, player) + candidates.extend([x for x in possible if x not in candidates]) + checked_doors.update(checked) + flat_candidates = [] + for candidate in candidates: + # not valid if: Normal Coupled and Pair in is Checked and Pair is not in Candidates + if (world.decoupledoors[player] or candidate.type != DoorType.Normal + or candidate.dest not in checked_doors or candidate.dest in candidates): + flat_candidates.append(candidate) + + paired_candidates = build_pair_list(flat_candidates) + builder.candidates.big = paired_candidates else: r_set = builder.master_sector.region_set() for r in r_set: @@ -2113,15 +2264,177 @@ def find_big_key_candidates(builder, used, world, player): if ext.door: d = ext.door if d.bigKey and d.type in [DoorType.Normal, DoorType.Interior]: - builder.candidates.big.add(d) + builder.candidates.big.append(d) -def find_small_key_door_candidates(builder, start_regions, world, player): +def find_big_key_door_candidates(region, checked, used, world, player): + decoupled = world.decoupledoors[player] + dungeon_name = region.dungeon.name + candidates = [] + checked_doors = list(checked) + queue = deque([(region, None, None)]) + while len(queue) > 0: + current, last_door, last_region = queue.pop() + for ext in current.exits: + d = ext.door + controlled = d + if d and d.controller: + d = d.controller + if (d and not d.blocked and d.dest is not last_door and d.dest is not last_region + and d not in checked_doors and d not in used): + valid = False + if (0 <= d.doorListPos < 4 and d.type in [DoorType.Interior, DoorType.Normal] + and not d.entranceFlag and d.direction in [Direction.North, Direction.South]): + room = world.get_room(d.roomIndex, player) + position, kind = room.doorList[d.doorListPos] + if d.type == DoorType.Interior: + valid = kind in okay_interiors + if valid and d.dest not in candidates: # interior doors are not separable yet + candidates.append(d.dest) + elif d.type == DoorType.Normal: + if decoupled: + valid = kind in okay_normals + else: + d2 = d.dest + if d2 not in candidates and d2 not in used: + if d2.type == DoorType.Normal: + room_b = world.get_room(d2.roomIndex, player) + pos_b, kind_b = room_b.doorList[d2.doorListPos] + valid = kind in okay_normals and kind_b in okay_normals and valid_key_door_pair(d, d2) + else: + valid = kind in okay_normals + if valid and 0 <= d2.doorListPos < 4: + candidates.append(d2) + else: + valid = True + if valid and d not in candidates: + candidates.append(d) + connected = ext.connected_region + if valid_region_to_explore(connected, dungeon_name, world, player): + queue.append((ext.connected_region, controlled, current)) + if d is not None: + checked_doors.append(d) + return candidates, checked_doors + + +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: + custom_bk_doors = world.custom_door_types[player]['Big Key Door'][builder.name] + else: + custom_bk_doors = [] + if custom_bk_doors: + bk_door_pool = filter_key_door_pool(bk_door_pool, custom_bk_doors) + bk_doors_needed -= len(custom_bk_doors) + if len(bk_door_pool) < bk_doors_needed: + if not drop: + return None, 0 + bk_doors_needed = len(bk_door_pool) + combinations = ncr(len(bk_door_pool), bk_doors_needed) + itr = 0 + sample_list = build_sample_list(combinations, 10000) + proposal = kth_combination(sample_list[itr], bk_door_pool, bk_doors_needed) + proposal.extend(custom_bk_doors) + + start_regions = filter_start_regions(builder, start_regions, world, player) + while not validate_bk_layout(proposal, builder, start_regions, world, player): + itr += 1 + if itr >= len(sample_list): + if not drop: + return None, 0 + bk_doors_needed -= 1 + if bk_doors_needed < 0: + raise Exception(f'Bad dungeon {builder.name} - maybe custom bk doors are bad') + combinations = ncr(len(bk_door_pool), bk_doors_needed) + sample_list = build_sample_list(combinations, 10000) + itr = 0 + proposal = kth_combination(sample_list[itr], bk_door_pool, bk_doors_needed) + proposal.extend(custom_bk_doors) + builder.bk_door_proposal = proposal + return proposal, bk_doors_needed + + +def find_current_bk_doors(builder): + current_doors = [] + for region in builder.master_sector.regions: + for ext in region.exits: + d = ext.door + if d and d.type != DoorType.Logical and d.bigKey: + current_doors.append(d) + return current_doors + + +def reassign_big_key_doors(bk_map, world, player): + for name, big_doors in bk_map.items(): + flat_proposal = flatten_pair_list(big_doors) + builder = world.dungeon_layouts[player][name] + queue = deque(find_current_bk_doors(builder)) + while len(queue) > 0: + d = queue.pop() + if d.type is DoorType.Interior and d not in flat_proposal and d.dest not in flat_proposal: + if not d.entranceFlag: + world.get_room(d.roomIndex, player).change(d.doorListPos, DoorKind.Normal) + d.bigKey = False + elif d.type is DoorType.Normal and d not in flat_proposal: + if not d.entranceFlag: + world.get_room(d.roomIndex, player).change(d.doorListPos, DoorKind.Normal) + d.bigKey = False + for obj in big_doors: + if type(obj) is tuple: + d1 = obj[0] + d2 = obj[1] + if d1.type is DoorType.Interior: + change_door_to_big_key(d1, world, player) + d2.bigKey = True # ensure flag is set + else: + names = [d1.name, d2.name] + found = False + for dp in world.paired_doors[player]: + if dp.door_a in names and dp.door_b in names: + dp.pair = True + found = True + elif dp.door_a in names: + dp.pair = False + elif dp.door_b in names: + dp.pair = False + if not found: + 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) + else: + d = obj + if d.type is DoorType.Interior: + change_door_to_big_key(d, world, player) + d.dest.bigKey = True # ensure flag is set + elif d.type is DoorType.SpiralStairs: + pass # we don't have spiral stairs candidates yet that aren't already key doors + elif d.type is DoorType.Normal: + change_door_to_big_key(d, world, player) + if not world.decoupledoors[player] and d.dest: + if d.dest.type in [DoorType.Normal]: + dest_room = world.get_room(d.dest.roomIndex, 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) + + +def change_door_to_big_key(d, world, player): + d.bigKey = True + room = world.get_room(d.roomIndex, player) + if room.doorList[d.doorListPos][1] != DoorKind.BigKey: + verify_door_list_pos(d, room, world, player) + room.change(d.doorListPos, DoorKind.BigKey) + + +def find_small_key_door_candidates(builder, start_regions, used, world, player): # traverse dungeon and find candidates candidates = [] checked_doors = set() for region in start_regions: - possible, checked = find_key_door_candidates(region, checked_doors, world, player) + possible, checked = find_key_door_candidates(region, checked_doors, used, world, player) candidates.extend([x for x in possible if x not in candidates]) checked_doors.update(checked) flat_candidates = [] @@ -2132,25 +2445,26 @@ def find_small_key_door_candidates(builder, start_regions, world, player): flat_candidates.append(candidate) paired_candidates = build_pair_list(flat_candidates) - builder.candidates = paired_candidates + builder.candidates.small = paired_candidates def calc_used_dungeon_items(builder, world, player): base = 2 + basic_flag = world.doorShuffle[player] == 'basic' if not world.bigkeyshuffle[player]: if builder.bk_required and not builder.bk_provided: base += 1 - if not world.compassshuffle[player]: + if not world.compassshuffle[player] and (builder.name not in ['Hyrule Castle', 'Agahnims Tower'] or not basic_flag): base += 1 - if not world.mapshuffle[player]: + if not world.mapshuffle[player] and (builder.name != 'Agahnims Tower' or not basic_flag): base += 1 return base -def find_valid_combination(builder, start_regions, world, player, drop_keys=True): +def find_valid_combination(builder, target, start_regions, world, player, drop_keys=True): logger = logging.getLogger('') - key_door_pool = list(builder.candidates) # can these be a set? - key_doors_needed = builder.key_doors_num + key_door_pool = list(builder.candidates.small) + key_doors_needed = target if player in world.custom_door_types: custom_key_doors = world.custom_door_types[player]['Key Door'][builder.name] else: @@ -2163,8 +2477,9 @@ def find_valid_combination(builder, start_regions, world, player, drop_keys=True if len(key_door_pool) < key_doors_needed: if not drop_keys: logger.info('No valid layouts for %s with %s doors', builder.name, builder.key_doors_num) - return False + return None, 0 builder.key_doors_num -= key_doors_needed - len(key_door_pool) # reduce number of key doors + key_doors_needed = len(key_door_pool) logger.info('%s: %s', world.fish.translate("cli", "cli", "lowering.keys.candidates"), builder.name) combinations = ncr(len(key_door_pool), key_doors_needed) itr = 0 @@ -2181,12 +2496,12 @@ def find_valid_combination(builder, start_regions, world, player, drop_keys=True if itr >= len(sample_list): if not drop_keys: logger.info('No valid layouts for %s with %s doors', builder.name, builder.key_doors_num) - return False + return None, 0 logger.info('%s: %s', world.fish.translate("cli","cli","lowering.keys.layouts"), builder.name) builder.key_doors_num -= 1 key_doors_needed -= 1 - if builder.key_doors_num < 0: - raise Exception('Bad dungeon %s - less than 0 key doors not valid' % builder.name) + if key_doors_needed < 0: + raise Exception(f'Bad dungeon {builder.name} - less than 0 key doors or invalid custom key door') combinations = ncr(len(key_door_pool), max(0, key_doors_needed)) sample_list = build_sample_list(combinations) itr = 0 @@ -2200,14 +2515,15 @@ def find_valid_combination(builder, start_regions, world, player, drop_keys=True # make changes if player not in world.key_logic.keys(): world.key_logic[player] = {} + builder.total_keys = builder.key_doors_num analyze_dungeon(key_layout, world, player) builder.key_door_proposal = proposal world.key_logic[player][builder.name] = key_layout.key_logic world.key_layout[player][builder.name] = key_layout - return True + return builder.key_door_proposal, key_doors_needed -def build_sample_list(combinations, max_combinations=1000000): +def build_sample_list(combinations, max_combinations=100000): if combinations <= max_combinations: sample_list = list(range(0, int(combinations))) else: @@ -2273,10 +2589,13 @@ def flatten_pair_list(paired_list): return flat_list -okay_normals = [DoorKind.Normal, DoorKind.SmallKey, DoorKind.Bombable, DoorKind.Dashable, DoorKind.DungeonChanger] +okay_normals = [DoorKind.Normal, DoorKind.SmallKey, DoorKind.Bombable, DoorKind.Dashable, + DoorKind.DungeonChanger, DoorKind.BigKey] + +okay_interiors = [DoorKind.Normal, DoorKind.SmallKey, DoorKind.Bombable, DoorKind.Dashable, DoorKind.BigKey] -def find_key_door_candidates(region, checked, world, player): +def find_key_door_candidates(region, checked, used, world, player): decoupled = world.decoupledoors[player] dungeon_name = region.dungeon.name candidates = [] @@ -2289,14 +2608,15 @@ def find_key_door_candidates(region, checked, world, player): controlled = d if d and d.controller: d = d.controller - if d and not d.blocked and d.dest is not last_door and d.dest is not last_region and d not in checked_doors: + if (d and not d.blocked and d.dest is not last_door and d.dest is not last_region + and d not in checked_doors and d not in used): valid = False if (0 <= d.doorListPos < 4 and d.type in [DoorType.Interior, DoorType.Normal, DoorType.SpiralStairs] and not d.entranceFlag): room = world.get_room(d.roomIndex, player) position, kind = room.doorList[d.doorListPos] if d.type == DoorType.Interior: - valid = kind in [DoorKind.Normal, DoorKind.SmallKey, DoorKind.Bombable, DoorKind.Dashable] + valid = kind in okay_interiors if valid and d.dest not in candidates: # interior doors are not separable yet candidates.append(d.dest) elif d.type == DoorType.SpiralStairs: @@ -2306,7 +2626,7 @@ def find_key_door_candidates(region, checked, world, player): valid = kind in okay_normals else: d2 = d.dest - if d2 not in candidates: + if d2 not in candidates and d2 not in used: if d2.type == DoorType.Normal: room_b = world.get_room(d2.roomIndex, player) pos_b, kind_b = room_b.doorList[d2.doorListPos] @@ -2320,7 +2640,7 @@ def find_key_door_candidates(region, checked, world, player): if valid and d not in candidates: candidates.append(d) connected = ext.connected_region - if valid_region_to_explore_lim(connected, dungeon_name, world, player): + if valid_region_to_explore(connected, dungeon_name, world, player): queue.append((ext.connected_region, controlled, current)) if d is not None: checked_doors.append(d) @@ -2333,72 +2653,80 @@ def valid_key_door_pair(door1, door2): return len(door1.entrance.parent_region.exits) <= 1 or len(door2.entrance.parent_region.exits) <= 1 -def reassign_key_doors(builder, world, player): +def reassign_key_doors(small_map, world, player): logger = logging.getLogger('') - logger.debug('Key doors for %s', builder.name) - proposal = builder.key_door_proposal - flat_proposal = flatten_pair_list(proposal) - queue = deque(find_current_key_doors(builder)) - while len(queue) > 0: - d = queue.pop() - if d.type is DoorType.SpiralStairs and d not in proposal: - room = world.get_room(d.roomIndex, player) - if room.doorList[d.doorListPos][1] == DoorKind.StairKeyLow: - room.delete(d.doorListPos) - else: - if len(room.doorList) > 1: - room.mirror(d.doorListPos) # I think this works for crossed now - else: + for name, small_doors in small_map.items(): + logger.debug(f'Key doors for {name}') + builder = world.dungeon_layouts[player][name] + proposal = builder.key_door_proposal + flat_proposal = flatten_pair_list(proposal) + queue = deque(find_current_key_doors(builder)) + while len(queue) > 0: + d = queue.pop() + if d.type is DoorType.SpiralStairs and d not in proposal: + room = world.get_room(d.roomIndex, player) + if room.doorList[d.doorListPos][1] == DoorKind.StairKeyLow: room.delete(d.doorListPos) - d.smallKey = False - elif d.type is DoorType.Interior and d not in flat_proposal and d.dest not in flat_proposal: - if not d.entranceFlag: - world.get_room(d.roomIndex, player).change(d.doorListPos, DoorKind.Normal) - d.smallKey = False - d.dest.smallKey = False - queue.remove(d.dest) - elif d.type is DoorType.Normal and d not in flat_proposal: - if not d.entranceFlag: - world.get_room(d.roomIndex, player).change(d.doorListPos, DoorKind.Normal) - d.smallKey = False - for dp in world.paired_doors[player]: - if dp.door_a == d.name or dp.door_b == d.name: - dp.pair = False - for obj in proposal: - if type(obj) is tuple: - d1 = obj[0] - d2 = obj[1] - if d1.type is DoorType.Interior: - change_door_to_small_key(d1, world, player) - d2.smallKey = True # ensure flag is set - else: - names = [d1.name, d2.name] - found = False + else: + if len(room.doorList) > 1: + room.mirror(d.doorListPos) # I think this works for crossed now + else: + room.delete(d.doorListPos) + d.smallKey = False + elif d.type is DoorType.Interior and d not in flat_proposal and d.dest not in flat_proposal: + if not d.entranceFlag: + world.get_room(d.roomIndex, player).change(d.doorListPos, DoorKind.Normal) + d.smallKey = False + d.dest.smallKey = False + queue.remove(d.dest) + elif d.type is DoorType.Normal and d not in flat_proposal: + if not d.entranceFlag: + world.get_room(d.roomIndex, player).change(d.doorListPos, DoorKind.Normal) + d.smallKey = False for dp in world.paired_doors[player]: - if dp.door_a in names and dp.door_b in names: - dp.pair = True - found = True - elif dp.door_a in names: + if dp.door_a == d.name or dp.door_b == d.name: dp.pair = False - elif dp.door_b in names: - dp.pair = False - if not found: - 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) - else: - d = obj - if d.type is DoorType.Interior: - change_door_to_small_key(d, world, player) - d.dest.smallKey = True # ensure flag is set - elif d.type is DoorType.SpiralStairs: - pass # we don't have spiral stairs candidates yet that aren't already key doors - elif d.type is DoorType.Normal: - change_door_to_small_key(d, world, player) - world.spoiler.set_door_type(d.name, 'Key Door', player) - logger.debug('Key Door: %s', d.name) + for obj in proposal: + if type(obj) is tuple: + d1 = obj[0] + d2 = obj[1] + if d1.type is DoorType.Interior: + change_door_to_small_key(d1, world, player) + d2.smallKey = True # ensure flag is set + else: + names = [d1.name, d2.name] + found = False + for dp in world.paired_doors[player]: + if dp.door_a in names and dp.door_b in names: + dp.pair = True + found = True + elif dp.door_a in names: + dp.pair = False + elif dp.door_b in names: + dp.pair = False + if not found: + 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) + else: + d = obj + if d.type is DoorType.Interior: + change_door_to_small_key(d, world, player) + d.dest.smallKey = True # ensure flag is set + elif d.type is DoorType.SpiralStairs: + pass # we don't have spiral stairs candidates yet that aren't already key doors + elif d.type is DoorType.Normal: + change_door_to_small_key(d, world, player) + if not world.decoupledoors[player] and d.dest: + if d.dest.type in [DoorType.Normal]: + dest_room = world.get_room(d.dest.roomIndex, 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) def change_door_to_small_key(d, world, player): @@ -2499,7 +2827,7 @@ def remove_pair(door, world, player): def stateful_door(door, kind): if 0 <= door.doorListPos < 4: - return kind in [DoorKind.Normal, DoorKind.SmallKey, DoorKind.Bombable, DoorKind.Dashable] #, DoorKind.BigKey] + return kind in [DoorKind.Normal, DoorKind.SmallKey, DoorKind.Bombable, DoorKind.Dashable, DoorKind.BigKey] return False @@ -2780,7 +3108,8 @@ def explore_state(state, world, player): while len(state.avail_doors) > 0: door = state.next_avail_door().door connect_region = world.get_entrance(door.name, player).connected_region - if state.can_traverse(door) and not state.visited(connect_region) and valid_region_to_explore(connect_region, world, player): + if (state.can_traverse(door) and not state.visited(connect_region) + and valid_region_to_explore(connect_region, state.dungeon, world, player)): state.visit_region(connect_region) state.add_all_doors_check_unattached(connect_region, world, player) @@ -2789,7 +3118,8 @@ def explore_state_proposed_traps(state, proposed_traps, world, player): while len(state.avail_doors) > 0: door = state.next_avail_door().door connect_region = world.get_entrance(door.name, player).connected_region - if not state.visited(connect_region) and valid_region_to_explore(connect_region, world, player): + if (not state.visited(connect_region) + and valid_region_to_explore(connect_region, state.dungeon, world, player)): state.visit_region(connect_region) state.add_all_doors_check_proposed_traps(connect_region, proposed_traps, world, player) @@ -2851,6 +3181,7 @@ class DROptions(Flag): # Open_Desert_Wall = 0x80 # No longer pre-opening desert wall - unused Hide_Total = 0x100 DarkWorld_Spawns = 0x200 + BigKeyDoor_Shuffle = 0x400 # DATA GOES DOWN HERE @@ -3913,4 +4244,21 @@ bomb_dash_counts = { 'Ganons Tower': (2, 1) } +# small, big, trap, bomb, dash, hidden, tricky +door_type_counts = { + 'Hyrule Castle': (4, 0, 1, 0, 2, 0, 0), + 'Eastern Palace': (2, 2, 0, 0, 0, 0, 0), + 'Desert Palace': (4, 1, 0, 0, 0, 0, 0), + 'Agahnims Tower': (4, 0, 1, 0, 0, 1, 0), + '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), + '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), + 'Turtle Rock': (6, 2, 2, 0, 2, 0, 1), # 2 bombs kind of for entrances, but I put 0 here + 'Ganons Tower': (8, 2, 5, 2, 1, 0, 0) +} + diff --git a/DungeonGenerator.py b/DungeonGenerator.py index 7e3b2ef7..f06c3535 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -808,6 +808,7 @@ class ExplorationState(object): self.prize_door_set = {} self.prize_doors = [] self.prize_doors_opened = False + self.prize_received = False def copy(self): ret = ExplorationState(dungeon=self.dungeon) @@ -839,6 +840,7 @@ class ExplorationState(object): ret.prize_door_set = dict(self.prize_door_set) ret.prize_doors = list(self.prize_doors) ret.prize_doors_opened = self.prize_doors_opened + ret.prize_received = self.prize_received return ret def next_avail_door(self): @@ -984,6 +986,20 @@ class ExplorationState(object): elif not self.in_door_list(door, self.avail_doors): self.append_door_to_list(door, self.avail_doors) + def add_all_doors_check_big_keys(self, region, big_key_door_proposal, world, player): + for door in get_doors(world, region, player): + 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 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: + if 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 visited(self, region): if self.crystal == CrystalBarrier.Either: return region in self.visited_blue and region in self.visited_orange @@ -1213,6 +1229,8 @@ class DungeonBuilder(object): self.combo_size = None self.flex = 0 self.key_door_proposal = None + self.bk_door_proposal = None + self.trap_door_proposal = None self.allowance = None if 'Stonewall' in name: @@ -1279,9 +1297,7 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player, dunge dungeon_map = {} for key in dungeon_pool: - dungeon_map[key] = DungeonBuilder(key) - for key in dungeon_boss_sectors.keys(): - current_dungeon = dungeon_map[key] + current_dungeon = dungeon_map[key] = DungeonBuilder(key) for r_name in dungeon_boss_sectors[key]: assign_sector(find_sector(r_name, candidate_sectors), current_dungeon, candidate_sectors, global_pole) if key == 'Hyrule Castle' and world.mode[player] == 'standard': @@ -1293,7 +1309,7 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player, dunge candidate_sectors, global_pole) entrances_map, potentials, connections = connections_tuple accessible_sectors, reverse_d_map = set(), {} - for key in dungeon_entrances.keys(): + for key in dungeon_pool: current_dungeon = dungeon_map[key] current_dungeon.all_entrances = dungeon_entrances[key] for r_name in current_dungeon.all_entrances: @@ -1419,6 +1435,8 @@ def identify_destination_sectors(accessible_sectors, reverse_d_map, dungeon_map, if ent_name in found_connections: continue sector = find_sector(ent_name, reverse_d_map.keys()) + if sector is None: + continue if sector in accessible_sectors: found_connections.add(ent_name) accessible_overworld.add(region) # todo: drops don't give ow access diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index bb4f6835..8a70abcf 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -248,7 +248,7 @@ def find_all_locations(sector): def calc_max_chests(builder, key_layout, world, player): - if world.doorShuffle[player] != 'crossed': + if world.doorShuffle[player] in ['basic', 'vanilla']: return len(world.get_dungeon(key_layout.sector.name, player).small_keys) return max(0, builder.key_doors_num - key_layout.max_drops) @@ -1169,6 +1169,16 @@ def expand_key_state(state, flat_proposal, world, player): state.add_all_doors_check_keys(connect_region, flat_proposal, world, player) +def expand_big_key_state(state, flat_proposal, world, player): + while len(state.avail_doors) > 0: + exp_door = state.next_avail_door() + door = exp_door.door + connect_region = world.get_entrance(door.name, player).connected_region + if state.validate(door, connect_region, world, player): + state.visit_region(connect_region, key_checks=True) + state.add_all_doors_check_big_keys(connect_region, flat_proposal, world, player) + + def flatten_pair_list(paired_list): flat_list = [] for d in paired_list: @@ -1398,6 +1408,42 @@ def prize_relevance(key_layout, dungeon_entrance): return None +def prize_relevance_sig2(start_regions, d_name, dungeon_entrance): + if len(start_regions) > 1 and dungeon_entrance and dungeon_table[d_name].prize: + if dungeon_entrance.name in ['Ganons Tower', 'Inverted Ganons Tower']: + return 'GT' + elif dungeon_entrance.name == 'Pyramid Fairy': + return 'BigBomb' + return None + + +def validate_bk_layout(proposal, builder, start_regions, world, player): + bk_special = check_bk_special(builder.master_sector.regions, world, player) + if world.bigkeyshuffle[player] and (world.dropshuffle[player] or not bk_special): + return True + flat_proposal = flatten_pair_list(proposal) + state = ExplorationState(dungeon=builder.name) + state.big_key_special = bk_special + for region in start_regions: + dungeon_entrance, portal_door = find_outside_connection(region) + prize_relevant_flag = prize_relevance_sig2(start_regions, builder.name, dungeon_entrance) + if prize_relevant_flag: + state.append_door_to_list(portal_door, state.prize_doors) + state.prize_door_set[portal_door] = dungeon_entrance + # key_layout.prize_relevant = prize_relevant_flag + else: + state.visit_region(region, key_checks=True) + state.add_all_doors_check_big_keys(region, flat_proposal, world, player) + expand_big_key_state(state, flat_proposal, world, player) + if bk_special: + for loc in state.found_locations: + if loc.forced_big_key(): + return True + else: + return len(state.bk_found) > 0 + return False + + # Soft lock stuff def validate_key_layout(key_layout, world, player): # retro is all good - except for hyrule castle in standard mode @@ -1601,7 +1647,7 @@ def create_key_counters(key_layout, world, player): state.key_locations = len(builder.key_door_proposal) - builder.key_drop_cnt else: builder = world.dungeon_layouts[player][key_layout.sector.name] - state.key_locations = builder.total_keys - builder.key_drop_cnt + state.key_locations = max(0, builder.total_keys - builder.key_drop_cnt) state.big_key_special = False for region in key_layout.sector.regions: for location in region.locations: diff --git a/Main.py b/Main.py index 80c79887..4e6b9c4b 100644 --- a/Main.py +++ b/Main.py @@ -106,6 +106,7 @@ def main(args, seed=None, fish=None): world.enemy_damage = args.enemy_damage.copy() 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.experimental = args.experimental.copy() world.dungeon_counters = args.dungeon_counters.copy() world.fish = fish diff --git a/Rom.py b/Rom.py index 4cb0354c..d6717617 100644 --- a/Rom.py +++ b/Rom.py @@ -37,7 +37,7 @@ from source.dungeon.RoomList import Room0127 JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = 'add982e935888df04ddfa570bc07bede' +RANDOMIZERBASEHASH = 'afcd895b87559cd29b04aa3714cbc929' class JsonRom(object): @@ -738,11 +738,11 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): # setup dr option flags based on experimental, etc. dr_flags = DROptions.Eternal_Mini_Bosses if world.doorShuffle[player] == 'vanilla' else DROptions.Town_Portal - if world.doorShuffle[player] == 'crossed': + if world.doorShuffle[player] not in ['vanilla', 'basic']: dr_flags |= DROptions.Map_Info if world.collection_rate[player] and world.goal[player] not in ['triforcehunt', 'trinity']: dr_flags |= DROptions.Debug - if world.doorShuffle[player] == 'crossed' and world.logic[player] != 'nologic'\ + if world.doorShuffle[player] not in ['vanilla', 'basic'] and world.logic[player] != 'nologic'\ and world.mixed_travel[player] == 'prevent': # PoD Falling Bridge or Hammjump # 1FA607: db $2D, $79, $69 ; 0x0069: Vertical Rail ↕ | { 0B, 1E } | Size: 05 @@ -763,6 +763,8 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): dr_flags |= DROptions.DarkWorld_Spawns if world.logic[player] != 'nologic': dr_flags |= DROptions.Fix_EG + if world.door_type_mode in ['big', 'chaos']: + dr_flags |= DROptions.BigKeyDoor_Shuffle my_locations = world.get_filled_locations(player) valid_locations = [l for l in my_locations if ((l.type == LocationType.Pot and not l.forced_item) @@ -772,7 +774,8 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): valid_loc_by_dungeon = valid_dungeon_locations(valid_locations) # fix hc big key problems (map and compass too) - if world.doorShuffle[player] == 'crossed' or world.dropshuffle[player] or world.pottery[player] not in ['none', 'cave']: + if (world.doorShuffle[player] not in ['vanilla', 'basic'] or world.dropshuffle[player] + or world.pottery[player] not in ['none', 'cave']): rom.write_byte(0x151f1, 2) rom.write_byte(0x15270, 2) sanctuary = world.get_region('Sanctuary', player) @@ -800,7 +803,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): rom.write_bytes(0x13fff4, [0xe4, 0x00]) # patch doors - if world.doorShuffle[player] == 'crossed': + if world.doorShuffle[player] not in ['vanilla', 'basic']: rom.write_byte(0x138002, 2) for name, layout in world.key_layout[player].items(): offset = compass_data[name][4]//2 @@ -1377,7 +1380,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): if len(portal_list) == 1: portal_idx = 0 else: - if world.doorShuffle[player] == 'crossed': + if world.doorShuffle[player] not in ['vanilla', 'basic']: # the random choice excludes sanctuary portal_idx = next((i for i, elem in enumerate(portal_list) if world.get_portal(elem, player).chosen), random.choice([1, 2, 3])) @@ -1392,7 +1395,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): rom.write_bytes(0x53E56+ow_map_index*2, int16_as_bytes(coords[1])) rom.write_byte(0x53EA6+ow_map_index, world_indicator) # in crossed doors - flip the compass exists flags - if world.doorShuffle[player] == 'crossed': + if world.doorShuffle[player] not in ['vanilla', 'basic']: for dungeon, portal_list in dungeon_portals.items(): ow_map_index = dungeon_table[dungeon].map_index exists_flag = any(x for x in world.get_dungeon(dungeon, player).dungeon_items if x.type == 'Compass') @@ -1495,7 +1498,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): rom.write_bytes(0x180188, [0x20, 0, 0]) # Zelda respawn refills (magic, bombs, arrows) rom.write_bytes(0x18018B, [0x20, 0, 0]) # Mantle respawn refills (magic, bombs, arrows) magic_max, magic_small = 0x80, 0x20 - if world.doorShuffle[player] == 'crossed': + if world.doorShuffle[player] not in ['vanilla', 'basic']: # Uncle respawn refills (magic, bombs, arrows) rom.write_bytes(0x180185, [max(0x20, magic_max), max(3, bomb_max), max(10, bow_max)]) rom.write_bytes(0x180188, [0x20, 3, 10]) # Zelda respawn refills (magic, bombs, arrows) @@ -2081,13 +2084,13 @@ def write_strings(rom, world, player, team): # Next we write a few hints for specific inconvenient locations. We don't make many because in entrance this is highly unpredictable. locations_to_hint = InconvenientLocations.copy() - if world.doorShuffle[player] != 'crossed': + if world.doorShuffle[player] == 'vanilla': locations_to_hint.extend(InconvenientDungeonLocations) if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull']: locations_to_hint.extend(InconvenientVanillaLocations) random.shuffle(locations_to_hint) hint_count = 3 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull'] else 5 - hint_count -= 2 if world.doorShuffle[player] == 'crossed' else 0 + hint_count -= 2 if world.doorShuffle[player] not in ['vanilla', 'basic'] else 0 del locations_to_hint[hint_count:] for location in locations_to_hint: if location == 'Swamp Left': @@ -2150,7 +2153,7 @@ def write_strings(rom, world, player, team): items_to_hint.extend(BigKeys) random.shuffle(items_to_hint) hint_count = 5 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull'] else 8 - hint_count += 2 if world.doorShuffle[player] == 'crossed' else 0 + hint_count += 2 if world.doorShuffle[player] not in ['vanilla', 'basic'] else 0 while hint_count > 0 and len(items_to_hint) > 0: this_item = items_to_hint.pop(0) this_location = world.find_items_not_key_only(this_item, player) @@ -2163,8 +2166,8 @@ def write_strings(rom, world, player, team): tt[hint_locations.pop(0)] = this_hint hint_count -= 1 - # Adding a hint for the Thieves' Town Attic location in Crossed door shuffle. - if world.doorShuffle[player] in ['crossed']: + # Adding a hint for the Thieves' Town Attic location in mixed door shuffles. + if world.doorShuffle[player] not in ['vanilla', 'basic']: attic_hint = world.get_location("Thieves' Town - Attic", player).parent_region.dungeon.name this_hint = 'A cracked floor can be found in ' + attic_hint + '.' if world.intensity[player] < 2 and hint_locations[0] == 'telepathic_tile_thieves_town_upstairs': @@ -2324,7 +2327,7 @@ def write_strings(rom, world, player, team): tt['tablet_bombos_book'] = bombos_text # attic hint - if world.doorShuffle[player] in ['crossed']: + if world.doorShuffle[player] not in ['vanilla', 'basic']: attic_hint = world.get_location("Thieves' Town - Attic", player).parent_region.dungeon.name tt['blind_not_that_way'] = f'{attic_hint} is too bright for my eyes' # see tagalog.asm tables at 957,967 or Follower_HandleTrigger in JPDASM diff --git a/RoomData.py b/RoomData.py index cfdd183f..03700e32 100644 --- a/RoomData.py +++ b/RoomData.py @@ -395,8 +395,8 @@ class DoorKind(Enum): Bombable = 0x2E BlastWall = 0x30 Hidden = 0x32 - TrapTriggerable = 0x36 # right side trap or south side trap (West, South) - Trap2 = 0x38 # left side trap or north side trap (East, North) + TrapTriggerable = 0x36 # right side trap or bottom side trap (West, North) + Trap2 = 0x38 # left side trap or top side trap (East, South) NormalLow2 = 0x40 TrapTriggerableLow = 0x44 Warp = 0x46 diff --git a/data/base2current.bps b/data/base2current.bps index 060733f6..d657ad40 100644 Binary files a/data/base2current.bps and b/data/base2current.bps differ diff --git a/mystery_example.yml b/mystery_example.yml index 8363d05d..34ce9963 100644 --- a/mystery_example.yml +++ b/mystery_example.yml @@ -17,6 +17,11 @@ 1: 2 2: 2 3: 4 + door_type_mode: + original: 2 + big: 2 + all: 1 + chaos: 1 decoupledoors: off dropshuffle: on: 1 diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index 8b971856..bc7feeac 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -155,6 +155,7 @@ "door_shuffle": { "choices": [ "basic", + "partitioned", "crossed", "vanilla" ] @@ -164,7 +165,15 @@ "3", "2", "1", "random" ] }, - "deoupledoors": { + "door_type_mode": { + "choices":[ + "original", + "big", + "all", + "chaos" + ] + }, + "decoupledoors": { "action": "store_true", "type": "bool" }, diff --git a/resources/app/cli/lang/en.json b/resources/app/cli/lang/en.json index 57c4971c..681570aa 100644 --- a/resources/app/cli/lang/en.json +++ b/resources/app/cli/lang/en.json @@ -205,6 +205,7 @@ "door_shuffle": [ "Select Door Shuffling Algorithm. (default: %(default)s)", "Basic: Doors are mixed within a single dungeon.", + "Partitioned Doors are mixed in 3 partitions: L1-3+HC+AT, D1-4, D5-8", "Crossed: Doors are mixed between all dungeons.", "Vanilla: All doors are connected the same way they were in the", " base game." @@ -216,6 +217,13 @@ "3: And shuffles dungeon lobbies", "random: Picks one of those at random" ], + "door_type_mode" : [ + "Door Types to Shuffle (default: %(default)s)", + "original: Shuffles key doors, bombable, and dashable doors", + "big: Adds big key doors", + "all: Adds traps doors (and any future supported door types)", + "chaos: Increases the number of door types in all dungeon pools" + ], "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 8e3f0f8d..c8232000 100644 --- a/resources/app/gui/lang/en.json +++ b/resources/app/gui/lang/en.json @@ -73,6 +73,7 @@ "randomizer.dungeon.dungeondoorshuffle": "Dungeon Door Shuffle", "randomizer.dungeon.dungeondoorshuffle.vanilla": "Vanilla", "randomizer.dungeon.dungeondoorshuffle.basic": "Basic", + "randomizer.dungeon.dungeondoorshuffle.partitioned": "Partitioned", "randomizer.dungeon.dungeondoorshuffle.crossed": "Crossed", "randomizer.dungeon.dungeonintensity": "Intensity Level", @@ -81,6 +82,12 @@ "randomizer.dungeon.dungeonintensity.3": "3: Dungeon Lobbies", "randomizer.dungeon.dungeonintensity.random": "Random", + "randomizer.dungeon.door_type_mode": "Door Types to Shuffle", + "randomizer.dungeon.door_type_mode.original": "Small Key Doors, Bomb Doors, Dash Doors", + "randomizer.dungeon.door_type_mode.big": "Adds Big Key Doors", + "randomizer.dungeon.door_type_mode.all": "Adds Trap Doors", + "randomizer.dungeon.door_type_mode.chaos": "Increases all door types", + "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 b45df12f..df7d3474 100644 --- a/resources/app/gui/randomize/dungeon/widgets.json +++ b/resources/app/gui/randomize/dungeon/widgets.json @@ -6,6 +6,7 @@ "options": [ "vanilla", "basic", + "partitioned", "crossed" ] }, @@ -22,6 +23,19 @@ "width": 45 } }, + "door_type_mode": { + "type": "selectbox", + "default": "basic", + "options": [ + "original", + "big", + "all", + "chaos" + ], + "config": { + "width": 45 + } + }, "decoupledoors": { "type": "checkbox" }, "keydropshuffle": { "type": "checkbox" }, "pottery": { diff --git a/source/classes/CustomSettings.py b/source/classes/CustomSettings.py index 57f9cf7e..253f9310 100644 --- a/source/classes/CustomSettings.py +++ b/source/classes/CustomSettings.py @@ -87,6 +87,7 @@ class CustomSettings(object): args.standardize_palettes[p] = get_setting(settings['standardize_palettes'], 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.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]) @@ -182,6 +183,7 @@ class CustomSettings(object): settings_dict[p]['shuffle'] = world.shuffle[p] 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]['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 677e163b..b95604d4 100644 --- a/source/classes/constants.py +++ b/source/classes/constants.py @@ -95,6 +95,7 @@ SETTINGSTOPROCESS = { "bigkeyshuffle": "bigkeyshuffle", "dungeondoorshuffle": "door_shuffle", "dungeonintensity": "intensity", + "door_type_mode": "door_type_mode", "decoupledoors": "decoupledoors", "keydropshuffle": "keydropshuffle", "dropshuffle": "dropshuffle", diff --git a/source/dungeon/DungeonStitcher.py b/source/dungeon/DungeonStitcher.py index 05721c9b..b179cb1c 100644 --- a/source/dungeon/DungeonStitcher.py +++ b/source/dungeon/DungeonStitcher.py @@ -135,9 +135,9 @@ def create_random_proposal(doors_to_connect, world, player): proposal[secondary_door] = primary_door primary_bucket[opp_hook].remove(secondary_door) secondary_bucket[next_hook].remove(primary_door) - logger.debug(f'Linking {primary_door.name} <-> {secondary_door.name}') + logger.debug(f' Linking {primary_door.name} <-> {secondary_door.name}') else: - logger.debug(f'Linking {primary_door.name} -> {secondary_door.name}') + logger.debug(f' Linking {primary_door.name} -> {secondary_door.name}') def decouple_check(primary_list, secondary_list, primary_door, secondary_door, world, player): @@ -203,11 +203,11 @@ def modify_proposal(proposed_map, explored_state, doors_to_connect, hash_code_se itr += 1 if not world.decoupledoors[player]: - logger.debug(f'Re-linking {attempt.name} <-> {new_door.name}') - logger.debug(f'Re-linking {old_attempt.name} <-> {old_target.name}') + logger.debug(f' Re-linking {attempt.name} <-> {new_door.name}') + logger.debug(f' Re-linking {old_attempt.name} <-> {old_target.name}') else: - logger.debug(f'Re-Linking {attempt.name} -> {new_door.name}') - logger.debug(f'Re-Linking {old_attempt.name} -> {old_target.name}') + logger.debug(f' Re-Linking {attempt.name} -> {new_door.name}') + logger.debug(f' Re-Linking {old_attempt.name} -> {old_target.name}') hash_code_set.add(hash_code) return proposed_map, hash_code @@ -349,12 +349,7 @@ def connect_doors(a, b): return # Connect supported types if a.type in [DoorType.Normal, DoorType.SpiralStairs, DoorType.Open, DoorType.StraightStairs, DoorType.Ladder]: - if a.blocked: - connect_one_way(b.entrance, a.entrance) - elif b.blocked: - connect_one_way(a.entrance, b.entrance) - else: - connect_two_way(a.entrance, b.entrance) + connect_two_way(a.entrance, b.entrance) dep_doors, target = [], None if len(a.dependents) > 0: dep_doors, target = a.dependents, b @@ -527,7 +522,7 @@ class ExplorationState(object): self.key_locations += 1 if location.name not in dungeon_events and '- Prize' not in location.name and location.name not in ['Agahnim 1', 'Agahnim 2']: self.ttl_locations += 1 - if location not in self.found_locations: # todo: special logic for TT Boss? + if location not in self.found_locations: self.found_locations.append(location) if not bk_flag: self.bk_found.add(location) @@ -616,7 +611,7 @@ class ExplorationState(object): elif not self.in_door_list(door, self.avail_doors): self.append_door_to_list(door, self.avail_doors, flag) - def add_all_doors_check_proposed_2(self, region, proposed_map, valid_doors, flag, world, player): + 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: self.visited_doors.add(door) @@ -625,16 +620,16 @@ class ExplorationState(object): 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, flag) + 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, flag) + 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, flag) + 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): diff --git a/source/tools/MysteryUtils.py b/source/tools/MysteryUtils.py index 8325719b..3816ef7c 100644 --- a/source/tools/MysteryUtils.py +++ b/source/tools/MysteryUtils.py @@ -77,6 +77,7 @@ def roll_settings(weights): door_shuffle = get_choice('door_shuffle') 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.decoupledoors = get_choice('decoupledoors') == 'on' ret.experimental = get_choice('experimental') == 'on' ret.collection_rate = get_choice('collection_rate') == 'on'