diff --git a/BaseClasses.py b/BaseClasses.py index d4ef007f..b2dbf047 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -24,6 +24,7 @@ class World(object): self.players = players self.teams = 1 self.owShuffle = owShuffle.copy() + self.owKeepSimilar = {} self.shuffle = shuffle.copy() self.doorShuffle = doorShuffle.copy() self.intensity = {} @@ -2109,6 +2110,7 @@ class Spoiler(object): 'weapons': self.world.swords, 'goal': self.world.goal, 'ow_shuffle': self.world.owShuffle, + 'ow_keepsimilar': self.world.owKeepSimilar, 'shuffle': self.world.shuffle, 'door_shuffle': self.world.doorShuffle, 'intensity': self.world.intensity, @@ -2187,6 +2189,7 @@ class Spoiler(object): outfile.write('Difficulty:'.ljust(line_width) + '%s\n' % self.metadata['item_pool'][player]) outfile.write('Item Functionality:'.ljust(line_width) + '%s\n' % self.metadata['item_functionality'][player]) outfile.write('Overworld Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['ow_shuffle'][player]) + outfile.write('Keep OW Edges Together:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['ow_keepsimilar'][player] else 'No')) outfile.write('Entrance Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['shuffle'][player]) outfile.write('Door Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['door_shuffle'][player]) outfile.write('Intensity:'.ljust(line_width) + '%s\n' % self.metadata['intensity'][player]) diff --git a/CLI.py b/CLI.py index 3a7b4967..a3489915 100644 --- a/CLI.py +++ b/CLI.py @@ -94,7 +94,7 @@ def parse_cli(argv, no_defaults=False): for player in range(1, multiargs.multi + 1): playerargs = parse_cli(shlex.split(getattr(ret, f"p{player}")), True) - for name in ['logic', 'mode', 'swords', 'goal', 'difficulty', 'item_functionality', 'ow_shuffle', + for name in ['logic', 'mode', 'swords', 'goal', 'difficulty', 'item_functionality', 'ow_shuffle', 'ow_keepsimilar', 'shuffle', 'door_shuffle', 'intensity', 'crystals_ganon', 'crystals_gt', 'openpyramid', 'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory', 'triforce_pool_min', 'triforce_pool_max', 'triforce_goal_min', 'triforce_goal_max', @@ -142,7 +142,8 @@ def parse_settings(): # Shuffle Ganon defaults to TRUE "openpyramid": False, "shuffleganon": True, - "ow_shuffle": "full", + "ow_shuffle": "vanilla", + "ow_keepsimilar": False, "shuffle": "vanilla", "shufflepots": False, diff --git a/Main.py b/Main.py index c0371f59..84d04426 100644 --- a/Main.py +++ b/Main.py @@ -73,6 +73,7 @@ def main(args, seed=None, fish=None): world.crystals_needed_for_gt = {player: random.randint(0, 7) if args.crystals_gt[player] == 'random' else int(args.crystals_gt[player]) for player in range(1, world.players + 1)} world.crystals_ganon_orig = args.crystals_ganon.copy() world.crystals_gt_orig = args.crystals_gt.copy() + world.owKeepSimilar = args.ow_keepsimilar.copy() world.open_pyramid = args.openpyramid.copy() world.boss_shuffle = args.shufflebosses.copy() world.enemy_shuffle = args.shuffleenemies.copy() diff --git a/Mystery.py b/Mystery.py index d14deb07..b26e6ff9 100644 --- a/Mystery.py +++ b/Mystery.py @@ -153,6 +153,7 @@ def roll_settings(weights): overworld_shuffle = get_choice('overworld_shuffle') ret.ow_shuffle = overworld_shuffle if overworld_shuffle != 'none' else 'vanilla' + ret.ow_keepsimilar = get_choice('ow_keepsimilar') entrance_shuffle = get_choice('entrance_shuffle') ret.shuffle = entrance_shuffle if entrance_shuffle != 'none' else 'vanilla' door_shuffle = get_choice('door_shuffle') diff --git a/OWEdges.py b/OWEdges.py index 8bfe36a6..efba1ef6 100644 --- a/OWEdges.py +++ b/OWEdges.py @@ -1,5 +1,5 @@ -from BaseClasses import OWEdge, Direction, Terrain +from BaseClasses import OWEdge, Direction, Terrain, WorldType, PolSlot # constants We = Direction.West @@ -10,6 +10,13 @@ No = Direction.North Ld = Terrain.Land Wr = Terrain.Water +LW = WorldType.Light +DW = WorldType.Dark + +Vt = PolSlot.NorthSouth +Hz = PolSlot.EastWest + + def create_owedges(world, player): edges = [ # name, owID,dir,type,edge_id,(owSlot) #(vram, scrollY, scrollX, linkY, linkX, camY, camX, unk1, unk2) @@ -302,3 +309,307 @@ def create_owedges(world, player): def create_owedge(player, name, owIndex, direction, terrain, edge_id, owSlotIndex=0xff): return OWEdge(player, name, owIndex, direction, terrain, edge_id, owSlotIndex) + + +OWEdgeGroups = { + (LW, Hz, Ld, 1): ( + [ + ['Lost Woods EN'], + ['East Death Mountain EN'], + ['Sanctuary EC'], + ['Graveyard EC'], + ['Forgotten Forest ES'], + ['Kakariko ES'], + ['Hyrule Castle ES'], + ['Maze Race ES'], + ['Kakariko Suburb ES'], + ['Links House ES'], + ['Cave 45 EC'], + ['Dam EC'], + ['South Pass ES'], + ['Potion Shop EC'], + ['Lake Hylia ES'], + ['Stone Bridge EN'], + ['West Death Mountain EN'], + ['West Death Mountain ES'] + ], + [ + ['Lumberjack WN'], + ['Death Mountain TR Pegs WN'], + ['Graveyard WC'], + ['Useless Fairy WC'], + ['Hyrule Castle WN'], + ['Blacksmith WS'], + ['Sand Dune WN'], + ['Kakariko Suburb WS'], + ['Flute Boy WS'], + ['Stone Bridge WS'], + ['C Whirlpool WC'], + ['South Pass WC'], + ['Lake Hylia WS'], + ['Zora Warning WC'], + ['Octoballoon WS'], + ['Tree Line WN'], + ['East Death Mountain WN'], + ['East Death Mountain WS'] + ] + ), + (LW, Vt, Ld, 1): ( + [ + ['Lumberjack SW'], + ['DM Ascent SE'], + ['Lost Woods SE'], + ['Zora Approach SE'], + ['Kakariko Fortune SC'], + ['Wooden Bridge SW'], + ['Kakariko SE'], + ['Hyrule Castle SW'], + ['Hyrule Castle SE'], + ['Sand Dune SC'], + ['Eastern Palace SW'], + ['Eastern Palace SE'], + ['Central Bonk Rock SW'], + ['Links House SC'], + ['Stone Bridge SC'], + ['C Whirlpool SC'], + ['Statues SC'], + ['Tree Line SE'], + ['Ice Rod Cave SE'] + ], + [ + ['DM Ascent NW'], + ['Kakariko Pond NE'], + ['Kakariko Fortune NE'], + ['Zora Warning NE'], + ['Kakariko NE'], + ['Sand Dune NW'], + ['Kakariko Suburb NE'], + ['Central Bonk Rock NW'], + ['Links House NE'], + ['Stone Bridge NC'], + ['Tree Line NW'], + ['Eastern Nook NE'], + ['C Whirlpool NW'], + ['Statues NC'], + ['Lake Hylia NW'], + ['Dam NC'], + ['South Pass NC'], + ['Lake Hylia NE'], + ['Octoballoon NE'] + ], + ), + (LW, Hz, Ld, 2): ( + [ + ['Kakariko Fortune EN', 'Kakariko Fortune ES'], + ['Kakariko Pond EN', 'Kakariko Pond ES'], + ['Desert EC', 'Desert ES'], + ['Purple Chest EC', 'Purple Chest ES'], + ['Useless Fairy EC', 'Useless Fairy ES'], + ['C Whirlpool EN', 'C Whirlpool ES'] + ], + [ + ['Kakariko Pond WN', 'Kakariko Pond WS'], + ['Sanctuary WN', 'Sanctuary WS'], + ['Purple Chest WC', 'Purple Chest WS'], + ['Dam WC', 'Dam WS'], + ['Potion Shop WC', 'Potion Shop WS'], + ['Statues WN', 'Statues WS'] + ], + ), + (LW, Vt, Ld, 2): ( + [ + ['Lost Woods SW', 'Lost Woods SC'], + ['Lost Woods Pass SW', 'Lost Woods Pass SE'], + ['Kakariko Pond SW', 'Kakariko Pond SE'], + ['Flute Boy SW', 'Flute Boy SC'], + ['Useless Fairy SW', 'Useless Fairy SE'] + ], + [ + ['Lost Woods Pass NW', 'Lost Woods Pass NE'], + ['Kakariko NW', 'Kakariko NC'], + ['Forgotten Forest NW', 'Forgotten Forest NE'], + ['Cave 45 NW', 'Cave 45 NC'], + ['Wooden Bridge NW', 'Wooden Bridge NE'] + ], + ), + (LW, Hz, Ld, 3): ( + [['Central Bonk Rock EN', 'Central Bonk Rock EC', 'Central Bonk Rock ES']], + [['Links House WN', 'Links House WC', 'Links House WS']] + ), + (LW, Hz, Wr, 1): ( + [ + ['Potion Shop EN'], + ['Lake Hylia EC'], + ['Stone Bridge EC'], + ['Useless Fairy EN'], + ['C Whirlpool EC'] + ], + [ + ['Zora Warning WN'], + ['Octoballoon WC'], + ['Tree Line WC'], + ['Potion Shop WN'], + ['Statues WC'] + ] + ), + (LW, Vt, Wr, 1): ( + [ + ['Tree Line SC'], + ['Ice Rod Cave SW'], + ['Useless Fairy SC'] + ], + [ + ['Lake Hylia NC'], + ['Octoballoon NW'], + ['Wooden Bridge NC'] + ] + ), + (DW, Hz, Ld, 1): ( + [ + ['Skull Woods EN'], + ['East Dark Death Mountain EN'], + ['Dark Chapel EC'], + ['Dark Graveyard ES'], + ['Village of Outcasts ES'], + ['Pyramid ES'], + ['Frog ES'], + ['Big Bomb Shop ES'], + ['Circle of Bushes EC'], + ['Swamp Palace EC'], + ['Dark South Pass ES'], + ['Dark Witch EC'], + ['Dark Lake Hylia ES'], + ['Hammer Bridge EN'], + ['West Dark Death Mountain EN'], + ['West Dark Death Mountain ES'] + ], + [ + ['Dark Lumberjack WN'], + ['Turtle Rock WN'], + ['Dark Graveyard WC'], + ['Qirn Jump WC'], + ['Hammer Pegs WS'], + ['Dark Dune WN'], + ['Stumpy WS'], + ['Hammer Bridge WS'], + ['Dark C Whirlpool WC'], + ['Dark South Pass WC'], + ['Dark Lake Hylia WS'], + ['Catfish Approach WC'], + ['Southeast DW WS'], + ['Dark Tree Line WN'], + ['East Dark Death Mountain WN'], + ['East Dark Death Mountain WS'] + ] + ), + (DW, Vt, Ld, 1): ( + [ + ['Dark Lumberjack SW'], + ['Bumper Cave SE'], + ['Skull Woods SE'], + ['Catfish SE'], + ['Dark Fortune SC'], + ['Broken Bridge SW'], + ['Village of Outcasts SE'], + ['Pyramid SW'], + ['Pyramid SE'], + ['Dark Dune SC'], + ['Palace of Darkness SW'], + ['Palace of Darkness SE'], + ['Dark Bonk Rock SW'], + ['Big Bomb Shop SC'], + ['Hammer Bridge SC'], + ['Dark C Whirlpool SC'], + ['Hype Cave SC'], + ['Dark Tree Line SE'], + ['Dark Shopping Mall SE'] + ], + [ + ['Bumper Cave NW'], + ['Outcast Pond NE'], + ['Dark Fortune NE'], + ['Catfish Approach NE'], + ['Village of Outcasts NE'], + ['Dark Dune NW'], + ['Frog NE'], + ['Dark Bonk Rock NW'], + ['Big Bomb Shop NE'], + ['Hammer Bridge NC'], + ['Dark Tree Line NW'], + ['Palace of Darkness Nook NE'], + ['Dark C Whirlpool NW'], + ['Hype Cave NC'], + ['Dark Lake Hylia NW'], + ['Swamp Palace NC'], + ['Dark South Pass NC'], + ['Dark Lake Hylia NE'], + ['Southeast DW NE'] + ], + ), + (DW, Hz, Ld, 2): ( + [ + ['Dark Fortune EN', 'Dark Fortune ES'], + ['Outcast Pond EN', 'Outcast Pond ES'], + ['Dark Purple Chest EC', 'Dark Purple Chest ES'], + ['Dig Game EC', 'Dig Game ES'], + ['Qirn Jump EC', 'Qirn Jump ES'], + ['Dark C Whirlpool EN', 'Dark C Whirlpool ES'] + ], + [ + ['Outcast Pond WN', 'Outcast Pond WS'], + ['Dark Chapel WN', 'Dark Chapel WS'], + ['Swamp Palace WC', 'Swamp Palace WS'], + ['Frog WC', 'Frog WS'], + ['Dark Witch WC', 'Dark Witch WS'], + ['Hype Cave WN', 'Hype Cave WS'] + ], + ), + (DW, Vt, Ld, 2): ( + [ + ['Skull Woods SW', 'Skull Woods SC'], + ['Skull Woods Pass SW', 'Skull Woods Pass SE'], + ['Outcast Pond SW', 'Outcast Pond SE'], + ['Stumpy SW', 'Stumpy SC'], + ['Qirn Jump SW', 'Qirn Jump SE'] + ], + [ + ['Skull Woods Pass NW', 'Skull Woods Pass NE'], + ['Village of Outcasts NW', 'Village of Outcasts NC'], + ['Shield Shop NW', 'Shield Shop NE'], + ['Circle of Bushes NW', 'Circle of Bushes NC'], + ['Broken Bridge NW', 'Broken Bridge NE'] + ], + ), + (DW, Hz, Ld, 3): ( + [['Dark Bonk Rock EN', 'Dark Bonk Rock EC', 'Dark Bonk Rock ES']], + [['Big Bomb Shop WN', 'Big Bomb Shop WC', 'Big Bomb Shop WS']] + ), + (DW, Hz, Wr, 1): ( + [ + ['Dark Witch EN'], + ['Dark Lake Hylia EC'], + ['Hammer Bridge EC'], + ['Qirn Jump EN'], + ['Dark C Whirlpool EC'] + ], + [ + ['Catfish Approach WN'], + ['Southeast DW WC'], + ['Dark Tree Line WC'], + ['Dark Witch WN'], + ['Hype Cave WC'] + ] + ), + (DW, Vt, Wr, 1): ( + [ + ['Dark Tree Line SC'], + ['Dark Shopping Mall SW'], + ['Qirn Jump SC'] + ], + [ + ['Dark Lake Hylia NC'], + ['Southeast DW NW'], + ['Broken Bridge NC'] + ] + ) +} diff --git a/OverworldShuffle.py b/OverworldShuffle.py index a3eb05d0..47390070 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -1,5 +1,6 @@ import random from BaseClasses import OWEdge, WorldType, Direction, Terrain +from OWEdges import OWEdgeGroups __version__ = '0.1.0.4-u' @@ -16,14 +17,12 @@ def link_overworld(world, player): if world.owShuffle[player] == 'vanilla': for exitname, destname in default_connections: connect_two_way(world, exitname, destname, player) - elif world.owShuffle[player] == 'full': - remaining_edges = list() + else: + remaining_edges = [] for exitname, destname in default_connections: remaining_edges.append(exitname) remaining_edges.append(destname) - - #for exitname, destname in default_connections: - #connect_two_way(world, exitname, destname, player) + if world.mode[player] == 'standard': for exitname, destname in standard_connections: connect_two_way(world, exitname, destname, player) @@ -36,9 +35,30 @@ def link_overworld(world, player): remaining_edges.remove(exitname) remaining_edges.remove(destname) - connect_remaining(world, remaining_edges, player) - else: - raise NotImplementedError('Shuffling not supported yet') + if world.owShuffle[player] == 'full': + if world.owKeepSimilar[player]: + #TODO: remove edges from list that are already placed, Std and Plando + # shuffle edges in groups that connect the same pair of tiles + for grouping in (OWEdgeGroups, None): + if grouping is not None: #TODO: Figure out why ^ has to be a tuple for this to work + groups = list(grouping.values()) + random.shuffle(groups) + for (forward_edge_sets, back_edge_sets) in groups: + assert len(forward_edge_sets) == len(back_edge_sets) + random.shuffle(back_edge_sets) + + for (forward_set, back_set) in zip(forward_edge_sets, back_edge_sets): + assert len(forward_set) == len(back_set) + for (forward_edge, back_edge) in zip(forward_set, back_set): + connect_two_way(world, forward_edge, back_edge, player) + remaining_edges.remove(forward_edge) + remaining_edges.remove(back_edge) + + assert len(remaining_edges) == 0, remaining_edges + else: + connect_remaining(world, remaining_edges, player) + else: + raise NotImplementedError('Shuffling not supported yet') def connect_custom(world, player): diff --git a/README.md b/README.md index 95ccca33..f3a75f42 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ Alternatively, run ```Gui.py``` for a simple graphical user interface. (WIP) Only extra settings are found here. All door and entrance randomizer settings are supported. See their [readme](https://github.com/Aerinon/ALttPDoorRandomizer/blob/master/README.md) -## Overworld Shuffle (--owShuffle) +## Overworld Shuffle (--ow_shuffle) ### Full @@ -52,6 +52,10 @@ OW Transitions are shuffled within each world separately. OW is not shuffled. +## Keep Similar Edges Together (--ow_keepsimilar) + +This keeps similar edge transitions together. ie. The 2 west edges will be paired to another set of two similar edges + # Command Line Options @@ -66,3 +70,9 @@ Show the help message and exit. ``` For specifying the overworld shuffle you want as above. (default: vanilla) + +``` +--ow_keepsimilar +``` + +This keeps similar edge transitions paired together with other pairs of transitions diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index e1b8ad65..cfdba1cb 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -59,22 +59,22 @@ "expert" ] }, - "shopsanity" : { + "shopsanity": { "action": "store_true", "type": "bool" }, - "keydropshuffle" : { + "keydropshuffle": { "action": "store_true", "type": "bool" }, - "mixed_travel" : { + "mixed_travel": { "choices": [ "prevent", "allow", "force" ] }, - "standardize_palettes" : { + "standardize_palettes": { "choices": [ "standardize", "original" @@ -116,6 +116,10 @@ "crossed" ] }, + "ow_keepsimilar": { + "action": "store_true", + "type": "bool" + }, "shuffle": { "choices": [ "vanilla", @@ -140,8 +144,11 @@ ] }, "intensity": { - "choices":[ - "3", "2", "1", "random" + "choices": [ + "3", + "2", + "1", + "random" ] }, "experimental": { @@ -158,12 +165,28 @@ }, "crystals_ganon": { "choices": [ - "7", "6", "5", "4", "3", "2", "1", "0", "random" + "7", + "6", + "5", + "4", + "3", + "2", + "1", + "0", + "random" ] }, "crystals_gt": { "choices": [ - "7", "6", "5", "4", "3", "2", "1", "0", "random" + "7", + "6", + "5", + "4", + "3", + "2", + "1", + "0", + "random" ] }, "openpyramid": { @@ -376,4 +399,4 @@ }, "outputname": {}, "code": {} -} +} \ No newline at end of file diff --git a/resources/app/cli/lang/en.json b/resources/app/cli/lang/en.json index d9340c92..ee0d50bc 100644 --- a/resources/app/cli/lang/en.json +++ b/resources/app/cli/lang/en.json @@ -203,6 +203,7 @@ " overworld screens from the same world.", "Crossed: Overworld screen transitions can lead to any other overworld screen." ], + "ow_keepsimilar": [ "This keeps similar edge transitions together. ie. the two west edges on Potion Shop will be paired with another similar pair." ], "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 9be11b70..eadfe436 100644 --- a/resources/app/gui/lang/en.json +++ b/resources/app/gui/lang/en.json @@ -114,6 +114,8 @@ "randomizer.overworld.overworldshuffle.simple": "Simple", "randomizer.overworld.overworldshuffle.full": "Full", "randomizer.overworld.overworldshuffle.crossed": "Crossed", + + "randomizer.overworld.keepsimilar": "Keep Similar Edges Together", "randomizer.entrance.openpyramid": "Pre-open Pyramid Hole", "randomizer.entrance.shuffleganon": "Include Ganon's Tower and Pyramid Hole in shuffle pool", diff --git a/resources/app/gui/randomize/overworld/widgets.json b/resources/app/gui/randomize/overworld/widgets.json index 8ccc6c5e..5aa2d7cc 100644 --- a/resources/app/gui/randomize/overworld/widgets.json +++ b/resources/app/gui/randomize/overworld/widgets.json @@ -7,6 +7,10 @@ "vanilla", "full" ] + }, + "keepsimilar": { + "type": "checkbox", + "default": true } } -} +} \ No newline at end of file diff --git a/source/classes/constants.py b/source/classes/constants.py index 5e614db0..68df9519 100644 --- a/source/classes/constants.py +++ b/source/classes/constants.py @@ -71,7 +71,8 @@ SETTINGSTOPROCESS = { "sortingalgo": "algorithm" }, "overworld": { - "overworldshuffle": "ow_shuffle" + "overworldshuffle": "ow_shuffle", + "keepsimilar": "ow_keepsimilar" }, "entrance": { "openpyramid": "openpyramid",