From cc7145c6b80d482ce91971c26b0cb597e84e3687 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sat, 7 Mar 2020 23:35:55 +0100 Subject: [PATCH 1/8] remove collections_extended dependency and replace with much faster stdlib Counter --- BaseClasses.py | 69 +++++++++++++++++++++++++------------------------- 1 file changed, 34 insertions(+), 35 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 96752d8f..37074f22 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -2,7 +2,7 @@ import copy from enum import Enum, unique, Flag import logging import json -from collections import OrderedDict, deque, defaultdict +from collections import OrderedDict, Counter, deque, defaultdict from source.classes.BabelFish import BabelFish from EntranceShuffle import door_addresses @@ -235,41 +235,41 @@ class World(object): if ret.has('Golden Sword', item.player): pass elif ret.has('Tempered Sword', item.player) and self.difficulty_requirements[item.player].progressive_sword_limit >= 4: - ret.prog_items.add(('Golden Sword', item.player)) + ret.prog_items['Golden Sword', item.player] += 1 elif ret.has('Master Sword', item.player) and self.difficulty_requirements[item.player].progressive_sword_limit >= 3: - ret.prog_items.add(('Tempered Sword', item.player)) + ret.prog_items['Tempered Sword', item.player] += 1 elif ret.has('Fighter Sword', item.player) and self.difficulty_requirements[item.player].progressive_sword_limit >= 2: - ret.prog_items.add(('Master Sword', item.player)) + ret.prog_items['Master Sword', item.player] += 1 elif self.difficulty_requirements[item.player].progressive_sword_limit >= 1: - ret.prog_items.add(('Fighter Sword', item.player)) + ret.prog_items['Fighter Sword', item.player] += 1 elif 'Glove' in item.name: if ret.has('Titans Mitts', item.player): pass elif ret.has('Power Glove', item.player): - ret.prog_items.add(('Titans Mitts', item.player)) + ret.prog_items['Titans Mitts', item.player] += 1 else: - ret.prog_items.add(('Power Glove', item.player)) + ret.prog_items['Power Glove', item.player] += 1 elif 'Shield' in item.name: if ret.has('Mirror Shield', item.player): pass elif ret.has('Red Shield', item.player) and self.difficulty_requirements[item.player].progressive_shield_limit >= 3: - ret.prog_items.add(('Mirror Shield', item.player)) + ret.prog_items['Mirror Shield', item.player] += 1 elif ret.has('Blue Shield', item.player) and self.difficulty_requirements[item.player].progressive_shield_limit >= 2: - ret.prog_items.add(('Red Shield', item.player)) + ret.prog_items['Red Shield', item.player] += 1 elif self.difficulty_requirements[item.player].progressive_shield_limit >= 1: - ret.prog_items.add(('Blue Shield', item.player)) + ret.prog_items['Blue Shield', item.player] += 1 elif 'Bow' in item.name: if ret.has('Silver Arrows', item.player): pass elif ret.has('Bow', item.player) and self.difficulty_requirements[item.player].progressive_bow_limit >= 2: - ret.prog_items.add(('Silver Arrows', item.player)) + ret.prog_items['Silver Arrows', item.player] += 1 elif self.difficulty_requirements[item.player].progressive_bow_limit >= 1: - ret.prog_items.add(('Bow', item.player)) + ret.prog_items['Bow', item.player] += 1 elif item.name.startswith('Bottle'): if ret.bottle_count(item.player) < self.difficulty_requirements[item.player].progressive_bottle_limit: - ret.prog_items.add((item.name, item.player)) + ret.prog_items[item.name, item.player] += 1 elif item.advancement or item.smallkey or item.bigkey: - ret.prog_items.add((item.name, item.player)) + ret.prog_items[item.name, item.player] += 1 for item in self.itempool: soft_collect(item) @@ -408,7 +408,7 @@ class World(object): class CollectionState(object): def __init__(self, parent): - self.prog_items = bag() + self.prog_items = Counter() self.world = parent self.reachable_regions = {player: set() for player in range(1, parent.players + 1)} self.colored_regions = {player: {} for player in range(1, parent.players + 1)} @@ -584,14 +584,14 @@ class CollectionState(object): def has(self, item, player, count=1): if count == 1: return (item, player) in self.prog_items - return self.prog_items.count((item, player)) >= count + return self.prog_items[item, player] >= count def has_key(self, item, player, count=1): if self.world.retro[player]: return self.can_buy_unlimited('Small Key (Universal)', player) if count == 1: return (item, player) in self.prog_items - return self.prog_items.count((item, player)) >= count + return self.prog_items[item, player] >= count def can_buy_unlimited(self, item, player): for shop in self.world.shops: @@ -600,7 +600,7 @@ class CollectionState(object): return False def item_count(self, item, player): - return self.prog_items.count((item, player)) + return self.prog_items[item, player] def has_crystals(self, count, player): crystals = ['Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 5', 'Crystal 6', 'Crystal 7'] @@ -734,46 +734,46 @@ class CollectionState(object): if self.has('Golden Sword', item.player): pass elif self.has('Tempered Sword', item.player) and self.world.difficulty_requirements[item.player].progressive_sword_limit >= 4: - self.prog_items.add(('Golden Sword', item.player)) + self.prog_items['Golden Sword', item.player] += 1 changed = True elif self.has('Master Sword', item.player) and self.world.difficulty_requirements[item.player].progressive_sword_limit >= 3: - self.prog_items.add(('Tempered Sword', item.player)) + self.prog_items['Tempered Sword', item.player] += 1 changed = True elif self.has('Fighter Sword', item.player) and self.world.difficulty_requirements[item.player].progressive_sword_limit >= 2: - self.prog_items.add(('Master Sword', item.player)) + self.prog_items['Master Sword', item.player] += 1 changed = True elif self.world.difficulty_requirements[item.player].progressive_sword_limit >= 1: - self.prog_items.add(('Fighter Sword', item.player)) + self.prog_items['Fighter Sword', item.player] += 1 changed = True elif 'Glove' in item.name: if self.has('Titans Mitts', item.player): pass elif self.has('Power Glove', item.player): - self.prog_items.add(('Titans Mitts', item.player)) + self.prog_items['Titans Mitts', item.player] += 1 changed = True else: - self.prog_items.add(('Power Glove', item.player)) + self.prog_items['Power Glove', item.player] += 1 changed = True elif 'Shield' in item.name: if self.has('Mirror Shield', item.player): pass elif self.has('Red Shield', item.player) and self.world.difficulty_requirements[item.player].progressive_shield_limit >= 3: - self.prog_items.add(('Mirror Shield', item.player)) + self.prog_items['Mirror Shield', item.player] += 1 changed = True elif self.has('Blue Shield', item.player) and self.world.difficulty_requirements[item.player].progressive_shield_limit >= 2: - self.prog_items.add(('Red Shield', item.player)) + self.prog_items['Red Shield', item.player] += 1 changed = True elif self.world.difficulty_requirements[item.player].progressive_shield_limit >= 1: - self.prog_items.add(('Blue Shield', item.player)) + self.prog_items['Blue Shield', item.player] += 1 changed = True elif 'Bow' in item.name: if self.has('Silver Arrows', item.player): pass elif self.has('Bow', item.player): - self.prog_items.add(('Silver Arrows', item.player)) + self.prog_items['Silver Arrows', item.player] += 1 changed = True else: - self.prog_items.add(('Bow', item.player)) + self.prog_items['Bow', item.player] += 1 changed = True elif 'Armor' in item.name: if self.has('Red Mail', item.player): @@ -787,10 +787,10 @@ class CollectionState(object): elif item.name.startswith('Bottle'): if self.bottle_count(item.player) < self.world.difficulty_requirements[item.player].progressive_bottle_limit: - self.prog_items.add((item.name, item.player)) + self.prog_items[item.name, item.player] += 1 changed = True elif event or item.advancement: - self.prog_items.add((item.name, item.player)) + self.prog_items[item.name, item.player] += 1 changed = True self.stale[item.player] = True @@ -839,11 +839,10 @@ class CollectionState(object): to_remove = None if to_remove is not None: - try: - self.prog_items.remove((to_remove, item.player)) - except ValueError: - return + self.prog_items[to_remove, item.player] -= 1 + if self.prog_items[to_remove, item.player] < 1: + del (self.prog_items[to_remove, item.player]) # invalidate caches, nothing can be trusted anymore now self.reachable_regions[item.player] = set() self.stale[item.player] = True From cf70210ed14b2bbad63abcc350d4d61b791d0eb2 Mon Sep 17 00:00:00 2001 From: compiling <8335770+compiling@users.noreply.github.com> Date: Sat, 9 May 2020 20:28:20 +1000 Subject: [PATCH 2/8] Fix pre_validate rejecting Desert Palace when no chests are in the back. --- DoorShuffle.py | 2 +- DungeonGenerator.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/DoorShuffle.py b/DoorShuffle.py index 2365a426..626cb262 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -365,7 +365,7 @@ def main_dungeon_generation(dungeon_builders, recombinant_builders, connections_ name = ' '.join(builder.name.split(' ')[:-1]) origin_list = list(builder.entrance_list) find_enabled_origins(builder.sectors, enabled_entrances, origin_list, entrances_map, name) - if len(origin_list) <= 0 or not pre_validate(builder, origin_list, world, player): + if len(origin_list) <= 0 or not pre_validate(builder, origin_list, split_dungeon, world, player): if last_key == builder.name or loops > 1000: origin_name = world.get_region(origin_list[0], player).entrances[0].parent_region.name if len(origin_list) > 0 else 'no origin' raise Exception('Infinite loop detected for "%s" located at %s' % (builder.name, origin_name)) diff --git a/DungeonGenerator.py b/DungeonGenerator.py index 5a51fa3a..a411a2f7 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -25,7 +25,7 @@ class GraphPiece: # Dungeons shouldn't be generated until all entrances are appropriately accessible -def pre_validate(builder, entrance_region_names, world, player): +def pre_validate(builder, entrance_region_names, split_dungeon, world, player): entrance_regions = convert_regions(entrance_region_names, world, player) proposed_map = {} doors_to_connect = {} @@ -36,7 +36,7 @@ def pre_validate(builder, entrance_region_names, world, player): for door in sector.outstanding_doors: doors_to_connect[door.name] = door all_regions.update(sector.regions) - bk_needed = bk_needed or determine_if_bk_needed(sector, False, world, player) + bk_needed = bk_needed or determine_if_bk_needed(sector, split_dungeon, world, player) bk_special = bk_special or check_for_special(sector) dungeon, hangers, hooks = gen_dungeon_info(builder.name, builder.sectors, entrance_regions, proposed_map, doors_to_connect, bk_needed, bk_special, world, player) From 12172366213731d3347c35154aa9003b5d00601d Mon Sep 17 00:00:00 2001 From: compiling <8335770+compiling@users.noreply.github.com> Date: Sun, 10 May 2020 19:27:13 +1000 Subject: [PATCH 3/8] Replace world exploration with a faster algorithm - use BFS and keep track of all entrances that are currently blocked by progression items. New algorithm also obsoletes sweep_for_crystal_access Set up door and entrance caches in advance Replace CrystalBarrier with FastEnum for bitfield arithmetic --- BaseClasses.py | 190 ++++++++++++++++++++------------------------- Doors.py | 6 +- EntranceShuffle.py | 40 ++++++++-- InvertedRegions.py | 3 +- Main.py | 9 ++- Regions.py | 1 + Rom.py | 3 +- Rules.py | 59 ++++++-------- 8 files changed, 157 insertions(+), 154 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 37074f22..0b5f0da3 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1,12 +1,20 @@ import copy -from enum import Enum, unique, Flag -import logging import json +import logging from collections import OrderedDict, Counter, deque, defaultdict +from enum import Enum, unique + +try: + from fast_enum import FastEnum +except ImportError: + from enum import Flag + FastEnum = Flag + # Bitflag logic is significantly faster when not using normal python enums. + logging.info('fast-enum module not found - falling back to slow enums. Run `pip install fast-enum` to remove this warning.') + from source.classes.BabelFish import BabelFish -from EntranceShuffle import door_addresses -from _vendor.collections_extended import bag +from EntranceShuffle import door_addresses, indirect_connections from Utils import int16_as_bytes from Tables import normal_offset_table, spiral_offset_table, multiply_lookup, divisor_lookup from RoomData import Room @@ -126,6 +134,12 @@ class World(object): for region in regions if regions else self.regions: region.world = self self._region_cache[region.player][region.name] = region + for exit in region.exits: + self._entrance_cache[(exit.name, exit.player)] = exit + + def initialize_doors(self, doors): + for door in doors: + self._door_cache[(door.name, door.player)] = door def get_regions(self, player=None): return self.regions if player is None else self._region_cache[player].values() @@ -384,7 +398,6 @@ class World(object): prog_locations = [location for location in self.get_locations() if location.item is not None and (location.item.advancement or location.event) and location not in state.locations_checked] while prog_locations: - state.sweep_for_crystal_access() sphere = [] # build up spheres of collection radius. Everything in each sphere is independent from each other in dependencies and only depends on lower spheres for location in prog_locations: @@ -410,9 +423,8 @@ class CollectionState(object): def __init__(self, parent): self.prog_items = Counter() self.world = parent - self.reachable_regions = {player: set() for player in range(1, parent.players + 1)} - self.colored_regions = {player: {} for player in range(1, parent.players + 1)} - self.blocked_color_regions = {player: set() for player in range(1, parent.players + 1)} + self.reachable_regions = {player: dict() for player in range(1, parent.players + 1)} + self.blocked_connections = {player: dict() for player in range(1, parent.players + 1)} self.events = [] self.path = {} self.locations_checked = set() @@ -421,88 +433,71 @@ class CollectionState(object): self.collect(item, True) def update_reachable_regions(self, player): - player_regions = self.world.get_regions(player) self.stale[player] = False rrp = self.reachable_regions[player] - ccr = self.colored_regions[player] - blocked = self.blocked_color_regions[player] - new_regions = True - reachable_regions_count = len(rrp) - while new_regions: - player_regions = [region for region in player_regions if region not in rrp] - for candidate in player_regions: - if candidate.can_reach_private(self): - rrp.add(candidate) - if candidate.type == RegionType.Dungeon: - c_switch_present = False - for ext in candidate.exits: - door = self.world.check_for_door(ext.name, player) - if door is not None and door.crystal == CrystalBarrier.Either: - c_switch_present = True - break - if c_switch_present: - ccr[candidate] = CrystalBarrier.Either - self.spread_crystal_access(candidate, CrystalBarrier.Either, rrp, ccr, player) - for ext in candidate.exits: - connect = ext.connected_region - if connect in rrp and not ext.can_reach(self): - blocked.add(candidate) - else: - color_type = CrystalBarrier.Null - for entrance in candidate.entrances: - if entrance.parent_region in rrp: - if entrance.can_reach(self): - door = self.world.check_for_door(entrance.name, player) - if door is None or entrance.parent_region.type != RegionType.Dungeon: - color_type |= CrystalBarrier.Orange - elif entrance.parent_region in ccr.keys(): - color_type |= (ccr[entrance.parent_region] & (door.crystal or CrystalBarrier.Either)) - else: - blocked.add(entrance.parent_region) - if color_type: - ccr[candidate] = color_type - for ext in candidate.exits: - connect = ext.connected_region - if connect in rrp and connect in ccr: - door = self.world.check_for_door(ext.name, player) - if door is not None and not door.blocked: - if ext.can_reach(self): - new_color = ccr[connect] | (ccr[candidate] & (door.crystal or CrystalBarrier.Either)) - if new_color != ccr[connect]: - self.spread_crystal_access(candidate, new_color, rrp, ccr, player) - else: - blocked.add(candidate) - new_regions = len(rrp) > reachable_regions_count - reachable_regions_count = len(rrp) + bc = self.blocked_connections[player] - def spread_crystal_access(self, region, crystal, rrp, ccr, player): - queue = deque([(region, crystal)]) - visited = set() - updated = False - while len(queue) > 0: - region, crystal = queue.popleft() - visited.add(region) - for ext in region.exits: - connect = ext.connected_region - if connect is not None and connect.type == RegionType.Dungeon: - if connect not in visited and connect in rrp and connect in ccr: - if ext.can_reach(self): - door = self.world.check_for_door(ext.name, player) + # init on first call - this can't be done on construction since the regions don't exist yet + start = self.world.get_region('Menu', player) + if not start in rrp: + rrp[start] = CrystalBarrier.Orange + for exit in start.exits: + bc[exit] = CrystalBarrier.Orange + + queue = deque(self.blocked_connections[player].items()) + + # run BFS on all connections, and keep track of those blocked by missing items + while True: + try: + connection, crystal_state = queue.popleft() + new_region = connection.connected_region + if new_region is None or new_region in rrp and (new_region.type != RegionType.Dungeon or (rrp[new_region] & crystal_state) == crystal_state): + bc.pop(connection, None) + elif connection.can_reach(self): + if new_region.type == RegionType.Dungeon: + new_crystal_state = crystal_state + for exit in new_region.exits: + door = exit.door + if door is not None and door.crystal == CrystalBarrier.Either: + new_crystal_state = CrystalBarrier.Either + break + if new_region in rrp: + new_crystal_state |= rrp[new_region] + + rrp[new_region] = new_crystal_state + + for exit in new_region.exits: + door = exit.door if door is not None and not door.blocked: - current_crystal = ccr[connect] - new_crystal = current_crystal | (crystal & (door.crystal or CrystalBarrier.Either)) - if current_crystal != new_crystal: - updated = True - ccr[connect] = new_crystal - queue.append((connect, new_crystal)) - return updated + door_crystal_state = new_crystal_state & (door.crystal or CrystalBarrier.Either) + bc[exit] = door_crystal_state + queue.append((exit, door_crystal_state)) + elif door is None: + queue.append((exit, new_crystal_state)) + else: + new_crystal_state = CrystalBarrier.Orange + rrp[new_region] = new_crystal_state + bc.pop(connection, None) + for exit in new_region.exits: + bc[exit] = new_crystal_state + queue.append((exit, new_crystal_state)) + + self.path[new_region] = (new_region.name, self.path.get(connection, None)) + + # Retry connections if the new region can unblock them + if new_region.name in indirect_connections: + new_entrance = self.world.get_entrance(indirect_connections[new_region.name], player) + if new_entrance in bc and new_entrance not in queue and new_entrance.parent_region in rrp: + queue.append((new_entrance, rrp[new_entrance.parent_region])) + except IndexError: + break + def copy(self): ret = CollectionState(self.world) ret.prog_items = self.prog_items.copy() ret.reachable_regions = {player: copy.copy(self.reachable_regions[player]) for player in range(1, self.world.players + 1)} - ret.colored_regions = {player: copy.copy(self.colored_regions[player]) for player in range(1, self.world.players + 1)} - ret.blocked_color_regions = {player: copy.copy(self.blocked_color_regions[player]) for player in range(1, self.world.players + 1)} + ret.blocked_connections = {player: copy.copy(self.blocked_connections[player]) for player in range(1, self.world.players + 1)} ret.events = copy.copy(self.events) ret.path = copy.copy(self.path) ret.locations_checked = copy.copy(self.locations_checked) @@ -523,19 +518,6 @@ class CollectionState(object): return spot.can_reach(self) - def sweep_for_crystal_access(self): - for player, rrp in self.reachable_regions.items(): - updated = True - while updated: - if self.stale[player]: - self.update_reachable_regions(player) - updated = False - dungeon_regions = self.blocked_color_regions[player] - ccr = self.colored_regions[player] - for region in dungeon_regions.copy(): - if region in ccr.keys(): - updated |= self.spread_crystal_access(region, ccr[region], rrp, ccr, player) - self.stale[player] = updated def sweep_for_events(self, key_only=False, locations=None): # this may need improvement @@ -554,18 +536,13 @@ class CollectionState(object): self.collect(event.item, True, event) new_locations = len(reachable_events) > checked_locations checked_locations = len(reachable_events) - if new_locations: - self.sweep_for_crystal_access() + def can_reach_blue(self, region, player): - if region not in self.colored_regions[player].keys(): - return False - return self.colored_regions[player][region] in [CrystalBarrier.Blue, CrystalBarrier.Either] + return region in self.reachable_regions[player] and self.reachable_regions[player][region] in [CrystalBarrier.Blue, CrystalBarrier.Either] def can_reach_orange(self, region, player): - if region not in self.colored_regions[player].keys(): - return False - return self.colored_regions[player][region] in [CrystalBarrier.Orange, CrystalBarrier.Either] + return region in self.reachable_regions[player] and self.reachable_regions[player][region] in [CrystalBarrier.Orange, CrystalBarrier.Either] def _do_not_flood_the_keys(self, reachable_events): adjusted_checks = list(reachable_events) @@ -844,7 +821,8 @@ class CollectionState(object): if self.prog_items[to_remove, item.player] < 1: del (self.prog_items[to_remove, item.player]) # invalidate caches, nothing can be trusted anymore now - self.reachable_regions[item.player] = set() + self.reachable_regions[item.player] = dict() + self.blocked_connections[item.player] = set() self.stale[item.player] = True def __getattr__(self, item): @@ -934,10 +912,11 @@ class Entrance(object): self.access_rule = lambda state: True self.player = player self.door = None + self.hide_path = False def can_reach(self, state): if self.parent_region.can_reach(state) and self.access_rule(state): - if not self in state.path: + if not self.hide_path and not self in state.path: state.path[self] = (self.name, state.path.get(self.parent_region, (self.parent_region.name, None))) return True @@ -1134,8 +1113,7 @@ class PolSlot(Enum): EastWest = 1 Stairs = 2 -@unique -class CrystalBarrier(Flag): +class CrystalBarrier(FastEnum): Null = 0 # no special requirement Blue = 1 # blue must be down and explore state set to Blue Orange = 2 # orange must be down and explore state set to Orange diff --git a/Doors.py b/Doors.py index 5151a244..d1d713da 100644 --- a/Doors.py +++ b/Doors.py @@ -41,7 +41,7 @@ Intr = DoorType.Interior def create_doors(world, player): - world.doors += [ + doors = [ # hyrule castle create_door(player, 'Hyrule Castle Lobby W', Nrml).dir(We, 0x61, Mid, High).toggler().pos(0), create_door(player, 'Hyrule Castle Lobby E', Nrml).dir(Ea, 0x61, Mid, High).toggler().pos(2), @@ -1069,6 +1069,10 @@ def create_doors(world, player): create_door(player, 'GT Brightly Lit Hall NW', Nrml).dir(No, 0x1d, Left, High).big_key().pos(0), create_door(player, 'GT Agahnim 2 SW', Nrml).dir(So, 0x0d, Left, High).no_exit().trap(0x4).pos(0) ] + + world.doors += doors + world.initialize_doors(doors) + create_paired_doors(world, player) # swamp events diff --git a/EntranceShuffle.py b/EntranceShuffle.py index c4747374..30e50f04 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -1204,7 +1204,9 @@ def link_inverted_entrances(world, player): sanc_doors = [door for door in Inverted_Dark_Sanctuary_Doors if door in bomb_shop_doors] sanc_door = random.choice(sanc_doors) bomb_shop_doors.remove(sanc_door) - connect_doors(world, [sanc_door], ['Inverted Dark Sanctuary'], player) + + connect_entrance(world, sanc_door, 'Inverted Dark Sanctuary', player) + world.get_entrance('Inverted Dark Sanctuary Exit', player).connect(world.get_entrance(sanc_door, player).parent_region) lw_dm_entrances = ['Paradox Cave (Bottom)', 'Paradox Cave (Middle)', 'Paradox Cave (Top)', 'Old Man House (Bottom)', 'Fairy Ascension Cave (Bottom)', 'Fairy Ascension Cave (Top)', 'Spiral Cave (Bottom)', 'Old Man Cave (East)', @@ -1279,7 +1281,8 @@ def link_inverted_entrances(world, player): sanc_doors = [door for door in Inverted_Dark_Sanctuary_Doors if door in dw_entrances] sanc_door = random.choice(sanc_doors) dw_entrances.remove(sanc_door) - connect_doors(world, [sanc_door], ['Inverted Dark Sanctuary'], player) + connect_entrance(world, sanc_door, 'Inverted Dark Sanctuary', player) + world.get_entrance('Inverted Dark Sanctuary Exit', player).connect(world.get_entrance(sanc_door, player).parent_region) # tavern back door cannot be shuffled yet connect_doors(world, ['Tavern North'], ['Tavern'], player) @@ -1410,7 +1413,8 @@ def link_inverted_entrances(world, player): sanc_doors = [door for door in Inverted_Dark_Sanctuary_Doors if door in dw_entrances] sanc_door = random.choice(sanc_doors) dw_entrances.remove(sanc_door) - connect_doors(world, [sanc_door], ['Inverted Dark Sanctuary'], player) + connect_entrance(world, sanc_door, 'Inverted Dark Sanctuary', player) + world.get_entrance('Inverted Dark Sanctuary Exit', player).connect(world.get_entrance(sanc_door, player).parent_region) # place old man house # no dw must exits in inverted, but we randomize whether cave is in light or dark world @@ -1547,7 +1551,8 @@ def link_inverted_entrances(world, player): sanc_doors = [door for door in Inverted_Dark_Sanctuary_Doors if door in entrances] sanc_door = random.choice(sanc_doors) entrances.remove(sanc_door) - connect_doors(world, [sanc_door], ['Inverted Dark Sanctuary'], player) + connect_entrance(world, sanc_door, 'Inverted Dark Sanctuary', player) + world.get_entrance('Inverted Dark Sanctuary Exit', player).connect(world.get_entrance(sanc_door, player).parent_region) # tavern back door cannot be shuffled yet connect_doors(world, ['Tavern North'], ['Tavern'], player) @@ -1680,7 +1685,8 @@ def link_inverted_entrances(world, player): sanc_door = random.choice(sanc_doors) entrances.remove(sanc_door) doors.remove(sanc_door) - connect_doors(world, [sanc_door], ['Inverted Dark Sanctuary'], player) + connect_entrance(world, sanc_door, 'Inverted Dark Sanctuary', player) + world.get_entrance('Inverted Dark Sanctuary Exit', player).connect(world.get_entrance(sanc_door, player).parent_region) # now let's deal with mandatory reachable stuff def extract_reachable_exit(cavelist): @@ -2814,7 +2820,10 @@ Isolated_LH_Doors = ['Kings Grave', 'Turtle Rock Isolated Ledge Entrance'] # these are connections that cannot be shuffled and always exist. They link together separate parts of the world we need to divide into regions -mandatory_connections = [('Lake Hylia Central Island Pier', 'Lake Hylia Central Island'), +mandatory_connections = [('Links House S&Q', 'Links House'), + ('Sanctuary S&Q', 'Sanctuary'), + ('Old Man S&Q', 'Old Man House'), + ('Lake Hylia Central Island Pier', 'Lake Hylia Central Island'), ('Lake Hylia Central Island Teleporter', 'Dark Lake Hylia Central Island'), ('Zoras River', 'Zoras River'), ('Kings Grave Outer Rocks', 'Kings Grave Area'), @@ -2923,7 +2932,11 @@ mandatory_connections = [('Lake Hylia Central Island Pier', 'Lake Hylia Central ('Pyramid Drop', 'East Dark World') ] -inverted_mandatory_connections = [('Lake Hylia Central Island Pier', 'Lake Hylia Central Island'), +inverted_mandatory_connections = [('Links House S&Q', 'Inverted Links House'), + ('Dark Sanctuary S&Q', 'Inverted Dark Sanctuary'), + ('Old Man S&Q', 'Old Man House'), + ('Castle Ledge S&Q', 'Hyrule Castle Ledge'), + ('Lake Hylia Central Island Pier', 'Lake Hylia Central Island'), ('Lake Hylia Island', 'Lake Hylia Island'), ('Zoras River', 'Zoras River'), ('Kings Grave Outer Rocks', 'Kings Grave Area'), @@ -3352,6 +3365,7 @@ inverted_default_connections = [('Waterfall of Wishing', 'Waterfall of Wishing' ('Inverted Links House Exit', 'South Dark World'), ('Inverted Big Bomb Shop', 'Inverted Big Bomb Shop'), ('Inverted Dark Sanctuary', 'Inverted Dark Sanctuary'), + ('Inverted Dark Sanctuary Exit', 'West Dark World'), ('Old Man Cave (West)', 'Bumper Cave'), ('Old Man Cave (East)', 'Death Mountain Return Cave'), ('Old Man Cave Exit (West)', 'West Dark World'), @@ -3485,11 +3499,23 @@ inverted_default_dungeon_connections = [('Desert Palace Entrance (South)', 'Dese ] # format: +indirect_connections = { + 'Turtle Rock (Top)': 'Turtle Rock', + 'East Dark World': 'Pyramid Fairy', + 'Big Bomb Shop': 'Pyramid Fairy', + 'Dark Desert': 'Pyramid Fairy', + 'West Dark World': 'Pyramid Fairy', + 'South Dark World': 'Pyramid Fairy', + 'Light World': 'Pyramid Fairy', + 'Old Man Cave': 'Old Man S&Q' +} # Key=Name # addr = (door_index, exitdata) # multiexit # | ([addr], None) # holes # exitdata = (room_id, ow_area, vram_loc, scroll_y, scroll_x, link_y, link_x, camera_y, camera_x, unknown_1, unknown_2, door_1, door_2) +# ToDo somehow merge this with creation of the locations + # ToDo somehow merge this with creation of the locations door_addresses = {'Links House': (0x00, (0x0104, 0x2c, 0x0506, 0x0a9a, 0x0832, 0x0ae8, 0x08b8, 0x0b07, 0x08bf, 0x06, 0xfe, 0x0816, 0x0000)), 'Inverted Big Bomb Shop': (0x00, (0x0104, 0x2c, 0x0506, 0x0a9a, 0x0832, 0x0ae8, 0x08b8, 0x0b07, 0x08bf, 0x06, 0xfe, 0x0816, 0x0000)), diff --git a/InvertedRegions.py b/InvertedRegions.py index cea882dd..0c64cf4c 100644 --- a/InvertedRegions.py +++ b/InvertedRegions.py @@ -6,6 +6,7 @@ from Regions import create_lw_region, create_dw_region, create_cave_region, crea def create_inverted_regions(world, player): world.regions += [ + create_dw_region(player, 'Menu', None, ['Links House S&Q', 'Dark Sanctuary S&Q', 'Old Man S&Q', 'Castle Ledge S&Q']), create_lw_region(player, 'Light World', ['Mushroom', 'Bottle Merchant', 'Flute Spot', 'Sunken Treasure', 'Purple Chest', 'Bombos Tablet'], ["Blinds Hideout", "Hyrule Castle Secret Entrance Drop", 'Kings Grave Outer Rocks', 'Dam', 'Inverted Big Bomb Shop', 'Tavern North', 'Chicken House', 'Aginahs Cave', 'Sahasrahlas Hut', 'Kakariko Well Drop', 'Kakariko Well Cave', @@ -176,7 +177,7 @@ def create_inverted_regions(world, player): create_cave_region(player, 'C-Shaped House', 'a house with a chest', ['C-Shaped House']), create_cave_region(player, 'Chest Game', 'a game of 16 chests', ['Chest Game']), create_cave_region(player, 'Red Shield Shop', 'the rare shop'), - create_cave_region(player, 'Inverted Dark Sanctuary', 'a storyteller'), + create_cave_region(player, 'Inverted Dark Sanctuary', 'a storyteller', None, ['Inverted Dark Sanctuary Exit']), create_cave_region(player, 'Bumper Cave', 'a connector', None, ['Bumper Cave Exit (Bottom)', 'Bumper Cave Exit (Top)']), create_dw_region(player, 'Skull Woods Forest', None, ['Skull Woods First Section Hole (East)', 'Skull Woods First Section Hole (West)', 'Skull Woods First Section Hole (North)', 'Skull Woods First Section Door', 'Skull Woods Second Section Door (East)']), diff --git a/Main.py b/Main.py index 5e181f82..64b66228 100644 --- a/Main.py +++ b/Main.py @@ -370,7 +370,6 @@ def copy_world(world): create_inverted_regions(ret, player) create_dungeon_regions(ret, player) create_shops(ret, player) - create_doors(ret, player) create_rooms(ret, player) create_dungeons(ret, player) @@ -417,6 +416,10 @@ def copy_world(world): ret.state.stale = {player: True for player in range(1, world.players + 1)} ret.doors = world.doors + for door in ret.doors: + entrance = ret.check_for_entrance(door.name, door.player) + if entrance is not None: + entrance.door = door ret.paired_doors = world.paired_doors ret.rooms = world.rooms ret.inaccessible_regions = world.inaccessible_regions @@ -468,7 +471,6 @@ def create_playthrough(world): logging.getLogger('').debug(world.fish.translate("cli","cli","building.collection.spheres")) while sphere_candidates: state.sweep_for_events(key_only=True) - state.sweep_for_crystal_access() sphere = [] # build up spheres of collection radius. Everything in each sphere is independent from each other in dependencies and only depends on lower spheres @@ -486,7 +488,7 @@ def create_playthrough(world): logging.getLogger('').debug(world.fish.translate("cli","cli","building.calculating.spheres"), len(collection_spheres), len(sphere), len(prog_locations)) if not sphere: - logging.getLogger('').debug(world.fish.translate("cli","cli","cannot.reach.items"), [world.fish.translate("cli","cli","cannot.reach.item") % (location.item.name, location.item.player, location.name, location.player) for location in sphere_candidates]) + logging.getLogger('').error(world.fish.translate("cli","cli","cannot.reach.items"), [world.fish.translate("cli","cli","cannot.reach.item") % (location.item.name, location.item.player, location.name, location.player) for location in sphere_candidates]) if any([world.accessibility[location.item.player] != 'none' for location in sphere_candidates]): raise RuntimeError(world.fish.translate("cli","cli","cannot.reach.progression")) else: @@ -530,7 +532,6 @@ def create_playthrough(world): collection_spheres = [] while required_locations: state.sweep_for_events(key_only=True) - state.sweep_for_crystal_access() sphere = list(filter(lambda loc: state.can_reach(loc) and state.not_flooding_a_key(world, loc), required_locations)) diff --git a/Regions.py b/Regions.py index cbe722de..f91c71cd 100644 --- a/Regions.py +++ b/Regions.py @@ -4,6 +4,7 @@ from BaseClasses import Region, Location, Entrance, RegionType, Shop, ShopType def create_regions(world, player): world.regions += [ + create_lw_region(player, 'Menu', None, ['Links House S&Q', 'Sanctuary S&Q', 'Old Man S&Q']), create_lw_region(player, 'Light World', ['Mushroom', 'Bottle Merchant', 'Flute Spot', 'Sunken Treasure', 'Purple Chest'], ["Blinds Hideout", "Hyrule Castle Secret Entrance Drop", 'Zoras River', 'Kings Grave Outer Rocks', 'Dam', 'Links House', 'Tavern North', 'Chicken House', 'Aginahs Cave', 'Sahasrahlas Hut', 'Kakariko Well Drop', 'Kakariko Well Cave', diff --git a/Rom.py b/Rom.py index 9589875f..34a1b725 100644 --- a/Rom.py +++ b/Rom.py @@ -2121,7 +2121,8 @@ def set_inverted_mode(world, player, rom): rom.write_bytes(snes_to_pc(0x06B2AB), [0xF0, 0xE1, 0x05]) def patch_shuffled_dark_sanc(world, rom, player): - dark_sanc_entrance = str(world.get_region('Inverted Dark Sanctuary', player).entrances[0].name) + dark_sanc = world.get_region('Inverted Dark Sanctuary', player) + dark_sanc_entrance = str([i for i in dark_sanc.entrances if i.parent_region.name != 'Menu'][0].name) room_id, ow_area, vram_loc, scroll_y, scroll_x, link_y, link_x, camera_y, camera_x, unknown_1, unknown_2, door_1, door_2 = door_addresses[dark_sanc_entrance][1] door_index = door_addresses[str(dark_sanc_entrance)][0] diff --git a/Rules.py b/Rules.py index 4dce2231..63f8bec2 100644 --- a/Rules.py +++ b/Rules.py @@ -1,6 +1,7 @@ import logging from BaseClasses import CollectionState, RegionType, DoorType from Regions import key_only_locations +from Items import ItemFactory from RoomData import DoorKind from collections import deque @@ -9,20 +10,10 @@ def set_rules(world, player): if world.logic[player] == 'nologic': logging.getLogger('').info('WARNING! Seeds generated under this logic often require major glitches and may be impossible!') - if world.mode[player] != 'inverted': - world.get_region('Links House', player).can_reach_private = lambda state: True - world.get_region('Sanctuary', player).can_reach_private = lambda state: True - old_rule = world.get_region('Old Man House', player).can_reach - world.get_region('Old Man House', player).can_reach_private = lambda state: state.can_reach('Old Man', 'Location', player) or old_rule(state) - return - else: - world.get_region('Inverted Links House', player).can_reach_private = lambda state: True - world.get_region('Inverted Dark Sanctuary', player).entrances[0].parent_region.can_reach_private = lambda state: True - if world.shuffle[player] != 'vanilla': - old_rule = world.get_region('Old Man House', player).can_reach - world.get_region('Old Man House', player).can_reach_private = lambda state: state.can_reach('Old Man', 'Location', player) or old_rule(state) - world.get_region('Hyrule Castle Ledge', player).can_reach_private = lambda state: True - return + world.get_region('Menu', player).can_reach_private = lambda state: True + for exit in world.get_region('Menu', player).exits: + exit.hide_path = True + return global_rules(world, player) if world.mode[player] != 'inverted': @@ -113,8 +104,11 @@ def global_rules(world, player): add_item_rule(world.get_location('Ganon', player), lambda item: item.name == 'Triforce' and item.player == player) # we can s&q to the old man house after we rescue him. This may be somewhere completely different if caves are shuffled! - old_rule = world.get_region('Old Man House', player).can_reach_private - world.get_region('Old Man House', player).can_reach_private = lambda state: state.can_reach('Old Man', 'Location', player) or old_rule(state) + world.get_region('Menu', player).can_reach_private = lambda state: True + for exit in world.get_region('Menu', player).exits: + exit.hide_path = True + + set_rule(world.get_entrance('Old Man S&Q', player), lambda state: state.can_reach('Old Man', 'Location', player)) set_rule(world.get_location('Sunken Treasure', player), lambda state: state.has('Open Floodgate', player)) set_rule(world.get_location('Dark Blacksmith Ruins', player), lambda state: state.has('Return Smith', player)) @@ -384,16 +378,6 @@ def global_rules(world, player): def default_rules(world, player): - if world.mode[player] == 'standard': - # Links house requires reaching Sanc so skipping that chest isn't a softlock. - world.get_region('Hyrule Castle Secret Entrance', player).can_reach_private = lambda state: True - old_rule = world.get_region('Links House', player).can_reach_private - world.get_region('Links House', player).can_reach_private = lambda state: state.has('Zelda Delivered', player) or old_rule(state) - else: - # these are default save&quit points and always accessible - world.get_region('Links House', player).can_reach_private = lambda state: True - world.get_region('Sanctuary', player).can_reach_private = lambda state: True - # overworld requirements set_rule(world.get_entrance('Kings Grave', player), lambda state: state.has_Boots(player)) set_rule(world.get_entrance('Kings Grave Outer Rocks', player), lambda state: state.can_lift_heavy_rocks(player)) @@ -506,12 +490,7 @@ def default_rules(world, player): def inverted_rules(world, player): # s&q regions. link's house entrance is set to true so the filler knows the chest inside can always be reached - world.get_region('Inverted Links House', player).can_reach_private = lambda state: True - world.get_region('Inverted Links House', player).entrances[0].can_reach = lambda state: True - world.get_region('Inverted Dark Sanctuary', player).entrances[0].parent_region.can_reach_private = lambda state: True - - old_rule = world.get_region('Hyrule Castle Ledge', player).can_reach_private - world.get_region('Hyrule Castle Ledge', player).can_reach_private = lambda state: (state.has_Mirror(player) and state.has('Beat Agahnim 1', player) and state.can_reach_light_world(player)) or old_rule(state) + set_rule(world.get_entrance('Castle Ledge S&Q', player), lambda state: state.has_Mirror(player) and state.has('Beat Agahnim 1', player)) # overworld requirements set_rule(world.get_location('Maze Race', player), lambda state: state.has_Pearl(player)) @@ -824,7 +803,19 @@ std_kill_rooms = { } # all trap rooms? +def add_connection(parent_name, target_name, entrance_name, world, player): + parent = world.get_region(parent_name, player) + target = world.get_region(target_name, player) + connection = Entrance(player, entrance_name, parent) + parent.exits.append(connection) + connection.connect(target) + + def standard_rules(world, player): + add_connection('Menu', 'Hyrule Castle Secret Entrance', 'Uncle S&Q', world, player) + world.get_entrance('Uncle S&Q', player).hide_path = True + set_rule(world.get_entrance('Links House S&Q', player), lambda state: state.can_reach('Sanctuary', 'Region', player)) + set_rule(world.get_entrance('Sanctuary S&Q', player), lambda state: state.can_reach('Sanctuary', 'Region', player)) # these are because of rails if world.shuffle[player] != 'vanilla': set_rule(world.get_entrance('Hyrule Castle Exit (East)', player), lambda state: state.has('Zelda Delivered', player)) @@ -1039,7 +1030,7 @@ def set_big_bomb_rules(world, player): # the basic routes assume you can reach eastern light world with the bomb. # you can then use the southern teleporter, or (if you have beaten Aga1) the hyrule castle gate warp def basic_routes(state): - return southern_teleporter(state) or state.can_reach('Top of Pyramid', 'Entrance', player) + return southern_teleporter(state) or state.has('Beat Agahnim 1', player) # Key for below abbreviations: # P = pearl @@ -1072,7 +1063,7 @@ def set_big_bomb_rules(world, player): #1. Mirror and enter via gate: Need mirror and Aga1 #2. cross peg bridge: Need hammer and moon pearl # -> CPB or (M and A) - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: cross_peg_bridge(state) or (state.has_Mirror(player) and state.can_reach('Top of Pyramid', 'Entrance', player))) + add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: cross_peg_bridge(state) or (state.has_Mirror(player) and state.has('Beat Agahnim 1', player))) elif bombshop_entrance.name in Isolated_DW_entrances: # 1. mirror then flute then basic routes # -> M and Flute and BR From 0700af4dbd9cd786468ffb6cfbb65af0242d025d Mon Sep 17 00:00:00 2001 From: compiling <8335770+compiling@users.noreply.github.com> Date: Tue, 12 May 2020 20:38:58 +1000 Subject: [PATCH 4/8] Logic updates to Spike Cave, Bosses Fix Bomb rules to exclude Spectacle Rock Cave for inverted Require a weapon for Castle Tower kill rooms --- BaseClasses.py | 13 ++++++------- Bosses.py | 12 +++++++++--- EntranceShuffle.py | 4 ++-- Rules.py | 15 +++++++++++---- 4 files changed, 28 insertions(+), 16 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 0b5f0da3..20f31d8e 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -611,9 +611,9 @@ class CollectionState(object): def can_extend_magic(self, player, smallmagic=16, fullrefill=False): #This reflects the total magic Link has, not the total extra he has. basemagic = 8 - if self.has('Quarter Magic', player): + if self.has('Magic Upgrade (1/4)', player): basemagic = 32 - elif self.has('Half Magic', player): + elif self.has('Magic Upgrade (1/2)', player): basemagic = 16 if self.can_buy_unlimited('Green Potion', player) or self.can_buy_unlimited('Blue Potion', player): if self.world.difficulty_adjustments[player] == 'hard' and not fullrefill: @@ -634,9 +634,8 @@ class CollectionState(object): def can_shoot_arrows(self, player): if self.world.retro[player]: - #TODO: need to decide how we want to handle wooden arrows longer-term (a can-buy-a check, or via dynamic shop location) - #FIXME: Should do something about hard+ ganon only silvers. For the moment, i believe they effective grant wooden, so we are safe - return self.has('Bow', player) and (self.has('Silver Arrows', player) or self.can_buy_unlimited('Single Arrow', player)) + #todo: Non-progressive silvers grant wooden arrows, but progressive bows do not. Always require shop arrows to be safe + return self.has('Bow', player) and self.can_buy_unlimited('Single Arrow', player) return self.has('Bow', player) def can_get_good_bee(self, player): @@ -756,10 +755,10 @@ class CollectionState(object): if self.has('Red Mail', item.player): pass elif self.has('Blue Mail', item.player): - self.prog_items.add(('Red Mail', item.player)) + self.prog_items['Red Mail', item.player] += 1 changed = True else: - self.prog_items.add(('Blue Mail', item.player)) + self.prog_items['Blue Mail', item.player] += 1 changed = True elif item.name.startswith('Bottle'): diff --git a/Bosses.py b/Bosses.py index ea02b8ba..eb139950 100644 --- a/Bosses.py +++ b/Bosses.py @@ -18,6 +18,7 @@ def ArmosKnightsDefeatRule(state, player): # Magic amounts are probably a bit overkill return ( state.has_blunt_weapon(player) or + state.can_shoot_arrows(player) or (state.has('Cane of Somaria', player) and state.can_extend_magic(player, 10)) or (state.has('Cane of Byrna', player) and state.can_extend_magic(player, 16)) or (state.has('Ice Rod', player) and state.can_extend_magic(player, 32)) or @@ -26,18 +27,19 @@ def ArmosKnightsDefeatRule(state, player): state.has('Red Boomerang', player)) def LanmolasDefeatRule(state, player): - # TODO: Allow the canes here? return ( state.has_blunt_weapon(player) or state.has('Fire Rod', player) or state.has('Ice Rod', player) or + state.has('Cane of Somaria', player) or + state.has('Cane of Byrna', player) or state.can_shoot_arrows(player)) def MoldormDefeatRule(state, player): return state.has_blunt_weapon(player) def HelmasaurKingDefeatRule(state, player): - return state.has_blunt_weapon(player) or state.can_shoot_arrows(player) + return state.has_sword(player) or state.can_shoot_arrows(player) def ArrghusDefeatRule(state, player): if not state.has('Hookshot', player): @@ -95,7 +97,11 @@ def VitreousDefeatRule(state, player): def TrinexxDefeatRule(state, player): if not (state.has('Fire Rod', player) and state.has('Ice Rod', player)): return False - return state.has('Hammer', player) or state.has_beam_sword(player) or (state.has_sword(player) and state.can_extend_magic(player, 32)) + return (state.has('Hammer', player) or + state.has('Golden Sword', player) or + state.has('Tempered Sword', player) or + (state.has('Master Sword', player) and state.can_extend_magic(player, 16)) or + (state.has_sword(player) and state.can_extend_magic(player, 32))) def AgahnimDefeatRule(state, player): return state.has_sword(player) or state.has('Hammer', player) or state.has('Bug Catching Net', player) diff --git a/EntranceShuffle.py b/EntranceShuffle.py index 30e50f04..c5424820 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -2304,6 +2304,8 @@ Bomb_Shop_Multi_Cave_Doors = ['Hyrule Castle Entrance (South)', 'Death Mountain Return Cave (East)', 'Death Mountain Return Cave (West)', 'Spectacle Rock Cave Peak', + 'Spectacle Rock Cave', + 'Spectacle Rock Cave (Bottom)', 'Paradox Cave (Bottom)', 'Paradox Cave (Middle)', 'Paradox Cave (Top)', @@ -2644,8 +2646,6 @@ Inverted_Bomb_Shop_Multi_Cave_Doors = ['Hyrule Castle Entrance (South)', 'Death Mountain Return Cave (East)', 'Death Mountain Return Cave (West)', 'Spectacle Rock Cave Peak', - 'Spectacle Rock Cave', - 'Spectacle Rock Cave (Bottom)', 'Paradox Cave (Bottom)', 'Paradox Cave (Middle)', 'Paradox Cave (Top)', diff --git a/Rules.py b/Rules.py index 63f8bec2..7eadbdda 100644 --- a/Rules.py +++ b/Rules.py @@ -1,10 +1,10 @@ import logging -from BaseClasses import CollectionState, RegionType, DoorType -from Regions import key_only_locations -from Items import ItemFactory -from RoomData import DoorKind from collections import deque +from BaseClasses import CollectionState, RegionType, DoorType, Entrance +from Regions import key_only_locations +from RoomData import DoorKind + def set_rules(world, player): @@ -162,6 +162,13 @@ def global_rules(world, player): set_defeat_dungeon_boss_rule(world.get_location('Tower of Hera - Boss', player)) set_defeat_dungeon_boss_rule(world.get_location('Tower of Hera - Prize', player)) + # Castle Tower + set_rule(world.get_entrance('Tower Gold Knights SW', player), lambda state: state.can_kill_most_things(player)) + set_rule(world.get_entrance('Tower Gold Knights EN', player), lambda state: state.can_kill_most_things(player)) + set_rule(world.get_entrance('Tower Dark Archers WN', player), lambda state: state.can_kill_most_things(player)) + set_rule(world.get_entrance('Tower Red Spears WN', player), lambda state: state.can_kill_most_things(player)) + set_rule(world.get_entrance('Tower Red Guards EN', player), lambda state: state.can_kill_most_things(player)) + set_rule(world.get_entrance('Tower Red Guards SW', player), lambda state: state.can_kill_most_things(player)) set_rule(world.get_entrance('Tower Altar NW', player), lambda state: state.has_sword(player)) set_defeat_dungeon_boss_rule(world.get_location('Agahnim 1', player)) From c7103e6919558f411c922186de7589ec6e52ff25 Mon Sep 17 00:00:00 2001 From: compiling <8335770+compiling@users.noreply.github.com> Date: Tue, 12 May 2020 21:19:01 +1000 Subject: [PATCH 5/8] Add fast-enum requirement to ci --- resources/app/meta/manifests/pip_requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/app/meta/manifests/pip_requirements.txt b/resources/app/meta/manifests/pip_requirements.txt index 93e06fa8..a8c8af56 100644 --- a/resources/app/meta/manifests/pip_requirements.txt +++ b/resources/app/meta/manifests/pip_requirements.txt @@ -1 +1,2 @@ aenum +fast-enum \ No newline at end of file From c817e9ce91f83d49396eb208986bdd3c96492726 Mon Sep 17 00:00:00 2001 From: compiling <8335770+compiling@users.noreply.github.com> Date: Tue, 12 May 2020 21:38:37 +1000 Subject: [PATCH 6/8] Fix issue with crossed generation (Save and Quit entrance is not handled) --- DoorShuffle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DoorShuffle.py b/DoorShuffle.py index 626cb262..1e0136fa 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -398,7 +398,7 @@ def determine_entrance_list(world, player): region = world.get_region(region_name, player) for ent in region.entrances: parent = ent.parent_region - if parent.type != RegionType.Dungeon or parent.name == 'Sewer Drop': + if (parent.type != RegionType.Dungeon and parent.name != 'Menu') or parent.name == 'Sewer Drop': if parent.name not in world.inaccessible_regions[player]: entrance_map[key].append(region_name) else: From ac8cd92ab68c5e39d17c4142b4db26fb3005d0b7 Mon Sep 17 00:00:00 2001 From: compiling <8335770+compiling@users.noreply.github.com> Date: Wed, 13 May 2020 20:16:49 +1000 Subject: [PATCH 7/8] Remove logging line for missing module - seems to not work properly and CI should handle it. --- BaseClasses.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 20f31d8e..b9af9e37 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -9,8 +9,6 @@ try: except ImportError: from enum import Flag FastEnum = Flag - # Bitflag logic is significantly faster when not using normal python enums. - logging.info('fast-enum module not found - falling back to slow enums. Run `pip install fast-enum` to remove this warning.') from source.classes.BabelFish import BabelFish From 8b1bb810c10b56d592772d9639812755b7ab84a6 Mon Sep 17 00:00:00 2001 From: compiling <8335770+compiling@users.noreply.github.com> Date: Sun, 17 May 2020 22:42:46 +1000 Subject: [PATCH 8/8] Link requires flippers to exit the water from Diver Down state in Swamp. --- Rules.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Rules.py b/Rules.py index 7eadbdda..ae388e5c 100644 --- a/Rules.py +++ b/Rules.py @@ -206,6 +206,8 @@ def global_rules(world, player): set_rule(world.get_entrance('Swamp Flooded Room Ladder', player), lambda state: state.has('Drained Swamp', player)) set_rule(world.get_location('Swamp Palace - Flooded Room - Left', player), lambda state: state.has('Drained Swamp', player)) set_rule(world.get_location('Swamp Palace - Flooded Room - Right', player), lambda state: state.has('Drained Swamp', player)) + set_rule(world.get_entrance('Swamp Flooded Spot Ladder', player), lambda state: state.has('Flippers', player) or state.has('Drained Swamp', player)) + set_rule(world.get_entrance('Swamp Drain Left Up Stairs', player), lambda state: state.has('Flippers', player) or state.has('Drained Swamp', player)) set_rule(world.get_entrance('Swamp Waterway NW', player), lambda state: state.has('Flippers', player)) set_rule(world.get_entrance('Swamp Waterway N', player), lambda state: state.has('Flippers', player)) set_rule(world.get_entrance('Swamp Waterway NE', player), lambda state: state.has('Flippers', player))