diff --git a/Adjuster.py b/Adjuster.py index c6f42e6e..a6e964a8 100755 --- a/Adjuster.py +++ b/Adjuster.py @@ -36,6 +36,7 @@ def main(): parser.add_argument('--ow_palettes', default='default', choices=['default', 'random', 'blackout']) parser.add_argument('--uw_palettes', default='default', choices=['default', 'random', 'blackout']) parser.add_argument('--reduce_flashing', help='Reduce some in-game flashing.', action='store_true') + parser.add_argument('--shuffle_sfx', help='Shuffles sound sfx', action='store_true') parser.add_argument('--sprite', help='''\ Path to a sprite sheet to use for Link. Needs to be in binary format and have a length of 0x7000 (28672) bytes, diff --git a/AdjusterMain.py b/AdjusterMain.py index bc463444..7d7e2f6e 100644 --- a/AdjusterMain.py +++ b/AdjusterMain.py @@ -25,7 +25,7 @@ def adjust(args): args.sprite = None apply_rom_settings(rom, args.heartbeep, args.heartcolor, args.quickswap, args.fastmenu, args.disablemusic, - args.sprite, args.ow_palettes, args.uw_palettes, args.reduce_flashing) + args.sprite, args.ow_palettes, args.uw_palettes, args.reduce_flashing, args.shuffle_sfx) output_path.cached_path = args.outputpath rom.write_to_file(output_path('%s.sfc' % outfilebase)) diff --git a/BaseClasses.py b/BaseClasses.py index 365bbd18..98b4ad34 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -19,13 +19,14 @@ from RoomData import Room class World(object): - def __init__(self, players, owShuffle, owSwap, shuffle, doorShuffle, logic, mode, swords, difficulty, difficulty_adjustments, + def __init__(self, players, owShuffle, owCrossed, owMixed, shuffle, doorShuffle, logic, mode, swords, difficulty, difficulty_adjustments, timer, progressive, goal, algorithm, accessibility, shuffle_ganon, retro, custom, customitemarray, hints): self.players = players self.teams = 1 self.owShuffle = owShuffle.copy() - self.owSwap = owSwap.copy() + self.owCrossed = owCrossed.copy() self.owKeepSimilar = {} + self.owMixed = owMixed.copy() self.owFluteShuffle = {} self.shuffle = shuffle.copy() self.doorShuffle = doorShuffle.copy() @@ -86,6 +87,7 @@ class World(object): self._room_cache = {} self.dungeon_layouts = {} self.inaccessible_regions = {} + self.enabled_entrances = {} self.key_logic = {} self.pool_adjustment = {} self.key_layout = defaultdict(dict) @@ -103,6 +105,7 @@ class World(object): self.__dict__.setdefault(attr, {})[player] = val set_player_attr('_region_cache', {}) set_player_attr('player_names', []) + set_player_attr('owswaps', [[],[],[]]) set_player_attr('remote_items', False) set_player_attr('required_medallions', ['Ether', 'Quake']) set_player_attr('swamp_patch_required', False) @@ -110,7 +113,7 @@ class World(object): set_player_attr('ganon_at_pyramid', True) set_player_attr('ganonstower_vanilla', True) set_player_attr('sewer_light_cone', self.mode[player] == 'standard') - set_player_attr('fix_trock_doors', self.shuffle[player] != 'vanilla' or self.mode[player] == 'inverted') + set_player_attr('fix_trock_doors', self.shuffle[player] != 'vanilla' or ((self.mode[player] == 'inverted') != (0x05 in self.owswaps[player][0] and self.owMixed[player]))) set_player_attr('fix_skullwoods_exit', self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple'] or self.doorShuffle[player] not in ['vanilla']) set_player_attr('fix_palaceofdarkness_exit', self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple']) set_player_attr('fix_trock_exit', self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple']) @@ -123,7 +126,7 @@ class World(object): set_player_attr('compassshuffle', False) set_player_attr('keyshuffle', False) set_player_attr('bigkeyshuffle', False) - set_player_attr('bomblogic', False) + set_player_attr('bombbag', False) set_player_attr('difficulty_requirements', None) set_player_attr('boss_shuffle', 'none') set_player_attr('enemy_shuffle', 'none') @@ -150,9 +153,11 @@ class World(object): set_player_attr('mixed_travel', 'prevent') set_player_attr('standardize_palettes', 'standardize') set_player_attr('force_fix', {'gt': False, 'sw': False, 'pod': False, 'tr': False}) - set_player_attr('owswaps', [[],[],[]]) set_player_attr('prizes', {'pull': [0, 0, 0], 'crab': [0, 0], 'stun': 0, 'fish': 0}) + set_player_attr('exp_cache', defaultdict(dict)) + set_player_attr('enabled_entrances', {}) + def get_name_string_for_object(self, obj): return obj.name if self.players == 1 else f'{obj.name} ({self.get_player_names(obj.player)})' @@ -455,6 +460,10 @@ class World(object): def clear_location_cache(self): self._cached_locations = None + def clear_exp_cache(self): + for p in range(1, self.players + 1): + self.exp_cache[p].clear() + def get_unfilled_locations(self, player=None): return [location for location in self.get_locations() if (player is None or location.player == player) and location.item is None] @@ -487,8 +496,10 @@ class World(object): else: return all((self.has_beaten_game(state, p) for p in range(1, self.players + 1))) - def can_beat_game(self, starting_state=None): + def can_beat_game(self, starting_state=None, log_error=False): if starting_state: + if self.has_beaten_game(starting_state): + return True state = starting_state.copy() else: state = CollectionState(self) @@ -507,6 +518,9 @@ class World(object): if not sphere: # ran out of places and did not finish yet, quit + if log_error: + missing_locations = ", ".join([x.name for x in prog_locations]) + logging.getLogger('').error(f'Cannot reach the following locations: {missing_locations}') return False for location in sphere: @@ -521,17 +535,25 @@ class World(object): class CollectionState(object): - def __init__(self, parent): - self.prog_items = Counter() + def __init__(self, parent, skip_init=False): self.world = parent - self.reachable_regions = {player: dict() for player in range(1, parent.players + 1)} - self.blocked_connections = {player: dict() for player in range(1, parent.players + 1)} - self.events = [] - self.path = {} - self.locations_checked = set() - self.stale = {player: True for player in range(1, parent.players + 1)} - for item in parent.precollected_items: - self.collect(item, True) + if not skip_init: + self.prog_items = Counter() + self.reachable_regions = {player: dict() for player in range(1, parent.players + 1)} + self.blocked_connections = {player: dict() for player in range(1, parent.players + 1)} + self.events = [] + self.path = {} + self.locations_checked = set() + self.stale = {player: True for player in range(1, parent.players + 1)} + for item in parent.precollected_items: + self.collect(item, True) + # reached vs. opened in the counter + self.door_counter = {player: (Counter(), Counter()) for player in range(1, parent.players + 1)} + self.reached_doors = {player: set() for player in range(1, parent.players + 1)} + self.opened_doors = {player: set() for player in range(1, parent.players + 1)} + self.dungeons_to_check = {player: defaultdict(dict) for player in range(1, parent.players + 1)} + self.dungeon_limits = None + # self.trace = None def update_reachable_regions(self, player): self.stale[player] = False @@ -542,68 +564,386 @@ class CollectionState(object): start = self.world.get_region('Menu', player) if not start in rrp: rrp[start] = CrystalBarrier.Orange - for exit in start.exits: - bc[exit] = CrystalBarrier.Orange + for conn in start.exits: + bc[conn] = CrystalBarrier.Orange queue = deque(self.blocked_connections[player].items()) + self.traverse_world(queue, rrp, bc, player) + unresolved_events = [x for y in self.reachable_regions[player] for x in y.locations + if x.event and x.item and (x.item.smallkey or x.item.bigkey or x.item.advancement) + and x not in self.locations_checked and x.can_reach(self)] + unresolved_events = self._do_not_flood_the_keys(unresolved_events) + if len(unresolved_events) == 0: + self.check_key_doors_in_dungeons(rrp, player) + + def traverse_world(self, queue, rrp, bc, player): # run BFS on all connections, and keep track of those blocked by missing items - while True: - try: - connection, crystal_state = queue.popleft() - new_region = connection.connected_region - if new_region is None or new_region in rrp and (new_region.type != RegionType.Dungeon or (rrp[new_region] & crystal_state) == crystal_state): + while len(queue) > 0: + connection, crystal_state = queue.popleft() + new_region = connection.connected_region + if not self.should_visit(new_region, rrp, crystal_state, player): + if not new_region or not self.dungeon_limits or self.possibly_connected_to_dungeon(new_region, player): bc.pop(connection, None) - elif connection.can_reach(self): - if new_region.type == RegionType.Dungeon: - new_crystal_state = crystal_state - for exit in new_region.exits: - door = exit.door - if door is not None and door.crystal == CrystalBarrier.Either and door.entrance.can_reach(self): - new_crystal_state = CrystalBarrier.Either - break - if new_region in rrp: - new_crystal_state |= rrp[new_region] + elif connection.can_reach(self): + bc.pop(connection, None) + if new_region.type == RegionType.Dungeon: + new_crystal_state = crystal_state + if new_region in rrp: + new_crystal_state |= rrp[new_region] - rrp[new_region] = new_crystal_state - - for exit in new_region.exits: - door = exit.door - if door is not None and not door.blocked: + rrp[new_region] = new_crystal_state + for conn in new_region.exits: + door = conn.door + if door is not None and not door.blocked: + if self.valid_crystal(door, new_crystal_state): door_crystal_state = door.crystal if door.crystal else new_crystal_state - bc[exit] = door_crystal_state - queue.append((exit, door_crystal_state)) - elif door is None: - queue.append((exit, new_crystal_state)) - else: - new_crystal_state = CrystalBarrier.Orange - rrp[new_region] = new_crystal_state - bc.pop(connection, None) - for exit in new_region.exits: - bc[exit] = new_crystal_state - queue.append((exit, new_crystal_state)) + bc[conn] = door_crystal_state + queue.append((conn, door_crystal_state)) + elif door is None: + # note: no door in dungeon indicates what exactly? (always traversable)? + queue.append((conn, new_crystal_state)) + else: + new_crystal_state = CrystalBarrier.Orange + rrp[new_region] = new_crystal_state + for conn in new_region.exits: + bc[conn] = new_crystal_state + queue.append((conn, new_crystal_state)) - self.path[new_region] = (new_region.name, self.path.get(connection, None)) + self.path[new_region] = (new_region.name, self.path.get(connection, None)) - # Retry connections if the new region can unblock them - if new_region.name in indirect_connections: - new_entrance = self.world.get_entrance(indirect_connections[new_region.name], player) - if new_entrance in bc and new_entrance not in queue and new_entrance.parent_region in rrp: - queue.append((new_entrance, rrp[new_entrance.parent_region])) - except IndexError: - break + # Retry connections if the new region can unblock them + if new_region.name in indirect_connections: + new_entrance = self.world.get_entrance(indirect_connections[new_region.name], player) + if new_entrance in bc and new_entrance.parent_region in rrp: + new_crystal_state = rrp[new_entrance.parent_region] + if (new_entrance, new_crystal_state) not in queue: + queue.append((new_entrance, new_crystal_state)) + # else those connections that are not accessible yet + if self.is_small_door(connection): + door = connection.door + dungeon_name = connection.parent_region.dungeon.name + key_logic = self.world.key_logic[player][dungeon_name] + if door.name not in self.reached_doors[player]: + self.door_counter[player][0][dungeon_name] += 1 + self.reached_doors[player].add(door.name) + if key_logic.sm_doors[door]: + self.reached_doors[player].add(key_logic.sm_doors[door].name) + if not connection.can_reach(self): + checklist_key = 'Universal' if self.world.retro[player] else dungeon_name + checklist = self.dungeons_to_check[player][checklist_key] + checklist[connection.name] = (connection, crystal_state) + elif door.name not in self.opened_doors[player]: + opened_doors = self.opened_doors[player] + door = connection.door + if door.name not in opened_doors: + self.door_counter[player][1][dungeon_name] += 1 + opened_doors.add(door.name) + key_logic = self.world.key_logic[player][dungeon_name] + if key_logic.sm_doors[door]: + opened_doors.add(key_logic.sm_doors[door].name) + def should_visit(self, new_region, rrp, crystal_state, player): + if not new_region: + return False + if self.dungeon_limits and not self.possibly_connected_to_dungeon(new_region, player): + return False + if new_region not in rrp: + return True + if new_region.type != RegionType.Dungeon: + return False + return (rrp[new_region] & crystal_state) != crystal_state + + def possibly_connected_to_dungeon(self, new_region, player): + if new_region.dungeon: + return new_region.dungeon.name in self.dungeon_limits + else: + return new_region.name in self.world.inaccessible_regions[player] + + @staticmethod + def valid_crystal(door, new_crystal_state): + return (not door.crystal or door.crystal == CrystalBarrier.Either or new_crystal_state == CrystalBarrier.Either + or new_crystal_state == door.crystal) + + def check_key_doors_in_dungeons(self, rrp, player): + for dungeon_name, checklist in self.dungeons_to_check[player].items(): + if self.apply_dungeon_exploration(rrp, player, dungeon_name, checklist): + continue + init_door_candidates = self.should_explore_child_state(self, dungeon_name, player) + key_total = self.prog_items[(dungeon_keys[dungeon_name], player)] # todo: universal + remaining_keys = key_total - self.door_counter[player][1][dungeon_name] + if not init_door_candidates or remaining_keys == 0: + continue + dungeon_doors = {x.name for x in self.world.key_logic[player][dungeon_name].sm_doors.keys()} + + def valid_d_door(x): + return x in dungeon_doors + + child_states = deque() + child_states.append(self) + visited_opened_doors = set() + visited_opened_doors.add(frozenset(self.opened_doors[player])) + terminal_states, common_regions, common_bc, common_doors = [], {}, {}, set() + while len(child_states) > 0: + next_child = child_states.popleft() + door_candidates = CollectionState.should_explore_child_state(next_child, dungeon_name, player) + child_checklist = next_child.dungeons_to_check[player][dungeon_name] + if door_candidates: + for chosen_door in door_candidates: + child_state = next_child.copy() + child_queue = deque() + child_state.door_counter[player][1][dungeon_name] += 1 + if isinstance(chosen_door, tuple): + child_state.opened_doors[player].add(chosen_door[0]) + child_state.opened_doors[player].add(chosen_door[1]) + if chosen_door[0] in child_checklist: + child_queue.append(child_checklist[chosen_door[0]]) + if chosen_door[1] in child_checklist: + child_queue.append(child_checklist[chosen_door[1]]) + else: + child_state.opened_doors[player].add(chosen_door) + if chosen_door in child_checklist: + child_queue.append(child_checklist[chosen_door]) + if child_state.opened_doors[player] not in visited_opened_doors: + done = False + while not done: + rrp_ = child_state.reachable_regions[player] + bc_ = child_state.blocked_connections[player] + child_state.set_dungeon_limits(player, dungeon_name) + child_queue.extend([(x, y) for x, y in bc_.items() + if child_state.possibly_connected_to_dungeon(x.parent_region, + player)]) + child_state.traverse_world(child_queue, rrp_, bc_, player) + new_events = child_state.sweep_for_events_once(player) + child_state.stale[player] = False + if new_events: + for conn in bc_: + if conn.parent_region.dungeon and conn.parent_region.dungeon.name == dungeon_name: + child_queue.append((conn, bc_[conn])) + done = not new_events + if child_state.opened_doors[player] not in visited_opened_doors: + visited_opened_doors.add(frozenset(child_state.opened_doors[player])) + child_states.append(child_state) + else: + terminal_states.append(next_child) + common_regions, common_bc, common_doors, first = {}, {}, set(), True + bc = self.blocked_connections[player] + for term_state in terminal_states: + t_rrp = term_state.reachable_regions[player] + t_bc = term_state.blocked_connections[player] + if first: + first = False + common_regions = {x: y for x, y in t_rrp.items() if x not in rrp or y != rrp[x]} + common_bc = {x: y for x, y in t_bc.items() if x not in bc} + common_doors = {x for x in term_state.opened_doors[player] - self.opened_doors[player] + if valid_d_door(x)} + else: + cm_rrp = {x: y for x, y in t_rrp.items() if x not in rrp or y != rrp[x]} + common_regions = {k: self.comb_crys(v, cm_rrp[k]) for k, v in common_regions.items() + if k in cm_rrp and self.crys_agree(v, cm_rrp[k])} + common_bc.update({x: y for x, y in t_bc.items() if x not in bc and x not in common_bc}) + common_doors &= {x for x in term_state.opened_doors[player] - self.opened_doors[player] + if valid_d_door(x)} + + terminal_queue = deque() + for door in common_doors: + pair = self.find_door_pair(player, dungeon_name, door) + if door not in self.reached_doors[player]: + self.door_counter[player][0][dungeon_name] += 1 + self.reached_doors[player].add(door) + if pair not in self.reached_doors[player]: + self.reached_doors[player].add(pair) + self.opened_doors[player].add(door) + if door in checklist: + terminal_queue.append(checklist[door]) + if pair not in self.opened_doors[player]: + self.door_counter[player][1][dungeon_name] += 1 + + self.set_dungeon_limits(player, dungeon_name) + rrp_ = self.reachable_regions[player] + bc_ = self.blocked_connections[player] + for block, crystal in bc_.items(): + if (block, crystal) not in terminal_queue and self.possibly_connected_to_dungeon(block.connected_region, player): + terminal_queue.append((block, crystal)) + self.traverse_world(terminal_queue, rrp_, bc_, player) + self.dungeon_limits = None + + rrp = self.reachable_regions[player] + missing_regions = {x: y for x, y in common_regions.items() if x not in rrp} + paths = {} + for k in missing_regions: + rrp[k] = missing_regions[k] + possible_path = terminal_states[0].path[k] + self.path[k] = paths[k] = possible_path + missing_bc = {} + for blocked, crystal in common_bc.items(): + if (blocked not in bc and blocked.parent_region in rrp + and self.should_visit(blocked.connected_region, rrp, crystal, player)): + missing_bc[blocked] = crystal + for k in missing_bc: + bc[k] = missing_bc[k] + self.record_dungeon_exploration(player, dungeon_name, checklist, + common_doors, missing_regions, missing_bc, paths) + checklist.clear() + + @staticmethod + def comb_crys(a, b): + return a if a == b or a != CrystalBarrier.Either else b + + @staticmethod + def crys_agree(a, b): + return a == b or a == CrystalBarrier.Either or b == CrystalBarrier.Either + + def find_door_pair(self, player, dungeon_name, name): + for door in self.world.key_logic[player][dungeon_name].sm_doors.keys(): + if door.name == name: + paired_door = self.world.key_logic[player][dungeon_name].sm_doors[door] + return paired_door.name if paired_door else None + return None + + def set_dungeon_limits(self, player, dungeon_name): + if self.world.retro[player] and self.world.mode[player] == 'standard': + self.dungeon_limits = ['Hyrule Castle', 'Agahnims Tower'] + else: + self.dungeon_limits = [dungeon_name] + + @staticmethod + def should_explore_child_state(state, dungeon_name, player): + small_key_name = dungeon_keys[dungeon_name] + key_total = state.prog_items[(small_key_name, player)] + remaining_keys = key_total - state.door_counter[player][1][dungeon_name] + unopened_doors = state.door_counter[player][0][dungeon_name] - state.door_counter[player][1][dungeon_name] + if remaining_keys > 0 and unopened_doors > 0: + key_logic = state.world.key_logic[player][dungeon_name] + door_candidates, skip = [], set() + for door, paired in key_logic.sm_doors.items(): + if door.name in state.reached_doors[player] and door.name not in state.opened_doors[player]: + if door.name not in skip: + if paired: + door_candidates.append((door.name, paired.name)) + skip.add(paired.name) + else: + door_candidates.append(door.name) + return door_candidates + return None + + @staticmethod + def print_rrp(rrp): + logger = logging.getLogger('') + logger.debug('RRP Checking') + for region, packet in rrp.items(): + new_crystal_state, logic, path = packet + logger.debug(f'\nRegion: {region.name} (CS: {str(new_crystal_state)})') + for i in range(0, len(logic)): + logger.debug(f'{logic[i]}') + logger.debug(f'{",".join(str(x) for x in path[i])}') def copy(self): - ret = CollectionState(self.world) + ret = CollectionState(self.world, skip_init=True) ret.prog_items = self.prog_items.copy() ret.reachable_regions = {player: copy.copy(self.reachable_regions[player]) for player in range(1, self.world.players + 1)} ret.blocked_connections = {player: copy.copy(self.blocked_connections[player]) for player in range(1, self.world.players + 1)} ret.events = copy.copy(self.events) ret.path = copy.copy(self.path) ret.locations_checked = copy.copy(self.locations_checked) + ret.stale = {player: self.stale[player] for player in range(1, self.world.players + 1)} + ret.door_counter = {player: (copy.copy(self.door_counter[player][0]), copy.copy(self.door_counter[player][1])) + for player in range(1, self.world.players + 1)} + ret.reached_doors = {player: copy.copy(self.reached_doors[player]) for player in range(1, self.world.players + 1)} + ret.opened_doors = {player: copy.copy(self.opened_doors[player]) for player in range(1, self.world.players + 1)} + ret.dungeons_to_check = { + player: defaultdict(dict, {name: copy.copy(checklist) + for name, checklist in self.dungeons_to_check[player].items()}) + for player in range(1, self.world.players + 1)} return ret + def apply_dungeon_exploration(self, rrp, player, dungeon_name, checklist): + bc = self.blocked_connections[player] + ec = self.world.exp_cache[player] + prog_set = self.reduce_prog_items(player, dungeon_name) + exp_key = (prog_set, frozenset(checklist)) + if dungeon_name in ec and exp_key in ec[dungeon_name]: + # apply + common_doors, missing_regions, missing_bc, paths = ec[dungeon_name][exp_key] + terminal_queue = deque() + for door in common_doors: + pair = self.find_door_pair(player, dungeon_name, door) + if door not in self.reached_doors[player]: + self.door_counter[player][0][dungeon_name] += 1 + self.reached_doors[player].add(door) + if pair not in self.reached_doors[player]: + self.reached_doors[player].add(pair) + self.opened_doors[player].add(door) + if door in checklist: + terminal_queue.append(checklist[door]) + if pair not in self.opened_doors[player]: + self.door_counter[player][1][dungeon_name] += 1 + + self.set_dungeon_limits(player, dungeon_name) + rrp_ = self.reachable_regions[player] + bc_ = self.blocked_connections[player] + for block, crystal in bc_.items(): + if (block, crystal) not in terminal_queue and self.possibly_connected_to_dungeon(block.connected_region, player): + terminal_queue.append((block, crystal)) + self.traverse_world(terminal_queue, rrp_, bc_, player) + self.dungeon_limits = None + + for k in missing_regions: + rrp[k] = missing_regions[k] + for r, path in paths.items(): + self.path[r] = path + for k in missing_bc: + bc[k] = missing_bc[k] + + return True + return False + + def record_dungeon_exploration(self, player, dungeon_name, checklist, + common_doors, missing_regions, missing_bc, paths): + ec = self.world.exp_cache[player] + prog_set = self.reduce_prog_items(player, dungeon_name) + exp_key = (prog_set, frozenset(checklist)) + ec[dungeon_name][exp_key] = (common_doors, missing_regions, missing_bc, paths) + + def reduce_prog_items(self, player, dungeon_name): + # todo: possibly could include an analysis of dungeon items req. like Hammer, Hookshot, etc + # cross dungeon requirements may be necessary for keysanity - which invalidates the above + # todo: universal smalls where needed + life_count, bottle_count = 0, 0 + reduced = Counter() + for item, cnt in self.prog_items.items(): + item_name, item_player = item + if item_player == player and self.check_if_progressive(item_name): + if item_name.startswith('Bottle'): # I think magic requirements can require multiple bottles + bottle_count += cnt + elif item_name in ['Boss Heart Container', 'Sanctuary Heart Container', 'Piece of Heart']: + if 'Container' in item_name: + life_count += 1 + elif 'Piece of Heart' == item_name: + life_count += .25 + else: + reduced[item] = cnt + if bottle_count > 0: + reduced[('Bottle', player)] = 1 + if life_count >= 1: + reduced[('Heart Container', player)] = 1 + return frozenset(reduced.items()) + + @staticmethod + def check_if_progressive(item_name): + return (item_name in + ['Bow', 'Progressive Bow', 'Progressive Bow (Alt)', 'Book of Mudora', 'Hammer', 'Hookshot', + 'Magic Mirror', 'Ocarina', 'Pegasus Boots', 'Power Glove', 'Cape', 'Mushroom', 'Shovel', + 'Lamp', 'Magic Powder', 'Moon Pearl', 'Cane of Somaria', 'Fire Rod', 'Flippers', 'Ice Rod', + 'Titans Mitts', 'Bombos', 'Ether', 'Quake', 'Master Sword', 'Tempered Sword', 'Fighter Sword', + 'Golden Sword', 'Progressive Sword', 'Progressive Glove', 'Silver Arrows', 'Green Pendant', + 'Blue Pendant', 'Red Pendant', 'Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 5', + 'Crystal 6', 'Crystal 7', 'Blue Boomerang', 'Red Boomerang', 'Blue Shield', 'Red Shield', + 'Mirror Shield', 'Progressive Shield', 'Bug Catching Net', 'Cane of Byrna', + 'Boss Heart Container', 'Sanctuary Heart Container', 'Piece of Heart', 'Magic Upgrade (1/2)', + 'Magic Upgrade (1/4)'] + or item_name.startswith(('Bottle', 'Small Key', 'Big Key'))) + def can_reach(self, spot, resolution_hint=None, player=None): try: spot_type = spot.spot_type @@ -619,6 +959,16 @@ class CollectionState(object): return spot.can_reach(self) + def sweep_for_events_once(self, player): + locations = self.world.get_filled_locations(player) + checked_locations = set([l for l in locations if l in self.locations_checked]) + reachable_events = [location for location in locations if location.event and location.can_reach(self)] + reachable_events = self._do_not_flood_the_keys(reachable_events) + for event in reachable_events: + if event not in checked_locations: + self.events.append((event.name, event.player)) + self.collect(event.item, True, event) + return len(reachable_events) > len(checked_locations) def sweep_for_events(self, key_only=False, locations=None): # this may need improvement @@ -666,6 +1016,13 @@ class CollectionState(object): or not self.location_can_be_flooded(flood_location)) return True + @staticmethod + def is_small_door(connection): + return connection and connection.door and connection.door.smallKey + + def is_door_open(self, door_name, player): + return door_name in self.opened_doors[player] + @staticmethod def location_can_be_flooded(location): return location.parent_region.name in ['Swamp Trench 1 Alcove', 'Swamp Trench 2 Alcove'] @@ -894,7 +1251,7 @@ class CollectionState(object): def can_use_bombs(self, player): if self.world.swords[player] == 'bombs': return self.has_bomb_level(player, 1) - return (not self.world.bomblogic[player] or self.has('Bomb Upgrade (+10)', player)) and self.can_farm_bombs(player) + return (not self.world.bombbag[player] or self.has('Bomb Upgrade (+10)', player)) and self.can_farm_bombs(player) def can_hit_crystal(self, player): return (self.can_use_bombs(player) @@ -1278,6 +1635,7 @@ class Region(object): self.is_light_world = False # will be set aftermaking connections. self.is_dark_world = False self.spot_type = 'Region' + self.terrain = None self.hint_text = hint self.recursion_count = 0 self.player = player @@ -1883,6 +2241,7 @@ class Sector(object): self.entrance_sector = None self.destination_entrance = False self.equations = None + self.item_logic = set() def region_set(self): if self.r_name_set is None: @@ -2135,9 +2494,7 @@ class Location(object): return self.always_allow(state, item) or (self.parent_region.can_fill(item) and self.item_rule(item) and (not check_access or self.can_reach(state))) def can_reach(self, state): - if self.parent_region.can_reach(state) and self.access_rule(state): - return True - return False + return self.parent_region.can_reach(state) and self.access_rule(state) def forced_big_key(self): if self.forced_item and self.forced_item.bigkey and self.player == self.forced_item.player: @@ -2164,6 +2521,12 @@ class Location(object): world = self.parent_region.world if self.parent_region and self.parent_region.world else None return world.get_name_string_for_object(self) if world else f'{self.name} (Player {self.player})' + def __eq__(self, other): + return self.name == other.name and self.player == other.player + + def __hash__(self): + return hash((self.name, self.player)) + class Item(object): @@ -2207,6 +2570,15 @@ class Item(object): def compass(self): return self.type == 'Compass' + @property + def dungeon(self): + if not self.smallkey and not self.bigkey and not self.map and not self.compass: + return None + item_dungeon = self.name.split('(')[1][:-1] + if item_dungeon == 'Escape': + item_dungeon = 'Hyrule Castle' + return item_dungeon + def __str__(self): return str(self.__unicode__()) @@ -2422,12 +2794,13 @@ class Spoiler(object): 'logic': self.world.logic, 'mode': self.world.mode, 'retro': self.world.retro, - 'bomblogic': self.world.bomblogic, + 'bombbag': self.world.bombbag, 'weapons': self.world.swords, 'goal': self.world.goal, 'ow_shuffle': self.world.owShuffle, - 'ow_swap': self.world.owSwap, + 'ow_crossed': self.world.owCrossed, 'ow_keepsimilar': self.world.owKeepSimilar, + 'ow_mixed': self.world.owMixed, 'ow_fluteshuffle': self.world.owFluteShuffle, 'shuffle': self.world.shuffle, 'door_shuffle': self.world.doorShuffle, @@ -2510,9 +2883,10 @@ class Spoiler(object): outfile.write('Difficulty:'.ljust(line_width) + '%s\n' % self.metadata['item_pool'][player]) outfile.write('Item Functionality:'.ljust(line_width) + '%s\n' % self.metadata['item_functionality'][player]) outfile.write('Overworld Layout Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['ow_shuffle'][player]) - outfile.write('Overworld Tile Swap:'.ljust(line_width) + '%s\n' % self.metadata['ow_swap'][player]) if self.metadata['ow_shuffle'][player] != 'vanilla': outfile.write('Keep Similar OW Edges Together:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['ow_keepsimilar'][player] else 'No')) + outfile.write('Crossed OW:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['ow_crossed'][player] else 'No')) + outfile.write('Mixed OW:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['ow_mixed'][player] else 'No')) outfile.write('Flute Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['ow_fluteshuffle'][player]) outfile.write('Entrance Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['shuffle'][player]) outfile.write('Door Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['door_shuffle'][player]) @@ -2538,7 +2912,7 @@ class Spoiler(object): outfile.write('Experimental:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['experimental'][player] else 'No')) outfile.write('Key Drops shuffled:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['keydropshuffle'][player] else 'No')) outfile.write('Shopsanity:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['shopsanity'][player] else 'No')) - outfile.write('Bomblogic:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['bomblogic'][player] else 'No')) + outfile.write('Bombbag:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['bombbag'][player] else 'No')) if self.doors: outfile.write('\n\nDoors:\n\n') outfile.write('\n'.join( @@ -2586,7 +2960,7 @@ class Spoiler(object): for player in range(1, self.world.players + 1): if self.world.boss_shuffle[player] != 'none': - bossmap = self.bosses[player] if self.world.players > 1 else self.bosses + bossmap = self.bosses[str(player)] if self.world.players > 1 else self.bosses outfile.write(f'\n\nBosses ({self.world.get_player_names(player)}):\n\n') outfile.write('\n'.join([f'{x}: {y}' for x, y in bossmap.items() if y not in ['Agahnim', 'Agahnim 2', 'Ganon']])) @@ -2629,6 +3003,22 @@ dungeon_names = [ 'Swamp Palace', 'Skull Woods', 'Thieves Town', 'Ice Palace', 'Misery Mire', 'Turtle Rock', 'Ganons Tower' ] +dungeon_keys = { + 'Hyrule Castle': 'Small Key (Escape)', + 'Eastern Palace': 'Small Key (Eastern Palace)', + 'Desert Palace': 'Small Key (Desert Palace)', + 'Tower of Hera': 'Small Key (Tower of Hera)', + 'Agahnims Tower': 'Small Key (Agahnims Tower)', + 'Palace of Darkness': 'Small Key (Palace of Darkness)', + 'Swamp Palace': 'Small Key (Swamp Palace)', + 'Skull Woods': 'Small Key (Skull Woods)', + 'Thieves Town': 'Small Key (Thieves Town)', + 'Ice Palace': 'Small Key (Ice Palace)', + 'Misery Mire': 'Small Key (Misery Mire)', + 'Turtle Rock': 'Small Key (Turtle Rock)', + 'Ganons Tower': 'Small Key (Ganons Tower)', + 'Universal': 'Small Key (Universal)' +} class PotItem(FastEnum): Nothing = 0x0 @@ -2675,11 +3065,10 @@ class Pot(object): self.flags = flags -# byte 0: DDOO EEEE (DR, OR, ER) +# byte 0: DDOO OEEE (DR, OR, ER) dr_mode = {"basic": 1, "crossed": 2, "vanilla": 0} -or_mode = {"parallel": 1, "full": 2, "vanilla": 0} -er_mode = {"vanilla": 0, "simple": 1, "restricted": 2, "full": 3, "crossed": 4, "insanity": 5, "restricted_legacy": 8, - "full_legacy": 9, "madness_legacy": 10, "insanity_legacy": 11, "dungeonsfull": 7, "dungeonssimple": 6} +or_mode = {"vanilla": 0, "parallel": 1, "full": 1} +er_mode = {"vanilla": 0, "simple": 1, "restricted": 3, "full": 3, "crossed": 4, "insanity": 5, "dungeonsfull": 7, "dungeonssimple": 7} # byte 1: LLLW WSSS (logic, mode, sword) logic_mode = {"noglitches": 0, "minorglitches": 1, "nologic": 2, "owglitches": 3, "majorglitches": 4} @@ -2714,7 +3103,7 @@ class Settings(object): @staticmethod def make_code(w, p): code = bytes([ - (dr_mode[w.doorShuffle[p]] << 6) | (or_mode[w.owShuffle[p]] << 4) | er_mode[w.shuffle[p]], + (dr_mode[w.doorShuffle[p]] << 6) | (or_mode[w.owShuffle[p]] << 5) | (0x10 if w.owCrossed[p] != 'none' else 0) | (0x08 if w.owMixed[p] else 0) | er_mode[w.shuffle[p]], (logic_mode[w.logic[p]] << 5) | (world_mode[w.mode[p]] << 3) | (sword_mode[w.swords[p]]), @@ -2780,3 +3169,9 @@ class Settings(object): args.enemy_health[p] = r(e_health)[(settings[7] & 0xE0) >> 5] args.enemy_damage[p] = r(e_dmg)[(settings[7] & 0x18) >> 3] args.shufflepots[p] = True if settings[7] & 0x4 else False + + +class KeyRuleType(FastEnum): + WorstCase = 0 + AllowSmall = 1 + Lock = 2 diff --git a/Bosses.py b/Bosses.py index ae4283d6..96128591 100644 --- a/Bosses.py +++ b/Bosses.py @@ -192,16 +192,16 @@ def place_bosses(world, player): if world.boss_shuffle[player] == "simple": # vanilla bosses shuffled bosses = placeable_bosses + ['Armos Knights', 'Lanmolas', 'Moldorm'] - else: # all bosses present, the three duplicates chosen at random - bosses = all_bosses + [random.choice(placeable_bosses) for _ in range(3)] + else: # all bosses present, the three duplicates chosen at random + bosses = all_bosses + random.sample(placeable_bosses, 3) logging.getLogger('').debug('Bosses chosen %s', bosses) - random.shuffle(bosses) for [loc, level] in boss_locations: loc_text = loc + (' ('+level+')' if level else '') - boss = next((b for b in bosses if can_place_boss(world, player, b, loc, level)), None) - if not boss: + try: + boss = random.choice([b for b in bosses if can_place_boss(world, player, b, loc, level)]) + except IndexError: raise FillError('Could not place boss for location %s' % loc_text) bosses.remove(boss) diff --git a/CHANGELOG.md b/CHANGELOG.md index 94736e42..1d6898aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,50 @@ # Changelog +### 0.1.9.1 +- Fixed logic issue with leaving IP entrance not requiring flippers +- ~~Merged DR v0.5.1.1 - Map Indicator Fix/Boss Shuffle Bias/Shop Hints~~ + +### 0.1.9.0 +- Expanded Crossed OW to four separate options, see Readme for details +- Crossed OW will now play a short SFX when changing worlds +- Improved Link/Bunny state in Crossed OW +- Fixed issue with TR Pegs when fluting directly from an area with hammerpegs +- Updated OW GUI layout + +### 0.1.8.2 +- Fixed issue with game crashing on using Flute +- Fixed issues with Link/Bunny state in Crossed OW +- Fixed issue with Standard+Parallel not using vanilla connections for Escape +- Fixed issue with Mystery for OW boolean options +- ~~Merged DR v0.5.1.0 - Major Keylogic Update~~ + +### 0.1.8.1 +- Fixed issue with activating flute in DW (OW Mixed) +- Fixed issue with Parallel+Crossed not generating +- Fixed issue with Standard not generating +- Fixed issue with Swordless not generating +- Fixed logic for Graveyard Ledge and Kings Tomb + +### 0.1.8.0 +- Moved Crossed to its own checkbox option +- Removed Legacy ER shuffles +- Added OW Shuffle support for Plando module (needs user testing) +- Fixed issue with Sanc start at TR as bunny when it is LW +- Fixed issue with Pyramid Hole not getting shuffled +- ~~Merged DR v0.5.0.3 - Minor DR fixes~~ + +### 0.1.7.4 +- Fixed issue with Mixed OW failing to generate when HC/Pyramid is swapped +- Various fixes to improve generation rates for Mixed OW Shuffle +- ~~Merged DR v0.5.0.2 - Shuffle SFX~~ + +### 0.1.7.3 +- Fixed minor issue with ambient SFX stopping and starting on OW screen load +- MSU-1 changed to play LW2 (track 60) when Aga1 is killed instead of ped pull +- Added dynamic flute exits for all LW OW regions +- Improved spoiler log playthru pathing accuracy by including flute routing +- Fixed issue with generating a filename for vanilla OW settings + ### 0.1.7.2 - Fixed music algorithm to play correct track in OW Shuffle - Removed convenient portal on WDM in OW Layout Shuffle @@ -11,7 +56,7 @@ ### 0.1.7.0 - Expanded new DR bomb logic to all modes (bomb usage in logic only if there is an unlimited supply of bombs available) -- ~~Merged DR v0.5.0.1 - Bomblogic mode / Enemizer fixes~~ +- ~~Merged DR v0.5.0.1 - Bombbag mode / Enemizer fixes~~ ### 0.1.6.9 - ~~Merged DR v0.4.0.12 - Secure random update / Credits fix~~ diff --git a/CLI.py b/CLI.py index 826a80ae..c0b13d53 100644 --- a/CLI.py +++ b/CLI.py @@ -93,18 +93,18 @@ def parse_cli(argv, no_defaults=False): for player in range(1, multiargs.multi + 1): playerargs = parse_cli(shlex.split(getattr(ret, f"p{player}")), True) - for name in ['logic', 'mode', 'swords', 'goal', 'difficulty', 'item_functionality', 'ow_shuffle', - 'ow_swap', 'ow_keepsimilar', 'ow_fluteshuffle', 'shuffle', 'door_shuffle', - 'intensity', 'crystals_ganon', 'crystals_gt', 'ganon_item', 'openpyramid', + for name in ['logic', 'mode', 'swords', 'goal', 'difficulty', 'item_functionality', + 'ow_shuffle', 'ow_crossed', 'ow_keepsimilar', 'ow_mixed', 'ow_fluteshuffle', + 'shuffle', 'door_shuffle', 'intensity', 'crystals_ganon', 'crystals_gt', 'ganon_item', 'openpyramid', 'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory', - 'bomblogic', + 'bombbag', 'triforce_pool_min', 'triforce_pool_max', 'triforce_goal_min', 'triforce_goal_max', 'triforce_min_difference', 'triforce_goal', 'triforce_pool', 'shufflelinks', 'pseudoboots', 'retro', 'accessibility', 'hints', 'beemizer', 'experimental', 'dungeon_counters', 'shufflebosses', 'shuffleenemies', 'enemy_health', 'enemy_damage', 'shufflepots', 'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor', 'heartbeep', 'remote_items', 'shopsanity', 'keydropshuffle', 'mixed_travel', 'standardize_palettes', 'code', - 'reduce_flashing']: + 'reduce_flashing', 'shuffle_sfx']: value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name) if player == 1: setattr(ret, name, {1: value}) @@ -128,7 +128,7 @@ def parse_settings(): settings = { "lang": "en", "retro": False, - "bomblogic": False, + "bombbag": False, "mode": "open", "logic": "noglitches", "goal": "ganon", @@ -147,8 +147,9 @@ def parse_settings(): "openpyramid": False, "shuffleganon": True, "ow_shuffle": "vanilla", - "ow_swap": "vanilla", + "ow_crossed": "none", "ow_keepsimilar": False, + "ow_mixed": False, "ow_fluteshuffle": "vanilla", "shuffle": "vanilla", "shufflelinks": False, @@ -198,6 +199,7 @@ def parse_settings(): "ow_palettes": "default", "uw_palettes": "default", "reduce_flashing": False, + "shuffle_sfx": False, # Spoiler defaults to TRUE # Playthrough defaults to TRUE diff --git a/DoorShuffle.py b/DoorShuffle.py index 8ad99965..ef8d1863 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -1,22 +1,21 @@ import RaceRandom as random from collections import defaultdict, deque import logging -import operator as op import time from enum import unique, Flag from typing import DefaultDict, Dict, List -from functools import reduce -from BaseClasses import RegionType, Region, Door, DoorType, Direction, Sector, CrystalBarrier, DungeonInfo +from BaseClasses import RegionType, Region, Door, DoorType, Direction, Sector, CrystalBarrier, DungeonInfo, dungeon_keys from Doors import reset_portals from Dungeons import dungeon_regions, region_starts, standard_starts, split_region_starts -from Dungeons import dungeon_bigs, dungeon_keys, dungeon_hints +from Dungeons import dungeon_bigs, dungeon_hints from Items import ItemFactory from RoomData import DoorKind, PairedDoor, reset_rooms from DungeonGenerator import ExplorationState, convert_regions, generate_dungeon, 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, GenerationException -from KeyDoorShuffle import analyze_dungeon, validate_vanilla_key_logic, build_key_layout, validate_key_layout +from KeyDoorShuffle import analyze_dungeon, build_key_layout, validate_key_layout +from Utils import ncr, kth_combination def link_doors(world, player): @@ -90,7 +89,7 @@ def link_doors_main(world, player): if world.mode[player] == 'standard': world.get_portal('Sanctuary', player).destination = True world.get_portal('Desert East', player).destination = True - if world.mode[player] == 'inverted': + if (world.mode[player] == 'inverted') != (0x30 in world.owswaps[player][0] and world.owMixed[player]): world.get_portal('Desert West', player).destination = True else: world.get_portal('Skull 2 West', player).destination = True @@ -102,6 +101,8 @@ def link_doors_main(world, player): connect_portal(portal, world, player) if not world.doorShuffle[player] == 'vanilla': fix_big_key_doors_with_ugly_smalls(world, player) + else: + unmark_ugly_smalls(world, player) if world.doorShuffle[player] == 'vanilla': for entrance, ext in open_edges: connect_two_way(world, entrance, ext, player) @@ -214,8 +215,8 @@ def vanilla_key_logic(world, player): analyze_dungeon(key_layout, world, player) world.key_logic[player][builder.name] = key_layout.key_logic log_key_logic(builder.name, key_layout.key_logic) - if world.shuffle[player] == 'vanilla' and world.owShuffle[player] == 'vanilla' and world.owSwap[player] == 'vanilla' and world.accessibility[player] == 'items' and not world.retro[player] and not world.keydropshuffle[player]: - validate_vanilla_key_logic(world, player) + # if world.shuffle[player] == 'vanilla' and world.owShuffle[player] == 'vanilla' and world.owCrossed[player] == 'none' and not world.owMixed[player] and world.accessibility[player] == 'items' and not world.retro[player] and not world.keydropshuffle[player]: + # validate_vanilla_key_logic(world, player) # some useful functions @@ -317,6 +318,13 @@ def connect_one_way(world, entrancename, exitname, player): y.dest = x +def unmark_ugly_smalls(world, player): + for d in ['Eastern Hint Tile Blocked Path SE', 'Eastern Darkness S', 'Thieves Hallway SE', 'Mire Left Bridge S', + 'TR Lava Escape SE', 'GT Hidden Spikes SE']: + door = world.get_door(d, player) + door.smallKey = False + + def fix_big_key_doors_with_ugly_smalls(world, player): remove_ugly_small_key_doors(world, player) unpair_big_key_doors(world, player) @@ -503,6 +511,9 @@ def analyze_portals(world, player): raise Exception('please inspect this case') if len(reachable_portals) == 1: info.sole_entrance = reachable_portals[0] + if world.intensity[player] < 2 and world.doorShuffle[player] == 'basic' and dungeon == 'Desert Palace': + if len(inaccessible_portals) == 1 and inaccessible_portals[0] == 'Desert Back': + info.required_passage.clear() # can't make a passage at this intensity level, something else must exit info_map[dungeon] = info for dungeon, info in info_map.items(): @@ -740,7 +751,7 @@ def handle_split_dungeons(dungeon_builders, recombinant_builders, entrances_map, def main_dungeon_generation(dungeon_builders, recombinant_builders, connections_tuple, world, player): entrances_map, potentials, connections = connections_tuple - enabled_entrances = {} + enabled_entrances = world.enabled_entrances[player] = {} sector_queue = deque(dungeon_builders.values()) last_key, loops = None, 0 logging.getLogger('').info(world.fish.translate("cli", "cli", "generating.dungeon")) @@ -1071,6 +1082,7 @@ def assign_cross_keys(dungeon_builders, world, player): # Step 3: Initial valid combination find - reduce flex if needed for name, builder in dungeon_builders.items(): suggested = builder.key_doors_num - builder.key_drop_cnt + builder.total_keys = builder.key_doors_num find_valid_combination(builder, start_regions_map[name], world, player) actual_chest_keys = builder.key_doors_num - builder.key_drop_cnt if actual_chest_keys < suggested: @@ -1087,6 +1099,7 @@ def assign_cross_keys(dungeon_builders, world, player): name = builder.name logger.debug('Cross Dungeon: Increasing key count by 1 for %s', name) builder.key_doors_num += 1 + builder.total_keys = builder.key_doors_num result = find_valid_combination(builder, start_regions_map[name], world, player, drop_keys=False) if result: remaining -= 1 @@ -1102,9 +1115,9 @@ def assign_cross_keys(dungeon_builders, world, player): logger.debug('Cross Dungeon: Keys unable to assign in pool %s', remaining) # Last Step: Adjust Small Key Dungeon Pool - if not world.retro[player]: - for name, builder in dungeon_builders.items(): - reassign_key_doors(builder, world, player) + for name, builder in dungeon_builders.items(): + reassign_key_doors(builder, world, player) + if not world.retro[player]: log_key_logic(builder.name, world.key_logic[player][builder.name]) actual_chest_keys = max(builder.key_doors_num - builder.key_drop_cnt, 0) dungeon = world.get_dungeon(name, player) @@ -1380,7 +1393,7 @@ def shuffle_key_doors(builder, world, player): skips.append(world.get_door(dp.door_a, player)) break num_key_doors += 1 - builder.key_doors_num = num_key_doors + builder.key_doors_num = builder.total_keys = num_key_doors find_small_key_door_candidates(builder, start_regions, world, player) find_valid_combination(builder, start_regions, world, player) reassign_key_doors(builder, world, player) @@ -1443,6 +1456,14 @@ def find_valid_combination(builder, start_regions, world, player, drop_keys=True random.shuffle(sample_list) proposal = kth_combination(sample_list[itr], builder.candidates, builder.key_doors_num) + # eliminate start region if portal marked as destination + excluded = {} + for region in start_regions: + portal = next((x for x in world.dungeon_portals[player] if x.door.entrance.parent_region == region), None) + if portal and portal.destination: + excluded[region] = None + start_regions = [x for x in start_regions if x not in excluded.keys()] + key_layout = build_key_layout(builder, start_regions, proposal, world, player) while not validate_key_layout(key_layout, world, player): itr += 1 @@ -1578,28 +1599,6 @@ def find_key_door_candidates(region, checked, world, player): return candidates, checked_doors -def kth_combination(k, l, r): - if r == 0: - return [] - elif len(l) == r: - return l - else: - i = ncr(len(l)-1, r-1) - if k < i: - return l[0:1] + kth_combination(k, l[1:], r-1) - else: - return kth_combination(k-i, l[1:], r) - - -def ncr(n, r): - if r == 0: - return 1 - r = min(r, n-r) - numerator = reduce(op.mul, range(n, n-r, -1), 1) - denominator = reduce(op.mul, range(1, r+1), 1) - return numerator / denominator - - def reassign_key_doors(builder, world, player): logger = logging.getLogger('') logger.debug('Key doors for %s', builder.name) @@ -1828,6 +1827,10 @@ def find_inaccessible_regions(world, player): if connect.type is not RegionType.Dungeon or connect.name.endswith(' Portal'): queue.append(connect) world.inaccessible_regions[player].extend([r.name for r in all_regions.difference(visited_regions) if valid_inaccessible_region(r)]) + if (world.mode[player] == 'inverted') != (0x1b in world.owswaps[player][0] and world.owMixed[player]): + ledge = world.get_region('Hyrule Castle Ledge', 1) + if any(x for x in ledge.exits if x.connected_region.name == 'Agahnims Tower Portal'): + world.inaccessible_regions[player].append('Hyrule Castle Ledge') logger = logging.getLogger('') logger.debug('Inaccessible Regions:') for r in world.inaccessible_regions[player]: @@ -1843,7 +1846,9 @@ def find_accessible_entrances(world, player, builder): elif world.mode[player] != 'inverted': start_regions = ['Links House', 'Sanctuary'] else: - start_regions = ['Links House', 'Dark Sanctuary Hint', 'Hyrule Castle Ledge'] + start_regions = ['Links House', 'Dark Sanctuary Hint'] + if (world.mode[player] == 'inverted') != (0x1b in world.owswaps[player][0] and world.owMixed[player]): + start_regions.append('Hyrule Castle Ledge') regs = convert_regions(start_regions, world, player) visited_regions = set() visited_entrances = [] @@ -1858,7 +1863,7 @@ def find_accessible_entrances(world, player, builder): while len(queue) > 0: next_region = queue.popleft() visited_regions.add(next_region) - if world.mode[player] == 'inverted' and next_region.name == 'Tower Agahnim 1': + if (world.mode[player] == 'inverted') != (0x1b in world.owswaps[player][0] and world.owMixed[player]) and next_region.name == 'Tower Agahnim 1': connect = world.get_region('Hyrule Castle Ledge', player) if connect not in queue and connect not in visited_regions: queue.append(connect) @@ -1866,7 +1871,7 @@ def find_accessible_entrances(world, player, builder): connect = ext.connected_region if connect is None or ext.door and ext.door.blocked: continue - if world.mode[player] == 'standard' and builder.name == 'Hyrule Castle' and ext.name in ['Hyrule Castle Main Gate (North)', 'Top of Pyramid (Inner)', 'Inverted Pyramid Entrance' ]: + if world.mode[player] == 'standard' and builder.name == 'Hyrule Castle' and (ext.name.startswith('Flute From') or ext.name in ['Hyrule Castle Main Gate (North)', 'Top of Pyramid (Inner)', 'Inverted Pyramid Entrance']): continue if connect.name in entrances and connect not in visited_entrances: visited_entrances.append(connect.name) @@ -1917,14 +1922,18 @@ def check_required_paths(paths, world, player): if dungeon_name in world.dungeon_layouts[player].keys(): builder = world.dungeon_layouts[player][dungeon_name] if len(paths[dungeon_name]) > 0: - states_to_explore = defaultdict(list) + states_to_explore = {} for path in paths[dungeon_name]: if type(path) is tuple: - states_to_explore[tuple([path[0]])] = path[1] + states_to_explore[tuple([path[0]])] = (path[1], 'any') else: - states_to_explore[tuple(builder.path_entrances)].append(path) + common_starts = tuple(builder.path_entrances) + 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, dest_regs in states_to_explore.items(): + for start_regs, info in states_to_explore.items(): + dest_regs, path_type = info if type(dest_regs) is not list: dest_regs = [dest_regs] check_paths = convert_regions(dest_regs, world, player) @@ -1941,11 +1950,17 @@ def check_required_paths(paths, world, player): cached_initial_state = state else: state = cached_initial_state - valid, bad_region = check_if_regions_visited(state, check_paths) + if path_type == 'any': + valid, bad_region = check_if_any_regions_visited(state, check_paths) + else: + valid, bad_region = check_if_all_regions_visited(state, check_paths) if not valid: if check_for_pinball_fix(state, bad_region, world, player): explore_state(state, world, player) - valid, bad_region = check_if_regions_visited(state, check_paths) + if path_type == 'any': + valid, bad_region = check_if_any_regions_visited(state, check_paths) + else: + valid, bad_region = check_if_all_regions_visited(state, check_paths) if not valid: raise Exception('%s cannot reach %s' % (dungeon_name, bad_region.name)) @@ -1985,7 +2000,7 @@ def explore_state_not_inaccessible(state, world, player): state.add_all_doors_check_unattached(connect_region, world, player) -def check_if_regions_visited(state, check_paths): +def check_if_any_regions_visited(state, check_paths): valid = False breaking_region = None for region_target in check_paths: @@ -1997,6 +2012,13 @@ def check_if_regions_visited(state, check_paths): return valid, breaking_region +def check_if_all_regions_visited(state, check_paths): + for region_target in check_paths: + if not state.visited_at_all(region_target): + return False, region_target + return True, None + + def check_for_pinball_fix(state, bad_region, world, player): pinball_region = world.get_region('Skull Pinball', player) # todo: lobby shuffle diff --git a/DungeonGenerator.py b/DungeonGenerator.py index 1c49cba0..f579f285 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -39,14 +39,15 @@ def pre_validate(builder, entrance_region_names, split_dungeon, world, player): proposed_map = {} doors_to_connect = {} all_regions = set() - bk_needed = False bk_special = False for sector in builder.sectors: for door in sector.outstanding_doors: doors_to_connect[door.name] = door all_regions.update(sector.regions) - bk_needed = bk_needed or determine_if_bk_needed(sector, split_dungeon, world, player) - bk_special = bk_special or check_for_special(sector) + bk_special |= check_for_special(sector.regions) + bk_needed = False + for sector in builder.sectors: + bk_needed |= determine_if_bk_needed(sector, split_dungeon, bk_special, world, player) paths = determine_paths_for_dungeon(world, player, all_regions, builder.name) dungeon, hangers, hooks = gen_dungeon_info(builder.name, builder.sectors, entrance_regions, all_regions, proposed_map, doors_to_connect, bk_needed, bk_special, world, player) @@ -101,19 +102,28 @@ def generate_dungeon_find_proposal(builder, entrance_region_names, split_dungeon excluded = {} for region in entrance_regions: portal = next((x for x in world.dungeon_portals[player] if x.door.entrance.parent_region == region), None) - if portal and portal.destination: - excluded[region] = None + if portal: + if portal.destination: + excluded[region] = None + elif len(entrance_regions) > 1: + p_region = portal.door.entrance.connected_region + access_region = next(x.parent_region for x in p_region.entrances + if x.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld]) + if (access_region.name in world.inaccessible_regions[player] and + region.name not in world.enabled_entrances[player]): + excluded[region] = None entrance_regions = [x for x in entrance_regions if x not in excluded.keys()] doors_to_connect = {} all_regions = set() - bk_needed = False bk_special = False for sector in builder.sectors: for door in sector.outstanding_doors: doors_to_connect[door.name] = door all_regions.update(sector.regions) - bk_needed = bk_needed or determine_if_bk_needed(sector, split_dungeon, world, player) - bk_special = bk_special or check_for_special(sector) + bk_special |= check_for_special(sector.regions) + bk_needed = False + for sector in builder.sectors: + bk_needed |= determine_if_bk_needed(sector, split_dungeon, bk_special, world, player) proposed_map = {} choices_master = [[]] depth = 0 @@ -187,8 +197,8 @@ def generate_dungeon_find_proposal(builder, entrance_region_names, split_dungeon return proposed_map -def determine_if_bk_needed(sector, split_dungeon, world, player): - if not split_dungeon: +def determine_if_bk_needed(sector, split_dungeon, bk_special, world, player): + if not split_dungeon or bk_special: for region in sector.regions: for ext in region.exits: door = world.check_for_door(ext.name, player) @@ -197,8 +207,8 @@ def determine_if_bk_needed(sector, split_dungeon, world, player): return False -def check_for_special(sector): - for region in sector.regions: +def check_for_special(regions): + for region in regions: for loc in region.locations: if loc.forced_big_key(): return True @@ -417,6 +427,8 @@ def check_valid(name, dungeon, hangers, hooks, proposed_map, doors_to_connect, a # origin has no more hooks, but not all doors have been proposed if not world.bigkeyshuffle[player]: possible_bks = len(dungeon['Origin'].possible_bk_locations) + if bk_special and check_for_special(dungeon['Origin'].visited_regions): + possible_bks = 1 true_origin_hooks = [x for x in dungeon['Origin'].hooks.keys() if not x.bigKey or possible_bks > 0 or not bk_needed] if len(true_origin_hooks) == 0 and len(proposed_map.keys()) < len(doors_to_connect): return False @@ -450,7 +462,8 @@ def check_valid(name, dungeon, hangers, hooks, proposed_map, doors_to_connect, a bk_possible = not bk_needed or (world.bigkeyshuffle[player] and not bk_special) for piece in dungeon.values(): all_visited.update(piece.visited_regions) - if not bk_possible and len(piece.possible_bk_locations) > 0: + if ((not bk_possible and len(piece.possible_bk_locations) > 0) or + (bk_special and check_for_special(piece.visited_regions))): bk_possible = True if len(all_regions.difference(all_visited)) > 0: return False @@ -807,6 +820,10 @@ class ExplorationState(object): self.dungeon = dungeon self.pinball_used = False + self.prize_door_set = {} + self.prize_doors = [] + self.prize_doors_opened = False + def copy(self): ret = ExplorationState(dungeon=self.dungeon) ret.unattached_doors = list(self.unattached_doors) @@ -833,6 +850,10 @@ class ExplorationState(object): ret.non_door_entrances = list(self.non_door_entrances) ret.dungeon = self.dungeon ret.pinball_used = self.pinball_used + + ret.prize_door_set = dict(self.prize_door_set) + ret.prize_doors = list(self.prize_doors) + ret.prize_doors_opened = self.prize_doors_opened return ret def next_avail_door(self): @@ -842,6 +863,8 @@ class ExplorationState(object): return exp_door def visit_region(self, region, key_region=None, key_checks=False, bk_Flag=False): + if region.type != RegionType.Dungeon: + self.crystal = CrystalBarrier.Orange if self.crystal == CrystalBarrier.Either: if region not in self.visited_blue: self.visited_blue.append(region) @@ -868,6 +891,8 @@ class ExplorationState(object): if location.name in flooded_keys_reverse.keys() and self.location_found( flooded_keys_reverse[location.name]): self.perform_event(flooded_keys_reverse[location.name], key_region) + if '- Prize' in location.name: + self.prize_received = True def flooded_key_check(self, location): if location.name not in flooded_keys.keys(): @@ -1103,9 +1128,9 @@ def valid_region_to_explore_in_regions(region, all_regions, world, player): def valid_region_to_explore(region, name, world, player): if region is None: return False - return (region.type == RegionType.Dungeon and region.dungeon.name in name)\ - or region.name in world.inaccessible_regions[player]\ - or (region.name == 'Hyrule Castle Ledge' and world.mode[player] == 'standard') + return ((region.type == RegionType.Dungeon and region.dungeon and region.dungeon.name in name) + or region.name in world.inaccessible_regions[player] + or (region.name == 'Hyrule Castle Ledge' and world.mode[player] == 'standard')) def get_doors(world, region, player): @@ -1175,6 +1200,7 @@ class DungeonBuilder(object): self.pre_open_stonewalls = set() # used by stonewall system self.candidates = None + self.total_keys = None self.key_doors_num = None self.combo_size = None self.flex = 0 @@ -1290,13 +1316,16 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player, sanc_builder = random.choice(lw_builders) assign_sector(sanc, sanc_builder, candidate_sectors, global_pole) + bow_sectors, retro_std_flag = {}, world.retro[player] and world.mode[player] == 'standard' free_location_sectors = {} crystal_switches = {} crystal_barriers = {} polarized_sectors = {} neutral_sectors = {} for sector in candidate_sectors: - if sector.chest_locations > 0: + if retro_std_flag and 'Bow' in sector.item_logic: # these need to be distributed outside of HC + bow_sectors[sector] = None + elif sector.chest_locations > 0: free_location_sectors[sector] = None elif sector.c_switch: crystal_switches[sector] = None @@ -1306,6 +1335,8 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player, neutral_sectors[sector] = None else: polarized_sectors[sector] = None + if bow_sectors: + assign_bow_sectors(dungeon_map, bow_sectors, global_pole) assign_location_sectors(dungeon_map, free_location_sectors, global_pole) leftover = assign_crystal_switch_sectors(dungeon_map, crystal_switches, crystal_barriers, global_pole) ensure_crystal_switches_reachable(dungeon_map, leftover, polarized_sectors, crystal_barriers, global_pole) @@ -1471,6 +1502,9 @@ def define_sector_features(sectors): sector.blue_barrier = True if door.bigKey: sector.bk_required = True + if region.name in ['PoD Mimics 2', 'PoD Bow Statue Right', 'PoD Mimics 1', 'GT Mimics 1', 'GT Mimics 2', + 'Eastern Single Eyegore', 'Eastern Duo Eyegores']: + sector.item_logic.add('Bow') def assign_sector(sector, dungeon, candidate_sectors, global_pole): @@ -1521,6 +1555,19 @@ def find_sector(r_name, sectors): return None +def assign_bow_sectors(dungeon_map, bow_sectors, global_pole): + sector_list = list(bow_sectors) + random.shuffle(sector_list) + population = [] + for name in dungeon_map: + if name != 'Hyrule Castle': + population.append(name) + choices = random.choices(population, k=len(sector_list)) + for i, choice in enumerate(choices): + builder = dungeon_map[choice] + assign_sector(sector_list[i], builder, bow_sectors, global_pole) + + def assign_location_sectors(dungeon_map, free_location_sectors, global_pole): valid = False choices = None @@ -3274,6 +3321,7 @@ def check_for_valid_layout(builder, sector_list, builder_info): possible_regions.add(portal.door.entrance.parent_region.name) if builder.name in dungeon_drops.keys(): possible_regions.update(dungeon_drops[builder.name]) + independents = find_independent_entrances(possible_regions, world, player) for name, split_build in builder.split_dungeon_map.items(): name_bits = name.split(" ") orig_name = " ".join(name_bits[:-1]) @@ -3286,7 +3334,8 @@ def check_for_valid_layout(builder, sector_list, builder_info): if r_name not in entrance_regions: entrance_regions.append(r_name) # entrance_regions = [x for x in entrance_regions if x not in split_check_entrance_invalid] - proposal = generate_dungeon_find_proposal(split_build, entrance_regions, True, world, player) + split = any(x for x in independents if x not in entrance_regions) + proposal = generate_dungeon_find_proposal(split_build, entrance_regions, split, world, player) # record split proposals builder.valid_proposal[name] = proposal builder.exception_list = list(sector_list) @@ -3301,6 +3350,32 @@ def check_for_valid_layout(builder, sector_list, builder_info): return len(unreached_doors) == 0, unreached_doors +def find_independent_entrances(entrance_regions, world, player): + independents = set() + for region in entrance_regions: + portal = next((x for x in world.dungeon_portals[player] if x.door.entrance.parent_region.name == region), None) + if portal: + if portal.destination: + continue + elif len(entrance_regions) > 1: + p_region = portal.door.entrance.connected_region + access_region = next(x.parent_region for x in p_region.entrances + if x.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld]) + if access_region.name in world.inaccessible_regions[player]: + continue + else: + r = world.get_region(region, player) + access_region = next(x.parent_region for x in r.entrances + if x.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld] + or x.parent_region.name == 'Sewer Drop') + if access_region.name == 'Sewer Drop': + access_region = next(x.parent_region for x in access_region.entrances) + if access_region.name in world.inaccessible_regions[player]: + continue + independents.add(region) + return independents + + def resolve_equations(builder, sector_list): unreached_doors = defaultdict(list) equations = {x: y for x, y in copy_door_equations(builder, sector_list).items() if len(y) > 0} diff --git a/Dungeons.py b/Dungeons.py index 596da920..6fe38cfb 100644 --- a/Dungeons.py +++ b/Dungeons.py @@ -375,6 +375,38 @@ flexible_starts = { 'Skull Woods': ['Skull Left Drop', 'Skull Pinball'] } + +class DungeonInfo: + + def __init__(self, free, keys, bk, map, compass, bk_drop, drops, prize=None): + # todo reduce static maps ideas: prize, bk_name, sm_name, cmp_name, map_name): + self.free_items = free + self.key_num = keys + self.bk_present = bk + self.map_present = map + self.compass_present = compass + self.bk_drops = bk_drop + self.key_drops = drops + self.prize = prize + + +dungeon_table = { + 'Hyrule Castle': DungeonInfo(6, 1, False, True, False, True, 3, None), + 'Eastern Palace': DungeonInfo(3, 0, True, True, True, False, 2, 'Eastern Palace - Prize'), + 'Desert Palace': DungeonInfo(2, 1, True, True, True, False, 3, 'Desert Palace - Prize'), + 'Tower of Hera': DungeonInfo(2, 1, True, True, True, False, 0, 'Tower of Hera - Prize'), + 'Agahnims Tower': DungeonInfo(0, 2, False, False, False, False, 2, None), + 'Palace of Darkness': DungeonInfo(5, 6, True, True, True, False, 0, 'Palace of Darkness - Prize'), + 'Swamp Palace': DungeonInfo(6, 1, True, True, True, False, 5, 'Swamp Palace - Prize'), + 'Skull Woods': DungeonInfo(2, 3, True, True, True, False, 2, 'Skull Woods - Prize'), + 'Thieves Town': DungeonInfo(4, 1, True, True, True, False, 2, "Thieves' Town - Prize"), + 'Ice Palace': DungeonInfo(3, 2, True, True, True, False, 4, 'Ice Palace - Prize'), + 'Misery Mire': DungeonInfo(2, 3, True, True, True, False, 3, 'Misery Mire - Prize'), + 'Turtle Rock': DungeonInfo(5, 4, True, True, True, False, 2, 'Turtle Rock - Prize'), + 'Ganons Tower': DungeonInfo(20, 4, True, True, True, False, 4, None), +} + + dungeon_keys = { 'Hyrule Castle': 'Small Key (Escape)', 'Eastern Palace': 'Small Key (Eastern Palace)', @@ -407,6 +439,7 @@ dungeon_bigs = { 'Ganons Tower': 'Big Key (Ganons Tower)' } + dungeon_hints = { 'Hyrule Castle': 'in Hyrule Castle', 'Eastern Palace': 'in Eastern Palace', diff --git a/ER_hint_reference.txt b/ER_hint_reference.txt index 999fb436..8bc73fc9 100644 --- a/ER_hint_reference.txt +++ b/ER_hint_reference.txt @@ -14,7 +14,7 @@ In the vanilla, dungeonssimple, and dungeonsfull shuffles, the following ratios 8 hints for valuable items. 7 junk hints. -In the simple, restricted, and restricted legacy shuffles, these are the ratios: +In the simple, restricted shuffles, these are the ratios: 2 hints for inconvenient entrances. 1 hint for an inconvenient dungeon entrance. diff --git a/EntranceShuffle.py b/EntranceShuffle.py index 495a99be..8fda03fc 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -29,7 +29,7 @@ def link_entrances(world, player): # if we do not shuffle, set default connections if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull']: - for exitname, regionname in default_connections: + for exitname, regionname in default_connections + default_connector_connections + default_drop_connections + default_item_connections + default_shop_connections: connect_simple(world, exitname, regionname, player) if world.shuffle[player] == 'vanilla': for exitname, regionname in default_dungeon_connections: @@ -50,18 +50,18 @@ def link_entrances(world, player): # inverted entrance mods for owid in swapped_connections.keys(): - if (world.mode[player] == 'inverted') != (owid in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if invFlag != (owid in world.owswaps[player][0] and world.owMixed[player]): for (entrancename, exitname) in swapped_connections[owid]: try: connect_two_way(world, entrancename, exitname, player) except RuntimeError: connect_entrance(world, entrancename, exitname, player) - if (world.mode[player] == 'inverted') != (0x03 in world.owswaps[player][0] and world.owSwap[player] == 'mixed') and \ - (world.mode[player] == 'inverted') == (0x0a in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if invFlag != (0x03 in world.owswaps[player][0] and world.owMixed[player]) and \ + invFlag == (0x0a in world.owswaps[player][0] and world.owMixed[player]): connect_entrance(world, 'Death Mountain Return Cave (West)', 'Dark Death Mountain Healer Fairy', player) - elif (world.mode[player] == 'inverted') != (0x0a in world.owswaps[player][0] and world.owSwap[player] == 'mixed') and \ - (world.mode[player] == 'inverted') == (0x03 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + elif invFlag != (0x0a in world.owswaps[player][0] and world.owMixed[player]) and \ + invFlag == (0x03 in world.owswaps[player][0] and world.owMixed[player]): connect_two_way(world, 'Bumper Cave (Top)', 'Death Mountain Return Cave Exit (West)', player) # dungeon entrance shuffle @@ -225,7 +225,7 @@ def link_entrances(world, player): random.shuffle(remaining_entrances) old_man_entrance = remaining_entrances.pop() - connect_two_way(world, old_man_entrance if not invFlag else 'Bumper Cave (Bottom)', 'Old Man Cave Exit (West)', player) + connect_two_way(world, old_man_entrance if invFlag == (0x0a in world.owswaps[player][0] and world.owMixed[player]) else 'Bumper Cave (Bottom)', 'Old Man Cave Exit (West)', player) connect_two_way(world, old_man_exit, 'Old Man Cave Exit (East)', player) if invFlag and old_man_exit == 'Spike Cave': bomb_shop_doors.remove('Spike Cave') @@ -239,10 +239,7 @@ def link_entrances(world, player): connect_caves(world, remaining_entrances if not invFlag else lw_dm_entrances, [], caves, player) # scramble holes - if not invFlag: - scramble_holes(world, player) - else: - scramble_inverted_holes(world, player) + scramble_holes(world, player) # place blacksmith, has limited options if invFlag: @@ -371,70 +368,12 @@ def link_entrances(world, player): connect_caves(world, lw_entrances, dw_entrances, caves, player) # scramble holes - if not invFlag: - scramble_holes(world, player) - else: - scramble_inverted_holes(world, player) + scramble_holes(world, player) doors = lw_entrances + dw_entrances # place remaining doors connect_doors(world, doors, door_targets, player) - elif not invFlag and world.shuffle[player] == 'restricted_legacy': - simple_shuffle_dungeons(world, player) - - lw_entrances = list(LW_Entrances) - dw_entrances = list(DW_Entrances) - dw_must_exits = list(DW_Entrances_Must_Exit) - old_man_entrances = list(Old_Man_Entrances) - caves = list(Cave_Exits) - three_exit_caves = list(Cave_Three_Exits) - single_doors = list(Single_Cave_Doors) - bomb_shop_doors = list(Bomb_Shop_Single_Cave_Doors) - blacksmith_doors = list(Blacksmith_Single_Cave_Doors) - door_targets = list(Single_Cave_Targets) - - # only use two exit caves to do mandatory dw connections - connect_mandatory_exits(world, dw_entrances, caves, dw_must_exits, player) - # add three exit doors to pool for remainder - caves.extend(three_exit_caves) - - # place old man, has limited options - # exit has to come from specific set of doors, the entrance is free to move about - random.shuffle(old_man_entrances) - old_man_exit = old_man_entrances.pop() - lw_entrances.extend(old_man_entrances) - random.shuffle(lw_entrances) - old_man_entrance = lw_entrances.pop() - connect_two_way(world, old_man_entrance, 'Old Man Cave Exit (West)', player) - connect_two_way(world, old_man_exit, 'Old Man Cave Exit (East)', player) - - # place Old Man House in Light World - connect_caves(world, lw_entrances, [], Old_Man_House, player) - - # connect rest. There's 2 dw entrances remaining, so we will not run into parity issue placing caves - connect_caves(world, lw_entrances, dw_entrances, caves, player) - - # scramble holes - scramble_holes(world, player) - - # place blacksmith, has limited options - random.shuffle(blacksmith_doors) - blacksmith_hut = blacksmith_doors.pop() - connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player) - bomb_shop_doors.extend(blacksmith_doors) - - # place dam and pyramid fairy, have limited options - random.shuffle(bomb_shop_doors) - bomb_shop = bomb_shop_doors.pop() - connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) - single_doors.extend(bomb_shop_doors) - - # tavern back door cannot be shuffled yet - connect_doors(world, ['Tavern North'], ['Tavern'], player) - - # place remaining doors - connect_doors(world, single_doors, door_targets, player) elif world.shuffle[player] == 'full': skull_woods_shuffle(world, player) @@ -475,9 +414,7 @@ def link_entrances(world, player): connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) elif invFlag or world.doorShuffle[player] == 'vanilla': caves.append(tuple(random.sample(['Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)'],3))) - lw_entrances.append('Hyrule Castle Entrance (South)') - else: - lw_entrances.append('Hyrule Castle Entrance (South)') + lw_entrances.append('Hyrule Castle Entrance (South)') if not world.shuffle_ganon: connect_two_way(world, 'Ganons Tower' if not invFlag else 'Agahnims Tower', 'Ganons Tower Exit', player) @@ -632,10 +569,7 @@ def link_entrances(world, player): connect_caves(world, lw_entrances, dw_entrances, caves, player) # scramble holes - if not invFlag: - scramble_holes(world, player) - else: - scramble_inverted_holes(world, player) + scramble_holes(world, player) doors = lw_entrances + dw_entrances @@ -776,331 +710,10 @@ def link_entrances(world, player): connect_caves(world, entrances, [], caves, player) # scramble holes - if not invFlag: - scramble_holes(world, player) - else: - scramble_inverted_holes(world, player) + scramble_holes(world, player) # place remaining doors connect_doors(world, entrances, door_targets, player) - elif not invFlag and world.shuffle[player] == 'full_legacy': - skull_woods_shuffle(world, player) - - lw_entrances = list(LW_Entrances + LW_Dungeon_Entrances + Old_Man_Entrances) - dw_entrances = list(DW_Entrances + DW_Dungeon_Entrances) - dw_must_exits = list(DW_Entrances_Must_Exit + DW_Dungeon_Entrances_Must_Exit) - lw_must_exits = list(LW_Dungeon_Entrances_Must_Exit) - old_man_entrances = list(Old_Man_Entrances + ['Tower of Hera']) - caves = list(Cave_Exits + Dungeon_Exits + Cave_Three_Exits) # don't need to consider three exit caves, have one exit caves to avoid parity issues - single_doors = list(Single_Cave_Doors) - bomb_shop_doors = list(Bomb_Shop_Single_Cave_Doors) - blacksmith_doors = list(Blacksmith_Single_Cave_Doors) - door_targets = list(Single_Cave_Targets) - - if world.mode[player] == 'standard': - # must connect front of hyrule castle to do escape - connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) - else: - caves.append(tuple(random.sample(['Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)'],3))) - lw_entrances.append('Hyrule Castle Entrance (South)') - - if not world.shuffle_ganon: - connect_two_way(world, 'Ganons Tower', 'Ganons Tower Exit', player) - else: - dw_entrances.append('Ganons Tower') - caves.append('Ganons Tower Exit') - - # we randomize which world requirements we fulfill first so we get better dungeon distribution - if random.randint(0, 1) == 0: - connect_mandatory_exits(world, lw_entrances, caves, lw_must_exits, player) - connect_mandatory_exits(world, dw_entrances, caves, dw_must_exits, player) - else: - connect_mandatory_exits(world, dw_entrances, caves, dw_must_exits, player) - connect_mandatory_exits(world, lw_entrances, caves, lw_must_exits, player) - if world.mode[player] == 'standard': - # rest of hyrule castle must be in light world - connect_caves(world, lw_entrances, [], [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')], player) - - # place old man, has limited options - # exit has to come from specific set of doors, the entrance is free to move about - old_man_entrances = [door for door in old_man_entrances if door in lw_entrances] - random.shuffle(old_man_entrances) - old_man_exit = old_man_entrances.pop() - lw_entrances.remove(old_man_exit) - - random.shuffle(lw_entrances) - old_man_entrance = lw_entrances.pop() - connect_two_way(world, old_man_entrance, 'Old Man Cave Exit (West)', player) - connect_two_way(world, old_man_exit, 'Old Man Cave Exit (East)', player) - - # place Old Man House in Light World - connect_caves(world, lw_entrances, [], list(Old_Man_House), player) #need this to avoid badness with multiple seeds - - # now scramble the rest - connect_caves(world, lw_entrances, dw_entrances, caves, player) - - # scramble holes - scramble_holes(world, player) - - # place blacksmith, has limited options - random.shuffle(blacksmith_doors) - blacksmith_hut = blacksmith_doors.pop() - connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player) - bomb_shop_doors.extend(blacksmith_doors) - - # place bomb shop, has limited options - random.shuffle(bomb_shop_doors) - bomb_shop = bomb_shop_doors.pop() - connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) - single_doors.extend(bomb_shop_doors) - - # tavern back door cannot be shuffled yet - connect_doors(world, ['Tavern North'], ['Tavern'], player) - - # place remaining doors - connect_doors(world, single_doors, door_targets, player) - elif not invFlag and world.shuffle[player] == 'madness_legacy': - # here lie dragons, connections are no longer two way - lw_entrances = list(LW_Entrances + LW_Dungeon_Entrances + Old_Man_Entrances) - dw_entrances = list(DW_Entrances + DW_Dungeon_Entrances) - dw_entrances_must_exits = list(DW_Entrances_Must_Exit + DW_Dungeon_Entrances_Must_Exit) - - lw_doors = list(LW_Entrances + LW_Dungeon_Entrances + LW_Dungeon_Entrances_Must_Exit) + ['Kakariko Well Cave', 'Bat Cave Cave', 'North Fairy Cave', 'Sanctuary', 'Lost Woods Hideout Stump', - 'Lumberjack Tree Cave'] + list(Old_Man_Entrances) - dw_doors = list(DW_Entrances + DW_Dungeon_Entrances + DW_Entrances_Must_Exit + DW_Dungeon_Entrances_Must_Exit) + ['Skull Woods First Section Door', 'Skull Woods Second Section Door (East)', 'Skull Woods Second Section Door (West)'] - - random.shuffle(lw_doors) - random.shuffle(dw_doors) - - dw_entrances_must_exits.append('Skull Woods Second Section Door (West)') - dw_entrances.append('Skull Woods Second Section Door (East)') - dw_entrances.append('Skull Woods First Section Door') - - lw_entrances.extend(['Kakariko Well Cave', 'Bat Cave Cave', 'North Fairy Cave', 'Sanctuary', 'Lost Woods Hideout Stump', 'Lumberjack Tree Cave']) - - lw_entrances_must_exits = list(LW_Dungeon_Entrances_Must_Exit) - - old_man_entrances = list(Old_Man_Entrances) + ['Tower of Hera'] - - mandatory_light_world = ['Old Man House Exit (Bottom)', 'Old Man House Exit (Top)'] - mandatory_dark_world = [] - caves = list(Cave_Exits + Dungeon_Exits + Cave_Three_Exits) - - # shuffle up holes - - lw_hole_entrances = ['Kakariko Well Drop', 'Bat Cave Drop', 'North Fairy Cave Drop', 'Lost Woods Hideout Drop', 'Lumberjack Tree Tree', 'Sanctuary Grave'] - dw_hole_entrances = ['Skull Woods First Section Hole (East)', 'Skull Woods First Section Hole (West)', 'Skull Woods First Section Hole (North)', 'Skull Woods Second Section Hole'] - - hole_targets = [('Kakariko Well Exit', 'Kakariko Well (top)'), - ('Bat Cave Exit', 'Bat Cave (right)'), - ('North Fairy Cave Exit', 'North Fairy Cave'), - ('Lost Woods Hideout Exit', 'Lost Woods Hideout (top)'), - ('Lumberjack Tree Exit', 'Lumberjack Tree (top)'), - (('Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)'), 'Skull Back Drop')] - - if world.mode[player] == 'standard': - # cannot move uncle cave - connect_entrance(world, 'Hyrule Castle Secret Entrance Drop', 'Hyrule Castle Secret Entrance', player) - connect_exit(world, 'Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance Stairs', player) - connect_entrance(world, 'Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Secret Entrance Exit', player) - else: - lw_hole_entrances.append('Hyrule Castle Secret Entrance Drop') - hole_targets.append(('Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance')) - lw_doors.append('Hyrule Castle Secret Entrance Stairs') - lw_entrances.append('Hyrule Castle Secret Entrance Stairs') - - if not world.shuffle_ganon: - connect_two_way(world, 'Ganons Tower', 'Ganons Tower Exit', player) - connect_two_way(world, 'Pyramid Entrance', 'Pyramid Exit', player) - connect_entrance(world, 'Pyramid Hole', 'Pyramid', player) - else: - dw_entrances.append('Ganons Tower') - caves.append('Ganons Tower Exit') - dw_hole_entrances.append('Pyramid Hole') - hole_targets.append(('Pyramid Exit', 'Pyramid')) - dw_entrances_must_exits.append('Pyramid Entrance') - dw_doors.extend(['Ganons Tower', 'Pyramid Entrance']) - - random.shuffle(lw_hole_entrances) - random.shuffle(dw_hole_entrances) - random.shuffle(hole_targets) - - # decide if skull woods first section should be in light or dark world - sw_light = random.randint(0, 1) == 0 - if sw_light: - sw_hole_pool = lw_hole_entrances - mandatory_light_world.append('Skull Woods First Section Exit') - else: - sw_hole_pool = dw_hole_entrances - mandatory_dark_world.append('Skull Woods First Section Exit') - for target in ['Skull Left Drop', 'Skull Pinball', 'Skull Pot Circle']: - connect_entrance(world, sw_hole_pool.pop(), target, player) - - # sanctuary has to be in light world - connect_entrance(world, lw_hole_entrances.pop(), 'Sewer Drop', player) - mandatory_light_world.append('Sanctuary Exit') - - # fill up remaining holes - for hole in dw_hole_entrances: - exits, target = hole_targets.pop() - mandatory_dark_world.append(exits) - connect_entrance(world, hole, target, player) - - for hole in lw_hole_entrances: - exits, target = hole_targets.pop() - mandatory_light_world.append(exits) - connect_entrance(world, hole, target, player) - - # hyrule castle handling - if world.mode[player] == 'standard': - # must connect front of hyrule castle to do escape - connect_entrance(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) - connect_exit(world, 'Hyrule Castle Exit (South)', 'Hyrule Castle Entrance (South)', player) - mandatory_light_world.append(('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')) - else: - lw_doors.append('Hyrule Castle Entrance (South)') - lw_entrances.append('Hyrule Castle Entrance (South)') - caves.append(('Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')) - - # now let's deal with mandatory reachable stuff - def extract_reachable_exit(cavelist): - random.shuffle(cavelist) - candidate = None - for cave in cavelist: - if isinstance(cave, tuple) and len(cave) > 1: - # special handling: TRock and Spectracle Rock cave have two entries that we should consider entrance only - # ToDo this should be handled in a more sensible manner - if cave[0] in ['Turtle Rock Exit (Front)', 'Spectacle Rock Cave Exit (Peak)'] and len(cave) == 2: - continue - candidate = cave - break - if candidate is None: - raise RuntimeError('No suitable cave.') - cavelist.remove(candidate) - return candidate - - def connect_reachable_exit(entrance, general, worldspecific, worldoors): - # select which one is the primary option - if random.randint(0, 1) == 0: - primary = general - secondary = worldspecific - else: - primary = worldspecific - secondary = general - - try: - cave = extract_reachable_exit(primary) - except RuntimeError: - cave = extract_reachable_exit(secondary) - - exit = cave[-1] - cave = cave[:-1] - connect_exit(world, exit, entrance, player) - connect_entrance(world, worldoors.pop(), exit, player) - # rest of cave now is forced to be in this world - worldspecific.append(cave) - - # we randomize which world requirements we fulfill first so we get better dungeon distribution - if random.randint(0, 1) == 0: - for entrance in lw_entrances_must_exits: - connect_reachable_exit(entrance, caves, mandatory_light_world, lw_doors) - for entrance in dw_entrances_must_exits: - connect_reachable_exit(entrance, caves, mandatory_dark_world, dw_doors) - else: - for entrance in dw_entrances_must_exits: - connect_reachable_exit(entrance, caves, mandatory_dark_world, dw_doors) - for entrance in lw_entrances_must_exits: - connect_reachable_exit(entrance, caves, mandatory_light_world, lw_doors) - - # place old man, has limited options - # exit has to come from specific set of doors, the entrance is free to move about - old_man_entrances = [entrance for entrance in old_man_entrances if entrance in lw_entrances] - random.shuffle(old_man_entrances) - old_man_exit = old_man_entrances.pop() - lw_entrances.remove(old_man_exit) - - connect_exit(world, 'Old Man Cave Exit (East)', old_man_exit, player) - connect_entrance(world, lw_doors.pop(), 'Old Man Cave Exit (East)', player) - mandatory_light_world.append('Old Man Cave Exit (West)') - - # we connect up the mandatory associations we have found - for mandatory in mandatory_light_world: - if not isinstance(mandatory, tuple): - mandatory = (mandatory,) - for exit in mandatory: - # point out somewhere - connect_exit(world, exit, lw_entrances.pop(), player) - # point in from somewhere - connect_entrance(world, lw_doors.pop(), exit, player) - - for mandatory in mandatory_dark_world: - if not isinstance(mandatory, tuple): - mandatory = (mandatory,) - for exit in mandatory: - # point out somewhere - connect_exit(world, exit, dw_entrances.pop(), player) - # point in from somewhere - connect_entrance(world, dw_doors.pop(), exit, player) - - # handle remaining caves - while caves: - # connect highest exit count caves first, prevent issue where we have 2 or 3 exits across worlds left to fill - cave_candidate = (None, 0) - for i, cave in enumerate(caves): - if isinstance(cave, str): - cave = (cave,) - if len(cave) > cave_candidate[1]: - cave_candidate = (i, len(cave)) - cave = caves.pop(cave_candidate[0]) - - place_lightworld = random.randint(0, 1) == 0 - if place_lightworld: - target_doors = lw_doors - target_entrances = lw_entrances - else: - target_doors = dw_doors - target_entrances = dw_entrances - - if isinstance(cave, str): - cave = (cave,) - - # check if we can still fit the cave into our target group - if len(target_doors) < len(cave): - if not place_lightworld: - target_doors = lw_doors - target_entrances = lw_entrances - else: - target_doors = dw_doors - target_entrances = dw_entrances - - for exit in cave: - connect_exit(world, exit, target_entrances.pop(), player) - connect_entrance(world, target_doors.pop(), exit, player) - - # handle simple doors - - single_doors = list(Single_Cave_Doors) - bomb_shop_doors = list(Bomb_Shop_Single_Cave_Doors) - blacksmith_doors = list(Blacksmith_Single_Cave_Doors) - door_targets = list(Single_Cave_Targets) - - # place blacksmith, has limited options - random.shuffle(blacksmith_doors) - blacksmith_hut = blacksmith_doors.pop() - connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player) - bomb_shop_doors.extend(blacksmith_doors) - - # place dam and pyramid fairy, have limited options - random.shuffle(bomb_shop_doors) - bomb_shop = bomb_shop_doors.pop() - connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) - single_doors.extend(bomb_shop_doors) - - # tavern back door cannot be shuffled yet - connect_doors(world, ['Tavern North'], ['Tavern'], player) - - # place remaining doors - connect_doors(world, single_doors, door_targets, player) elif world.shuffle[player] == 'insanity': # beware ye who enter here @@ -1178,20 +791,28 @@ def link_entrances(world, player): if not world.shuffle_ganon: connect_two_way(world, 'Ganons Tower' if not invFlag else 'Agahnims Tower', 'Ganons Tower Exit', player) - connect_two_way(world, 'Pyramid Entrance' if not invFlag else 'Inverted Pyramid Entrance', 'Pyramid Exit', player) - connect_entrance(world, 'Pyramid Hole' if not invFlag else 'Inverted Pyramid Hole', 'Pyramid', player) + connect_two_way(world, 'Pyramid Entrance' if invFlag == (0x1b in world.owswaps[player][0] and world.owMixed[player]) else 'Inverted Pyramid Entrance', 'Pyramid Exit', player) + connect_entrance(world, 'Pyramid Hole' if invFlag == (0x1b in world.owswaps[player][0] and world.owMixed[player]) else 'Inverted Pyramid Hole', 'Pyramid', player) else: caves.extend(['Ganons Tower Exit', 'Pyramid Exit']) hole_targets.append('Pyramid') + if not invFlag: - exit_pool.extend(['Ganons Tower', 'Pyramid Entrance']) + exit_pool.extend(['Ganons Tower']) + doors.extend(['Ganons Tower']) + else: + exit_pool.extend(['Agahnims Tower']) + doors.extend(['Agahnims Tower']) + + if invFlag == (0x1b in world.owswaps[player][0] and world.owMixed[player]): + exit_pool.extend(['Pyramid Entrance']) hole_entrances.append('Pyramid Hole') entrances_must_exits.append('Pyramid Entrance') - doors.extend(['Ganons Tower', 'Pyramid Entrance']) + doors.extend(['Pyramid Entrance']) else: - exit_pool.extend(['Agahnims Tower', 'Inverted Pyramid Entrance']) + exit_pool.extend(['Inverted Pyramid Entrance']) hole_entrances.append('Inverted Pyramid Hole') - doors.extend(['Agahnims Tower', 'Inverted Pyramid Entrance']) + doors.extend(['Inverted Pyramid Entrance']) random.shuffle(hole_entrances) random.shuffle(hole_targets) @@ -1310,198 +931,6 @@ def link_entrances(world, player): # place remaining doors connect_doors(world, doors, door_targets, player) - elif world.shuffle[player] == 'insanity_legacy': - world.fix_fake_world[player] = False - # beware ye who enter here - - if not invFlag: - entrances_must_exits = DW_Entrances_Must_Exit + DW_Dungeon_Entrances_Must_Exit + LW_Dungeon_Entrances_Must_Exit + ['Skull Woods Second Section Door (West)'] - - doors = LW_Entrances + LW_Dungeon_Entrances + DW_Entrances + DW_Dungeon_Entrances + Old_Man_Entrances + ['Skull Woods Second Section Door (East)', 'Skull Woods First Section Door', 'Kakariko Well Cave', 'Bat Cave Cave', 'North Fairy Cave', 'Sanctuary', 'Lost Woods Hideout Stump', 'Lumberjack Tree Cave'] - else: - entrances_must_exits = Inverted_LW_Entrances_Must_Exit + Inverted_LW_Dungeon_Entrances_Must_Exit - - doors = Inverted_LW_Entrances + Inverted_LW_Dungeon_Entrances + Inverted_LW_Entrances_Must_Exit + Inverted_LW_Dungeon_Entrances_Must_Exit + ['Kakariko Well Cave', 'Bat Cave Cave', 'North Fairy Cave', 'Sanctuary', 'Lost Woods Hideout Stump', 'Lumberjack Tree Cave', 'Hyrule Castle Secret Entrance Stairs'] + Inverted_Old_Man_Entrances +\ - Inverted_DW_Entrances + Inverted_DW_Dungeon_Entrances + ['Skull Woods First Section Door', 'Skull Woods Second Section Door (East)', 'Skull Woods Second Section Door (West)'] +\ - Inverted_LW_Single_Cave_Doors + Inverted_DW_Single_Cave_Doors + ['Desert Palace Entrance (West)', 'Desert Palace Entrance (North)'] - - exit_pool = list(doors) - - # randomize which desert ledge door is a must-exit - if random.randint(0, 1) == 0: - entrances_must_exits.append('Desert Palace Entrance (North)') - else: - entrances_must_exits.append('Desert Palace Entrance (West)') - - doors = LW_Entrances + LW_Dungeon_Entrances + LW_Dungeon_Entrances_Must_Exit + ['Kakariko Well Cave', 'Bat Cave Cave', 'North Fairy Cave', 'Sanctuary', 'Lost Woods Hideout Stump', 'Lumberjack Tree Cave'] + Old_Man_Entrances +\ - DW_Entrances + DW_Dungeon_Entrances + DW_Entrances_Must_Exit + DW_Dungeon_Entrances_Must_Exit + ['Skull Woods First Section Door', 'Skull Woods Second Section Door (East)', 'Skull Woods Second Section Door (West)'] - - random.shuffle(doors) - - old_man_entrances = list(Old_Man_Entrances) + ['Tower of Hera'] - - caves = Cave_Exits + Dungeon_Exits + Cave_Three_Exits + ['Old Man House Exit (Bottom)', 'Old Man House Exit (Top)', 'Skull Woods First Section Exit', 'Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)', - 'Kakariko Well Exit', 'Bat Cave Exit', 'North Fairy Cave Exit', 'Lost Woods Hideout Exit', 'Lumberjack Tree Exit', 'Sanctuary Exit'] - - # shuffle up holes - - hole_entrances = ['Kakariko Well Drop', 'Bat Cave Drop', 'North Fairy Cave Drop', 'Lost Woods Hideout Drop', 'Lumberjack Tree Tree', 'Sanctuary Grave', - 'Skull Woods First Section Hole (East)', 'Skull Woods First Section Hole (West)', 'Skull Woods First Section Hole (North)', 'Skull Woods Second Section Hole'] - - hole_targets = ['Kakariko Well (top)', 'Bat Cave (right)', 'North Fairy Cave', 'Lost Woods Hideout (top)', 'Lumberjack Tree (top)', 'Sewer Drop', 'Skull Back Drop', - 'Skull Left Drop', 'Skull Pinball', 'Skull Pot Circle'] - - # tavern back door cannot be shuffled yet - connect_doors(world, ['Tavern North'], ['Tavern'], player) - - if world.mode[player] == 'standard': - # cannot move uncle cave - connect_entrance(world, 'Hyrule Castle Secret Entrance Drop', 'Hyrule Castle Secret Entrance', player) - connect_exit(world, 'Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance Stairs', player) - connect_entrance(world, 'Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Secret Entrance Exit', player) - else: - hole_entrances.append('Hyrule Castle Secret Entrance Drop') - hole_targets.append('Hyrule Castle Secret Entrance') - doors.append('Hyrule Castle Secret Entrance Stairs') - caves.append('Hyrule Castle Secret Entrance Exit') - - if not world.shuffle_ganon: - connect_two_way(world, 'Ganons Tower', 'Ganons Tower Exit', player) - if not invFlag: - connect_two_way(world, 'Pyramid Entrance', 'Pyramid Exit', player) - connect_entrance(world, 'Pyramid Hole', 'Pyramid', player) - else: - connect_two_way(world, 'Inverted Pyramid Entrance', 'Pyramid Exit', player) - connect_entrance(world, 'Inverted Pyramid Hole', 'Pyramid', player) - else: - - caves.extend(['Ganons Tower Exit', 'Pyramid Exit']) - hole_targets.append('Pyramid') - if not invFlag: - hole_entrances.append('Pyramid Hole') - doors.extend(['Agahnims Tower', 'Pyramid Entrance']) - exit_pool.extend(['Agahnims Tower', 'Pyramid Entrance']) - else: - hole_entrances.append('Inverted Pyramid Hole') - doors.extend(['Agahnims Tower', 'Inverted Pyramid Entrance']) - exit_pool.extend(['Agahnims Tower', 'Inverted Pyramid Entrance']) - - random.shuffle(hole_entrances) - random.shuffle(hole_targets) - random.shuffle(exit_pool) - - # fill up holes - for hole in hole_entrances: - connect_entrance(world, hole, hole_targets.pop(), player) - - # hyrule castle handling - if world.mode[player] == 'standard': - # must connect front of hyrule castle to do escape - connect_entrance(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) - connect_exit(world, 'Hyrule Castle Exit (South)', 'Hyrule Castle Entrance (South)', player) - caves.append(('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')) - else: - doors.append('Hyrule Castle Entrance (South)') - caves.append(('Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')) - - if not world.shufflelinks[player]: - if not invFlag: - links_house = 'Links House' - else: - links_house = 'Big Shop Shop' - else: - if not invFlag: - links_house_doors = [i for i in doors if i not in Sanctuary_Doors + Isolated_LH_Doors] - else: - links_house_doors = [i for i in doors if i not in Inverted_Dark_Sanctuary_Doors + Isolated_LH_Doors] - links_house = random.choice(links_house_doors) - connect_two_way(world, links_house, 'Links House Exit', player) - connect_exit(world, 'Chris Houlihan Room Exit', links_house, player) # should always match link's house, except for plandos - doors.remove(links_house) - exit_pool.remove(links_house) - - if not invFlag: - sanc_doors = [door for door in exit_pool] #[door for door in Sanctuary_Doors if door in exit_pool] - else: - sanc_doors = [door for door in Inverted_Dark_Sanctuary_Doors if door in exit_pool] - sanc_door = random.choice(sanc_doors) - exit_pool.remove(sanc_door) - doors.remove(sanc_door) - connect_entrance(world, sanc_door, 'Dark Sanctuary Hint', player) - world.get_entrance('Dark Sanctuary Hint Exit', player).connect(world.get_entrance(sanc_door, player).parent_region) - - # now let's deal with mandatory reachable stuff - def extract_reachable_exit(cavelist): - random.shuffle(cavelist) - candidate = None - for cave in cavelist: - if isinstance(cave, tuple) and len(cave) > 1: - # special handling: TRock has two entries that we should consider entrance only - # ToDo this should be handled in a more sensible manner - if cave[0] in ['Turtle Rock Exit (Front)', 'Spectacle Rock Cave Exit (Peak)'] and len(cave) == 2: - continue - candidate = cave - break - if candidate is None: - raise RuntimeError('No suitable cave.') - cavelist.remove(candidate) - return candidate - - def connect_reachable_exit(entrance, caves, doors, exit_pool): - cave = extract_reachable_exit(caves) - - exit = cave[-1] - cave = cave[:-1] - connect_exit(world, exit, entrance, player) - connect_entrance(world, doors.pop(), exit, player) - # rest of cave now is forced to be in this world - exit_pool.remove(entrance) - caves.append(cave) - - # connect mandatory exits - for entrance in entrances_must_exits: - connect_reachable_exit(entrance, caves, doors, exit_pool) - - # place old man, has limited options - # exit has to come from specific set of doors, the entrance is free to move about - old_man_entrances = [entrance for entrance in old_man_entrances if entrance in exit_pool] - random.shuffle(old_man_entrances) - old_man_exit = old_man_entrances.pop() - exit_pool.remove(old_man_exit) - - connect_exit(world, 'Old Man Cave Exit (East)', old_man_exit, player) - connect_entrance(world, doors.pop(), 'Old Man Cave Exit (East)', player) - caves.append('Old Man Cave Exit (West)') - - # handle simple doors - single_doors = list(Single_Cave_Doors) - bomb_shop_doors = list(Bomb_Shop_Single_Cave_Doors) - blacksmith_doors = list(Blacksmith_Single_Cave_Doors) - door_targets = list(Single_Cave_Targets) - - # place blacksmith, has limited options - random.shuffle(blacksmith_doors) - blacksmith_hut = blacksmith_doors.pop() - connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player) - doors.remove(blacksmith_hut) - - # place dam and pyramid fairy, have limited options - random.shuffle(bomb_shop_doors) - bomb_shop = bomb_shop_doors.pop() - connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) - doors.remove(bomb_shop) - - # handle remaining caves - for cave in caves: - if isinstance(cave, str): - cave = (cave,) - - for exit in cave: - connect_exit(world, exit, exit_pool.pop(), player) - connect_entrance(world, doors.pop(), exit, player) - - # place remaining doors - connect_doors(world, single_doors, door_targets, player) else: raise NotImplementedError('Shuffling not supported yet') @@ -1514,7 +943,7 @@ def link_entrances(world, player): world.powder_patch_required[player] = True # check for ganon location - if world.get_entrance('Pyramid Hole' if not invFlag else 'Inverted Pyramid Hole', player).connected_region.name != 'Pyramid': + if world.get_entrance('Pyramid Hole' if invFlag == (0x03 in world.owswaps[player][0] and world.owMixed[player]) else 'Inverted Pyramid Hole', player).connected_region.name != 'Pyramid': world.ganon_at_pyramid[player] = False # check for Ganon's Tower location @@ -1600,12 +1029,6 @@ def scramble_holes(world, player): ('Lost Woods Hideout Exit', 'Lost Woods Hideout (top)'), ('Lumberjack Tree Exit', 'Lumberjack Tree (top)')] - if not world.shuffle_ganon: - connect_two_way(world, 'Pyramid Entrance', 'Pyramid Exit', player) - connect_entrance(world, 'Pyramid Hole', 'Pyramid', player) - else: - hole_targets.append(('Pyramid Exit', 'Pyramid')) - if world.mode[player] == 'standard': # cannot move uncle cave connect_two_way(world, 'Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Secret Entrance Exit', player) @@ -1617,56 +1040,31 @@ def scramble_holes(world, player): # do not shuffle sanctuary into pyramid hole unless shuffle is crossed if world.shuffle[player] == 'crossed': hole_targets.append(('Sanctuary Exit', 'Sewer Drop')) - if world.shuffle_ganon: - random.shuffle(hole_targets) - exit, target = hole_targets.pop() - connect_two_way(world, 'Pyramid Entrance', exit, player) - connect_entrance(world, 'Pyramid Hole', target, player) - if world.shuffle[player] != 'crossed': - hole_targets.append(('Sanctuary Exit', 'Sewer Drop')) - - random.shuffle(hole_targets) - for entrance, drop in hole_entrances: - exit, target = hole_targets.pop() - connect_two_way(world, entrance, exit, player) - connect_entrance(world, drop, target, player) - - -def scramble_inverted_holes(world, player): - hole_entrances = [('Kakariko Well Cave', 'Kakariko Well Drop'), - ('Bat Cave Cave', 'Bat Cave Drop'), - ('North Fairy Cave', 'North Fairy Cave Drop'), - ('Lost Woods Hideout Stump', 'Lost Woods Hideout Drop'), - ('Lumberjack Tree Cave', 'Lumberjack Tree Tree'), - ('Sanctuary', 'Sanctuary Grave')] - - hole_targets = [('Kakariko Well Exit', 'Kakariko Well (top)'), - ('Bat Cave Exit', 'Bat Cave (right)'), - ('North Fairy Cave Exit', 'North Fairy Cave'), - ('Lost Woods Hideout Exit', 'Lost Woods Hideout (top)'), - ('Lumberjack Tree Exit', 'Lumberjack Tree (top)')] + # determine pyramid hole if not world.shuffle_ganon: - connect_two_way(world, 'Inverted Pyramid Entrance', 'Pyramid Exit', player) - connect_entrance(world, 'Inverted Pyramid Hole', 'Pyramid', player) + if (world.mode[player] == 'inverted') == (0x03 in world.owswaps[player][0] and world.owMixed[player]): + connect_two_way(world, 'Pyramid Entrance', 'Pyramid Exit', player) + connect_entrance(world, 'Pyramid Hole', 'Pyramid', player) + else: + connect_two_way(world, 'Inverted Pyramid Entrance', 'Pyramid Exit', player) + connect_entrance(world, 'Inverted Pyramid Hole', 'Pyramid', player) else: hole_targets.append(('Pyramid Exit', 'Pyramid')) - - hole_entrances.append(('Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Secret Entrance Drop')) - hole_targets.append(('Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance')) - - # do not shuffle sanctuary into pyramid hole unless shuffle is crossed - if world.shuffle[player] == 'crossed': - hole_targets.append(('Sanctuary Exit', 'Sewer Drop')) - if world.shuffle_ganon: random.shuffle(hole_targets) exit, target = hole_targets.pop() - connect_two_way(world, 'Inverted Pyramid Entrance', exit, player) - connect_entrance(world, 'Inverted Pyramid Hole', target, player) + if (world.mode[player] == 'inverted') == (0x03 in world.owswaps[player][0] and world.owMixed[player]): + connect_two_way(world, 'Pyramid Entrance', exit, player) + connect_entrance(world, 'Pyramid Hole', target, player) + else: + connect_two_way(world, 'Inverted Pyramid Entrance', exit, player) + connect_entrance(world, 'Inverted Pyramid Hole', target, player) + if world.shuffle[player] != 'crossed': hole_targets.append(('Sanctuary Exit', 'Sewer Drop')) + # shuffle the rest random.shuffle(hole_targets) for entrance, drop in hole_entrances: exit, target = hole_targets.pop() @@ -1805,10 +1203,11 @@ def connect_doors(world, doors, targets, player): """This works inplace""" random.shuffle(doors) random.shuffle(targets) - while doors: - door = doors.pop() - target = targets.pop() + placing = min(len(doors), len(targets)) + for door, target in zip(doors, targets): connect_entrance(world, door, target, player) + doors[:] = doors[placing:] + targets[:] = targets[placing:] def skull_woods_shuffle(world, player): @@ -1831,6 +1230,7 @@ def simple_shuffle_dungeons(world, player): dungeon_entrances.append('Ganons Tower') dungeon_exits.append('Ganons Tower Exit') else: + # TODO: Should we be ignoring world.shuffle_ganon?? dungeon_entrances.append('Ganons Tower') dungeon_exits.append('Agahnims Tower Exit') @@ -2708,7 +2108,7 @@ mandatory_connections = [('Links House S&Q', 'Links House'), ('Hookshot Cave Middle to Back', 'Hookshot Cave (Back)'), ('Hookshot Cave Back to Middle', 'Hookshot Cave (Middle)'), ('Ganon Drop', 'Bottom of Pyramid') - ] + ] open_mandatory_connections = [('Sanctuary S&Q', 'Sanctuary'), ('Other World S&Q', 'Pyramid Area')] @@ -2718,40 +2118,13 @@ inverted_mandatory_connections = [('Sanctuary S&Q', 'Dark Sanctuary Hint'), ('Dark Sanctuary Hint Exit', 'Dark Chapel Area')] # non-shuffled entrance links -default_connections = [('Waterfall of Wishing', 'Waterfall of Wishing'), - ("Blinds Hideout", "Blinds Hideout"), - ('Dam', 'Dam'), - ('Lumberjack House', 'Lumberjack House'), - ('Hyrule Castle Secret Entrance Drop', 'Hyrule Castle Secret Entrance'), - ('Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Secret Entrance'), - ('Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Courtyard Northeast'), +default_connections = [('Lumberjack House', 'Lumberjack House'), ('Bonk Fairy (Light)', 'Bonk Fairy (Light)'), ('Lake Hylia Fairy', 'Lake Hylia Healer Fairy'), ('Lake Hylia Fortune Teller', 'Lake Hylia Fortune Teller'), ('Light Hype Fairy', 'Swamp Healer Fairy'), ('Desert Fairy', 'Desert Healer Fairy'), - ('Kings Grave', 'Kings Grave'), ('Tavern North', 'Tavern'), - ('Chicken House', 'Chicken House'), - ('Aginahs Cave', 'Aginahs Cave'), - ('Sahasrahlas Hut', 'Sahasrahlas Hut'), - ('Cave Shop (Lake Hylia)', 'Cave Shop (Lake Hylia)'), - ('Capacity Upgrade', 'Capacity Upgrade'), - ('Kakariko Well Drop', 'Kakariko Well (top)'), - ('Kakariko Well Cave', 'Kakariko Well (bottom)'), - ('Kakariko Well Exit', 'Kakariko Area'), - ('Blacksmiths Hut', 'Blacksmiths Hut'), - ('Bat Cave Drop', 'Bat Cave (right)'), - ('Bat Cave Cave', 'Bat Cave (left)'), - ('Bat Cave Exit', 'Blacksmith Area'), - ('Sick Kids House', 'Sick Kids House'), - ('Elder House (East)', 'Elder House'), - ('Elder House (West)', 'Elder House'), - ('Elder House Exit (East)', 'Kakariko Area'), - ('Elder House Exit (West)', 'Kakariko Area'), - ('North Fairy Cave Drop', 'North Fairy Cave'), - ('North Fairy Cave', 'North Fairy Cave'), - ('North Fairy Cave Exit', 'River Bend Area'), ('Lost Woods Gamble', 'Lost Woods Gamble'), ('Fortune Teller (Light)', 'Fortune Teller (Light)'), ('Snitch Lady (East)', 'Snitch Lady (East)'), @@ -2759,112 +2132,148 @@ default_connections = [('Waterfall of Wishing', 'Waterfall of Wishing'), ('Bush Covered House', 'Bush Covered House'), ('Tavern (Front)', 'Tavern (Front)'), ('Light World Bomb Hut', 'Light World Bomb Hut'), - ('Kakariko Shop', 'Kakariko Shop'), - ('Lost Woods Hideout Drop', 'Lost Woods Hideout (top)'), - ('Lost Woods Hideout Stump', 'Lost Woods Hideout (bottom)'), - ('Lost Woods Hideout Exit', 'Lost Woods East Area'), - ('Lumberjack Tree Tree', 'Lumberjack Tree (top)'), - ('Lumberjack Tree Cave', 'Lumberjack Tree (bottom)'), - ('Lumberjack Tree Exit', 'Lumberjack Area'), - ('Cave 45', 'Cave 45'), - ('Graveyard Cave', 'Graveyard Cave'), - ('Checkerboard Cave', 'Checkerboard Cave'), - ('Mini Moldorm Cave', 'Mini Moldorm Cave'), ('Long Fairy Cave', 'Long Fairy Cave'), # near East Light World Teleporter ('Good Bee Cave', 'Good Bee Cave'), ('20 Rupee Cave', '20 Rupee Cave'), ('50 Rupee Cave', '50 Rupee Cave'), - ('Ice Rod Cave', 'Ice Rod Cave'), - ('Bonk Rock Cave', 'Bonk Rock Cave'), - ('Library', 'Library'), ('Kakariko Gamble Game', 'Kakariko Gamble Game'), - ('Potion Shop', 'Potion Shop'), - ('Two Brothers House (East)', 'Two Brothers House'), - ('Two Brothers House (West)', 'Two Brothers House'), - ('Two Brothers House Exit (East)', 'Kakariko Suburb Area'), - ('Two Brothers House Exit (West)', 'Maze Race Ledge'), - - ('Sanctuary', 'Sanctuary Portal'), - ('Sanctuary Grave', 'Sewer Drop'), - ('Sanctuary Exit', 'Sanctuary Area'), - - ('Old Man Cave (West)', 'Old Man Cave Ledge'), - ('Old Man Cave (East)', 'Old Man Cave'), - ('Old Man Cave Exit (West)', 'Mountain Entry Entrance'), - ('Old Man Cave Exit (East)', 'West Death Mountain (Bottom)'), - ('Old Man House (Bottom)', 'Old Man House'), - ('Old Man House Exit (Bottom)', 'West Death Mountain (Bottom)'), - ('Old Man House (Top)', 'Old Man House Back'), - ('Old Man House Exit (Top)', 'West Death Mountain (Bottom)'), - ('Death Mountain Return Cave (East)', 'Death Mountain Return Cave'), - ('Death Mountain Return Cave (West)', 'Death Mountain Return Cave'), - ('Death Mountain Return Cave Exit (West)', 'Mountain Entry Ledge'), - ('Death Mountain Return Cave Exit (East)', 'West Death Mountain (Bottom)'), - ('Spectacle Rock Cave Peak', 'Spectacle Rock Cave (Peak)'), - ('Spectacle Rock Cave (Bottom)', 'Spectacle Rock Cave (Bottom)'), - ('Spectacle Rock Cave', 'Spectacle Rock Cave (Top)'), - ('Spectacle Rock Cave Exit', 'West Death Mountain (Bottom)'), - ('Spectacle Rock Cave Exit (Top)', 'West Death Mountain (Bottom)'), - ('Spectacle Rock Cave Exit (Peak)', 'West Death Mountain (Bottom)'), - ('Paradox Cave (Bottom)', 'Paradox Cave Front'), - ('Paradox Cave (Middle)', 'Paradox Cave'), - ('Paradox Cave (Top)', 'Paradox Cave'), - ('Paradox Cave Exit (Bottom)', 'East Death Mountain (Bottom)'), - ('Paradox Cave Exit (Middle)', 'East Death Mountain (Bottom)'), - ('Paradox Cave Exit (Top)', 'East Death Mountain (Top East)'), ('Hookshot Fairy', 'Hookshot Fairy'), - ('Fairy Ascension Cave (Bottom)', 'Fairy Ascension Cave (Bottom)'), - ('Fairy Ascension Cave (Top)', 'Fairy Ascension Cave (Top)'), - ('Fairy Ascension Cave Exit (Bottom)', 'Fairy Ascension Plateau'), - ('Fairy Ascension Cave Exit (Top)', 'Fairy Ascension Ledge'), - ('Spiral Cave', 'Spiral Cave (Top)'), - ('Spiral Cave (Bottom)', 'Spiral Cave (Bottom)'), - ('Spiral Cave Exit', 'East Death Mountain (Bottom)'), - ('Spiral Cave Exit (Top)', 'Spiral Cave Ledge'), - - ('Pyramid Fairy', 'Pyramid Fairy'), + ('East Dark World Hint', 'East Dark World Hint'), ('Palace of Darkness Hint', 'Palace of Darkness Hint'), - ('Dark Lake Hylia Shop', 'Dark Lake Hylia Shop'), ('Dark Lake Hylia Fairy', 'Dark Lake Hylia Healer Fairy'), ('Dark Lake Hylia Ledge Fairy', 'Dark Lake Hylia Ledge Healer Fairy'), ('Dark Lake Hylia Ledge Spike Cave', 'Dark Lake Hylia Ledge Spike Cave'), ('Dark Lake Hylia Ledge Hint', 'Dark Lake Hylia Ledge Hint'), - ('Hype Cave', 'Hype Cave'), ('Bonk Fairy (Dark)', 'Bonk Fairy (Dark)'), - ('Brewery', 'Brewery'), - ('C-Shaped House', 'C-Shaped House'), - ('Chest Game', 'Chest Game'), - ('Dark World Hammer Peg Cave', 'Dark World Hammer Peg Cave'), - ('Bumper Cave (Bottom)', 'Bumper Cave'), - ('Bumper Cave (Top)', 'Bumper Cave'), - ('Red Shield Shop', 'Red Shield Shop'), ('Dark Sanctuary Hint', 'Dark Sanctuary Hint'), ('Fortune Teller (Dark)', 'Fortune Teller (Dark)'), - ('Dark World Shop', 'Village of Outcasts Shop'), - ('Dark World Lumberjack Shop', 'Dark World Lumberjack Shop'), - ('Dark World Potion Shop', 'Dark World Potion Shop'), ('Archery Game', 'Archery Game'), - ('Bumper Cave Exit (Top)', 'Bumper Cave Ledge'), - ('Bumper Cave Exit (Bottom)', 'Bumper Cave Entrance'), - ('Mire Shed', 'Mire Shed'), ('Dark Desert Hint', 'Dark Desert Hint'), ('Dark Desert Fairy', 'Dark Desert Healer Fairy'), - ('Spike Cave', 'Spike Cave'), - ('Hookshot Cave', 'Hookshot Cave (Front)'), - ('Superbunny Cave (Top)', 'Superbunny Cave (Top)'), - ('Cave Shop (Dark Death Mountain)', 'Cave Shop (Dark Death Mountain)'), ('Dark Death Mountain Fairy', 'Dark Death Mountain Healer Fairy'), - ('Superbunny Cave (Bottom)', 'Superbunny Cave (Bottom)'), - ('Superbunny Cave Exit (Top)', 'East Dark Death Mountain (Top)'), - ('Superbunny Cave Exit (Bottom)', 'East Dark Death Mountain (Bottom)'), - ('Hookshot Cave Front Exit', 'East Dark Death Mountain (Top)'), - ('Hookshot Cave Back Exit', 'Dark Death Mountain Floating Island'), - ('Hookshot Cave Back Entrance', 'Hookshot Cave (Back)'), - ('Mimic Cave', 'Mimic Cave'), + ] - ('Pyramid Exit', 'Pyramid Exit Ledge') - ] +default_connector_connections = [('Old Man Cave (West)', 'Old Man Cave Ledge'), + ('Old Man Cave (East)', 'Old Man Cave'), + ('Old Man Cave Exit (West)', 'Mountain Entry Entrance'), + ('Old Man Cave Exit (East)', 'West Death Mountain (Bottom)'), + ('Old Man House (Bottom)', 'Old Man House'), + ('Old Man House Exit (Bottom)', 'West Death Mountain (Bottom)'), + ('Old Man House (Top)', 'Old Man House Back'), + ('Old Man House Exit (Top)', 'West Death Mountain (Bottom)'), + ('Death Mountain Return Cave (East)', 'Death Mountain Return Cave'), + ('Death Mountain Return Cave (West)', 'Death Mountain Return Cave'), + ('Death Mountain Return Cave Exit (West)', 'Mountain Entry Ledge'), + ('Death Mountain Return Cave Exit (East)', 'West Death Mountain (Bottom)'), + ('Spectacle Rock Cave Peak', 'Spectacle Rock Cave (Peak)'), + ('Spectacle Rock Cave (Bottom)', 'Spectacle Rock Cave (Bottom)'), + ('Spectacle Rock Cave', 'Spectacle Rock Cave (Top)'), + ('Spectacle Rock Cave Exit', 'West Death Mountain (Bottom)'), + ('Spectacle Rock Cave Exit (Top)', 'West Death Mountain (Bottom)'), + ('Spectacle Rock Cave Exit (Peak)', 'West Death Mountain (Bottom)'), + ('Spiral Cave', 'Spiral Cave (Top)'), + ('Spiral Cave (Bottom)', 'Spiral Cave (Bottom)'), + ('Spiral Cave Exit', 'East Death Mountain (Bottom)'), + ('Spiral Cave Exit (Top)', 'Spiral Cave Ledge'), + ('Fairy Ascension Cave (Bottom)', 'Fairy Ascension Cave (Bottom)'), + ('Fairy Ascension Cave (Top)', 'Fairy Ascension Cave (Top)'), + ('Fairy Ascension Cave Exit (Bottom)', 'Fairy Ascension Plateau'), + ('Fairy Ascension Cave Exit (Top)', 'Fairy Ascension Ledge'), + ('Paradox Cave (Bottom)', 'Paradox Cave Front'), + ('Paradox Cave (Middle)', 'Paradox Cave'), + ('Paradox Cave (Top)', 'Paradox Cave'), + ('Paradox Cave Exit (Bottom)', 'East Death Mountain (Bottom)'), + ('Paradox Cave Exit (Middle)', 'East Death Mountain (Bottom)'), + ('Paradox Cave Exit (Top)', 'East Death Mountain (Top East)'), + ('Elder House (East)', 'Elder House'), + ('Elder House (West)', 'Elder House'), + ('Elder House Exit (East)', 'Kakariko Area'), + ('Elder House Exit (West)', 'Kakariko Area'), + ('Two Brothers House (East)', 'Two Brothers House'), + ('Two Brothers House (West)', 'Two Brothers House'), + ('Two Brothers House Exit (East)', 'Kakariko Suburb Area'), + ('Two Brothers House Exit (West)', 'Maze Race Ledge'), + ('Bumper Cave (Bottom)', 'Bumper Cave'), + ('Bumper Cave (Top)', 'Bumper Cave'), + ('Bumper Cave Exit (Top)', 'Bumper Cave Ledge'), + ('Bumper Cave Exit (Bottom)', 'Bumper Cave Entrance'), + ('Superbunny Cave (Top)', 'Superbunny Cave (Top)'), + ('Superbunny Cave (Bottom)', 'Superbunny Cave (Bottom)'), + ('Superbunny Cave Exit (Top)', 'East Dark Death Mountain (Top)'), + ('Superbunny Cave Exit (Bottom)', 'East Dark Death Mountain (Bottom)'), + ('Hookshot Cave', 'Hookshot Cave (Front)'), + ('Hookshot Cave Back Entrance', 'Hookshot Cave (Back)'), + ('Hookshot Cave Front Exit', 'East Dark Death Mountain (Top)'), + ('Hookshot Cave Back Exit', 'Dark Death Mountain Floating Island') + ] + +default_item_connections = [('Mimic Cave', 'Mimic Cave'), + ('Waterfall of Wishing', 'Waterfall of Wishing'), + ('Bonk Rock Cave', 'Bonk Rock Cave'), + ('Graveyard Cave', 'Graveyard Cave'), + ('Kings Grave', 'Kings Grave'), + ('Potion Shop', 'Potion Shop'), + ('Blinds Hideout', 'Blinds Hideout'), + ('Chicken House', 'Chicken House'), + ('Sick Kids House', 'Sick Kids House'), + ('Sahasrahlas Hut', 'Sahasrahlas Hut'), + ('Blacksmiths Hut', 'Blacksmiths Hut'), + ('Library', 'Library'), + ('Checkerboard Cave', 'Checkerboard Cave'), + ('Aginahs Cave', 'Aginahs Cave'), + ('Cave 45', 'Cave 45'), + ('Mini Moldorm Cave', 'Mini Moldorm Cave'), + ('Ice Rod Cave', 'Ice Rod Cave'), + ('Dam', 'Dam'), + ('Spike Cave', 'Spike Cave'), + ('Chest Game', 'Chest Game'), + ('C-Shaped House', 'C-Shaped House'), + ('Brewery', 'Brewery'), + ('Pyramid Fairy', 'Pyramid Fairy'), + ('Dark World Hammer Peg Cave', 'Dark World Hammer Peg Cave'), + ('Mire Shed', 'Mire Shed'), + ('Hype Cave', 'Hype Cave') + ] + +default_shop_connections = [('Kakariko Shop', 'Kakariko Shop'), + ('Cave Shop (Lake Hylia)', 'Cave Shop (Lake Hylia)'), + ('Capacity Upgrade', 'Capacity Upgrade'), + ('Dark World Lumberjack Shop', 'Dark World Lumberjack Shop'), + ('Cave Shop (Dark Death Mountain)', 'Cave Shop (Dark Death Mountain)'), + ('Dark World Potion Shop', 'Dark World Potion Shop'), + ('Dark World Shop', 'Village of Outcasts Shop'), + ('Red Shield Shop', 'Red Shield Shop'), + ('Dark Lake Hylia Shop', 'Dark Lake Hylia Shop') + ] + +default_drop_connections = [('Lost Woods Hideout Drop', 'Lost Woods Hideout (top)'), + ('Lost Woods Hideout Stump', 'Lost Woods Hideout (bottom)'), + ('Lost Woods Hideout Exit', 'Lost Woods East Area'), + ('Lumberjack Tree Tree', 'Lumberjack Tree (top)'), + ('Lumberjack Tree Cave', 'Lumberjack Tree (bottom)'), + ('Lumberjack Tree Exit', 'Lumberjack Area'), + ('Sanctuary', 'Sanctuary Portal'), + ('Sanctuary Grave', 'Sewer Drop'), + ('Sanctuary Exit', 'Sanctuary Area'), + ('North Fairy Cave Drop', 'North Fairy Cave'), + ('North Fairy Cave', 'North Fairy Cave'), + ('North Fairy Cave Exit', 'River Bend Area'), + ('Kakariko Well Drop', 'Kakariko Well (top)'), + ('Kakariko Well Cave', 'Kakariko Well (bottom)'), + ('Kakariko Well Exit', 'Kakariko Area'), + ('Hyrule Castle Secret Entrance Drop', 'Hyrule Castle Secret Entrance'), + ('Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Secret Entrance'), + ('Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Courtyard Northeast'), + ('Bat Cave Drop', 'Bat Cave (right)'), + ('Bat Cave Cave', 'Bat Cave (left)'), + ('Bat Cave Exit', 'Blacksmith Area'), + ('Pyramid Hole', 'Pyramid'), + ('Pyramid Entrance', 'Bottom of Pyramid'), + ('Inverted Pyramid Hole', 'Pyramid'), + ('Inverted Pyramid Entrance', 'Bottom of Pyramid'), + ('Pyramid Exit', 'Pyramid Exit Ledge') + ] swapped_connections = { 0x03: [ @@ -2883,18 +2292,14 @@ swapped_connections = { ] } -open_default_connections = [('Links House', 'Links House'), +open_default_connections = [('Links House', 'Links House'), ('Links House Exit', 'Links House Area'), - ('Big Bomb Shop', 'Big Bomb Shop'), - ('Pyramid Hole', 'Pyramid'), - ('Pyramid Entrance', 'Bottom of Pyramid') - ] + ('Big Bomb Shop', 'Big Bomb Shop') + ] -inverted_default_connections = [('Links House', 'Big Bomb Shop'), - ('Links House Exit', 'Big Bomb Shop Area'), - ('Big Bomb Shop', 'Links House'), - ('Inverted Pyramid Hole', 'Pyramid'), - ('Inverted Pyramid Entrance', 'Bottom of Pyramid') +inverted_default_connections = [('Links House', 'Big Bomb Shop'), + ('Links House Exit', 'Big Bomb Shop Area'), + ('Big Bomb Shop', 'Links House') ] # non shuffled dungeons @@ -2950,29 +2355,40 @@ default_dungeon_connections = [('Desert Palace Entrance (South)', 'Desert South ('Dark Death Mountain Ledge (East)', 'Turtle Rock Chest Portal'), ('Turtle Rock Isolated Ledge Exit', 'Dark Death Mountain Isolated Ledge'), ('Turtle Rock Isolated Ledge Entrance', 'Turtle Rock Eye Bridge Portal') - ] + ] open_default_dungeon_connections = [('Ganons Tower', 'Ganons Tower Portal'), ('Ganons Tower Exit', 'West Dark Death Mountain (Top)'), ('Agahnims Tower', 'Agahnims Tower Portal'), ('Agahnims Tower Exit', 'Hyrule Castle Ledge') - ] + ] inverted_default_dungeon_connections = [('Ganons Tower', 'Agahnims Tower Portal'), ('Ganons Tower Exit', 'Hyrule Castle Ledge'), ('Agahnims Tower', 'Ganons Tower Portal'), ('Agahnims Tower Exit', 'West Dark Death Mountain (Top)') - ] + ] indirect_connections = { 'Turtle Rock Ledge': 'Turtle Rock', - 'Pyramid Area': 'Pyramid Fairy', - 'East Dark World': 'Pyramid Fairy', 'Big Bomb Shop': 'Pyramid Fairy', - 'Dark Desert': 'Pyramid Fairy', - 'West Dark World': 'Pyramid Fairy', - 'South Dark World': 'Pyramid Fairy', - 'Light World': 'Pyramid Fairy', + #'East Dark World': 'Pyramid Fairy', + 'Pyramid Area': 'Pyramid Fairy', # HC Ledge/Courtyard + #'Dark Desert': 'Pyramid Fairy', + 'Misery Mire Area': 'Pyramid Fairy', # Desert/Checkerboard Ledge + #'West Dark World': 'Pyramid Fairy', + 'Dark Chapel Area': 'Pyramid Fairy', # Bonk Rocks + 'Dark Graveyard North': 'Pyramid Fairy', # Graveyard Ledge/Kings Tomb + #'South Dark World': 'Pyramid Fairy', + 'Dig Game Ledge': 'Pyramid Fairy', # Brother House Left + 'Stumpy Approach Area': 'Pyramid Fairy', # Cave 45 + # Inverted Cases + #'Light World': 'Pyramid Fairy', + 'Lost Woods West Area': 'Pyramid Fairy', # Skull Woods Back + 'East Death Mountain (Top East)': 'Pyramid Fairy', # Floating Island + 'Blacksmith Area': 'Pyramid Fairy', # Hammerpegs + 'Forgotten Forest Area': 'Pyramid Fairy', # Shield Shop + 'Desert Area': 'Pyramid Fairy', # Mire Area 'Old Man Cave': 'Old Man S&Q' } # format: diff --git a/Fill.py b/Fill.py index 3e046600..015760ca 100644 --- a/Fill.py +++ b/Fill.py @@ -1,4 +1,6 @@ import RaceRandom as random +import collections +import itertools import logging from BaseClasses import CollectionState @@ -211,7 +213,7 @@ def fill_restrictive(world, base_state, locations, itempool, keys_in_itempool = and valid_key_placement(item_to_place, location, itempool if (keys_in_itempool and keys_in_itempool[item_to_place.player]) else world.itempool, world): spot_to_fill = location break - elif item_to_place.smallkey or item_to_place.bigkey: + if item_to_place.smallkey or item_to_place.bigkey: location.item = None if spot_to_fill is None: @@ -221,7 +223,10 @@ def fill_restrictive(world, base_state, locations, itempool, keys_in_itempool = if world.accessibility[item_to_place.player] != 'none': logging.getLogger('').warning('Not all items placed. Game beatable anyway. (Could not place %s)' % item_to_place) continue - raise FillError('No more spots to place %s' % item_to_place) + spot_to_fill = last_ditch_placement(item_to_place, locations, world, maximum_exploration_state, + base_state, itempool, keys_in_itempool, single_player_placement) + if spot_to_fill is None: + raise FillError('No more spots to place %s' % item_to_place) world.push_item(spot_to_fill, item_to_place, False) track_outside_keys(item_to_place, spot_to_fill, world) @@ -250,9 +255,7 @@ def valid_key_placement(item, location, itempool, world): def track_outside_keys(item, location, world): if not item.smallkey: return - item_dungeon = item.name.split('(')[1][:-1] - if item_dungeon == 'Escape': - item_dungeon = 'Hyrule Castle' + item_dungeon = item.dungeon if location.player == item.player: loc_dungeon = location.parent_region.dungeon if loc_dungeon and loc_dungeon.name == item_dungeon: @@ -260,6 +263,80 @@ def track_outside_keys(item, location, world): world.key_logic[item.player][item_dungeon].outside_keys += 1 +def last_ditch_placement(item_to_place, locations, world, state, base_state, itempool, + keys_in_itempool=None, single_player_placement=False): + def location_preference(loc): + if not loc.item.advancement: + return 1 + if loc.item.type and loc.item.type != 'Sword': + if loc.item.type in ['Map', 'Compass']: + return 2 + else: + return 3 + return 4 + + if item_to_place.type == 'Crystal': + possible_swaps = [x for x in state.locations_checked if x.item.type == 'Crystal'] + else: + possible_swaps = [x for x in state.locations_checked + if x.item.type not in ['Event', 'Crystal'] and not x.forced_item] + swap_locations = sorted(possible_swaps, key=location_preference) + + for location in swap_locations: + old_item = location.item + new_pool = list(itempool) + [old_item] + new_spot = find_spot_for_item(item_to_place, [location], world, base_state, new_pool, + keys_in_itempool, single_player_placement) + if new_spot: + restore_item = new_spot.item + new_spot.item = item_to_place + swap_spot = find_spot_for_item(old_item, locations, world, base_state, itempool, + keys_in_itempool, single_player_placement) + if swap_spot: + logging.getLogger('').debug(f'Swapping {old_item} for {item_to_place}') + world.push_item(swap_spot, old_item, False) + swap_spot.event = True + locations.remove(swap_spot) + locations.append(new_spot) + return new_spot + else: + new_spot.item = restore_item + else: + location.item = old_item + return None + + +def find_spot_for_item(item_to_place, locations, world, base_state, pool, + keys_in_itempool=None, single_player_placement=False): + def sweep_from_pool(): + new_state = base_state.copy() + for item in pool: + new_state.collect(item, True) + new_state.sweep_for_events() + return new_state + for location in locations: + maximum_exploration_state = sweep_from_pool() + perform_access_check = True + old_item = None + if world.accessibility[item_to_place.player] == 'none': + perform_access_check = not world.has_beaten_game(maximum_exploration_state, item_to_place.player) if single_player_placement else not world.has_beaten_game(maximum_exploration_state) + + if item_to_place.smallkey or item_to_place.bigkey: # a better test to see if a key can go there + old_item = location.item + location.item = item_to_place + test_state = maximum_exploration_state.copy() + test_state.stale[item_to_place.player] = True + else: + test_state = maximum_exploration_state + if (not single_player_placement or location.player == item_to_place.player) \ + and location.can_fill(test_state, item_to_place, perform_access_check) \ + and valid_key_placement(item_to_place, location, pool if (keys_in_itempool and keys_in_itempool[item_to_place.player]) else world.itempool, world): + return location + if item_to_place.smallkey or item_to_place.bigkey: + location.item = old_item + return None + + def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None): # If not passed in, then get a shuffled list of locations to fill in if not fill_locations: @@ -420,9 +497,8 @@ def sell_keys(world, player): def balance_multiworld_progression(world): state = CollectionState(world) - checked_locations = [] - unchecked_locations = world.get_locations().copy() - random.shuffle(unchecked_locations) + checked_locations = set() + unchecked_locations = set(world.get_locations()) reachable_locations_count = {} for player in range(1, world.players + 1): @@ -430,7 +506,7 @@ def balance_multiworld_progression(world): def get_sphere_locations(sphere_state, locations): sphere_state.sweep_for_events(key_only=True, locations=locations) - return [loc for loc in locations if sphere_state.can_reach(loc) and sphere_state.not_flooding_a_key(sphere_state.world, loc)] + return {loc for loc in locations if sphere_state.can_reach(loc) and sphere_state.not_flooding_a_key(sphere_state.world, loc)} while True: sphere_locations = get_sphere_locations(state, unchecked_locations) @@ -441,38 +517,42 @@ def balance_multiworld_progression(world): if checked_locations: threshold = max(reachable_locations_count.values()) - 20 - balancing_players = [player for player, reachables in reachable_locations_count.items() if reachables < threshold] - if balancing_players is not None and len(balancing_players) > 0: + balancing_players = {player for player, reachables in reachable_locations_count.items() if reachables < threshold} + if balancing_players: balancing_state = state.copy() balancing_unchecked_locations = unchecked_locations.copy() balancing_reachables = reachable_locations_count.copy() balancing_sphere = sphere_locations.copy() - candidate_items = [] + candidate_items = collections.defaultdict(set) while True: for location in balancing_sphere: if location.event and (world.keyshuffle[location.item.player] or not location.item.smallkey) and (world.bigkeyshuffle[location.item.player] or not location.item.bigkey): balancing_state.collect(location.item, True, location) - if location.item.player in balancing_players and not location.locked: - candidate_items.append(location) + player = location.item.player + if player in balancing_players and not location.locked and location.player != player: + candidate_items[player].add(location) balancing_sphere = get_sphere_locations(balancing_state, balancing_unchecked_locations) for location in balancing_sphere: balancing_unchecked_locations.remove(location) balancing_reachables[location.player] += 1 - if world.has_beaten_game(balancing_state) or all([reachables >= threshold for reachables in balancing_reachables.values()]): + if world.has_beaten_game(balancing_state) or all(reachables >= threshold for reachables in balancing_reachables.values()): break elif not balancing_sphere: raise RuntimeError('Not all required items reachable. Something went terribly wrong here.') - unlocked_locations = [l for l in unchecked_locations if l not in balancing_unchecked_locations] + unlocked_locations = collections.defaultdict(set) + for l in unchecked_locations: + if l not in balancing_unchecked_locations: + unlocked_locations[l.player].add(l) items_to_replace = [] for player in balancing_players: - locations_to_test = [l for l in unlocked_locations if l.player == player] - # only replace items that end up in another player's world - items_to_test = [l for l in candidate_items if l.item.player == player and l.player != player] + locations_to_test = unlocked_locations[player] + items_to_test = candidate_items[player] while items_to_test: testing = items_to_test.pop() reducing_state = state.copy() - for location in [*[l for l in items_to_replace if l.item.player == player], *items_to_test]: + for location in itertools.chain((l for l in items_to_replace if l.item.player == player), + items_to_test): reducing_state.collect(location.item, True, location) reducing_state.sweep_for_events(locations=locations_to_test) @@ -486,33 +566,44 @@ def balance_multiworld_progression(world): items_to_replace.append(testing) replaced_items = False - replacement_locations = [l for l in checked_locations if not l.event and not l.locked] + # sort then shuffle to maintain deterministic behaviour, + # while allowing use of set for better algorithm growth behaviour elsewhere + replacement_locations = sorted((l for l in checked_locations if not l.event and not l.locked), + key=lambda loc: (loc.name, loc.player)) + random.shuffle(replacement_locations) + items_to_replace.sort(key=lambda item: (item.name, item.player)) + random.shuffle(items_to_replace) while replacement_locations and items_to_replace: - new_location = replacement_locations.pop() old_location = items_to_replace.pop() + for new_location in replacement_locations: + if (new_location.can_fill(state, old_location.item, False) and + old_location.can_fill(state, new_location.item, False)): + replacement_locations.remove(new_location) + new_location.item, old_location.item = old_location.item, new_location.item + if world.shopsanity[new_location.player]: + check_shop_swap(new_location) + if world.shopsanity[old_location.player]: + check_shop_swap(old_location) + new_location.event, old_location.event = True, False + logging.debug(f"Progression balancing moved {new_location.item} to {new_location}, " + f"displacing {old_location.item} into {old_location}") + state.collect(new_location.item, True, new_location) + replaced_items = True + break + else: + logging.warning(f"Could not Progression Balance {old_location.item}") - while not new_location.can_fill(state, old_location.item, False) or (new_location.item and not old_location.can_fill(state, new_location.item, False)): - replacement_locations.insert(0, new_location) - new_location = replacement_locations.pop() - - new_location.item, old_location.item = old_location.item, new_location.item - if world.shopsanity[new_location.player]: - check_shop_swap(new_location) - if world.shopsanity[old_location.player]: - check_shop_swap(old_location) - new_location.event, old_location.event = True, False - state.collect(new_location.item, True, new_location) - replaced_items = True if replaced_items: - for location in get_sphere_locations(state, [l for l in unlocked_locations if l.player in balancing_players]): + unlocked = {fresh for player in balancing_players for fresh in unlocked_locations[player]} + for location in get_sphere_locations(state, unlocked): unchecked_locations.remove(location) reachable_locations_count[location.player] += 1 - sphere_locations.append(location) + sphere_locations.add(location) for location in sphere_locations: if location.event and (world.keyshuffle[location.item.player] or not location.item.smallkey) and (world.bigkeyshuffle[location.item.player] or not location.item.bigkey): state.collect(location.item, True, location) - checked_locations.extend(sphere_locations) + checked_locations |= sphere_locations if world.has_beaten_game(state): break @@ -606,7 +697,7 @@ def balance_money_progression(world): sphere_locations = get_sphere_locations(state, unchecked_locations) checked_locations = [] for player in range(1, world.players+1): - kiki_payable = state.prog_items[('Moon Pearl', player)] > 0 or world.mode[player] == 'inverted' + kiki_payable = state.prog_items[('Moon Pearl', player)] > 0 or (world.mode[player] == 'inverted') != (0x1e in world.owswaps[player][0] and world.owMixed[player]) if kiki_payable and world.get_region('Palace of Darkness Area', player) in state.reachable_regions[player]: if not kiki_paid[player]: kiki_check[player] = True @@ -651,7 +742,7 @@ def balance_money_progression(world): if room not in rooms_visited[player] and world.get_region(room, player) in state.reachable_regions[player]: wallet[player] += income rooms_visited[player].add(room) - if checked_locations: + if checked_locations or len(unchecked_locations) == 0: if world.has_beaten_game(state): done = True continue @@ -661,11 +752,11 @@ def balance_money_progression(world): solvent = set() insolvent = set() for player in range(1, world.players+1): - if wallet[player] >= sphere_costs[player] > 0: + if wallet[player] >= sphere_costs[player] >= 0: solvent.add(player) if sphere_costs[player] > 0 and sphere_costs[player] > wallet[player]: insolvent.add(player) - if len(solvent) == 0: + if len([p for p in solvent if len(locked_by_money[p]) > 0]) == 0: target_player = min(insolvent, key=lambda p: sphere_costs[p]-wallet[p]) difference = sphere_costs[target_player]-wallet[target_player] logger.debug(f'Money balancing needed: Player {target_player} short {difference}') @@ -705,7 +796,7 @@ def balance_money_progression(world): for player in solvent: wallet[player] -= sphere_costs[player] for location in locked_by_money[player]: - if location == 'Kiki': + if isinstance(location, str) and location == 'Kiki': kiki_paid[player] = True else: state.collect(location.item, True, location) diff --git a/ItemList.py b/ItemList.py index afd48f61..ae5f36a2 100644 --- a/ItemList.py +++ b/ItemList.py @@ -37,7 +37,7 @@ Difficulty = namedtuple('Difficulty', ['baseitems', 'bottles', 'bottle_count', 'same_bottle', 'progressiveshield', 'basicshield', 'progressivearmor', 'basicarmor', 'swordless', 'bombs_only', 'progressivesword', 'basicsword', 'basicbow', 'timedohko', 'timedother', - 'retro', 'bomblogic', + 'retro', 'bombbag', 'extras', 'progressive_sword_limit', 'progressive_shield_limit', 'progressive_armor_limit', 'progressive_bottle_limit', 'progressive_bow_limit', 'heart_piece_limit', 'boss_heart_container_limit']) @@ -62,7 +62,7 @@ difficulties = { timedohko = ['Green Clock'] * 25, timedother = ['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10, retro = ['Small Key (Universal)'] * 18 + ['Rupees (20)'] * 10, - bomblogic = ['Bomb Upgrade (+10)'] * 2, + bombbag = ['Bomb Upgrade (+10)'] * 2, extras = [normalfirst15extra, normalsecond15extra, normalthird10extra, normalfourth5extra, normalfinal25extra], progressive_sword_limit = 4, progressive_shield_limit = 3, @@ -89,7 +89,7 @@ difficulties = { timedohko = ['Green Clock'] * 25, timedother = ['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10, retro = ['Small Key (Universal)'] * 13 + ['Rupees (5)'] * 15, - bomblogic = ['Bomb Upgrade (+10)'] * 2, + bombbag = ['Bomb Upgrade (+10)'] * 2, extras = [normalfirst15extra, normalsecond15extra, normalthird10extra, normalfourth5extra, normalfinal25extra], progressive_sword_limit = 3, progressive_shield_limit = 2, @@ -116,7 +116,7 @@ difficulties = { timedohko = ['Green Clock'] * 20 + ['Red Clock'] * 5, timedother = ['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10, retro = ['Small Key (Universal)'] * 13 + ['Rupees (5)'] * 15, - bomblogic = ['Bomb Upgrade (+10)'] * 2, + bombbag = ['Bomb Upgrade (+10)'] * 2, extras = [normalfirst15extra, normalsecond15extra, normalthird10extra, normalfourth5extra, normalfinal25extra], progressive_sword_limit = 2, progressive_shield_limit = 1, @@ -258,10 +258,10 @@ def generate_itempool(world, player): # set up item pool if world.custom: - (pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon, lamps_needed_for_dark_rooms) = make_custom_item_pool(world.progressive, world.shuffle[player], world.difficulty[player], world.timer, world.goal[player], world.mode[player], world.swords[player], world.retro[player], world.bomblogic[player], world.customitemarray) + (pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon, lamps_needed_for_dark_rooms) = make_custom_item_pool(world.progressive, world.shuffle[player], world.difficulty[player], world.timer, world.goal[player], world.mode[player], world.swords[player], world.retro[player], world.bombbag[player], world.customitemarray) world.rupoor_cost = min(world.customitemarray[player]["rupoorcost"], 9999) else: - (pool, placed_items, precollected_items, clock_mode, lamps_needed_for_dark_rooms) = get_pool_core(world.progressive, world.owShuffle[player], world.owSwap[player], world.shuffle[player], world.difficulty[player], world.treasure_hunt_total[player], world.timer, world.goal[player], world.mode[player], world.swords[player], world.retro[player], world.bomblogic[player], world.doorShuffle[player], world.logic[player]) + (pool, placed_items, precollected_items, clock_mode, lamps_needed_for_dark_rooms) = get_pool_core(world.progressive, world.shuffle[player], world.difficulty[player], world.treasure_hunt_total[player], world.timer, world.goal[player], world.mode[player], world.swords[player], world.retro[player], world.bombbag[player], world.doorShuffle[player], world.logic[player]) if player in world.pool_adjustment.keys(): amt = world.pool_adjustment[player] @@ -300,7 +300,7 @@ def generate_itempool(world, player): if item in ['Hammer', 'Fire Rod', 'Cane of Somaria', 'Cane of Byrna']: if item not in possible_weapons: possible_weapons.append(item) - if not world.bomblogic[player] and item in ['Bombs (10)']: + if not world.bombbag[player] and item in ['Bombs (10)']: if item not in possible_weapons and world.doorShuffle[player] != 'crossed': possible_weapons.append(item) starting_weapon = random.choice(possible_weapons) @@ -331,6 +331,11 @@ def generate_itempool(world, player): p_item = next(item for item in items if item.name == potion and item.player == player) p_item.priority = True # don't beemize one of each potion + if world.bombbag[player]: + for item in items: + if item.name == 'Bomb Upgrade (+10)' and item.player == player: + item.advancement = True + world.lamps_needed_for_dark_rooms = lamps_needed_for_dark_rooms if clock_mode is not None: @@ -355,11 +360,11 @@ def generate_itempool(world, player): # rather than making all hearts/heart pieces progression items (which slows down generation considerably) # We mark one random heart container as an advancement item (or 4 heart pieces in expert mode) if world.difficulty[player] in ['normal', 'hard'] and not (world.custom and world.customitemarray[player]["heartcontainer"] == 0): - [item for item in items if item.name == 'Boss Heart Container'][0].advancement = True + next(item for item in items if item.name == 'Boss Heart Container').advancement = True elif world.difficulty[player] in ['expert'] and not (world.custom and world.customitemarray[player]["heartpiece"] < 4): - adv_heart_pieces = [item for item in items if item.name == 'Piece of Heart'][0:4] - for hp in adv_heart_pieces: - hp.advancement = True + adv_heart_pieces = (item for item in items if item.name == 'Piece of Heart') + for i in range(4): + next(adv_heart_pieces).advancement = True beeweights = {'0': {None: 100}, '1': {None: 75, 'trap': 25}, @@ -405,6 +410,7 @@ def set_up_take_anys(world, player): if world.mode[player] == 'inverted': if 'Dark Sanctuary Hint' in take_any_locations: take_any_locations.remove('Dark Sanctuary Hint') + if (world.mode[player] == 'inverted') != (0x29 in world.owswaps[player][0] and world.owMixed[player]): if 'Archery Game' in take_any_locations: take_any_locations.remove('Archery Game') @@ -549,7 +555,7 @@ def set_up_shops(world, player): cap_shop.inventory[1] = None else: cap_shop.inventory[0] = None - if world.bomblogic[player]: + if world.bombbag[player]: for item in world.itempool: if item.name == 'Bomb Upgrade (+10)' and item.player == player: item.advancement = True @@ -560,7 +566,7 @@ def set_up_shops(world, player): world.itempool.append(ItemFactory('Rupees (50)', player)) # replace the bomb upgrade else: cap_shop = world.get_region('Capacity Upgrade', player).shop - cap_shop.inventory[0] = cap_shop.inventory[1] # remove bomb capacity upgrades in bomblogic + cap_shop.inventory[0] = cap_shop.inventory[1] # remove bomb capacity upgrades in bombbag def customize_shops(world, player): @@ -602,7 +608,7 @@ def customize_shops(world, player): shop.shopkeeper_config = shopkeeper # handle capacity upgrades - randomly choose a bomb bunch or arrow bunch to become capacity upgrades if world.difficulty[player] == 'normal': - if not found_bomb_upgrade and len(possible_replacements) > 0 and not world.bomblogic[player]: + if not found_bomb_upgrade and len(possible_replacements) > 0 and not world.bombbag[player]: choices = [] for shop, idx, loc, item in possible_replacements: if item.name in ['Bombs (3)', 'Bombs (10)']: @@ -628,6 +634,7 @@ def customize_shops(world, player): upgrade.location = loc change_shop_items_to_rupees(world, player, shops_to_customize) balance_prices(world, player) + check_hints(world, player) def randomize_price(price): @@ -731,6 +738,15 @@ def balance_prices(world, player): # loc.parent_region.shop.inventory[slot]['price'] = new_price +def check_hints(world, player): + if world.shuffle[player] in ['simple', 'restricted', 'full', 'crossed', 'insanity']: + for shop, location_list in shop_to_location_table.items(): + if shop in ['Capacity Upgrade', 'Light World Death Mountain Shop', 'Potion Shop']: + continue # near the queen, near potions, and near 7 chests are fine + for loc_name in location_list: # other shops are indistinguishable in ER + world.get_location(loc_name, player).hint_text = f'for sale' + + repeatable_shop_items = ['Single Arrow', 'Arrows (10)', 'Bombs (3)', 'Bombs (10)', 'Red Potion', 'Small Heart', 'Blue Shield', 'Red Shield', 'Bee', 'Small Key (Universal)', 'Blue Potion', 'Green Potion'] @@ -750,7 +766,7 @@ rupee_chart = {'Rupee (1)': 1, 'Rupees (5)': 5, 'Rupees (20)': 20, 'Rupees (50)' 'Rupees (100)': 100, 'Rupees (300)': 300} -def get_pool_core(progressive, owShuffle, owSwap, shuffle, difficulty, treasure_hunt_total, timer, goal, mode, swords, retro, bomblogic, door_shuffle, logic): +def get_pool_core(progressive, shuffle, difficulty, treasure_hunt_total, timer, goal, mode, swords, retro, bombbag, door_shuffle, logic): pool = [] placed_items = {} precollected_items = [] @@ -797,10 +813,10 @@ def get_pool_core(progressive, owShuffle, owSwap, shuffle, difficulty, treasure_ diff = difficulties[difficulty] pool.extend(diff.baseitems) - if bomblogic: + if bombbag: pool = [item.replace('Bomb Upgrade (+5)','Rupees (5)') for item in pool] pool = [item.replace('Bomb Upgrade (+10)','Rupees (5)') for item in pool] - pool.extend(diff.bomblogic) + pool.extend(diff.bombbag) # expert+ difficulties produce the same contents for # all bottles, since only one bottle is available @@ -904,7 +920,7 @@ def get_pool_core(progressive, owShuffle, owSwap, shuffle, difficulty, treasure_ pool = [item.replace('Bomb Upgrade (+10)', 'Small Heart') for item in pool] return (pool, placed_items, precollected_items, clock_mode, lamps_needed_for_dark_rooms) -def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, swords, retro, bomblogic, customitemarray): +def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, swords, retro, bombbag, customitemarray): if isinstance(customitemarray,dict) and 1 in customitemarray: customitemarray = customitemarray[1] pool = [] @@ -1017,25 +1033,24 @@ def test(): for mode in ['open', 'standard', 'inverted', 'retro']: for swords in ['random', 'assured', 'swordless', 'vanilla', 'bombs']: for progressive in ['on', 'off']: - for shuffle in ['full', 'insanity_legacy']: + for shuffle in ['vanilla', 'full', 'crossed', 'insanity']: for logic in ['noglitches', 'minorglitches', 'owglitches', 'nologic']: for retro in [True, False]: for door_shuffle in ['basic', 'crossed', 'vanilla']: - for owShuffle in ['full', 'vanilla']: - for owSwap in ['mixed', 'vanilla']: - out = get_pool_core(progressive, owShuffle, owSwap, shuffle, difficulty, 30, timer, goal, mode, swords, retro, bomblogic, door_shuffle, logic) - count = len(out[0]) + len(out[1]) + for bombbag in [True, False]: + out = get_pool_core(progressive, shuffle, difficulty, 30, timer, goal, mode, swords, retro, bombbag, door_shuffle, logic) + count = len(out[0]) + len(out[1]) - correct_count = total_items_to_place - if goal == 'pedestal' and swords != 'vanilla': - # pedestal goals generate one extra item - correct_count += 1 - if retro: - correct_count += 28 - try: - assert count == correct_count, "expected {0} items but found {1} items for {2}".format(correct_count, count, (progressive, shuffle, difficulty, timer, goal, mode, swords, retro, bomblogic)) - except AssertionError as e: - print(e) + correct_count = total_items_to_place + if goal == 'pedestal' and swords != 'vanilla': + # pedestal goals generate one extra item + correct_count += 1 + if retro: + correct_count += 28 + try: + assert count == correct_count, "expected {0} items but found {1} items for {2}".format(correct_count, count, (progressive, shuffle, difficulty, timer, goal, mode, swords, retro, bombbag)) + except AssertionError as e: + print(e) if __name__ == '__main__': test() diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index 549a739c..102fc15d 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -2,9 +2,9 @@ import itertools import logging from collections import defaultdict, deque -from BaseClasses import DoorType +from BaseClasses import DoorType, dungeon_keys, KeyRuleType, RegionType from Regions import dungeon_events -from Dungeons import dungeon_keys, dungeon_bigs +from Dungeons import dungeon_keys, dungeon_bigs, dungeon_table from DungeonGenerator import ExplorationState, special_big_key_doors @@ -25,6 +25,8 @@ class KeyLayout(object): self.all_locations = set() self.item_locations = set() + self.found_doors = set() + self.prize_relevant = False # bk special? # bk required? True if big chests or big doors exists @@ -35,6 +37,7 @@ class KeyLayout(object): self.max_chests = calc_max_chests(builder, self, world, player) self.all_locations = set() self.item_locations = set() + self.prize_relevant = False class KeyLogic(object): @@ -54,10 +57,11 @@ class KeyLogic(object): self.location_rules = {} self.outside_keys = 0 self.dungeon = dungeon_name + self.sm_doors = {} def check_placement(self, unplaced_keys, big_key_loc=None): for rule in self.placement_rules: - if not rule.is_satisfiable(self.outside_keys, unplaced_keys): + if not rule.is_satisfiable(self.outside_keys, unplaced_keys, big_key_loc): return False if big_key_loc: for rule_a, rule_b in itertools.combinations(self.placement_rules, 2): @@ -65,6 +69,15 @@ class KeyLogic(object): return False return True + def reset(self): + self.door_rules.clear() + self.bk_restricted.clear() + self.bk_locked.clear() + self.sm_restricted.clear() + self.bk_doors.clear() + self.bk_chests.clear() + self.placement_rules.clear() + class DoorRules(object): @@ -79,6 +92,8 @@ class DoorRules(object): self.small_location = None self.opposite = None + self.new_rules = {} # keyed by type, or type+lock_item -> number + class LocationRule(object): def __init__(self): @@ -101,6 +116,7 @@ class PlacementRule(object): self.needed_keys_w_bk = None self.needed_keys_wo_bk = None self.check_locations_w_bk = None + self.special_bk_avail = False self.check_locations_wo_bk = None self.bk_relevant = True self.key_reduced = False @@ -112,6 +128,10 @@ class PlacementRule(object): rule_locations = rule.check_locations_wo_bk if rule_blocked else rule.check_locations_w_bk if check_locations is None or rule_locations is None: return False + if not bk_blocked and big_key_loc not in check_locations: # bk is not available, so rule doesn't apply + return False + if not rule_blocked and big_key_loc not in rule_locations: # bk is not available, so rule doesn't apply + return False check_locations = check_locations - {big_key_loc} rule_locations = rule_locations - {big_key_loc} threshold = self.needed_keys_wo_bk if bk_blocked else self.needed_keys_w_bk @@ -134,13 +154,23 @@ class PlacementRule(object): left -= rule_needed return False - def is_satisfiable(self, outside_keys, unplaced_keys): + def is_satisfiable(self, outside_keys, unplaced_keys, big_key_loc): bk_blocked = False if self.bk_conditional_set: for loc in self.bk_conditional_set: if loc.item and loc.item.bigkey: bk_blocked = True break + else: + def loc_has_bk(l): + return (big_key_loc is not None and big_key_loc == l) or (l.item and l.item.bigkey) + + # todo: sometimes the bk avail rule doesn't mean the bk must be avail or this rule is invalid + # but sometimes it certainly does + # check threshold vs len(check_loc) maybe to determine bk isn't relevant? + bk_found = self.special_bk_avail or any(loc for loc in self.check_locations_w_bk if loc_has_bk(loc)) + if not bk_found: + return True check_locations = self.check_locations_wo_bk if bk_blocked else self.check_locations_w_bk if not bk_blocked and check_locations is None: return True @@ -170,6 +200,8 @@ class KeyCounter(object): self.important_location = False self.other_locations = {} self.important_locations = {} + self.prize_doors_opened = False + self.prize_received = False def used_smalls_loc(self, reserve=0): return max(self.used_keys + reserve - len(self.key_only_locations), 0) @@ -209,8 +241,19 @@ def calc_max_chests(builder, key_layout, world, player): def analyze_dungeon(key_layout, world, player): + key_layout.key_logic.reset() key_layout.key_counters = create_key_counters(key_layout, world, player) key_logic = key_layout.key_logic + for door in key_layout.proposal: + if isinstance(door, tuple): + key_logic.sm_doors[door[0]] = door[1] + key_logic.sm_doors[door[1]] = door[0] + else: + if door.dest and door.type != DoorType.SpiralStairs: + key_logic.sm_doors[door] = door.dest + key_logic.sm_doors[door.dest] = door + else: + key_logic.sm_doors[door] = None find_bk_locked_sections(key_layout, world, player) key_logic.bk_chests.update(find_big_chest_locations(key_layout.all_chest_locations)) @@ -218,7 +261,9 @@ def analyze_dungeon(key_layout, world, player): if world.retro[player] and world.mode[player] != 'standard': return - original_key_counter = find_counter({}, False, key_layout) + original_key_counter = find_counter({}, False, key_layout, False) + if key_layout.big_key_special and forced_big_key_avail(original_key_counter.other_locations) is not None: + original_key_counter = find_counter({}, True, key_layout, False) queue = deque([(None, original_key_counter)]) doors_completed = set() visited_cid = set() @@ -237,28 +282,39 @@ def analyze_dungeon(key_layout, world, player): if not big_avail: if chest_keys == non_big_locs and chest_keys > 0 and available <= possible_smalls and not avail_bigs: key_logic.bk_restricted.update(filter_big_chest(key_counter.free_locations)) + # note to self: this is due to the enough_small_locations function in validate_key_layout_sub_loop + # I don't like this exception here or there + elif available <= possible_smalls and avail_bigs and non_big_locs > 0: + max_ctr = find_max_counter(key_layout) + bk_lockdown = [x for x in max_ctr.free_locations if x not in key_counter.free_locations] + key_logic.bk_restricted.update(filter_big_chest(bk_lockdown)) # try to relax the rules here? - smallest requirement that doesn't force a softlock child_queue = deque() for child in key_counter.child_doors.keys(): - if not child.bigKey or not key_layout.big_key_special or big_avail: + if can_open_door_by_counter(child, key_counter, key_layout, world, player): odd_counter = create_odd_key_counter(child, key_counter, key_layout, world, player) empty_flag = empty_counter(odd_counter) child_queue.append((child, odd_counter, empty_flag)) while len(child_queue) > 0: child, odd_counter, empty_flag = child_queue.popleft() - if not child.bigKey and child not in doors_completed: - best_counter = find_best_counter(child, odd_counter, key_counter, key_layout, world, player, False, empty_flag) - rule = create_rule(best_counter, key_counter, key_layout, world, player) + prize_flag = key_counter.prize_doors_opened + if child in key_layout.flat_prop and child not in doors_completed: + best_counter = find_best_counter(child, key_layout, odd_counter, False, empty_flag) + rule = create_rule(best_counter, key_counter, world, player) + create_worst_case_rule(rule, best_counter, world, player) check_for_self_lock_key(rule, child, best_counter, key_layout, world, player) bk_restricted_rules(rule, child, odd_counter, empty_flag, key_counter, key_layout, world, player) key_logic.door_rules[child.name] = rule + elif not child.bigKey and child not in doors_completed: + prize_flag = True doors_completed.add(child) - next_counter = find_next_counter(child, key_counter, key_layout) + next_counter = find_next_counter(child, key_counter, key_layout, prize_flag) ctr_id = cid(next_counter, key_layout) if ctr_id not in visited_cid: queue.append((child, next_counter)) visited_cid.add(ctr_id) - check_rules(original_key_counter, key_layout, world, player) + # todo: why is this commented out? + # check_rules(original_key_counter, key_layout, world, player) # Flip bk rules if more restrictive, to prevent placing a big key in a softlocking location for rule in key_logic.door_rules.values(): @@ -274,6 +330,8 @@ def create_exhaustive_placement_rules(key_layout, world, player): key_logic = key_layout.key_logic max_ctr = find_max_counter(key_layout) for code, key_counter in key_layout.key_counters.items(): + if skip_key_counter_due_to_prize(key_layout, key_counter): + continue # we have the prize, we are not concerned about this case accessible_loc = set() accessible_loc.update(key_counter.free_locations) accessible_loc.update(key_counter.key_only_locations) @@ -294,7 +352,9 @@ def create_exhaustive_placement_rules(key_layout, world, player): else: placement_self_lock_adjustment(rule, max_ctr, blocked_loc, key_counter, world, player) rule.check_locations_w_bk = accessible_loc - check_sm_restriction_needed(key_layout, max_ctr, rule, blocked_loc) + if key_layout.big_key_special: + rule.special_bk_avail = forced_big_key_avail(key_counter.important_locations) is not None + # check_sm_restriction_needed(key_layout, max_ctr, rule, blocked_loc) else: if big_key_progress(key_counter) and only_sm_doors(key_counter): create_inclusive_rule(key_layout, max_ctr, code, key_counter, blocked_loc, accessible_loc, min_keys, world, player) @@ -308,6 +368,10 @@ def create_exhaustive_placement_rules(key_layout, world, player): refine_location_rules(key_layout) +def skip_key_counter_due_to_prize(key_layout, key_counter): + return key_layout.prize_relevant and key_counter.prize_received and not key_counter.prize_doors_opened + + def placement_self_lock_adjustment(rule, max_ctr, blocked_loc, ctr, world, player): if len(blocked_loc) == 1 and world.accessibility[player] != 'locations': blocked_others = set(max_ctr.other_locations).difference(set(ctr.other_locations)) @@ -320,6 +384,7 @@ def placement_self_lock_adjustment(rule, max_ctr, blocked_loc, ctr, world, playe rule.needed_keys_w_bk -= 1 +# this rule is suspect - commented out usages for now def check_sm_restriction_needed(key_layout, max_ctr, rule, blocked): if rule.needed_keys_w_bk == key_layout.max_chests + len(max_ctr.key_only_locations): key_layout.key_logic.sm_restricted.update(blocked.difference(max_ctr.key_only_locations)) @@ -413,7 +478,8 @@ def refine_placement_rules(key_layout, max_ctr): rule_b = temp if rule_a.bk_conditional_set and rule_b.check_locations_w_bk: common_needed = min(rule_a.needed_keys_wo_bk, rule_b.needed_keys_w_bk) - if len(rule_b.check_locations_w_bk & rule_a.check_locations_wo_bk) < common_needed: + common_locs = len(rule_b.check_locations_w_bk & rule_a.check_locations_wo_bk) + if (common_needed - common_locs) * 2 > key_layout.max_chests: key_logic.bk_restricted.update(rule_a.bk_conditional_set) rules_to_remove.append(rule_a) changed = True @@ -478,7 +544,7 @@ def create_inclusive_rule(key_layout, max_ctr, code, key_counter, blocked_loc, a else: placement_self_lock_adjustment(rule, max_ctr, blocked_loc, key_counter, world, player) rule.check_locations_w_bk = accessible_loc - check_sm_restriction_needed(key_layout, max_ctr, rule, blocked_loc) + # check_sm_restriction_needed(key_layout, max_ctr, rule, blocked_loc) key_logic.placement_rules.append(rule) adjust_locations_rules(key_logic, rule, accessible_loc, key_layout, key_counter, max_ctr) @@ -538,6 +604,8 @@ def relative_empty_counter(odd_counter, key_counter): return False if len(set(odd_counter.free_locations).difference(key_counter.free_locations)) > 0: return False + if len(set(odd_counter.other_locations).difference(key_counter.other_locations)) > 0: + return False # important only if len(set(odd_counter.important_locations).difference(key_counter.important_locations)) > 0: return False @@ -594,33 +662,50 @@ def unique_child_door_2(child, key_counter): return True -def find_best_counter(door, odd_counter, key_counter, key_layout, world, player, skip_bk, empty_flag): # try to waste as many keys as possible? - ignored_doors = {door, door.dest} if door is not None else {} - finished = False - opened_doors = dict(key_counter.open_doors) - bk_opened = key_counter.big_key_opened - # new_counter = key_counter - last_counter = key_counter - while not finished: - door_set = find_potential_open_doors(last_counter, ignored_doors, key_layout, skip_bk) - if door_set is None or len(door_set) == 0: - finished = True - continue - for new_door in door_set: - proposed_doors = {**opened_doors, **dict.fromkeys([new_door, new_door.dest])} - bk_open = bk_opened or new_door.bigKey - new_counter = find_counter(proposed_doors, bk_open, key_layout) - bk_open = new_counter.big_key_opened - # this means the new_door invalidates the door / leads to the same stuff - if not empty_flag and relative_empty_counter(odd_counter, new_counter): - ignored_doors.add(new_door) - elif empty_flag or key_wasted(new_door, door, last_counter, new_counter, key_layout, world, player): - last_counter = new_counter - opened_doors = proposed_doors - bk_opened = bk_open - else: - ignored_doors.add(new_door) - return last_counter +# def find_best_counter(door, odd_counter, key_counter, key_layout, world, player, skip_bk, empty_flag): # try to waste as many keys as possible? +# ignored_doors = {door, door.dest} if door is not None else {} +# finished = False +# opened_doors = dict(key_counter.open_doors) +# bk_opened = key_counter.big_key_opened +# # new_counter = key_counter +# last_counter = key_counter +# while not finished: +# door_set = find_potential_open_doors(last_counter, ignored_doors, key_layout, skip_bk) +# if door_set is None or len(door_set) == 0: +# finished = True +# continue +# for new_door in door_set: +# proposed_doors = {**opened_doors, **dict.fromkeys([new_door, new_door.dest])} +# bk_open = bk_opened or new_door.bigKey +# new_counter = find_counter(proposed_doors, bk_open, key_layout) +# bk_open = new_counter.big_key_opened +# # this means the new_door invalidates the door / leads to the same stuff +# if not empty_flag and relative_empty_counter(odd_counter, new_counter): +# ignored_doors.add(new_door) +# elif empty_flag or key_wasted(new_door, door, last_counter, new_counter, key_layout, world, player): +# last_counter = new_counter +# opened_doors = proposed_doors +# bk_opened = bk_open +# else: +# ignored_doors.add(new_door) +# return last_counter + + +def find_best_counter(door, key_layout, odd_counter, skip_bk, empty_flag): + best, best_ctr, locations = 0, None, 0 + for code, counter in key_layout.key_counters.items(): + if door not in counter.open_doors: + if best_ctr is None or counter.used_keys > best or (counter.used_keys == best and count_locations(counter) > locations): + if not skip_bk or not counter.big_key_opened: + if empty_flag or not relative_empty_counter(odd_counter, counter): + best = counter.used_keys + best_ctr = counter + locations = count_locations(counter) + return best_ctr + + +def count_locations(ctr): + return len(ctr.free_locations) + len(ctr.key_only_locations) + len(ctr.other_locations) + len(ctr.important_locations) def find_worst_counter(door, odd_counter, key_counter, key_layout, skip_bk): # try to waste as many keys as possible? @@ -638,7 +723,7 @@ def find_worst_counter(door, odd_counter, key_counter, key_layout, skip_bk): # for new_door in door_set: proposed_doors = {**opened_doors, **dict.fromkeys([new_door, new_door.dest])} bk_open = bk_opened or new_door.bigKey - new_counter = find_counter(proposed_doors, bk_open, key_layout) + new_counter = find_counter(proposed_doors, bk_open, key_layout, key_counter.prize_doors_opened) bk_open = new_counter.big_key_opened if not new_door.bigKey and progressive_ctr(new_counter, last_counter) and relative_empty_counter_2(odd_counter, new_counter): ignored_doors.add(new_door) @@ -690,7 +775,7 @@ def key_wasted(new_door, old_door, old_counter, new_counter, key_layout, world, for new_child in new_children: proposed_doors = {**opened_doors, **dict.fromkeys([new_child, new_child.dest])} bk_open = bk_opened or new_door.bigKey - new_counter = find_counter(proposed_doors, bk_open, key_layout) + new_counter = find_counter(proposed_doors, bk_open, key_layout, current_counter.prize_doors_opened) if key_wasted(new_child, old_door, current_counter, new_counter, key_layout, world, player): wasted_keys += 1 if new_avail - wasted_keys < old_avail: @@ -698,10 +783,11 @@ def key_wasted(new_door, old_door, old_counter, new_counter, key_layout, world, return False -def find_next_counter(new_door, old_counter, key_layout): +def find_next_counter(new_door, old_counter, key_layout, prize_flag=None): proposed_doors = {**old_counter.open_doors, **dict.fromkeys([new_door, new_door.dest])} bk_open = old_counter.big_key_opened or new_door.bigKey - return find_counter(proposed_doors, bk_open, key_layout) + prize_flag = prize_flag if prize_flag else old_counter.prize_doors_opened + return find_counter(proposed_doors, bk_open, key_layout, prize_flag) def check_special_locations(locations): @@ -717,7 +803,7 @@ def calc_avail_keys(key_counter, world, player): return raw_avail - key_counter.used_keys -def create_rule(key_counter, prev_counter, key_layout, world, player): +def create_rule(key_counter, prev_counter, world, player): # prev_chest_keys = available_chest_small_keys(prev_counter, world) # prev_avail = prev_chest_keys + len(prev_counter.key_only_locations) chest_keys = available_chest_small_keys(key_counter, world, player) @@ -736,6 +822,11 @@ def create_rule(key_counter, prev_counter, key_layout, world, player): return DoorRules(rule_num, is_valid) +def create_worst_case_rule(rules, key_counter, world, player): + required_keys = key_counter.used_keys + 1 # this makes more sense, if key_counter has wasted all keys + rules.new_rules[KeyRuleType.WorstCase] = required_keys + + def check_for_self_lock_key(rule, door, parent_counter, key_layout, world, player): if world.accessibility[player] != 'locations': counter = find_inverted_counter(door, parent_counter, key_layout, world, player) @@ -744,11 +835,12 @@ def check_for_self_lock_key(rule, door, parent_counter, key_layout, world, playe if len(counter.free_locations) == 1 and len(counter.key_only_locations) == 0 and not counter.important_location: rule.allow_small = True rule.small_location = next(iter(counter.free_locations)) + rule.new_rules[KeyRuleType.AllowSmall] = rule.new_rules[KeyRuleType.WorstCase] - 1 def find_inverted_counter(door, parent_counter, key_layout, world, player): # open all doors in counter - counter = open_all_counter(parent_counter, key_layout, door=door) + counter = open_all_counter(parent_counter, key_layout, world, player, door=door) max_counter = find_max_counter(key_layout) # find the difference inverted_counter = KeyCounter(key_layout.max_chests) @@ -764,7 +856,7 @@ def find_inverted_counter(door, parent_counter, key_layout, world, player): return inverted_counter -def open_all_counter(parent_counter, key_layout, door=None, skipBk=False): +def open_all_counter(parent_counter, key_layout, world, player, door=None, skipBk=False): changed = True counter = parent_counter proposed_doors = dict.fromkeys(parent_counter.open_doors.keys()) @@ -776,14 +868,12 @@ def open_all_counter(parent_counter, key_layout, door=None, skipBk=False): if skipBk: if not child.bigKey: doors_to_open[child] = None - elif not child.bigKey or not key_layout.big_key_special or counter.big_key_opened: + elif can_open_door_by_counter(child, counter, key_layout, world, player): doors_to_open[child] = None if len(doors_to_open.keys()) > 0: proposed_doors = {**proposed_doors, **doors_to_open} - bk_hint = counter.big_key_opened - for d in doors_to_open.keys(): - bk_hint = bk_hint or d.bigKey - counter = find_counter(proposed_doors, bk_hint, key_layout) + bk_hint = counter.big_key_opened or any(d.bigKey for d in doors_to_open.keys()) + counter = find_counter(proposed_doors, bk_hint, key_layout, True) changed = True return counter @@ -804,7 +894,7 @@ def open_some_counter(parent_counter, key_layout, ignored_doors): bk_hint = counter.big_key_opened for d in doors_to_open.keys(): bk_hint = bk_hint or d.bigKey - counter = find_counter(proposed_doors, bk_hint, key_layout) + counter = find_counter(proposed_doors, bk_hint, key_layout, parent_counter.prize_doors_opened) changed = True return counter @@ -845,16 +935,16 @@ def big_key_drop_available(key_counter): def bk_restricted_rules(rule, door, odd_counter, empty_flag, key_counter, key_layout, world, player): if key_counter.big_key_opened: return - best_counter = find_best_counter(door, odd_counter, key_counter, key_layout, world, player, True, empty_flag) - bk_rule = create_rule(best_counter, key_counter, key_layout, world, player) + best_counter = find_best_counter(door, key_layout, odd_counter, True, empty_flag) + bk_rule = create_rule(best_counter, key_counter, world, player) if bk_rule.small_key_num >= rule.small_key_num: return door_open = find_next_counter(door, best_counter, key_layout) ignored_doors = dict_intersection(best_counter.child_doors, door_open.child_doors) dest_ignored = [] - for door in ignored_doors.keys(): - if door.dest not in ignored_doors: - dest_ignored.append(door.dest) + for d in ignored_doors.keys(): + if d.dest not in ignored_doors: + dest_ignored.append(d.dest) ignored_doors = {**ignored_doors, **dict.fromkeys(dest_ignored)} post_counter = open_some_counter(door_open, key_layout, ignored_doors.keys()) unique_loc = dict_difference(post_counter.free_locations, best_counter.free_locations) @@ -862,8 +952,8 @@ def bk_restricted_rules(rule, door, odd_counter, empty_flag, key_counter, key_la if len(unique_loc) > 0: # and bk_rule.is_valid rule.alternate_small_key = bk_rule.small_key_num rule.alternate_big_key_loc.update(unique_loc) - # elif not bk_rule.is_valid: - # key_layout.key_logic.bk_restricted.update(unique_loc) + if not door.bigKey: + rule.new_rules[(KeyRuleType.Lock, key_layout.key_logic.bk_name)] = best_counter.used_keys + 1 def find_worst_counter_wo_bk(small_key_num, accessible_set, door, odd_ctr, key_counter, key_layout): @@ -887,12 +977,19 @@ def find_worst_counter_wo_bk(small_key_num, accessible_set, door, odd_ctr, key_c return worst_counter, post_counter, bk_rule_num -def open_a_door(door, child_state, flat_proposal): +def open_a_door(door, child_state, flat_proposal, world, player): if door.bigKey or door.name in special_big_key_doors: child_state.big_key_opened = True child_state.avail_doors.extend(child_state.big_doors) child_state.opened_doors.extend(set([d.door for d in child_state.big_doors])) child_state.big_doors.clear() + elif door in child_state.prize_door_set: + child_state.prize_doors_opened = True + for exp_door in child_state.prize_doors: + new_region = exp_door.door.entrance.parent_region + child_state.visit_region(new_region, key_checks=True) + child_state.add_all_doors_check_keys(new_region, flat_proposal, world, player) + child_state.prize_doors.clear() else: child_state.opened_doors.append(door) doors_to_open = [x for x in child_state.small_doors if x.door == door] @@ -935,6 +1032,7 @@ def only_sm_doors(key_counter): return False return True + # doesn't count dest doors def count_unique_small_doors(key_counter, proposal): cnt = 0 @@ -949,7 +1047,7 @@ def count_unique_small_doors(key_counter, proposal): def exist_relevant_big_doors(key_counter, key_layout): - bk_counter = find_counter(key_counter.open_doors, True, key_layout, False) + bk_counter = find_counter(key_counter.open_doors, True, key_layout, key_counter.prize_doors_opened, False) if bk_counter is not None: diff = dict_difference(bk_counter.free_locations, key_counter.free_locations) if len(diff) > 0: @@ -982,14 +1080,6 @@ def filter_big_chest(locations): return [x for x in locations if '- Big Chest' not in x.name] -def count_locations_exclude_logic(locations, key_logic): - cnt = 0 - for loc in locations: - if not location_is_bk_locked(loc, key_logic) and not loc.forced_item and not prize_or_event(loc): - cnt += 1 - return cnt - - def location_is_bk_locked(loc, key_logic): return loc in key_logic.bk_chests or loc in key_logic.bk_locked @@ -998,18 +1088,36 @@ def prize_or_event(loc): return loc.name in dungeon_events or '- Prize' in loc.name or loc.name in ['Agahnim 1', 'Agahnim 2'] -def count_free_locations(state): +def boss_unavail(loc, world, player): + # todo: ambrosia + # return world.bossdrops[player] == 'ambrosia' and "- Boss" in loc.name + return False + + +def blind_boss_unavail(loc, state, world, player): + if loc.name == "Thieves' Town - Boss": + # todo: check attic + return (loc.parent_region.dungeon.boss.name == 'Blind' and + (not any(x for x in state.found_locations if x.name == 'Suspicious Maiden') or + (world.get_region('Thieves Attic Window', player).dungeon.name == 'Thieves Town' and + not any(x for x in state.found_locations if x.name == 'Attic Cracked Floor')))) + return False + + +def count_free_locations(state, world, player): cnt = 0 for loc in state.found_locations: - if not prize_or_event(loc) and not loc.forced_item: + if (not prize_or_event(loc) and not loc.forced_item and not boss_unavail(loc, world, player) + and not blind_boss_unavail(loc, state, world, player)): cnt += 1 return cnt -def count_locations_exclude_big_chest(state): +def count_locations_exclude_big_chest(state, world, player): cnt = 0 for loc in state.found_locations: - if '- Big Chest' not in loc.name and not loc.forced_item and not prize_or_event(loc): + if ('- Big Chest' not in loc.name and not loc.forced_item and not boss_unavail(loc, world, player) + and not prize_or_event(loc) and not blind_boss_unavail(loc, state, world, player)): cnt += 1 return cnt @@ -1197,7 +1305,7 @@ def check_rules_deep(original_counter, key_layout, world, player): elif not door.bigKey: can_open = True if can_open: - can_progress = smalls_opened or not big_maybe_not_found + can_progress = (big_avail or not big_maybe_not_found) if door.bigKey else smalls_opened next_counter = find_next_counter(door, counter, key_layout) c_id = cid(next_counter, key_layout) if c_id not in completed: @@ -1265,6 +1373,13 @@ def check_bk_special(regions, world, player): return False +def forced_big_key_avail(locations): + for loc in locations: + if loc.forced_big_key(): + return loc + return None + + # Soft lock stuff def validate_key_layout(key_layout, world, player): # retro is all good - except for hyrule castle in standard mode @@ -1275,8 +1390,16 @@ def validate_key_layout(key_layout, world, player): state.key_locations = key_layout.max_chests state.big_key_special = check_bk_special(key_layout.sector.regions, world, player) for region in key_layout.start_regions: - state.visit_region(region, key_checks=True) - state.add_all_doors_check_keys(region, flat_proposal, world, player) + dungeon_entrance, portal_door = find_outside_connection(region) + if (len(key_layout.start_regions) > 1 and dungeon_entrance and + dungeon_entrance.name in ['Ganons Tower', 'Pyramid Fairy'] + and dungeon_table[key_layout.key_logic.dungeon].prize): + state.append_door_to_list(portal_door, state.prize_doors) + state.prize_door_set[portal_door] = dungeon_entrance + key_layout.prize_relevant = True + else: + state.visit_region(region, key_checks=True) + state.add_all_doors_check_keys(region, flat_proposal, world, player) return validate_key_layout_sub_loop(key_layout, state, {}, flat_proposal, None, 0, world, player) @@ -1287,7 +1410,10 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa if not smalls_avail and num_bigs == 0: return True # I think that's the end # todo: fix state to separate out these types - ttl_locations = count_free_locations(state) if state.big_key_opened else count_locations_exclude_big_chest(state) + if state.big_key_opened: + ttl_locations = count_free_locations(state, world, player) + else: + ttl_locations = count_locations_exclude_big_chest(state, world, player) ttl_small_key_only = count_small_key_only_locations(state) available_small_locations = cnt_avail_small_locations(ttl_locations, ttl_small_key_only, state, world, player) available_big_locations = cnt_avail_big_locations(ttl_locations, state, world, player) @@ -1301,14 +1427,16 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa if smalls_done and bk_done: return False else: + # todo: pretty sure you should OR these paths together, maybe when there's one location and it can + # either be small or big key if smalls_avail and available_small_locations > 0: for exp_door in state.small_doors: state_copy = state.copy() - open_a_door(exp_door.door, state_copy, flat_proposal) + open_a_door(exp_door.door, state_copy, flat_proposal, world, player) state_copy.used_smalls += 1 if state_copy.used_smalls > ttl_small_key_only: state_copy.used_locations += 1 - code = state_id(state_copy, flat_proposal) + code = validate_id(state_copy, flat_proposal) if code not in checked_states.keys(): valid = validate_key_layout_sub_loop(key_layout, state_copy, checked_states, flat_proposal, state, available_small_locations, world, player) @@ -1319,10 +1447,23 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa return False if not state.big_key_opened and (available_big_locations >= num_bigs > 0 or (found_forced_bk and num_bigs > 0)): state_copy = state.copy() - open_a_door(state.big_doors[0].door, state_copy, flat_proposal) + open_a_door(state.big_doors[0].door, state_copy, flat_proposal, world, player) if not found_forced_bk: state_copy.used_locations += 1 - code = state_id(state_copy, flat_proposal) + code = validate_id(state_copy, flat_proposal) + if code not in checked_states.keys(): + valid = validate_key_layout_sub_loop(key_layout, state_copy, checked_states, flat_proposal, + state, available_small_locations, world, player) + checked_states[code] = valid + else: + valid = checked_states[code] + if not valid: + return False + # todo: feel like you only open these if the boss is available??? + if not state.prize_doors_opened and key_layout.prize_relevant: + state_copy = state.copy() + open_a_door(next(iter(state_copy.prize_door_set)), state_copy, flat_proposal, world, player) + code = validate_id(state_copy, flat_proposal) if code not in checked_states.keys(): valid = validate_key_layout_sub_loop(key_layout, state_copy, checked_states, flat_proposal, state, available_small_locations, world, player) @@ -1341,7 +1482,7 @@ def invalid_self_locking_key(key_layout, state, prev_state, prev_avail, world, p state_copy = state.copy() while len(new_bk_doors) > 0: for door in new_bk_doors: - open_a_door(door.door, state_copy, key_layout.flat_prop) + open_a_door(door.door, state_copy, key_layout.flat_prop, world, player) new_bk_doors = set(state_copy.big_doors).difference(set(prev_state.big_doors)) expand_key_state(state_copy, key_layout.flat_prop, world, player) new_locations = set(state_copy.found_locations).difference(set(prev_state.found_locations)) @@ -1373,29 +1514,55 @@ def cnt_avail_small_locations(free_locations, key_only, state, world, player): return state.key_locations - state.used_smalls +def cnt_avail_small_locations_by_ctr(free_locations, counter, layout, world, player): + if not world.keyshuffle[player] and not world.retro[player]: + bk_adj = 1 if counter.big_key_opened and not layout.big_key_special else 0 + avail_chest_keys = min(free_locations - bk_adj, layout.max_chests) + return max(0, avail_chest_keys + len(counter.key_only_locations) - counter.used_keys) + return layout.max_chests + len(counter.key_only_locations) - counter.used_keys + + def cnt_avail_big_locations(ttl_locations, state, world, player): if not world.bigkeyshuffle[player]: return max(0, ttl_locations - state.used_locations) if not state.big_key_special else 0 return 1 if not state.big_key_special else 0 +def cnt_avail_big_locations_by_ctr(ttl_locations, counter, layout, world, player): + if not world.bigkeyshuffle[player]: + bk_adj = 1 if counter.big_key_opened and not layout.big_key_special else 0 + used_locations = max(0, counter.used_keys - len(counter.key_only_locations)) + bk_adj + return max(0, ttl_locations - used_locations) if not layout.big_key_special else 0 + return 1 if not layout.big_key_special else 0 + + def create_key_counters(key_layout, world, player): key_counters = {} + key_layout.found_doors.clear() flat_proposal = key_layout.flat_prop state = ExplorationState(dungeon=key_layout.sector.name) if world.doorShuffle[player] == 'vanilla': - state.key_locations = len(world.get_dungeon(key_layout.sector.name, player).small_keys) + builder = world.dungeon_layouts[player][key_layout.sector.name] + state.key_locations = len(builder.key_door_proposal) - builder.key_drop_cnt else: - state.key_locations = world.dungeon_layouts[player][key_layout.sector.name].key_doors_num - state.big_key_special, special_region = False, None + builder = world.dungeon_layouts[player][key_layout.sector.name] + state.key_locations = builder.total_keys - builder.key_drop_cnt + state.big_key_special = False for region in key_layout.sector.regions: for location in region.locations: if location.forced_big_key(): state.big_key_special = True - special_region = region for region in key_layout.start_regions: - state.visit_region(region, key_checks=True) - state.add_all_doors_check_keys(region, flat_proposal, world, player) + dungeon_entrance, portal_door = find_outside_connection(region) + if (len(key_layout.start_regions) > 1 and dungeon_entrance and + dungeon_entrance.name in ['Ganons Tower', 'Pyramid Fairy'] + and dungeon_table[key_layout.key_logic.dungeon].prize): + state.append_door_to_list(portal_door, state.prize_doors) + state.prize_door_set[portal_door] = dungeon_entrance + key_layout.prize_relevant = True + else: + state.visit_region(region, key_checks=True) + state.add_all_doors_check_keys(region, flat_proposal, world, player) expand_key_state(state, flat_proposal, world, player) code = state_id(state, key_layout.flat_prop) key_counters[code] = create_key_counter(state, key_layout, world, player) @@ -1403,12 +1570,15 @@ def create_key_counters(key_layout, world, player): while len(queue) > 0: next_key_counter, parent_state = queue.popleft() for door in next_key_counter.child_doors: + key_layout.found_doors.add(door) + if door.dest in flat_proposal and door.type != DoorType.SpiralStairs: + key_layout.found_doors.add(door.dest) child_state = parent_state.copy() if door.bigKey or door.name in special_big_key_doors: key_layout.key_logic.bk_doors.add(door) # open the door, if possible - if not door.bigKey or not child_state.big_key_special or child_state.visited_at_all(special_region): - open_a_door(door, child_state, flat_proposal) + if can_open_door(door, child_state, world, player): + open_a_door(door, child_state, flat_proposal, world, player) expand_key_state(child_state, flat_proposal, world, player) code = state_id(child_state, key_layout.flat_prop) if code not in key_counters.keys(): @@ -1418,9 +1588,57 @@ def create_key_counters(key_layout, world, player): return key_counters +def find_outside_connection(region): + portal = next((x for x in region.entrances if ' Portal' in x.parent_region.name), None) + if portal: + dungeon_entrance = next(x for x in portal.parent_region.entrances + if x.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld]) + portal_entrance = next(x for x in portal.parent_region.entrances if x.parent_region == region) + return dungeon_entrance, portal_entrance.door + return None, None + + +def can_open_door(door, state, world, player): + if state.big_key_opened: + ttl_locations = count_free_locations(state, world, player) + else: + ttl_locations = count_locations_exclude_big_chest(state, world, player) + if door.smallKey: + ttl_small_key_only = count_small_key_only_locations(state) + available_small_locations = cnt_avail_small_locations(ttl_locations, ttl_small_key_only, state, world, player) + return available_small_locations > 0 + elif door.bigKey: + available_big_locations = cnt_avail_big_locations(ttl_locations, state, world, player) + found_forced_bk = state.found_forced_bk() + return not state.big_key_opened and (available_big_locations > 0 or found_forced_bk) + else: + return True + + +def can_open_door_by_counter(door, counter: KeyCounter, layout, world, player): + if counter.big_key_opened: + ttl_locations = len(counter.free_locations) + else: + ttl_locations = len([x for x in counter.free_locations if '- Big Chest' not in x.name]) + + if door.smallKey: + # ttl_small_key_only = len(counter.key_only_locations) + return cnt_avail_small_locations_by_ctr(ttl_locations, counter, layout, world, player) > 0 + elif door.bigKey: + if counter.big_key_opened: + return False + if layout.big_key_special: + return any(x for x in counter.other_locations.keys() if x.forced_item and x.forced_item.bigkey) + else: + available_big_locations = cnt_avail_big_locations_by_ctr(ttl_locations, counter, layout, world, player) + return available_big_locations > 0 + else: + return True + + def create_key_counter(state, key_layout, world, player): key_counter = KeyCounter(key_layout.max_chests) - key_counter.child_doors.update(dict.fromkeys(unique_doors(state.small_doors+state.big_doors))) + key_counter.child_doors.update(dict.fromkeys(unique_doors(state.small_doors+state.big_doors+state.prize_doors))) for loc in state.found_locations: if important_location(loc, world, player): key_counter.important_location = True @@ -1437,6 +1655,10 @@ def create_key_counter(state, key_layout, world, player): key_counter.open_doors.update(dict.fromkeys(state.opened_doors)) key_counter.used_keys = count_unique_sm_doors(state.opened_doors) key_counter.big_key_opened = state.big_key_opened + if len(state.prize_door_set) > 0 and state.prize_doors_opened: + key_counter.prize_doors_opened = True + if any(x for x in key_counter.important_locations if '- Prize' in x.name): + key_counter.prize_received = True return key_counter @@ -1487,11 +1709,23 @@ def state_id(state, flat_proposal): s_id = '1' if state.big_key_opened else '0' for d in flat_proposal: s_id += '1' if d in state.opened_doors else '0' + if len(state.prize_door_set) > 0: + s_id += '1' if state.prize_doors_opened else '0' return s_id -def find_counter(opened_doors, bk_hint, key_layout, raise_on_error=True): - counter = find_counter_hint(opened_doors, bk_hint, key_layout) +def validate_id(state, flat_proposal): + s_id = '1' if state.big_key_opened else '0' + for d in flat_proposal: + s_id += '1' if d in state.opened_doors else '0' + if len(state.prize_door_set) > 0: + s_id += '1' if state.prize_doors_opened else '0' + s_id += str(state.used_locations) + return s_id + + +def find_counter(opened_doors, bk_hint, key_layout, prize_flag, raise_on_error=True): + counter = find_counter_hint(opened_doors, bk_hint, key_layout, prize_flag) if counter is not None: return counter more_doors = [] @@ -1500,43 +1734,47 @@ def find_counter(opened_doors, bk_hint, key_layout, raise_on_error=True): if door.dest not in opened_doors.keys(): more_doors.append(door.dest) if len(more_doors) > len(opened_doors.keys()): - counter = find_counter_hint(dict.fromkeys(more_doors), bk_hint, key_layout) + counter = find_counter_hint(dict.fromkeys(more_doors), bk_hint, key_layout, prize_flag) if counter is not None: return counter if raise_on_error: - raise Exception('Unable to find door permutation. Init CID: %s' % counter_id(opened_doors, bk_hint, key_layout.flat_prop)) + cid = counter_id(opened_doors, bk_hint, key_layout.flat_prop, key_layout.prize_relevant, prize_flag) + raise Exception(f'Unable to find door permutation. Init CID: {cid}') return None -def find_counter_hint(opened_doors, bk_hint, key_layout): - cid = counter_id(opened_doors, bk_hint, key_layout.flat_prop) +def find_counter_hint(opened_doors, bk_hint, key_layout, prize_flag): + cid = counter_id(opened_doors, bk_hint, key_layout.flat_prop, key_layout.prize_relevant, prize_flag) if cid in key_layout.key_counters.keys(): return key_layout.key_counters[cid] if not bk_hint: - cid = counter_id(opened_doors, True, key_layout.flat_prop) + cid = counter_id(opened_doors, True, key_layout.flat_prop, key_layout.prize_relevant, prize_flag) if cid in key_layout.key_counters.keys(): return key_layout.key_counters[cid] return None def find_max_counter(key_layout): - max_counter = find_counter_hint(dict.fromkeys(key_layout.flat_prop), False, key_layout) + max_counter = find_counter_hint(dict.fromkeys(key_layout.found_doors), False, key_layout, True) if max_counter is None: raise Exception("Max Counter is none - something is amiss") if len(max_counter.child_doors) > 0: - max_counter = find_counter_hint(dict.fromkeys(key_layout.flat_prop), True, key_layout) + max_counter = find_counter_hint(dict.fromkeys(key_layout.found_doors), True, key_layout, True) return max_counter -def counter_id(opened_doors, bk_unlocked, flat_proposal): +def counter_id(opened_doors, bk_unlocked, flat_proposal, prize_relevant, prize_flag): s_id = '1' if bk_unlocked else '0' for d in flat_proposal: s_id += '1' if d in opened_doors.keys() else '0' + if prize_relevant: + s_id += '1' if prize_flag else '0' return s_id def cid(counter, key_layout): - return counter_id(counter.open_doors, counter.big_key_opened, key_layout.flat_prop) + return counter_id(counter.open_doors, counter.big_key_opened, key_layout.flat_prop, + key_layout.prize_relevant, counter.prize_doors_opened) # class SoftLockException(Exception): @@ -1671,7 +1909,7 @@ def val_mire(key_logic, world, player): def val_turtle(key_logic, world, player): # todo: check vanilla key logic when TR back doors are accessible - if world.shuffle[player] == 'vanilla' and world.mode[player] != 'inverted' and world.logic[player] in ('noglitches', 'minorglitches'): + if world.shuffle[player] == 'vanilla' and (world.mode[player] == 'inverted') == (0x05 in world.owswaps[player][0] and world.owMixed[player]) and world.logic[player] in ('noglitches', 'minorglitches'): val_rule(key_logic.door_rules['TR Hub NW'], 1) val_rule(key_logic.door_rules['TR Pokey 1 NW'], 2) val_rule(key_logic.door_rules['TR Chain Chomps Down Stairs'], 3) @@ -1746,8 +1984,18 @@ def validate_key_placement(key_layout, world, player): found_locations = set(i for i in counter.free_locations if big_found or "- Big Chest" not in i.name) found_keys = sum(1 for i in found_locations if i.item is not None and i.item.name == smallkey_name and i.item.player == player) + \ len(counter.key_only_locations) + keys_outside + if key_layout.prize_relevant: + found_prize = any(x for x in counter.important_locations if '- Prize' in x.name) + if not found_prize and dungeon_table[key_layout.sector.name].prize: + prize_loc = world.get_location(dungeon_table[key_layout.sector.name].prize, player) + # todo: pyramid fairy only care about crystals 5 & 6 + found_prize = 'Crystal' not in prize_loc.item.name + else: + found_prize = False can_progress = (not counter.big_key_opened and big_found and any(d.bigKey for d in counter.child_doors)) or \ - found_keys > counter.used_keys and any(not d.bigKey for d in counter.child_doors) + found_keys > counter.used_keys and any(not d.bigKey for d in counter.child_doors) or \ + self_locked_child_door(key_layout, counter) or \ + (key_layout.prize_relevant and not counter.prize_doors_opened and found_prize) if not can_progress: missing_locations = set(max_counter.free_locations.keys()).difference(found_locations) missing_items = [l for l in missing_locations if l.item is None or (l.item.name != smallkey_name and l.item.name != bigkey_name) or "- Boss" in l.name] @@ -1761,3 +2009,11 @@ def validate_key_placement(key_layout, world, player): return True + +def self_locked_child_door(key_layout, counter): + if len(counter.child_doors) == 1: + door = next(iter(counter.child_doors.keys())) + return door.smallKey and key_layout.key_logic.door_rules[door.name].allow_small + return False + + diff --git a/Main.py b/Main.py index 949d0ab6..7d66094e 100644 --- a/Main.py +++ b/Main.py @@ -16,7 +16,7 @@ from OverworldGlitchRules import create_owg_connections from PotShuffle import shuffle_pots from Regions import create_regions, create_shops, mark_light_world_regions, mark_dark_world_regions, create_dungeon_regions, adjust_locations from OWEdges import create_owedges -from OverworldShuffle import link_overworld +from OverworldShuffle import link_overworld, update_world_regions, create_flute_exits from EntranceShuffle import link_entrances from Rom import patch_rom, patch_race_rom, patch_enemizer, apply_rom_settings, LocalRom, JsonRom, get_hash_string from Doors import create_doors @@ -29,7 +29,7 @@ from Fill import sell_potions, sell_keys, balance_multiworld_progression, balanc from ItemList import generate_itempool, difficulties, fill_prizes, customize_shops from Utils import output_path, parse_player_names -__version__ = '0.5.0.1-u' +__version__ = '0.5.1.1-u' from source.classes.BabelFish import BabelFish @@ -86,7 +86,7 @@ def main(args, seed=None, fish=None): for player, code in args.code.items(): if code: Settings.adjust_args_from_code(code, player, args) - world = World(args.multi, args.ow_shuffle, args.ow_swap, args.shuffle, args.door_shuffle, args.logic, args.mode, args.swords, + world = World(args.multi, args.ow_shuffle, args.ow_crossed, args.ow_mixed, args.shuffle, args.door_shuffle, args.logic, args.mode, args.swords, args.difficulty, args.item_functionality, args.timer, args.progressive, args.goal, args.algorithm, args.accessibility, args.shuffleganon, args.retro, args.custom, args.customitemarray, args.hints) logger = logging.getLogger('') @@ -105,7 +105,7 @@ def main(args, seed=None, fish=None): world.compassshuffle = args.compassshuffle.copy() world.keyshuffle = args.keyshuffle.copy() world.bigkeyshuffle = args.bigkeyshuffle.copy() - world.bomblogic = args.bomblogic.copy() + world.bombbag = args.bombbag.copy() world.crystals_needed_for_ganon = {player: random.randint(0, 7) if args.crystals_ganon[player] == 'random' else int(args.crystals_ganon[player]) for player in range(1, world.players + 1)} world.crystals_needed_for_gt = {player: random.randint(0, 7) if args.crystals_gt[player] == 'random' else int(args.crystals_gt[player]) for player in range(1, world.players + 1)} world.crystals_ganon_orig = args.crystals_ganon.copy() @@ -189,6 +189,8 @@ def main(args, seed=None, fish=None): for player in range(1, world.players + 1): link_overworld(world, player) + update_world_regions(world, player) + create_flute_exits(world, player) logger.info(world.fish.translate("cli","cli","shuffling.world")) @@ -280,7 +282,7 @@ def main(args, seed=None, fish=None): balance_multiworld_progression(world) # if we only check for beatable, we can do this sanity check first before creating the rom - if not world.can_beat_game(): + if not world.can_beat_game(log_error=True): raise RuntimeError(world.fish.translate("cli","cli","cannot.beat.game")) for player in range(1, world.players+1): @@ -288,7 +290,7 @@ def main(args, seed=None, fish=None): customize_shops(world, player) balance_money_progression(world) - if world.owShuffle[1] != 'vanilla' or world.owSwap[1] != 'vanilla' or str(world.seed).startswith('M'): + if world.owShuffle[1] != 'vanilla' or world.owCrossed[1] != 'none' or world.owMixed[1] or str(world.seed).startswith('M'): outfilebase = f'OR_{args.outputname if args.outputname else world.seed}' else: outfilebase = f'DR_{args.outputname if args.outputname else world.seed}' @@ -332,7 +334,8 @@ def main(args, seed=None, fish=None): apply_rom_settings(rom, args.heartbeep[player], args.heartcolor[player], args.quickswap[player], args.fastmenu[player], args.disablemusic[player], args.sprite[player], - args.ow_palettes[player], args.uw_palettes[player], args.reduce_flashing[player]) + args.ow_palettes[player], args.uw_palettes[player], args.reduce_flashing[player], + args.shuffle_sfx[player]) if args.jsonout: jsonout[f'patch_t{team}_p{player}'] = rom.patches @@ -393,7 +396,7 @@ def main(args, seed=None, fish=None): def copy_world(world): # ToDo: Not good yet - ret = World(world.players, world.owShuffle, world.owSwap, world.shuffle, world.doorShuffle, world.logic, world.mode, world.swords, + ret = World(world.players, world.owShuffle, world.owCrossed, world.owMixed, world.shuffle, world.doorShuffle, world.logic, world.mode, world.swords, world.difficulty, world.difficulty_adjustments, world.timer, world.progressive, world.goal, world.algorithm, world.accessibility, world.shuffle_ganon, world.retro, world.custom, world.customitemarray, world.hints) ret.teams = world.teams @@ -422,7 +425,7 @@ def copy_world(world): ret.compassshuffle = world.compassshuffle.copy() ret.keyshuffle = world.keyshuffle.copy() ret.bigkeyshuffle = world.bigkeyshuffle.copy() - ret.bomblogic = world.bomblogic.copy() + ret.bombbag = world.bombbag.copy() ret.crystals_needed_for_ganon = world.crystals_needed_for_ganon.copy() ret.crystals_needed_for_gt = world.crystals_needed_for_gt.copy() ret.crystals_ganon_orig = world.crystals_ganon_orig.copy() @@ -447,8 +450,12 @@ def copy_world(world): ret.owflutespots = world.owflutespots.copy() ret.prizes = world.prizes.copy() + ret.exp_cache = world.exp_cache.copy() + for player in range(1, world.players + 1): create_regions(ret, player) + update_world_regions(ret, player) + create_flute_exits(ret, player) create_dungeon_regions(ret, player) create_shops(ret, player) create_rooms(ret, player) @@ -503,6 +510,7 @@ def copy_world(world): # these need to be modified properly by set_rules new_location.access_rule = lambda state: True new_location.item_rule = lambda state: True + new_location.forced_item = location.forced_item # copy remaining itempool. No item in itempool should have an assigned location for item in world.itempool: @@ -585,11 +593,11 @@ def create_playthrough(world): while sphere_candidates: state.sweep_for_events(key_only=True) - sphere = [] + sphere = set() # build up spheres of collection radius. Everything in each sphere is independent from each other in dependencies and only depends on lower spheres for location in sphere_candidates: if state.can_reach(location) and state.not_flooding_a_key(world, location): - sphere.append(location) + sphere.add(location) for location in sphere: sphere_candidates.remove(location) @@ -610,21 +618,24 @@ def create_playthrough(world): # in the second phase, we cull each sphere such that the game is still beatable, reducing each range of influence to the bare minimum required inside it for num, sphere in reversed(list(enumerate(collection_spheres))): - to_delete = [] + to_delete = set() for location in sphere: # we remove the item at location and check if game is still beatable logging.getLogger('').debug('Checking if %s (Player %d) is required to beat the game.', location.item.name, location.item.player) old_item = location.item location.item = None + # todo: this is not very efficient, but I'm not sure how else to do it for this backwards logic + # world.clear_exp_cache() if world.can_beat_game(state_cache[num]): - to_delete.append(location) + # logging.getLogger('').debug(f'{old_item.name} (Player {old_item.player}) is not required') + to_delete.add(location) else: # still required, got to keep it around + # logging.getLogger('').debug(f'{old_item.name} (Player {old_item.player}) is required') location.item = old_item # cull entries in spheres for spoiler walkthrough at end - for location in to_delete: - sphere.remove(location) + sphere -= to_delete # second phase, sphere 0 for item in [i for i in world.precollected_items if i.advancement]: @@ -640,7 +651,7 @@ def create_playthrough(world): # used to access it was deemed not required.) So we need to do one final sphere collection pass # to build up the correct spheres - required_locations = [item for sphere in collection_spheres for item in sphere] + required_locations = {item for sphere in collection_spheres for item in sphere} state = CollectionState(world) collection_spheres = [] while required_locations: @@ -656,7 +667,10 @@ def create_playthrough(world): logging.getLogger('').debug(world.fish.translate("cli","cli","building.final.spheres"), len(collection_spheres), len(sphere), len(required_locations)) if not sphere: - raise RuntimeError(world.fish.translate("cli","cli","cannot.reach.required")) + if world.has_beaten_game(state): + required_locations.clear() + else: + raise RuntimeError(world.fish.translate("cli","cli","cannot.reach.required")) # store the required locations for statistical analysis old_world.required_locations = [(location.name, location.player) for sphere in collection_spheres for location in sphere] @@ -677,11 +691,11 @@ def create_playthrough(world): old_world.spoiler.paths = dict() for player in range(1, world.players + 1): old_world.spoiler.paths.update({location.gen_name(): get_path(state, location.parent_region) for sphere in collection_spheres for location in sphere if location.player == player}) - for _, path in dict(old_world.spoiler.paths).items(): + for path in dict(old_world.spoiler.paths).values(): if any(exit == 'Pyramid Fairy' for (_, exit) in path): old_world.spoiler.paths[str(world.get_region('Big Bomb Shop', player))] = get_path(state, world.get_region('Big Bomb Shop', player)) # we can finally output our playthrough - old_world.spoiler.playthrough = OrderedDict([("0", [str(item) for item in world.precollected_items if item.advancement])]) + old_world.spoiler.playthrough = {"0": [str(item) for item in world.precollected_items if item.advancement]} for i, sphere in enumerate(collection_spheres): old_world.spoiler.playthrough[str(i + 1)] = {location.gen_name(): str(location.item) for location in sphere} diff --git a/MultiClient.py b/MultiClient.py index bf64a2d7..501630e0 100644 --- a/MultiClient.py +++ b/MultiClient.py @@ -846,7 +846,7 @@ async def track_locations(ctx : Context, roomid, roomdata): new_locations.append(Regions.lookup_name_to_id[location]) try: - if roomid in location_shop_ids: + if ctx.shop_mode or ctx.retro_mode: misc_data = await snes_read(ctx, SHOP_ADDR, SHOP_SRAM_LEN) for cnt, b in enumerate(misc_data): my_check = Regions.shop_table_by_location_id[0x400000 + cnt] diff --git a/Mystery.py b/Mystery.py index 7fca2d3d..33719b69 100644 --- a/Mystery.py +++ b/Mystery.py @@ -31,6 +31,7 @@ def main(): parser.add_argument('--rom') parser.add_argument('--enemizercli') parser.add_argument('--outputpath') + parser.add_argument('--loglevel', default='info', choices=['debug', 'info', 'warning', 'error', 'critical']) for player in range(1, multiargs.multi + 1): parser.add_argument(f'--p{player}', help=argparse.SUPPRESS) args = parser.parse_args() @@ -63,6 +64,7 @@ def main(): erargs.race = True erargs.outputname = seedname erargs.outputpath = args.outputpath + erargs.loglevel = args.loglevel if args.rom: erargs.rom = args.rom @@ -133,9 +135,9 @@ def roll_settings(weights): overworld_shuffle = get_choice('overworld_shuffle') ret.ow_shuffle = overworld_shuffle if overworld_shuffle != 'none' else 'vanilla' - overworld_swap = get_choice('overworld_swap') - ret.ow_swap = overworld_swap if overworld_swap != 'none' else 'vanilla' - ret.ow_keepsimilar = get_choice('overworld_keepsimilar') + ret.ow_crossed = get_choice('overworld_crossed') + ret.ow_keepsimilar = get_choice('overworld_keepsimilar') == 'on' + ret.ow_mixed = get_choice('overworld_mixed') == 'on' overworld_flute = get_choice('flute_shuffle') ret.ow_fluteshuffle = overworld_flute if overworld_flute != 'none' else 'vanilla' entrance_shuffle = get_choice('entrance_shuffle') @@ -186,7 +188,7 @@ def roll_settings(weights): ret.retro = True ret.retro = get_choice('retro') == 'on' # this overrides world_state if used - ret.bomblogic = get_choice('bomblogic') == 'on' + ret.bombbag = get_choice('bombbag') == 'on' ret.hints = get_choice('hints') == 'on' @@ -247,6 +249,7 @@ def roll_settings(weights): ret.heartbeep = get_choice('heartbeep', romweights) ret.ow_palettes = get_choice('ow_palettes', romweights) ret.uw_palettes = get_choice('uw_palettes', romweights) + ret.shuffle_sfx = get_choice('shuffle_sfx', romweights) == 'on' return ret diff --git a/OWEdges.py b/OWEdges.py index b8c5ceab..ebf1b15b 100644 --- a/OWEdges.py +++ b/OWEdges.py @@ -697,7 +697,6 @@ OWTileRegions = bidict({ 'Spectacle Rock Ledge': 0x03, 'West Death Mountain (Bottom)': 0x03, - 'Death Mountain Floating Island': 0x05, 'East Death Mountain (Top West)': 0x05, 'East Death Mountain (Top East)': 0x05, 'Spiral Cave Ledge': 0x05, @@ -706,6 +705,7 @@ OWTileRegions = bidict({ 'Fairy Ascension Plateau': 0x05, 'East Death Mountain (Bottom Left)': 0x05, 'East Death Mountain (Bottom)': 0x05, + 'Death Mountain Floating Island': 0x05, 'Death Mountain TR Pegs': 0x07, 'Death Mountain TR Pegs Ledge': 0x07, @@ -846,6 +846,9 @@ OWTileRegions = bidict({ 'East Dark Death Mountain (Top)': 0x45, 'East Dark Death Mountain (Bottom Left)': 0x45, 'East Dark Death Mountain (Bottom)': 0x45, + 'Dark Death Mountain Ledge': 0x45, + 'Dark Death Mountain Isolated Ledge': 0x45, + 'Dark Death Mountain Floating Island': 0x45, 'Turtle Rock Area': 0x47, 'Turtle Rock Ledge': 0x47, @@ -867,6 +870,7 @@ OWTileRegions = bidict({ 'Dark Chapel Area': 0x53, 'Dark Graveyard Area': 0x54, + 'Dark Graveyard North': 0x54, 'Qirn Jump Area': 0x55, 'Qirn Jump East Bank': 0x55, diff --git a/OverworldShuffle.py b/OverworldShuffle.py index 612a5f7e..558f089f 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -1,8 +1,8 @@ import RaceRandom as random, logging, copy -from BaseClasses import OWEdge, WorldType, RegionType, Direction, Terrain, PolSlot +from BaseClasses import OWEdge, WorldType, RegionType, Direction, Terrain, PolSlot, Entrance from OWEdges import OWTileRegions, OWTileGroups, OWEdgeGroups, OpenStd, parallel_links, IsParallel -__version__ = '0.1.7.2-u' +__version__ = '0.1.9.1-u' def link_overworld(world, player): # setup mandatory connections @@ -11,213 +11,254 @@ def link_overworld(world, player): for exitname, destname in temporary_mandatory_connections: connect_two_way(world, exitname, destname, player) - # tile shuffle - trimmed_groups = copy.deepcopy(OWEdgeGroups) - if world.owSwap[player] != 'vanilla': - tile_groups = {} - for (name, groupType) in OWTileGroups.keys(): - if world.mode[player] != 'standard' or name not in ['Castle', 'Links', 'Central Bonk Rocks']: - if world.shuffle[player] in ['vanilla', 'simple', 'dungeonssimple']: - tile_groups[(name,)] = ([], [], []) + def performSwap(groups, swaps): + def getParallel(edgename): + if edgename in parallel_links: + return parallel_links[edgename] + elif edgename in parallel_links.inverse: + return parallel_links.inverse[edgename][0] + else: + raise Exception('No parallel edge found for edge %s', edgename) + + def getNewSets(all_set, other_set): + new_all_set = list(map(getParallel, all_set)) + if not all(edge in orig_swaps for edge in new_all_set): + raise Exception('Cannot move a parallel edge without the other') + else: + for edge in new_all_set: + swaps.remove(edge) + new_other_set = getNewSet(other_set) + return (new_all_set, new_other_set) + + def getNewSet(edge_set): + new_set = [] + for edge in edge_set: + if edge in orig_swaps: + new_edge = getParallel(edge) + if new_edge not in orig_swaps: + raise Exception('Cannot move a parallel edge without the other') + new_set.append(new_edge) + swaps.remove(new_edge) else: - tile_groups[(name, groupType)] = ([], [], []) - - for (name, groupType) in OWTileGroups.keys(): - if world.mode[player] != 'standard' or name not in ['Castle', 'Links', 'Central Bonk Rocks']: - (lw_owids, dw_owids) = OWTileGroups[(name, groupType,)] - if world.shuffle[player] in ['vanilla', 'simple', 'dungeonssimple']: - (exist_owids, exist_lw_regions, exist_dw_regions) = tile_groups[(name,)] - exist_owids.extend(lw_owids) - exist_owids.extend(dw_owids) - for owid in lw_owids: - exist_lw_regions.extend(OWTileRegions.inverse[owid]) - for owid in dw_owids: - exist_dw_regions.extend(OWTileRegions.inverse[owid]) - tile_groups[(name,)] = (exist_owids, exist_lw_regions, exist_dw_regions) + new_set.append(edge) + return new_set + + # swaps edges from one pool to another + orig_swaps = copy.deepcopy(swaps) + new_groups = {} + for group in groups.keys(): + new_groups[group] = ([],[]) + + for group in groups.keys(): + (mode, wrld, dir, terrain, parallel, count) = group + for (forward_set, back_set) in zip(groups[group][0], groups[group][1]): + anyF = any(edge in orig_swaps for edge in forward_set) + anyB = any(edge in orig_swaps for edge in back_set) + allF = all(edge in orig_swaps for edge in forward_set) + allB = all(edge in orig_swaps for edge in back_set) + if not (anyF or anyB): + # no change + new_groups[group][0].append(forward_set) + new_groups[group][1].append(back_set) + elif allF and allB: + # move both sets + if parallel == IsParallel.Yes and not (all(edge in orig_swaps for edge in map(getParallel, forward_set)) and all(edge in orig_swaps for edge in map(getParallel, back_set))): + raise Exception('Cannot move a parallel edge without the other') + new_groups[(OpenStd.Open, WorldType((int(wrld) + 1) % 2), dir, terrain, parallel, count)][0].append(forward_set) + new_groups[(OpenStd.Open, WorldType((int(wrld) + 1) % 2), dir, terrain, parallel, count)][1].append(back_set) + for edge in forward_set: + swaps.remove(edge) + for edge in back_set: + swaps.remove(edge) + elif anyF or anyB: + if parallel == IsParallel.Yes: + if allF or allB: + # move one set + if allF and not (world.owKeepSimilar[player] and anyB): + (new_forward_set, new_back_set) = getNewSets(forward_set, back_set) + elif allB and not (world.owKeepSimilar[player] and anyF): + (new_back_set, new_forward_set) = getNewSets(back_set, forward_set) + else: + raise Exception('Cannot move an edge out of a Similar group') + new_groups[group][0].append(new_forward_set) + new_groups[group][1].append(new_back_set) + else: + # move individual edges + if not world.owKeepSimilar[player]: + new_groups[group][0].append(getNewSet(forward_set) if anyF else forward_set) + new_groups[group][1].append(getNewSet(back_set) if anyB else back_set) + else: + raise Exception('Cannot move an edge out of a Similar group') + else: + raise NotImplementedError('Cannot move one side of a non-parallel connection') else: - (exist_owids, exist_lw_regions, exist_dw_regions) = tile_groups[(name, groupType)] - exist_owids.extend(lw_owids) - exist_owids.extend(dw_owids) - for owid in lw_owids: - exist_lw_regions.extend(OWTileRegions.inverse[owid]) - for owid in dw_owids: - exist_dw_regions.extend(OWTileRegions.inverse[owid]) - tile_groups[(name, groupType)] = (exist_owids, exist_lw_regions, exist_dw_regions) - - #tile shuffle happens here, the groups that remain in the list are the tiles that get swapped - removed = list() - for group in tile_groups.keys(): - if random.randint(0, 1): - removed.append(group) - for group in removed: - tile_groups.pop(group, None) - - #save shuffled tiles to world object - for group in tile_groups.keys(): - (owids, lw_regions, dw_regions) = tile_groups[group] - (exist_owids, exist_lw_regions, exist_dw_regions) = world.owswaps[player] - exist_owids.extend(owids) - exist_lw_regions.extend(lw_regions) - exist_dw_regions.extend(dw_regions) - world.owswaps[player] = [exist_owids, exist_lw_regions, exist_dw_regions] - - #replace LW edges with DW - ignore_list = list() #TODO: Remove ignore_list when special OW areas are included in pool - for edgeset in temporary_mandatory_connections: - for edge in edgeset: - ignore_list.append(edge) - - swapped_edges = list() - def getSwappedEdges(world, lst, player): - for regionname in lst: - region = world.get_region(regionname, player) - for exit in region.exits: - if exit.spot_type == 'OWEdge' and exit.name not in ignore_list: - swapped_edges.append(exit.name) - - getSwappedEdges(world, world.owswaps[player][1], player) - getSwappedEdges(world, world.owswaps[player][2], player) - - def performSwap(groups, swaps, nonParallelOnly=False): - try: - for group in groups.keys(): - (mode, wrld, dir, terrain, parallel, count) = group - for p in range(0, len(groups[group])): - edgepool = groups[group][p] - for s in range(0, len(edgepool)): - if s <= len(edgepool): - for e in range(0, len(edgepool[s])): - if len(edgepool) > 0 and edgepool[s][e] in swaps: - if parallel == IsParallel.Yes: - if not nonParallelOnly: - if wrld == WorldType.Light and edgepool[s][e] in parallel_links: - logging.getLogger('').debug('%s was moved', edgepool[s][e]) - swaps.remove(edgepool[s][e]) - groups[group][p][s][e] = parallel_links[edgepool[s][e]] - elif wrld == WorldType.Dark and edgepool[s][e] in parallel_links.inverse: - logging.getLogger('').debug('%s was moved', edgepool[s][e]) - swaps.remove(edgepool[s][e]) - groups[group][p][s][e] = parallel_links.inverse[edgepool[s][e]][0] - else: - for edge in edgepool[s]: - logging.getLogger('').debug('%s was moved', edge) - swaps.remove(edge) - groups[(mode, WorldType((int(wrld) + 1) % 2), dir, terrain, parallel, count)][p].append(edgepool[s]) - groups[group][p].remove(edgepool[s]) - except IndexError: - #TODO: Figure out a way to handle index changes on the fly when removing items - logging.getLogger('').warning('OW Tile Swap encountered minor IndexError... retrying') - - if 0x28 in world.owswaps[player][0]: #handle Frog/Dig Game swap manually due to NP/P relationship with LW - trimmed_groups[(OpenStd.Open, WorldType.Dark, PolSlot.EastWest, Terrain.Land, IsParallel.Yes, 1)][0].append(['Maze Race ES']) - trimmed_groups[(OpenStd.Open, WorldType.Dark, PolSlot.EastWest, Terrain.Land, IsParallel.Yes, 1)][1].append(['Kakariko Suburb WS']) - trimmed_groups[(OpenStd.Open, WorldType.Light, PolSlot.EastWest, Terrain.Land, IsParallel.Yes, 1)][0].remove(['Maze Race ES']) - trimmed_groups[(OpenStd.Open, WorldType.Light, PolSlot.EastWest, Terrain.Land, IsParallel.Yes, 1)][1].remove(['Kakariko Suburb WS']) - - trimmed_groups[(OpenStd.Open, WorldType.Light, PolSlot.EastWest, Terrain.Land, IsParallel.No, 2)][0].append(['Dig Game EC', 'Dig Game ES']) - trimmed_groups[(OpenStd.Open, WorldType.Light, PolSlot.EastWest, Terrain.Land, IsParallel.No, 2)][1].append(['Frog WC', 'Frog WS']) - trimmed_groups[(OpenStd.Open, WorldType.Dark, PolSlot.EastWest, Terrain.Land, IsParallel.No, 2)] = [[],[]] - - swapped_edges.remove('Maze Race ES') - swapped_edges.remove('Kakariko Suburb WS') - swapped_edges.remove('Dig Game EC') - swapped_edges.remove('Dig Game ES') - swapped_edges.remove('Frog WC') - swapped_edges.remove('Frog WS') - - tries = 5 - while tries > 0: - performSwap(trimmed_groups, swapped_edges) - if len(swapped_edges) == 0: - tries = 0 - continue - tries -= 1 - assert len(swapped_edges) == 0 - - #move swapped regions to other world - if world.owSwap[player] == 'mixed': - for name in world.owswaps[player][1]: - region = world.get_region(name, player) - region.type = RegionType.DarkWorld - for name in world.owswaps[player][2]: - region = world.get_region(name, player) - region.type = RegionType.LightWorld + raise NotImplementedError('Invalid OW Edge swap scenario') + return new_groups - # make new connections + tile_groups = reorganize_tile_groups(world, player) + trimmed_groups = copy.deepcopy(OWEdgeGroups) + swapped_edges = list() + + # restructure Maze Race/Suburb/Frog/Dig Game manually due to NP/P relationship + if world.owKeepSimilar[player]: + for group in trimmed_groups.keys(): + (std, region, axis, terrain, parallel, _) = group + if parallel == IsParallel.Yes: + (forward_edges, back_edges) = trimmed_groups[group] + if ['Maze Race ES'] in forward_edges: + forward_edges = list(filter((['Maze Race ES']).__ne__, forward_edges)) + trimmed_groups[(std, region, axis, terrain, IsParallel.No, 1)][0].append(['Maze Race ES']) + if ['Kakariko Suburb WS'] in back_edges: + back_edges = list(filter((['Kakariko Suburb WS']).__ne__, back_edges)) + trimmed_groups[(std, region, axis, terrain, IsParallel.No, 1)][1].append(['Kakariko Suburb WS']) + trimmed_groups[group] = (forward_edges, back_edges) + else: + for group in trimmed_groups.keys(): + (std, region, axis, terrain, _, _) = group + (forward_edges, back_edges) = trimmed_groups[group] + if ['Dig Game EC', 'Dig Game ES'] in forward_edges: + forward_edges = list(filter((['Dig Game EC', 'Dig Game ES']).__ne__, forward_edges)) + trimmed_groups[(std, region, axis, terrain, IsParallel.Yes, 1)][0].append(['Dig Game ES']) + trimmed_groups[(std, region, axis, terrain, IsParallel.No, 1)][0].append(['Dig Game EC']) + if ['Frog WC', 'Frog WS'] in back_edges: + back_edges = list(filter((['Frog WC', 'Frog WS']).__ne__, back_edges)) + trimmed_groups[(std, region, axis, terrain, IsParallel.Yes, 1)][1].append(['Frog WS']) + trimmed_groups[(std, region, axis, terrain, IsParallel.No, 1)][1].append(['Frog WC']) + trimmed_groups[group] = (forward_edges, back_edges) + + # tile shuffle + logging.getLogger('').debug('Swapping overworld tiles') + if world.owMixed[player]: + swapped_edges = shuffle_tiles(world, tile_groups, world.owswaps[player], player) + + # move swapped regions/edges to other world + trimmed_groups = performSwap(trimmed_groups, swapped_edges) + assert len(swapped_edges) == 0, 'Not all edges were swapped successfully: ' + ', '.join(swapped_edges ) + + update_world_regions(world, player) + + # apply tile logical connections for owid in ow_connections.keys(): - if (world.mode[player] == 'inverted') == (owid in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (owid in world.owswaps[player][0] and world.owMixed[player]): for (exitname, regionname) in ow_connections[owid][0]: connect_simple(world, exitname, regionname, player) else: for (exitname, regionname) in ow_connections[owid][1]: connect_simple(world, exitname, regionname, player) - connected_edges = [] - - connect_custom(world, connected_edges, player) + # crossed shuffle + logging.getLogger('').debug('Crossing overworld edges') + if world.owCrossed[player] in ['grouped', 'limited', 'chaos']: + if world.owCrossed[player] == 'grouped': + crossed_edges = shuffle_tiles(world, tile_groups, [[],[],[]], player) + elif world.owCrossed[player] in ['limited', 'chaos']: + crossed_edges = list() + crossed_candidates = list() + for group in trimmed_groups.keys(): + (mode, wrld, dir, terrain, parallel, count) = group + if parallel == IsParallel.Yes and wrld == WorldType.Light and (mode == OpenStd.Open or world.mode[player] != 'standard'): + for (forward_set, back_set) in zip(trimmed_groups[group][0], trimmed_groups[group][1]): + if world.owKeepSimilar[player]: + if world.owCrossed[player] == 'chaos' and random.randint(0, 1): + for edge in forward_set: + crossed_edges.append(edge) + elif world.owCrossed[player] == 'limited': + crossed_candidates.append(forward_set) + else: + for edge in forward_set: + if world.owCrossed[player] == 'chaos' and random.randint(0, 1): + crossed_edges.append(edge) + elif world.owCrossed[player] == 'limited': + crossed_candidates.append(edge) + if world.owCrossed[player] == 'limited': + random.shuffle(crossed_candidates) + for edge_set in crossed_candidates[:9]: + for edge in edge_set: + crossed_edges.append(edge) + for edge in copy.deepcopy(crossed_edges): + if edge in parallel_links: + crossed_edges.append(parallel_links[edge]) + elif edge in parallel_links.inverse: + crossed_edges.append(parallel_links.inverse[edge][0]) + + trimmed_groups = performSwap(trimmed_groups, crossed_edges) + assert len(crossed_edges) == 0, 'Not all edges were crossed successfully: ' + ', '.join(crossed_edges) # layout shuffle + logging.getLogger('').debug('Shuffling overworld layout') + connected_edges = [] + if world.owShuffle[player] == 'vanilla': - for grouping in (trimmed_groups,): - groups = list(trimmed_groups.values()) + # vanilla transitions + groups = list(trimmed_groups.values()) for (forward_edge_sets, back_edge_sets) in groups: assert len(forward_edge_sets) == len(back_edge_sets) for (forward_set, back_set) in zip(forward_edge_sets, back_edge_sets): assert len(forward_set) == len(back_set) for (forward_edge, back_edge) in zip(forward_set, back_set): - connect_two_way(world, forward_edge, back_edge, player) - connected_edges.append(forward_edge) - connected_edges.append(back_edge) - - assert len(connected_edges) == len(default_connections) * 2, connected_edges + connect_two_way(world, forward_edge, back_edge, player, connected_edges) else: - if world.owKeepSimilar[player] and world.owShuffle[player] == 'parallel': + if world.owKeepSimilar[player] and world.owShuffle[player] in ['vanilla', 'parallel']: for exitname, destname in parallelsimilar_connections: - connect_two_way(world, exitname, destname, player) - connected_edges.append(exitname) - connected_edges.append(destname) + connect_two_way(world, exitname, destname, player, connected_edges) #TODO: Remove, just for testing for exitname, destname in test_connections: - connect_two_way(world, exitname, destname, player) - connected_edges.append(exitname) - connected_edges.append(destname) + connect_two_way(world, exitname, destname, player, connected_edges) + connect_custom(world, connected_edges, player) + + # layout shuffle trimmed_groups = remove_reserved(world, trimmed_groups, connected_edges, player) - groups = reorganize_groups(world, trimmed_groups, player) - #all shuffling occurs here - random.shuffle(groups) + if world.mode[player] == 'standard': + random.shuffle(groups[2:]) # keep first 2 groups (Standard) first + else: + random.shuffle(groups) + for (forward_edge_sets, back_edge_sets) in groups: assert len(forward_edge_sets) == len(back_edge_sets) + random.shuffle(forward_edge_sets) random.shuffle(back_edge_sets) - for (forward_set, back_set) in zip(forward_edge_sets, back_edge_sets): - assert len(forward_set) == len(back_set) - for (forward_edge, back_edge) in zip(forward_set, back_set): - connect_two_way(world, forward_edge, back_edge, player) - connected_edges.append(forward_edge) - connected_edges.append(back_edge) - if world.owShuffle[player] == 'parallel': - if forward_edge in parallel_links.keys() or forward_edge in parallel_links.inverse.keys(): - try: - parallel_forward_edge = parallel_links[forward_edge] if forward_edge in parallel_links.keys() else parallel_links.inverse[forward_edge][0] - parallel_back_edge = parallel_links[back_edge] if back_edge in parallel_links.keys() else parallel_links.inverse[back_edge][0] - connect_two_way(world, parallel_forward_edge, parallel_back_edge, player) - connected_edges.append(parallel_forward_edge) - connected_edges.append(parallel_back_edge) - except KeyError: - # TODO: Figure out why non-parallel edges are getting into parallel groups - raise KeyError('No parallel edge for edge %d' % back_edge) - - assert len(connected_edges) == len(default_connections) * 2, connected_edges - + if len(forward_edge_sets) > 0: + f = 0 + b = 0 + while f < len(forward_edge_sets) and b < len(back_edge_sets): + forward_set = forward_edge_sets[f] + back_set = back_edge_sets[b] + while forward_set[0] in connected_edges: + f += 1 + if f < len(forward_edge_sets): + forward_set = forward_edge_sets[f] + else: + forward_set = None + break + f += 1 + while back_set[0] in connected_edges: + b += 1 + if b < len(back_edge_sets): + back_set = back_edge_sets[b] + else: + back_set = None + break + b += 1 + if forward_set is not None and back_set is not None: + assert len(forward_set) == len(back_set) + for (forward_edge, back_edge) in zip(forward_set, back_set): + connect_two_way(world, forward_edge, back_edge, player, connected_edges) + elif forward_set is not None: + logging.getLogger('').warning("Edge '%s' could not find a valid connection" % forward_set[0]) + elif back_set is not None: + logging.getLogger('').warning("Edge '%s' could not find a valid connection" % back_set[0]) + assert len(connected_edges) == len(default_connections) * 2, connected_edges + # flute shuffle def connect_flutes(flute_destinations): for o in range(0, len(flute_destinations)): owslot = flute_destinations[o] regions = flute_data[owslot][0] - if (world.mode[player] == 'inverted') == (flute_data[owslot][1] in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (flute_data[owslot][1] in world.owswaps[player][0] and world.owMixed[player]): connect_simple(world, 'Flute Spot ' + str(o + 1), regions[0], player) else: connect_simple(world, 'Flute Spot ' + str(o + 1), regions[1], player) @@ -239,7 +280,7 @@ def link_overworld(world, player): new_ignored.add(exit.connected_region.name) getIgnored(exit.connected_region.name, base_owid, OWTileRegions[exit.connected_region.name]) - if (world.mode[player] == 'inverted') == (flute_data[owid][1] in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (flute_data[owid][1] in world.owswaps[player][0] and world.owMixed[player]): new_region = flute_data[owid][0][0] else: new_region = flute_data[owid][0][1] @@ -276,21 +317,39 @@ def link_overworld(world, player): world.owflutespots[player] = new_spots connect_flutes(new_spots) - def connect_custom(world, connected_edges, player): if hasattr(world, 'custom_overworld') and world.custom_overworld[player]: for edgename1, edgename2 in world.custom_overworld[player]: - connect_two_way(world, edgename1, edgename2, player) - connected_edges.append(edgename1) - connected_edges.append(edgename2) + if edgename1 in connected_edges or edgename2 in connected_edges: + owedge1 = world.check_for_owedge(edgename1, player) + owedge2 = world.check_for_owedge(edgename2, player) + if owedge1.dest is not None and owedge1.dest.name == owedge2.name: + continue # if attempting to connect a pair that was already connected earlier, allow it to continue + raise RuntimeError('Invalid plando connection: rule violation based on current settings') + connect_two_way(world, edgename1, edgename2, player, connected_edges) + if world.owKeepSimilar[player]: #TODO: If connecting an edge that belongs to a similar pair, the remaining edges need to get connected automatically + continue def connect_simple(world, exitname, regionname, player): world.get_entrance(exitname, player).connect(world.get_region(regionname, player)) -def connect_two_way(world, edgename1, edgename2, player): +def connect_two_way(world, edgename1, edgename2, player, connected_edges=None): edge1 = world.get_entrance(edgename1, player) edge2 = world.get_entrance(edgename2, player) - + x = world.check_for_owedge(edgename1, player) + y = world.check_for_owedge(edgename2, player) + + if x is None: + raise Exception('%s is not a valid edge.', edgename1) + elif y is None: + raise Exception('%s is not a valid edge.', edgename2) + if connected_edges is not None: + if edgename1 in connected_edges or edgename2 in connected_edges: + if (x.dest and x.dest.name == edgename2) and (y.dest and y.dest.name == edgename1): + return + else: + raise Exception('Edges \'%s\' and \'%s\' already connected elsewhere', edgename1, edgename2) + # if these were already connected somewhere, remove the backreference if edge1.connected_region is not None: edge1.connected_region.entrances.remove(edge1) @@ -299,32 +358,114 @@ def connect_two_way(world, edgename1, edgename2, player): edge1.connect(edge2.parent_region) edge2.connect(edge1.parent_region) - x = world.check_for_owedge(edgename1, player) - y = world.check_for_owedge(edgename2, player) - if x is None: - logging.getLogger('').error('%s is not a valid edge.', edgename1) - elif y is None: - logging.getLogger('').error('%s is not a valid edge.', edgename2) - else: - x.dest = y - y.dest = x + x.dest = y + y.dest = x - if world.owShuffle[player] != 'vanilla' or world.owSwap[player] != 'vanilla': + if world.owShuffle[player] != 'vanilla' or world.owMixed[player] or world.owCrossed[player] != 'none': world.spoiler.set_overworld(edgename2, edgename1, 'both', player) + if connected_edges is not None: + connected_edges.append(edgename1) + connected_edges.append(edgename2) + + # connecting parallel connections + if world.owShuffle[player] in ['vanilla', 'parallel']: + if (edgename1 in parallel_links.keys() or edgename1 in parallel_links.inverse.keys()): + try: + parallel_forward_edge = parallel_links[edgename1] if edgename1 in parallel_links.keys() else parallel_links.inverse[edgename1][0] + parallel_back_edge = parallel_links[edgename2] if edgename2 in parallel_links.keys() else parallel_links.inverse[edgename2][0] + if not (parallel_forward_edge in connected_edges) and not (parallel_back_edge in connected_edges): + connect_two_way(world, parallel_forward_edge, parallel_back_edge, player, connected_edges) + except KeyError: + # TODO: Figure out why non-parallel edges are getting into parallel groups + raise KeyError('No parallel edge for edge %s' % edgename2) + +def shuffle_tiles(world, groups, result_list, player): + swapped_edges = list() + + # tile shuffle happens here + removed = list() + for group in groups.keys(): + if random.randint(0, 1): + removed.append(group) + + # save shuffled tiles to list + for group in groups.keys(): + if group not in removed: + (owids, lw_regions, dw_regions) = groups[group] + (exist_owids, exist_lw_regions, exist_dw_regions) = result_list + exist_owids.extend(owids) + exist_lw_regions.extend(lw_regions) + exist_dw_regions.extend(dw_regions) + result_list = [exist_owids, exist_lw_regions, exist_dw_regions] + + # replace LW edges with DW + ignore_list = list() #TODO: Remove ignore_list when special OW areas are included in pool + for edgeset in temporary_mandatory_connections: + for edge in edgeset: + ignore_list.append(edge) + + if world.owCrossed[player] != 'polar': + # in polar, the actual edge connections remain vanilla + def getSwappedEdges(world, lst, player): + for regionname in lst: + region = world.get_region(regionname, player) + for exit in region.exits: + if exit.spot_type == 'OWEdge' and exit.name not in ignore_list: + swapped_edges.append(exit.name) + + getSwappedEdges(world, result_list[1], player) + getSwappedEdges(world, result_list[2], player) + + return swapped_edges + +def reorganize_tile_groups(world, player): + groups = {} + for (name, groupType) in OWTileGroups.keys(): + if world.mode[player] != 'standard' or name not in ['Castle', 'Links', 'Central Bonk Rocks']: + if world.shuffle[player] in ['vanilla', 'simple', 'dungeonssimple']: + groups[(name,)] = ([], [], []) + else: + groups[(name, groupType)] = ([], [], []) + + for (name, groupType) in OWTileGroups.keys(): + if world.mode[player] != 'standard' or name not in ['Castle', 'Links', 'Central Bonk Rocks']: + (lw_owids, dw_owids) = OWTileGroups[(name, groupType,)] + if world.shuffle[player] in ['vanilla', 'simple', 'dungeonssimple']: + (exist_owids, exist_lw_regions, exist_dw_regions) = groups[(name,)] + exist_owids.extend(lw_owids) + exist_owids.extend(dw_owids) + for owid in lw_owids: + exist_lw_regions.extend(OWTileRegions.inverse[owid]) + for owid in dw_owids: + exist_dw_regions.extend(OWTileRegions.inverse[owid]) + groups[(name,)] = (exist_owids, exist_lw_regions, exist_dw_regions) + else: + (exist_owids, exist_lw_regions, exist_dw_regions) = groups[(name, groupType)] + exist_owids.extend(lw_owids) + exist_owids.extend(dw_owids) + for owid in lw_owids: + exist_lw_regions.extend(OWTileRegions.inverse[owid]) + for owid in dw_owids: + exist_dw_regions.extend(OWTileRegions.inverse[owid]) + groups[(name, groupType)] = (exist_owids, exist_lw_regions, exist_dw_regions) + return groups + def remove_reserved(world, groupedlist, connected_edges, player): new_grouping = {} for group in groupedlist.keys(): new_grouping[group] = ([], []) for group in groupedlist.keys(): - (std, region, axis, terrain, parallel, count) = group + (_, region, _, _, _, _) = group (forward_edges, back_edges) = groupedlist[group] + # remove edges already connected (thru plando and other forced connections) for edge in connected_edges: forward_edges = list(list(filter((edge).__ne__, i)) for i in forward_edges) back_edges = list(list(filter((edge).__ne__, i)) for i in back_edges) - + + # remove parallel edges from pool, since they get added during shuffle if world.owShuffle[player] == 'parallel' and region == WorldType.Dark: for edge in parallel_links: forward_edges = list(list(filter((parallel_links[edge]).__ne__, i)) for i in forward_edges) @@ -336,8 +477,6 @@ def remove_reserved(world, groupedlist, connected_edges, player): forward_edges = list(filter(([]).__ne__, forward_edges)) back_edges = list(filter(([]).__ne__, back_edges)) - #TODO: Remove edges set in connect_custom. The lists above can be left with invalid counts if connect_custom removes entries, they need to get put into their appropriate group - (exist_forward_edges, exist_back_edges) = new_grouping[group] exist_forward_edges.extend(forward_edges) exist_back_edges.extend(back_edges) @@ -347,11 +486,12 @@ def remove_reserved(world, groupedlist, connected_edges, player): return new_grouping def reorganize_groups(world, groups, player): + # predefined shuffle groups get reorganized here + # this restructures the candidate pool based on the chosen settings if world.owShuffle[player] == 'full': - #predefined shuffle groups get reorganized here if world.owKeepSimilar[player]: if world.mode[player] == 'standard': - #tuple stays (A,B,C,D,_,F) + # tuple goes to (A,B,C,D,_,F) for grouping in (groups,): new_grouping = {} @@ -369,7 +509,7 @@ def reorganize_groups(world, groups, player): return list(new_grouping.values()) else: - #tuple goes to (_,B,C,D,_,F) + # tuple goes to (_,B,C,D,_,F) for grouping in (groups,): new_grouping = {} @@ -388,7 +528,7 @@ def reorganize_groups(world, groups, player): return list(new_grouping.values()) else: if world.mode[player] == 'standard': - #tuple stays (A,B,C,D,_,_) + # tuple goes to (A,B,C,D,_,_) for grouping in (groups,): new_grouping = {} @@ -409,7 +549,7 @@ def reorganize_groups(world, groups, player): return list(new_grouping.values()) else: - #tuple goes to (_,B,C,D,_,_) + # tuple goes to (_,B,C,D,_,_) for grouping in (groups,): new_grouping = {} @@ -430,14 +570,13 @@ def reorganize_groups(world, groups, player): return list(new_grouping.values()) elif world.owShuffle[player] == 'parallel': - #predefined shuffle groups get reorganized here if world.owKeepSimilar[player]: if world.mode[player] == 'standard': - #tuple stays (A,B,C,D,E,F) + # tuple stays (A,B,C,D,E,F) for grouping in (groups,): return list(grouping.values()) else: - #tuple goes to (_,B,C,D,E,F) + # tuple goes to (_,B,C,D,E,F) for grouping in (groups,): new_grouping = {} @@ -456,7 +595,7 @@ def reorganize_groups(world, groups, player): return list(new_grouping.values()) else: if world.mode[player] == 'standard': - #tuple stays (A,B,C,D,E,_) + # tuple goes to (A,B,C,D,E,_) for grouping in (groups,): new_grouping = {} @@ -477,7 +616,7 @@ def reorganize_groups(world, groups, player): return list(new_grouping.values()) else: - #tuple goes to (_,B,C,D,E,_) + # tuple goes to (_,B,C,D,E,_) for grouping in (groups,): new_grouping = {} @@ -500,6 +639,25 @@ def reorganize_groups(world, groups, player): else: raise NotImplementedError('Shuffling not supported yet') +def create_flute_exits(world, player): + for region in (r for r in world.regions if r.player == player and r.terrain == Terrain.Land and r.name not in ['Zoras Domain', 'Master Sword Meadow', 'Hobo Bridge']): + if (not world.owMixed[player] and region.type == RegionType.LightWorld) \ + or (world.owMixed[player] and region.type in [RegionType.LightWorld, RegionType.DarkWorld] \ + and (region.name not in world.owswaps[player][1] or region.name in world.owswaps[player][2])): + exitname = 'Flute From ' + region.name + exit = Entrance(region.player, exitname, region) + exit.access_rule = lambda state: state.can_flute(player) + exit.connect(world.get_region('Flute Sky', player)) + region.exits.append(exit) + world.initialize_regions() + +def update_world_regions(world, player): + if world.owMixed[player]: + for name in world.owswaps[player][1]: + world.get_region(name, player).type = RegionType.DarkWorld + for name in world.owswaps[player][2]: + world.get_region(name, player).type = RegionType.LightWorld + test_connections = [ #('Links House ES', 'Octoballoon WS'), #('Links House NE', 'Lost Woods Pass SW') @@ -513,9 +671,7 @@ temporary_mandatory_connections = [ ] # these are connections that cannot be shuffled and always exist. They link together separate parts of the world we need to divide into regions -mandatory_connections = [('Flute Away', 'Flute Sky'), - - # Whirlpool Connections +mandatory_connections = [# Whirlpool Connections ('C Whirlpool', 'River Bend Water'), ('River Bend Whirlpool', 'C Whirlpool Water'), ('Lake Hylia Whirlpool', 'Zora Waterfall Water'), @@ -639,6 +795,8 @@ mandatory_connections = [('Flute Away', 'Flute Sky'), ('Skull Woods Pass Bush Row (East)', 'Skull Woods Pass West Area'), #pearl ('Skull Woods Pass Rock (Top)', 'Skull Woods Pass East Bottom Area'), #mitts ('Skull Woods Pass Rock (Bottom)', 'Skull Woods Pass East Top Area'), #mitts + ('Dark Graveyard Bush (South)', 'Dark Graveyard North'), #pearl + ('Dark Graveyard Bush (North)', 'Dark Graveyard Area'), #pearl ('Qirn Jump Water Drop', 'Qirn Jump Water'), #flippers ('Qirn Jump East Water Drop', 'Qirn Jump Water'), #flippers ('Qirn Jump Pier', 'Qirn Jump East Bank'), @@ -1100,9 +1258,8 @@ ow_connections = { } parallelsimilar_connections = [('Maze Race ES', 'Kakariko Suburb WS'), - ('Dig Game EC', 'Frog WC'), - ('Dig Game ES', 'Frog WS') - ] + ('Dig Game EC', 'Frog WC') + ] # non shuffled overworld default_connections = [#('Lost Woods NW', 'Master Sword Meadow SC'), diff --git a/Plando.py b/Plando.py index 2a4b911d..fa9fa848 100755 --- a/Plando.py +++ b/Plando.py @@ -9,6 +9,7 @@ import sys from BaseClasses import World from Regions import create_regions +from OverworldShuffle import link_overworld from EntranceShuffle import link_entrances, connect_entrance, connect_two_way, connect_exit from Rom import patch_rom, LocalRom, write_string_to_rom, apply_rom_settings, get_sprite_from_name from Rules import set_rules @@ -42,6 +43,11 @@ def main(args): create_regions(world, 1) create_dungeons(world, 1) + text_patches = [] + prefill_world(world, args.plando, text_patches) + + link_overworld(world, 1) + link_entrances(world, 1) logger.info('Calculating Access Rules.') @@ -50,9 +56,7 @@ def main(args): logger.info('Fill the world.') - text_patches = [] - - fill_world(world, args.plando, text_patches) + fill_world(world, args.plando) if world.get_entrance('Dam', 1).connected_region.name != 'Dam' or world.get_entrance('Swamp Palace', 1).connected_region.name != 'Swamp Palace (Entrance)': world.swamp_patch_required[1] = True @@ -92,75 +96,14 @@ def main(args): return world -def fill_world(world, plando, text_patches): - mm_medallion = 'Ether' - tr_medallion = 'Quake' +def fill_world(world, plando): logger = logging.getLogger('') with open(plando, 'r') as plandofile: for line in plandofile.readlines(): + line = line.lstrip() if line.startswith('#'): continue - if ':' in line: - line = line.lstrip() - - if line.startswith('!'): - if line.startswith('!mm_medallion'): - _, medallionstr = line.split(':', 1) - mm_medallion = medallionstr.strip() - elif line.startswith('!tr_medallion'): - _, medallionstr = line.split(':', 1) - tr_medallion = medallionstr.strip() - elif line.startswith('!mode'): - _, modestr = line.split(':', 1) - world.mode = {1: modestr.strip()} - elif line.startswith('!logic'): - _, logicstr = line.split(':', 1) - world.logic = {1: logicstr.strip()} - elif line.startswith('!goal'): - _, goalstr = line.split(':', 1) - world.goal = {1: goalstr.strip()} - elif line.startswith('!light_cone_sewers'): - _, sewerstr = line.split(':', 1) - world.sewer_light_cone = {1: sewerstr.strip().lower() == 'true'} - elif line.startswith('!light_cone_lw'): - _, lwconestr = line.split(':', 1) - world.light_world_light_cone = lwconestr.strip().lower() == 'true' - elif line.startswith('!light_cone_dw'): - _, dwconestr = line.split(':', 1) - world.dark_world_light_cone = dwconestr.strip().lower() == 'true' - elif line.startswith('!fix_trock_doors'): - _, trdstr = line.split(':', 1) - world.fix_trock_doors = {1: trdstr.strip().lower() == 'true'} - elif line.startswith('!fix_trock_exit'): - _, trfstr = line.split(':', 1) - world.fix_trock_exit = {1: trfstr.strip().lower() == 'true'} - elif line.startswith('!fix_gtower_exit'): - _, gtfstr = line.split(':', 1) - world.fix_gtower_exit = gtfstr.strip().lower() == 'true' - elif line.startswith('!fix_pod_exit'): - _, podestr = line.split(':', 1) - world.fix_palaceofdarkness_exit = {1: podestr.strip().lower() == 'true'} - elif line.startswith('!fix_skullwoods_exit'): - _, swestr = line.split(':', 1) - world.fix_skullwoods_exit = {1: swestr.strip().lower() == 'true'} - elif line.startswith('!check_beatable_only'): - _, chkbtstr = line.split(':', 1) - world.check_beatable_only = chkbtstr.strip().lower() == 'true' - elif line.startswith('!ganon_death_pyramid_respawn'): - _, gnpstr = line.split(':', 1) - world.ganon_at_pyramid = gnpstr.strip().lower() == 'true' - elif line.startswith('!save_quit_boss'): - _, sqbstr = line.split(':', 1) - world.save_and_quite_from_boss = sqbstr.strip().lower() == 'true' - elif line.startswith('!text_'): - textname, text = line.split(':', 1) - text_patches.append([textname.lstrip('!text_').strip(), 'text', text.strip()]) - #temporarilly removed. New credits system not ready to handle this. - #elif line.startswith('!credits_'): - # textname, text = line.split(':', 1) - # text_patches.append([textname.lstrip('!credits_').strip(), 'credits', text.strip()]) - continue - + elif ':' in line: locationstr, itemstr = line.split(':', 1) location = world.get_location(locationstr.strip(), 1) if location is None: @@ -172,6 +115,7 @@ def fill_world(world, plando, text_patches): world.push_item(location, item) if item.smallkey or item.bigkey: location.event = True + #TODO: move entrance stuff to prefill_world to work like OW stuff elif '<=>' in line: entrance, exit = line.split('<=>', 1) connect_two_way(world, entrance.strip(), exit.strip(), 1) @@ -182,14 +126,106 @@ def fill_world(world, plando, text_patches): entrance, exit = line.split('<=', 1) connect_exit(world, exit.strip(), entrance.strip(), 1) - world.required_medallions[1] = (mm_medallion, tr_medallion) - # set up Agahnim Events world.get_location('Agahnim 1', 1).event = True world.get_location('Agahnim 1', 1).item = ItemFactory('Beat Agahnim 1', 1) world.get_location('Agahnim 2', 1).event = True world.get_location('Agahnim 2', 1).item = ItemFactory('Beat Agahnim 2', 1) +def prefill_world(world, plando, text_patches): + mm_medallion = 'Ether' + tr_medallion = 'Quake' + logger = logging.getLogger('') + with open(plando, 'r') as plandofile: + for line in plandofile.readlines(): + line = line.lstrip() + if line.startswith('#'): + continue + elif line.startswith('!'): + if line.startswith('!mm_medallion'): + _, medallionstr = line.split(':', 1) + mm_medallion = medallionstr.strip() + elif line.startswith('!tr_medallion'): + _, medallionstr = line.split(':', 1) + tr_medallion = medallionstr.strip() + elif line.startswith('!mode'): + _, modestr = line.split(':', 1) + world.mode = {1: modestr.strip()} + elif line.startswith('!logic'): + _, logicstr = line.split(':', 1) + world.logic = {1: logicstr.strip()} + elif line.startswith('!goal'): + _, goalstr = line.split(':', 1) + world.goal = {1: goalstr.strip()} + elif line.startswith('!owShuffle'): + _, modestr = line.split(':', 1) + world.owShuffle = {1: modestr.strip()} + elif line.startswith('!owCrossed'): + _, modestr = line.split(':', 1) + world.owCrossed = {1: modestr.strip()} + elif line.startswith('!owKeepSimilar'): + _, modestr = line.split(':', 1) + modestr = modestr.strip().lower() + world.owKeepSimilar = {1: True if modestr in ('true', 'yes', 'on', 'enabled') else False} + elif line.startswith('!light_cone_sewers'): + _, sewerstr = line.split(':', 1) + world.sewer_light_cone = {1: sewerstr.strip().lower() == 'true'} + elif line.startswith('!light_cone_lw'): + _, lwconestr = line.split(':', 1) + world.light_world_light_cone = lwconestr.strip().lower() == 'true' + elif line.startswith('!light_cone_dw'): + _, dwconestr = line.split(':', 1) + world.dark_world_light_cone = dwconestr.strip().lower() == 'true' + elif line.startswith('!fix_trock_doors'): + _, trdstr = line.split(':', 1) + world.fix_trock_doors = {1: trdstr.strip().lower() == 'true'} + elif line.startswith('!fix_trock_exit'): + _, trfstr = line.split(':', 1) + world.fix_trock_exit = {1: trfstr.strip().lower() == 'true'} + elif line.startswith('!fix_gtower_exit'): + _, gtfstr = line.split(':', 1) + world.fix_gtower_exit = gtfstr.strip().lower() == 'true' + elif line.startswith('!fix_pod_exit'): + _, podestr = line.split(':', 1) + world.fix_palaceofdarkness_exit = {1: podestr.strip().lower() == 'true'} + elif line.startswith('!fix_skullwoods_exit'): + _, swestr = line.split(':', 1) + world.fix_skullwoods_exit = {1: swestr.strip().lower() == 'true'} + elif line.startswith('!check_beatable_only'): + _, chkbtstr = line.split(':', 1) + world.check_beatable_only = chkbtstr.strip().lower() == 'true' + elif line.startswith('!ganon_death_pyramid_respawn'): + _, gnpstr = line.split(':', 1) + world.ganon_at_pyramid = gnpstr.strip().lower() == 'true' + elif line.startswith('!save_quit_boss'): + _, sqbstr = line.split(':', 1) + world.save_and_quite_from_boss = sqbstr.strip().lower() == 'true' + elif line.startswith('!text_'): + textname, text = line.split(':', 1) + text_patches.append([textname.lstrip('!text_').strip(), 'text', text.strip()]) + #temporarilly removed. New credits system not ready to handle this. + #elif line.startswith('!credits_'): + # textname, text = line.split(':', 1) + # text_patches.append([textname.lstrip('!credits_').strip(), 'credits', text.strip()]) + continue + elif line.startswith('$'): + edge1, edge2 = line.split('=', 1) + if world.custom_overworld is None: + world.custom_overworld = {1: []} + world.custom_overworld[1].append(edge1.strip(), edge2.strip()) + #TODO: Do entrances similar to OW + elif '<=>' in line: + entrance, exit = line.split('<=>', 1) + #connect_two_way(world, entrance.strip(), exit.strip(), 1) + elif '=>' in line: + entrance, exit = line.split('=>', 1) + #connect_entrance(world, entrance.strip(), exit.strip(), 1) + elif '<=' in line: + entrance, exit = line.split('<=', 1) + #connect_exit(world, exit.strip(), entrance.strip(), 1) + + world.required_medallions[1] = (mm_medallion, tr_medallion) + def start(): parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter) diff --git a/Plandomizer_Template.txt b/Plandomizer_Template.txt index 6c94cc76..41782f89 100644 --- a/Plandomizer_Template.txt +++ b/Plandomizer_Template.txt @@ -242,3 +242,10 @@ Ganons Tower - Mini Helmasaur Room - Right: Nothing Ganons Tower - Pre-Moldorm Chest: Small Key (Ganons Tower) Ganons Tower - Validation Chest: Nothing Ganon: Triforce + +# set Overworld connections (lines starting with $, separate edges with =) +!owShuffle: parallel +#!owMixed: true # Mixed OW not supported yet +!owCrossed: none +!owKeepSimilar: true +$Links House NE = Kakariko Village SE diff --git a/README.md b/README.md index 0a389551..221c5987 100644 --- a/README.md +++ b/README.md @@ -9,21 +9,19 @@ See https://alttpr.com/ for more details on the normal randomizer. This is a very new mode of LTTPR so the tools and info is very limited. - There is an [OW Rando Cheat Sheet](https://zelda.codemann8.com/images/shared/ow-rando-reference-sheet.png) that shows all the transitions that exist and are candidates for shuffle. - There is OW tracking capability within the following trackers: - - CodeTracker, an [EmoTracker](https://emotracker.net) package for LTTPR - [Community Tracker](https://alttptracker.dunka.net/) + - CodeTracker, an [EmoTracker](https://emotracker.net) package for LTTPR - There is an [OW OWG Reference Sheet](https://zelda.codemann8.com/images/shared/ow-owg-reference-sheet.png) that shows all the in-logic places where boots/mirror clips and fake flippers are expected from the player. # Known Issues -(Updated 2021-06-23) +(Updated 2021-08-26) ### If you want to playtest this, know these things: - Big Red Bomb may require bomb duping as ledge drops may be in the way of your path to the Pyramid Fairy crack -- Do NOT grab the Frogsmith until you have seen the Blacksmith location. Doing so may prevent you from continuing in your save file. - If you fake flipper, beware of transitioning south. You could end up at the top of the waterfall in the southeast of either world. If you mistakenly drop down, it is important to NOT make any other movements and S+Q immediately when the game allows you to (might take several seconds, the game has to scroll back to the original point of water entry) or there will be a hardlock. Falling from the waterfall is avoidable but it is super easy to do as it is super close to the transition. -- In Crossed OW Tile Swap, there are some interesting bunny water-walk situations that can occur, these are mean to be out-of-logic but beware of logic bugs around this area. +- In Crossed OW, there are some interesting bunny swimming situations that can occur, these are meant to be out-of-logic but beware of logic bugs around this area. But also, hardlocks can occur; if you take damage, be sure to S+Q immediately before moving in any direction, or you may get an infinite screen wrap glitch. ### Known bugs: -- ~~In Mixed OW Tile Swap, Smith and Stumpy have issues when their tiles are swapped. Progression cannot be found on them when these tiles are swapped~~ (Fixed in 0.1.6.4) - Screens that loop on itself and also have free-standing items, the sprites are duplicated and can cause item duplication - When OWG are performed to enter mega-tile screens (large OW screens), there is a small chance that an incorrect VRAM reference value causes the map graphics to offset in increments of 16 pixels @@ -67,32 +65,58 @@ OW Transitions are shuffled, but both worlds will have a matching layout. OW Transitions are shuffled within each world separately. -## Overworld Tile Swap (--ow_swap) +## Crossed Options (--ow_crossed) -### Vanilla +This allows OW connections to be shuffled cross-world. -OW tiles remain in their original worlds. +Polar and Grouped both are guaranteed to result in two separated planes of tiles. To navigate to the other plane, you have the following methods: 1) Normal portals 2) Mirroring on DW tiles 3) Fluting to a LW tile that was previously unreachable -### Mixed +Limited and Chaos are not bound to follow a two-plane framework. This means that it could be possible to travel on foot to every tile without entering a normal portal. -OW tiles are randomly chosen to become a part of the opposite world +See each option to get more details on the differences. -### Crossed +### None -OW tiles remain in their original world, but transitions can now be travel cross-world. +Transitions will remain same-world. -## Visual Representation of Main OW Shuffle Settings +### Polar -![OW Shuffle Settings Combination](https://zelda.codemann8.com/images/shared/ow-modes.gif) +Only effective if Mixed/Tile Swap is enabled. Enabling Polar preserves the original/vanilla connections even when tiles are swapped/mixed. This results in a completely vanilla overworld, except that some tiles will transform Link to a Bunny (as per Mixed swapping some tiles to the other world). This offers an interesting twist on Mixed where you have a pre-conditioned knowledge of the terrain you will encounter, but not necessarily be able to do what you need to do there. (see Tile Swap/Mixed section for more details) + +### Grouped + +This option shuffles connections cross-world in the same manner as Tile Swap/Mixed, the connections leading in and coming out of a group of tiles are crossed. Unlike Polar, this uses a different set of tile groups as a basis of crossing connections, albeit the same rule govern which groups of tiles must cross together (see Tile Swap/Mixed for more details) + +### Limited + +Every transition independently is a candidate to be chosen as a cross-world connection, however only 9 transitions become crossed (in each world). This option abides by the Keep Similar Edges Together option and will guarantee same effect on all edges in a Similar Edge group if enabled. If a Similar Edge group is chosen from the pool of candidates, it only counts as one portal, not multiple. + +Note: Only parallel connections (a connection that also exists in the opposite world) are considered for cross-world connections, which means that the same connection in the opposite world will also connect cross-world. + +Motive: Why 9 connections? To imitate the effect of the 9 standard portals that exist. + +### Chaos + +Same as Limited, except that there is no limit to the number of cross-world connections that are made. Each transition has an equal 50/50 chance of being a crossed connection. ## Keep Similar Edges Together (--ow_keepsimilar) This keeps similar edge transitions together. ie. The 2 west edges of Potion Shop will be paired to another set of two similar edges +Note: This affects OW Layout Shuffle mostly, but also affects Limited and Chaos modes in Crossed OW. + +## Tile Swap / Mixed Overworld (--ow_mixed) + +OW tiles are randomly chosen to become a part of the opposite world. When on the Overworld, there will be an L or D in the upper left corner, indicating which world you are currently in. Mirroring still works the same, you must be in the DW to mirror to the LW. + +Note: Tiles are put into groups that must be shuffled together when certain settings are enabled. For instance, if ER is disabled, then any tiles that have a connector cave that leads to another tile, those tiles must swap together; (an exception to this is the Old Man Rescue cave which has been modified similar to how Inverted modifies it, Old Man Rescue is ALWAYS accessible from the Light World) + ## Flute Shuffle (--ow_fluteshuffle) When enabled, new flute spots are generated and gives the player the option to cancel out of the flute menu by pressing X. +Note: Desert Teleporter Ledge is always guaranteed to be chosen. One of the three Mountain tiles are guaranteed if OW Layout Shuffle is set to Vanilla. + ### Vanilla Flute spots remain unchanged. @@ -136,10 +160,10 @@ Show the help message and exit. For specifying the overworld layout shuffle you want as above. (default: vanilla) ``` ---ow_swap +--ow_crossed ``` -For specifying the overworld tile swap you want as above. (default: vanilla) +For specifying the type of cross-world connections you want on the overworld ``` --ow_keepsimilar @@ -147,6 +171,12 @@ For specifying the overworld tile swap you want as above. (default: vanilla) This keeps similar edge transitions paired together with other pairs of transitions +``` +--ow_mixed +``` + +This gives each OW tile a random chance to be swapped to the opposite world + ``` --ow_fluteshuffle ``` diff --git a/RELEASENOTES.md b/RELEASENOTES.md index c0fae696..4d212275 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,14 +1,43 @@ # New Features -Bomb Logic added as an option. This removes your ability to use bombs until you find a "bomb bag", a +10 Bomb Capacity item. It is accounted for in the logic, so you aren't expected to get items behind bomb walls until you have found the bomb capacity item. The upgrades are removed from the upgrade fairy as well. +## Shuffle SFX + +Shuffles a large portion of the sounds effects. Can be used with the adjuster. + +CLI: ```--shuffle_sfx``` -``` ---bomblogic -``` +## Bomb Logic + +When enabling this option, you do not start with bomb capacity but rather you must find 1 of 2 bomb bags. (They are represented by the +10 capacity item.) Bomb capacity upgrades are otherwise unavailable. + +CLI: ```--bombbag``` + # Bug Fixes and Notes. + +* 0.5.1.1 + * Shop hints in ER are now more generic instead of using "near X" because they aren't near that anymore + * Added memory location for mutliworld scripts to read what item was just obtain (longer than one frame) + * Fix for bias in boss shuffle "full" + * Fix for certain lone big chests in keysanity (allowed you to get contents without big key) + * Fix for pinball checking + * Fix for multi-entrance dungeons + * 2 fixes for big key placement logic + * ensure big key is placed early if the validator assumes it) + * Open big key doors appropriately when generating rules and big key is forced somewhere + * Updated cutoff entrances for intensity 3 +* 0.5.1.0 + * Large logic refactor introducing a new method of key logic + * Some performance optimization + * Some outstanding bug fixes (boss shuffle "full" picks three unique bosses to be duplicated, e.g.) +* 0.5.0.3 + * Fixed a bug in retro+vanilla and big key placement + * Fixed a problem with shops not registering in the Multiclient until you visit one + * Fixed a bug in the Mystery code with sfx +* 0.5.0.2 + * --shuffle_sfx option added * 0.5.0.1 - * --bomblogic option added + * --bombbag option added * 0.5.0.0 * Handles headered roms for enemizer (Thanks compiling) * Warning added for earlier version of python (Thanks compiling) diff --git a/Regions.py b/Regions.py index 40d84a28..ab494e12 100644 --- a/Regions.py +++ b/Regions.py @@ -1,6 +1,6 @@ import collections from Items import ItemFactory -from BaseClasses import Region, Location, Entrance, RegionType, Shop, ShopType +from BaseClasses import Region, Location, Entrance, RegionType, Terrain, Shop, ShopType def create_regions(world, player): @@ -30,7 +30,7 @@ def create_regions(world, player): create_lw_region(player, 'Mountain Entry Entrance', None, ['Mountain Entry Entrance Rock (East)', 'Mountain Entry Entrance Ledge Drop', 'Old Man Cave (West)', 'Bumper Cave Entry Mirror Spot']), create_lw_region(player, 'Mountain Entry Ledge', None, ['Mountain Entry Ledge Drop', 'Death Mountain Return Cave (West)', 'Bumper Cave Ledge Mirror Spot']), create_lw_region(player, 'Zora Waterfall Area', None, ['Zora Waterfall Water Entry', 'Catfish Mirror Spot', 'Zora Waterfall SE', 'Zora Waterfall NE']), - create_lw_region(player, 'Zora Waterfall Water', None, ['Waterfall of Wishing Cave Entry', 'Zora Waterfall Landing', 'Zora Whirlpool']), + create_lw_region(player, 'Zora Waterfall Water', None, ['Waterfall of Wishing Cave Entry', 'Zora Waterfall Landing', 'Zora Whirlpool'], Terrain.Water), create_lw_region(player, 'Waterfall of Wishing Cave', None, ['Zora Waterfall Water Drop', 'Waterfall of Wishing']), create_lw_region(player, 'Zoras Domain', ['King Zora', 'Zora\'s Ledge'], ['Zoras Domain SW']), create_lw_region(player, 'Lost Woods Pass West Area', None, ['Skull Woods Pass West Mirror Spot', 'Lost Woods Pass NW', 'Lost Woods Pass SW']), @@ -45,13 +45,13 @@ def create_regions(world, player): create_lw_region(player, 'Kings Grave Area', None, ['Kings Grave Inner Rocks', 'Kings Grave', 'Dark Graveyard Grave Mirror Spot']), create_lw_region(player, 'River Bend Area', None, ['North Fairy Cave Drop', 'River Bend Water Drop', 'North Fairy Cave', 'Qirn Jump Mirror Spot', 'River Bend WC', 'River Bend SW']), create_lw_region(player, 'River Bend East Bank', None, ['River Bend East Water Drop', 'Qirn Jump East Mirror Spot', 'River Bend SE', 'River Bend EC', 'River Bend ES']), - create_lw_region(player, 'River Bend Water', None, ['River Bend West Pier', 'River Bend East Pier', 'River Bend EN', 'River Bend SC', 'River Bend Whirlpool']), + create_lw_region(player, 'River Bend Water', None, ['River Bend West Pier', 'River Bend East Pier', 'River Bend EN', 'River Bend SC', 'River Bend Whirlpool'], Terrain.Water), create_lw_region(player, 'Potion Shop Area', None, ['Potion Shop Water Drop', 'Potion Shop Rock (South)', 'Potion Shop', 'Dark Witch Mirror Spot', 'Potion Shop WC', 'Potion Shop WS']), create_lw_region(player, 'Potion Shop Northeast', None, ['Potion Shop Northeast Water Drop', 'Potion Shop Rock (North)', 'Dark Witch Northeast Mirror Spot', 'Potion Shop EC']), - create_lw_region(player, 'Potion Shop Water', None, ['Potion Shop WN', 'Potion Shop EN']), + create_lw_region(player, 'Potion Shop Water', None, ['Potion Shop WN', 'Potion Shop EN'], Terrain.Water), create_lw_region(player, 'Zora Approach Area', None, ['Zora Approach Rocks (West)', 'Zora Approach Bottom Ledge Drop', 'Zora Approach Water Drop', 'Catfish Approach Mirror Spot', 'Zora Approach WC']), create_lw_region(player, 'Zora Approach Ledge', None, ['Zora Approach Rocks (East)', 'Zora Approach Ledge Drop', 'Catfish Approach Ledge Mirror Spot', 'Zora Approach NE']), - create_lw_region(player, 'Zora Approach Water', None, ['Zora Approach WN']), + create_lw_region(player, 'Zora Approach Water', None, ['Zora Approach WN'], Terrain.Water), create_lw_region(player, 'Kakariko Area', ['Bottle Merchant'], ['Kakariko Southwest Bush (North)', 'Kakariko Yard Bush (South)', 'Kakariko Well Drop', 'Kakariko Well Cave', 'Blinds Hideout', 'Elder House (West)', 'Elder House (East)', 'Snitch Lady (West)', 'Snitch Lady (East)', 'Chicken House', 'Sick Kids House', 'Kakariko Shop', 'Tavern (Front)', 'Tavern North', 'Village of Outcasts Mirror Spot', 'Kakariko NW', 'Kakariko NC', 'Kakariko NE', 'Kakariko ES', 'Kakariko SE']), @@ -66,7 +66,7 @@ def create_regions(world, player): create_lw_region(player, 'Hyrule Castle East Entry', None, ['Hyrule Castle Outer East Rock', 'Pyramid Entry Mirror Spot', 'Hyrule Castle ES']), create_lw_region(player, 'Wooden Bridge Area', None, ['Wooden Bridge Bush (South)', 'Wooden Bridge Water Drop', 'Broken Bridge West Mirror Spot', 'Broken Bridge East Mirror Spot', 'Wooden Bridge NW', 'Wooden Bridge SW']), create_lw_region(player, 'Wooden Bridge Northeast', None, ['Wooden Bridge Bush (North)', 'Wooden Bridge Northeast Water Drop', 'Broken Bridge Northeast Mirror Spot', 'Wooden Bridge NE']), - create_lw_region(player, 'Wooden Bridge Water', None, ['Wooden Bridge NC']), + create_lw_region(player, 'Wooden Bridge Water', None, ['Wooden Bridge NC'], Terrain.Water), create_lw_region(player, 'Eastern Palace Area', None, ['Sahasrahlas Hut', 'Eastern Palace', 'Palace of Darkness Mirror Spot', 'Eastern Palace SW', 'Eastern Palace SE']), create_lw_region(player, 'Eastern Cliff', None, ['Sand Dunes Ledge Drop', 'Stone Bridge East Ledge Drop', 'Tree Line Ledge Drop', 'Eastern Palace Ledge Drop']), create_lw_region(player, 'Blacksmith Area', None, ['Blacksmiths Hut', 'Bat Cave Cave', 'Bat Cave Ledge Peg', 'Hammer Pegs Mirror Spot', 'Hammer Pegs Entry Mirror Spot', 'Blacksmith WS']), @@ -79,13 +79,13 @@ def create_regions(world, player): create_lw_region(player, 'Flute Boy Area', ['Flute Spot'], ['Stumpy Mirror Spot', 'Flute Boy SC']), create_lw_region(player, 'Flute Boy Pass', None, ['Stumpy Pass Mirror Spot', 'Flute Boy WS', 'Flute Boy SW']), create_lw_region(player, 'Central Bonk Rocks Area', None, ['Bonk Fairy (Light)', 'Dark Bonk Rocks Mirror Spot', 'Central Bonk Rocks NW', 'Central Bonk Rocks SW', 'Central Bonk Rocks EN', 'Central Bonk Rocks EC', 'Central Bonk Rocks ES']), - create_lw_region(player, 'Links House Area', None, ['Links House', 'Big Bomb Shop Mirror Spot', 'Links House NE', 'Links House WN', 'Links House WC', 'Links House WS', 'Links House SC', 'Links House ES', 'Flute Away']), + create_lw_region(player, 'Links House Area', None, ['Links House', 'Big Bomb Shop Mirror Spot', 'Links House NE', 'Links House WN', 'Links House WC', 'Links House WS', 'Links House SC', 'Links House ES']), create_lw_region(player, 'Stone Bridge Area', None, ['Hammer Bridge North Mirror Spot', 'Hammer Bridge South Mirror Spot', 'Stone Bridge NC', 'Stone Bridge EN', 'Stone Bridge WS', 'Stone Bridge SC']), - create_lw_region(player, 'Stone Bridge Water', None, ['Dark Hobo Mirror Spot', 'Stone Bridge WC', 'Stone Bridge EC']), + create_lw_region(player, 'Stone Bridge Water', None, ['Dark Hobo Mirror Spot', 'Stone Bridge WC', 'Stone Bridge EC'], Terrain.Water), create_lw_region(player, 'Hobo Bridge', ['Hobo'], ['Hobo EC']), create_lw_region(player, 'Central Cliffs', None, ['Central Bonk Rocks Cliff Ledge Drop', 'Links House Cliff Ledge Drop', 'Stone Bridge Cliff Ledge Drop', 'Lake Hylia Area Cliff Ledge Drop', 'Lake Hylia Island FAWT Ledge Drop', 'Stone Bridge EC Cliff Water Drop', 'Tree Line WC Cliff Water Drop', 'C Whirlpool Outer Cliff Ledge Drop', 'C Whirlpool Cliff Ledge Drop', 'South Teleporter Cliff Ledge Drop', 'Statues Cliff Ledge Drop']), create_lw_region(player, 'Tree Line Area', None, ['Lake Hylia Fairy', 'Dark Tree Line Mirror Spot', 'Tree Line WN', 'Tree Line NW', 'Tree Line SE']), - create_lw_region(player, 'Tree Line Water', None, ['Tree Line WC', 'Tree Line SC']), + create_lw_region(player, 'Tree Line Water', None, ['Tree Line WC', 'Tree Line SC'], Terrain.Water), create_lw_region(player, 'Eastern Nook Area', None, ['Long Fairy Cave', 'Darkness Nook Mirror Spot', 'East Hyrule Teleporter', 'Eastern Nook NE']), create_lw_region(player, 'Desert Area', None, ['Desert Palace Statue Move', 'Checkerboard Ledge Approach', 'Aginahs Cave', 'Misery Mire Mirror Spot', 'Desert ES']), create_lw_region(player, 'Desert Ledge', ['Desert Ledge'], ['Desert Ledge Outer Rocks', 'Desert Ledge Drop', 'Desert Palace Entrance (West)', 'Misery Mire Ledge Mirror Spot']), @@ -100,16 +100,16 @@ def create_regions(world, player): create_lw_region(player, 'Flute Boy Bush Entry', None, ['Flute Boy Bush (North)', 'Stumpy Bush Entry Mirror Spot', 'Flute Boy Approach NC']), create_lw_region(player, 'Cave 45 Ledge', None, ['Cave 45 Inverted Leave', 'Cave 45 Ledge Drop', 'Cave 45']), create_lw_region(player, 'C Whirlpool Area', None, ['C Whirlpool Rock (Bottom)', 'C Whirlpool Water Entry', 'Dark C Whirlpool Mirror Spot', 'South Hyrule Teleporter', 'C Whirlpool EN', 'C Whirlpool ES', 'C Whirlpool SC']), - create_lw_region(player, 'C Whirlpool Water', None, ['C Whirlpool Landing', 'C Whirlpool', 'C Whirlpool EC']), + create_lw_region(player, 'C Whirlpool Water', None, ['C Whirlpool Landing', 'C Whirlpool', 'C Whirlpool EC'], Terrain.Water), create_lw_region(player, 'C Whirlpool Outer Area', None, ['C Whirlpool Rock (Top)', 'Dark C Whirlpool Outer Mirror Spot', 'C Whirlpool WC', 'C Whirlpool NW']), create_lw_region(player, 'Statues Area', None, ['Statues Water Entry', 'Light Hype Fairy', 'Hype Cave Mirror Spot', 'Statues NC', 'Statues WN', 'Statues WS', 'Statues SC']), - create_lw_region(player, 'Statues Water', None, ['Statues Landing', 'Statues WC']), + create_lw_region(player, 'Statues Water', None, ['Statues Landing', 'Statues WC'], Terrain.Water), create_lw_region(player, 'Lake Hylia Area', None, ['Lake Hylia Water Drop', 'Lake Hylia Fortune Teller', 'Cave Shop (Lake Hylia)', 'Ice Lake Mirror Spot', 'Lake Hylia NW']), create_lw_region(player, 'Lake Hylia South Shore', None, ['Lake Hylia South Water Drop', 'Mini Moldorm Cave', 'Ice Lake Southwest Mirror Spot', 'Ice Lake Southeast Mirror Spot', 'Lake Hylia WS', 'Lake Hylia ES']), create_lw_region(player, 'Lake Hylia Northeast Bank', None, ['Lake Hylia Northeast Water Drop', 'Ice Lake Northeast Mirror Spot', 'Lake Hylia NE']), create_lw_region(player, 'Lake Hylia Central Island', None, ['Lake Hylia Central Water Drop', 'Capacity Upgrade', 'Ice Palace Mirror Spot', 'Lake Hylia Teleporter']), create_lw_region(player, 'Lake Hylia Island', ['Lake Hylia Island'], ['Lake Hylia Island Water Drop']), - create_lw_region(player, 'Lake Hylia Water', None, ['Lake Hylia Central Island Pier', 'Lake Hylia Island Pier', 'Lake Hylia West Pier', 'Lake Hylia East Pier', 'Lake Hylia NC', 'Lake Hylia EC', 'Lake Hylia Whirlpool']), + create_lw_region(player, 'Lake Hylia Water', None, ['Lake Hylia Central Island Pier', 'Lake Hylia Island Pier', 'Lake Hylia West Pier', 'Lake Hylia East Pier', 'Lake Hylia NC', 'Lake Hylia EC', 'Lake Hylia Whirlpool'], Terrain.Water), create_lw_region(player, 'Ice Cave Area', None, ['Ice Rod Cave', 'Good Bee Cave', '20 Rupee Cave', 'Shopping Mall Mirror Spot', 'Ice Cave SE', 'Ice Cave SW']), create_lw_region(player, 'Desert Pass Area', ['Purple Chest'], ['Desert Pass Ladder (South)', 'Desert Fairy', '50 Rupee Cave', 'Swamp Nook Mirror Spot', 'Desert Pass WS', 'Desert Pass EC', 'Desert Pass Rocks (North)']), create_lw_region(player, 'Desert Pass Southeast', None, ['Desert Pass Rocks (South)', 'Swamp Nook Southeast Mirror Spot', 'Desert Pass ES']), @@ -117,8 +117,8 @@ def create_regions(world, player): create_lw_region(player, 'Dam Area', ['Sunken Treasure'], ['Dam', 'Swamp Mirror Spot', 'Dam WC', 'Dam WS', 'Dam NC', 'Dam EC']), create_lw_region(player, 'South Pass Area', None, ['Dark South Pass Mirror Spot', 'South Pass WC', 'South Pass NC', 'South Pass ES']), create_lw_region(player, 'Octoballoon Area', None, ['Octoballoon Water Drop', 'Bomber Corner Mirror Spot', 'Octoballoon WS', 'Octoballoon NE']), - create_lw_region(player, 'Octoballoon Water', None, ['Octoballoon Pier', 'Octoballoon WC', 'Octoballoon Whirlpool']), - create_lw_region(player, 'Octoballoon Water Ledge', None, ['Octoballoon Waterfall Water Drop', 'Octoballoon NW']), + create_lw_region(player, 'Octoballoon Water', None, ['Octoballoon Pier', 'Octoballoon WC', 'Octoballoon Whirlpool'], Terrain.Water), + create_lw_region(player, 'Octoballoon Water Ledge', None, ['Octoballoon Waterfall Water Drop', 'Octoballoon NW'], Terrain.Water), create_dw_region(player, 'Skull Woods Forest', None, ['Skull Woods Bush Rock (East)', 'Skull Woods First Section Hole (East)', 'Skull Woods First Section Hole (West)', 'Skull Woods First Section Hole (North)', 'Skull Woods First Section Door', 'Skull Woods Second Section Door (East)', 'Lost Woods East Mirror Spot', 'Skull Woods SE']), @@ -148,16 +148,17 @@ def create_regions(world, player): create_dw_region(player, 'Dark Fortune Area', None, ['Fortune Teller (Dark)', 'Kakariko Fortune Mirror Spot', 'Dark Fortune NE', 'Dark Fortune EN', 'Dark Fortune ES', 'Dark Fortune SC']), create_dw_region(player, 'Outcast Pond Area', None, ['Kakariko Pond Mirror Spot', 'Outcast Pond NE', 'Outcast Pond WN', 'Outcast Pond WS', 'Outcast Pond SW', 'Outcast Pond SE', 'Outcast Pond EN', 'Outcast Pond ES']), create_dw_region(player, 'Dark Chapel Area', None, ['Dark Sanctuary Hint', 'Sanctuary Mirror Spot', 'Bonk Rock Ledge Mirror Spot', 'Dark Chapel WN', 'Dark Chapel WS', 'Dark Chapel EC']), - create_dw_region(player, 'Dark Graveyard Area', None, ['Graveyard Ledge Mirror Spot', 'Kings Grave Mirror Spot', 'Dark Graveyard WC', 'Dark Graveyard EC']), + create_dw_region(player, 'Dark Graveyard Area', None, ['Dark Graveyard Bush (South)', 'Dark Graveyard WC', 'Dark Graveyard EC']), + create_dw_region(player, 'Dark Graveyard North', None, ['Graveyard Ledge Mirror Spot', 'Kings Grave Mirror Spot', 'Dark Graveyard Bush (North)']), create_dw_region(player, 'Qirn Jump Area', None, ['Qirn Jump Water Drop', 'River Bend Mirror Spot', 'Qirn Jump WC', 'Qirn Jump SW']), create_dw_region(player, 'Qirn Jump East Bank', None, ['Qirn Jump East Water Drop', 'River Bend East Mirror Spot', 'Qirn Jump SE', 'Qirn Jump EC', 'Qirn Jump ES']), - create_dw_region(player, 'Qirn Jump Water', None, ['Qirn Jump Pier', 'Qirn Jump Whirlpool', 'Qirn Jump EN', 'Qirn Jump SC']), + create_dw_region(player, 'Qirn Jump Water', None, ['Qirn Jump Pier', 'Qirn Jump Whirlpool', 'Qirn Jump EN', 'Qirn Jump SC'], Terrain.Water), create_dw_region(player, 'Dark Witch Area', None, ['Dark Witch Water Drop', 'Dark Witch Rock (South)', 'Dark World Potion Shop', 'Potion Shop Mirror Spot', 'Dark Witch WC', 'Dark Witch WS']), create_dw_region(player, 'Dark Witch Northeast', None, ['Dark Witch Northeast Water Drop', 'Dark Witch Rock (North)', 'Potion Shop Northeast Mirror Spot', 'Dark Witch EC']), - create_dw_region(player, 'Dark Witch Water', None, ['Dark Witch WN', 'Dark Witch EN']), + create_dw_region(player, 'Dark Witch Water', None, ['Dark Witch WN', 'Dark Witch EN'], Terrain.Water), create_dw_region(player, 'Catfish Approach Area', None, ['Catfish Approach Rocks (West)', 'Catfish Approach Bottom Ledge Drop', 'Catfish Approach Water Drop', 'Zora Approach Mirror Spot', 'Catfish Approach WC']), create_dw_region(player, 'Catfish Approach Ledge', None, ['Catfish Approach Rocks (East)', 'Catfish Approach Ledge Drop', 'Zora Approach Ledge Mirror Spot', 'Catfish Approach NE']), - create_dw_region(player, 'Catfish Approach Water', None, ['Catfish Approach WN']), + create_dw_region(player, 'Catfish Approach Water', None, ['Catfish Approach WN'], Terrain.Water), create_dw_region(player, 'Village of Outcasts Area', None, ['Village of Outcasts Pegs', 'Chest Game', 'Thieves Town', 'C-Shaped House', 'Brewery', 'Kakariko Mirror Spot', 'Village of Outcasts NW', 'Village of Outcasts NC', 'Village of Outcasts NE', 'Village of Outcasts ES', 'Village of Outcasts SE']), create_dw_region(player, 'Dark Grassy Lawn', None, ['Grassy Lawn Pegs', 'Dark World Shop', 'Kakariko Grass Mirror Spot']), create_dw_region(player, 'Shield Shop Area', None, ['Shield Shop Fence (Outer) Ledge Drop', 'Forgotton Forest Mirror Spot', 'Shield Shop NW', 'Shield Shop NE']), @@ -168,7 +169,7 @@ def create_regions(world, player): create_dw_region(player, 'Broken Bridge Area', None, ['Broken Bridge Hammer Rock (South)', 'Broken Bridge Water Drop', 'Wooden Bridge Mirror Spot', 'Broken Bridge SW']), create_dw_region(player, 'Broken Bridge Northeast', None, ['Broken Bridge Hammer Rock (North)', 'Broken Bridge Hookshot Gap', 'Broken Bridge Northeast Water Drop', 'Wooden Bridge Northeast Mirror Spot', 'Broken Bridge NE']), create_dw_region(player, 'Broken Bridge West', None, ['Broken Bridge West Water Drop', 'Wooden Bridge West Mirror Spot', 'Broken Bridge NW']), - create_dw_region(player, 'Broken Bridge Water', None, ['Broken Bridge NC']), + create_dw_region(player, 'Broken Bridge Water', None, ['Broken Bridge NC'], Terrain.Water), create_dw_region(player, 'Palace of Darkness Area', None, ['Palace of Darkness Hint', 'Palace of Darkness', 'Eastern Palace Mirror Spot', 'Palace of Darkness SW', 'Palace of Darkness SE']), create_dw_region(player, 'Darkness Cliff', None, ['Dark Dunes Ledge Drop', 'Hammer Bridge North Ledge Drop', 'Dark Tree Line Ledge Drop', 'Palace of Darkness Ledge Drop']), create_dw_region(player, 'Hammer Pegs Entry', None, ['Peg Area Rocks (West)', 'Blacksmith Entry Mirror Spot', 'Hammer Pegs WS']), @@ -185,11 +186,11 @@ def create_regions(world, player): create_dw_region(player, 'Big Bomb Shop Area', None, ['Big Bomb Shop', 'Links House Mirror Spot', 'Big Bomb Shop NE', 'Big Bomb Shop WN', 'Big Bomb Shop WC', 'Big Bomb Shop WS', 'Big Bomb Shop SC', 'Big Bomb Shop ES']), create_dw_region(player, 'Hammer Bridge North Area', None, ['Hammer Bridge Pegs (North)', 'Hammer Bridge Water Drop', 'Stone Bridge Mirror Spot', 'Hammer Bridge NC', 'Hammer Bridge EN']), create_dw_region(player, 'Hammer Bridge South Area', None, ['Hammer Bridge Pegs (South)', 'Stone Bridge South Mirror Spot', 'Hammer Bridge WS', 'Hammer Bridge SC']), - create_dw_region(player, 'Hammer Bridge Water', None, ['Hammer Bridge Pier', 'Hobo Mirror Spot', 'Hammer Bridge EC']), + create_dw_region(player, 'Hammer Bridge Water', None, ['Hammer Bridge Pier', 'Hobo Mirror Spot', 'Hammer Bridge EC'], Terrain.Water), create_dw_region(player, 'Dark Central Cliffs', None, ['Dark Bonk Rocks Cliff Ledge Drop', 'Bomb Shop Cliff Ledge Drop', 'Hammer Bridge South Cliff Ledge Drop', 'Ice Lake Area Cliff Ledge Drop', 'Ice Palace Island FAWT Ledge Drop', 'Hammer Bridge EC Cliff Water Drop', 'Dark Tree Line WC Cliff Water Drop', 'Dark C Whirlpool Outer Cliff Ledge Drop', 'Dark C Whirlpool Cliff Ledge Drop', 'Hype Cliff Ledge Drop', 'Dark South Teleporter Cliff Ledge Drop']), create_dw_region(player, 'Dark Tree Line Area', None, ['Dark Lake Hylia Fairy', 'Tree Line Mirror Spot', 'Dark Tree Line WN', 'Dark Tree Line NW', 'Dark Tree Line SE']), - create_dw_region(player, 'Dark Tree Line Water', None, ['Dark Tree Line WC', 'Dark Tree Line SC']), + create_dw_region(player, 'Dark Tree Line Water', None, ['Dark Tree Line WC', 'Dark Tree Line SC'], Terrain.Water), create_dw_region(player, 'Palace of Darkness Nook Area', None, ['East Dark World Hint', 'East Dark World Teleporter', 'Eastern Nook Mirror Spot', 'Palace of Darkness Nook NE']), create_dw_region(player, 'Misery Mire Area', None, ['Mire Shed', 'Misery Mire', 'Dark Desert Fairy', 'Dark Desert Hint', 'Desert Mirror Spot', 'Desert Ledge Mirror Spot', 'Checkerboard Mirror Spot', 'DP Stairs Mirror Spot', 'DP Entrance (North) Mirror Spot']), create_dw_region(player, 'Misery Mire Teleporter Ledge', None, ['Misery Mire Teleporter Ledge Drop', 'Misery Mire Teleporter']), @@ -197,15 +198,15 @@ def create_regions(world, player): create_dw_region(player, 'Stumpy Approach Area', None, ['Stumpy Approach Bush (South)', 'Cave 45 Mirror Spot', 'Stumpy Approach NW', 'Stumpy Approach EC']), create_dw_region(player, 'Stumpy Approach Bush Entry', None, ['Stumpy Approach Bush (North)', 'Flute Boy Entry Mirror Spot', 'Stumpy Approach NC']), create_dw_region(player, 'Dark C Whirlpool Area', None, ['Dark C Whirlpool Rock (Bottom)', 'South Dark World Teleporter', 'C Whirlpool Mirror Spot', 'Dark C Whirlpool Water Entry', 'Dark C Whirlpool EN', 'Dark C Whirlpool ES', 'Dark C Whirlpool SC']), - create_dw_region(player, 'Dark C Whirlpool Water', None, ['Dark C Whirlpool Landing', 'Dark C Whirlpool EC']), + create_dw_region(player, 'Dark C Whirlpool Water', None, ['Dark C Whirlpool Landing', 'Dark C Whirlpool EC'], Terrain.Water), create_dw_region(player, 'Dark C Whirlpool Outer Area', None, ['Dark C Whirlpool Rock (Top)', 'C Whirlpool Outer Mirror Spot', 'Dark C Whirlpool WC', 'Dark C Whirlpool NW']), create_dw_region(player, 'Hype Cave Area', None, ['Hype Cave Water Entry', 'Hype Cave', 'Statues Mirror Spot', 'Hype Cave NC', 'Hype Cave WN', 'Hype Cave WS', 'Hype Cave SC']), - create_dw_region(player, 'Hype Cave Water', None, ['Hype Cave Landing', 'Hype Cave WC']), + create_dw_region(player, 'Hype Cave Water', None, ['Hype Cave Landing', 'Hype Cave WC'], Terrain.Water), create_dw_region(player, 'Ice Lake Area', None, ['Ice Lake Water Drop', 'Dark Lake Hylia Shop', 'Lake Hylia Mirror Spot', 'Ice Lake NW']), create_dw_region(player, 'Ice Lake Northeast Bank', None, ['Ice Lake Northeast Water Drop', 'Lake Hylia Northeast Mirror Spot', 'Ice Lake NE']), create_dw_region(player, 'Ice Lake Ledge (West)', None, ['Ice Lake Southwest Water Drop', 'South Shore Mirror Spot', 'Ice Lake WS']), create_dw_region(player, 'Ice Lake Ledge (East)', None, ['Ice Lake Southeast Water Drop', 'South Shore East Mirror Spot', 'Ice Lake ES']), - create_dw_region(player, 'Ice Lake Water', None, ['Ice Lake Northeast Pier', 'Lake Hylia Island Mirror Spot', 'Ice Lake NC', 'Ice Lake EC']), + create_dw_region(player, 'Ice Lake Water', None, ['Ice Lake Northeast Pier', 'Lake Hylia Island Mirror Spot', 'Ice Lake NC', 'Ice Lake EC'], Terrain.Water), create_dw_region(player, 'Ice Lake Moat', None, ['Ice Lake Moat Water Entry', 'Ice Lake Northeast Pier Bomb Jump', 'Ice Palace Approach', 'Lake Hylia Water Mirror Spot']), create_dw_region(player, 'Ice Palace Area', None, ['Ice Palace Leave', 'Ice Palace', 'Ice Palace Teleporter', 'Lake Hylia Central Island Mirror Spot']), create_dw_region(player, 'Shopping Mall Area', None, ['Dark Lake Hylia Ledge Fairy', 'Dark Lake Hylia Ledge Hint', 'Dark Lake Hylia Ledge Spike Cave', 'Ice Cave Mirror Spot', 'Shopping Mall SW', 'Shopping Mall SE']), @@ -213,8 +214,8 @@ def create_regions(world, player): create_dw_region(player, 'Swamp Area', None, ['Swamp Palace', 'Dam Mirror Spot', 'Swamp WC', 'Swamp WS', 'Swamp NC', 'Swamp EC']), create_dw_region(player, 'Dark South Pass Area', None, ['South Pass Mirror Spot', 'Dark South Pass WC', 'Dark South Pass NC', 'Dark South Pass ES']), create_dw_region(player, 'Bomber Corner Area', None, ['Bomber Corner Water Drop', 'Octoballoon Mirror Spot', 'Bomber Corner WS', 'Bomber Corner NE']), - create_dw_region(player, 'Bomber Corner Water', None, ['Bomber Corner Pier', 'Bomber Corner Whirlpool', 'Bomber Corner WC']), - create_dw_region(player, 'Bomber Corner Water Ledge', None, ['Bomber Corner Waterfall Water Drop', 'Bomber Corner NW']), + create_dw_region(player, 'Bomber Corner Water', None, ['Bomber Corner Pier', 'Bomber Corner Whirlpool', 'Bomber Corner WC'], Terrain.Water), + create_dw_region(player, 'Bomber Corner Water Ledge', None, ['Bomber Corner Waterfall Water Drop', 'Bomber Corner NW'], Terrain.Water), create_cave_region(player, 'Lost Woods Gamble', 'a game of chance'), create_cave_region(player, 'Lost Woods Hideout (top)', 'a drop\'s exit', ['Lost Woods Hideout'], ['Lost Woods Hideout (top to bottom)']), @@ -1013,12 +1014,16 @@ def create_menu_region(player, name, locations=None, exits=None): return _create_region(player, name, RegionType.Menu, 'Menu', locations, exits) -def create_lw_region(player, name, locations=None, exits=None): - return _create_region(player, name, RegionType.LightWorld, 'Light World', locations, exits) +def create_lw_region(player, name, locations=None, exits=None, terrain=Terrain.Land): + region = _create_region(player, name, RegionType.LightWorld, 'Light World', locations, exits) + region.terrain = terrain + return region -def create_dw_region(player, name, locations=None, exits=None): - return _create_region(player, name, RegionType.DarkWorld, 'Dark World', locations, exits) +def create_dw_region(player, name, locations=None, exits=None, terrain=Terrain.Land): + region = _create_region(player, name, RegionType.DarkWorld, 'Dark World', locations, exits) + region.terrain = terrain + return region def create_cave_region(player, name, hint='Hyrule', locations=None, exits=None): @@ -1111,7 +1116,7 @@ def mark_dark_world_regions(world, player): def create_shops(world, player): world.shops[player] = [] for region_name, (room_id, type, shopkeeper, custom, locked, inventory, sram) in shop_table.items(): - if world.mode[player] == 'inverted' and region_name == 'Dark Lake Hylia Shop': + if (world.mode[player] == 'inverted') != (0x35 in world.owswaps[player][0] and world.owMixed[player]) and region_name == 'Dark Lake Hylia Shop': locked = True inventory = [('Blue Potion', 160), ('Blue Shield', 50), ('Bombs (10)', 50)] region = world.get_region(region_name, player) @@ -1531,9 +1536,9 @@ location_table = {'Mushroom': (0x180013, 0x186338, False, 'in the woods'), 'Red Shield Shop - Left': (None, None, False, 'for sale as a curiosity'), 'Red Shield Shop - Middle': (None, None, False, 'for sale as a curiosity'), 'Red Shield Shop - Right': (None, None, False, 'for sale as a curiosity'), - 'Potion Shop - Left': (None, None, False, 'for sale near the witch'), - 'Potion Shop - Middle': (None, None, False, 'for sale near the witch'), - 'Potion Shop - Right': (None, None, False, 'for sale near the witch'), + 'Potion Shop - Left': (None, None, False, 'for sale near potions'), + 'Potion Shop - Middle': (None, None, False, 'for sale near potions'), + 'Potion Shop - Right': (None, None, False, 'for sale near potions'), } lookup_id_to_name = {data[0]: name for name, data in location_table.items() if type(data[0]) == int} diff --git a/Rom.py b/Rom.py index 7d2bc546..c908d52a 100644 --- a/Rom.py +++ b/Rom.py @@ -29,9 +29,11 @@ from Items import ItemFactory from EntranceShuffle import door_addresses, exit_ids from OverworldShuffle import default_flute_connections, flute_data +from source.classes.SFX import randomize_sfx + JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = 'b9914f4de5a16b9a8bb94363fec0ac15' +RANDOMIZERBASEHASH = '2233f0ed58291daf0a4aaac58826ac95' class JsonRom(object): @@ -101,8 +103,7 @@ class LocalRom(object): self.buffer[address] = value def write_bytes(self, startaddress, values): - for i, value in enumerate(values): - self.write_byte(startaddress + i, value) + self.buffer[startaddress:startaddress + len(values)] = values def write_to_file(self, file): with open(file, 'wb') as outfile: @@ -625,71 +626,20 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): if world.pot_contents[player]: write_pots_to_rom(rom, world.pot_contents[player]) - # patch overworld edges - inverted_buffer = [0] * 0x82 - if world.owShuffle[player] != 'vanilla' or world.owSwap[player] != 'vanilla': - owMode = 0 - if world.owShuffle[player] == 'parallel': - owMode = 1 - elif world.owShuffle[player] == 'full': - owMode = 2 - - if world.owSwap[player] == 'mixed': - owMode |= 0x100 - world.fix_fake_world[player] = True - elif world.owSwap[player] == 'crossed': - owMode |= 0x200 - world.fix_fake_world[player] = True - - write_int16(rom, 0x150002, owMode) - - owFlags = 0 - if world.owKeepSimilar[player]: - owFlags |= 0x1 - if world.owFluteShuffle[player] != 'vanilla': - owFlags |= 0x100 - - write_int16(rom, 0x150004, owFlags) - - rom.write_byte(0x18004C, 0x01) # patch for allowing Frogsmith to enter multi-entrance caves - - # patches map data specific for OW Shuffle - #inverted_buffer[0x03] = inverted_buffer[0x03] | 0x2 # convenient portal on WDM - inverted_buffer[0x1A] = inverted_buffer[0x1A] | 0x2 # rocks added to prevent OWG hardlock - inverted_buffer[0x1B] = inverted_buffer[0x1B] | 0x2 # rocks added to prevent OWG hardlock - inverted_buffer[0x22] = inverted_buffer[0x22] | 0x2 # rocks added to prevent OWG hardlock - inverted_buffer[0x3F] = inverted_buffer[0x3F] | 0x2 # added C to terrain - #inverted_buffer[0x43] = inverted_buffer[0x43] | 0x2 # convenient portal on WDDM - inverted_buffer[0x5A] = inverted_buffer[0x5A] | 0x2 # rocks added to prevent OWG hardlock - inverted_buffer[0x5B] = inverted_buffer[0x5B] | 0x2 # rocks added to prevent OWG hardlock - inverted_buffer[0x62] = inverted_buffer[0x62] | 0x2 # rocks added to prevent OWG hardlock - inverted_buffer[0x7F] = inverted_buffer[0x7F] | 0x2 # added C to terrain - - if world.owSwap[player] == 'mixed': - for b in world.owswaps[player][0]: - # load inverted maps - inverted_buffer[b] = (inverted_buffer[b] & 0xFE) | ((inverted_buffer[b] + 1) % 2) - - # set world flag - rom.write_byte(0x153A00 + b, 0x00 if b >= 0x40 else 0x40) - - for edge in world.owedges: - if edge.dest is not None and isinstance(edge.dest, OWEdge) and edge.player == player: - write_int16(rom, edge.getAddress() + 0x0a, edge.vramLoc) - write_int16(rom, edge.getAddress() + 0x0e, edge.getTarget()) - # patch flute spots + owFlags = 0 if world.owFluteShuffle[player] == 'vanilla': flute_spots = default_flute_connections else: flute_spots = world.owflutespots[player] + owFlags |= 0x100 for o in range(0, len(flute_spots)): owslot = flute_spots[o] offset = 0 data = flute_data[owslot] - if (world.mode[player] == 'inverted') != (data[1] in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') != (data[1] in world.owswaps[player][0] and world.owMixed[player]): offset = 0x40 write_int16(rom, snes_to_pc(0x02E849 + (o * 2)), data[1] + offset) # owid @@ -709,6 +659,54 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): rom.write_byte(snes_to_pc(0x0AB793 + o), data[11] & 0xff) # Y low byte rom.write_byte(snes_to_pc(0x0AB79B + o), data[11] // 0x100) # Y high byte + # patch overworld edges + inverted_buffer = [0] * 0x82 + if world.owShuffle[player] != 'vanilla' or world.owCrossed[player] != 'none' or world.owMixed[player]: + owMode = 0 + if world.owShuffle[player] == 'parallel': + owMode = 1 + elif world.owShuffle[player] == 'full': + owMode = 2 + + if world.owKeepSimilar[player] and (world.owShuffle[player] != 'vanilla' or world.owCrossed[player] in ['limited', 'chaos']): + owMode |= 0x100 + if world.owCrossed[player] != 'none' and (world.owCrossed[player] != 'polar' or world.owMixed[player]): + owMode |= 0x200 + world.fix_fake_world[player] = True + if world.owMixed[player]: + owMode |= 0x400 + + write_int16(rom, 0x150002, owMode) + + write_int16(rom, 0x150004, owFlags) + + rom.write_byte(0x18004C, 0x01) # patch for allowing Frogsmith to enter multi-entrance caves + + # patches map data specific for OW Shuffle + #inverted_buffer[0x03] = inverted_buffer[0x03] | 0x2 # convenient portal on WDM + inverted_buffer[0x1A] = inverted_buffer[0x1A] | 0x2 # rocks added to prevent OWG hardlock + inverted_buffer[0x1B] = inverted_buffer[0x1B] | 0x2 # rocks added to prevent OWG hardlock + inverted_buffer[0x22] = inverted_buffer[0x22] | 0x2 # rocks added to prevent OWG hardlock + inverted_buffer[0x3F] = inverted_buffer[0x3F] | 0x2 # added C to terrain + #inverted_buffer[0x43] = inverted_buffer[0x43] | 0x2 # convenient portal on WDDM + inverted_buffer[0x5A] = inverted_buffer[0x5A] | 0x2 # rocks added to prevent OWG hardlock + inverted_buffer[0x5B] = inverted_buffer[0x5B] | 0x2 # rocks added to prevent OWG hardlock + inverted_buffer[0x62] = inverted_buffer[0x62] | 0x2 # rocks added to prevent OWG hardlock + inverted_buffer[0x7F] = inverted_buffer[0x7F] | 0x2 # added C to terrain + + if world.owMixed[player]: + for b in world.owswaps[player][0]: + # load inverted maps + inverted_buffer[b] = (inverted_buffer[b] & 0xFE) | ((inverted_buffer[b] + 1) % 2) + + # set world flag + rom.write_byte(0x153A00 + b, 0x00 if b >= 0x40 else 0x40) + + for edge in world.owedges: + if edge.dest is not None and isinstance(edge.dest, OWEdge) and edge.player == player: + write_int16(rom, edge.getAddress() + 0x0a, edge.vramLoc) + write_int16(rom, edge.getAddress() + 0x0e, edge.getTarget()) + # patch entrance/exits/holes for region in world.regions: @@ -1165,7 +1163,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): rom.write_bytes(0x184000, [ # original_item, limit, replacement_item, filler 0x12, 0x01, 0x35, 0xFF, # lamp -> 5 rupees - 0x51, 0x00 if world.bomblogic[player] else 0x06, 0x31 if world.bomblogic[player] else 0x52, 0xFF, # 6 +5 bomb upgrades -> +10 bomb upgrade. If bomblogic -> turns into Bombs (10) + 0x51, 0x00 if world.bombbag[player] else 0x06, 0x31 if world.bombbag[player] else 0x52, 0xFF, # 6 +5 bomb upgrades -> +10 bomb upgrade. If bombbag -> turns into Bombs (10) 0x53, 0x06, 0x54, 0xFF, # 6 +5 arrow upgrades -> +10 arrow upgrade 0x58, 0x01, 0x36 if world.retro[player] else 0x43, 0xFF, # silver arrows -> single arrow (red 20 in retro mode) 0x3E, difficulty.boss_heart_container_limit, 0x47, 0xff, # boss heart -> green 20 @@ -1346,7 +1344,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): equip[0x36C] = 0x18 equip[0x36D] = 0x18 equip[0x379] = 0x68 - if world.bomblogic[player]: + if world.bombbag[player]: starting_max_bombs = 0 else: starting_max_bombs = 10 @@ -1673,7 +1671,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): rom.write_bytes(0x180188, [0, 0, 10]) # Zelda respawn refills (magic, bombs, arrows) rom.write_bytes(0x18018B, [0, 0, 10]) # Mantle respawn refills (magic, bombs, arrows) bow_max, bow_small = 70, 10 - elif uncle_location.item is not None and uncle_location.item.name in ['Bomb Upgrade (+10)' if world.bomblogic[player] else 'Bombs (10)']: + elif uncle_location.item is not None and uncle_location.item.name in ['Bomb Upgrade (+10)' if world.bombbag[player] else 'Bombs (10)']: rom.write_byte(0x18004E, 2) # Escape Fill (bombs) rom.write_bytes(0x180185, [0, 50, 0]) # Uncle respawn refills (magic, bombs, arrows) rom.write_bytes(0x180188, [0, 3, 0]) # Zelda respawn refills (magic, bombs, arrows) @@ -1837,7 +1835,7 @@ def hud_format_text(text): def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, sprite, - ow_palettes, uw_palettes, reduce_flashing): + ow_palettes, uw_palettes, reduce_flashing, shuffle_sfx): if not os.path.exists("data/sprites/official/001.link.1.zspr") and rom.orig_buffer: dump_zspr(rom.orig_buffer[0x80000:0x87000], rom.orig_buffer[0xdd308:0xdd380], @@ -1926,6 +1924,9 @@ def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, spr elif uw_palettes == 'blackout': blackout_uw_palettes(rom) + if shuffle_sfx: + randomize_sfx(rom) + if isinstance(rom, LocalRom): rom.write_crc() @@ -2264,10 +2265,10 @@ def write_strings(rom, world, player, team): if world.shuffle[player] in ['insanity', 'madness_legacy', 'insanity_legacy']: entrances_to_hint.update(InsanityEntrances) if world.shuffle_ganon: - if world.mode[player] == 'inverted': + if world.mode[player] == 'inverted' != (0x1b in world.owswaps[player][0] and world.owMixed[player]): entrances_to_hint.update({'Inverted Pyramid Entrance': 'The extra castle passage'}) else: - entrances_to_hint.update({'Pyramid Ledge': 'The pyramid ledge'}) + entrances_to_hint.update({'Pyramid Entrance': 'The pyramid ledge'}) hint_count = 4 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull'] else 0 for entrance in all_entrances: if entrance.name in entrances_to_hint: @@ -2334,7 +2335,7 @@ def write_strings(rom, world, player, team): tt[hint_locations.pop(0)] = this_hint # Adding a guaranteed hint for the Flute in overworld shuffle. - if world.owShuffle[player] in ['parallel','full']: + if world.owShuffle[player] != 'vanilla' or world.owMixed[player]: this_location = world.find_items_not_key_only('Ocarina', player) if this_location: this_hint = this_location[0].item.hint_text + ' can be found ' + hint_text(this_location[0]) + '.' @@ -2342,7 +2343,7 @@ def write_strings(rom, world, player, team): # Lastly we write hints to show where certain interesting items are. It is done the way it is to re-use the silver code and also to give one hint per each type of item regardless of how many exist. This supports many settings well. items_to_hint = RelevantItems.copy() - if world.owShuffle[player] in ['parallel','full']: + if world.owShuffle[player] != 'vanilla' or world.owMixed[player]: items_to_hint.remove('Ocarina') if world.keyshuffle[player]: items_to_hint.extend(SmallKeys) @@ -2351,7 +2352,7 @@ def write_strings(rom, world, player, team): 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 += 1 if world.owShuffle[player] in ['parallel', 'full'] else 0 + hint_count += 1 if world.owShuffle[player] != 'vanilla' or world.owMixed[player] else 0 while hint_count > 0: this_item = items_to_hint.pop(0) this_location = world.find_items_not_key_only(this_item, player) @@ -2563,7 +2564,7 @@ def set_inverted_mode(world, player, rom, inverted_buffer): # load inverted maps for b in range(0x00, len(inverted_buffer)): inverted_buffer[b] = (inverted_buffer[b] & 0xFE) | ((inverted_buffer[b] + 1) % 2) - + rom.write_byte(snes_to_pc(0x0283E0), 0xF0) # residual portals rom.write_byte(snes_to_pc(0x02B34D), 0xF0) rom.write_byte(snes_to_pc(0x06DB78), 0x8B) # dark-style portal @@ -2574,7 +2575,7 @@ def set_inverted_mode(world, player, rom, inverted_buffer): rom.write_byte(snes_to_pc(0x08D40C), 0xD0) # morph poof rom.write_byte(snes_to_pc(0x0ABFBB), 0x90) # move mirror portal indicator to correct map (0xB0 normally) rom.write_byte(snes_to_pc(0x0280A6), 0xD0) # use starting point prompt instead of start at pyramid - + write_int16(rom, snes_to_pc(0x02D8D4), 0x112) # change sanctuary spawn point to dark sanc rom.write_bytes(snes_to_pc(0x02D8E8), [0x22, 0x22, 0x22, 0x23, 0x04, 0x04, 0x04, 0x05]) write_int16(rom, snes_to_pc(0x02D91A), 0x0400) @@ -2599,10 +2600,11 @@ def set_inverted_mode(world, player, rom, inverted_buffer): if world.doorShuffle[player] == 'vanilla' or world.intensity[player] < 3: write_int16(rom, 0x15AEE + 2*0x38, 0x00E0) write_int16(rom, 0x15AEE + 2*0x25, 0x000C) - if (world.mode[player] == 'inverted') != (0x03 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + + if (world.mode[player] == 'inverted') != (0x03 in world.owswaps[player][0] and world.owMixed[player]): if world.shuffle[player] in ['vanilla', 'dungeonsfull', 'dungeonssimple']: rom.write_bytes(snes_to_pc(0x308350), [0x00, 0x00, 0x01]) # mountain cave starts on OW - + write_int16(rom, snes_to_pc(0x02D8DE), 0x00F1) # change mountain cave spawn point to just outside old man cave rom.write_bytes(snes_to_pc(0x02D910), [0x1F, 0x1E, 0x1F, 0x1F, 0x03, 0x02, 0x03, 0x03]) write_int16(rom, snes_to_pc(0x02D924), 0x0300) @@ -2622,18 +2624,18 @@ def set_inverted_mode(world, player, rom, inverted_buffer): rom.write_byte(snes_to_pc(0x02D9B8), 0x12) rom.write_bytes(0x180247, [0x00, 0x5A, 0x00, 0x00, 0x00, 0x00, 0x00]) #indicates the overworld door being used for the single entrance spawn point - if (world.mode[player] == 'inverted') != (0x05 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') != (0x05 in world.owswaps[player][0] and world.owMixed[player]): rom.write_bytes(snes_to_pc(0x1BC655), [0x4A, 0x1D, 0x82]) # add warp under rock - if (world.mode[player] == 'inverted') != (0x07 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') != (0x07 in world.owswaps[player][0] and world.owMixed[player]): rom.write_bytes(snes_to_pc(0x1BC387), [0xDD, 0xD1]) # add warps under rocks rom.write_bytes(snes_to_pc(0x1BD1DD), [0xA4, 0x06, 0x82, 0x9E, 0x06, 0x82, 0xFF, 0xFF]) # add warps under rocks rom.write_byte(0x180089, 0x01) # open TR after exit rom.write_bytes(0x0086E, [0x5C, 0x00, 0xA0, 0xA1]) # TR tail if world.shuffle[player] in ['vanilla']: world.fix_trock_doors[player] = True - if (world.mode[player] == 'inverted') != (0x10 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') != (0x10 in world.owswaps[player][0] and world.owMixed[player]): rom.write_bytes(snes_to_pc(0x1BC67A), [0x2E, 0x0B, 0x82]) # add warp under rock - if (world.mode[player] == 'inverted') != (0x1B in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') != (0x1B in world.owswaps[player][0] and world.owMixed[player]): write_int16(rom, 0x15AEE + 2 * 0x06, 0x0020) # post aga hyrule castle spawn rom.write_byte(0x15B8C + 0x06, 0x1B) write_int16(rom, 0x15BDB + 2 * 0x06, 0x00AE) @@ -2647,7 +2649,7 @@ def set_inverted_mode(world, player, rom, inverted_buffer): rom.write_byte(0x1607C + 0x06, 0xF2) write_int16(rom, 0x160CB + 2 * 0x06, 0x0000) write_int16(rom, 0x16169 + 2 * 0x06, 0x0000) - + write_int16(rom, snes_to_pc(0x02E859), 0x001B) # move flute spot 9 write_int16(rom, snes_to_pc(0x02E87B), 0x00AE) write_int16(rom, snes_to_pc(0x02E89D), 0x0610) @@ -2665,12 +2667,12 @@ def set_inverted_mode(world, player, rom, inverted_buffer): 0x0C, 0x00, 0x7A, 0xAE, 0x0C, 0x00, 0x8A, 0xAE, 0x0C, 0x00, 0x67, 0x97, 0x0C, 0x00, 0x8D, 0x97, 0x0C, 0x00]) - + rom.write_byte(snes_to_pc(0x00D009), 0x31) # castle hole graphics rom.write_byte(snes_to_pc(0x00D0E8), 0xE0) rom.write_byte(snes_to_pc(0x00D1C7), 0x00) write_int16(rom, snes_to_pc(0x1BE8DA), 0x39AD) # add color for shading for castle hole - + #castle hole map16 data write_int16s(rom, snes_to_pc(0x0FF1C8), [0x190F, 0x190F, 0x190F, 0x194C, 0x190F, 0x194B, 0x190F, 0x195C, 0x594B, 0x194C, @@ -2685,18 +2687,18 @@ def set_inverted_mode(world, player, rom, inverted_buffer): write_int16s(rom, snes_to_pc(0x1BB810), [0x00BE, 0x00C0, 0x013E]) # update pyramid hole entrance write_int16s(rom, snes_to_pc(0x1BB836), [0x001B, 0x001B, 0x001B]) - + write_int16(rom, snes_to_pc(0x308300), 0x0140) # add extra pyramid hole write_int16(rom, snes_to_pc(0x308320), 0x001B) if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull']: rom.write_byte(snes_to_pc(0x308340), 0x7B) - + rom.write_byte(snes_to_pc(0x00DB9D), 0x1A) # make retreat bat gfx available in HC area rom.write_byte(snes_to_pc(0x00DC09), 0x1A) rom.write_byte(snes_to_pc(0x1AF696), 0xF0) # bat sprite retreat : bat X position rom.write_byte(snes_to_pc(0x1AF6B2), 0x33) # bat sprite retreat : bat delay - + write_int16(rom, snes_to_pc(0x1af504), 0x148B) # prioritize retreat Bat and use 3rd sprite group write_int16(rom, snes_to_pc(0x1af50c), 0x149B) write_int16(rom, snes_to_pc(0x1af514), 0x14A4) @@ -2715,33 +2717,33 @@ def set_inverted_mode(world, player, rom, inverted_buffer): write_int16(rom, snes_to_pc(0x1af57c), 0x548E) write_int16(rom, snes_to_pc(0x1af584), 0x14AE) write_int16(rom, snes_to_pc(0x1af58c), 0x54AE) - + rom.write_byte(0xF6E58, 0x80) # no whirlpool under castle gate rom.write_byte(snes_to_pc(0x09D436), 0xF3) # replace whirlpool with harmless sprite - + write_int16(rom, 0xDB96F + 2 * 0x35, 0x001B) # move pyramid exit door write_int16(rom, 0xDBA71 + 2 * 0x35, 0x011C) - if (world.mode[player] == 'inverted') != (0x29 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') != (0x29 in world.owswaps[player][0] and world.owMixed[player]): rom.write_bytes(snes_to_pc(0x06B2AB), [0xF0, 0xE1, 0x05]) # frog pickup on contact - if (world.mode[player] == 'inverted') != (0x2C in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') != (0x2C in world.owswaps[player][0] and world.owMixed[player]): if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull']: rom.write_byte(0x15B8C, 0x6C) # exit links at bomb shop area rom.write_byte(0xDBB73 + 0x00, 0x53) # switch bomb shop and links house rom.write_byte(0xDBB73 + 0x52, 0x01) - if (world.mode[player] == 'inverted') != (0x2F in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') != (0x2F in world.owswaps[player][0] and world.owMixed[player]): rom.write_bytes(snes_to_pc(0x1BC80D), [0xB2, 0x0B, 0x82]) # add warp under rock - if (world.mode[player] == 'inverted') != (0x30 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') != (0x30 in world.owswaps[player][0] and world.owMixed[player]): rom.write_bytes(snes_to_pc(0x1BC81E), [0x94, 0x1D, 0x82]) # add warp under rock - if (world.mode[player] == 'inverted') != (0x33 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') != (0x33 in world.owswaps[player][0] and world.owMixed[player]): rom.write_bytes(snes_to_pc(0x1BC3DF), [0xD8, 0xD1]) # add warp under rock rom.write_bytes(snes_to_pc(0x1BD1D8), [0xA8, 0x02, 0x82, 0xFF, 0xFF]) # add warp under rock - if (world.mode[player] == 'inverted') != (0x35 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') != (0x35 in world.owswaps[player][0] and world.owMixed[player]): rom.write_bytes(snes_to_pc(0x1BC85A), [0x50, 0x0F, 0x82]) # add warp under rock - + # apply inverted map changes for b in range(0x00, len(inverted_buffer)): rom.write_byte(0x153B00 + b, inverted_buffer[b]) - + def patch_shuffled_dark_sanc(world, rom, player): dark_sanc = world.get_region('Dark Sanctuary Hint', player) dark_sanc_entrance = str([i for i in dark_sanc.entrances if i.parent_region.name != 'Menu'][0].name) @@ -3048,7 +3050,7 @@ def write_pots_to_rom(rom, pot_contents): pots = [pot for pot in pot_contents[i] if pot.item != PotItem.Nothing] if len(pots) > 0: write_int16(rom, pot_item_room_table_lookup + 2*i, n) - rom.write_bytes(n, itertools.chain(*((pot.x,pot.y,pot.item) for pot in pots))) + rom.write_bytes(n, list(itertools.chain.from_iterable(((pot.x, pot.y, pot.item) for pot in pots)))) n += 3*len(pots) + 2 rom.write_bytes(n - 2, [0xFF,0xFF]) else: diff --git a/Rules.py b/Rules.py index e0aaa08c..416ff29e 100644 --- a/Rules.py +++ b/Rules.py @@ -3,7 +3,7 @@ import logging from collections import deque import OverworldGlitchRules -from BaseClasses import CollectionState, RegionType, DoorType, Entrance, CrystalBarrier +from BaseClasses import CollectionState, RegionType, DoorType, Entrance, CrystalBarrier, KeyRuleType from RoomData import DoorKind from OverworldGlitchRules import overworld_glitches_rules @@ -174,8 +174,8 @@ def global_rules(world, player): exit.hide_path = True world.get_region('Flute Sky', player).can_reach_private = lambda state: True - for exit in world.get_region('Flute Sky', player).exits: - exit.hide_path = True + #for exit in world.get_region('Flute Sky', player).exits: + # exit.hide_path = True set_rule(world.get_entrance('Old Man S&Q', player), lambda state: state.can_reach('Old Man', 'Location', player)) @@ -804,8 +804,6 @@ def default_rules(world, player): set_rule(world.get_entrance('Dark C Whirlpool Rock (Bottom)', player), lambda state: state.can_lift_rocks(player)) set_rule(world.get_entrance('Dark C Whirlpool Rock (Top)', player), lambda state: state.can_lift_rocks(player)) - set_rule(world.get_entrance('Flute Away', player), lambda state: state.can_flute(player)) - set_rule(world.get_entrance('Zora Waterfall Water Drop', player), lambda state: state.has('Flippers', player)) set_rule(world.get_entrance('Zora Waterfall Water Entry', player), lambda state: state.has('Flippers', player)) set_rule(world.get_entrance('Waterfall of Wishing Cave Entry', player), lambda state: state.has('Flippers', player)) @@ -842,6 +840,7 @@ def default_rules(world, player): set_rule(world.get_entrance('Ice Lake Northeast Water Drop', player), lambda state: state.has('Flippers', player)) set_rule(world.get_entrance('Ice Lake Southwest Water Drop', player), lambda state: state.has('Flippers', player)) set_rule(world.get_entrance('Ice Lake Southeast Water Drop', player), lambda state: state.has('Flippers', player)) + set_rule(world.get_entrance('Ice Lake Moat Water Entry', player), lambda state: state.has('Flippers', player)) set_rule(world.get_entrance('Shopping Mall SW', player), lambda state: state.has('Flippers', player)) set_rule(world.get_entrance('Bomber Corner Water Drop', player), lambda state: state.has('Flippers', player)) set_rule(world.get_entrance('Bomber Corner Waterfall Water Drop', player), lambda state: state.has('Flippers', player)) @@ -862,7 +861,7 @@ def ow_rules(world, player): else: set_rule(world.get_entrance('Agahnims Tower', player), lambda state: state.has_crystals(world.crystals_needed_for_gt[player], player)) - if (world.mode[player] == 'inverted') == (0x00 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x00 in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Lost Woods East Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Lost Woods Entry Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Lost Woods Pedestal Mirror Spot', player), lambda state: state.has_Mirror(player)) @@ -877,15 +876,12 @@ def ow_rules(world, player): set_rule(world.get_entrance('Skull Woods Forgotten (Middle) Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Skull Woods Front Mirror Spot', player), lambda state: state.has_Mirror(player)) - if (world.mode[player] == 'inverted') == (0x02 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x02 in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Lumberjack Mirror Spot', player), lambda state: state.has_Mirror(player)) else: set_rule(world.get_entrance('Dark Lumberjack Mirror Spot', player), lambda state: state.has_Mirror(player)) - if (world.mode[player] == 'inverted') == (0x03 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): - set_rule(world.get_entrance('GT Entry Approach', player), lambda state: state.has_crystals(world.crystals_needed_for_gt[player], player)) - set_rule(world.get_entrance('GT Entry Leave', player), lambda state: state.has_crystals(world.crystals_needed_for_gt[player], player) or state.world.shuffle[player] in ('restricted', 'full', 'crossed', 'insanity')) - + if (world.mode[player] == 'inverted') == (0x03 in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('West Death Mountain (Top) Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Spectacle Rock Mirror Spot', player), lambda state: state.has_Mirror(player)) else: @@ -893,7 +889,7 @@ def ow_rules(world, player): set_rule(world.get_entrance('Bubble Boy Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('West Dark Death Mountain (Bottom) Mirror Spot', player), lambda state: state.has_Mirror(player)) - if (world.mode[player] == 'inverted') == (0x05 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x05 in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('East Death Mountain (Top West) Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('East Death Mountain (Top East) Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Mimic Cave Mirror Spot', player), lambda state: state.has_Mirror(player)) @@ -915,7 +911,7 @@ def ow_rules(world, player): set_rule(world.get_entrance('Dark Floating Island Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Dark Death Mountain Teleporter (East)', player), lambda state: state.can_lift_heavy_rocks(player)) - if (world.mode[player] == 'inverted') == (0x07 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x07 in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('TR Pegs Area Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('TR Pegs Teleporter', player), lambda state: state.has('Hammer', player)) else: @@ -923,7 +919,7 @@ def ow_rules(world, player): set_rule(world.get_entrance('Turtle Rock Ledge Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Turtle Rock Teleporter', player), lambda state: state.has('Hammer', player) and state.can_lift_heavy_rocks(player) and state.has_Pearl(player)) - if (world.mode[player] == 'inverted') == (0x0a in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x0a in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Mountain Entry Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Mountain Entry Ledge Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Mountain Entry Entrance Mirror Spot', player), lambda state: state.has_Mirror(player)) @@ -932,12 +928,12 @@ def ow_rules(world, player): set_rule(world.get_entrance('Bumper Cave Entry Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Bumper Cave Ledge Mirror Spot', player), lambda state: state.has_Mirror(player)) - if (world.mode[player] == 'inverted') == (0x0f in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x0f in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Zora Waterfall Mirror Spot', player), lambda state: state.has_Mirror(player)) else: set_rule(world.get_entrance('Catfish Mirror Spot', player), lambda state: state.has_Mirror(player)) - if (world.mode[player] == 'inverted') == (0x10 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x10 in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Lost Woods Pass West Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Lost Woods Pass East Top Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Lost Woods Pass East Bottom Mirror Spot', player), lambda state: state.has_Mirror(player)) @@ -950,24 +946,24 @@ def ow_rules(world, player): set_rule(world.get_entrance('West Dark World Teleporter (Hammer)', player), lambda state: state.has('Hammer', player) and state.can_lift_rocks(player) and state.has_Pearl(player)) set_rule(world.get_entrance('West Dark World Teleporter (Rock)', player), lambda state: state.can_lift_heavy_rocks(player) and state.has_Pearl(player)) # bunny cannot lift bushes - if (world.mode[player] == 'inverted') == (0x11 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x11 in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Kakariko Fortune Mirror Spot', player), lambda state: state.has_Mirror(player)) else: set_rule(world.get_entrance('Outcast Fortune Mirror Spot', player), lambda state: state.has_Mirror(player)) - if (world.mode[player] == 'inverted') == (0x12 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x12 in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Kakariko Pond Mirror Spot', player), lambda state: state.has_Mirror(player)) else: set_rule(world.get_entrance('Outcast Pond Mirror Spot', player), lambda state: state.has_Mirror(player)) - if (world.mode[player] == 'inverted') == (0x13 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x13 in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Sanctuary Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Bonk Rock Ledge Mirror Spot', player), lambda state: state.has_Mirror(player)) else: set_rule(world.get_entrance('Dark Chapel Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Dark Chapel Ledge Mirror Spot', player), lambda state: state.has_Mirror(player)) - if (world.mode[player] == 'inverted') == (0x14 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x14 in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Graveyard Ledge Mirror Spot', player), lambda state: state.has_Pearl(player) and state.has_Mirror(player)) set_rule(world.get_entrance('Kings Grave Mirror Spot', player), lambda state: state.has_Pearl(player) and state.has_Mirror(player)) else: @@ -977,28 +973,28 @@ def ow_rules(world, player): set_rule(world.get_entrance('Dark Graveyard Ledge Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Dark Graveyard Grave Mirror Spot', player), lambda state: state.has_Mirror(player)) - if (world.mode[player] == 'inverted') == (0x15 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x15 in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('River Bend Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('River Bend East Mirror Spot', player), lambda state: state.has_Mirror(player)) else: set_rule(world.get_entrance('Qirn Jump Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Qirn Jump East Mirror Spot', player), lambda state: state.has_Mirror(player)) - if (world.mode[player] == 'inverted') == (0x16 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x16 in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Potion Shop Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Potion Shop Northeast Mirror Spot', player), lambda state: state.has_Mirror(player)) else: set_rule(world.get_entrance('Dark Witch Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Dark Witch Northeast Mirror Spot', player), lambda state: state.has_Mirror(player)) - if (world.mode[player] == 'inverted') == (0x17 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x17 in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Zora Approach Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Zora Approach Ledge Mirror Spot', player), lambda state: state.has_Mirror(player)) else: set_rule(world.get_entrance('Catfish Approach Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Catfish Approach Ledge Mirror Spot', player), lambda state: state.has_Mirror(player)) - if (world.mode[player] == 'inverted') == (0x18 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x18 in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Kakariko Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Kakariko Grass Mirror Spot', player), lambda state: state.has_Mirror(player)) else: @@ -1006,13 +1002,13 @@ def ow_rules(world, player): set_rule(world.get_entrance('Village of Outcasts Southwest Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Hammer House Mirror Spot', player), lambda state: state.has_Mirror(player)) - if (world.mode[player] == 'inverted') == (0x1a in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x1a in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Forgotton Forest Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Forgotton Forest Fence Mirror Spot', player), lambda state: state.has_Mirror(player)) else: set_rule(world.get_entrance('Shield Shop Mirror Spot', player), lambda state: state.has_Mirror(player)) - if (world.mode[player] == 'inverted') == (0x1b in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x1b in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Inverted Pyramid Hole', player), lambda state: False) set_rule(world.get_entrance('Inverted Pyramid Entrance', player), lambda state: False) set_rule(world.get_entrance('Pyramid Hole', player), lambda state: state.has('Beat Agahnim 2', player) or world.open_pyramid[player]) @@ -1039,7 +1035,7 @@ def ow_rules(world, player): set_rule(world.get_entrance('Pyramid Entry Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Post Aga Inverted Teleporter', player), lambda state: state.has('Beat Agahnim 1', player)) - if (world.mode[player] == 'inverted') == (0x1d in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x1d in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Wooden Bridge Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Wooden Bridge Northeast Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Wooden Bridge West Mirror Spot', player), lambda state: state.has_Mirror(player)) @@ -1048,12 +1044,12 @@ def ow_rules(world, player): set_rule(world.get_entrance('Broken Bridge East Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Broken Bridge Northeast Mirror Spot', player), lambda state: state.has_Mirror(player)) - if (world.mode[player] == 'inverted') == (0x1e in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x1e in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Eastern Palace Mirror Spot', player), lambda state: state.has_Mirror(player)) else: set_rule(world.get_entrance('Palace of Darkness Mirror Spot', player), lambda state: state.has_Mirror(player)) - if (world.mode[player] == 'inverted') == (0x22 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x22 in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Blacksmith Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Blacksmith Entry Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Bat Cave Ledge Mirror Spot', player), lambda state: state.has_Mirror(player)) @@ -1061,19 +1057,19 @@ def ow_rules(world, player): set_rule(world.get_entrance('Hammer Pegs Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Hammer Pegs Entry Mirror Spot', player), lambda state: state.has_Mirror(player)) - if (world.mode[player] == 'inverted') == (0x25 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x25 in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Sand Dunes Mirror Spot', player), lambda state: state.has_Mirror(player)) else: set_rule(world.get_entrance('Dark Dunes Mirror Spot', player), lambda state: state.has_Mirror(player)) - if (world.mode[player] == 'inverted') == (0x28 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x28 in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Maze Race Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Maze Race Ledge Mirror Spot', player), lambda state: state.has_Mirror(player)) else: set_rule(world.get_entrance('Dig Game Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Dig Game Ledge Mirror Spot', player), lambda state: state.has_Mirror(player)) - if (world.mode[player] == 'inverted') == (0x29 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x29 in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Kakariko Suburb Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Kakariko Suburb South Mirror Spot', player), lambda state: state.has_Mirror(player)) else: @@ -1081,24 +1077,24 @@ def ow_rules(world, player): set_rule(world.get_entrance('Frog Prison Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Archery Game Mirror Spot', player), lambda state: state.has_Mirror(player)) - if (world.mode[player] == 'inverted') == (0x2a in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x2a in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Flute Boy Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Flute Boy Pass Mirror Spot', player), lambda state: state.has_Mirror(player)) else: set_rule(world.get_entrance('Stumpy Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Stumpy Pass Mirror Spot', player), lambda state: state.has_Mirror(player)) - if (world.mode[player] == 'inverted') == (0x2b in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x2b in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Central Bonk Rocks Mirror Spot', player), lambda state: state.has_Mirror(player)) else: set_rule(world.get_entrance('Dark Bonk Rocks Mirror Spot', player), lambda state: state.has_Mirror(player)) - if (world.mode[player] == 'inverted') == (0x2c in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x2c in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Links House Mirror Spot', player), lambda state: state.has_Mirror(player)) else: set_rule(world.get_entrance('Big Bomb Shop Mirror Spot', player), lambda state: state.has_Mirror(player)) - if (world.mode[player] == 'inverted') == (0x2d in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x2d in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Stone Bridge Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Stone Bridge South Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Hobo Mirror Spot', player), lambda state: state.has_Mirror(player) and state.has_Pearl(player) and state.has('Flippers', player)) @@ -1107,19 +1103,19 @@ def ow_rules(world, player): set_rule(world.get_entrance('Hammer Bridge South Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Dark Hobo Mirror Spot', player), lambda state: state.has_Mirror(player) and state.has_Pearl(player) and state.has('Flippers', player)) - if (world.mode[player] == 'inverted') == (0x2e in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x2e in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Tree Line Mirror Spot', player), lambda state: state.has_Mirror(player)) else: set_rule(world.get_entrance('Dark Tree Line Mirror Spot', player), lambda state: state.has_Mirror(player)) - if (world.mode[player] == 'inverted') == (0x2f in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x2f in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Eastern Nook Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('East Hyrule Teleporter', player), lambda state: state.has('Hammer', player) and state.can_lift_rocks(player) and state.has_Pearl(player)) # bunny cannot use hammer else: set_rule(world.get_entrance('Darkness Nook Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('East Dark World Teleporter', player), lambda state: state.has('Hammer', player) and state.can_lift_rocks(player) and state.has_Pearl(player)) - if (world.mode[player] == 'inverted') == (0x30 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x30 in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Checkerboard Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Desert Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Desert Ledge Mirror Spot', player), lambda state: state.has_Mirror(player)) @@ -1134,14 +1130,14 @@ def ow_rules(world, player): set_rule(world.get_entrance('Misery Mire Main Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Misery Mire Teleporter', player), lambda state: state.can_lift_heavy_rocks(player)) - if (world.mode[player] == 'inverted') == (0x32 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x32 in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Cave 45 Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Flute Boy Entry Mirror Spot', player), lambda state: state.has_Mirror(player)) else: set_rule(world.get_entrance('Stumpy Approach Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Stumpy Bush Entry Mirror Spot', player), lambda state: state.has_Mirror(player)) - if (world.mode[player] == 'inverted') == (0x33 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x33 in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('C Whirlpool Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('C Whirlpool Outer Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('South Hyrule Teleporter', player), lambda state: state.has('Hammer', player) and state.can_lift_rocks(player) and state.has_Pearl(player)) # bunny cannot use hammer @@ -1152,12 +1148,12 @@ def ow_rules(world, player): set_rule(world.get_entrance('South Dark World Teleporter', player), lambda state: state.has('Hammer', player) and state.can_lift_rocks(player) and state.has_Pearl(player)) set_rule(world.get_entrance('South Teleporter Cliff Ledge Drop', player), lambda state: state.can_lift_rocks(player) and state.has_Pearl(player)) - if (world.mode[player] == 'inverted') == (0x34 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x34 in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Statues Mirror Spot', player), lambda state: state.has_Mirror(player)) else: set_rule(world.get_entrance('Hype Cave Mirror Spot', player), lambda state: state.has_Mirror(player)) - if (world.mode[player] == 'inverted') == (0x35 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x35 in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Lake Hylia Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Lake Hylia Northeast Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Lake Hylia Island Mirror Spot', player), lambda state: state.has_Mirror(player) and state.has_Pearl(player) and state.has('Flippers', player)) @@ -1174,12 +1170,12 @@ def ow_rules(world, player): set_rule(world.get_entrance('Ice Palace Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Ice Palace Teleporter', player), lambda state: state.can_lift_heavy_rocks(player)) - if (world.mode[player] == 'inverted') == (0x37 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x37 in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Ice Cave Mirror Spot', player), lambda state: state.has_Mirror(player)) else: set_rule(world.get_entrance('Shopping Mall Mirror Spot', player), lambda state: state.has_Mirror(player)) - if (world.mode[player] == 'inverted') == (0x3a in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x3a in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Desert Pass Ledge Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Desert Pass Mirror Spot', player), lambda state: state.has_Mirror(player)) else: @@ -1187,17 +1183,17 @@ def ow_rules(world, player): set_rule(world.get_entrance('Swamp Nook Southeast Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Swamp Nook Pegs Mirror Spot', player), lambda state: state.has_Mirror(player)) - if (world.mode[player] == 'inverted') == (0x3b in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x3b in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Dam Mirror Spot', player), lambda state: state.has_Mirror(player)) else: set_rule(world.get_entrance('Swamp Mirror Spot', player), lambda state: state.has_Mirror(player)) - if (world.mode[player] == 'inverted') == (0x3c in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x3c in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('South Pass Mirror Spot', player), lambda state: state.has_Mirror(player)) else: set_rule(world.get_entrance('Dark South Pass Mirror Spot', player), lambda state: state.has_Mirror(player)) - if (world.mode[player] == 'inverted') == (0x3f in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x3f in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Octoballoon Mirror Spot', player), lambda state: state.has_Mirror(player)) else: set_rule(world.get_entrance('Bomber Corner Mirror Spot', player), lambda state: state.has_Mirror(player)) @@ -1285,6 +1281,8 @@ def ow_bunny_rules(world, player): add_bunny_rule(world.get_entrance('Skull Woods Pass Bush Row (East)', player), player) add_bunny_rule(world.get_entrance('Skull Woods Pass Rock (Top)', player), player) add_bunny_rule(world.get_entrance('Skull Woods Pass Rock (Bottom)', player), player) + add_bunny_rule(world.get_entrance('Dark Graveyard Bush (South)', player), player) + add_bunny_rule(world.get_entrance('Dark Graveyard Bush (North)', player), player) add_bunny_rule(world.get_entrance('Dark Witch Rock (North)', player), player) add_bunny_rule(world.get_entrance('Dark Witch Rock (South)', player), player) add_bunny_rule(world.get_entrance('Catfish Approach Rocks (West)', player), player) @@ -1344,6 +1342,7 @@ def ow_bunny_rules(world, player): add_bunny_rule(world.get_entrance('Ice Lake Northeast Water Drop', player), player) add_bunny_rule(world.get_entrance('Ice Lake Southwest Water Drop', player), player) add_bunny_rule(world.get_entrance('Ice Lake Southeast Water Drop', player), player) + add_bunny_rule(world.get_entrance('Ice Lake Moat Water Entry', player), player) add_bunny_rule(world.get_entrance('Shopping Mall SW', player), player) add_bunny_rule(world.get_entrance('Bomber Corner Water Drop', player), player) add_bunny_rule(world.get_entrance('Bomber Corner Waterfall Water Drop', player), player) @@ -1509,9 +1508,10 @@ def swordless_rules(world, player): if world.mode[player] != 'inverted': set_rule(world.get_entrance('Agahnims Tower', player), lambda state: state.has('Cape', player) or state.has('Hammer', player) or state.has('Beat Agahnim 1', player)) # barrier gets removed after killing agahnim, relevant for entrance shuffle + set_rule(world.get_entrance('Misery Mire', player), lambda state: state.has_misery_mire_medallion(player)) # sword not required to use medallion for opening in swordless (!) set_rule(world.get_entrance('Turtle Rock', player), lambda state: state.has_turtle_rock_medallion(player) and state.can_reach('Turtle Rock Ledge', 'Region', player)) # sword not required to use medallion for opening in swordless (!) - add_bunny_rule(world.get_entrance('Turtle Rock', player), player) add_bunny_rule(world.get_entrance('Misery Mire', player), player) + add_bunny_rule(world.get_entrance('Turtle Rock', player), player) def bomb_mode_rules(world, player): set_rule(world.get_entrance('Tower Altar NW', player), lambda state: True) @@ -1593,7 +1593,7 @@ def standard_rules(world, player): def bomb_escape_rule(): loc = world.get_location("Link's Uncle", player) - return loc.item and loc.item.name in ['Bomb Upgrade (+10)' if world.bomblogic[player] else 'Bombs (10)'] + return loc.item and loc.item.name in ['Bomb Upgrade (+10)' if world.bombbag[player] else 'Bombs (10)'] def standard_escape_rule(state): return state.can_kill_most_things(player) or bomb_escape_rule() @@ -2090,7 +2090,7 @@ def set_inverted_big_bomb_rules(world, player): else: raise Exception('No logic found for routing from %s to the pyramid.' % bombshop_entrance.name) - if world.owShuffle[player] != 'vanilla' or world.owSwap[player] != 'vanilla': + if world.owShuffle[player] != 'vanilla' or world.owMixed[player] or world.owCrossed[player] != 'none': add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: False) #temp disable progression until routing to Pyramid get be guaranteed @@ -2366,14 +2366,8 @@ bunny_impassible_doors = { def add_key_logic_rules(world, player): key_logic = world.key_logic[player] for d_name, d_logic in key_logic.items(): - for door_name, keys in d_logic.door_rules.items(): - spot = world.get_entrance(door_name, player) - if not world.retro[player] or world.mode[player] != 'standard' or not retro_in_hc(spot): - rule = create_advanced_key_rule(d_logic, player, keys) - if keys.opposite: - rule = or_rule(rule, create_advanced_key_rule(d_logic, player, keys.opposite)) - add_rule(spot, rule) - + for door_name, rule in d_logic.door_rules.items(): + add_rule(world.get_entrance(door_name, player), eval_small_key_door(door_name, d_name, player)) for location in d_logic.bk_restricted: if not location.forced_item: forbid_item(location, d_logic.bk_name, player) @@ -2382,7 +2376,10 @@ def add_key_logic_rules(world, player): for door in d_logic.bk_doors: add_rule(world.get_entrance(door.name, player), create_rule(d_logic.bk_name, player)) for chest in d_logic.bk_chests: - add_rule(world.get_location(chest.name, player), create_rule(d_logic.bk_name, player)) + big_chest = world.get_location(chest.name, player) + add_rule(big_chest, create_rule(d_logic.bk_name, player)) + if len(d_logic.bk_doors) == 0 and len(d_logic.bk_chests) <= 1: + set_always_allow(big_chest, lambda state, item: item.name == d_logic.bk_name and item.player == player) if world.retro[player]: for d_name, layout in world.key_layout[player].items(): for door in layout.flat_prop: @@ -2390,6 +2387,36 @@ def add_key_logic_rules(world, player): add_rule(door.entrance, create_key_rule('Small Key (Universal)', player, 1)) +def eval_small_key_door_main(state, door_name, dungeon, player): + if state.is_door_open(door_name, player): + return True + key_logic = state.world.key_logic[player][dungeon] + door_rule = key_logic.door_rules[door_name] + door_openable = False + for ruleType, number in door_rule.new_rules.items(): + if door_openable: + return True + if ruleType == KeyRuleType.WorstCase: + door_openable |= state.has_sm_key(key_logic.small_key_name, player, number) + elif ruleType == KeyRuleType.AllowSmall: + if (door_rule.small_location.item and door_rule.small_location.item.name == key_logic.small_key_name + and door_rule.small_location.item.player == player): + return True # always okay if allow small is on + elif isinstance(ruleType, tuple): + lock, lock_item = ruleType + # this doesn't track logical locks yet, i.e. hammer locks the item and hammer is there, but the item isn't + for loc in door_rule.alternate_big_key_loc: + spot = state.world.get_location(loc, player) + if spot.item and spot.item.name == lock_item: + door_openable |= state.has_sm_key(key_logic.small_key_name, player, number) + break + return door_openable + + +def eval_small_key_door(door_name, dungeon, player): + return lambda state: eval_small_key_door_main(state, door_name, dungeon, player) + + def retro_in_hc(spot): return spot.parent_region.dungeon.name == 'Hyrule Castle' if spot.parent_region.dungeon else False diff --git a/Utils.py b/Utils.py index 01d4f8bf..0d259936 100644 --- a/Utils.py +++ b/Utils.py @@ -1,10 +1,12 @@ #!/usr/bin/env python3 import os import re +import operator as op import subprocess import sys import xml.etree.ElementTree as ET from collections import defaultdict +from functools import reduce def int16_as_bytes(value): @@ -116,6 +118,28 @@ def make_new_base2current(old_rom='Zelda no Densetsu - Kamigami no Triforce (Jap return "New Rom Hash: " + basemd5.hexdigest() +def kth_combination(k, l, r): + if r == 0: + return [] + elif len(l) == r: + return l + else: + i = ncr(len(l)-1, r-1) + if k < i: + return l[0:1] + kth_combination(k, l[1:], r-1) + else: + return kth_combination(k-i, l[1:], r) + + +def ncr(n, r): + if r == 0: + return 1 + r = min(r, n-r) + numerator = reduce(op.mul, range(n, n-r, -1), 1) + denominator = reduce(op.mul, range(1, r+1), 1) + return numerator / denominator + + entrance_offsets = { 'Sanctuary': 0x2, 'HC West': 0x3, @@ -651,18 +675,18 @@ class bidict(dict): super(bidict, self).__init__(*args, **kwargs) self.inverse = {} for key, value in self.items(): - self.inverse.setdefault(value,[]).append(key) + self.inverse.setdefault(value,[]).append(key) def __setitem__(self, key, value): if key in self: - self.inverse[self[key]].remove(key) + self.inverse[self[key]].remove(key) super(bidict, self).__setitem__(key, value) - self.inverse.setdefault(value,[]).append(key) + self.inverse.setdefault(value,[]).append(key) def __delitem__(self, key): value = self[key] self.inverse.setdefault(value,[]).remove(key) - if value in self.inverse and not self.inverse[value]: + if value in self.inverse and not self.inverse[value]: del self.inverse[value] super(bidict, self).__delitem__(key) diff --git a/asm/doortables.asm b/asm/doortables.asm index 8ee0dd60..eae16430 100644 --- a/asm/doortables.asm +++ b/asm/doortables.asm @@ -582,6 +582,8 @@ dw $00bc, $00a2, $00a3, $00c2, $001a, $0049, $0014, $008c dw $009f, $0066, $005d, $00a8, $00a9, $00aa, $00b9, $0052 ; HC West Hall, TR Dash Bridge, TR Hub, Pod Arena, GT Petting Zoo, Ice Spike Cross dw $0050, $00c5, $00c6, $0009, $0003, $002a, $007d, $005e +; Sewer Drop, Mire Cross, GT Crystal Circles +dw $0011, $00b2, $003d dw $ffff ; dungeon tables diff --git a/asm/owrando.asm b/asm/owrando.asm index 8cd7bcf1..8082d38f 100644 --- a/asm/owrando.asm +++ b/asm/owrando.asm @@ -341,23 +341,27 @@ OWNewDestination: sep #$30 : lda OWOppSlotOffset,y : !add $04 : asl : and #$7f : sta $700 ; crossed OW shuffle - lda.l OWMode+1 : and #$ff : cmp #$02 : bne .return - ldx $05 : lda.l OWTileWorldAssoc,x : sta.l $7ef3ca ; change world + LDA.l OWMode+1 : AND.b #!FLAG_OW_CROSSED : beq .return + ldx $05 : lda.l OWTileWorldAssoc,x : cmp.l $7ef3ca : beq .return + sta.l $7ef3ca ; change world + lda #$38 : sta $012f ; play sfx - #$3b is an alternative - ; toggle bunny mode - lda $7ef357 : bne .nobunny - lda.l InvertedMode : bne .inverted - lda $7ef3ca : and.b #$40 : bra + - .inverted lda $7ef3ca : and.b #$40 : eor #$40 - + cmp #$40 : bne .nobunny - ; turn into bunny - lda $5d : cmp #$17 : beq .return - lda #$17 : sta $5d - lda #$01 : sta $2e0 - bra .return - .nobunny - lda $5d : cmp #$17 : bne .return - stz $5d : stz $2e0 + ; toggle bunny mode + + lda $7ef357 : bne .nobunny + lda.l InvertedMode : bne .inverted + lda $7ef3ca : and.b #$40 : bra + + .inverted lda $7ef3ca : and.b #$40 : eor #$40 + + cmp #$40 : bne .nobunny + ; turn into bunny + lda $5d : cmp #$04 : beq + ; if swimming, continue + lda #$17 : sta $5d + + lda #$01 : sta $02e0 : sta $56 + bra .return + + .nobunny + lda $5d : cmp #$17 : bne + ; retain current state unless bunny + stz $5d + + stz $02e0 : stz $56 .return lda $05 : sta $8a diff --git a/data/base2current.bps b/data/base2current.bps index cc13e078..a071dc1e 100644 Binary files a/data/base2current.bps and b/data/base2current.bps differ diff --git a/mystery_example.yml b/mystery_example.yml index b6b6f051..6bef94e6 100644 --- a/mystery_example.yml +++ b/mystery_example.yml @@ -3,13 +3,15 @@ vanilla: 0 parallel: 2 full: 2 + overworld_crossed: + on: 1 + off: 1 overworld_keepsimilar: on: 1 off: 1 - overworld_swap: - vanilla: 0 - mixed: 2 - crossed: 2 + overworld_mixed: + on: 1 + off: 1 flute_shuffle: vanilla: 0 balanced: 1 @@ -31,6 +33,9 @@ pot_shuffle: on: 1 off: 3 + bombbag: + on: 1 + off: 4 entrance_shuffle: none: 15 dungeonssimple: 3 @@ -147,3 +152,6 @@ half: 0 quarter: 1 off: 0 + shuffle_sfx: + on: 1 + off: 1 diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index d7bf9410..5a956c8a 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -119,17 +119,23 @@ "full" ] }, - "ow_swap": { + "ow_crossed": { "choices": [ - "vanilla", - "mixed", - "crossed" + "none", + "polar", + "grouped", + "limited", + "chaos" ] }, "ow_keepsimilar": { "action": "store_true", "type": "bool" }, + "ow_mixed": { + "action": "store_true", + "type": "bool" + }, "ow_fluteshuffle": { "choices": [ "vanilla", @@ -145,10 +151,6 @@ "full", "crossed", "insanity", - "restricted_legacy", - "full_legacy", - "madness_legacy", - "insanity_legacy", "dungeonsfull", "dungeonssimple" ] @@ -266,6 +268,10 @@ "action": "store_true", "type": "bool" }, + "shuffle_sfx": { + "action": "store_true", + "type": "bool" + }, "mapshuffle": { "action": "store_true", "type": "bool" @@ -287,7 +293,7 @@ "type": "bool", "help": "suppress" }, - "bomblogic": { + "bombbag": { "action": "store_true", "type": "bool" }, diff --git a/resources/app/cli/lang/en.json b/resources/app/cli/lang/en.json index 88d5f083..87f7136b 100644 --- a/resources/app/cli/lang/en.json +++ b/resources/app/cli/lang/en.json @@ -2,7 +2,7 @@ "cli": { "yes": "Yes", "no": "No", - "app.title": "ALttP Door Randomizer Version %s - Seed: %s, Code: %s", + "app.title": "ALttP Door Randomizer Version %s : --seed %s --code %s", "version": "Version", "seed": "Seed", "player": "Player", @@ -194,10 +194,8 @@ " which they are entered.", "Vanilla: All entrances are in the same locations they were", " in the base game.", - "Legacy shuffles preserve behavior from older versions of the", - "entrance randomizer including significant technical limitations.", "The dungeon variants only mix up dungeons and keep the rest of", - "the overworld vanilla." + "the entrances vanilla." ], "ow_shuffle": [ "This shuffles the layout of the overworld.", @@ -208,22 +206,27 @@ "Full: Overworld transitions are shuffled, but both worlds", " will have an independent map shape." ], - "ow_swap": [ - "This swaps the tiles of the overworld from one world to the other.", - "Vanilla: All overworld tiles remain in their original world as", - " they were in the base game.", - "Mixed: Overworld tiles are randomly chosen to become part of", - " the opposite world.", - "Crossed: Overworld tiles remain in their original world, but", - " the transitions are shuffled cross world." + "ow_crossed": [ + "This allows cross-world connections to occur on the overworld.", + "None: No transitions are cross-world connections.", + "Polar: Only used when Mixed is enabled. This retains original", + " connections even when overworld tiles are swapped.", + "Limited: Exactly nine transitions are randomly chosen as", + " cross-world connections (to emulate the nine portals).", + "Chaos: Every transition has a 50/50 chance to become a", + " crossworld connection." ], "ow_keepsimilar": [ "This keeps similar edge transitions together. ie. the two west edges on", "Potion Shop will be paired with another similar pair." ], + "ow_mixed": [ + "Overworld tiles are randomly chosen to become part of the opposite world." + ], "ow_fluteshuffle": [ "This randomizes the flute spot destinations.", "Vanilla: All flute spots remain unchanged.", - "Balanced: New flute spots will be generated but prevents flute spots from being on any adjacent screen.", + "Balanced: New flute spots will be generated but prevents flute", + " spots from being on any adjacent screen.", "Random: New flute spots will be generated with minimal bias." ], "door_shuffle": [ @@ -304,7 +307,7 @@ "and a few other little things make this more like Zelda-1. (default: %(default)s)" ], "pseudoboots": [ " Players starts with pseudo boots that allow dashing but no item checks (default: %(default)s"], - "bomblogic": ["Start with 0 bomb capacity. Two capacity upgrades (+10) are added to the pool (default: %(default)s)" ], + "bombbag": ["Start with 0 bomb capacity. Two capacity upgrades (+10) are added to the pool (default: %(default)s)" ], "startinventory": [ "Specifies a list of items that will be in your starting inventory (separated by commas). (default: %(default)s)" ], "usestartinventory": [ "Toggle usage of Starting Inventory." ], "custom": [ "Not supported." ], @@ -337,6 +340,7 @@ "sprite that will be extracted." ], "reduce_flashing": [ "Reduce some in-game flashing (default: %(default)s)" ], + "shuffle_sfx": [ "Shuffle sounds effects (default: %(default)s)" ], "create_rom": [ "Create an output rom file. (default: %(default)s)" ], "gui": [ "Launch the GUI. (default: %(default)s)" ], "jsonout": [ diff --git a/resources/app/gui/adjust/overview/widgets.json b/resources/app/gui/adjust/overview/widgets.json index b61fff0e..34560c99 100644 --- a/resources/app/gui/adjust/overview/widgets.json +++ b/resources/app/gui/adjust/overview/widgets.json @@ -2,7 +2,8 @@ "checkboxes": { "nobgm": { "type": "checkbox" }, "quickswap": { "type": "checkbox" }, - "reduce_flashing": {"type": "checkbox"} + "reduce_flashing": {"type": "checkbox" }, + "shuffle_sfx": {"type": "checkbox" } }, "leftAdjustFrame": { "heartcolor": { diff --git a/resources/app/gui/lang/en.json b/resources/app/gui/lang/en.json index ab0f0d45..bfcea394 100644 --- a/resources/app/gui/lang/en.json +++ b/resources/app/gui/lang/en.json @@ -3,6 +3,7 @@ "adjust.nobgm": "Disable Music & MSU-1", "adjust.quickswap": "L/R Quickswapping", "adjust.reduce_flashing": "Reduce Flashing", + "adjust.shuffle_sfx": "Shuffle Sound Effects", "adjust.heartcolor": "Heart Color", "adjust.heartcolor.red": "Red", @@ -116,12 +117,14 @@ "randomizer.overworld.overworldshuffle.parallel": "Parallel", "randomizer.overworld.overworldshuffle.full": "Full", - "randomizer.overworld.overworldswap": "Tile Swap", - "randomizer.overworld.overworldswap.vanilla": "Vanilla", - "randomizer.overworld.overworldswap.mixed": "Mixed", - "randomizer.overworld.overworldswap.crossed": "Crossed", - + "randomizer.overworld.crossed": "Crossed", + "randomizer.overworld.crossed.none": "None", + "randomizer.overworld.crossed.polar": "Polar", + "randomizer.overworld.crossed.grouped": "Grouped", + "randomizer.overworld.crossed.limited": "Limited", + "randomizer.overworld.crossed.chaos": "Chaos", "randomizer.overworld.keepsimilar": "Keep Similar Edges Together", + "randomizer.overworld.mixed": "Tile Swap (Mixed)", "randomizer.overworld.overworldflute": "Flute Shuffle", "randomizer.overworld.overworldflute.vanilla": "Vanilla", @@ -151,6 +154,7 @@ "randomizer.gameoptions.nobgm": "Disable Music & MSU-1", "randomizer.gameoptions.quickswap": "L/R Quickswapping", "randomizer.gameoptions.reduce_flashing": "Reduce Flashing", + "randomizer.gameoptions.shuffle_sfx": "Shuffle Sound Effects", "randomizer.gameoptions.heartcolor": "Heart Color", "randomizer.gameoptions.heartcolor.red": "Red", @@ -207,7 +211,7 @@ "randomizer.item.hints": "Include Helpful Hints", "randomizer.item.retro": "Retro mode (universal keys)", "randomizer.item.pseudoboots": "Start with Pseudo Boots", - "randomizer.item.bomblogic": "Bomblogic", + "randomizer.item.bombbag": "Bombbag", "randomizer.item.worldstate": "World State", "randomizer.item.worldstate.standard": "Standard", diff --git a/resources/app/gui/randomize/entrando/widgets.json b/resources/app/gui/randomize/entrando/widgets.json index 7701936b..ffeeb976 100644 --- a/resources/app/gui/randomize/entrando/widgets.json +++ b/resources/app/gui/randomize/entrando/widgets.json @@ -12,10 +12,6 @@ "full", "crossed", "insanity", - "restricted_legacy", - "full_legacy", - "madness_legacy", - "insanity_legacy", "dungeonsfull", "dungeonssimple" ] diff --git a/resources/app/gui/randomize/gameoptions/widgets.json b/resources/app/gui/randomize/gameoptions/widgets.json index bc9714e5..cca75d00 100644 --- a/resources/app/gui/randomize/gameoptions/widgets.json +++ b/resources/app/gui/randomize/gameoptions/widgets.json @@ -2,7 +2,8 @@ "checkboxes": { "nobgm": { "type": "checkbox" }, "quickswap": { "type": "checkbox" }, - "reduce_flashing": { "type": "checkbox" } + "reduce_flashing": { "type": "checkbox" }, + "shuffle_sfx": { "type": "checkbox" } }, "leftRomOptionsFrame": { "heartcolor": { diff --git a/resources/app/gui/randomize/item/widgets.json b/resources/app/gui/randomize/item/widgets.json index c4dc9f05..bed86116 100644 --- a/resources/app/gui/randomize/item/widgets.json +++ b/resources/app/gui/randomize/item/widgets.json @@ -1,7 +1,7 @@ { "checkboxes": { "retro": { "type": "checkbox" }, - "bomblogic": { "type": "checkbox" }, + "bombbag": { "type": "checkbox" }, "shopsanity": { "type": "checkbox" }, "hints": { "type": "checkbox" diff --git a/resources/app/gui/randomize/overworld/widgets.json b/resources/app/gui/randomize/overworld/widgets.json index ff63a057..faf4a100 100644 --- a/resources/app/gui/randomize/overworld/widgets.json +++ b/resources/app/gui/randomize/overworld/widgets.json @@ -9,15 +9,21 @@ "full" ] }, - "overworldswap": { + "crossed": { "type": "selectbox", - "default": "mixed", + "default": "vanilla", "options": [ - "vanilla", - "mixed", - "crossed" + "none", + "polar", + "grouped", + "limited", + "chaos" ] }, + "mixed": { + "type": "checkbox", + "default": true + }, "overworldflute": { "type": "selectbox", "default": "vanilla", diff --git a/source/classes/SFX.py b/source/classes/SFX.py new file mode 100644 index 00000000..d5de414d --- /dev/null +++ b/source/classes/SFX.py @@ -0,0 +1,183 @@ +import random +from Utils import int16_as_bytes + + +class SFX(object): + + def __init__(self, name, sfx_set, orig_id, addr, chain, accomp=False): + self.name = name + self.sfx_set = sfx_set + self.orig_id = orig_id + self.addr = addr + self.chain = chain + self.accomp = accomp + + self.target_set = None + self.target_id = None + self.target_chain = None + + +def init_sfx_data(): + sfx_pool = [SFX('Slash1', 0x02, 0x01, 0x2614, []), SFX('Slash2', 0x02, 0x02, 0x2625, []), + SFX('Slash3', 0x02, 0x03, 0x2634, []), SFX('Slash4', 0x02, 0x04, 0x2643, []), + SFX('Wall clink', 0x02, 0x05, 0x25DD, []), SFX('Bombable door clink', 0x02, 0x06, 0x25D7, []), + SFX('Fwoosh shooting', 0x02, 0x07, 0x25B7, []), SFX('Arrow hitting wall', 0x02, 0x08, 0x25E3, []), + SFX('Boomerang whooshing', 0x02, 0x09, 0x25AD, []), SFX('Hookshot', 0x02, 0x0A, 0x25C7, []), + SFX('Placing bomb', 0x02, 0x0B, 0x2478, []), + SFX('Bomb exploding/Quake/Bombos/Exploding wall', 0x02, 0x0C, 0x269C, []), + SFX('Powder', 0x02, 0x0D, 0x2414, [0x3f]), SFX('Fire rod shot', 0x02, 0x0E, 0x2404, []), + SFX('Ice rod shot', 0x02, 0x0F, 0x24C3, []), SFX('Hammer use', 0x02, 0x10, 0x23FA, []), + SFX('Hammering peg', 0x02, 0x11, 0x23F0, []), SFX('Digging', 0x02, 0x12, 0x23CD, []), + SFX('Flute use', 0x02, 0x13, 0x23A0, [0x3e]), SFX('Cape on', 0x02, 0x14, 0x2380, []), + SFX('Cape off/Wallmaster grab', 0x02, 0x15, 0x2390, []), SFX('Staircase', 0x02, 0x16, 0x232C, []), + SFX('Staircase', 0x02, 0x17, 0x2344, []), SFX('Staircase', 0x02, 0x18, 0x2356, []), + SFX('Staircase', 0x02, 0x19, 0x236E, []), SFX('Tall grass/Hammer hitting bush', 0x02, 0x1A, 0x2316, []), + SFX('Mire shallow water', 0x02, 0x1B, 0x2307, []), SFX('Shallow water', 0x02, 0x1C, 0x2301, []), + SFX('Lifting object', 0x02, 0x1D, 0x22BB, []), SFX('Cutting grass', 0x02, 0x1E, 0x2577, []), + SFX('Item breaking', 0x02, 0x1F, 0x22E9, []), SFX('Item falling in pit', 0x02, 0x20, 0x22DA, []), + SFX('Bomb hitting ground/General bang', 0x02, 0x21, 0x22CF, []), + SFX('Pushing object/Armos bounce', 0x02, 0x22, 0x2107, []), SFX('Boots dust', 0x02, 0x23, 0x22B1, []), + SFX('Splashing', 0x02, 0x24, 0x22A5, [0x3d]), SFX('Mire shallow water again?', 0x02, 0x25, 0x2296, []), + SFX('Link taking damage', 0x02, 0x26, 0x2844, []), SFX('Fainting', 0x02, 0x27, 0x2252, []), + SFX('Item splash', 0x02, 0x28, 0x2287, []), SFX('Rupee refill', 0x02, 0x29, 0x243F, [0x3b]), + SFX('Fire rod shot hitting wall/Bombos spell', 0x02, 0x2A, 0x2033, []), + SFX('Heart beep/Text box', 0x02, 0x2B, 0x1FF2, []), SFX('Sword up', 0x02, 0x2C, 0x1FD9, [0x3a]), + SFX('Magic drain', 0x02, 0x2D, 0x20A6, []), SFX('GT opening', 0x02, 0x2E, 0x1FCA, [0x39]), + SFX('GT opening/Water drain', 0x02, 0x2F, 0x1F47, [0x38]), SFX('Cucco', 0x02, 0x30, 0x1EF1, []), + SFX('Fairy', 0x02, 0x31, 0x20CE, []), SFX('Bug net', 0x02, 0x32, 0x1D47, []), + SFX('Teleport2', 0x02, 0x33, 0x1CDC, [], True), SFX('Teleport1', 0x02, 0x34, 0x1F6F, [0x33]), + SFX('Quake/Vitreous/Zora king/Armos/Pyramid/Lanmo', 0x02, 0x35, 0x1C67, [0x36]), + SFX('Mire entrance (extends above)', 0x02, 0x36, 0x1C64, [], True), + SFX('Spin charged', 0x02, 0x37, 0x1A43, []), SFX('Water sound', 0x02, 0x38, 0x1F6F, [], True), + SFX('GT opening thunder', 0x02, 0x39, 0x1F9C, [], True), SFX('Sword up', 0x02, 0x3A, 0x1FE7, [], True), + SFX('Quiet rupees', 0x02, 0x3B, 0x2462, [], True), SFX('Error beep', 0x02, 0x3C, 0x1A37, []), + SFX('Big splash', 0x02, 0x3D, 0x22AB, [], True), SFX('Flute again', 0x02, 0x3E, 0x23B5, [], True), + SFX('Powder paired', 0x02, 0x3F, 0x2435, [], True), + + SFX('Sword beam', 0x03, 0x01, 0x1A18, []), + SFX('TR opening', 0x03, 0x02, 0x254E, []), SFX('Pyramid hole', 0x03, 0x03, 0x224A, []), + SFX('Angry soldier', 0x03, 0x04, 0x220E, []), SFX('Lynel shot/Javelin toss', 0x03, 0x05, 0x25B7, []), + SFX('BNC swing/Phantom ganon/Helma tail/Arrghus swoosh', 0x03, 0x06, 0x21F5, []), + SFX('Cannon fire', 0x03, 0x07, 0x223D, []), SFX('Damage to enemy; $0BEX.4=1', 0x03, 0x08, 0x21E6, []), + SFX('Enemy death', 0x03, 0x09, 0x21C1, []), SFX('Collecting rupee', 0x03, 0x0A, 0x21A9, []), + SFX('Collecting heart', 0x03, 0x0B, 0x2198, []), + SFX('Non-blank text character', 0x03, 0x0C, 0x218E, []), + SFX('HUD heart (used explicitly by sanc heart?)', 0x03, 0x0D, 0x21B5, []), + SFX('Opening chest', 0x03, 0x0E, 0x2182, []), + SFX('♪Do do do doooooo♫', 0x03, 0x0F, 0x24B9, [0x3C, 0x3D, 0x3E, 0x3F]), + SFX('Opening/Closing map (paired)', 0x03, 0x10, 0x216D, [0x3b]), + SFX('Opening item menu/Bomb shop guy breathing', 0x03, 0x11, 0x214F, []), + SFX('Closing item menu/Bomb shop guy breathing', 0x03, 0x12, 0x215E, []), + SFX('Throwing object (sprites use it as well)/Stalfos jump', 0x03, 0x13, 0x213B, []), + SFX('Key door/Trinecks/Dash key landing/Stalfos Knight collapse', 0x03, 0x14, 0x246C, []), + SFX('Door closing/OW door opening/Chest opening (w/ $29 in $012E)', 0x03, 0x15, 0x212F, []), + SFX('Armos Knight thud', 0x03, 0x16, 0x2123, []), SFX('Rat squeak', 0x03, 0x17, 0x25A6, []), + SFX('Dragging/Mantle moving', 0x03, 0x18, 0x20DD, []), + SFX('Fireball/Laser shot; Somehow used by Trinexx???', 0x03, 0x19, 0x250A, []), + SFX('Chest reveal jingle ', 0x03, 0x1A, 0x1E8A, [0x38]), + SFX('Puzzle jingle', 0x03, 0x1B, 0x20B6, [0x3a]), SFX('Damage to enemy', 0x03, 0x1C, 0x1A62, []), + SFX('Potion refill/Magic drain', 0x03, 0x1D, 0x20A6, []), + SFX('Flapping (Duck/Cucco swarm/Ganon bats/Keese/Raven/Vulture)', 0x03, 0x1E, 0x2091, []), + SFX('Link falling', 0x03, 0x1F, 0x204B, []), SFX('Menu/Text cursor moved', 0x03, 0x20, 0x276C, []), + SFX('Damage to boss', 0x03, 0x21, 0x27E2, []), SFX('Boss dying/Deleting file', 0x03, 0x22, 0x26CF, []), + SFX('Spin attack/Medallion swoosh', 0x03, 0x23, 0x2001, [0x39]), + SFX('OW map perspective change', 0x03, 0x24, 0x2043, []), + SFX('Pressure switch', 0x03, 0x25, 0x1E9D, []), + SFX('Lightning/Game over/Laser/Ganon bat/Trinexx lunge', 0x03, 0x26, 0x1E7B, []), + SFX('Agahnim charge', 0x03, 0x27, 0x1E40, []), SFX('Agahnim/Ganon teleport', 0x03, 0x28, 0x26F7, []), + SFX('Agahnim shot', 0x03, 0x29, 0x1E21, []), + SFX('Somaria/Byrna/Ether spell/Helma fire ball', 0x03, 0x2A, 0x1E12, []), + SFX('Electrocution', 0x03, 0x2B, 0x1DF3, []), SFX('Bees', 0x03, 0x2C, 0x1DC0, []), + SFX('Milestone, also via text', 0x03, 0x2D, 0x1DA9, [0x37]), + SFX('Collecting heart container', 0x03, 0x2E, 0x1D5D, [0x35, 0x34]), + SFX('Collecting absorbable key', 0x03, 0x2F, 0x1D80, [0x33]), + SFX('Byrna spark/Item plop/Magic bat zap/Blob emerge', 0x03, 0x30, 0x1B53, []), + SFX('Sprite falling/Moldorm shuffle', 0x03, 0x31, 0x1ACA, []), + SFX('Bumper boing/Somaria punt/Blob transmutation/Sprite boings', 0x03, 0x32, 0x1A78, []), + SFX('Jingle (paired $2F→$33)', 0x03, 0x33, 0x1D93, [], True), + SFX('Depressing jingle (paired $2E→$35→$34)', 0x03, 0x34, 0x1D66, [], True), + SFX('Ugly jingle (paired $2E→$35→$34)', 0x03, 0x35, 0x1D73, [], True), + SFX('Wizzrobe shot/Helma fireball split/Mothula beam/Blue balls', 0x03, 0x36, 0x1AA7, []), + SFX('Dinky jingle (paired $2D→$37)', 0x03, 0x37, 0x1DB4, [], True), + SFX('Apathetic jingle (paired $1A→$38)', 0x03, 0x38, 0x1E93, [], True), + SFX('Quiet swish (paired $23→$39)', 0x03, 0x39, 0x2017, [], True), + SFX('Defective jingle (paired $1B→$3A)', 0x03, 0x3A, 0x20C0, [], True), + SFX('Petulant jingle (paired $10→$3B)', 0x03, 0x3B, 0x2176, [], True), + SFX('Triumphant jingle (paired $0F→$3C→$3D→$3E→$3F)', 0x03, 0x3C, 0x248A, [], True), + SFX('Less triumphant jingle ($0F→$3C→$3D→$3E→$3F)', 0x03, 0x3D, 0x2494, [], True), + SFX('"You tried, I guess" jingle (paired $0F→$3C→$3D→$3E→$3F)', 0x03, 0x3E, 0x249E, [], True), + SFX('"You didn\'t really try" jingle (paired $0F→$3C→$3D→$3E→$3F)', 0x03, 0x3F, 0x2480, [], True)] + return sfx_pool + + +def shuffle_sfx_data(): + sfx_pool = init_sfx_data() + sfx_map = {2: {}, 3: {}} + accompaniment_map = {2: set(), 3: set()} + candidates = [] + for sfx in sfx_pool: + sfx_map[sfx.sfx_set][sfx.orig_id] = sfx + if not sfx.accomp: + candidates.append((sfx.sfx_set, sfx.orig_id)) + else: + accompaniment_map[sfx.sfx_set].add(sfx.orig_id) + chained_sfx = [x for x in sfx_pool if len(x.chain) > 0] + + random.shuffle(candidates) + + # place chained sfx first + random.shuffle(chained_sfx) # todo: sort largest to smallest + chained_sfx = sorted(chained_sfx, key=lambda x: len(x.chain), reverse=True) + for chained in chained_sfx: + chosen_slot = next(x for x in candidates if len(accompaniment_map[x[0]]) - len(chained.chain) >= 0) + if chosen_slot is None: + raise Exception('Something went wrong with sfx chains') + chosen_set, chosen_id = chosen_slot + chained.target_set, chained.target_id = chosen_slot + chained.target_chain = [] + for downstream in chained.chain: + next_slot = accompaniment_map[chosen_set].pop() + ds_acc = sfx_map[chained.sfx_set][downstream] + ds_acc.target_set, ds_acc.target_id = chosen_set, next_slot + chained.target_chain.append(next_slot) + candidates.remove(chosen_slot) + sfx_pool.remove(chained) + + unchained_sfx = [x for x in sfx_pool if not x.accomp] + # do the rest + for sfx in unchained_sfx: + chosen_slot = candidates.pop() + sfx.target_set, sfx.target_id = chosen_slot + + return sfx_map + + +sfx_table = { + 2: 0x1a8c29, + 3: 0x1A8D25 +} + +# 0x1a8c29 +# d8059 + +sfx_accompaniment_table = { + 2: 0x1A8CA7, + 3: 0x1A8DA3 +} + + +def randomize_sfx(rom): + sfx_map = shuffle_sfx_data() + + for shuffled_sfx in sfx_map.values(): + for sfx in shuffled_sfx.values(): + base_address = sfx_table[sfx.target_set] + rom.write_bytes(base_address + (sfx.target_id * 2) - 2, int16_as_bytes(sfx.addr)) + ac_base = sfx_accompaniment_table[sfx.target_set] + last = sfx.target_id + if sfx.target_chain: + for chained in sfx.target_chain: + rom.write_byte(ac_base + last - 1, chained) + last = chained + rom.write_byte(ac_base + last - 1, 0) + diff --git a/source/classes/constants.py b/source/classes/constants.py index c983d98a..1e2320b9 100644 --- a/source/classes/constants.py +++ b/source/classes/constants.py @@ -57,7 +57,7 @@ SETTINGSTOPROCESS = { "item": { "hints": "hints", "retro": "retro", - "bomblogic": "bomblogic", + "bombbag": "bombbag", "shopsanity": "shopsanity", "pseudoboots": "pseudoboots", "worldstate": "mode", @@ -77,8 +77,9 @@ SETTINGSTOPROCESS = { }, "overworld": { "overworldshuffle": "ow_shuffle", - "overworldswap": "ow_swap", + "crossed": "ow_crossed", "keepsimilar": "ow_keepsimilar", + "mixed": "ow_mixed", "overworldflute": "ow_fluteshuffle" }, "entrance": { @@ -105,7 +106,7 @@ SETTINGSTOPROCESS = { "experimental": "experimental", "dungeon_counters": "dungeon_counters", "mixed_travel": "mixed_travel", - "standardize_palettes": "standardize_palettes", + "standardize_palettes": "standardize_palettes" }, "gameoptions": { "nobgm": "disablemusic", @@ -115,7 +116,8 @@ SETTINGSTOPROCESS = { "menuspeed": "fastmenu", "owpalettes": "ow_palettes", "uwpalettes": "uw_palettes", - "reduce_flashing": "reduce_flashing" + "reduce_flashing": "reduce_flashing", + "shuffle_sfx": "shuffle_sfx" }, "generation": { "createspoiler": "create_spoiler", diff --git a/source/gui/adjust/overview.py b/source/gui/adjust/overview.py index 55d72e8d..7e97b2b4 100644 --- a/source/gui/adjust/overview.py +++ b/source/gui/adjust/overview.py @@ -102,7 +102,8 @@ def adjust_page(top, parent, settings): "uwpalettes": "uw_palettes", "quickswap": "quickswap", "nobgm": "disablemusic", - "reduce_flashing": "reduce_flashing" + "reduce_flashing": "reduce_flashing", + "shuffle_sfx": "shuffle_sfx" } guiargs = Namespace() for option in options: diff --git a/source/gui/randomize/overworld.py b/source/gui/randomize/overworld.py index eb910b9f..77be948a 100644 --- a/source/gui/randomize/overworld.py +++ b/source/gui/randomize/overworld.py @@ -1,4 +1,4 @@ -from tkinter import ttk, Frame, Label, W, E, NW, LEFT, RIGHT, X, TOP +from tkinter import ttk, Frame, Label, W, E, NW, LEFT, RIGHT, X, Y, TOP import source.gui.widgets as widgets import json import os @@ -16,12 +16,11 @@ def overworld_page(parent): # Load Overworld Shuffle option widgets as defined by JSON file # Defns include frame name, widget type, widget options, widget placement attributes # These get split left & right - self.frames["widgets"] = Frame(self) - self.frames["leftOverworldFrame"] = Frame(self.frames["widgets"]) - self.frames["rightOverworldFrame"] = Frame(self.frames["widgets"]) - self.frames["widgets"].pack(fill=X) - self.frames["leftOverworldFrame"].pack(side=LEFT) - self.frames["rightOverworldFrame"].pack(side=LEFT, anchor=NW) + self.frames["leftOverworldFrame"] = Frame(self) + self.frames["rightOverworldFrame"] = Frame(self) + + self.frames["leftOverworldFrame"].pack(side=LEFT, anchor=NW, fill=Y) + self.frames["rightOverworldFrame"].pack(anchor=NW, fill=Y) with open(os.path.join("resources","app","gui","randomize","overworld","widgets.json")) as overworldWidgets: myDict = json.load(overworldWidgets) @@ -29,9 +28,14 @@ def overworld_page(parent): dictWidgets = widgets.make_widgets_from_dict(self, theseWidgets, self.frames[framename]) for key in dictWidgets: self.widgets[key] = dictWidgets[key] - if key == "rightOverworldFrame": - self.widgets[key].pack(anchor=E) - else: - self.widgets[key].pack(anchor=E) + packAttrs = {"anchor":E} + if key == "keepsimilar": + packAttrs = {"side":LEFT, "pady":(18,0)} + elif key == "overworldflute": + packAttrs["pady"] = (20,0) + elif key == "mixed": + packAttrs = {"anchor":W, "padx":(79,0)} + + self.widgets[key].pack(packAttrs) return self