diff --git a/BaseClasses.py b/BaseClasses.py index 3615d0e9..6456c1c5 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -87,6 +87,7 @@ class World(object): self.owedges = [] self._owedge_cache = {} self.owswaps = {} + self.owcrossededges = {} self.owwhirlpools = {} self.owflutespots = {} self.owsectors = {} @@ -114,6 +115,7 @@ class World(object): set_player_attr('_region_cache', {}) set_player_attr('player_names', []) set_player_attr('owswaps', [[],[],[]]) + set_player_attr('owcrossededges', []) set_player_attr('owwhirlpools', []) set_player_attr('owsectors', None) set_player_attr('remote_items', False) @@ -321,6 +323,16 @@ class World(object): if isinstance(edgename, OWEdge): return edgename try: + if edgename[-1] == '*': + edgename = edgename[:-1] + edge = self.check_for_owedge(edgename, player) + if self.is_tile_swapped(edge.owIndex, player): + from OverworldShuffle import parallel_links + if edgename in parallel_links.keys() or edgename in parallel_links.inverse.keys(): + edgename = parallel_links[edgename] if edgename in parallel_links.keys() else parallel_links.inverse[edgename][0] + return self.check_for_owedge(edgename, player) + else: + raise Exception("Edge notated with * doesn't have a parallel edge: %s" & edgename) return self._owedge_cache[(edgename, player)] except KeyError: for edge in self.owedges: @@ -2248,7 +2260,7 @@ class OWEdge(object): self.unknownX = 0x0 self.unknownY = 0x0 - if self.owIndex < 0x40 or self.owIndex >= 0x80: + if self.owIndex & 0x40 == 0: self.worldType = WorldType.Light else: self.worldType = WorldType.Dark @@ -2289,6 +2301,12 @@ class OWEdge(object): self.specialID = special_id return self + def is_tile_swapped(self, world): + return world.is_tile_swapped(self.owIndex, self.player) + + def is_lw(self, world): + return (self.worldType == WorldType.Light) != self.is_tile_swapped(world) + def __eq__(self, other): return isinstance(other, self.__class__) and self.name == other.name diff --git a/OverworldShuffle.py b/OverworldShuffle.py index 788c9d2e..d18356b5 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -205,20 +205,40 @@ def link_overworld(world, player): # crossed shuffle logging.getLogger('').debug('Crossing overworld edges') - crossed_edges = list() - #TODO: Revisit with changes to Limited/Allowed - if world.owCrossed[player] not in ['none', 'grouped', 'polar', 'chaos']: + # customizer setup + force_crossed = set() + force_noncrossed = set() + count_crossed = 0 + limited_crossed = 9 if world.owCrossed[player] == 'limited' else -1 + if world.customizer: + custom_crossed = world.customizer.get_owcrossed() + if custom_crossed and player in custom_crossed: + custom_crossed = custom_crossed[player] + if 'force_crossed' in custom_crossed and len(custom_crossed['force_crossed']) > 0: + for edgename in custom_crossed['force_crossed']: + edge = world.check_for_owedge(edgename, player) + force_crossed.add(edge.name) + if 'force_noncrossed' in custom_crossed and len(custom_crossed['force_noncrossed']) > 0: + for edgename in custom_crossed['force_noncrossed']: + edge = world.check_for_owedge(edgename, player) + force_noncrossed.add(edge.name) + if 'limit_crossed' in custom_crossed: + limited_crossed = custom_crossed['limit_crossed'] + + if limited_crossed > -1: + # connect forced crossed non-parallel edges based on previously determined tile flips for edge in swapped_edges: - crossed_edges.append(edge) - - if world.owCrossed[player] in ['grouped', 'limited'] or (world.owShuffle[player] == 'vanilla' and world.owCrossed[player] == 'chaos'): if edge not in parallel_links_new: + world.owcrossededges[player].append(edge) + count_crossed = count_crossed + 1 + + if world.owCrossed[player] == 'grouped' or (world.owShuffle[player] == 'vanilla' and world.owCrossed[player] == 'chaos') or limited_crossed > -1: if world.owCrossed[player] == 'grouped': # the idea is to XOR the new flips with the ones from Mixed so that non-parallel edges still work # Polar corresponds to Grouped with no flips in ow_crossed_tiles_mask ow_crossed_tiles_mask = [[],[],[]] - crossed_edges = shuffle_tiles(world, define_tile_groups(world, player, True), ow_crossed_tiles_mask, True, player) + world.owcrossededges[player] = shuffle_tiles(world, define_tile_groups(world, player, True), ow_crossed_tiles_mask, True, player) ow_crossed_tiles = [i for i in range(0x82) if (i in world.owswaps[player][0]) != (i in ow_crossed_tiles_mask[0])] # update spoiler @@ -244,32 +264,77 @@ def link_overworld(world, player): (mode, wrld, dir, terrain, parallel, count) = group if wrld == WorldType.Light and mode != OpenStd.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) if forward_set[0] in parallel_links_new: + forward_parallel = [parallel_links_new[e] for e in forward_set] + back_parallel = [parallel_links_new[e] for e in back_set] + forward_combine = forward_set+forward_parallel + back_combine = back_set+back_parallel + combine_set = forward_combine+back_combine + + skip_forward = False + if world.owShuffle[player] == 'vanilla': + if any(edge in force_crossed for edge in combine_set): + if not any(edge in force_noncrossed for edge in combine_set): + if any(edge in force_crossed for edge in forward_combine): + world.owcrossededges[player].extend(forward_set) + count_crossed = count_crossed + 1 + continue + else: + world.owcrossededges[player].extend(back_set) + count_crossed = count_crossed + 1 + continue + else: + raise GenerationException('Conflict detected in force_crossed and force_noncrossed') + if any(edge in list(force_noncrossed)+world.owcrossededges[player] for edge in combine_set): + continue 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': + skip_back = False + if any(edge in force_crossed for edge in forward_combine): + if not any(edge in force_noncrossed for edge in forward_combine): + world.owcrossededges[player].extend(forward_set) + count_crossed = count_crossed + 1 + skip_forward = True + else: + raise GenerationException('Conflict detected in force_crossed and force_noncrossed') + if any(edge in force_crossed for edge in back_combine): + if not any(edge in force_noncrossed for edge in back_combine): + world.owcrossededges[player].extend(back_set) + count_crossed = count_crossed + 1 + skip_back = True + else: + raise GenerationException('Conflict detected in force_crossed and force_noncrossed') + if any(edge in list(force_noncrossed)+world.owcrossededges[player] for edge in forward_combine): + skip_forward = True + if any(edge in list(force_noncrossed)+world.owcrossededges[player] for edge in back_combine): + skip_back = True + if not skip_back: + if limited_crossed > -1: + crossed_candidates.append(back_set) + elif random.randint(0, 1): + world.owcrossededges[player].extend(back_set) + count_crossed = count_crossed + 1 + if not skip_forward: + if limited_crossed > -1: + crossed_candidates.append(forward_set) + elif random.randint(0, 1): + world.owcrossededges[player].extend(forward_set) + count_crossed = count_crossed + 1 + assert len(world.owcrossededges[player]) == len(set(world.owcrossededges[player])), "Same edge added to crossed edges" + + if limited_crossed > -1: + limit = limited_crossed - count_crossed 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): + for edge_set in crossed_candidates[:limit]: + world.owcrossededges[player].extend(edge_set) + assert len(world.owcrossededges[player]) == len(set(world.owcrossededges[player])), "Same edge candidate added to crossed edges" + + for edge in copy.deepcopy(world.owcrossededges[player]): if edge in parallel_links_new: - crossed_edges.append(parallel_links_new[edge]) - elif edge in parallel_links_new.inverse: - crossed_edges.append(parallel_links_new.inverse[edge][0]) + if parallel_links_new[edge] not in world.owcrossededges[player]: + world.owcrossededges[player].append(parallel_links_new[edge]) # after tile flip and crossed, determine edges that need to flip - edges_to_swap = [e for e in swapped_edges+crossed_edges if (e not in swapped_edges) or (e not in crossed_edges)] + edges_to_swap = [e for e in swapped_edges+world.owcrossededges[player] if (e not in swapped_edges) or (e not in world.owcrossededges[player])] # whirlpool shuffle logging.getLogger('').debug('Shuffling whirlpools') @@ -367,53 +432,142 @@ def link_overworld(world, player): # layout shuffle groups = adjust_edge_groups(world, trimmed_groups, edges_to_swap, player) - connect_custom(world, connected_edges, groups, player) + connect_custom(world, connected_edges, groups, (force_crossed, force_noncrossed), player) tries = 100 valid_layout = False connected_edge_cache = connected_edges.copy() + groups_cache = copy.deepcopy(groups) while not valid_layout and tries > 0: + def connect_set(forward_set, back_set, connected_edges): + 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]) + connected_edges = connected_edge_cache.copy() + groups = copy.deepcopy(groups_cache) + groupKeys = list(groups.keys()) if world.mode[player] == 'standard': - random.shuffle(groups[2:]) # keep first 2 groups (Standard) first + random.shuffle(groupKeys[2:]) # keep first 2 groups (Standard) first else: - random.shuffle(groups) + random.shuffle(groupKeys) - for (forward_edge_sets, back_edge_sets) in groups: - assert len(forward_edge_sets) == len(back_edge_sets) + for key in groupKeys: + (mode, wrld, dir, terrain, parallel, count) = key + (forward_edge_sets, back_edge_sets) = groups[key] + def remove_connected(): + s = 0 + while s < len(forward_edge_sets): + forward_set = forward_edge_sets[s] + if forward_set[0] in connected_edges: + del forward_edge_sets[s] + continue + s += 1 + s = 0 + while s < len(back_edge_sets): + back_set = back_edge_sets[s] + if back_set[0] in connected_edges: + del back_edge_sets[s] + continue + s += 1 + assert len(forward_edge_sets) == len(back_edge_sets) + + remove_connected() random.shuffle(forward_edge_sets) random.shuffle(back_edge_sets) - 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] + if wrld is None and len(force_crossed) + len(force_noncrossed) > 0: + # divide forward/back sets into LW/DW + forward_lw_sets, forward_dw_sets = [], [] + back_lw_sets, back_dw_sets = [], [] + forward_parallel_lw_sets, forward_parallel_dw_sets = [], [] + back_parallel_lw_sets, back_parallel_dw_sets = [], [] + + for edge_set in forward_edge_sets: + if world.check_for_owedge(edge_set[0], player).is_lw(world): + forward_lw_sets.append(edge_set) + if parallel == IsParallel.Yes: + forward_parallel_lw_sets.append([parallel_links_new[e] for e in edge_set]) + else: + forward_dw_sets.append(edge_set) + if parallel == IsParallel.Yes: + forward_parallel_dw_sets.append([parallel_links_new[e] for e in edge_set]) + for edge_set in back_edge_sets: + if world.check_for_owedge(edge_set[0], player).is_lw(world): + back_lw_sets.append(edge_set) + if parallel == IsParallel.Yes: + back_parallel_lw_sets.append([parallel_links_new[e] for e in edge_set]) + else: + back_dw_sets.append(edge_set) + if parallel == IsParallel.Yes: + back_parallel_dw_sets.append([parallel_links_new[e] for e in edge_set]) + + crossed_sets = [] + noncrossed_sets = [] + def add_to_crossed_sets(sets, parallel_sets): + for i in range(0, len(sets)): + affected_edges = set(sets[i]+(parallel_sets[i] if parallel == IsParallel.Yes else [])) + if sets[i] not in crossed_sets and len(set.intersection(set(force_crossed), affected_edges)) > 0: + crossed_sets.append(sets[i]) + if sets not in noncrossed_sets and len(set.intersection(set(force_noncrossed), affected_edges)) > 0: + noncrossed_sets.append(sets[i]) + if sets[i] in crossed_sets and sets[i] in noncrossed_sets: + raise GenerationException('Conflict in force crossed/non-crossed definition') + add_to_crossed_sets(forward_lw_sets, forward_parallel_lw_sets) + add_to_crossed_sets(forward_dw_sets, forward_parallel_dw_sets) + add_to_crossed_sets(back_lw_sets, back_parallel_lw_sets) + add_to_crossed_sets(back_dw_sets, back_parallel_dw_sets) + + # random connect forced crossed/noncrossed + c = 0 + while c < len(noncrossed_sets): + if noncrossed_sets[c] in forward_edge_sets: + forward_set = noncrossed_sets[c] + if forward_set in forward_lw_sets: + back_set = next(s for s in back_lw_sets if s in back_edge_sets and s not in crossed_sets) 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] + back_set = next(s for s in back_dw_sets if s in back_edge_sets and s not in crossed_sets) + elif noncrossed_sets[c] in back_edge_sets: + back_set = noncrossed_sets[c] + if back_set in back_lw_sets: + forward_set = next(s for s in forward_lw_sets if s in forward_edge_sets and s not in crossed_sets) 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]) + forward_set = next(s for s in forward_dw_sets if s in forward_edge_sets and s not in crossed_sets) + else: + c = c + 1 + continue + connect_set(forward_set, back_set, connected_edges) + remove_connected() + c = c + 1 + c = 0 + while c < len(crossed_sets): + if crossed_sets[c] in forward_edge_sets: + forward_set = crossed_sets[c] + if forward_set in forward_lw_sets: + back_set = next(s for s in back_dw_sets if s in back_edge_sets) + else: + back_set = next(s for s in back_lw_sets if s in back_edge_sets) + elif crossed_sets[c] in back_edge_sets: + back_set = crossed_sets[c] + if back_set in back_lw_sets: + forward_set = next(s for s in forward_dw_sets if s in forward_edge_sets) + else: + forward_set = next(s for s in forward_lw_sets if s in forward_edge_sets) + else: + c = c + 1 + continue + connect_set(forward_set, back_set, connected_edges) + remove_connected() + c = c + 1 + + while len(forward_edge_sets) > 0 and len(back_edge_sets) > 0: + connect_set(forward_edge_sets[0], back_edge_sets[0], connected_edges) + remove_connected() assert len(connected_edges) == len(default_connections) * 2, connected_edges world.owsectors[player] = build_sectors(world, player) @@ -583,8 +737,9 @@ def link_overworld(world, player): s[0x3a],s[0x3b],s[0x3c], s[0x3f]) world.spoiler.set_map('flute', text_output, new_spots, player) -def connect_custom(world, connected_edges, groups, player): - def remove_pair_from_pool(edgename1, edgename2): +def connect_custom(world, connected_edges, groups, forced, player): + forced_crossed, forced_noncrossed = forced + def remove_pair_from_pool(edgename1, edgename2, is_crossed): def add_to_unresolved(forward_set, back_set): if len(forward_set) > 1: if edgename1 in forward_set: @@ -593,8 +748,8 @@ def connect_custom(world, connected_edges, groups, player): else: back_set.remove(edgename1) forward_set.remove(edgename2) - unresolved_similars.append(tuple((forward_set, back_set))) - for forward_pool, back_pool in groups: + unresolved_similars.append(tuple((forward_set, back_set, is_crossed))) + for forward_pool, back_pool in groups.values(): if not len(forward_pool): continue if len(forward_pool[0]) == 1: @@ -635,7 +790,7 @@ def connect_custom(world, connected_edges, groups, player): else: break for pair in unresolved_similars: - forward_set, back_set = pair + forward_set, back_set, _ = pair if edgename1 in forward_set: if edgename2 in back_set: unresolved_similars.remove(pair) @@ -659,24 +814,45 @@ def connect_custom(world, connected_edges, groups, player): custom_edges = custom_edges[player] if 'two-way' in custom_edges: unresolved_similars = [] + def validate_crossed_allowed(edge1, edge2, is_crossed): + return not ((not is_crossed and (edge1 in forced_crossed or edge2 in forced_crossed)) + or (is_crossed and (edge1 in forced_noncrossed or edge2 in forced_noncrossed))) for edgename1, edgename2 in custom_edges['two-way'].items(): edge1 = world.check_for_owedge(edgename1, player) edge2 = world.check_for_owedge(edgename2, player) - if edgename1 not in connected_edges and edgename2 not in connected_edges: + is_crossed = edge1.is_lw(world) != edge2.is_lw(world) + if not validate_crossed_allowed(edge1.name, edge2.name, is_crossed): + if edgename2[-1] == '*': + edge2 = world.check_for_owedge(edge2.name + '*', player) + is_crossed = not is_crossed + else: + raise GenerationException('Violation of force crossed rules: \'%s\' <-> \'%s\'', edgename1, edgename2) + if edge1.name not in connected_edges and edge2.name not in connected_edges: # attempt connection - remove_pair_from_pool(edgename1, edgename2) - connect_two_way(world, edgename1, edgename2, player, connected_edges) + remove_pair_from_pool(edge1.name, edge2.name, is_crossed) + connect_two_way(world, edge1.name, edge2.name, player, connected_edges) # resolve parallel - remove_pair_from_pool(parallel_forward_edge, parallel_back_edge) - elif not edge1.dest or not edge2.dest or edge1.dest.name != edgename2 or edge2.dest.name != edgename1: if world.owShuffle[player] == 'parallel' and edge1.name in parallel_links_new: parallel_forward_edge = parallel_links_new[edge1.name] parallel_back_edge = parallel_links_new[edge2.name] + if validate_crossed_allowed(parallel_forward_edge, parallel_back_edge, is_crossed): + remove_pair_from_pool(parallel_forward_edge, parallel_back_edge, is_crossed) + else: + raise GenerationException('Violation of force crossed rules on parallel connection: \'%s\' <-> \'%s\'', edgename1, edgename2) + elif not edge1.dest or not edge2.dest or edge1.dest.name != edge2.name or edge2.dest.name != edge1.name: raise GenerationException('OW Edge already connected: \'%s\' <-> \'%s\'', edgename1, edgename2) # connect leftover similars - for forward_pool, back_pool in unresolved_similars: + for forward_pool, back_pool, is_crossed in unresolved_similars: for (forward_edge, back_edge) in zip(forward_pool, back_pool): - connect_two_way(world, forward_edge, back_edge, player, connected_edges) + if validate_crossed_allowed(forward_edge, back_edge, is_crossed): + connect_two_way(world, forward_edge, back_edge, player, connected_edges) + else: + raise GenerationException('Violation of force crossed rules on unresolved similars: \'%s\' <-> \'%s\'', forward_edge, back_edge) + if world.owShuffle[player] == 'parallel' and forward_edge in parallel_links_new: + parallel_forward_edge = parallel_links_new[forward_edge] + parallel_back_edge = parallel_links_new[back_edge] + if not validate_crossed_allowed(parallel_forward_edge, parallel_back_edge, is_crossed): + raise GenerationException('Violation of force crossed rules on parallel unresolved similars: \'%s\' <-> \'%s\'', forward_edge, back_edge) def connect_two_way(world, edgename1, edgename2, player, connected_edges=None): edge1 = world.get_entrance(edgename1, player) @@ -1054,12 +1230,16 @@ def reorganize_groups(world, groups, player): def adjust_edge_groups(world, trimmed_groups, edges_to_swap, player): groups = defaultdict(lambda: ([],[])) + limited_crossed = False + if world.customizer: + custom_crossed = world.customizer.get_owcrossed() + limited_crossed = custom_crossed and (player in custom_crossed) and ('limit_crossed' in custom_crossed[player]) for (key, group) in trimmed_groups.items(): (mode, wrld, dir, terrain, parallel, count) = key if mode == OpenStd.Standard: groups[key] = group else: - if world.owCrossed[player] == 'chaos': + if world.owCrossed[player] == 'chaos' and not limited_crossed: groups[(mode, None, dir, terrain, parallel, count)][0].extend(group[0]) groups[(mode, None, dir, terrain, parallel, count)][1].extend(group[1]) else: @@ -1069,7 +1249,7 @@ def adjust_edge_groups(world, trimmed_groups, edges_to_swap, player): if edge_set[0] in edges_to_swap: new_world += 1 groups[(mode, WorldType(new_world % 2), dir, terrain, parallel, count)][i].append(edge_set) - return list(groups.values()) + return groups def create_flute_exits(world, player): flute_in_pool = True if player not in world.customitemarray else any(i for i, n in world.customitemarray[player].items() if i == 'flute' and n > 0) diff --git a/docs/customizer_example.yaml b/docs/customizer_example.yaml index ecf209b1..36bfbdde 100644 --- a/docs/customizer_example.yaml +++ b/docs/customizer_example.yaml @@ -69,6 +69,24 @@ placements: Palace of Darkness - Big Chest: Hammer Capacity Upgrade - Left: Moon Pearl Turtle Rock - Pokey 2 Key Drop: Ice Rod +ow-edges: + 1: + two-way: + Kakariko Fortune ES*: Sanctuary WN* + Central Bonk Rocks EC: Potion Shop WN + Central Bonk Rocks ES: Potion Shop WC +ow-crossed: + 1: + force_crossed: + - Links House ES* + - Kakariko Fortune ES* + force_noncrossed: + - Links House NE + limit_crossed: 9 # emulates Limited Crossed +ow-whirlpools: + 1: + two-way: + River Bend Whirlpool: Lake Hylia Whirlpool ow-tileflips: 1: force_flip: diff --git a/source/classes/CustomSettings.py b/source/classes/CustomSettings.py index 58c9494b..96330516 100644 --- a/source/classes/CustomSettings.py +++ b/source/classes/CustomSettings.py @@ -201,6 +201,11 @@ class CustomSettings(object): return self.file_source['ow-edges'] return None + def get_owcrossed(self): + if 'ow-crossed' in self.file_source: + return self.file_source['ow-crossed'] + return None + def get_whirlpools(self): if 'ow-whirlpools' in self.file_source: return self.file_source['ow-whirlpools']