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 d657ad40..f20e5150 100644 Binary files a/data/base2current.bps and b/data/base2current.bps differ 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