From 09fbdc46cadb018f2731a67d8345aa1ad2f55953 Mon Sep 17 00:00:00 2001 From: aerinon Date: Fri, 5 Aug 2022 14:01:19 -0600 Subject: [PATCH] Finish bomb/dash doors Lots of minor fixes Fixed a few existing bugs --- DoorShuffle.py | 417 +++++++++++++++++++++++------- DungeonGenerator.py | 12 +- Main.py | 1 + Rom.py | 5 +- RoomData.py | 6 +- data/base2current.bps | Bin 93211 -> 93229 bytes source/dungeon/DungeonStitcher.py | 3 +- 7 files changed, 338 insertions(+), 106 deletions(-) diff --git a/DoorShuffle.py b/DoorShuffle.py index 42d32c24..69baf30f 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -18,7 +18,7 @@ from source.dungeon.DungeonStitcher import ExplorationState as ExplorationState2 # from DungeonGenerator import generate_dungeon 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 dungeon_portals, dungeon_drops, connect_doors, count_reserved_locations 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 @@ -139,7 +139,7 @@ def link_doors_main(world, player): ['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': - pool = [list(dungeon_regions.keys()), sum(r for r in dungeon_regions.values())] + 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]) @@ -172,6 +172,9 @@ def mark_regions(world, player): def create_door_spoiler(world, player): logger = logging.getLogger('') + shuffled_door_types = [DoorType.Normal, DoorType.SpiralStairs] + if world.intensity[player] > 1: + shuffled_door_types += [DoorType.Open, DoorType.StraightStairs, DoorType.Ladder] queue = deque(world.dungeon_layouts[player].values()) while len(queue) > 0: @@ -185,20 +188,23 @@ def create_door_spoiler(world, player): for ext in next.exits: door_a = ext.door connect = ext.connected_region - if door_a and door_a.type in [DoorType.Normal, DoorType.SpiralStairs, DoorType.Open, - DoorType.StraightStairs, DoorType.Ladder] and door_a not in done: + if door_a and door_a.type in shuffled_door_types and door_a not in done: done.add(door_a) + door_b = door_a.dest if door_b and not isinstance(door_b, Region): - done.add(door_b) - if not door_a.blocked and not door_b.blocked: - world.spoiler.set_door(door_a.name, door_b.name, 'both', player, builder.name) - elif door_a.blocked: - world.spoiler.set_door(door_b.name, door_a.name, 'entrance', player, builder.name) - elif door_b.blocked: + if world.decoupledoors[player]: world.spoiler.set_door(door_a.name, door_b.name, 'entrance', player, builder.name) else: - logger.warning('This is a bug during door spoiler') + done.add(door_b) + if not door_a.blocked and not door_b.blocked: + world.spoiler.set_door(door_a.name, door_b.name, 'both', player, builder.name) + elif door_a.blocked: + world.spoiler.set_door(door_b.name, door_a.name, 'entrance', player, builder.name) + elif door_b.blocked: + world.spoiler.set_door(door_a.name, door_b.name, 'entrance', player, builder.name) + else: + logger.warning('This is a bug during door spoiler') elif not isinstance(door_b, Region): logger.warning('Door not connected: %s', door_a.name) if connect and connect.type == RegionType.Dungeon and connect not in visited: @@ -305,12 +311,7 @@ def connect_door_only(world, exit_name, region, player): def connect_interior_doors(a, b, world, player): door_a = world.get_door(a, player) door_b = world.get_door(b, player) - if door_a.blocked: - connect_one_way(world, b, a, player) - elif door_b.blocked: - connect_one_way(world, a, b, player) - else: - connect_two_way(world, a, b, player) + connect_two_way(world, a, b, player) def connect_two_way(world, entrancename, exitname, player): @@ -790,7 +791,7 @@ def main_dungeon_pool(dungeon_pool, world, player): door_type_pools = [] for pool, region_list in dungeon_pool: if len(pool) == 1: - dungeon_key = next(pool) + dungeon_key = next(iter(pool)) sector_pool = convert_to_sectors(region_list, world, player) merge_sectors(sector_pool, world, player) dungeon_builders[dungeon_key] = simple_dungeon_builder(dungeon_key, sector_pool) @@ -823,6 +824,17 @@ def main_dungeon_pool(dungeon_pool, world, player): check_required_paths(paths, world, player) + for pool, door_type_pool in door_type_pools: + for name in pool: + builder = world.dungeon_layouts[player][name] + region_set = builder.master_sector.region_set() + builder.bk_required = len(builder.bk_door_proposal) > 0 or any(x in region_set for x in special_bk_regions) + dungeon = world.get_dungeon(name, player) + if not builder.bk_required or builder.bk_provided: + dungeon.big_key = None + elif builder.bk_required and not builder.bk_provided: + dungeon.big_key = ItemFactory(dungeon_bigs[name], player) + all_dungeon_items_cnt = len(list(y for x in world.dungeons if x.player == player for y in x.all_items)) target_items = 34 if world.retro[player]: @@ -835,12 +847,12 @@ 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) +special_bk_regions = ['Hyrule Dungeon Cellblock', "Thieves Blind's Cell"] + + def cross_dungeon_clean_up(world, player): # Re-assign dungeon bosses gt = world.get_dungeon('Ganons Tower', player) @@ -956,47 +968,6 @@ def finish_up_work(world, player): refine_boss_exits(world, player) -# def unpair_all_doors(world, player): -# for paired_door in world.paired_doors[player]: -# paired_door.pair = False - -def within_dungeon(world, player): - add_inaccessible_doors(world, player) - entrances_map, potentials, connections = determine_entrance_list(world, player) - connections_tuple = (entrances_map, potentials, connections) - - dungeon_builders = {} - for key in dungeon_regions.keys(): - sector_list = convert_to_sectors(dungeon_regions[key], world, player) - dungeon_builders[key] = simple_dungeon_builder(key, sector_list) - dungeon_builders[key].entrance_list = list(entrances_map[key]) - recombinant_builders = {} - entrances, splits = create_dungeon_entrances(world, player) - builder_info = entrances, splits, connections_tuple, world, player - handle_split_dungeons(dungeon_builders, recombinant_builders, entrances_map, builder_info) - main_dungeon_generation(dungeon_builders, recombinant_builders, connections_tuple, world, player) - - paths = determine_required_paths(world, player) - check_required_paths(paths, world, player) - - setup_custom_door_types(world, player) - # shuffle_key_doors for dungeons - logging.getLogger('').info(world.fish.translate("cli", "cli", "shuffling.keydoors")) - start = time.process_time() - for builder in world.dungeon_layouts[player].values(): - shuffle_key_doors(builder, world, player) - logging.getLogger('').info('%s: %s', world.fish.translate("cli", "cli", "keydoor.shuffle.time"), time.process_time()-start) - if not world.decoupledoors[player]: - smooth_door_pairs(world, player) - - if world.intensity[player] >= 3: - portal = world.get_portal('Sanctuary', player) - target = portal.door.entrance.parent_region - connect_simple_door(world, 'Sanctuary Mirror Route', target, player) - - refine_boss_exits(world, player) - - def handle_split_dungeons(dungeon_builders, recombinant_builders, entrances_map, builder_info): dungeon_entrances, split_dungeon_entrances, c_tuple, world, player = builder_info if dungeon_entrances is None: @@ -1601,7 +1572,12 @@ def refine_boss_exits(world, player): if len(reachable_portals) == 0: reachable_portals = possible_portals unreachable = world.inaccessible_regions[player] - filtered = [x for x in reachable_portals if x.door.entrance.connected_region.name not in unreachable] + filtered = [] + for reachable in reachable_portals: + for entrance in reachable.door.entrance.connected_region.entrances: + parent = entrance.parent_region + if parent.type != RegionType.Dungeon and parent.name not in unreachable: + filtered.append(reachable) if 0 < len(filtered) < len(reachable_portals): reachable_portals = filtered chosen_one = random.choice(reachable_portals) if len(reachable_portals) > 1 else reachable_portals[0] @@ -1766,8 +1742,7 @@ class BuilderDoorCandidates: self.small = [] self.big = [] self.trap = [] - self.bombable = [] - self.dashable = [] + self.bomb_dash = [] def shuffle_door_types(door_type_pools, paths, world, player): @@ -1776,17 +1751,19 @@ def shuffle_door_types(door_type_pools, paths, world, player): 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) + + world.paired_doors[player].clear() + used_doors = shuffle_trap_doors(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 + used_doors = shuffle_bomb_dash_doors(door_type_pools, used_doors, start_regions_map, world, player) + # handle paired list -def shuffle_door_traps(door_type_pools, paths, start_regions_map, 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 @@ -1804,6 +1781,8 @@ def shuffle_door_traps(door_type_pools, paths, start_regions_map, world, player) 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: + return used_doors for dungeon in pool: builder = world.dungeon_layouts[player][dungeon] proportion = len(builder.candidates.trap) @@ -1862,6 +1841,8 @@ def shuffle_big_key_doors(door_type_pools, used_doors, start_regions_map, world, 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) + if ttl == 0: + return used_doors for dungeon in pool: builder = world.dungeon_layouts[player][dungeon] proportion = len(builder.candidates.big) @@ -1934,6 +1915,7 @@ def shuffle_small_key_doors(door_type_pools, used_doors, start_regions_map, worl 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 + remaining -= suggested + builder.key_drop_cnt builder.combo_size = combo_size flex_map[dungeon] = (limit - suggested) if suggested < limit else 0 for dungeon in pool: @@ -1986,6 +1968,79 @@ def shuffle_small_key_doors(door_type_pools, used_doors, start_regions_map, worl return used_doors +def shuffle_bomb_dash_doors(door_type_pools, used_doors, start_regions_map, world, player): + for pool, door_type_pool in door_type_pools: + ttl = 0 + suggestion_map, bd_map = {}, {} + remaining_bomb = door_type_pool.bombable + remaining_dash = door_type_pool.dashable + + if player in world.custom_door_types: + custom_bomb_doors = world.custom_door_types[player]['Bomb Door'] + custom_dash_doors = world.custom_door_types[player]['Dash Door'] + else: + custom_bomb_doors = defaultdict(list) + custom_dash_doors = defaultdict(list) + + for dungeon in pool: + builder = world.dungeon_layouts[player][dungeon] + find_bd_candidates(builder, start_regions_map[dungeon], used_doors, world, player) + if custom_bomb_doors[dungeon]: + builder.candidates.bomb_dash = filter_key_door_pool(builder.candidates.bomb_dash, custom_bomb_doors[dungeon]) + remaining_bomb -= len(custom_bomb_doors[dungeon]) + if custom_dash_doors[dungeon]: + builder.candidates.bomb_dash = filter_key_door_pool(builder.candidates.bomb_dash, custom_dash_doors[dungeon]) + remaining_dash -= len(custom_dash_doors[dungeon]) + ttl += len(builder.candidates.bomb_dash) + if ttl == 0: + return used_doors + for dungeon in pool: + builder = world.dungeon_layouts[player][dungeon] + proportion = len(builder.candidates.bomb_dash) + calc = int(round(proportion * door_type_pool.bombable/ttl)) + suggested_bomb = min(proportion, calc) + remaining_bomb -= suggested_bomb + calc = int(round(proportion * door_type_pool.dashable/ttl)) + suggested_dash = min(proportion, calc) + remaining_dash -= suggested_dash + suggestion_map[dungeon] = suggested_bomb, suggested_dash + for dungeon in pool: + builder = world.dungeon_layouts[player][dungeon] + bomb_doors, dash_doors, bd_number = find_valid_bd_combination(builder, suggestion_map[dungeon], world, player) + bd_map[dungeon] = (bomb_doors, dash_doors) + if bd_number < suggestion_map[dungeon][0] + suggestion_map[dungeon][1]: + remaining_bomb += suggestion_map[dungeon][0] - len(bomb_doors) + remaining_dash += suggestion_map[dungeon][1] - len(dash_doors) + suggestion_map[dungeon] = len(bomb_doors), len(dash_doors) + builder_order = [x for x in pool] + random.shuffle(builder_order) + queue = deque(builder_order) + while len(queue) > 0 and (remaining_bomb > 0 or remaining_dash > 0): + dungeon = queue.popleft() + builder = world.dungeon_layouts[player][dungeon] + type_pool = [] + if remaining_bomb > 0: + type_pool.append('bomb') + if remaining_dash > 0: + type_pool.append('dash') + type_choice = random.choice(type_pool) + pair = suggestion_map[dungeon] + pair = pair[0] + (1 if type_choice == 'bomb' else 0), pair[1] + (1 if type_choice == 'dash' else 0) + bomb_doors, dash_doors, bd_number = find_valid_bd_combination(builder, pair, world, player) + if bomb_doors and dash_doors: + bd_map[dungeon] = (bomb_doors, dash_doors) + remaining_bomb -= (1 if type_choice == 'bomb' else 0) + remaining_dash -= (1 if type_choice == 'dash' else 0) + suggestion_map[dungeon] = pair + queue.append(dungeon) + # time to re-assign + reassign_bd_doors(bd_map, world, player) + for name, pair in bd_map.items(): + used_doors.update(flatten_pair_list(pair[0])) + used_doors.update(flatten_pair_list(pair[1])) + return used_doors + + def shuffle_key_doors(builder, world, player): start_regions = convert_regions(builder.path_entrances, world, player) # count number of key doors - this could be a table? @@ -2049,7 +2104,7 @@ def find_trappable_candidates(builder, world, player): else: r_set = builder.master_sector.region_set() for r in r_set: - for ext in r.exits: + 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: @@ -2188,7 +2243,7 @@ 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) + # 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 @@ -2213,18 +2268,19 @@ def change_door_to_trap(d, world, player): if d.type is DoorType.Interior: kind = room.kind(d) new_kind = None - if kind == DoorKind.TrapTriggerable and d.direction in [Direction.North, Direction.West]: + if kind == DoorKind.TrapTriggerable and d.direction in [Direction.South, Direction.East]: new_kind = DoorKind.Trap - elif kind == DoorKind.Trap2 and d.direction in [Direction.South, Direction.East]: + elif kind == DoorKind.Trap2 and d.direction in [Direction.North, Direction.West]: new_kind = DoorKind.Trap - elif d.direction in [Direction.North, Direction.West]: - new_kind = DoorKind.Trap2 elif d.direction in [Direction.South, Direction.East]: + new_kind = DoorKind.Trap2 + elif d.direction in [Direction.North, Direction.West]: new_kind = DoorKind.TrapTriggerable if new_kind: d.blocked = True - verify_door_list_pos(d, room, world, player, pos=3) - d.trapFlag = {0: 0x4, 1: 0x2, 2: 0x1}[d.doorListPos] + pos = 3 if d.type == DoorType.Normal else 4 + verify_door_list_pos(d, room, world, player, pos) + d.trapFlag = {0: 0x4, 1: 0x2, 2: 0x1, 3: 0x8}[d.doorListPos] room.change(d.doorListPos, new_kind) if d.entrance.connected_region is not None: d.entrance.connected_region.entrances.remove(d.entrance) @@ -2281,10 +2337,10 @@ def find_big_key_door_candidates(region, checked, used, world, player): 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): + and d not in checked_doors): 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]): + and not d.entranceFlag and d.direction in [Direction.North, Direction.South] and d not in used): room = world.get_room(d.roomIndex, player) position, kind = room.doorList[d.doorListPos] if d.type == DoorType.Interior: @@ -2366,6 +2422,7 @@ def find_current_bk_doors(builder): def reassign_big_key_doors(bk_map, world, player): + logger = logging.getLogger('') for name, big_doors in bk_map.items(): flat_proposal = flatten_pair_list(big_doors) builder = world.dungeon_layouts[player][name] @@ -2388,21 +2445,11 @@ def reassign_big_key_doors(bk_map, world, player): 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)) + 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}') else: d = obj if d.type is DoorType.Interior: @@ -2419,6 +2466,7 @@ def reassign_big_key_doors(bk_map, world, player): 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}') def change_door_to_big_key(d, world, player): @@ -2449,7 +2497,7 @@ def find_small_key_door_candidates(builder, start_regions, used, world, player): def calc_used_dungeon_items(builder, world, player): - base = 2 + base = max(count_reserved_locations(world, player, builder.location_set), 2) basic_flag = world.doorShuffle[player] == 'basic' if not world.bigkeyshuffle[player]: if builder.bk_required and not builder.bk_provided: @@ -2523,7 +2571,186 @@ def find_valid_combination(builder, target, start_regions, world, player, drop_k return builder.key_door_proposal, key_doors_needed -def build_sample_list(combinations, max_combinations=100000): +def find_bd_candidates(builder, start_regions, used, world, player): + # traverse dungeon and find candidates + candidates = [] + checked_doors = set() + for region in start_regions: + possible, checked = find_bd_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) + builder.candidates.bomb_dash = build_pair_list(flat_candidates) + + +def find_bd_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): + valid = False + if (0 <= d.doorListPos < 4 and d.type in [DoorType.Interior, DoorType.Normal] and not d.entranceFlag + and d not in used): + 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_bd_combination(builder, suggested, world, player): + # bombable/dashable doors could be excluded in escape in standard until we can guarantee bomb access + # if world.mode[player] == 'standard' and builder.name == 'Hyrule Castle': + # return None, None, 0 + 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: + 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: + custom_bomb_doors = [] + custom_dash_doors = [] + if custom_bomb_doors: + bd_door_pool = filter_key_door_pool(bd_door_pool, custom_bomb_doors) + bomb_doors_needed -= len(custom_bomb_doors) + if custom_dash_doors: + bd_door_pool = filter_key_door_pool(bd_door_pool, custom_dash_doors) + dash_doors_needed -= len(custom_dash_doors) + while len(bd_door_pool) < bomb_doors_needed + dash_doors_needed: + bomb_doors_needed = round(len(bd_door_pool) * bomb_doors_needed/ttl_needed) + dash_doors_needed = round(len(bd_door_pool) * dash_doors_needed/ttl_needed) + bomb_proposal = random.sample(bd_door_pool, k=bomb_doors_needed) + bomb_proposal.extend(custom_bomb_doors) + dash_pool = [x for x in bd_door_pool if x not in bomb_proposal] + dash_proposal = random.sample(dash_pool, k=dash_doors_needed) + dash_proposal.extend(custom_dash_doors) + return bomb_proposal, dash_proposal, ttl_needed + + +def reassign_bd_doors(bd_map, world, player): + for name, pair in bd_map.items(): + flat_bomb_proposal = flatten_pair_list(pair[0]) + flat_dash_proposal = flatten_pair_list(pair[1]) + + def not_in_proposal(door): + return (door not in flat_bomb_proposal and door.dest not in flat_bomb_proposal and + door not in flat_dash_proposal and door.dest not in flat_bomb_proposal) + + builder = world.dungeon_layouts[player][name] + queue = deque(find_current_bd_doors(builder, world)) + while len(queue) > 0: + d = queue.pop() + if d.type is DoorType.Interior and not_in_proposal(d): + if not d.entranceFlag: + world.get_room(d.roomIndex, player).change(d.doorListPos, DoorKind.Normal) + elif d.type is DoorType.Normal and not_in_proposal(d): + if not d.entranceFlag: + world.get_room(d.roomIndex, player).change(d.doorListPos, DoorKind.Normal) + do_bombable_dashable(flat_bomb_proposal, DoorKind.Bombable, world, player) + do_bombable_dashable(flat_dash_proposal, DoorKind.Dashable, world, player) + + +def do_bombable_dashable(proposal, kind, world, player): + for obj in proposal: + if type(obj) is tuple: + d1 = obj[0] + d2 = obj[1] + if d1.type is DoorType.Interior: + change_door_to_kind(d1, kind, world, player) + 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_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) + else: + d = obj + if d.type is DoorType.Interior: + change_door_to_kind(d, kind, world, player) + elif d.type is DoorType.Normal: + change_door_to_kind(d, kind, world, player) + if not world.decoupledoors[player] and d.dest: + if d.dest.type in okay_normals and not std_forbidden(d.dest, world, player): + dest_room = world.get_room(d.dest.roomIndex, player) + if stateful_door(d.dest, dest_room.kind(d.dest)): + 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) + + +def find_current_bd_doors(builder, world): + current_doors = [] + for region in builder.master_sector.regions: + for ext in region.exits: + d = ext.door + if d and d.type in [DoorType.Interior, DoorType.Normal]: + kind = d.kind(world) + if kind in [DoorKind.Dashable, DoorKind.Bombable]: + current_doors.append(d) + return current_doors + + +def change_door_to_kind(d, kind, world, player): + room = world.get_room(d.roomIndex, player) + if room.doorList[d.doorListPos][1] != kind: + verify_door_list_pos(d, room, world, player) + room.change(d.doorListPos, kind) + + +def build_sample_list(combinations, max_combinations=10000): if combinations <= max_combinations: sample_list = list(range(0, int(combinations))) else: @@ -2609,10 +2836,10 @@ def find_key_door_candidates(region, checked, used, world, player): 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): + and d not in checked_doors): valid = False if (0 <= d.doorListPos < 4 and d.type in [DoorType.Interior, DoorType.Normal, DoorType.SpiralStairs] - and not d.entranceFlag): + and not d.entranceFlag and d not in used): room = world.get_room(d.roomIndex, player) position, kind = room.doorList[d.doorListPos] if d.type == DoorType.Interior: diff --git a/DungeonGenerator.py b/DungeonGenerator.py index f06c3535..055a7252 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -15,6 +15,8 @@ from Regions import dungeon_events, flooded_keys_reverse from Dungeons import dungeon_regions, split_region_starts from RoomData import DoorKind +from source.dungeon.DungeonStitcher import generate_dungeon_find_proposal + class GraphPiece: @@ -62,7 +64,7 @@ def generate_dungeon(builder, entrance_region_names, split_dungeon, world, playe if builder.valid_proposal: # we made this earlier in gen, just use it proposed_map = builder.valid_proposal else: - proposed_map = generate_dungeon_find_proposal(builder, entrance_region_names, split_dungeon, world, player) + proposed_map = generate_dungeon_find_proposal_old(builder, entrance_region_names, split_dungeon, world, player) builder.valid_proposal = proposed_map queue = collections.deque(proposed_map.items()) while len(queue) > 0: @@ -80,7 +82,7 @@ def generate_dungeon(builder, entrance_region_names, split_dungeon, world, playe return master_sector -def generate_dungeon_find_proposal(builder, entrance_region_names, split_dungeon, world, player): +def generate_dungeon_find_proposal_old(builder, entrance_region_names, split_dungeon, world, player): logger = logging.getLogger('') name = builder.name entrance_regions = convert_regions(entrance_region_names, world, player) @@ -868,9 +870,9 @@ 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: + if not bk_Flag and (not location.forced_item or 'Big Key' in location.item.name): self.bk_found.add(location) if location.name in dungeon_events and location.name not in self.events: if self.flooded_key_check(location): @@ -1199,6 +1201,7 @@ class DungeonBuilder(object): self.name = name self.sectors = [] self.location_cnt = 0 + self.location_set = set() self.key_drop_cnt = 0 self.dungeon_items = None # during fill how many dungeon items are left self.free_items = None # during fill how many dungeon items are left @@ -1569,6 +1572,7 @@ def assign_sector_helper(sector, builder): builder.sectors.append(sector) builder.location_cnt += sector.chest_locations builder.key_drop_cnt += sector.key_only_locations + builder.location_set.update(sector.chest_location_set) if sector.c_switch: builder.c_switch_present = True if sector.blue_barrier: diff --git a/Main.py b/Main.py index 4e6b9c4b..5efc990b 100644 --- a/Main.py +++ b/Main.py @@ -107,6 +107,7 @@ 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.decoupledoors = args.decoupledoors.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 d6717617..18f0c70b 100644 --- a/Rom.py +++ b/Rom.py @@ -37,7 +37,7 @@ from source.dungeon.RoomList import Room0127 JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = 'afcd895b87559cd29b04aa3714cbc929' +RANDOMIZERBASEHASH = 'b4daf51397414464562a772f72f0b551' class JsonRom(object): @@ -843,7 +843,8 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): if world.doorShuffle[player] != 'vanilla': for name, pair in boss_indicator.items(): dungeon_id, boss_door = pair - opposite_door = world.get_door(boss_door, player).dest + boss_region = world.get_door(boss_door, player).entrance.parent_region + opposite_door = next(iter(x for x in boss_region.entrances if x.name != 'Skull Final Drop WS')).door if opposite_door and isinstance(opposite_door, Door) and opposite_door.roomIndex > -1: dungeon_name = opposite_door.entrance.parent_region.dungeon.name dungeon_id = boss_indicator[dungeon_name][0] diff --git a/RoomData.py b/RoomData.py index 03700e32..85c3b1fd 100644 --- a/RoomData.py +++ b/RoomData.py @@ -320,7 +320,7 @@ class Room(object): for i, door in enumerate(self.doorList): if i >= pos: return None - pos, kind = door + position, kind = door if kind not in [DoorKind.SmallKey, DoorKind.Dashable, DoorKind.Bombable, DoorKind.TrapTriggerable, DoorKind.Trap, DoorKind.Trap2, DoorKind.TrapTriggerableLow, DoorKind.TrapLowE3, DoorKind.BigKey, DoorKind.StairKey, DoorKind.StairKey2, DoorKind.StairKeyLow, @@ -395,8 +395,8 @@ class DoorKind(Enum): Bombable = 0x2E BlastWall = 0x30 Hidden = 0x32 - TrapTriggerable = 0x36 # right side trap or bottom side trap (West, North) - Trap2 = 0x38 # left side trap or top side trap (East, South) + TrapTriggerable = 0x36 # right side trap or bottom side trap (door directions: West, North) + Trap2 = 0x38 # left side trap or top side trap (door directions: East, South) NormalLow2 = 0x40 TrapTriggerableLow = 0x44 Warp = 0x46 diff --git a/data/base2current.bps b/data/base2current.bps index d657ad40ba6c5ce7ceabd188463dc93b16f6db49..f20e51505f4403e3ec1f477e0fcba47d5b000d0f 100644 GIT binary patch delta 9728 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 delta 9675 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 diff --git a/source/dungeon/DungeonStitcher.py b/source/dungeon/DungeonStitcher.py index b179cb1c..aa74dee0 100644 --- a/source/dungeon/DungeonStitcher.py +++ b/source/dungeon/DungeonStitcher.py @@ -370,8 +370,7 @@ def connect_doors_one_way(a, b): return # Connect supported types if a.type in [DoorType.Normal, DoorType.SpiralStairs, DoorType.Open, DoorType.StraightStairs, DoorType.Ladder]: - if not a.blocked: - connect_one_way(a.entrance, b.entrance) + connect_one_way(a.entrance, b.entrance) dep_doors, target = [], None if len(a.dependents) > 0: dep_doors, target = a.dependents, b