diff --git a/BaseClasses.py b/BaseClasses.py index d8e34b6e..d3be01e2 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1089,6 +1089,10 @@ class CollectionState(object): rupee_farms = ['Archery Game', '50 Rupee Cave', '20 Rupee Cave'] + bush_crabs = ['Lost Woods East Area', 'Mountain Entry Area'] + pre_aga_bush_crabs = ['Lumberjack Area', 'South Pass Area'] + rock_crabs = ['Desert Pass Area'] + def can_reach_non_bunny(regionname): region = self.world.get_region(regionname, player) return region.can_reach(self) and ((self.world.mode[player] != 'inverted' and region.is_light_world) or (self.world.mode[player] == 'inverted' and region.is_dark_world) or self.has('Pearl', player)) @@ -1097,7 +1101,8 @@ class CollectionState(object): if can_reach_non_bunny(region): return True - if any(i in [0xda, 0xdb] for i in self.world.prizes[player]['pull']): + # tree pulls + if self.can_kill_most_things(player) and any(i in [0xda, 0xdb] for i in self.world.prizes[player]['pull']): for region in tree_pulls: if can_reach_non_bunny(region): return True @@ -1109,6 +1114,22 @@ class CollectionState(object): for region in post_aga_tree_pulls: if can_reach_non_bunny(region): return True + + # bush crabs (final item isn't considered) + if self.world.enemy_shuffle[player] == 'none': + if self.world.prizes[player]['crab'][0] in [0xda, 0xdb]: + for region in bush_crabs: + if can_reach_non_bunny(region): + return True + if not self.has('Beat Agahnim 1', player): + for region in pre_aga_bush_crabs: + if can_reach_non_bunny(region): + return True + if self.can_lift_rocks(player) and self.world.prizes[player]['crab'][0] in [0xda, 0xdb]: + for region in rock_crabs: + if can_reach_non_bunny(region): + return True + return False def can_farm_bombs(self, player): @@ -1173,7 +1194,7 @@ class CollectionState(object): return True # tree pulls - if any(i in [0xdc, 0xdd, 0xde] for i in self.world.prizes[player]['pull']): + if self.can_kill_most_things(player) and any(i in [0xdc, 0xdd, 0xde] for i in self.world.prizes[player]['pull']): for region in tree_pulls: if can_reach_non_bunny(region): return True @@ -1187,7 +1208,7 @@ class CollectionState(object): return True # bush crabs (final item isn't considered) - if self.world.enemy_shuffle[player] != 'none': + if self.world.enemy_shuffle[player] == 'none': if self.world.prizes[player]['crab'][0] in [0xdc, 0xdd, 0xde]: for region in bush_crabs: if can_reach_non_bunny(region): @@ -3141,7 +3162,18 @@ class Spoiler(object): if self.world.players > 1: outfile.write(str('(Player ' + str(player) + ')\n')) # player name outfile.write(self.maps[('swaps', player)]['text'] + '\n\n') - + + # crossed groups + for player in range(1, self.world.players + 1): + if ('groups', player) in self.maps: + outfile.write('OW Crossed Groups:\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'] + '\n\n') + if self.overworlds: # overworld transitions outfile.write('\n'.join(['%s%s %s %s' % (f'{self.world.get_player_names(entry["player"])}: ' if self.world.players > 1 else '', self.world.fish.translate("meta","overworlds",entry['entrance']), '<=>' if entry['direction'] == 'both' else '<=' if entry['direction'] == 'exit' else '=>', self.world.fish.translate("meta","overworlds",entry['exit'])) for entry in self.overworlds.values()])) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae14e2cf..dbb65f37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,15 @@ # Changelog +### 0.2.7.3 +- Restructured OWR algorithm to include some additional scenarios not previously allowed +- Added new Inverted D-pad controls for Social Distorion (ie. Mirror Mode) support +- Crossed OWR/Special OW Areas are now included in the spoiler log +- Fixed default TF pieces with Trinity in Mystery +- Added bush crabs to rupee farm logic (only in non-enemizer) +- Updated tree pull logic to also require ability to kill most things + ### 0.2.7.2 -- Special OW Area are now shuffled in Layout Shuffle (Zora/Hobo/Pedestal) +- Special OW Areas are now shuffled in Layout Shuffle (Zora/Hobo/Pedestal) - Fixed some broken water region graph modelling, fixed some reachability logic - Some minor code simplifications diff --git a/ItemList.py b/ItemList.py index 7ff27436..f1f9e2c9 100644 --- a/ItemList.py +++ b/ItemList.py @@ -759,9 +759,11 @@ def get_pool_core(progressive, shuffle, difficulty, treasure_hunt_total, timer, placed_items = {} precollected_items = [] clock_mode = None - if goal in ['triforcehunt', 'trinity']: - if treasure_hunt_total == 0: + if treasure_hunt_total == 0: + if goal == 'triforcehunt': treasure_hunt_total = 30 + elif goal == 'trinity': + treasure_hunt_total = 10 triforcepool = ['Triforce Piece'] * int(treasure_hunt_total) pool.extend(alwaysitems) diff --git a/Items.py b/Items.py index bc685236..dfb19e19 100644 --- a/Items.py +++ b/Items.py @@ -42,7 +42,7 @@ item_table = {'Bow': (True, False, None, 0x0B, 200, 'You have\nchosen the\narche 'Moon Pearl': (True, False, None, 0x1F, 200, ' Bunny Link\n be\n gone!', 'and the jaw breaker', 'fortune-telling kid', 'lunar orb for sale', 'shrooms for moon rock', 'moon boy plays ball again', 'the Moon Pearl'), 'Cane of Somaria': (True, False, None, 0x15, 250, 'I make blocks\nto hold down\nswitches!', 'and the red blocks', 'the block-making kid', 'block stick for sale', 'block stick for trade', 'cane boy makes blocks again', 'the Red Cane'), 'Fire Rod': (True, False, None, 0x07, 250, 'I\'m the hot\nrod. I make\nthings burn!', 'and the flamethrower', 'fire-starting kid', 'rage rod for sale', 'fungus for rage-rod', 'firestarter boy burns again', 'the Fire Rod'), - 'Flippers': (True, False, None, 0x1E, 250, 'fancy a swim?', 'and the toewebs', 'the swimming kid', 'finger webs for sale', 'shrooms let you swim', 'swimming boy swims again', 'the flippers'), + 'Flippers': (True, False, None, 0x1E, 250, 'fancy a swim?', 'and the toewebs', 'the swimming kid', 'finger webs for sale', 'shrooms let you swim', 'swimming boy swims again', 'the Flippers'), 'Ice Rod': (True, False, None, 0x08, 250, 'I\'m the cold\nrod. I make\nthings freeze!', 'and the freeze ray', 'the ice-bending kid', 'freeze ray for sale', 'fungus for ice-rod', 'ice-cube boy freezes again', 'the Ice Rod'), 'Titans Mitts': (True, False, None, 0x1C, 200, 'Now you can\nlift heavy\nstuff!', 'and the golden glove', 'body-building kid', 'carry glove for sale', 'fungus for bling-gloves', 'body-building boy has gold again', 'the Mitts'), 'Bombos': (True, False, None, 0x0F, 100, 'Burn, baby,\nburn! Fear my\nring of fire!', 'and the swirly coin', 'coin-collecting kid', 'swirly coin for sale', 'shrooms for swirly-coin', 'medallion boy melts room again', 'Bombos'), diff --git a/Main.py b/Main.py index 0489b8bd..8453c90a 100644 --- a/Main.py +++ b/Main.py @@ -135,7 +135,7 @@ def main(args, seed=None, fish=None): world.player_names[player].append(name) logger.info('') - if world.owShuffle[1] != 'vanilla' or world.owCrossed[1] not in ['none', 'polar'] or world.owMixed[1] or str(args.outputname).startswith('M'): + if world.owShuffle[1] != 'vanilla' or world.owCrossed[1] not in ['none', 'polar'] or world.owMixed[1] or world.owWhirlpoolShuffle[1] or world.owFluteShuffle[1] != 'vanilla' or str(args.outputname).startswith('M'): outfilebase = f'OR_{args.outputname if args.outputname else world.seed}' else: outfilebase = f'DR_{args.outputname if args.outputname else world.seed}' diff --git a/Mystery.py b/Mystery.py index faf5d16a..0e2a556d 100644 --- a/Mystery.py +++ b/Mystery.py @@ -208,12 +208,14 @@ def roll_settings(weights): ret.crystals_gt = get_choice('tower_open') ret.crystals_ganon = get_choice('ganon_open') - goal_min = get_choice_default('triforce_goal_min', default=20) - goal_max = get_choice_default('triforce_goal_max', default=20) - pool_min = get_choice_default('triforce_pool_min', default=30) - pool_max = get_choice_default('triforce_pool_max', default=30) + from ItemList import set_default_triforce + default_tf_goal, default_tf_pool = set_default_triforce(ret.goal, 0, 0) + goal_min = get_choice_default('triforce_goal_min', default=default_tf_goal) + goal_max = get_choice_default('triforce_goal_max', default=default_tf_goal) + pool_min = get_choice_default('triforce_pool_min', default=default_tf_pool) + pool_max = get_choice_default('triforce_pool_max', default=default_tf_pool) ret.triforce_goal = random.randint(int(goal_min), int(goal_max)) - min_diff = get_choice_default('triforce_min_difference', default=10) + min_diff = get_choice_default('triforce_min_difference', default=(default_tf_pool-default_tf_goal)) ret.triforce_pool = random.randint(max(int(pool_min), ret.triforce_goal + int(min_diff)), int(pool_max)) ret.mode = get_choice('world_state') diff --git a/OWEdges.py b/OWEdges.py index b79445a7..69c26b90 100644 --- a/OWEdges.py +++ b/OWEdges.py @@ -984,289 +984,6 @@ OWTileRegions = bidict({ 'Zoras Domain': 0x81 }) -OWTileGroups = { - ("Woods", "Regular", "None"): ( - [ - 0x00, 0x2d, 0x80 - ], - [ - 0x40, 0x6d - ] - ), - ("Lumberjack", "Regular", "None"): ( - [ - 0x02 - ], - [ - 0x42 - ] - ), - ("Mountain Entry", "Entrance", "None"): ( - [ - 0x03 - ], - [ - 0x43 - ] - ), - ("East Mountain", "Regular", "None"): ( - [ - 0x05 - ], - [ - 0x45 - ] - ), - ("East Mountain", "Entrance", "None"): ( - [ - 0x07 - ], - [ - 0x47 - ] - ), - ("Lake", "Regular", "Zora"): ( - [ - 0x0f, 0x81 - ], - [ - 0x4f - ] - ), - ("Lake", "Regular", "Lake"): ( - [ - 0x35 - ], - [ - 0x75 - ] - ), - ("Mountain Entry", "Regular", "None"): ( - [ - 0x0a - ], - [ - 0x4a - ] - ), - ("Woods Pass", "Regular", "None"): ( - [ - 0x10 - ], - [ - 0x50 - ] - ), - ("Fortune", "Regular", "None"): ( - [ - 0x11 - ], - [ - 0x51 - ] - ), - ("Whirlpools", "Regular", "Pond"): ( - [ - 0x12 - ], - [ - 0x52 - ] - ), - ("Whirlpools", "Regular", "Witch"): ( - [ - 0x15 - ], - [ - 0x55 - ] - ), - ("Whirlpools", "Regular", "CWhirlpool"): ( - [ - 0x33 - ], - [ - 0x73 - ] - ), - ("Whirlpools", "Regular", "Southeast"): ( - [ - 0x3f - ], - [ - 0x7f - ] - ), - ("Castle", "Entrance", "None"): ( - [ - 0x13, 0x14 - ], - [ - 0x53, 0x54 - ] - ), - ("Castle", "Regular", "None"): ( - [ - 0x1a, 0x1b - ], - [ - 0x5a, 0x5b - ] - ), - ("Witch", "Regular", "None"): ( - [ - 0x16 - ], - [ - 0x56 - ] - ), - ("Water Approach", "Regular", "None"): ( - [ - 0x17 - ], - [ - 0x57 - ] - ), - ("Village", "Regular", "None"): ( - [ - 0x18 - ], - [ - 0x58 - ] - ), - ("Wooden Bridge", "Regular", "None"): ( - [ - 0x1d - ], - [ - 0x5d - ] - ), - ("Eastern", "Regular", "None"): ( - [ - 0x1e - ], - [ - 0x5e - ] - ), - ("Blacksmith", "Regular", "None"): ( - [ - 0x22 - ], - [ - 0x62 - ] - ), - ("Dunes", "Regular", "None"): ( - [ - 0x25 - ], - [ - 0x65 - ] - ), - ("Game", "Regular", "None"): ( - [ - 0x28, 0x29 - ], - [ - 0x68, 0x69 - ] - ), - ("Grove", "Regular", "None"): ( - [ - 0x2a - ], - [ - 0x6a - ] - ), - ("Central Bonk Rocks", "Regular", "None"): ( - [ - 0x2b - ], - [ - 0x6b - ] - ), - ("Links", "Regular", "None"): ( - [ - 0x2c - ], - [ - 0x6c - ] - ), - ("Tree Line", "Regular", "None"): ( - [ - 0x2e - ], - [ - 0x6e - ] - ), - ("Nook", "Regular", "None"): ( - [ - 0x2f - ], - [ - 0x6f - ] - ), - ("Desert", "Regular", "None"): ( - [ - 0x30, 0x3a - ], - [ - 0x70, 0x7a - ] - ), - ("Grove Approach", "Regular", "None"): ( - [ - 0x32 - ], - [ - 0x72 - ] - ), - ("Hype", "Regular", "None"): ( - [ - 0x34 - ], - [ - 0x74 - ] - ), - ("Shopping Mall", "Regular", "None"): ( - [ - 0x37 - ], - [ - 0x77 - ] - ), - ("Swamp", "Regular", "None"): ( - [ - 0x3b - ], - [ - 0x7b - ] - ), - ("South Pass", "Regular", "None"): ( - [ - 0x3c - ], - [ - 0x7c - ] - ) -} - parallel_links = bidict({'Lost Woods SW': 'Skull Woods SW', 'Lost Woods SC': 'Skull Woods SC', 'Lost Woods SE': 'Skull Woods SE', diff --git a/OverworldShuffle.py b/OverworldShuffle.py index 37a7fbc8..3087ebdf 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -1,11 +1,12 @@ import RaceRandom as random, logging, copy -from collections import OrderedDict +from collections import OrderedDict, defaultdict from DungeonGenerator import GenerationException from BaseClasses import OWEdge, WorldType, RegionType, Direction, Terrain, PolSlot, Entrance from Regions import mark_dark_world_regions, mark_light_world_regions -from OWEdges import OWTileRegions, OWTileGroups, OWEdgeGroups, OWExitTypes, OpenStd, parallel_links, IsParallel +from OWEdges import OWTileRegions, OWEdgeGroups, OWExitTypes, OpenStd, parallel_links, IsParallel +from Utils import bidict -version_number = '0.2.7.2' +version_number = '0.2.7.3' version_branch = '' __version__ = '%s%s' % (version_number, version_branch) @@ -101,8 +102,8 @@ def link_overworld(world, player): else: raise NotImplementedError('Invalid OW Edge swap scenario') return new_groups - - tile_groups = reorganize_tile_groups(world, player) + + tile_groups = define_tile_groups(world, player, False) trimmed_groups = copy.deepcopy(OWEdgeGroups) swapped_edges = list() @@ -133,19 +134,20 @@ def link_overworld(world, player): trimmed_groups[(std, region, axis, terrain, IsParallel.No, 1)][1].append(['Frog WC']) trimmed_groups[group] = (forward_edges, back_edges) + connected_edges = [] + if world.owShuffle[player] != 'vanilla': + trimmed_groups = remove_reserved(world, trimmed_groups, connected_edges, player) + trimmed_groups = reorganize_groups(world, trimmed_groups, player) + # tile shuffle logging.getLogger('').debug('Swapping overworld tiles') if world.owMixed[player]: - swapped_edges = shuffle_tiles(world, tile_groups, world.owswaps[player], player) - - # move swapped regions/edges to other world - trimmed_groups = performSwap(trimmed_groups, swapped_edges) - assert len(swapped_edges) == 0, 'Not all edges were swapped successfully: ' + ', '.join(swapped_edges ) + swapped_edges = shuffle_tiles(world, tile_groups, world.owswaps[player], False, player) update_world_regions(world, player) # update spoiler - s = list(map(lambda x: ' ' if x not in world.owswaps[player][0] else 'S', [i for i in range(0x40)])) + s = list(map(lambda x: ' ' if x not in world.owswaps[player][0] else 'S', [i for i in range(0x40, 0x82)])) text_output = tile_swap_spoiler_table.replace('s', '%s') % ( s[0x02], s[0x07], s[0x00], s[0x03], s[0x05], s[0x00], s[0x02],s[0x03], s[0x05], s[0x07], s[0x0a], s[0x0f], @@ -157,9 +159,9 @@ def link_overworld(world, player): s[0x30], s[0x32],s[0x33],s[0x34],s[0x35], s[0x37], s[0x22], s[0x25], s[0x3a],s[0x3b],s[0x3c], s[0x3f], s[0x28],s[0x29],s[0x2a],s[0x2b],s[0x2c],s[0x2d],s[0x2e],s[0x2f], - s[0x32],s[0x33],s[0x34], s[0x37], + s[0x40], s[0x32],s[0x33],s[0x34], s[0x37], s[0x30], s[0x35], - s[0x3a],s[0x3b],s[0x3c], s[0x3f]) + s[0x41], s[0x3a],s[0x3b],s[0x3c], s[0x3f]) world.spoiler.set_map('swaps', text_output, world.owswaps[player][0], player) # apply tile logical connections @@ -175,42 +177,77 @@ def link_overworld(world, player): # crossed shuffle logging.getLogger('').debug('Crossing overworld edges') - if world.owCrossed[player] in ['grouped', 'limited', 'chaos']: + crossed_edges = list() + + # more Maze Race/Suburb/Frog/Dig Game fixes + 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'] + + #TODO: Revisit with changes to Limited/Allowed + if world.owCrossed[player] not in ['none', 'grouped', 'polar', 'chaos']: + for edge in swapped_edges: + if edge not in parallel_links_new and edge not in parallel_links_new.inverse: + crossed_edges.append(edge) + + if world.owCrossed[player] in ['grouped', 'limited'] or (world.owShuffle[player] == 'vanilla' and world.owCrossed[player] == 'chaos'): if world.owCrossed[player] == 'grouped': - ow_crossed_tiles = [[],[],[]] - crossed_edges = shuffle_tiles(world, tile_groups, ow_crossed_tiles, player) - elif world.owCrossed[player] in ['limited', 'chaos']: - crossed_edges = list() + # the idea is to XOR the new swaps with the ones from Mixed so that non-parallel edges still work + # Polar corresponds to Grouped with no swaps in ow_crossed_tiles_mask + ow_crossed_tiles_mask = [[],[],[]] + crossed_edges = shuffle_tiles(world, define_tile_groups(world, player, True), ow_crossed_tiles_mask, True, player) + ow_crossed_tiles = [i for i in range(0x82) if (i in world.owswaps[player][0]) != (i in ow_crossed_tiles_mask[0])] + + # update spoiler + s = list(map(lambda x: 'O' if x not in ow_crossed_tiles else 'X', [i for i in range(0x40, 0x82)])) + text_output = tile_swap_spoiler_table.replace('s', '%s') % ( s[0x02], s[0x07], + s[0x00], s[0x03], s[0x05], + s[0x00], s[0x02],s[0x03], s[0x05], s[0x07], s[0x0a], s[0x0f], + s[0x0a], s[0x0f], + s[0x10],s[0x11],s[0x12],s[0x13],s[0x14],s[0x15],s[0x16],s[0x17], s[0x10],s[0x11],s[0x12],s[0x13],s[0x14],s[0x15],s[0x16],s[0x17], + s[0x18], s[0x1a],s[0x1b], s[0x1d],s[0x1e], + s[0x22], s[0x25], s[0x1a], s[0x1d], + s[0x28],s[0x29],s[0x2a],s[0x2b],s[0x2c],s[0x2d],s[0x2e],s[0x2f], s[0x18], s[0x1b], s[0x1e], + s[0x30], s[0x32],s[0x33],s[0x34],s[0x35], s[0x37], s[0x22], s[0x25], + s[0x3a],s[0x3b],s[0x3c], s[0x3f], + s[0x28],s[0x29],s[0x2a],s[0x2b],s[0x2c],s[0x2d],s[0x2e],s[0x2f], + s[0x40], s[0x32],s[0x33],s[0x34], s[0x37], + 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) + else: crossed_candidates = list() for group in trimmed_groups.keys(): (mode, wrld, dir, terrain, parallel, count) = group - if parallel == IsParallel.Yes and wrld == WorldType.Light and (mode == OpenStd.Open or world.mode[player] != 'standard'): + if wrld == WorldType.Light and mode != OpenStd.Standard: for (forward_set, back_set) in zip(trimmed_groups[group][0], trimmed_groups[group][1]): - if world.owKeepSimilar[player]: - if world.owCrossed[player] == 'chaos' and random.randint(0, 1): - for edge in forward_set: - crossed_edges.append(edge) - elif world.owCrossed[player] == 'limited': - crossed_candidates.append(forward_set) - else: - for edge in forward_set: + if forward_set[0] in parallel_links_new or forward_set[0] in parallel_links_new.inverse: + if world.owKeepSimilar[player]: if world.owCrossed[player] == 'chaos' and random.randint(0, 1): - crossed_edges.append(edge) + for edge in forward_set: + crossed_edges.append(edge) elif world.owCrossed[player] == 'limited': - crossed_candidates.append([edge]) + crossed_candidates.append(forward_set) + else: + for edge in forward_set: + if world.owCrossed[player] == 'chaos' and random.randint(0, 1): + crossed_edges.append(edge) + elif world.owCrossed[player] == 'limited': + crossed_candidates.append([edge]) if world.owCrossed[player] == 'limited': random.shuffle(crossed_candidates) for edge_set in crossed_candidates[:9]: for edge in edge_set: crossed_edges.append(edge) for edge in copy.deepcopy(crossed_edges): - if edge in parallel_links: - crossed_edges.append(parallel_links[edge]) - elif edge in parallel_links.inverse: - crossed_edges.append(parallel_links.inverse[edge][0]) - - trimmed_groups = performSwap(trimmed_groups, crossed_edges) - assert len(crossed_edges) == 0, 'Not all edges were crossed successfully: ' + ', '.join(crossed_edges) + if edge in parallel_links_new: + crossed_edges.append(parallel_links_new[edge]) + elif edge in parallel_links_new.inverse: + crossed_edges.append(parallel_links_new.inverse[edge][0]) + + # after tile swap and crossed, determine edges that need to swap + edges_to_swap = [e for e in swapped_edges+crossed_edges if (e not in swapped_edges) or (e not in crossed_edges)] # whirlpool shuffle logging.getLogger('').debug('Shuffling whirlpools') @@ -233,14 +270,14 @@ def link_overworld(world, player): else: if ((world.owCrossed[player] == 'none' or (world.owCrossed[player] == 'polar' and not world.owMixed[player])) and (world.get_region(from_region, player).type == RegionType.LightWorld)) \ or world.owCrossed[player] not in ['none', 'polar', 'grouped'] \ - or (world.owCrossed[player] == 'grouped' and ((from_owid < 0x40) == (from_owid not in ow_crossed_tiles[0]))): + or (world.owCrossed[player] == 'grouped' and ((world.get_region(from_region, player).type == RegionType.LightWorld) == (from_owid not in ow_crossed_tiles))): whirlpool_candidates[0].append(tuple((from_owid, from_whirlpool, from_region))) else: whirlpool_candidates[1].append(tuple((from_owid, from_whirlpool, from_region))) if ((world.owCrossed[player] == 'none' or (world.owCrossed[player] == 'polar' and not world.owMixed[player])) and (world.get_region(to_region, player).type == RegionType.LightWorld)) \ or world.owCrossed[player] not in ['none', 'polar', 'grouped'] \ - or (world.owCrossed[player] == 'grouped' and ((to_owid < 0x40) == (to_owid not in ow_crossed_tiles[0]))): + or (world.owCrossed[player] == 'grouped' and ((world.get_region(to_region, player).type == RegionType.LightWorld) == (to_owid not in ow_crossed_tiles))): whirlpool_candidates[0].append(tuple((to_owid, to_whirlpool, to_region))) else: whirlpool_candidates[1].append(tuple((to_owid, to_whirlpool, to_region))) @@ -260,9 +297,12 @@ def link_overworld(world, player): # layout shuffle logging.getLogger('').debug('Shuffling overworld layout') - connected_edges = [] if world.owShuffle[player] == 'vanilla': + # apply outstanding swaps + trimmed_groups = performSwap(trimmed_groups, edges_to_swap) + assert len(edges_to_swap) == 0, 'Not all edges were swapped successfully: ' + ', '.join(edges_to_swap) + # vanilla transitions groups = list(trimmed_groups.values()) for (forward_edge_sets, back_edge_sets) in groups: @@ -285,8 +325,7 @@ def link_overworld(world, player): connect_custom(world, connected_edges, player) # layout shuffle - trimmed_groups = remove_reserved(world, trimmed_groups, connected_edges, player) - groups = reorganize_groups(world, trimmed_groups, player) + groups = adjust_edge_groups(world, trimmed_groups, edges_to_swap, player) tries = 20 valid_layout = False @@ -429,7 +468,7 @@ def link_overworld(world, player): # update spoiler new_spots = list(map(lambda o: flute_data[o][1], new_spots)) s = list(map(lambda x: ' ' if x not in new_spots else 'F', [i for i in range(0x40)])) - text_output = tile_swap_spoiler_table.replace('s', '%s') % ( s[0x02], s[0x07], + text_output = flute_spoiler_table.replace('s', '%s') % ( s[0x02], s[0x07], s[0x00], s[0x03], s[0x05], s[0x00], s[0x02],s[0x03], s[0x05], s[0x07], s[0x0a], s[0x0f], s[0x0a], s[0x0f], @@ -505,33 +544,94 @@ def connect_two_way(world, edgename1, edgename2, player, connected_edges=None): if not (parallel_forward_edge in connected_edges) and not (parallel_back_edge in connected_edges): connect_two_way(world, parallel_forward_edge, parallel_back_edge, player, connected_edges) except KeyError: - # TODO: Figure out why non-parallel edges are getting into parallel groups raise KeyError('No parallel edge for edge %s' % edgename2) -def shuffle_tiles(world, groups, result_list, player): +def shuffle_tiles(world, groups, result_list, do_grouped, player): swapped_edges = list() - valid_whirlpool_parity = False + group_parity = {} + for group_data in groups: + group = group_data[0] + parity = [0, 0, 0, 0, 0] + # vertical land + if 0x00 in group: + parity[0] += 1 + if 0x0f in group: + parity[0] += 1 + if 0x80 in group: + parity[0] -= 1 + if 0x81 in group: + parity[0] -= 1 + # horizontal land + if 0x1a in group: + parity[1] -= 1 + if 0x1b in group: + parity[1] += 1 + if 0x28 in group: + parity[1] += 1 + if 0x29 in group: + parity[1] -= 1 + if 0x30 in group: + parity[1] -= 2 + if 0x3a in group: + parity[1] += 2 + # horizontal water + if 0x2d in group: + parity[2] += 1 + if 0x80 in group: + parity[2] -= 1 + # whirlpool + if 0x0f in group: + parity[3] += 1 + if 0x12 in group: + parity[3] += 1 + if 0x33 in group: + parity[3] += 1 + if 0x35 in group: + parity[3] += 1 + # dropdown exit + if 0x00 in group or 0x02 in group or 0x13 in group or 0x15 in group or 0x18 in group or 0x22 in group: + parity[4] += 1 + if 0x1b in group and world.mode[player] != 'standard': + parity[4] += 1 + if 0x1b in group and world.shuffle_ganon: + parity[4] -= 1 + group_parity[group[0]] = parity + + attempts = 1000 + while True: + if attempts == 0: # expected to only occur with custom swaps + raise GenerationException('Could not find valid tile swaps') - while not valid_whirlpool_parity: # tile shuffle happens here removed = list() - for group in groups.keys(): - # if group[0] in ['Links', 'Central Bonk Rocks', 'Castle']: # TODO: Standard + Inverted + for group in groups: + # if 0x1b in group[0] or (0x1a in group[0] and world.owCrossed[player] == 'none'): # TODO: Standard + Inverted if random.randint(0, 1): removed.append(group) - + # save shuffled tiles to list new_results = [[],[],[]] - for group in groups.keys(): + for group in groups: if group not in removed: - (owids, lw_regions, dw_regions) = groups[group] + (owids, lw_regions, dw_regions) = group (exist_owids, exist_lw_regions, exist_dw_regions) = new_results exist_owids.extend(owids) exist_lw_regions.extend(lw_regions) exist_dw_regions.extend(dw_regions) - # check whirlpool parity - valid_whirlpool_parity = world.owCrossed[player] not in ['none', 'grouped'] or len([o for o in new_results[0] if o in [0x0f, 0x12, 0x15, 0x33, 0x35, 0x3f, 0x55, 0x7f]]) % 2 == 0 + parity = [sum(group_parity[group[0][0]][i] for group in groups if group not in removed) for i in range(5)] + parity[3] %= 2 # actual parity + if (world.owCrossed[player] == 'none' or do_grouped) and parity[:4] != [0, 0, 0, 0]: + attempts -= 1 + continue + # ensure sanc can be placed in LW in certain modes + if not do_grouped and world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'lean', 'crossed', 'insanity'] and world.mode[player] != 'inverted' and (world.doorShuffle[player] != 'crossed' or world.intensity[player] < 3 or world.mode[player] == 'standard'): + free_dw_drops = parity[4] + (1 if world.shuffle_ganon else 0) + free_drops = 6 + (1 if world.mode[player] != 'standard' else 0) + (1 if world.shuffle_ganon else 0) + if free_dw_drops == free_drops: + attempts -= 1 + continue + break (exist_owids, exist_lw_regions, exist_dw_regions) = result_list exist_owids.extend(new_results[0]) @@ -539,7 +639,7 @@ def shuffle_tiles(world, groups, result_list, player): exist_dw_regions.extend(new_results[2]) # replace LW edges with DW - if world.owCrossed[player] != 'polar': + if world.owCrossed[player] not in ['polar', 'grouped', 'chaos'] or do_grouped: # in polar, the actual edge connections remain vanilla def getSwappedEdges(world, lst, player): for regionname in lst: @@ -553,43 +653,75 @@ def shuffle_tiles(world, groups, result_list, player): return swapped_edges -def reorganize_tile_groups(world, player): - def get_group_key(group): - #(name, groupType, whirlpoolGroup) = group - new_group = list(group) - if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'simple', 'restricted']: - new_group[1] = None - if not world.owWhirlpoolShuffle[player] and world.owCrossed[player] == 'none': - new_group[2] = None - return tuple(new_group) +def define_tile_groups(world, player, do_grouped): + groups = [[i, i + 0x40] for i in range(0x40)] + + def get_group(id): + for group in groups: + if id in group: + return group + + def merge_groups(tile_links): + for link in tile_links: + merged_group = [] + for id in link: + if id not in merged_group: + group = get_group(id) + groups.remove(group) + merged_group += group + groups.append(merged_group) def can_shuffle_group(group): - (name, groupType, whirlpoolGroup) = group - return name not in ['Castle', 'Links', 'Central Bonk Rocks'] \ - or (world.mode[player] != 'standard' and (name != 'Castle' \ - or world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull'] \ - or (world.mode[player] == 'open' and world.doorShuffle[player] == 'crossed') \ - or world.owCrossed[player] in ['grouped', 'polar', 'chaos'])) \ - or (world.mode[player] == 'standard' and world.shuffle[player] in ['lean', 'crossed', 'insanity'] and name == 'Castle' and groupType == 'Entrance') - - groups = {} - for group in OWTileGroups.keys(): - if can_shuffle_group(group): - groups[get_group_key(group)] = ([], [], []) + # escape sequence should stay normal in standard + if world.mode[player] == 'standard' and (0x1b in group or 0x2b in group or 0x2c in group): + return False + + # sanctuary/chapel should not be swapped if S+Q guaranteed to output on that screen + if 0x13 in group and ((world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull'] \ + and (world.mode[player] in ['standard', 'inverted'] or world.doorShuffle[player] != 'crossed' or world.intensity[player] < 3)) \ + or (world.shuffle[player] == 'lite' and world.mode[player] == 'inverted')): + return False + + return True - for group in OWTileGroups.keys(): + for i in [0x00, 0x03, 0x05, 0x18, 0x1b, 0x1e, 0x30, 0x35]: + groups.remove(get_group(i + 1)) + groups.remove(get_group(i + 8)) + groups.remove(get_group(i + 9)) + groups.append([0x80]) + groups.append([0x81]) + + if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'simple']: + merge_groups([[0x03, 0x0a], [0x28, 0x29]]) + + if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'simple', 'restricted', 'full', 'lite']: + merge_groups([[0x13, 0x14]]) + + if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'simple', 'restricted']: + merge_groups([[0x05, 0x07]]) + + if world.shuffle[player] == 'vanilla' or (world.mode[player] == 'standard' and world.shuffle[player] in ['dungeonssimple', 'dungeonsfull']): + merge_groups([[0x13, 0x14, 0x1b]]) + + 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]]) + + if world.owShuffle[player] == 'parallel' and world.owKeepSimilar[player] and world.owCrossed[player] == 'none': + merge_groups([[0x28, 0x29]]) + + if not world.owWhirlpoolShuffle[player] and (world.owCrossed[player] == 'none' or do_grouped): + merge_groups([[0x0f, 0x35], [0x12, 0x15, 0x33, 0x3f]]) + + tile_groups = [] + for group in groups: if can_shuffle_group(group): - (lw_owids, dw_owids) = OWTileGroups[group] - (exist_owids, exist_lw_regions, exist_dw_regions) = groups[get_group_key(group)] - exist_owids.extend(lw_owids) - exist_owids.extend(dw_owids) - for owid in lw_owids: - exist_lw_regions.extend(OWTileRegions.inverse[owid]) - for owid in dw_owids: - exist_dw_regions.extend(OWTileRegions.inverse[owid]) - groups[get_group_key(group)] = (exist_owids, exist_lw_regions, exist_dw_regions) - - return groups + lw_regions = [] + dw_regions = [] + for id in group: + (lw_regions if id < 0x40 or id >= 0x80 else dw_regions).extend(OWTileRegions.inverse[id]) + tile_groups.append((group, lw_regions, dw_regions)) + + return tile_groups def remove_reserved(world, groupedlist, connected_edges, player): new_grouping = {} @@ -597,7 +729,6 @@ def remove_reserved(world, groupedlist, connected_edges, player): new_grouping[group] = ([], []) for group in groupedlist.keys(): - (_, region, _, _, _, _) = group (forward_edges, back_edges) = groupedlist[group] # remove edges already connected (thru plando and other forced connections) @@ -605,15 +736,6 @@ def remove_reserved(world, groupedlist, connected_edges, player): forward_edges = list(list(filter((edge).__ne__, i)) for i in forward_edges) back_edges = list(list(filter((edge).__ne__, i)) for i in back_edges) - # remove parallel edges from pool, since they get added during shuffle - if world.owShuffle[player] == 'parallel' and region == WorldType.Dark: - for edge in parallel_links: - forward_edges = list(list(filter((parallel_links[edge]).__ne__, i)) for i in forward_edges) - back_edges = list(list(filter((parallel_links[edge]).__ne__, i)) for i in back_edges) - for edge in parallel_links.inverse: - forward_edges = list(list(filter((parallel_links.inverse[edge][0]).__ne__, i)) for i in forward_edges) - back_edges = list(list(filter((parallel_links.inverse[edge][0]).__ne__, i)) for i in back_edges) - forward_edges = list(filter(([]).__ne__, forward_edges)) back_edges = list(filter(([]).__ne__, back_edges)) @@ -656,7 +778,26 @@ def reorganize_groups(world, groups, player): exist_back_edges.extend(back_edges) new_grouping[new_group] = (exist_forward_edges, exist_back_edges) - return list(new_grouping.values()) + return new_grouping + +def adjust_edge_groups(world, trimmed_groups, edges_to_swap, player): + groups = defaultdict(lambda: ([],[])) + for (key, group) in trimmed_groups.items(): + (mode, wrld, dir, terrain, parallel, count) = key + if mode == OpenStd.Standard: + groups[key] = group + else: + if world.owCrossed[player] == 'chaos': + groups[(mode, None, dir, terrain, parallel, count)][0].extend(group[0]) + groups[(mode, None, dir, terrain, parallel, count)][1].extend(group[1]) + else: + for i in range(2): + for edge_set in group[i]: + new_world = int(wrld) + if edge_set[0] in edges_to_swap: + new_world += 1 + groups[(mode, WorldType(new_world % 2), dir, terrain, parallel, count)][i].append(edge_set) + return list(groups.values()) def create_flute_exits(world, player): for region in (r for r in world.regions if r.player == player and r.terrain == Terrain.Land and r.name not in ['Zoras Domain', 'Master Sword Meadow', 'Hobo Bridge']): @@ -1807,6 +1948,26 @@ D(18)|s ss ss | +-+-+-+-+-+-+-+-+ E(20)| s s | D(18)| |s| |s| | F(28)|ssssssss| | s +-+ s +-+ s | G(30)|s ssss s| E(20)| |s| |s| | +H(38)| sss s| +-+-+-+-+-+-+-+-+ + +--------+ F(28)|s|s|s|s|s|s|s|s| + +-+ +-+-+-+-+-+-+-+-+ + Ped/Hobo: |s| G(30)| |s|s|s| |s| + +-+ | s +-+-+-+ s +-+ + Zora: |s| H(38)| |s|s|s| |s| + +-+ +---+-+-+-+---+-+""" + +flute_spoiler_table = \ +""" 0 1 2 3 4 5 6 7 + +---+-+---+---+-+ + 01234567 A(00)| |s| | |s| + +--------+ | s +-+ s | s +-+ +A(00)|s ss s s| B(08)| |s| | |s| +B(08)| s s| +-+-+-+-+-+-+-+-+ +C(10)|ssssssss| C(10)|s|s|s|s|s|s|s|s| +D(18)|s ss ss | +-+-+-+-+-+-+-+-+ +E(20)| s s | D(18)| |s| |s| | +F(28)|ssssssss| | s +-+ s +-+ s | +G(30)|s ssss s| E(20)| |s| |s| | H(38)| sss s| +-+-+-+-+-+-+-+-+ +--------+ F(28)|s|s|s|s|s|s|s|s| +-+-+-+-+-+-+-+-+ diff --git a/Rom.py b/Rom.py index 80d6ee95..4c5e1114 100644 --- a/Rom.py +++ b/Rom.py @@ -33,7 +33,7 @@ from source.classes.SFX import randomize_sfx JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '538bf256ff03bb7576991114395eeccc' +RANDOMIZERBASEHASH = 'fc6f1d6ba782d08ac92600c31cc7ee43' class JsonRom(object): diff --git a/asm/owrando.asm b/asm/owrando.asm index 04a6d576..01e29031 100644 --- a/asm/owrando.asm +++ b/asm/owrando.asm @@ -39,8 +39,17 @@ org $04E8B4 Overworld_LoadSpecialOverworld: -org $05af75 +; mirror hooks +org $02FBAB +JSL OWMirrorSpriteRestore : NOP +org $05AF75 +Sprite_6C_MirrorPortal: jsl OWPreserveMirrorSprite : nop #2 ; LDA $7EF3CA : BNE $05AFDF +org $05AFDF +Sprite_6C_MirrorPortal_missing_mirror: +JML OWMirrorSpriteDelete : NOP ; STZ $0DD0,X : BRA $05AFF1 +org $0ABFBF +JSL OWMirrorSpriteOnMap : BRA + : NOP #6 : + ; whirlpool shuffle cross world change org $02b3bd @@ -196,38 +205,61 @@ OWWhirlpoolUpdate: rtl } +OWMirrorSpriteOnMap: +{ + lda.w $1ac0,x : bit.b #$f0 : beq .continue + lda.b #$00 : rtl + .continue + ora.w $1ab0,x + ora.w $1ad0,x + ora.w $1ae0,x + rtl +} OWPreserveMirrorSprite: { - lda.l OWMode+1 : and.b #!FLAG_OW_CROSSED : beq .vanilla - rtl ; if OW Crossed, skip world check and continue + lda.l OWMode+1 : and.b #!FLAG_OW_CROSSED : beq .vanilla ; if OW Crossed, skip world check and continue + lda $10 : cmp.b #$0f : beq .vanilla + rtl .vanilla - lda InvertedMode : beq + - lda $7ef3ca : beq .deleteMirror + lda.l InvertedMode : beq + + lda.l $7ef3ca : beq .deleteMirror rtl - + lda $7ef3ca : bne .deleteMirror + + lda.l $7ef3ca : bne .deleteMirror rtl .deleteMirror - pla : lda #$de : pha ; in vanilla, if in dark world, jump to $05afdf - rtl + lda.b $10 : cmp.b #$0f : bne + + jsr.w OWMirrorSpriteMove ; if performing mirror superbunny + + pla : pla : pla : jml Sprite_6C_MirrorPortal_missing_mirror } OWMirrorSpriteMove: { lda.l OWMode+1 : and.b #!FLAG_OW_CROSSED : beq + - lda $1acf : eor #$80 : sta $1acf - + lda #$2c : jml.l $07A985 ; what we wrote over + lda.w $1acf : ora.b #$40 : sta.w $1acf + + rts +} +OWMirrorSpriteBonk: +{ + jsr.w OWMirrorSpriteMove + lda.b #$2c : jml.l SetGameModeLikeMirror ; what we wrote over +} +OWMirrorSpriteDelete: +{ + stz.w $0dd0,x ; what we wrote over + jsr.w OWMirrorSpriteMove + jml Sprite_6C_MirrorPortal_dont_do_warp } OWMirrorSpriteRestore: { lda.l OWMode+1 : and.b #!FLAG_OW_CROSSED : beq .return - lda InvertedMode : beq + - lda $7ef3ca : beq .return + lda.l InvertedMode : beq + + lda.l $7ef3ca : beq .return bra .restorePortal - + lda $7ef3ca : bne .return + + lda.l $7ef3ca : bne .return .restorePortal - lda $1acf : and #$0f : sta $1acf + lda.w $1acf : and.b #$0f : sta.w $1acf .return rep #$30 : lda.w $04AC ; what we wrote over @@ -603,7 +635,7 @@ OWWorldUpdate: ; x = owid of destination screen cmp #0 : beq + : lda #1 + cmp.l InvertedMode : bne + lda $1acf : and #$0f : sta $1acf : bra .playSfx ; bring portal back into position - + lda $1acf : eor #$80 : sta $1acf ; move portal off screen + + lda $1acf : ora #$40 : sta $1acf ; move portal off screen .playSfx lda #$38 : sta $012f ; play sfx - #$3b is an alternative diff --git a/data/base2current.bps b/data/base2current.bps index a080ac84..bcd2bb56 100644 Binary files a/data/base2current.bps and b/data/base2current.bps differ