diff --git a/.gitignore b/.gitignore index f731841f..9fefb70c 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,7 @@ weights/ /QUsb2Snes/ /output/ /enemizer/ +visualizations/ base2current.json diff --git a/BaseClasses.py b/BaseClasses.py index dcf7ff69..d2bd6ddf 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -20,11 +20,12 @@ from source.overworld.EntranceData import door_addresses class World(object): - def __init__(self, players, owShuffle, owCrossed, owMixed, shuffle, doorShuffle, logic, mode, swords, difficulty, difficulty_adjustments, + def __init__(self, players, owLayout, owParallel, owCrossed, owMixed, shuffle, doorShuffle, logic, mode, swords, difficulty, difficulty_adjustments, timer, progressive, goal, algorithm, accessibility, shuffle_ganon, custom, customitemarray, hints, spoiler_mode): self.players = players self.teams = 1 - self.owShuffle = owShuffle.copy() + self.owLayout = owLayout.copy() + self.owParallel = owParallel.copy() self.owTerrain = {} self.owKeepSimilar = {} self.owMixed = owMixed.copy() @@ -32,6 +33,7 @@ class World(object): self.owCrossed = self.owCrossed if self.owCrossed != 'polar' or self.owMixed else 'none' self.owWhirlpoolShuffle = {} self.owFluteShuffle = {} + self.owFog = {} self.shuffle = shuffle.copy() self.doorShuffle = doorShuffle.copy() self.intensity = {} @@ -86,6 +88,9 @@ class World(object): self.owswaps = {} self.owcrossededges = {} self.owwhirlpools = {} + self.owgrid = {} + self.owlayoutmap_lw = {} + self.owlayoutmap_dw = {} self.owflutespots = {} self.owsectors = {} self.allow_flip_sanc = {} @@ -118,6 +123,7 @@ class World(object): set_player_attr('owswaps', [[],[],[]]) set_player_attr('owcrossededges', []) set_player_attr('owwhirlpools', []) + set_player_attr('owgrid', None) set_player_attr('owsectors', None) set_player_attr('allow_flip_sanc', False) set_player_attr('remote_items', False) @@ -2344,12 +2350,11 @@ class OWEdge(object): def __init__(self, player, name, owIndex, direction, terrain, edge_id, owSlotIndex=0xff): self.player = player self.name = name - self.type = DoorType.Open self.direction = direction self.terrain = terrain + self.parallel = None self.specialEntrance = False self.specialExit = False - self.deadEnd = False # rom properties self.owIndex = owIndex @@ -2380,7 +2385,6 @@ class OWEdge(object): self.worldType = WorldType.Dark # logical properties - # self.connected = False # combine with Dest? self.dest = None self.dependents = [] self.dead = False @@ -2397,9 +2401,6 @@ class OWEdge(object): def getTarget(self): return self.dest.specialID if self.dest.specialExit else self.dest.edge_id - def dead_end(self): - self.deadEnd = True - def coordInfo(self, midpoint, vram_loc): self.midpoint = midpoint self.vramLoc = vram_loc @@ -3041,13 +3042,15 @@ class Spoiler(object): 'bow_mode': self.world.bow_mode, 'goal': self.world.goal, 'custom_goals': self.world.custom_goals, - 'ow_shuffle': self.world.owShuffle, + 'ow_layout': self.world.owLayout, + 'ow_parallel': self.world.owParallel, 'ow_terrain': self.world.owTerrain, 'ow_crossed': self.world.owCrossed, 'ow_keepsimilar': self.world.owKeepSimilar, 'ow_mixed': self.world.owMixed, 'ow_whirlpool': self.world.owWhirlpoolShuffle, 'ow_fluteshuffle': self.world.owFluteShuffle, + 'ow_fog': self.world.owFog, 'bonk_drops': self.world.shuffle_bonk_drops, 'shuffle_followers': self.world.shuffle_followers, 'shuffle': self.world.shuffle, @@ -3312,15 +3315,18 @@ class Spoiler(object): outfile.write('Enemy Drop Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['dropshuffle'][player]) outfile.write('Take Any Caves:'.ljust(line_width) + '%s\n' % self.metadata['take_any'][player]) outfile.write('\n') - outfile.write('Overworld Layout Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['ow_shuffle'][player]) - if self.metadata['ow_shuffle'][player] != 'vanilla': + outfile.write('Overworld Layout Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['ow_layout'][player]) + if self.metadata['ow_layout'][player] != 'vanilla': + outfile.write('Parallel OW:'.ljust(line_width) + '%s\n' % yn(self.metadata['ow_parallel'][player])) outfile.write('Free Terrain:'.ljust(line_width) + '%s\n' % yn(self.metadata['ow_terrain'][player])) outfile.write('Crossed OW:'.ljust(line_width) + '%s\n' % self.metadata['ow_crossed'][player]) - if self.metadata['ow_shuffle'][player] != 'vanilla' or self.metadata['ow_crossed'][player] != 'none': + if self.metadata['ow_layout'][player] != 'vanilla' or self.metadata['ow_crossed'][player] != 'none': outfile.write('Keep Similar OW Edges Together:'.ljust(line_width) + '%s\n' % yn(self.metadata['ow_keepsimilar'][player])) outfile.write('OW Tile Flip (Mixed):'.ljust(line_width) + '%s\n' % yn(self.metadata['ow_mixed'][player])) outfile.write('Whirlpool Shuffle:'.ljust(line_width) + '%s\n' % yn(self.metadata['ow_whirlpool'][player])) outfile.write('Flute Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['ow_fluteshuffle'][player]) + if self.metadata['ow_layout'][player] == 'grid' or self.metadata['ow_mixed'][player]: + outfile.write('Overworld Fog:'.ljust(line_width) + '%s\n' % yn(self.metadata['ow_fog'][player])) outfile.write('\n') outfile.write('Entrance Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['shuffle'][player]) if self.metadata['shuffle'][player] != 'vanilla': @@ -3431,41 +3437,32 @@ class Spoiler(object): outfile.write(f'{fairy}: {bottle}\n') if self.maps: + def write_map(type, title): + for player in range(1, self.world.players + 1): + if (type, player) in self.maps: + outfile.write('\n\n' + title + '\n\n') + break + for player in range(1, self.world.players + 1): + if (type, player) in self.maps: + if self.world.players > 1: + outfile.write(str('(Player ' + str(player) + ')\n')) # player name + outfile.write(self.maps[(type, player)]['text']) + if 'all' in self.settings or 'flute' in self.settings: # flute shuffle - for player in range(1, self.world.players + 1): - if ('flute', player) in self.maps: - outfile.write('\n\nFlute Spots:\n\n') - break - for player in range(1, self.world.players + 1): - if ('flute', player) in self.maps: - if self.world.players > 1: - outfile.write(str('(Player ' + str(player) + ')\n')) # player name - outfile.write(self.maps[('flute', player)]['text']) - + write_map('flute', 'Flute Spots:') + if 'all' in self.settings or 'overworld' in self.settings: # overworld tile flips - for player in range(1, self.world.players + 1): - if ('swaps', player) in self.maps: - outfile.write('\n\nOW Tile Flips:\n\n') - break - for player in range(1, self.world.players + 1): - if ('swaps', player) in self.maps: - if self.world.players > 1: - outfile.write(str('(Player ' + str(player) + ')\n')) # player name - outfile.write(self.maps[('swaps', player)]['text']) + write_map('swaps', 'OW Tile Flips:') # crossed groups - for player in range(1, self.world.players + 1): - if ('groups', player) in self.maps: - outfile.write('\n\nOW Crossed Groups:\n\n') - break - for player in range(1, self.world.players + 1): - if ('groups', player) in self.maps: - if self.world.players > 1: - outfile.write(str('(Player ' + str(player) + ')\n')) # player name - outfile.write(self.maps[('groups', player)]['text']) - + write_map('groups', 'OW Crossed Groups:') + + # grid layout + write_map('layout_grid_lw', 'Light World Layout:') + write_map('layout_grid_dw', 'Dark World Layout:') + if self.overworlds and ('all' in self.settings or 'overworld' in self.settings): outfile.write('\n\nOverworld Edges:\n\n') # overworld transitions @@ -3728,11 +3725,11 @@ boss_mode = {"none": 0, "simple": 1, "full": 2, "chaos": 3, 'random': 3, 'unique # byte 10: settings_version -# byte 11: OOOT WCCC (OWR layout, free terrain, whirlpools, OWR crossed) -or_mode = {"vanilla": 0, "parallel": 1, "full": 2} +# byte 11: POOT WCCC (parallel, OWR layout, free terrain, whirlpools, OWR crossed) +orlayout_mode = {"vanilla": 0, "grid": 1, "wild": 2} orcrossed_mode = {"none": 0, "polar": 1, "grouped": 2, "unrestricted": 4} -# byte 12: KMBQ FF?? (keep similar, mixed/tile flip, bonk drops, follower quests, flute spots) +# byte 12: KMBQ FFO? (keep similar, mixed/tile flip, bonk drops, follower quests, flute spots, fog) flutespot_mode = {"vanilla": 0, "balanced": 1, "random": 2} # byte 13: FBBB TTPP (flute_mode, bow_mode, take_any, prize shuffle) @@ -3795,12 +3792,12 @@ class Settings(object): settings_version, - (or_mode[w.owShuffle[p]] << 5) | (0x10 if w.owTerrain[p] else 0) + (0x80 if w.owParallel[p] else 0) | (orlayout_mode[w.owLayout[p]] << 5) | (0x10 if w.owTerrain[p] else 0) | (0x08 if w.owWhirlpoolShuffle[p] else 0) | orcrossed_mode[w.owCrossed[p]], (0x80 if w.owKeepSimilar[p] else 0) | (0x40 if w.owMixed[p] else 0) | (0x20 if w.shuffle_bonk_drops[p] else 0) | (0x10 if w.shuffle_followers[p] else 0) - | (flutespot_mode[w.owFluteShuffle[p]] << 4), + | (flutespot_mode[w.owFluteShuffle[p]] << 4) | (0x02 if w.owFog[p] else 0), (flute_mode[w.flute_mode[p]] << 7 | bow_mode[w.bow_mode[p]] << 4 | take_any_mode[w.take_any[p]] << 2 | prizeshuffle_mode[w.prizeshuffle[p]]), @@ -3877,7 +3874,8 @@ class Settings(object): args.algorithm = r(algo_mode)[(settings[9] & 0x38) >> 3] args.shufflebosses[p] = r(boss_mode)[(settings[9] & 0x07)] - args.ow_shuffle[p] = r(or_mode)[(settings[11] & 0xE0) >> 5] + args.ow_parallel[p] = True if settings[11] & 0x80 else False + args.ow_layout[p] = r(orlayout_mode)[(settings[11] & 0x60) >> 5] args.ow_terrain[p] = True if settings[11] & 0x10 else False args.ow_whirlpool[p] = True if settings[11] & 0x08 else False args.ow_crossed[p] = r(orcrossed_mode)[(settings[11] & 0x07)] @@ -3887,6 +3885,7 @@ class Settings(object): args.bonk_drops[p] = True if settings[12] & 0x20 else False args.shuffle_followers[p] = True if settings[12] & 0x10 else False args.ow_fluteshuffle[p] = r(flutespot_mode)[(settings[12] & 0x0C) >> 2] + args.ow_fog[p] = True if settings[12] & 0x02 else False if len(settings) > 13: args.flute_mode[p] = r(flute_mode)[(settings[13] & 0x80) >> 7] diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e06e72c..8d2f5957 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +# 0.7.0.0 +- New OW Layout Shuffle Mode: Grid +- Implemented Fog of War for Tile Flip + # 0.6.1.11 - Fixed bonk drops duplicate counting and potentially overwriting arbitrary values - Fixed boss icons on dungeon map check diff --git a/CLI.py b/CLI.py index 5eb4f3e2..3afd4a71 100644 --- a/CLI.py +++ b/CLI.py @@ -120,6 +120,19 @@ def parse_cli(argv, no_defaults=False): ret.take_any = 'random' if ret.take_any == 'none' else ret.take_any ret.keyshuffle = 'universal' + if ret.ow_unparallel: + ret.ow_parallel = False + + if ret.ow_shuffle == 'parallel': + ret.ow_layout = 'wild' + ret.ow_parallel = True + elif ret.ow_shuffle == 'full': + ret.ow_layout = 'wild' + ret.ow_parallel = False + + if ret.ow_no_fog: + ret.ow_fog = False + if player_num: defaults = copy.deepcopy(ret) for player in range(1, player_num + 1): @@ -130,9 +143,9 @@ def parse_cli(argv, no_defaults=False): for k, v in playersettings.items(): setattr(playerargs, k, v) - for name in ['logic', 'mode', 'swords', 'goal', 'difficulty', 'item_functionality', 'ow_shuffle', - 'ow_terrain', 'ow_crossed', 'ow_keepsimilar', 'ow_mixed', 'ow_whirlpool', 'ow_fluteshuffle', - 'flute_mode', 'bow_mode', 'take_any', 'boots_hint', 'shuffle_followers', + for name in ['logic', 'mode', 'swords', 'goal', 'difficulty', 'item_functionality', 'ow_shuffle', 'ow_layout', + 'ow_parallel', 'ow_terrain', 'ow_crossed', 'ow_keepsimilar', 'ow_mixed', 'ow_whirlpool', 'ow_fluteshuffle', + 'ow_fog', 'flute_mode', 'bow_mode', 'take_any', 'boots_hint', 'shuffle_followers', 'shuffle', 'door_shuffle', 'intensity', 'crystals_ganon', 'crystals_gt', 'openpyramid', 'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'prizeshuffle', 'startinventory', 'usestartinventory', 'bombbag', 'shuffleganon', 'overworld_map', 'restrict_boss_items', @@ -193,13 +206,18 @@ def parse_settings(): # Shuffle Ganon defaults to TRUE "openpyramid": "auto", "shuffleganon": True, - "ow_shuffle": "vanilla", + "ow_shuffle": "vanilla", # for backwards compatibility + "ow_layout": "vanilla", + "ow_parallel": True, + "ow_unparallel": False, "ow_terrain": False, "ow_crossed": "none", "ow_keepsimilar": False, "ow_mixed": False, "ow_whirlpool": False, "ow_fluteshuffle": "vanilla", + "ow_fog": True, + "ow_no_fog": False, "shuffle_followers": False, "bonk_drops": False, "shuffle": "vanilla", diff --git a/Main.py b/Main.py index a2dba973..bf5fc65c 100644 --- a/Main.py +++ b/Main.py @@ -432,7 +432,7 @@ def init_world(args, fish): customized.load_yaml(args.customizer) customized.adjust_args(args, False) - world = World(args.multi, args.ow_shuffle, args.ow_crossed, args.ow_mixed, args.shuffle, args.door_shuffle, args.logic, args.mode, args.swords, + world = World(args.multi, args.ow_layout, args.ow_parallel, 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.custom, args.customitemarray, args.hints, args.spoiler) @@ -453,6 +453,7 @@ def init_world(args, fish): world.owKeepSimilar = args.ow_keepsimilar.copy() world.owWhirlpoolShuffle = args.ow_whirlpool.copy() world.owFluteShuffle = args.ow_fluteshuffle.copy() + world.owFog = args.ow_fog.copy() world.shuffle_followers = args.shuffle_followers.copy() world.shuffle_bonk_drops = args.bonk_drops.copy() world.open_pyramid = args.openpyramid.copy() @@ -725,7 +726,7 @@ def set_starting_inventory(world, args): def copy_world(world): # ToDo: Not good yet - ret = World(world.players, world.owShuffle, world.owCrossed, world.owMixed, world.shuffle, world.doorShuffle, world.logic, world.mode, world.swords, + ret = World(world.players, world.owLayout, world.owParallel, 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.custom, world.customitemarray, world.hints, world.spoiler_mode) ret.teams = world.teams @@ -765,6 +766,7 @@ def copy_world(world): ret.owKeepSimilar = world.owKeepSimilar.copy() ret.owWhirlpoolShuffle = world.owWhirlpoolShuffle.copy() ret.owFluteShuffle = world.owFluteShuffle.copy() + ret.owFog = world.owFog.copy() ret.shuffle_followers = world.shuffle_followers.copy() ret.shuffle_bonk_drops = world.shuffle_bonk_drops.copy() ret.open_pyramid = world.open_pyramid.copy() @@ -946,7 +948,7 @@ def copy_world(world): def copy_world_premature(world, player, create_flute_exits=True): # ToDo: Not good yet - ret = World(world.players, world.owShuffle, world.owCrossed, world.owMixed, world.shuffle, world.doorShuffle, world.logic, world.mode, world.swords, + ret = World(world.players, world.owLayout, world.owParallel, 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.custom, world.customitemarray, world.hints, world.spoiler_mode) ret.teams = world.teams @@ -986,6 +988,7 @@ def copy_world_premature(world, player, create_flute_exits=True): ret.owKeepSimilar = world.owKeepSimilar.copy() ret.owWhirlpoolShuffle = world.owWhirlpoolShuffle.copy() ret.owFluteShuffle = world.owFluteShuffle.copy() + ret.owFog = world.owFog.copy() ret.shuffle_followers = world.shuffle_followers.copy() ret.shuffle_bonk_drops = world.shuffle_bonk_drops.copy() ret.open_pyramid = world.open_pyramid.copy() diff --git a/OWEdges.py b/OWEdges.py index 645e05ad..63967a07 100644 --- a/OWEdges.py +++ b/OWEdges.py @@ -321,15 +321,23 @@ def create_owedges(world, player): create_owedge(player, 'Hobo EC', 0x80, Ea, Wr, 0x4a) .coordInfo(0x008c, 0x0020).special_exit(0x81), create_owedge(player, 'Zoras Domain SW', 0x81, So, Ld, 0x41, 0x89).coordInfo(0x02a4, 0x1782).special_exit(0x82) ] - + world.owedges += edges world.initialize_owedges(edges) + set_parallel_owedge_links(world, player, edges) def create_owedge(player, name, owIndex, direction, terrain, edge_id, owSlotIndex=0xff): if name not in OWExitTypes['OWEdge']: OWExitTypes['OWEdge'].append(name) return OWEdge(player, name, owIndex, direction, terrain, edge_id, owSlotIndex) +def set_parallel_owedge_links(world, player, edges): + for edge in edges: + if edge.name in parallel_links: + dw_edge = world.get_owedge(parallel_links[edge.name], player) + edge.parallel = dw_edge + dw_edge.parallel = edge + OWEdgeGroups = { #(IsStandard, World, EdgeAxis, Terrain, HasParallel, NumberInGroup, CustomizerGroup) diff --git a/OverworldShuffle.py b/OverworldShuffle.py index d2a9ec5d..837151cc 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -8,7 +8,7 @@ from OWEdges import OWTileRegions, OWEdgeGroups, OWEdgeGroupsTerrain, OWExitType from OverworldGlitchRules import create_owg_connections from Utils import bidict -version_number = '0.6.1.11' +version_number = '0.7.0.0' # branch indicator is intentionally different across branches version_branch = '' @@ -114,37 +114,38 @@ def link_overworld(world, player): # restructure Maze Race/Suburb/Frog/Dig Game manually due to NP/P relationship parallel_links_new = bidict(parallel_links) # shallow copy is enough (deep copy is broken) - if world.owKeepSimilar[player]: - del parallel_links_new['Maze Race ES'] - del parallel_links_new['Kakariko Suburb WS'] - for group in trimmed_groups.keys(): - (std, region, axis, terrain, parallel, _, custom) = group - if parallel == IsParallel.Yes: + if world.owLayout[player] != 'grid': + if world.owKeepSimilar[player]: + del parallel_links_new['Maze Race ES'] + del parallel_links_new['Kakariko Suburb WS'] + for group in trimmed_groups.keys(): + (std, region, axis, terrain, parallel, _, custom) = group + if parallel == IsParallel.Yes: + (forward_edges, back_edges) = trimmed_groups[group] + if ['Maze Race ES'] in forward_edges: + forward_edges.remove(['Maze Race ES']) + trimmed_groups[(std, region, axis, terrain, IsParallel.No, 1, custom)][0].append(['Maze Race ES']) + if ['Kakariko Suburb WS'] in back_edges: + back_edges.remove(['Kakariko Suburb WS']) + trimmed_groups[(std, region, axis, terrain, IsParallel.No, 1, custom)][1].append(['Kakariko Suburb WS']) + trimmed_groups[group] = (forward_edges, back_edges) + else: + for group in trimmed_groups.keys(): + (std, region, axis, terrain, _, _, custom) = group (forward_edges, back_edges) = trimmed_groups[group] - if ['Maze Race ES'] in forward_edges: - forward_edges.remove(['Maze Race ES']) - trimmed_groups[(std, region, axis, terrain, IsParallel.No, 1, custom)][0].append(['Maze Race ES']) - if ['Kakariko Suburb WS'] in back_edges: - back_edges.remove(['Kakariko Suburb WS']) - trimmed_groups[(std, region, axis, terrain, IsParallel.No, 1, custom)][1].append(['Kakariko Suburb WS']) + if ['Dig Game EC', 'Dig Game ES'] in forward_edges: + forward_edges.remove(['Dig Game EC', 'Dig Game ES']) + trimmed_groups[(std, region, axis, terrain, IsParallel.Yes, 1, custom)][0].append(['Dig Game ES']) + trimmed_groups[(std, region, axis, terrain, IsParallel.No, 1, custom)][0].append(['Dig Game EC']) + if ['Frog WC', 'Frog WS'] in back_edges: + back_edges.remove(['Frog WC', 'Frog WS']) + trimmed_groups[(std, region, axis, terrain, IsParallel.Yes, 1, custom)][1].append(['Frog WS']) + trimmed_groups[(std, region, axis, terrain, IsParallel.No, 1, custom)][1].append(['Frog WC']) trimmed_groups[group] = (forward_edges, back_edges) - else: - for group in trimmed_groups.keys(): - (std, region, axis, terrain, _, _, custom) = group - (forward_edges, back_edges) = trimmed_groups[group] - if ['Dig Game EC', 'Dig Game ES'] in forward_edges: - forward_edges.remove(['Dig Game EC', 'Dig Game ES']) - trimmed_groups[(std, region, axis, terrain, IsParallel.Yes, 1, custom)][0].append(['Dig Game ES']) - trimmed_groups[(std, region, axis, terrain, IsParallel.No, 1, custom)][0].append(['Dig Game EC']) - if ['Frog WC', 'Frog WS'] in back_edges: - back_edges.remove(['Frog WC', 'Frog WS']) - trimmed_groups[(std, region, axis, terrain, IsParallel.Yes, 1, custom)][1].append(['Frog WS']) - trimmed_groups[(std, region, axis, terrain, IsParallel.No, 1, custom)][1].append(['Frog WC']) - trimmed_groups[group] = (forward_edges, back_edges) parallel_links_new = {**dict(parallel_links_new), **dict({e:p[0] for e, p in parallel_links_new.inverse.items()})} connected_edges = [] - if world.owShuffle[player] != 'vanilla': + if world.owLayout[player] != 'vanilla': trimmed_groups = remove_reserved(world, trimmed_groups, connected_edges, player) trimmed_groups = reorganize_groups(world, trimmed_groups, player) @@ -232,7 +233,7 @@ def link_overworld(world, player): if 'undefined_chance' in custom_crossed: undefined_chance = custom_crossed['undefined_chance'] - if limited_crossed > -1: + if limited_crossed > -1 and world.owLayout[player] != 'grid': # connect forced crossed non-parallel edges based on previously determined tile flips for edge in swapped_edges: if edge not in parallel_links_new: @@ -264,10 +265,10 @@ def link_overworld(world, player): s[0x30], s[0x35], s[0x41], s[0x3a],s[0x3b],s[0x3c], s[0x3f]) world.spoiler.set_map('groups', text_output, ow_crossed_tiles, player) - elif limited_crossed > -1 or (world.owShuffle[player] == 'vanilla' and world.owCrossed[player] == 'unrestricted'): + elif (limited_crossed > -1 and world.owLayout[player] != 'grid') or (world.owLayout[player] == 'vanilla' and world.owCrossed[player] == 'unrestricted'): crossed_candidates = list() for group in trimmed_groups.keys(): - (mode, wrld, dir, terrain, parallel, count, _) = group + (mode, wrld, _, terrain, parallel, _, _) = 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 forward_set[0] in parallel_links_new: @@ -278,7 +279,7 @@ def link_overworld(world, player): combine_set = forward_combine+back_combine skip_forward = False - if world.owShuffle[player] == 'vanilla': + if world.owLayout[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): @@ -412,7 +413,7 @@ def link_overworld(world, player): # layout shuffle logging.getLogger('').debug('Shuffling overworld layout') - if world.owShuffle[player] == 'vanilla': + if world.owLayout[player] == 'vanilla': # apply outstanding flips trimmed_groups = performSwap(trimmed_groups, edges_to_swap) assert len(edges_to_swap) == 0, 'Not all edges were flipped successfully: ' + ', '.join(edges_to_swap) @@ -425,8 +426,15 @@ def link_overworld(world, player): 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 world.owLayout[player] == 'grid': + from source.overworld.LayoutGenerator import generate_random_grid_layout + + for exitname, destname in special_screen_connections: + connect_two_way(world, exitname, destname, player, connected_edges) + + generate_random_grid_layout(world, player, connected_edges, ow_crossed_tiles if world.owCrossed[player] == 'grouped' else [], force_noncrossed, force_crossed, limited_crossed, undefined_chance / 100) else: - if world.owKeepSimilar[player] and world.owShuffle[player] == 'parallel': + if world.owKeepSimilar[player] and world.owParallel[player]: for exitname, destname in parallelsimilar_connections: connect_two_way(world, exitname, destname, player, connected_edges) @@ -576,7 +584,10 @@ def link_overworld(world, player): connect_simple(world, 'Flute Spot ' + str(o + 1), regions[1], player) if world.owFluteShuffle[player] == 'vanilla': - connect_flutes(default_flute_connections) + flute_spots = default_flute_connections.copy() + sort_flute_spots(world, player, flute_spots) + world.owflutespots[player] = flute_spots + connect_flutes(flute_spots) else: flute_spots = 8 flute_pool = list(flute_data.keys()) @@ -701,9 +712,9 @@ def link_overworld(world, player): region_total -= sector[0] flute_spots -= spots_to_place - + # connect new flute spots - new_spots.sort() + sort_flute_spots(world, player, new_spots) world.owflutespots[player] = new_spots connect_flutes(new_spots) @@ -822,7 +833,7 @@ def connect_custom(world, connected_edges, groups, forced, player): remove_pair_from_pool(edge1.name, edge2.name, is_crossed) connect_two_way(world, edge1.name, edge2.name, player, connected_edges) # resolve parallel - if world.owShuffle[player] == 'parallel' and edge1.name in parallel_links_new: + if world.owParallel[player] 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): @@ -838,13 +849,13 @@ def connect_custom(world, connected_edges, groups, forced, player): 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: + if world.owParallel[player] 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): +def connect_two_way(world, edgename1, edgename2, player, connected_edges=None, set_spoiler=True): edge1 = world.get_entrance(edgename1, player) edge2 = world.get_entrance(edgename2, player) x = world.get_owedge(edgename1, player) @@ -868,7 +879,7 @@ def connect_two_way(world, edgename1, edgename2, player, connected_edges=None): x.dest = y y.dest = x - if world.owShuffle[player] != 'vanilla' or world.owMixed[player] or world.owCrossed[player] != 'none': + if set_spoiler and (world.owLayout[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: @@ -876,7 +887,7 @@ def connect_two_way(world, edgename1, edgename2, player, connected_edges=None): connected_edges.append(edgename2) # connecting parallel connections - if world.owShuffle[player] in ['vanilla', 'parallel']: + if world.owLayout[player] == 'vanilla' or world.owParallel[player]: if edgename1 in parallel_links_new: try: parallel_forward_edge = parallel_links_new[edgename1] @@ -965,7 +976,7 @@ def determine_forced_flips(world, tile_ow_groups, do_grouped, player): for whirl1, whirl2 in custom_whirlpools.items(): if [whirlpool_map[whirl1], whirlpool_map[whirl2]] not in merged_owids and should_merge_group(whirlpool_map[whirl1], whirlpool_map[whirl2]): merged_owids.append([whirlpool_map[whirl1], whirlpool_map[whirl2]]) - if world.owShuffle[player] != 'vanilla': + if world.owLayout[player] != 'vanilla': custom_edges = world.customizer.get_owedges() if custom_edges and player in custom_edges: custom_edges = custom_edges[player] @@ -1071,6 +1082,9 @@ def shuffle_tiles(world, groups, result_list, do_grouped, forced_flips, player): exist_dw_regions.extend(dw_regions) parity = [sum(group_parity[group[0][0]][i] for group in groups if group not in removed) for i in range(6)] + if world.owLayout[player] == 'grid': + parity[1] = 0 + parity[2] = 0 if not world.owKeepSimilar[player]: parity[1] += 2*parity[2] parity[2] = 0 @@ -1164,12 +1178,16 @@ def define_tile_groups(world, do_grouped, player): if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'simple', 'restricted', 'district']: merge_groups([[0x05, 0x07]]) - # all non-parallel screens - if world.owShuffle[player] == 'vanilla' and (world.owCrossed[player] == 'none' or do_grouped): - merge_groups([[0x00, 0x2d, 0x80], [0x0f, 0x81], [0x1a, 0x1b], [0x28, 0x29], [0x30, 0x3a]]) + # special screens + if world.owLayout[player] != 'wild' and (world.owCrossed[player] == 'none' or do_grouped): + merge_groups([[0x00, 0x2d, 0x80], [0x0f, 0x81]]) + + # remaining non-parallel edges + if world.owLayout[player] == 'vanilla' and (world.owCrossed[player] == 'none' or do_grouped): + merge_groups([[0x1a, 0x1b], [0x28, 0x29], [0x30, 0x3a]]) # special case: non-parallel keep similar - if world.owShuffle[player] == 'parallel' and world.owKeepSimilar[player] and (world.owCrossed[player] == 'none' or do_grouped): + if world.owLayout[player] == 'wild' and world.owParallel[player] and world.owKeepSimilar[player] and (world.owCrossed[player] == 'none' or do_grouped): merge_groups([[0x28, 0x29]]) # whirlpool screens @@ -1225,7 +1243,7 @@ def reorganize_groups(world, groups, player): new_group[0] = None if world.owTerrain[player]: new_group[3] = None - if world.owShuffle[player] != 'parallel': + if not world.owParallel[player]: new_group[4] = None if not world.owKeepSimilar[player]: new_group[5] = None @@ -1294,6 +1312,15 @@ def adjust_edge_groups(world, trimmed_groups, edges_to_swap, player): groups[(mode, wrld, dir, terrain, parallel, count, group_name)][i].extend(matches) return groups +def sort_flute_spots(world, player, flute_spots): + if world.owLayout[player] != 'grid': + flute_spots.sort(key=lambda id: flute_data[id][1] if id != 0x03 or not world.is_tile_swapped(0x03, player) else 0x04) + else: + world_layout = world.owgrid[player][0] if world.mode[player] != 'inverted' else world.owgrid[player][1] + layout_list = sum(world_layout, []) + layout_map = {id & 0xBF: i for i, id in enumerate(layout_list)} + flute_spots.sort(key=lambda id: layout_map[flute_data[id][1] if id != 0x03 or not world.is_tile_swapped(0x03, player) else 0x04]) + def create_dynamic_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) if not flute_in_pool: @@ -2327,6 +2354,11 @@ parallelsimilar_connections = [('Maze Race ES', 'Kakariko Suburb WS'), ('Dig Game ES', 'Frog WS') ] +special_screen_connections = [('Lost Woods NW', 'Master Sword Meadow SC'), + ('Stone Bridge WC', 'Hobo EC'), + ('Zora Waterfall NE', 'Zoras Domain SW') + ] + # non shuffled overworld default_connections = [('Lost Woods NW', 'Master Sword Meadow SC'), ('Lost Woods SW', 'Lost Woods Pass NW'), diff --git a/Plando.py b/Plando.py index 25003f24..ade90949 100755 --- a/Plando.py +++ b/Plando.py @@ -24,7 +24,7 @@ def main(args): start_time = time.process_time() # initialize the world - world = World(1, 'vanilla', 'vanilla', 'vanilla', 'vanilla', 'noglitches', 'standard', 'normal', 'none', 'on', 'ganon', 'freshness', False, False, False, False, False, False, None, False) + world = World(1, 'vanilla', True, 'vanilla', 'vanilla', 'vanilla', 'noglitches', 'standard', 'normal', 'none', 'on', 'ganon', 'freshness', False, False, False, False, False, False, None, False) world.player_names[1].append("Player 1") logger = logging.getLogger('') @@ -157,9 +157,9 @@ def prefill_world(world, plando, text_patches): elif line.startswith('!goal'): _, goalstr = line.split(':', 1) world.goal = {1: goalstr.strip()} - elif line.startswith('!owShuffle'): + elif line.startswith('!owLayout'): _, modestr = line.split(':', 1) - world.owShuffle = {1: modestr.strip()} + world.owLayout = {1: modestr.strip()} elif line.startswith('!owCrossed'): _, modestr = line.split(':', 1) world.owCrossed = {1: modestr.strip()} diff --git a/Plandomizer_Template.txt b/Plandomizer_Template.txt index 41782f89..68b46544 100644 --- a/Plandomizer_Template.txt +++ b/Plandomizer_Template.txt @@ -244,7 +244,7 @@ Ganons Tower - Validation Chest: Nothing Ganon: Triforce # set Overworld connections (lines starting with $, separate edges with =) -!owShuffle: parallel +!owLayout: wild #!owMixed: true # Mixed OW not supported yet !owCrossed: none !owKeepSimilar: true diff --git a/README.md b/README.md index f5c6572a..86d65eee 100644 --- a/README.md +++ b/README.md @@ -132,20 +132,24 @@ Note: These changes do impact the logic. If you use `CodeTracker`, these Inverte Only settings specifically added by this Overworld Shuffle fork are found here. All door and entrance randomizer settings are supported. See their [readme](https://github.com/Aerinon/ALttPDoorRandomizer/blob/master/README.md) -## Overworld Layout Shuffle (--ow_shuffle) +## Overworld Layout Shuffle (--ow_layout) OW Edge Transitions are shuffled to create new world layouts. A brief visual representation of this can be viewed [here](https://zelda.codemann8.com/images/shared/ow-modes.gif). (This graphic also includes combinations of Crossed and Tile Flip) ### Vanilla OW Transitions are not shuffled. -### Parallel +### Grid -OW Transitions are shuffled, but both worlds will have a matching layout, similar to that of vanilla. +OW Screens are shuffled in such a way that they are still arranged on an 8x8 grid for each world like vanilla, and the OW Transitions are based on that arrangement. -### Full +### Wild -OW Transitions are shuffled within each world separately. +OW Transitions are shuffled with no respect to geometric coherence. + +## Parallel (--ow_unparallel to disable) + +With OW Layout Shuffle, this forces both worlds to have a matching layout. ## Free Terrain (--ow_terrain) @@ -389,11 +393,17 @@ Districts are a concept originally conceived by Aerinon in the Door Randomizer, Show the help message and exit. ``` ---ow_shuffle +--ow_layout ``` For specifying the overworld layout shuffle you want as above. (default: vanilla) +``` +--ow_unparallel +``` + +With OW Layout Shuffle, this no longer forces both worlds to have a matching layout. + ``` --ow_terrain ``` @@ -424,6 +434,12 @@ This gives each OW tile a random chance to be flipped to the opposite world For randomizing the flute spots around the overworld +``` +--ow_no_fog +``` + +With OW Grid Layout Shuffle or Mixed, this disables the fog that prevents you from seeing unvisited screens on the overworld map. + ``` --shuffle_followers ``` diff --git a/Rom.py b/Rom.py index 64577f96..4e467661 100644 --- a/Rom.py +++ b/Rom.py @@ -512,14 +512,15 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None): # patch flute spots owFlags = 0 - if world.owFluteShuffle[player] == 'vanilla': + owFog = 0 + if world.owFluteShuffle[player] == 'vanilla' and world.owLayout[player] != 'grid': flute_spots = default_flute_connections else: flute_spots = world.owflutespots[player] owFlags |= 0x0100 write_int16(rom, snes_to_pc(0x0AB7F7), 0xEAEA) - flute_writes = sorted([(f, flute_data[f][1]) for f in flute_spots], key = lambda f: f[1]) + flute_writes = [(f, flute_data[f][1]) for f in flute_spots] for o in range(0, len(flute_writes)): owid = flute_writes[o][0] offset = 0 @@ -544,26 +545,52 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None): write_int16(rom, snes_to_pc(0x02E937 + (o * 2)), data[base_index + 8]) # cam X write_int16(rom, snes_to_pc(0x02E959 + (o * 2)), data[base_index + 9]) # unknown 1 write_int16(rom, snes_to_pc(0x02E97B + (o * 2)), data[base_index + 10]) # unknown 2 - rom.write_byte(snes_to_pc(0x0AB783 + o), data[base_index + 12] & 0xff) # flute menu blip - X low byte - rom.write_byte(snes_to_pc(0x0AB78B + o), data[base_index + 12] // 0x100) # flute menu blip - X high byte - rom.write_byte(snes_to_pc(0x0AB793 + o), data[base_index + 11] & 0xff) # flute menu blip - Y low byte - rom.write_byte(snes_to_pc(0x0AB79B + o), data[base_index + 11] // 0x100) # flute menu blip - Y high byte + map_x, map_y = adjust_ow_coordinates_to_layout(world, player, data[base_index + 12], data[base_index + 11], world.mode[player] == 'inverted') + rom.write_byte(snes_to_pc(0x0AB783 + o), map_x & 0xff) # flute menu blip - X low byte + rom.write_byte(snes_to_pc(0x0AB78B + o), map_x // 0x100) # flute menu blip - X high byte + rom.write_byte(snes_to_pc(0x0AB793 + o), map_y & 0xff) # flute menu blip - Y low byte + rom.write_byte(snes_to_pc(0x0AB79B + o), map_y // 0x100) # flute menu blip - Y high byte # patch whirlpools if world.owWhirlpoolShuffle[player]: owFlags |= 0x01 write_int16s(rom, snes_to_pc(0x02EA5C), world.owwhirlpools[player]) - + + # set custom overworld map layout and fog + if world.owLayout[player] == 'grid': + owFlags |= 0x06 + owFog = 1 if world.owParallel[player] else 2 + grid = world.owgrid[player] + all_rows = grid[0] + grid[1] + all_cells = sum(all_rows, []) + rom.write_bytes(0x153C80, all_cells) + for pos, cell_id in enumerate(sum(grid[0], [])): + rom.write_byte(0x153D00 + cell_id % 0x40, pos) + for pos, cell_id in enumerate(sum(grid[1], [])): + rom.write_byte(0x153D40 + cell_id % 0x40, pos) + elif world.owMixed[player]: + owFlags |= 0x02 + owFog = 1 + large_screen_ids = [0x00, 0x03, 0x05, 0x18, 0x1B, 0x1E, 0x30, 0x35, 0x40, 0x43, 0x45, 0x58, 0x5B, 0x5E, 0x70, 0x75] + for cell_id in range(0x80): + if cell_id - 0x01 in large_screen_ids: + screen_id = cell_id - 0x01 + elif cell_id - 0x08 in large_screen_ids: + screen_id = cell_id - 0x08 + elif cell_id - 0x09 in large_screen_ids: + screen_id = cell_id - 0x09 + else: + screen_id = cell_id + world_flag = 0x40 if screen_id in world.owswaps[player][0] else 0x00 + rom.write_byte(0x153C80 + cell_id, cell_id ^ world_flag) + # patch overworld edges inverted_buffer = [0] * 0x82 owMode = 0 - if world.owShuffle[player] != 'vanilla' or world.owCrossed[player] not in ['none', 'polar'] or world.owMixed[player]: - 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] == 'unrestricted'): + if world.owLayout[player] != 'vanilla' or world.owCrossed[player] not in ['none', 'polar'] or world.owMixed[player]: + if world.owLayout[player] != 'vanilla': + owMode = 1 if world.owParallel[player] else 2 + if world.owKeepSimilar[player] and (world.owLayout[player] != 'vanilla' or world.owCrossed[player] == 'unrestricted'): owMode |= 0x0100 if world.owCrossed[player] != 'none' and (world.owCrossed[player] != 'polar' or world.owMixed[player]): owMode |= 0x0200 @@ -598,10 +625,11 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None): rom.write_byte(0x1539B0 + b + 9, world_flag) for edge in world.owedges: - if edge.dest is not None and isinstance(edge.dest, OWEdge) and edge.player == player: + if edge.player == player: write_int16(rom, edge.getAddress() + 0x0a, edge.vramLoc) if not edge.specialExit: - rom.write_byte(0x1539A0 + (edge.specialID - 0x80) * 2 if edge.specialEntrance else edge.getAddress() + 0x0e, edge.getTarget()) + destination = edge.getTarget() if edge.dest is not None and isinstance(edge.dest, OWEdge) else 0xFF + rom.write_byte(0x1539A0 + (edge.specialID - 0x80) * 2 if edge.specialEntrance else edge.getAddress() + 0x0e, destination) # patch bonk prizes if world.shuffle_bonk_drops[player]: @@ -630,6 +658,7 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None): write_int16(rom, 0x150002, owMode) write_int16(rom, 0x150004, owFlags) + write_int16(rom, 0x150008, owFog if world.owFog[player] else 0x00) # patch entrance/exits/holes for region in world.regions: @@ -1425,10 +1454,14 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None): y_map_position = [0x06E0, 0x0E50, 0xFF00, 0x0FD0, 0x06E0, 0x0D80, 0x0160, 0x0E80, 0x0130, 0x0840, 0x01B0] idx = ent owid = owid_map[idx] + map_x = x_map_position[idx] + map_y = y_map_position[idx] if owid != 0xFF: if (owid < 0x40) == (world.is_tile_swapped(owid, player)): coord_flags |= 0x8000 # world indicator flag - return (coord_flags | x_map_position[idx], y_map_position[idx]) + if coord_flags & 0x4000 == 0: + map_x, map_y = adjust_ow_coordinates_to_layout(world, player, map_x, map_y, coord_flags & 0x8000 != 0) + return (coord_flags | map_x, map_y) elif type(ent) is Location: from OverworldShuffle import OWTileRegions, ow_loc_prize_table if ent.name in ow_loc_prize_table: @@ -1449,8 +1482,10 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None): coords = (door_addresses[ent.name][1][6], door_addresses[ent.name][1][5]) else: raise Exception(f"No overworld map coordinates for entrance {ent.name}") - coords = ((0x8000 if ent.parent_region.type == RegionType.DarkWorld else 0x0000) | coords[0], coords[1]) + map_x, map_y = adjust_ow_coordinates_to_layout(world, player, coords[0], coords[1], ent.parent_region.type == RegionType.DarkWorld) + coords = ((0x8000 if ent.parent_region.type == RegionType.DarkWorld else 0x0000) | map_x, map_y) return coords + if world.overworld_map[player] == 'default': # disable HC/AT/GT icons if not world.owMixed[player]: @@ -2399,7 +2434,7 @@ def write_strings(rom, world, player, team): if world.is_tile_swapped(0x18, player) or world.flute_mode[player] == 'active': items_to_hint.remove(flute_item) flute_item = 'Ocarina (Activated)' - if world.owShuffle[player] != 'vanilla' or world.owMixed[player]: + if world.owLayout[player] != 'vanilla' or world.owMixed[player]: # Adding a guaranteed hint for the Flute in overworld shuffle. this_location = world.find_items_not_key_only(flute_item, player) if this_location and this_location not in hinted_locations: @@ -2417,7 +2452,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', 'district', 'swapped'] else 8 hint_count += 2 if world.doorShuffle[player] not in ['vanilla', 'basic'] else 0 - hint_count += 1 if world.owShuffle[player] != 'vanilla' or world.owCrossed[player] != 'none' or world.owMixed[player] else 0 + hint_count += 1 if world.owLayout[player] != 'vanilla' or world.owCrossed[player] != 'none' or world.owMixed[player] else 0 while hint_count > 0 and len(items_to_hint) > 0: this_item = items_to_hint.pop(0) this_location = world.find_items_not_key_only(this_item, player) @@ -3001,6 +3036,13 @@ def update_compasses(rom, dungeon_locations, world, player): if not provided_dungeon: rom.write_byte(0x186FFF, 0xff) +def adjust_ow_coordinates_to_layout(world, player, x, y, dw_flag): + if world.owLayout[player] != 'grid': + return (x, y) + layout_map = world.owlayoutmap_dw[player] if dw_flag else world.owlayoutmap_lw[player] + original_slot_id = ((y // 0x0200) % 0x08) * 0x08 + ((x // 0x0200) % 0x08) + new_slot_id = layout_map[original_slot_id] + return ((new_slot_id % 0x08) * 0x0200 + x % 0x0200, ((new_slot_id // 0x08) % 0x08) * 0x0200 + y % 0x0200) InconvenientDungeonEntrances = {'Turtle Rock': 'Turtle Rock Main', diff --git a/data/overworld/darkworld.png b/data/overworld/darkworld.png new file mode 100644 index 00000000..0c5c45fb Binary files /dev/null and b/data/overworld/darkworld.png differ diff --git a/data/overworld/lightworld.png b/data/overworld/lightworld.png new file mode 100644 index 00000000..9f2b3d7e Binary files /dev/null and b/data/overworld/lightworld.png differ diff --git a/docs/Customizer.md b/docs/Customizer.md index 17c5c337..5d5e083d 100644 --- a/docs/Customizer.md +++ b/docs/Customizer.md @@ -234,7 +234,7 @@ You may define a list of items and a list of locations. Those items will be cons ### ow-edges -This must be defined by player. Each player number should be listed with the appropriate sections and each of these players MUST have either `ow_shuffle` or `ow_crossed` enabled in the `settings` section in order for any values here to take effect. This section has two primary subsections: `two-way` and `groups`. +This must be defined by player. Each player number should be listed with the appropriate sections and each of these players MUST have either `ow_layout` or `ow_crossed` enabled in the `settings` section in order for any values here to take effect. This section has two primary subsections: `two-way` and `groups`. #### two-way @@ -253,6 +253,14 @@ someDescription: - Stone Bridge WS* ``` +### ow-grid + +`grid` contains additional options that only have an effect when `ow_layout` is set to `grid`. + +#### wrap_horizontal / wrap_vertical + +Set these to `true` to allow for overworld edge transitions to wrap from one side of a world to the opposite side. With `wrap_horizontal`, there can be east transitions on the eastern edge of the world map that send the player to the western edge of the world. With `wrap_vertical`, there can be south transitions on the southern edge of the world map that send the player to the northern edge of the world. + ### ow-crossed This must be defined by player. Each player number should be listed with the appropriate sections and each of these players MUST have `ow_crossed` enabled in the `settings` section in order for any values here to take effect. This section has four primary subsections: `force_crossed`, `force_noncrossed`, `limit_crossed`, and `undefined_chance`. There are also diff --git a/docs/customizer_example.yaml b/docs/customizer_example.yaml index 5390eeb3..c2f3943c 100644 --- a/docs/customizer_example.yaml +++ b/docs/customizer_example.yaml @@ -20,7 +20,8 @@ settings: shuffle_followers: true shuffle: crossed shufflelinks: true - ow_shuffle: parallel + ow_layout: wild + ow_parallel: true ow_terrain: true ow_crossed: grouped ow_keepsimilar: true diff --git a/presets/world/owr_districtshuffle-full.yaml b/presets/world/owr_districtshuffle-full.yaml index e9e62916..8903fb58 100644 --- a/presets/world/owr_districtshuffle-full.yaml +++ b/presets/world/owr_districtshuffle-full.yaml @@ -1,6 +1,7 @@ settings: 1: - ow_shuffle: full + ow_layout: wild + ow_parallel: false ow_keepsimilar: false ow-edges: 1: diff --git a/presets/world/owr_districtshuffle-pangea.yaml b/presets/world/owr_districtshuffle-pangea.yaml index e4d1ff3c..7f47e4f0 100644 --- a/presets/world/owr_districtshuffle-pangea.yaml +++ b/presets/world/owr_districtshuffle-pangea.yaml @@ -1,6 +1,7 @@ settings: 1: - ow_shuffle: full + ow_layout: wild + ow_parallel: false ow_keepsimilar: false ow-edges: 1: diff --git a/presets/world/owr_districtshuffle-vanillaborders.yaml b/presets/world/owr_districtshuffle-vanillaborders.yaml index 016dd9da..8f3f29d5 100644 --- a/presets/world/owr_districtshuffle-vanillaborders.yaml +++ b/presets/world/owr_districtshuffle-vanillaborders.yaml @@ -1,6 +1,7 @@ settings: 1: - ow_shuffle: full + ow_layout: wild + ow_parallel: false ow_keepsimilar: false ow-edges: 1: diff --git a/presets/world/owr_vanilla-mirroredsimilar.yaml b/presets/world/owr_vanilla-mirroredsimilar.yaml index f9411f08..9f302899 100644 --- a/presets/world/owr_vanilla-mirroredsimilar.yaml +++ b/presets/world/owr_vanilla-mirroredsimilar.yaml @@ -1,6 +1,7 @@ settings: 1: - ow_shuffle: full + ow_layout: wild + ow_parallel: false ow_terrain: true ow-edges: 1: diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index 8141614a..22044974 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -173,6 +173,22 @@ "full" ] }, + "ow_layout": { + "choices": [ + "vanilla", + "grid", + "wild" + ] + }, + "ow_parallel": { + "action": "store_true", + "help": "suppress", + "type": "bool" + }, + "ow_unparallel": { + "action": "store_true", + "type": "bool" + }, "ow_terrain": { "action": "store_true", "type": "bool" @@ -208,6 +224,15 @@ "random" ] }, + "ow_fog": { + "action": "store_true", + "help": "suppress", + "type": "bool" + }, + "ow_no_fog": { + "action": "store_true", + "type": "bool" + }, "shuffle": { "choices": [ "vanilla", diff --git a/resources/app/cli/lang/en.json b/resources/app/cli/lang/en.json index 44835df1..8eee7a1f 100644 --- a/resources/app/cli/lang/en.json +++ b/resources/app/cli/lang/en.json @@ -234,14 +234,18 @@ "the entrances vanilla." ], "ow_shuffle": [ + "Deprecated, use ow_layout and ow_unparallel instead." + ], + "ow_layout": [ "This shuffles the layout of the overworld.", "Vanilla: All overworld transitions are connected the same", " way they were in the base game.", - "Parallel: Overworld transitions are shuffled, but both worlds", - " will have the same pattern/shape.", - "Full: Overworld transitions are shuffled, but both worlds", - " will have an independent map shape." + "Grid: OW Screens are arranged on 8x8 grids and OW Transitions", + " work according to this arrangement.", + "Wild: OW Transitions are shuffled with no respect to geometric coherence." ], + "ow_unparallel": [ + "With OW Layout Shuffle, this no longer forces both worlds to have a matching layout." ], "ow_terrain": [ "With OW Layout Shuffle, this allows land and water edges to be connected." ], "ow_crossed": [ @@ -276,6 +280,9 @@ " spots from being on any adjacent screen.", "Random: New flute spots will be generated with minimal bias." ], + "ow_no_fog": [ + "With OW Grid Layout Shuffle or Mixed, this disables the fog that prevents", + "you from seeing unvisited screens on the overworld map." ], "door_shuffle": [ "Select Door Shuffling Algorithm. (default: %(default)s)", "Basic: Doors are mixed within a single dungeon.", diff --git a/resources/app/gui/lang/en.json b/resources/app/gui/lang/en.json index 0ad57757..c43a383e 100644 --- a/resources/app/gui/lang/en.json +++ b/resources/app/gui/lang/en.json @@ -157,10 +157,12 @@ "randomizer.enemizer.enemylogic.allow_all": "Allow special enemies anywhere", - "randomizer.overworld.overworldshuffle": "Layout Shuffle", - "randomizer.overworld.overworldshuffle.vanilla": "Vanilla", - "randomizer.overworld.overworldshuffle.parallel": "Parallel", - "randomizer.overworld.overworldshuffle.full": "Full", + "randomizer.overworld.layout": "Layout Shuffle", + "randomizer.overworld.layout.vanilla": "Vanilla", + "randomizer.overworld.layout.grid": "Grid", + "randomizer.overworld.layout.wild": "Wild", + + "randomizer.overworld.parallel": "Keep Worlds Parallel", "randomizer.overworld.terrain": "Free Terrain", @@ -181,6 +183,8 @@ "randomizer.overworld.overworldflute.balanced": "Balanced", "randomizer.overworld.overworldflute.random": "Random", + "randomizer.overworld.fog": "Overworld Map Fog", + "randomizer.entrance.openpyramid": "Pre-open Pyramid Hole", "randomizer.entrance.openpyramid.auto": "Auto", diff --git a/resources/app/gui/randomize/overworld/widgets.json b/resources/app/gui/randomize/overworld/widgets.json index c3c49c94..9a58ad66 100644 --- a/resources/app/gui/randomize/overworld/widgets.json +++ b/resources/app/gui/randomize/overworld/widgets.json @@ -1,13 +1,13 @@ { "topOverworldFrame": {}, "leftOverworldFrame": { - "overworldshuffle": { + "layout": { "type": "selectbox", "default": "vanilla", "options": [ "vanilla", - "parallel", - "full" + "grid", + "wild" ] }, "crossed": { @@ -18,7 +18,10 @@ "grouped", "polar", "unrestricted" - ] + ], + "config": { + "pady": [16,0] + } }, "mixed": { "type": "checkbox", @@ -45,22 +48,27 @@ "config": { "pady": [20,0] } + }, + "fog": { + "type": "checkbox", + "default": true, + "config": { + "pady": [20,0] + } } }, "rightOverworldFrame": { + "parallel": { + "type": "checkbox", + "default": true + }, "terrain": { "type": "checkbox", - "default": false, - "config": { - "pady": [3,0] - } + "default": false }, "keepsimilar": { "type": "checkbox", - "default": false, - "config": { - "pady": [6,0] - } + "default": false } } } \ No newline at end of file diff --git a/source/classes/CustomSettings.py b/source/classes/CustomSettings.py index 7a3f3712..653f8758 100644 --- a/source/classes/CustomSettings.py +++ b/source/classes/CustomSettings.py @@ -89,7 +89,8 @@ class CustomSettings(object): args.mystery = True else: settings = defaultdict(lambda: None, player_setting) - args.ow_shuffle[p] = get_setting(settings['ow_shuffle'], args.ow_shuffle[p]) + args.ow_layout[p] = get_setting(settings['ow_layout'], args.ow_layout[p]) + args.ow_parallel[p] = get_setting(settings['ow_parallel'], args.ow_parallel[p]) args.ow_terrain[p] = get_setting(settings['ow_terrain'], args.ow_terrain[p]) args.ow_crossed[p] = get_setting(settings['ow_crossed'], args.ow_crossed[p]) if args.ow_crossed[p] == 'chaos': @@ -100,6 +101,7 @@ class CustomSettings(object): args.ow_mixed[p] = get_setting(settings['ow_mixed'], args.ow_mixed[p]) args.ow_whirlpool[p] = get_setting(settings['ow_whirlpool'], args.ow_whirlpool[p]) args.ow_fluteshuffle[p] = get_setting(settings['ow_fluteshuffle'], args.ow_fluteshuffle[p]) + args.ow_fog[p] = get_setting(settings['ow_fog'], args.ow_fog[p]) args.shuffle_followers[p] = get_setting(settings['shuffle_followers'], args.shuffle_followers[p]) args.bonk_drops[p] = get_setting(settings['bonk_drops'], args.bonk_drops[p]) args.shuffle[p] = get_setting(settings['shuffle'], args.shuffle[p]) @@ -135,6 +137,14 @@ class CustomSettings(object): args.take_any[p] = 'random' if args.take_any[p] == 'none' else args.take_any[p] args.keyshuffle[p] = 'universal' + ow_shuffle = get_setting(settings['ow_shuffle'], args.ow_shuffle[p]) + if ow_shuffle == 'parallel': + args.ow_layout = 'wild' + args.ow_parallel = True + elif ow_shuffle == 'full': + args.ow_layout = 'wild' + args.ow_parallel = False + args.mixed_travel[p] = get_setting(settings['mixed_travel'], args.mixed_travel[p]) args.standardize_palettes[p] = get_setting(settings['standardize_palettes'], args.standardize_palettes[p]) @@ -253,6 +263,11 @@ class CustomSettings(object): return self.file_source['ow-edges'] return None + def get_owgrid(self): + if 'ow-grid' in self.file_source: + return self.file_source['ow-grid'] + return None + def get_owcrossed(self): if 'ow-crossed' in self.file_source: return self.file_source['ow-crossed'] @@ -356,13 +371,15 @@ class CustomSettings(object): self.world_rep['start_inventory'] = start_inv = {} for p in self.player_range: settings_dict[p] = {} - settings_dict[p]['ow_shuffle'] = world.owShuffle[p] + settings_dict[p]['ow_layout'] = world.owLayout[p] + settings_dict[p]['ow_parallel'] = world.owParallel[p] settings_dict[p]['ow_terrain'] = world.owTerrain[p] settings_dict[p]['ow_crossed'] = world.owCrossed[p] settings_dict[p]['ow_keepsimilar'] = world.owKeepSimilar[p] settings_dict[p]['ow_mixed'] = world.owMixed[p] settings_dict[p]['ow_whirlpool'] = world.owWhirlpoolShuffle[p] settings_dict[p]['ow_fluteshuffle'] = world.owFluteShuffle[p] + settings_dict[p]['ow_fog'] = world.owFog[p] settings_dict[p]['shuffle_followers'] = world.shuffle_followers[p] settings_dict[p]['bonk_drops'] = world.shuffle_bonk_drops[p] settings_dict[p]['shuffle'] = world.shuffle[p] diff --git a/source/classes/constants.py b/source/classes/constants.py index c8e017ef..069afeae 100644 --- a/source/classes/constants.py +++ b/source/classes/constants.py @@ -92,13 +92,15 @@ SETTINGSTOPROCESS = { "bombbag": "bombbag" }, "overworld": { - "overworldshuffle": "ow_shuffle", + "layout": "ow_layout", + "parallel": "ow_parallel", "terrain": "ow_terrain", "crossed": "ow_crossed", "keepsimilar": "ow_keepsimilar", "mixed": "ow_mixed", "whirlpool": "ow_whirlpool", - "overworldflute": "ow_fluteshuffle" + "overworldflute": "ow_fluteshuffle", + "fog": "ow_fog" }, "entrance": { "entranceshuffle": "shuffle", diff --git a/source/overworld/EntranceShuffle2.py b/source/overworld/EntranceShuffle2.py index 8ef10110..d189a457 100644 --- a/source/overworld/EntranceShuffle2.py +++ b/source/overworld/EntranceShuffle2.py @@ -635,7 +635,7 @@ def do_dark_sanc(entrances, exits, avail): forbidden.append('Links House') else: forbidden.append('Big Bomb Shop') - if avail.world.owShuffle[avail.player] == 'vanilla': + if avail.world.owLayout[avail.player] == 'vanilla': choices = [e for e in avail.world.districts[avail.player]['Northwest Dark World'].entrances if e not in forbidden and e in entrances] else: choices = [e for e in get_starting_entrances(avail) if e not in forbidden and e in entrances] @@ -679,7 +679,7 @@ def do_links_house(entrances, exits, avail, cross_world): forbidden.append(links_house_vanilla) forbidden.extend(Forbidden_Swap_Entrances) shuffle_mode = avail.world.shuffle[avail.player] - if avail.world.owShuffle[avail.player] == 'vanilla': + if avail.world.owLayout[avail.player] == 'vanilla': # simple shuffle - if shuffle_mode == 'simple': avail.links_on_mountain = True # taken care of by the logic below @@ -733,7 +733,7 @@ def do_links_house(entrances, exits, avail, cross_world): # links on dm dm_spots = LH_DM_Connector_List.union(LH_DM_Exit_Forbidden) - if links_house in dm_spots and avail.world.owShuffle[avail.player] == 'vanilla': + if links_house in dm_spots and avail.world.owLayout[avail.player] == 'vanilla': if avail.links_on_mountain: return # connector is fine logging.getLogger('').warning(f'Links House is placed in tight area and is now unhandled. Report any errors that occur from here.') diff --git a/source/overworld/LayoutGenerator.py b/source/overworld/LayoutGenerator.py new file mode 100644 index 00000000..467941ab --- /dev/null +++ b/source/overworld/LayoutGenerator.py @@ -0,0 +1,1423 @@ +import copy +import logging +import RaceRandom as random +import random as _random +from typing import List, Dict, Optional, Set, Tuple +from BaseClasses import OWEdge, World, Direction, Terrain +from OverworldShuffle import connect_two_way, validate_layout + +ENABLE_KEEP_SIMILAR_SPECIAL_HANDLING = False +PREVENT_WRAPPED_LARGE_SCREENS = False +DRAW_IMAGE = False + +large_screen_ids = [0x00, 0x03, 0x05, 0x18, 0x1B, 0x1E, 0x30, 0x35] + [0x40, 0x43, 0x45, 0x58, 0x5B, 0x5E, 0x70, 0x75] + +# ============================================================================ +# DATA STRUCTURES +# ============================================================================ + +class Screen: + """ + Represents a game map screen. + """ + __slots__ = ('id', 'big', 'dark_world', 'parallel', + 'edges', 'mixed_state') + + def __init__( + self, + id: int, + big: bool = False, + dark_world: bool = False, + parallel: Optional['Screen'] = None, + edges: Optional[Dict[str, OWEdge]] = None, + mixed_state: str = "normal" + ): + self.id = id + self.big = big + self.dark_world = dark_world + self.parallel = parallel + self.edges = edges if edges is not None else {} + self.mixed_state = mixed_state # "normal" or "swapped" + +class WorldPiece: + """ + Represents a piece within a world containing screens to be placed on the grid. + """ + __slots__ = ('screens', 'grid', 'width', 'height', 'north_edges', 'south_edges', + 'west_edges', 'east_edges', 'north_edges_water', 'south_edges_water', + 'west_edges_water', 'east_edges_water') + + def __init__( + self, + screens: List[List[Optional[Screen]]], + grid: Optional[List[List[int]]] = None, + width: int = 0, + height: int = 0, + north_edges: Optional[List[List[List[OWEdge]]]] = None, + south_edges: Optional[List[List[List[OWEdge]]]] = None, + west_edges: Optional[List[List[List[OWEdge]]]] = None, + east_edges: Optional[List[List[List[OWEdge]]]] = None, + north_edges_water: Optional[List[List[List[OWEdge]]]] = None, + south_edges_water: Optional[List[List[List[OWEdge]]]] = None, + west_edges_water: Optional[List[List[List[OWEdge]]]] = None, + east_edges_water: Optional[List[List[List[OWEdge]]]] = None + ): + self.screens = screens + self.grid = grid if grid is not None else [] + self.width = width + self.height = height + self.north_edges = north_edges if north_edges is not None else [] + self.south_edges = south_edges if south_edges is not None else [] + self.west_edges = west_edges if west_edges is not None else [] + self.east_edges = east_edges if east_edges is not None else [] + self.north_edges_water = north_edges_water if north_edges_water is not None else [] + self.south_edges_water = south_edges_water if south_edges_water is not None else [] + self.west_edges_water = west_edges_water if west_edges_water is not None else [] + self.east_edges_water = east_edges_water if east_edges_water is not None else [] + +class Piece: + """ + Represents a piece consisting of a main and optionally a parallel world piece. + """ + __slots__ = ('main', 'parallel', 'world', 'width', 'height', + 'invalid_wrap_row', 'invalid_wrap_column', 'restriction', + 'crossed_groups', 'delay', 'order', 'edge_sides', 'max_edges_per_side') + + def __init__( + self, + main: WorldPiece, + parallel: Optional[WorldPiece] = None, + world: int = 0, + width: int = 0, + height: int = 0, + invalid_wrap_row: Optional[List[int]] = None, + invalid_wrap_column: Optional[List[int]] = None, + restriction: Optional[List[int]] = None, + crossed_groups: Optional[List[List[int]]] = None, + delay: int = 0, + order: float = 0.0, + edge_sides: int = 0, + max_edges_per_side: int = 0 + ): + self.main = main + self.parallel = parallel + self.world = world # 0 or 1 + self.width = width + self.height = height + self.invalid_wrap_row = invalid_wrap_row if invalid_wrap_row is not None else [] + self.invalid_wrap_column = invalid_wrap_column if invalid_wrap_column is not None else [] + self.restriction = restriction + self.crossed_groups = crossed_groups if crossed_groups is not None else [] + self.delay = delay + self.order = order + self.edge_sides = edge_sides + self.max_edges_per_side = max_edges_per_side + +class GridInfo: + """ + Container for grid layout information during placement runs. + Stores screen IDs and edge information for both Light and Dark worlds. + """ + __slots__ = ( + 'grid', 'north_edges_grid', 'south_edges_grid', 'west_edges_grid', 'east_edges_grid', + 'north_edges_water_grid', 'south_edges_water_grid', 'west_edges_water_grid', 'east_edges_water_grid', + 'crossed_groups', 'edge_connection_seed' + ) + + def __init__( + self, + grid: List[List[List[int]]], + north_edges_grid: List[List[List[List[OWEdge]]]], + south_edges_grid: List[List[List[List[OWEdge]]]], + west_edges_grid: List[List[List[List[OWEdge]]]], + east_edges_grid: List[List[List[List[OWEdge]]]], + north_edges_water_grid: List[List[List[List[OWEdge]]]], + south_edges_water_grid: List[List[List[List[OWEdge]]]], + west_edges_water_grid: List[List[List[List[OWEdge]]]], + east_edges_water_grid: List[List[List[List[OWEdge]]]], + crossed_groups: List[List[int]], + edge_connection_seed: float + ): + self.grid = grid + self.north_edges_grid = north_edges_grid + self.south_edges_grid = south_edges_grid + self.west_edges_grid = west_edges_grid + self.east_edges_grid = east_edges_grid + self.north_edges_water_grid = north_edges_water_grid + self.south_edges_water_grid = south_edges_water_grid + self.west_edges_water_grid = west_edges_water_grid + self.east_edges_water_grid = east_edges_water_grid + self.crossed_groups = crossed_groups + self.edge_connection_seed = edge_connection_seed + +class LayoutGeneratorOptions: + """ + Configuration options for layout generation. + """ + __slots__ = ('horizontal_wrap', 'vertical_wrap', + 'large_screen_pool', 'distortion_chance', 'random_order', + 'multi_choice', 'max_delay', 'first_ignore_bonus_points', + 'penalty_full_edge_mismatch', 'penalty_partial_edge_mismatch', 'bonus_partial_edge_match', + 'bonus_full_edge_match', 'bonus_crossed_group_match', 'bonus_fill_parallel', + 'forced_non_crossed_edges', 'forced_crossed_edges', 'check_reachability', + 'crossed_chance', 'crossed_limit', + 'sort_by_edge_sides', 'sort_by_max_edges_per_side', 'sort_by_piece_size', + 'min_runs', 'max_runs', 'target_runs_times_successes') + + def __init__( + self, + horizontal_wrap: bool = True, + vertical_wrap: bool = True, + large_screen_pool: bool = False, + distortion_chance: float = 0.0, + random_order: int = 0, + multi_choice: int = 1, + max_delay: int = 10, + first_ignore_bonus_points: int = 0, + penalty_full_edge_mismatch: float = 1, + penalty_partial_edge_mismatch: float = 0, + bonus_partial_edge_match: float = 1, + bonus_full_edge_match: float = 1, + bonus_crossed_group_match: float = 1, + bonus_fill_parallel: float = 0, + forced_non_crossed_edges: Set[str] = [], + forced_crossed_edges: Set[str] = [], + crossed_chance: float = 0.5, + crossed_limit: int = -1, + check_reachability: bool = True, + sort_by_edge_sides: bool = False, + sort_by_max_edges_per_side: bool = False, + sort_by_piece_size: bool = False, + min_runs: int = 100, + max_runs: int = 10000, + target_runs_times_successes: int = 5000 + ): + self.horizontal_wrap = horizontal_wrap + self.vertical_wrap = vertical_wrap + self.large_screen_pool = large_screen_pool + self.distortion_chance = distortion_chance + self.random_order = random_order + self.multi_choice = multi_choice + self.max_delay = max_delay + self.first_ignore_bonus_points = first_ignore_bonus_points + self.penalty_full_edge_mismatch = penalty_full_edge_mismatch + self.penalty_partial_edge_mismatch = penalty_partial_edge_mismatch + self.bonus_partial_edge_match = bonus_partial_edge_match + self.bonus_full_edge_match = bonus_full_edge_match + self.bonus_crossed_group_match = bonus_crossed_group_match + self.bonus_fill_parallel = bonus_fill_parallel + self.forced_non_crossed_edges = forced_non_crossed_edges + self.forced_crossed_edges = forced_crossed_edges + self.check_reachability = check_reachability + self.crossed_chance = crossed_chance + self.crossed_limit = crossed_limit + self.sort_by_edge_sides = sort_by_edge_sides + self.sort_by_max_edges_per_side = sort_by_max_edges_per_side + self.sort_by_piece_size = sort_by_piece_size + self.min_runs = min_runs + self.max_runs = max_runs + self.target_runs_times_successes = target_runs_times_successes + +class LayoutGeneratorResult: + """ + Result object for the layout generation. + """ + __slots__ = ('grid_info', 'score', 'worst_score', 'average_score', 'successes', 'failures') + + def __init__( + self, + grid_info: Optional[GridInfo] = None, + score: int = 0, + worst_score: int = 0, + average_score: float = 0.0, + successes: int = 0, + failures: int = 0 + ): + self.grid_info = grid_info + self.score = score + self.worst_score = worst_score + self.average_score = average_score + self.successes = successes + self.failures = failures + +class PiecePlacementResult: + """ + Result object for the layout generator placement operations. + """ + __slots__ = ('success', 'piece', 'score_major', 'score_minor') + + def __init__( + self, + success: bool = False, + piece: Optional[Piece] = None, + score_major: float = 0, + score_minor: float = 0 + ): + self.success = success + self.piece = piece + self.score_major = score_major + self.score_minor = score_minor + +# ============================================================================ +# GRID INITIALIZATION +# ============================================================================ + +def create_empty_grid_info(edge_connection_seed: float) -> GridInfo: + return GridInfo( + grid=[[[-1] * 8 for _ in range(8)] for _ in range(2)], + north_edges_grid=[[[[] for _ in range(8)] for _ in range(8)] for _ in range(2)], + south_edges_grid=[[[[] for _ in range(8)] for _ in range(8)] for _ in range(2)], + west_edges_grid=[[[[] for _ in range(8)] for _ in range(8)] for _ in range(2)], + east_edges_grid=[[[[] for _ in range(8)] for _ in range(8)] for _ in range(2)], + north_edges_water_grid=[[[[] for _ in range(8)] for _ in range(8)] for _ in range(2)], + south_edges_water_grid=[[[[] for _ in range(8)] for _ in range(8)] for _ in range(2)], + west_edges_water_grid=[[[[] for _ in range(8)] for _ in range(8)] for _ in range(2)], + east_edges_water_grid=[[[[] for _ in range(8)] for _ in range(8)] for _ in range(2)], + crossed_groups=[[-1] * 8 for _ in range(8)], + edge_connection_seed=edge_connection_seed + ) + +def initialize_screens(world: World, player: int) -> Dict[int, Screen]: + overworld_screens: Dict[int, Screen] = {} + screen_edges_map = group_owedges_by_screens(world, player) + + for screen_id in range(0x80): + if screen_id - 0x01 not in large_screen_ids and screen_id - 0x08 not in large_screen_ids and screen_id - 0x09 not in large_screen_ids: + is_vanilla_dark = screen_id >= 0x40 + is_big = screen_id in large_screen_ids + is_flipped = world.owMixed[player] and screen_id in world.owswaps[player][0] + + screen = Screen( + id=screen_id, + big=is_big, + dark_world=not is_vanilla_dark if is_flipped else is_vanilla_dark, + mixed_state="swapped" if is_flipped else "normal" + ) + + if screen_id in screen_edges_map: + for edge in screen_edges_map[screen_id]: + screen.edges[edge.name] = edge + + overworld_screens[screen_id] = screen + + for light_id in range(0x40): + dark_id = light_id + 0x40 + if light_id in overworld_screens: + overworld_screens[light_id].parallel = overworld_screens[dark_id] + overworld_screens[dark_id].parallel = overworld_screens[light_id] + + return overworld_screens + +def group_owedges_by_screens(world: World, player: int) -> Dict[int, List[OWEdge]]: + screen_edges: Dict[int, List[OWEdge]] = {} + edges: List[OWEdge] = world.owedges + + for edge in edges: + # Skip edges that lead to/from special screens + if edge.player == player and not edge.specialEntrance and not edge.specialExit: + owIndex = edge.owIndex + if owIndex not in screen_edges: + screen_edges[owIndex] = [] + screen_edges[owIndex].append(edge) + + return screen_edges + +def initialize_large_screen_data(overworld_screens: Dict[int, Screen]) -> Tuple[Dict[int, Dict], Dict[int, Dict], Dict[int, Dict]]: + i: Dict[int, Dict] = {} + il: Dict[int, Dict] = {} + iw: Dict[int, Dict] = {} + define_large_screen_quadrants(overworld_screens, i, il, iw, 0x00, [], [], [], [], ["Lost Woods EN"], [], ["Lost Woods SW", "Lost Woods SC"], ["Lost Woods SE"]) + define_large_screen_quadrants(overworld_screens, i, il, iw, 0x40, [], [], [], [], ["Skull Woods EN"], [], ["Skull Woods SW", "Skull Woods SC"], ["Skull Woods SE"]) + define_large_screen_quadrants(overworld_screens, i, il, iw, 0x03, [], [], [], [], ["West Death Mountain EN"], ["West Death Mountain ES"], [], []) + define_large_screen_quadrants(overworld_screens, i, il, iw, 0x43, [], [], [], [], ["West Dark Death Mountain EN"], ["West Dark Death Mountain ES"], [], []) + define_large_screen_quadrants(overworld_screens, i, il, iw, 0x05, [], [], ["East Death Mountain WN"], ["East Death Mountain WS"], ["East Death Mountain EN"], [], [], []) + define_large_screen_quadrants(overworld_screens, i, il, iw, 0x45, [], [], ["East Dark Death Mountain WN"], ["East Dark Death Mountain WS"], ["East Dark Death Mountain EN"], [], [], []) + define_large_screen_quadrants(overworld_screens, i, il, iw, 0x18, ["Kakariko NW", "Kakariko NC"], ["Kakariko NE"], [], [], [], ["Kakariko ES"], [], ["Kakariko SE"]) + define_large_screen_quadrants(overworld_screens, i, il, iw, 0x58, ["Village of Outcasts NW", "Village of Outcasts NC"], ["Village of Outcasts NE"], [], [], [], ["Village of Outcasts ES"], [], ["Village of Outcasts SE"]) + define_large_screen_quadrants(overworld_screens, i, il, iw, 0x1B, [], [], ["Hyrule Castle WN"], [], [], ["Hyrule Castle ES"], ["Hyrule Castle SW"], ["Hyrule Castle SE"]) + define_large_screen_quadrants(overworld_screens, i, il, iw, 0x5B, [], [], [], [], [], ["Pyramid ES"], ["Pyramid SW"], ["Pyramid SE"]) + define_large_screen_quadrants(overworld_screens, i, il, iw, 0x1E, [], [], [], [], [], [], ["Eastern Palace SW"], ["Eastern Palace SE"]) + define_large_screen_quadrants(overworld_screens, i, il, iw, 0x5E, [], [], [], [], [], [], ["Palace of Darkness SW"], ["Palace of Darkness SE"]) + define_large_screen_quadrants(overworld_screens, i, il, iw, 0x30, [], [], [], [], [], ["Desert EC", "Desert ES"], [], []) + define_large_screen_quadrants(overworld_screens, i, il, iw, 0x70, [], [], [], [], [], [], [], []) + define_large_screen_quadrants(overworld_screens, i, il, iw, 0x35, ["Lake Hylia NW"], ["Lake Hylia NC", "Lake Hylia NE"], [], ["Lake Hylia WS"], [], ["Lake Hylia EC", "Lake Hylia ES"], [], []) + define_large_screen_quadrants(overworld_screens, i, il, iw, 0x75, ["Ice Lake NW"], ["Ice Lake NC", "Ice Lake NE"], [], ["Ice Lake WS"], [], ["Ice Lake EC", "Ice Lake ES"], [], []) + return (i, il, iw) + +def define_large_screen_quadrants( + overworld_screens: Dict[int, Screen], + large_screen_quadrant_info: Dict[int, Dict], + large_screen_quadrant_info_land: Dict[int, Dict], + large_screen_quadrant_info_water: Dict[int, Dict], + screen_id: int, + north1: List[str], north2: List[str], + west1: List[str], west2: List[str], + east1: List[str], east2: List[str], + south1: List[str], south2: List[str] +) -> None: + """ + Define edge info for large screens + Maps edge names to quadrants (NW, NE, SW, SE) + + Edge names are the actual edge names from OWEdges.py like "Lost Woods SW", "Kakariko NW", etc. + """ + edges = overworld_screens[screen_id].edges + info = { + "NW": {Direction.North: [], Direction.West: [], Direction.East: [], Direction.South: []}, + "NE": {Direction.North: [], Direction.West: [], Direction.East: [], Direction.South: []}, + "SW": {Direction.North: [], Direction.West: [], Direction.East: [], Direction.South: []}, + "SE": {Direction.North: [], Direction.West: [], Direction.East: [], Direction.South: []} + } + info["NW"][Direction.North] = [edges[name] for name in north1] + info["NE"][Direction.North] = [edges[name] for name in north2] + info["NW"][Direction.West] = [edges[name] for name in west1] + info["SW"][Direction.West] = [edges[name] for name in west2] + info["NE"][Direction.East] = [edges[name] for name in east1] + info["SE"][Direction.East] = [edges[name] for name in east2] + info["SW"][Direction.South] = [edges[name] for name in south1] + info["SE"][Direction.South] = [edges[name] for name in south2] + + large_screen_quadrant_info[screen_id] = info + + large_screen_quadrant_info_land[screen_id] = { + "NW": {}, "NE": {}, "SW": {}, "SE": {} + } + large_screen_quadrant_info_water[screen_id] = { + "NW": {}, "NE": {}, "SW": {}, "SE": {} + } + + for quadrant_name in ["NW", "NE", "SW", "SE"]: + for direction in [Direction.North, Direction.West, Direction.East, Direction.South]: + large_screen_quadrant_info_land[screen_id][quadrant_name][direction] = \ + [edge for edge in info[quadrant_name][direction] if edge.terrain != Terrain.Water] + large_screen_quadrant_info_water[screen_id][quadrant_name][direction] = \ + [edge for edge in info[quadrant_name][direction] if edge.terrain == Terrain.Water] + +# ============================================================================ +# PIECE CREATION +# ============================================================================ + +def create_piece_list(world: World, player: int, options: LayoutGeneratorOptions, crossed_group_b: List[int], overworld_screens: Dict[int, Screen], large_screen_quadrant_info: Dict[int, Dict], large_screen_quadrant_info_land: Dict[int, Dict], large_screen_quadrant_info_water: Dict[int, Dict]) -> List[Piece]: + piece_list: List[Piece] = [] + used_screens_set = set() + + all_large_screens = [s for s in overworld_screens.values() if s.big] + all_small_screens = [s for s in overworld_screens.values() if not s.big] + + if world.owParallel[player]: + # In Parallel, only use light world screens + # Each piece will automatically handle both worlds through parallel mechanism + all_large_screens = [s for s in all_large_screens if not s.dark_world] + all_small_screens = [s for s in all_small_screens if not s.dark_world] + + # In Standard mode, screens 0x1B, 0x2B, 0x2C are glued together as a single piece + if world.mode[player] == 'standard': + castle_screen = overworld_screens.get(0x1B) + central_bonk_screen = overworld_screens.get(0x2B) + links_house_screen = overworld_screens.get(0x2C) + + if castle_screen and central_bonk_screen and links_house_screen: + piece = create_piece(world, player, [ + [0x1B, 0x1B], + [0x1B, 0x1B], + [0x2B, 0x2C] + ], overworld_screens) + + if options.large_screen_pool: + piece.restriction = [0x00, 0x03, 0x05, 0x18, 0x1B, 0x1E, 0x30, 0x35] + + piece_list.append(piece) + used_screens_set.add(castle_screen) + used_screens_set.add(central_bonk_screen) + used_screens_set.add(links_house_screen) + + if world.owParallel[player]: + used_screens_set.add(castle_screen.parallel) + used_screens_set.add(central_bonk_screen.parallel) + used_screens_set.add(links_house_screen.parallel) + + # Add large screens + for screen in all_large_screens: + if screen not in used_screens_set: + piece = create_piece(world, player, [[screen.id, screen.id], [screen.id, screen.id]], overworld_screens) + if options.large_screen_pool: + piece.restriction = [0x00, 0x03, 0x05, 0x18, 0x1B, 0x1E, 0x30, 0x35] + piece_list.append(piece) + used_screens_set.add(screen) + if world.owParallel[player]: + used_screens_set.add(screen.parallel) + + # Add small screens + for screen in all_small_screens: + if screen not in used_screens_set: + piece = create_piece(world, player, [[screen.id]], overworld_screens) + if options.large_screen_pool: + piece.restriction = [s.id for s in overworld_screens.values() if not s.big] + piece_list.append(piece) + used_screens_set.add(screen) + if world.owParallel[player]: + used_screens_set.add(screen.parallel) + + # Add piece data + for piece in piece_list: + add_piece_data(world, player, piece, large_screen_quadrant_info, large_screen_quadrant_info_land, large_screen_quadrant_info_water) + # Handle crossed groups + if world.owCrossed[player] == 'polar' and world.owMixed[player]: + piece.crossed_groups = [[] for _ in range(8)] + for k in range(piece.height): + for l in range(piece.width): + piece.crossed_groups[k].append(-1) + screen = piece.main.screens[k][l] + if screen: + piece.crossed_groups[k][l] = 1 if screen.mixed_state == "swapped" else 0 + else: + if piece.parallel and piece.parallel.screens[k][l]: + piece.crossed_groups[k][l] = 1 if piece.parallel.screens[k][l].mixed_state == "swapped" else 0 + if world.owCrossed[player] == 'grouped': + piece.crossed_groups = [[] for _ in range(8)] + for k in range(piece.height): + for l in range(piece.width): + piece.crossed_groups[k].append(-1) + screen_id = piece.main.grid[k][l] + if screen_id != -1: + piece.crossed_groups[k][l] = 1 if screen_id in crossed_group_b else 0 + else: + if piece.parallel and piece.parallel.screens[k][l]: + piece.crossed_groups[k][l] = 1 if piece.parallel.grid[k][l] in crossed_group_b else 0 + + return piece_list + +def create_piece(world: World, player: int, grid: List[List[int]], overworld_screens: Dict[int, Screen]) -> Piece: + """ + Create piece from grid of screen IDs + Takes 2D array of screen IDs and creates main and parallel pieces + """ + piece = Piece( + main=WorldPiece(screens=[]), + width=len(grid[0]), + height=len(grid) + ) + + if world.owParallel[player]: + piece.parallel = WorldPiece(screens=[]) + + found_screens = set() + + for i in range(piece.height): + new_row = [] + new_row_parallel = [] + piece.main.screens.append(new_row) + if world.owParallel[player]: + piece.parallel.screens.append(new_row_parallel) + + for j in range(piece.width): + screen = overworld_screens.get(grid[i][j]) + new_row.append(screen) + if world.owParallel[player]: + new_row_parallel.append(screen.parallel if screen else None) + + if screen and screen not in found_screens: + found_screens.add(screen) + piece.world = 1 if screen.dark_world else 0 + if screen.big and PREVENT_WRAPPED_LARGE_SCREENS: + # For large screens, prevent wrapping at the second row/column + # This ensures the 2x2 piece doesn't split across the grid boundary + if (i + 1) not in piece.invalid_wrap_row: + piece.invalid_wrap_row.append(i + 1) + if (j + 1) not in piece.invalid_wrap_column: + piece.invalid_wrap_column.append(j + 1) + + return piece + +def add_piece_data(world: World, player: int, piece: Piece, large_screen_quadrant_info: Dict[int, Dict], large_screen_quadrant_info_land: Dict[int, Dict], large_screen_quadrant_info_water: Dict[int, Dict]) -> None: + """ + Add computed data to piece + Calls add_piece_grid_info for main and parallel pieces + """ + num_pieces = 2 if piece.parallel else 1 + for p in range(num_pieces): + world_piece = piece.main if p == 0 else piece.parallel + world_piece.width = len(world_piece.screens[0]) + world_piece.height = len(world_piece.screens) + add_piece_grid_info(world, player, world_piece, large_screen_quadrant_info, large_screen_quadrant_info_land, large_screen_quadrant_info_water) + + piece.width = piece.main.width + piece.height = piece.main.height + + # Calculate edge_sides and max_edges_per_side: 0 for multi-cell pieces + if piece.width == 1 and piece.height == 1: + edge_sides = 0 + max_edges_per_side = 0 + # Count edge sides and max edges for main piece and parallel piece (if exists) + for world_piece in ([piece.main, piece.parallel] if piece.parallel else [piece.main]): + north_count = len(world_piece.north_edges[0][0]) + (len(world_piece.north_edges_water[0][0]) if world_piece.north_edges_water else 0) + if north_count > 0: + edge_sides += 1 + max_edges_per_side = max(max_edges_per_side, north_count) + south_count = len(world_piece.south_edges[0][0]) + (len(world_piece.south_edges_water[0][0]) if world_piece.south_edges_water else 0) + if south_count > 0: + edge_sides += 1 + max_edges_per_side = max(max_edges_per_side, south_count) + west_count = len(world_piece.west_edges[0][0]) + (len(world_piece.west_edges_water[0][0]) if world_piece.west_edges_water else 0) + if west_count > 0: + edge_sides += 1 + max_edges_per_side = max(max_edges_per_side, west_count) + east_count = len(world_piece.east_edges[0][0]) + (len(world_piece.east_edges_water[0][0]) if world_piece.east_edges_water else 0) + if east_count > 0: + edge_sides += 1 + max_edges_per_side = max(max_edges_per_side, east_count) + piece.edge_sides = edge_sides + piece.max_edges_per_side = max_edges_per_side + else: + piece.edge_sides = 0 + piece.max_edges_per_side = 0 + +def add_piece_grid_info(world: World, player: int, piece: WorldPiece, large_screen_quadrant_info: Dict[int, Dict], large_screen_quadrant_info_land: Dict[int, Dict], large_screen_quadrant_info_water: Dict[int, Dict]) -> None: + """ + Populate piece edge information + Initializes 8x8 edge arrays and extracts edges from screens + """ + piece.grid = [[] for _ in range(8)] + piece.north_edges = [[] for _ in range(8)] + piece.south_edges = [[] for _ in range(8)] + piece.west_edges = [[] for _ in range(8)] + piece.east_edges = [[] for _ in range(8)] + + if not world.owTerrain[player]: + piece.north_edges_water = [[] for _ in range(8)] + piece.south_edges_water = [[] for _ in range(8)] + piece.west_edges_water = [[] for _ in range(8)] + piece.east_edges_water = [[] for _ in range(8)] + + for k in range(piece.height): + for l in range(piece.width): + piece.grid[k].append(piece.screens[k][l].id if piece.screens[k][l] else -1) + piece.north_edges[k].append([]) + piece.south_edges[k].append([]) + piece.west_edges[k].append([]) + piece.east_edges[k].append([]) + + if not world.owTerrain[player]: + piece.north_edges_water[k].append([]) + piece.south_edges_water[k].append([]) + piece.west_edges_water[k].append([]) + piece.east_edges_water[k].append([]) + + done_large = set() + for k in range(piece.height): + for l in range(piece.width): + screen = piece.screens[k][l] + if not screen: + continue + + if screen.big: + if screen.id not in done_large: + done_large.add(screen.id) + quadrant_info = (large_screen_quadrant_info[screen.id] if world.owTerrain[player] + else large_screen_quadrant_info_land[screen.id]) + + piece.north_edges[k][l] = [e for e in quadrant_info["NW"][Direction.North] if not e.dest] + piece.north_edges[k][l + 1] = [e for e in quadrant_info["NE"][Direction.North] if not e.dest] + piece.south_edges[k + 1][l] = [e for e in quadrant_info["SW"][Direction.South] if not e.dest] + piece.south_edges[k + 1][l + 1] = [e for e in quadrant_info["SE"][Direction.South] if not e.dest] + piece.west_edges[k][l] = [e for e in quadrant_info["NW"][Direction.West] if not e.dest] + piece.west_edges[k + 1][l] = [e for e in quadrant_info["SW"][Direction.West] if not e.dest] + piece.east_edges[k][l + 1] = [e for e in quadrant_info["NE"][Direction.East] if not e.dest] + piece.east_edges[k + 1][l + 1] = [e for e in quadrant_info["SE"][Direction.East] if not e.dest] + + if not world.owTerrain[player]: + quadrant_info = large_screen_quadrant_info_water[screen.id] + piece.north_edges_water[k][l] = [e for e in quadrant_info["NW"][Direction.North] if not e.dest] + piece.north_edges_water[k][l + 1] = [e for e in quadrant_info["NE"][Direction.North] if not e.dest] + piece.south_edges_water[k + 1][l] = [e for e in quadrant_info["SW"][Direction.South] if not e.dest] + piece.south_edges_water[k + 1][l + 1] = [e for e in quadrant_info["SE"][Direction.South] if not e.dest] + piece.west_edges_water[k][l] = [e for e in quadrant_info["NW"][Direction.West] if not e.dest] + piece.west_edges_water[k + 1][l] = [e for e in quadrant_info["SW"][Direction.West] if not e.dest] + piece.east_edges_water[k][l + 1] = [e for e in quadrant_info["NE"][Direction.East] if not e.dest] + piece.east_edges_water[k + 1][l + 1] = [e for e in quadrant_info["SE"][Direction.East] if not e.dest] + else: + for edge in sorted(screen.edges.values(), key=lambda e: e.midpoint): + if not edge.dest: + if edge.direction == Direction.North: + target = piece.north_edges[k][l] if world.owTerrain[player] or edge.terrain != Terrain.Water else piece.north_edges_water[k][l] + target.append(edge) + elif edge.direction == Direction.South: + target = piece.south_edges[k][l] if world.owTerrain[player] or edge.terrain != Terrain.Water else piece.south_edges_water[k][l] + target.append(edge) + elif edge.direction == Direction.West: + target = piece.west_edges[k][l] if world.owTerrain[player] or edge.terrain != Terrain.Water else piece.west_edges_water[k][l] + target.append(edge) + elif edge.direction == Direction.East: + target = piece.east_edges[k][l] if world.owTerrain[player] or edge.terrain != Terrain.Water else piece.east_edges_water[k][l] + target.append(edge) + +# ============================================================================ +# PLACEMENT ALGORITHM +# ============================================================================ + +def random_place_piece( + world: World, + player: int, + grid_info: GridInfo, + options: LayoutGeneratorOptions, + pieces: List[Piece], + ignore_bonus_points: bool +) -> PiecePlacementResult: + """ + Core placement algorithm + Evaluates all valid positions and scores each based on edge compatibility + Performance is critical within these deeply nested loops, every optimization matters + """ + use_crossed_groups = (world.owCrossed[player] == 'polar' and world.owMixed[player]) or world.owCrossed[player] == 'grouped' + is_unrestricted_crossed = world.owCrossed[player] == 'unrestricted' + keep_similar = ENABLE_KEEP_SIMILAR_SPECIAL_HANDLING and world.owKeepSimilar[player] + + width = 8 + height = 8 + horizontal_wrap = options.horizontal_wrap + vertical_wrap = options.vertical_wrap + distortion_chance = options.distortion_chance + use_distortion = distortion_chance > 0 + crossed_chance = options.crossed_chance + crossworld_weights = (1 - crossed_chance, crossed_chance) if is_unrestricted_crossed else (1, 0) + if not is_unrestricted_crossed: + crossed_score_weight = 1 + penalty_full_edge_mismatch = options.penalty_full_edge_mismatch + penalty_partial_edge_mismatch = options.penalty_partial_edge_mismatch + bonus_partial_edge_match = 0 if ignore_bonus_points else options.bonus_partial_edge_match + bonus_full_edge_match = 0 if ignore_bonus_points else options.bonus_full_edge_match + bonus_crossed_group_match = 0 if ignore_bonus_points else options.bonus_crossed_group_match + bonus_fill_parallel = 0 if ignore_bonus_points else options.bonus_fill_parallel + can_stop_early = penalty_full_edge_mismatch >= 0 and penalty_partial_edge_mismatch >= 0 + + grid = grid_info.grid + crossed_groups = grid_info.crossed_groups + north_edges_grid = grid_info.north_edges_grid + south_edges_grid = grid_info.south_edges_grid + west_edges_grid = grid_info.west_edges_grid + east_edges_grid = grid_info.east_edges_grid + north_edges_water_grid = grid_info.north_edges_water_grid + south_edges_water_grid = grid_info.south_edges_water_grid + west_edges_water_grid = grid_info.west_edges_water_grid + east_edges_water_grid = grid_info.east_edges_water_grid + + best_choices = [] + max_score_major = -1000000 + max_score_minor = -1000000 + + for c, piece in enumerate(pieces): + piece_main = piece.main + piece_parallel = piece.parallel + wrld = piece.world + invalid_wrap_row = piece.invalid_wrap_row + invalid_wrap_column = piece.invalid_wrap_column + restriction = piece.restriction + piece_width = piece.width + piece_height = piece.height + piece_crossed_groups = piece.crossed_groups + + grid_main_world = grid[wrld] + grid_other_world = grid[1 - wrld] + + i_range = height if vertical_wrap else height - piece_height + 1 + for i in range(i_range): + if i >= height - piece_height + 1 and (height - i) in invalid_wrap_row: + continue + + j_range = width if horizontal_wrap else width - piece_width + 1 + for j in range(j_range): + if j >= width - piece_width + 1 and (width - j) in invalid_wrap_column: + continue + if restriction and (i * 8 + j) not in restriction: + continue + + # Check for overlap + overlap = False + for k in range(piece_height): + row_idx = (i + k) % height + for l in range(piece_width): + col_idx = (j + l) % width + + if grid_main_world[row_idx][col_idx] != -1 and piece_main.screens[k][l]: + overlap = True + break + + if use_crossed_groups and crossed_groups[row_idx][col_idx] != -1 and crossed_groups[row_idx][col_idx] != piece_crossed_groups[k][l]: + overlap = True + break + + if piece_parallel and grid_other_world[row_idx][col_idx] != -1 and piece_parallel.screens[k][l]: + overlap = True + break + + if not overlap: + score_major = 0 + score_minor = 0 + + # Calculate scores based on edge compatibility + for k in range(piece_height): + row_idx = (i + k) % height + row_above = (i + k + height - 1) % height + row_below = (i + k + 1) % height + i_plus_k = i + k + + for l in range(piece_width): + col_idx = (j + l) % width + col_left = (j + l + width - 1) % width + col_right = (j + l + 1) % width + j_plus_l = j + l + + num_pieces = 2 if piece_parallel else 1 + for p in range(num_pieces): + world_piece = piece_main if p == 0 else piece_parallel + cw = wrld if p == 0 else 1 - wrld + + if not world_piece.screens[k][l]: + continue + + # Add small bias when the crossed group is already determined and matches the piece to avoid issues later on + if use_crossed_groups and not piece_parallel and crossed_groups[row_idx][col_idx] == piece_crossed_groups[k][l]: + score_minor += bonus_crossed_group_match + + if not piece_parallel and grid_other_world[row_idx][col_idx] != -1: + score_minor += bonus_fill_parallel + + for terrain in range(1 if world.owTerrain[player] else 2): + north_piece = world_piece.north_edges if terrain == 0 else world_piece.north_edges_water + south_piece = world_piece.south_edges if terrain == 0 else world_piece.south_edges_water + west_piece = world_piece.west_edges if terrain == 0 else world_piece.west_edges_water + east_piece = world_piece.east_edges if terrain == 0 else world_piece.east_edges_water + + north_edges = north_edges_grid if terrain == 0 else north_edges_water_grid + south_edges = south_edges_grid if terrain == 0 else south_edges_water_grid + west_edges = west_edges_grid if terrain == 0 else west_edges_water_grid + east_edges = east_edges_grid if terrain == 0 else east_edges_water_grid + + # Check boundary edges + if not vertical_wrap and i_plus_k == 0 and (not use_distortion or distortion_chance <= random.random()): + if north_piece[k][l]: + score_major -= penalty_full_edge_mismatch + else: + score_minor += bonus_full_edge_match + + if not vertical_wrap and i_plus_k == height - 1 and (not use_distortion or distortion_chance <= random.random()): + if south_piece[k][l]: + score_major -= penalty_full_edge_mismatch + else: + score_minor += bonus_full_edge_match + + if not horizontal_wrap and j_plus_l == 0 and (not use_distortion or distortion_chance <= random.random()): + if west_piece[k][l]: + score_major -= penalty_full_edge_mismatch + else: + score_minor += bonus_full_edge_match + + if not horizontal_wrap and j_plus_l == width - 1 and (not use_distortion or distortion_chance <= random.random()): + if east_piece[k][l]: + score_major -= penalty_full_edge_mismatch + else: + score_minor += bonus_full_edge_match + + for other_world_index in range(2 if is_unrestricted_crossed else 1): + # Check neighbor compatibility (north) + if is_unrestricted_crossed: + w = cw if other_world_index == 0 else 1 - cw + crossed_score_weight = crossworld_weights[other_world_index] + elif use_crossed_groups and crossed_groups[row_above][col_idx] != piece_crossed_groups[k][l]: + w = 1 - cw + else: + w = cw + + if (i_plus_k != 0 or vertical_wrap) and grid[w][row_above][col_idx] != -1 and (not use_distortion or distortion_chance <= random.random()): + piece_edges = len(north_piece[k][l]) + grid_edges = len(south_edges[w][row_above][col_idx]) + if piece_edges == grid_edges: + score_minor += bonus_full_edge_match * crossed_score_weight + elif not keep_similar and ((piece_edges == 0) == (grid_edges == 0)): + score_minor += bonus_partial_edge_match * crossed_score_weight + score_major -= penalty_partial_edge_mismatch * crossed_score_weight + else: + score_major -= penalty_full_edge_mismatch * crossed_score_weight + + # Check south neighbor + if is_unrestricted_crossed: + w = cw if other_world_index == 0 else 1 - cw + crossed_score_weight = crossworld_weights[other_world_index] + elif use_crossed_groups and crossed_groups[row_below][col_idx] != piece_crossed_groups[k][l]: + w = 1 - cw + else: + w = cw + + if (i_plus_k != height - 1 or vertical_wrap) and grid[w][row_below][col_idx] != -1 and (not use_distortion or distortion_chance <= random.random()): + piece_edges = len(south_piece[k][l]) + grid_edges = len(north_edges[w][row_below][col_idx]) + if piece_edges == grid_edges: + score_minor += bonus_full_edge_match * crossed_score_weight + elif not keep_similar and ((piece_edges == 0) == (grid_edges == 0)): + score_minor += bonus_partial_edge_match * crossed_score_weight + score_major -= penalty_partial_edge_mismatch * crossed_score_weight + else: + score_major -= penalty_full_edge_mismatch * crossed_score_weight + + # Check west neighbor + if is_unrestricted_crossed: + w = cw if other_world_index == 0 else 1 - cw + crossed_score_weight = crossworld_weights[other_world_index] + elif use_crossed_groups and crossed_groups[row_idx][col_left] != piece_crossed_groups[k][l]: + w = 1 - cw + else: + w = cw + + if (j_plus_l != 0 or horizontal_wrap) and grid[w][row_idx][col_left] != -1 and (not use_distortion or distortion_chance <= random.random()): + piece_edges = len(west_piece[k][l]) + grid_edges = len(east_edges[w][row_idx][col_left]) + if piece_edges == grid_edges: + score_minor += bonus_full_edge_match * crossed_score_weight + elif not keep_similar and ((piece_edges == 0) == (grid_edges == 0)): + score_minor += bonus_partial_edge_match * crossed_score_weight + score_major -= penalty_partial_edge_mismatch * crossed_score_weight + else: + score_major -= penalty_full_edge_mismatch * crossed_score_weight + + # Check east neighbor + if is_unrestricted_crossed: + w = cw if other_world_index == 0 else 1 - cw + crossed_score_weight = crossworld_weights[other_world_index] + elif use_crossed_groups and crossed_groups[row_idx][col_right] != piece_crossed_groups[k][l]: + w = 1 - cw + else: + w = cw + + if (j_plus_l != width - 1 or horizontal_wrap) and grid[w][row_idx][col_right] != -1 and (not use_distortion or distortion_chance <= random.random()): + piece_edges = len(east_piece[k][l]) + grid_edges = len(west_edges[w][row_idx][col_right]) + if piece_edges == grid_edges: + score_minor += bonus_full_edge_match * crossed_score_weight + elif not keep_similar and ((piece_edges == 0) == (grid_edges == 0)): + score_minor += bonus_partial_edge_match * crossed_score_weight + score_major -= penalty_partial_edge_mismatch * crossed_score_weight + else: + score_major -= penalty_full_edge_mismatch * crossed_score_weight + + if can_stop_early and score_major < max_score_major: + break + # This is so an we can break out of all remaining checks for the current placement option + else: + continue + break + else: + continue + break + else: + continue + break + else: + continue + break + + if score_major == max_score_major and score_minor == max_score_minor: + best_choices.append((c, i, j)) + + if score_major > max_score_major or (score_major == max_score_major and score_minor > max_score_minor): + max_score_major = score_major + max_score_minor = score_minor + best_choices = [(c, i, j)] + + if not best_choices: + return PiecePlacementResult(success=False, piece=None, score_major=0, score_minor=0) + + # Select random best choice + piece_index, row, column = random.choice(best_choices) + used_score_major = max_score_major + used_score_minor = max_score_minor + + piece = pieces[piece_index] + wrld = piece.world + + # Place the piece on the grid + for k in range(piece.height): + row_idx = (row + k) % height + for l in range(piece.width): + col_idx = (column + l) % width + num_pieces = 2 if piece.parallel else 1 + for p in range(num_pieces): + world_piece = piece.main if p == 0 else piece.parallel + w = wrld if p == 0 else 1 - wrld + + grid[w][row_idx][col_idx] = world_piece.grid[k][l] + north_edges_grid[w][row_idx][col_idx] = world_piece.north_edges[k][l] + south_edges_grid[w][row_idx][col_idx] = world_piece.south_edges[k][l] + west_edges_grid[w][row_idx][col_idx] = world_piece.west_edges[k][l] + east_edges_grid[w][row_idx][col_idx] = world_piece.east_edges[k][l] + + if not world.owTerrain[player]: + north_edges_water_grid[w][row_idx][col_idx] = world_piece.north_edges_water[k][l] + south_edges_water_grid[w][row_idx][col_idx] = world_piece.south_edges_water[k][l] + west_edges_water_grid[w][row_idx][col_idx] = world_piece.west_edges_water[k][l] + east_edges_water_grid[w][row_idx][col_idx] = world_piece.east_edges_water[k][l] + + if use_crossed_groups: + crossed_groups[row_idx][col_idx] = piece_crossed_groups[k][l] + + return PiecePlacementResult(success=True, piece=piece, score_major=used_score_major, score_minor=used_score_minor) + +def get_random_layout(world: World, player: int, connected_edges_cache: List[str], pieces_to_place: List[Piece], options: LayoutGeneratorOptions, prio_edges: List[str], overworld_screens: Dict[int, Screen]) -> LayoutGeneratorResult: + total_score = 0 + best_score = -1000000 + worst_score = 1000000 + best_grid_info = None + + successes = 0 + failures = 0 + run = 0 + while run < options.min_runs or (run * successes < options.target_runs_times_successes and run < options.max_runs): + run += 1 + connected_edges = connected_edges_cache.copy() + piece_list = pieces_to_place.copy() + + grid_info = create_empty_grid_info(random.random()) + for piece in piece_list: + piece.delay = 0 + + major_score = 0 + + # Order pieces by size, max_edges_per_side, edge_sides, and randomness + random.shuffle(piece_list) + if options.sort_by_edge_sides: + piece_list.sort(key=lambda p: p.edge_sides) + if options.sort_by_max_edges_per_side: + piece_list.sort(key=lambda p: p.max_edges_per_side, reverse=True) + if options.sort_by_piece_size: + piece_list.sort(key=lambda p: p.width * p.height, reverse=True) + if options.random_order > 0: + for i, piece in enumerate(piece_list): + piece.order = i + random.random() * (options.random_order + 1) + piece_list.sort(key=lambda p: p.order) + + # Place pieces + placed_pieces = set() + while piece_list: + pieces = [piece_list[0]] + if piece_list[0].delay < options.max_delay: + for i in range(1, min(options.multi_choice, len(piece_list))): + pieces.append(piece_list[i]) + + result = random_place_piece(world, player, grid_info, options, pieces, len(placed_pieces) < options.first_ignore_bonus_points) + + if not result.success: + failures += 1 + break + + if result.piece != piece_list[0]: + piece_list[0].delay += 1 + + placed_pieces.add(result.piece) + piece_list.remove(result.piece) + major_score += result.score_major + else: + # Successfully placed all pieces + if options.check_reachability: + disabled_count = connect_edges_for_screen_layout(world, player, grid_info, options, connected_edges, prio_edges, overworld_screens, False) + valid_layout = validate_layout(world, player) + # Clean up connected entrances and edges + for edge_name in connected_edges: + if edge_name not in connected_edges_cache: + entrance = world.get_entrance(edge_name, player) + entrance.connected_region.entrances.remove(entrance) + entrance.connected_region = None + edge = world.get_owedge(edge_name, player) + edge.dest = None + if not valid_layout: + failures += 1 + continue + logging.getLogger('').debug("Found valid layout with " + str(disabled_count)+ " disabled edges") + successes += 1 + score = -disabled_count + else: + successes += 1 + score = major_score + + total_score += score + + if score > best_score: + best_score = score + best_grid_info = grid_info + + if score < worst_score: + worst_score = score + + if best_grid_info is None: + return LayoutGeneratorResult( + successes=successes, + failures=failures + ) + + return LayoutGeneratorResult( + grid_info=best_grid_info, + score=best_score, + worst_score=worst_score, + average_score=total_score / successes, + successes=successes, + failures=failures + ) + +def get_prioritized_edges(world: World, player: int) -> List[str]: + prio_edges = [] + if world.accessibility[player] != 'none': + prio_edges += ['Desert EC'] + if not world.is_tile_swapped(0x3A, player): + prio_edges += ['Desert Pass WC'] + if world.is_tile_swapped(0x13, player): + prio_edges += ['Sanctuary WN'] + if world.owParallel[player]: + prio_edges += ['Dark Chapel WN'] + if world.owParallel[player]: + prio_edges += ['Flute Boy SC', 'Stumpy SC'] + else: + if world.is_tile_swapped(0x2A, player): + prio_edges += ['Flute Boy SC'] + else: + prio_edges += ['Stumpy SC'] + if world.owTerrain[player]: + prio_edges += ['Octoballoon NW', 'Bomber Corner NW'] + if world.is_tile_swapped(0x2D, player): + prio_edges += ['Stone Bridge EC'] + if world.owParallel[player]: + prio_edges += ['Hammer Bridge EC'] + if not world.is_tile_swapped(0x35, player): + prio_edges += ['Ice Lake ES'] + if world.owParallel[player]: + prio_edges += ['Lake Hylia ES'] + return prio_edges + +escape_screen_ids = set([0x1B, 0x2B, 0x2C]) + +def connect_edges_for_screen_layout(world: World, player: int, grid_info: GridInfo, options: LayoutGeneratorOptions, connected_edges: List[str], prio_edges: List[str], overworld_screens: Dict[int, Screen], final_placement: bool) -> int: + use_crossed_groups = (world.owCrossed[player] == 'polar' and world.owMixed[player]) or world.owCrossed[player] == 'grouped' + is_unrestricted_crossed = world.owCrossed[player] == 'unrestricted' + is_standard = world.mode[player] == 'standard' + edge_random = _random.Random(grid_info.edge_connection_seed) + left_to_connect: List[Direction, int, int, int, int] = [] + make_non_crossed = set() + make_crossed = set() + make_disabled = set() + undecided = [] + + # Collect information about all edge sets to connect + for dir in [Direction.East, Direction.South]: + for i in range(7 if dir == Direction.South and not options.vertical_wrap else 8): + for j in range(7 if dir == Direction.East and not options.horizontal_wrap else 8): + forced_escape = False + forced_non_crossed = False + forced_crossed = False + has_edges_1 = [False, False] + has_edges_2 = [False, False] + for w in range(2): + for terrain in range(1 if world.owTerrain[player] else 2): + left_to_connect.append((dir, w, i, j, terrain)) + + if is_unrestricted_crossed: + if dir == Direction.East: + west_edges = grid_info.west_edges_grid if terrain == 0 else grid_info.west_edges_water_grid + east_edges = grid_info.east_edges_grid if terrain == 0 else grid_info.east_edges_water_grid + edge_set_1 = east_edges[w][i][j] + edge_set_2 = west_edges[w][i][(j + 1) % 8] + if is_standard and grid_info.grid[w][i][j] in escape_screen_ids and grid_info.grid[w][i][(j + 1) % 8] in escape_screen_ids: + forced_escape = True + else: + north_edges = grid_info.north_edges_grid if terrain == 0 else grid_info.north_edges_water_grid + south_edges = grid_info.south_edges_grid if terrain == 0 else grid_info.south_edges_water_grid + edge_set_1 = south_edges[w][i][j] + edge_set_2 = north_edges[w][(i + 1) % 8][j] + if is_standard and grid_info.grid[w][i][j] in escape_screen_ids and grid_info.grid[w][(i + 1) % 8][j] in escape_screen_ids: + forced_escape = True + if any(edge for edge in edge_set_1 if edge.name in options.forced_non_crossed_edges) or any(edge for edge in edge_set_2 if edge.name in options.forced_non_crossed_edges): + forced_non_crossed = True + if any(edge for edge in edge_set_1 if edge.name in options.forced_crossed_edges) or any(edge for edge in edge_set_2 if edge.name in options.forced_crossed_edges): + forced_crossed = True + if edge_set_1: + has_edges_1[w] = True + if edge_set_2: + has_edges_2[w] = True + if is_unrestricted_crossed: + if forced_escape: + make_non_crossed.add((dir, i, j)) + elif forced_non_crossed and forced_crossed: + make_disabled.add((dir, i, j)) + elif forced_non_crossed: + make_non_crossed.add((dir, i, j)) + elif forced_crossed: + make_crossed.add((dir, i, j)) + elif has_edges_1[0] != has_edges_1[1] and has_edges_2[0] != has_edges_2[1]: + # On both sides of the transition only one world has any edges, so make sure we can connect those + (make_non_crossed if has_edges_1[0] == has_edges_2[0] else make_crossed).add((dir, i, j)) + else: + undecided.append((dir, i, j)) + + if is_unrestricted_crossed: + # Make outstanding crossed choices + if options.crossed_limit > 0: + edge_random.shuffle(undecided) + remaining_crossed_edges = len(undecided) if options.crossed_limit < 0 else max(0, options.crossed_limit - len(make_crossed)) + if remaining_crossed_edges > 0: + for x in undecided: + if edge_random.random() < options.crossed_chance: + make_crossed.add(x) + remaining_crossed_edges -= 1 + if remaining_crossed_edges == 0: + break + + # Connect the edge sets + for dir, w, i, j, terrain in left_to_connect: + if not is_unrestricted_crossed or not (dir, i, j) in make_disabled: + world_idx = w + if dir == Direction.East: + edges_1 = grid_info.east_edges_grid if terrain == 0 else grid_info.east_edges_water_grid + edges_2 = grid_info.west_edges_grid if terrain == 0 else grid_info.west_edges_water_grid + if use_crossed_groups and grid_info.crossed_groups[i][j] != grid_info.crossed_groups[i][(j + 1) % 8]: + world_idx = 1 - w + elif is_unrestricted_crossed and (dir, i, j) in make_crossed: + world_idx = 1 - w + connect_edge_sets(world, player, edges_1[w][i][j], edges_2[world_idx][i][(j + 1) % 8], edge_random, connected_edges, prio_edges, final_placement) + else: + edges_1 = grid_info.south_edges_grid if terrain == 0 else grid_info.south_edges_water_grid + edges_2 = grid_info.north_edges_grid if terrain == 0 else grid_info.north_edges_water_grid + if use_crossed_groups and grid_info.crossed_groups[i][j] != grid_info.crossed_groups[(i + 1) % 8][j]: + world_idx = 1 - w + elif is_unrestricted_crossed and (dir, i, j) in make_crossed: + world_idx = 1 - w + connect_edge_sets(world, player, edges_1[w][i][j], edges_2[world_idx][(i + 1) % 8][j], edge_random, connected_edges, prio_edges, final_placement) + + # Count disabled edges + disabled_count = 0 + for screen in overworld_screens.values(): + for edge in screen.edges.values(): + if not edge.dest: + disabled_count += 1 + return disabled_count + +def connect_edge_sets(world: World, player: int, edge_set_1: List[OWEdge], edge_set_2: List[OWEdge], edge_random: _random.Random, connected_edges: List[str], prio_edges: List[str], final_placement: bool) -> None: + if edge_set_1 and edge_set_2: + if world.owParallel[player]: + # Make sure that we do not connect parallel with non-parallel edges + parallel_edge_set_1 = [edge for edge in edge_set_1 if edge.parallel] + parallel_edge_set_2 = [edge for edge in edge_set_2 if edge.parallel] + if any(parallel_edge_set_1) and any(parallel_edge_set_2): + # Special case for screens that have both types of edges in the same direction (Dig Game and Frog) + if len(edge_set_1) == 2 and len(edge_set_2) == 2 and not edge_set_1[0].parallel and edge_set_1[1].parallel and not edge_set_2[0].parallel and edge_set_2[1].parallel: + connect_two_way(world, edge_set_1[0].name, edge_set_2[0].name, player, connected_edges, final_placement) + # Check if the edges already got connected when handling the other world + if any(edge for edge in parallel_edge_set_1 if edge.dest) or any(edge for edge in parallel_edge_set_2 if edge.dest): + return + # Special case for Maze Race and Kakariko Suburb with Keep Similar, only connect those when handling the other world + if ENABLE_KEEP_SIMILAR_SPECIAL_HANDLING and world.owKeepSimilar[player] and ((len(edge_set_1) == 1 and (edge_set_1[0].name == 'Maze Race ES' or edge_set_1[0].name == 'Kakariko Suburb WS')) or (len(edge_set_2) == 1 and (edge_set_2[0].name == 'Maze Race ES' or edge_set_2[0].name == 'Kakariko Suburb WS'))): + return + edge_set_1 = parallel_edge_set_1 + edge_set_2 = parallel_edge_set_2 + else: + non_parallel_edge_set_1 = [edge for edge in edge_set_1 if not edge.parallel] + non_parallel_edge_set_2 = [edge for edge in edge_set_2 if not edge.parallel] + if not any(non_parallel_edge_set_1) or not any(non_parallel_edge_set_2): + return + edge_set_1 = non_parallel_edge_set_1 + edge_set_2 = non_parallel_edge_set_2 + if len(edge_set_1) == len(edge_set_2): + for k in range(len(edge_set_1)): + connect_two_way(world, edge_set_1[k].name, edge_set_2[k].name, player, connected_edges, final_placement) + elif not ENABLE_KEEP_SIMILAR_SPECIAL_HANDLING or not world.owKeepSimilar[player]: + if len(edge_set_1) < len(edge_set_2): + edge_set_1, edge_set_2 = edge_set_2, edge_set_1 + # Not all edges from edge_set_1 can get connected + prio_set = [edge for edge in edge_set_1 if edge.name in prio_edges] + if len(prio_set) == len(edge_set_2): + for k in range(len(prio_set)): + connect_two_way(world, prio_set[k].name, edge_set_2[k].name, player, connected_edges, final_placement) + elif len(prio_set) < len(edge_set_2): + unconnected_edges = edge_random.sample([edge.name for edge in edge_set_1 if edge.name not in prio_edges], len(edge_set_1) - len(edge_set_2)) + edges_to_connect = [edge for edge in edge_set_1 if edge.name not in unconnected_edges] + for k in range(len(edge_set_2)): + connect_two_way(world, edges_to_connect[k].name, edge_set_2[k].name, player, connected_edges, final_placement) + else: + raise Exception("There should never be multiple edges with high priority in an edge set") + +# ============================================================================ +# GRID FORMATTING +# ============================================================================ + +def format_grid_for_spoiler(grid: List[List[int]]) -> str: + lines = [] + header = " " + for col in range(8): + header += f" {col} " + lines.append(header) + + for row in range(8): + border_line = " +" + for col in range(8): + if row > 0 and is_same_large_screen(grid, row, col, row - 1, col): + border_line += " " + else: + border_line += "--" + + # Check if we need a corner or continuation + if col < 7: + has_horizontal_left = row == 0 or not is_same_large_screen(grid, row, col, row - 1, col) + has_horizontal_right = row == 0 or not is_same_large_screen(grid, row, col + 1, row - 1, col + 1) + has_vertical_top = row == 0 or not is_same_large_screen(grid, row - 1, col, row - 1, col + 1) + has_vertical_bottom = not is_same_large_screen(grid, row, col, row, col + 1) + + if has_vertical_bottom or has_vertical_top: + if has_horizontal_left or has_horizontal_right: + border_line += "+" + else: + border_line += "|" + else: + if has_horizontal_left or has_horizontal_right: + border_line += "-" + else: + border_line += " " + else: + border_line += "+" + + lines.append(border_line) + + row_name = "ABCDEFGH"[row] + content_line = f"{row_name}({row * 8:02X})|" + for col in range(8): + screen_id = grid[row][col] + if screen_id == -1: + content_line += "--" + else: + content_line += f"{screen_id:02X}" + + # Check if we need a vertical separator after this cell + if col < 7: + if is_same_large_screen(grid, row, col, row, col + 1): + content_line += " " + else: + content_line += "|" + else: + content_line += "|" + + lines.append(content_line) + + bottom_border = " +" + for col in range(8): + bottom_border += "--" + if col < 7: + # Check if the bottom cells are part of the same large screen + if is_same_large_screen(grid, 7, col, 7, col + 1): + bottom_border += "-" + else: + bottom_border += "+" + else: + bottom_border += "+" + lines.append(bottom_border) + return "\n".join(lines) + +def is_same_large_screen(grid: List[List[int]], row1: int, col1: int, row2: int, col2: int) -> bool: + id1 = grid[row1 % 8][col1 % 8] + id2 = grid[row2 % 8][col2 % 8] + if id1 == -1 or id2 == -1: + return False + return id1 == id2 and id1 in large_screen_ids + +# ============================================================================ +# MAIN EXECUTION +# ============================================================================ + +def generate_random_grid_layout(world: World, player: int, connected_edges: List[str], crossed_group_b: List[int], forced_non_crossed: Set[str], forced_crossed: Set[str], crossed_limit: int, crossed_chance: float): + """Main execution function""" + import time + + horizontal_wrap = False + vertical_wrap = False + if world.customizer: + grid_options = world.customizer.get_owgrid() + if grid_options and player in grid_options: + grid_options = grid_options[player] + horizontal_wrap = 'wrap_horizontal' in grid_options and grid_options['wrap_horizontal'] == True + vertical_wrap = 'wrap_vertical' in grid_options and grid_options['wrap_vertical'] == True + + first_ignore_bonus = 2 + if not world.owParallel[player]: + first_ignore_bonus *= 2 + if world.owCrossed[player] == 'unrestricted': + first_ignore_bonus *= 2 + options = LayoutGeneratorOptions( + horizontal_wrap=horizontal_wrap, + vertical_wrap=vertical_wrap, + large_screen_pool=False, + distortion_chance=0.0, + random_order=6 if world.owParallel[player] else 12, + multi_choice=1, + max_delay=10, + penalty_full_edge_mismatch=1, + penalty_partial_edge_mismatch=1, + bonus_partial_edge_match=1, + bonus_full_edge_match=1, + bonus_crossed_group_match=1, + bonus_fill_parallel=1 if world.owCrossed[player] == 'unrestricted' else 0, + first_ignore_bonus_points=first_ignore_bonus, + forced_non_crossed_edges=forced_non_crossed, + forced_crossed_edges=forced_crossed, + crossed_chance=crossed_chance, + crossed_limit=crossed_limit, + check_reachability=True, + sort_by_edge_sides=world.owParallel[player] or not world.owTerrain[player], + sort_by_max_edges_per_side=False, + sort_by_piece_size=True, + min_runs=100, + max_runs=10000, + target_runs_times_successes=5000 + ) + + overworld_screens = initialize_screens(world, player) + large_screen_quadrant_info, large_screen_quadrant_info_land, large_screen_quadrant_info_water = initialize_large_screen_data(overworld_screens) + prio_edges = get_prioritized_edges(world, player) + pieces_to_place = create_piece_list(world, player, options, crossed_group_b, overworld_screens, large_screen_quadrant_info, large_screen_quadrant_info_land, large_screen_quadrant_info_water) + + start_time = time.time() + result = get_random_layout(world, player, connected_edges, pieces_to_place, options, prio_edges, overworld_screens) + elapsed_time = time.time() - start_time + + if result.grid_info: + connect_edges_for_screen_layout(world, player, result.grid_info, options, connected_edges, prio_edges, overworld_screens, True) + grid = result.grid_info.grid + + # Make new grid containing cell IDs for the overworld map + map_grid = copy.deepcopy(grid) + for w in range(2): + for i in range(8): + for j in range(8): + screen_id = map_grid[w][i][j] + if screen_id in large_screen_ids and map_grid[w][i][(j + 1) % 8] == screen_id and map_grid[w][(i + 1) % 8][j] == screen_id and map_grid[w][(i + 1) % 8][(j + 1) % 8] == screen_id: + map_grid[w][i][(j + 1) % 8] = screen_id + 0x01 + map_grid[w][(i + 1) % 8][j] = screen_id + 0x08 + map_grid[w][(i + 1) % 8][(j + 1) % 8] = screen_id + 0x09 + world.owgrid[player] = map_grid + world.owlayoutmap_lw[player] = {id & 0xBF: i for i, id in enumerate(sum(map_grid[0], []))} + world.owlayoutmap_dw[player] = {id & 0xBF: i for i, id in enumerate(sum(map_grid[1], []))} + + world.spoiler.set_map('layout_grid_lw', format_grid_for_spoiler(grid[0]), grid[0], player) + if not world.owParallel[player]: + world.spoiler.set_map('layout_grid_dw', format_grid_for_spoiler(grid[1]), grid[1], player) + + logger = logging.getLogger('') + logger.debug(f"\nLayout generation statistics:") + logger.debug(f" Best score: {result.score}") + logger.debug(f" Worst score: {result.worst_score}") + logger.debug(f" Average score: {result.average_score:.2f}") + logger.debug(f" Successes: {result.successes}") + logger.debug(f" Failures: {result.failures}") + logger.debug(f" Generation time: {elapsed_time:.3f}s") + + if DRAW_IMAGE: + logger.debug("Creating layout visualization...") + try: + from source.overworld.LayoutVisualizer import visualize_layout + visualize_layout(grid, "visualizations", overworld_screens, large_screen_quadrant_info) + except Exception as e: + logger.warning(f"Warning: Could not create visualization: {e}") + else: + raise Exception(f"Layout generation FAILED after {result.failures} attempts and {elapsed_time:.3f} seconds") \ No newline at end of file diff --git a/source/overworld/LayoutVisualizer.py b/source/overworld/LayoutVisualizer.py new file mode 100644 index 00000000..8dbdea34 --- /dev/null +++ b/source/overworld/LayoutVisualizer.py @@ -0,0 +1,473 @@ +import logging +import os +from datetime import datetime +from typing import Dict, List +from PIL import Image, ImageDraw +from BaseClasses import Direction, OWEdge +from source.overworld.LayoutGenerator import Screen + +def get_edge_lists(grid: List[List[List[int]]], + overworld_screens: Dict[int, Screen], + large_screen_quadrant_info: Dict[int, Dict]) -> Dict: + """ + Get list of edges for each cell and direction. + + Args: + grid: 3D list [world][row][col] containing screen IDs + overworld_screens: Dict of screen_id -> Screen objects + large_screen_quadrant_info: Dict of screen_id -> quadrant info for large screens + + Returns: + Dict mapping (world, row, col, direction) -> list of edges + Each edge has a .dest property (None if unconnected) + """ + GRID_SIZE = 8 + edge_lists = {} + + # Large screen base IDs + large_screen_base_ids = [0x00, 0x03, 0x05, 0x18, 0x1B, 0x1E, 0x30, 0x35, + 0x40, 0x43, 0x45, 0x58, 0x5B, 0x5E, 0x70, 0x75] + + for world_idx in range(2): + # Build a map of screen_id -> list of (row, col) positions for large screens + large_screen_positions = {} + for row in range(GRID_SIZE): + for col in range(GRID_SIZE): + screen_id = grid[world_idx][row][col] + if screen_id != -1 and screen_id in large_screen_base_ids: + if screen_id not in large_screen_positions: + large_screen_positions[screen_id] = [] + large_screen_positions[screen_id].append((row, col)) + + for row in range(GRID_SIZE): + for col in range(GRID_SIZE): + screen_id = grid[world_idx][row][col] + + if screen_id == -1: + # Empty cell - no edges + for direction in [Direction.North, Direction.South, Direction.East, Direction.West]: + edge_lists[(world_idx, row, col, direction)] = [] + continue + + screen = overworld_screens.get(screen_id) + if not screen: + for direction in [Direction.North, Direction.South, Direction.East, Direction.West]: + edge_lists[(world_idx, row, col, direction)] = [] + continue + + is_large = screen_id in large_screen_base_ids + + if is_large: + # For large screens, determine which quadrant this cell is + # Find all positions of this large screen and determine quadrant + positions = large_screen_positions.get(screen_id, [(row, col)]) + + # Determine quadrant by finding relative position + # The quadrant is determined by which cells are adjacent + quadrant = determine_large_screen_quadrant(row, col, positions, GRID_SIZE) + + # Get edges for this quadrant + if screen_id in large_screen_quadrant_info: + quad_info = large_screen_quadrant_info[screen_id] + + for direction in [Direction.North, Direction.South, Direction.East, Direction.West]: + edges = quad_info.get(quadrant, {}).get(direction, []) + edge_lists[(world_idx, row, col, direction)] = edges + else: + # No quadrant info - no edges + for direction in [Direction.North, Direction.South, Direction.East, Direction.West]: + edge_lists[(world_idx, row, col, direction)] = [] + else: + # Small screen - get edges directly + for direction in [Direction.North, Direction.South, Direction.East, Direction.West]: + edges_in_dir = [e for e in screen.edges.values() if e.direction == direction] + edge_lists[(world_idx, row, col, direction)] = edges_in_dir + + return edge_lists + +def determine_large_screen_quadrant(row: int, col: int, positions: List[tuple], grid_size: int) -> str: + """ + Determine which quadrant (NW, NE, SW, SE) a cell is in for a large screen. + Handles wrapping correctly by checking adjacency patterns. + + Args: + row: Current cell row + col: Current cell column + positions: List of all (row, col) positions for this large screen + grid_size: Size of the grid (8) + + Returns: + Quadrant string: "NW", "NE", "SW", or "SE" + """ + positions_set = set(positions) + + # Check which adjacent cells also belong to this large screen + has_right = ((row, (col + 1) % grid_size) in positions_set) + has_below = (((row + 1) % grid_size, col) in positions_set) + has_left = ((row, (col - 1) % grid_size) in positions_set) + has_above = (((row - 1) % grid_size, col) in positions_set) + + # Determine quadrant based on adjacency + # NW: has right and below neighbors + # NE: has left and below neighbors + # SW: has right and above neighbors + # SE: has left and above neighbors + + if has_right and has_below: + return "NW" + elif has_left and has_below: + return "NE" + elif has_right and has_above: + return "SW" + elif has_left and has_above: + return "SE" + else: + raise Exception("?") + +def is_crossed_edge(edge: OWEdge, overworld_screens: Dict[int, Screen]) -> bool: + if edge.dest is None: + return False + + source_screen = overworld_screens.get(edge.owIndex) + dest_screen = overworld_screens.get(edge.dest.owIndex) + return source_screen.dark_world != dest_screen.dark_world + +def visualize_layout(grid: List[List[List[int]]], output_dir: str, + overworld_screens: Dict[int, Screen], + large_screen_quadrant_info: Dict[int, Dict]) -> None: + # Constants + GRID_SIZE = 8 + BORDER_WIDTH = 1 + OUTPUT_CELL_SIZE = 64 # Each cell in output is always 64x64 pixels + + # Load the world images + try: + lightworld_img = Image.open("data/overworld/lightworld.png") + darkworld_img = Image.open("data/overworld/darkworld.png") + except FileNotFoundError as e: + raise FileNotFoundError(f"World image not found: {e}. Ensure lightworld.png and darkworld.png are in the data/overworld directory.") + + # Calculate source cell size from the base images + # Each world image is 8x8 screens, so divide by 8 to get source cell size + img_width, _ = lightworld_img.size + SOURCE_CELL_SIZE = img_width // GRID_SIZE # Size of each cell in the source image + + # Calculate dimensions for the output (always based on 64x64 cells) + world_width = GRID_SIZE * OUTPUT_CELL_SIZE + world_height = GRID_SIZE * OUTPUT_CELL_SIZE + + # Create output image (two worlds side by side with a small gap) + gap = 32 + output_width = world_width * 2 + gap + output_height = world_height + output_img = Image.new('RGB', (output_width, output_height), color='black') + + # Large screen base IDs (defined once for reuse) + large_screen_base_ids = [0x00, 0x03, 0x05, 0x18, 0x1B, 0x1E, 0x30, 0x35, + 0x40, 0x43, 0x45, 0x58, 0x5B, 0x5E, 0x70, 0x75] + + # Process both worlds + for world_idx in range(2): + x_offset = 0 if world_idx == 0 else (world_width + gap) + + # Build a map of screen_id -> list of (row, col) positions for large screens + large_screen_positions = {} + for row in range(GRID_SIZE): + for col in range(GRID_SIZE): + screen_id = grid[world_idx][row][col] + if screen_id != -1 and screen_id in large_screen_base_ids: + if screen_id not in large_screen_positions: + large_screen_positions[screen_id] = [] + large_screen_positions[screen_id].append((row, col)) + + # Process each cell in the grid individually + # This handles wrapped large screens correctly by drawing each quadrant separately + for row in range(GRID_SIZE): + for col in range(GRID_SIZE): + screen_id = grid[world_idx][row][col] + + if screen_id == -1: + # Empty cell - fill with black (already black from initialization) + continue + + is_large = screen_id in large_screen_base_ids + + # Calculate source position in the world image + source_row = (screen_id % 0x40) >> 3 + source_col = screen_id % 0x08 + world_img = lightworld_img if screen_id < 0x40 else darkworld_img + + if is_large: + # For large screens, determine which quadrant this cell represents + positions = large_screen_positions.get(screen_id, [(row, col)]) + quadrant = determine_large_screen_quadrant(row, col, positions, GRID_SIZE) + + # Map quadrant to source offset within the 2x2 large screen + quadrant_offsets = { + "NW": (0, 0), + "NE": (1, 0), + "SW": (0, 1), + "SE": (1, 1) + } + q_col_offset, q_row_offset = quadrant_offsets[quadrant] + + # Calculate source position for this quadrant + source_x = (source_col + q_col_offset) * SOURCE_CELL_SIZE + source_y = (source_row + q_row_offset) * SOURCE_CELL_SIZE + + # Crop single cell from source (the specific quadrant) + cropped = world_img.crop(( + source_x, + source_y, + source_x + SOURCE_CELL_SIZE, + source_y + SOURCE_CELL_SIZE + )) + else: + # Small screen (1x1) + source_x = source_col * SOURCE_CELL_SIZE + source_y = source_row * SOURCE_CELL_SIZE + + # Crop single cell from source + cropped = world_img.crop(( + source_x, + source_y, + source_x + SOURCE_CELL_SIZE, + source_y + SOURCE_CELL_SIZE + )) + + # Resize to output size (64x64 pixels) + resized = cropped.resize( + (OUTPUT_CELL_SIZE, OUTPUT_CELL_SIZE), + Image.LANCZOS + ) + + # Paste into output at grid position + dest_x = x_offset + col * OUTPUT_CELL_SIZE + dest_y = row * OUTPUT_CELL_SIZE + output_img.paste(resized, (dest_x, dest_y)) + + edge_lists = get_edge_lists(grid, overworld_screens, large_screen_quadrant_info) + + # Draw borders and edge connection indicators after all screens are placed + draw = ImageDraw.Draw(output_img) + + # Size of the indicator squares + INDICATOR_SIZE = 12 + + for world_idx in range(2): + x_offset = 0 if world_idx == 0 else (world_width + gap) + + # Build large screen positions map for this world + large_screen_positions = {} + for row in range(GRID_SIZE): + for col in range(GRID_SIZE): + screen_id = grid[world_idx][row][col] + if screen_id != -1 and screen_id in large_screen_base_ids: + if screen_id not in large_screen_positions: + large_screen_positions[screen_id] = [] + large_screen_positions[screen_id].append((row, col)) + + # Draw borders for each cell + for row in range(GRID_SIZE): + for col in range(GRID_SIZE): + screen_id = grid[world_idx][row][col] + + if screen_id == -1: + continue + + is_large = screen_id in large_screen_base_ids + + dest_x = x_offset + col * OUTPUT_CELL_SIZE + dest_y = row * OUTPUT_CELL_SIZE + + if is_large: + # For large screens, determine which quadrant this cell is + positions = large_screen_positions.get(screen_id, [(row, col)]) + quadrant = determine_large_screen_quadrant(row, col, positions, GRID_SIZE) + + # Draw border only on the outer edges of the large screen + # (not on internal edges between quadrants) + # NW: draw top and left borders + # NE: draw top and right borders + # SW: draw bottom and left borders + # SE: draw bottom and right borders + + if quadrant in ["NW", "NE"]: + # Draw top border + draw.line([(dest_x, dest_y), (dest_x + OUTPUT_CELL_SIZE - 1, dest_y)], fill='black', width=BORDER_WIDTH) + if quadrant in ["SW", "SE"]: + # Draw bottom border + draw.line([(dest_x, dest_y + OUTPUT_CELL_SIZE - 1), (dest_x + OUTPUT_CELL_SIZE - 1, dest_y + OUTPUT_CELL_SIZE - 1)], fill='black', width=BORDER_WIDTH) + if quadrant in ["NW", "SW"]: + # Draw left border + draw.line([(dest_x, dest_y), (dest_x, dest_y + OUTPUT_CELL_SIZE - 1)], fill='black', width=BORDER_WIDTH) + if quadrant in ["NE", "SE"]: + # Draw right border + draw.line([(dest_x + OUTPUT_CELL_SIZE - 1, dest_y), (dest_x + OUTPUT_CELL_SIZE - 1, dest_y + OUTPUT_CELL_SIZE - 1)], fill='black', width=BORDER_WIDTH) + else: + # Small screen - draw border around single cell + draw.rectangle( + [dest_x, dest_y, dest_x + OUTPUT_CELL_SIZE - 1, dest_y + OUTPUT_CELL_SIZE - 1], + outline='black', + width=BORDER_WIDTH + ) + + # Draw edge connection indicators for each cell + for row in range(GRID_SIZE): + for col in range(GRID_SIZE): + screen_id = grid[world_idx][row][col] + if screen_id == -1: + continue + + dest_x = x_offset + col * OUTPUT_CELL_SIZE + dest_y = row * OUTPUT_CELL_SIZE + + # Draw indicator for each direction (only if edges exist) + # Use bright colors for visibility + GREEN = (0, 255, 0) # Bright green + YELLOW = (255, 255, 0) # Bright yellow + RED = (255, 0, 0) # Bright red + + # North indicators - positioned based on edge midpoint + north_edges = edge_lists.get((world_idx, row, col, Direction.North), []) + if north_edges: + north_y = dest_y # Touch the top border + + for edge in north_edges: + # For north/south edges, midpoint gives the X coordinate + # Take midpoint modulo 0x0200, range 0-0x01FF maps to full side + midpoint = edge.midpoint % 0x0200 + # Map from game coordinate range to pixel position + edge_x_offset = (midpoint * OUTPUT_CELL_SIZE) // 0x0200 + edge_x = dest_x + edge_x_offset - INDICATOR_SIZE // 2 + + edge_color = GREEN if edge.dest is not None else YELLOW if any(e for e in north_edges if e.dest) else RED + draw.rectangle( + [edge_x, north_y, edge_x + INDICATOR_SIZE - 1, north_y + INDICATOR_SIZE - 1], + fill=edge_color, + outline='black' + ) + + # Draw diagonal cross if edge crosses between worlds + if edge.dest is not None and is_crossed_edge(edge, overworld_screens): + draw.line( + [edge_x, north_y, edge_x + INDICATOR_SIZE - 1, north_y + INDICATOR_SIZE - 1], + fill='black', + width=1 + ) + draw.line( + [edge_x + INDICATOR_SIZE - 1, north_y, edge_x, north_y + INDICATOR_SIZE - 1], + fill='black', + width=1 + ) + + # South indicators - positioned based on edge midpoint + south_edges = edge_lists.get((world_idx, row, col, Direction.South), []) + if south_edges: + south_y = dest_y + OUTPUT_CELL_SIZE - INDICATOR_SIZE # Touch the bottom border + + for edge in south_edges: + # For north/south edges, midpoint gives the X coordinate + # Take midpoint modulo 0x0200, range 0-0x01FF maps to full side + midpoint = edge.midpoint % 0x0200 + # Map from game coordinate range to pixel position + edge_x_offset = (midpoint * OUTPUT_CELL_SIZE) // 0x0200 + edge_x = dest_x + edge_x_offset - INDICATOR_SIZE // 2 + + edge_color = GREEN if edge.dest is not None else YELLOW if any(e for e in south_edges if e.dest) else RED + draw.rectangle( + [edge_x, south_y, edge_x + INDICATOR_SIZE - 1, south_y + INDICATOR_SIZE - 1], + fill=edge_color, + outline='black' + ) + + # Draw diagonal cross if edge crosses between worlds + if edge.dest is not None and is_crossed_edge(edge, overworld_screens): + draw.line( + [edge_x, south_y, edge_x + INDICATOR_SIZE - 1, south_y + INDICATOR_SIZE - 1], + fill='black', + width=1 + ) + draw.line( + [edge_x + INDICATOR_SIZE - 1, south_y, edge_x, south_y + INDICATOR_SIZE - 1], + fill='black', + width=1 + ) + + # West indicators - positioned based on edge midpoint + west_edges = edge_lists.get((world_idx, row, col, Direction.West), []) + if west_edges: + west_x = dest_x # Touch the left border + + for edge in west_edges: + # For west/east edges, midpoint gives the Y coordinate + # Take midpoint modulo 0x0200, range 0-0x01FF maps to full side + midpoint = edge.midpoint % 0x0200 + # Map from game coordinate range to pixel position + edge_y_offset = (midpoint * OUTPUT_CELL_SIZE) // 0x0200 + edge_y = dest_y + edge_y_offset - INDICATOR_SIZE // 2 + + edge_color = GREEN if edge.dest is not None else YELLOW if any(e for e in west_edges if e.dest) else RED + draw.rectangle( + [west_x, edge_y, west_x + INDICATOR_SIZE - 1, edge_y + INDICATOR_SIZE - 1], + fill=edge_color, + outline='black' + ) + + # Draw diagonal cross if edge crosses between worlds + if edge.dest is not None and is_crossed_edge(edge, overworld_screens): + draw.line( + [west_x, edge_y, west_x + INDICATOR_SIZE - 1, edge_y + INDICATOR_SIZE - 1], + fill='black', + width=1 + ) + draw.line( + [west_x + INDICATOR_SIZE - 1, edge_y, west_x, edge_y + INDICATOR_SIZE - 1], + fill='black', + width=1 + ) + + # East indicators - positioned based on edge midpoint + east_edges = edge_lists.get((world_idx, row, col, Direction.East), []) + if east_edges: + east_x = dest_x + OUTPUT_CELL_SIZE - INDICATOR_SIZE # Touch the right border + + for edge in east_edges: + # For west/east edges, midpoint gives the Y coordinate + # Take midpoint modulo 0x0200, range 0-0x01FF maps to full side + midpoint = edge.midpoint % 0x0200 + # Map from game coordinate range to pixel position + edge_y_offset = (midpoint * OUTPUT_CELL_SIZE) // 0x0200 + edge_y = dest_y + edge_y_offset - INDICATOR_SIZE // 2 + + edge_color = GREEN if edge.dest is not None else YELLOW if any(e for e in east_edges if e.dest) else RED + draw.rectangle( + [east_x, edge_y, east_x + INDICATOR_SIZE - 1, edge_y + INDICATOR_SIZE - 1], + fill=edge_color, + outline='black' + ) + + # Draw diagonal cross if edge crosses between worlds + if edge.dest is not None and is_crossed_edge(edge, overworld_screens): + draw.line( + [east_x, edge_y, east_x + INDICATOR_SIZE - 1, edge_y + INDICATOR_SIZE - 1], + fill='black', + width=1 + ) + draw.line( + [east_x + INDICATOR_SIZE - 1, edge_y, east_x, edge_y + INDICATOR_SIZE - 1], + fill='black', + width=1 + ) + + # Create output directory if it doesn't exist + os.makedirs(output_dir, exist_ok=True) + + # Generate filename with timestamp + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + filename = f"layout_{timestamp}.png" + filepath = os.path.join(output_dir, filename) + + # Save the image + output_img.save(filepath, "PNG") + logging.getLogger('').info(f"Layout visualization saved to {filepath}") \ No newline at end of file diff --git a/source/tools/MysteryUtils.py b/source/tools/MysteryUtils.py index 673f2224..4d0b72a8 100644 --- a/source/tools/MysteryUtils.py +++ b/source/tools/MysteryUtils.py @@ -120,8 +120,16 @@ def roll_settings(weights): ret.accessibility = get_choice('accessibility') ret.restrict_boss_items = get_choice('restrict_boss_items') + overworld_layout = get_choice('overworld_layout') + ret.ow_layout = overworld_layout if overworld_layout != 'none' else 'vanilla' + ret.ow_parallel = get_choice_bool('overworld_parallel') overworld_shuffle = get_choice('overworld_shuffle') - ret.ow_shuffle = overworld_shuffle if overworld_shuffle != 'none' else 'vanilla' + if overworld_shuffle == 'parallel': + ret.ow_layout = 'wild' + ret.ow_parallel = True + elif overworld_shuffle == 'full': + ret.ow_layout = 'wild' + ret.ow_parallel = False ret.ow_terrain = get_choice_bool('overworld_terrain') valid_options = {'none': 'none', 'polar': 'polar', 'grouped': 'polar', 'chaos': 'unrestricted', 'unrestricted': 'unrestricted'} ret.ow_crossed = get_choice('overworld_crossed') @@ -131,6 +139,7 @@ def roll_settings(weights): ret.ow_whirlpool = get_choice_bool('whirlpool_shuffle') overworld_flute = get_choice('flute_shuffle') ret.ow_fluteshuffle = overworld_flute if overworld_flute != 'none' else 'vanilla' + ret.ow_fog = get_choice_bool('overworld_fog') ret.shuffle_followers = get_choice_bool('shuffle_followers') ret.bonk_drops = get_choice_bool('bonk_drops') entrance_shuffle = get_choice('entrance_shuffle')