diff --git a/BaseClasses.py b/BaseClasses.py index 7c87f119..c4c7047a 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -2,7 +2,7 @@ import copy from enum import Enum, unique import logging import json -from collections import OrderedDict +from collections import OrderedDict, deque from _vendor.collections_extended import bag from Utils import int16_as_bytes from Tables import normal_offset_table, spiral_offset_table @@ -355,6 +355,7 @@ class CollectionState(object): self.prog_items = bag() 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.events = [] self.path = {} self.locations_checked = set() @@ -366,6 +367,7 @@ class CollectionState(object): player_regions = [region for region in self.world.regions if region.player == player] self.stale[player] = False rrp = self.reachable_regions[player] + ccr = self.colored_regions[player] new_regions = True reachable_regions_count = len(rrp) while new_regions: @@ -373,13 +375,53 @@ class CollectionState(object): for candidate in possible: 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 + if c_switch_present: + ccr[candidate] = CrystalBarrier.Either + self.spread_crystal_access(candidate, CrystalBarrier.Either, rrp, ccr, player) + else: + for entrance in candidate.entrances: + door = self.world.check_for_door(entrance.name, player) + if door is None or entrance.parent_region.type != RegionType.Dungeon: + ccr[candidate] = CrystalBarrier.Orange + if entrance.parent_region in ccr.keys(): + color_type = ccr[entrance.parent_region] + current_type = ccr[candidate] if candidate in ccr.keys() else None + ccr[candidate] = color_type if current_type is None or color_type == current_type else CrystalBarrier.Either new_regions = len(rrp) > reachable_regions_count reachable_regions_count = len(rrp) + def spread_crystal_access(self, region, crystal, rrp, ccr, player): + queue = deque([(region, crystal)]) + visited = set() + while len(queue) > 0: + region, crystal = queue.popleft() + visited.add(region) + for ext in region.exits: + connect = ext.connected_region + if connect not in visited and connect is not None and connect.type == RegionType.Dungeon: + if connect in rrp and ext.can_reach(self) and connect: + door = self.world.check_for_door(ext.name, player) + current_crystal = ccr[connect] + if door is not None and not door.blocked and current_crystal != crystal and current_crystal != CrystalBarrier.Either: + if door.crystal in [CrystalBarrier.Either, CrystalBarrier.Null]: + ccr[connect] = crystal + queue.append((connect, crystal)) + else: + queue.append((connect, door.crystal)) + if door.crystal != current_crystal: + ccr[connect] = CrystalBarrier.Either + 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.events = copy.copy(self.events) ret.path = copy.copy(self.path) ret.locations_checked = copy.copy(self.locations_checked) @@ -400,6 +442,15 @@ class CollectionState(object): return spot.can_reach(self) + def sweep_for_crystal_access(self): + for player, rrp in self.reachable_regions.items(): + dungeon_regions = [x for x in rrp if x.type == RegionType.Dungeon] + ccr = self.colored_regions[player] + for region in dungeon_regions: + if region in ccr.keys(): + self.spread_crystal_access(region, ccr[region], rrp, ccr, player) + self.stale[player] = True + def sweep_for_events(self, key_only=False, locations=None): # this may need improvement new_locations = True @@ -415,6 +466,18 @@ 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] + + 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] def _do_not_flood_the_keys(self, reachable_events): adjusted_checks = list(reachable_events) @@ -699,7 +762,7 @@ class CollectionState(object): class RegionType(Enum): LightWorld = 1 DarkWorld = 2 - Cave = 3 # Also includes Houses + Cave = 3 # Also includes Houses Dungeon = 4 @property diff --git a/DoorShuffle.py b/DoorShuffle.py index 5c7e7a18..22267f2a 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -1034,7 +1034,7 @@ def reassign_key_doors(builder, proposal, world, player): room.delete(d.doorListPos) else: if len(room.doorList) > 1: - room.mirror(d.doorListPos) + room.mirror(d.doorListPos) # todo: I don't think this works for crossed - maybe it will else: room.delete(d.doorListPos) d.smallKey = False diff --git a/Doors.py b/Doors.py index 8835f54c..a17e102a 100644 --- a/Doors.py +++ b/Doors.py @@ -1084,11 +1084,12 @@ def create_doors(world, player): world.get_door('Hera Startile Wide Holes', player).c_switch() world.get_door('PoD Arena Main SW', player).c_switch() + world.get_door('PoD Arena Bonk Path', player).c_switch() world.get_door('PoD Arena Bridge SE', player).c_switch() + world.get_door('PoD Arena Bridge Drop Down', player).c_switch() world.get_door('PoD Arena Main Orange Barrier', player).barrier(CrystalBarrier.Orange) # maybe you can cross this way with blue up?? world.get_door('PoD Arena Main Crystal Path', player).barrier(CrystalBarrier.Blue) - world.get_door('PoD Arena Crystals E', player).barrier(CrystalBarrier.Blue) world.get_door('PoD Arena Crystal Path', player).barrier(CrystalBarrier.Blue) world.get_door('PoD Sexy Statue W', player).c_switch() world.get_door('PoD Sexy Statue NW', player).c_switch() @@ -1185,6 +1186,7 @@ def create_doors(world, player): world.get_door('GT Hookshot Entry Blue Barrier', player).barrier(CrystalBarrier.Blue) world.get_door('GT Double Switch Orange Barrier', player).barrier(CrystalBarrier.Orange) world.get_door('GT Double Switch Orange Barrier 2', player).barrier(CrystalBarrier.Orange) + world.get_door('GT Double Switch Orange Path', player).barrier(CrystalBarrier.Orange) world.get_door('GT Double Switch Key Orange Path', player).barrier(CrystalBarrier.Orange) world.get_door('GT Double Switch Key Blue Path', player).barrier(CrystalBarrier.Blue) world.get_door('GT Double Switch Blue Barrier', player).barrier(CrystalBarrier.Blue) diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index 217a6e9f..4bd57642 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -118,9 +118,9 @@ def analyze_dungeon(key_layout, world, player): raw_avail = chest_keys + len(key_counter.key_only_locations) available = raw_avail - key_counter.used_keys possible_smalls = count_unique_small_doors(key_counter, key_layout.flat_prop) - avail_bigs = count_unique_big_doors(key_counter) + avail_bigs = exist_relevant_big_doors(key_counter, key_layout) if not key_counter.big_key_opened: - if chest_keys == count_locations_big_optional(key_counter.free_locations) and available <= possible_smalls and avail_bigs == 0: + if chest_keys == count_locations_big_optional(key_counter.free_locations) and available <= possible_smalls and not avail_bigs: key_logic.bk_restricted.update(filter_big_chest(key_counter.free_locations)) if not key_counter.big_key_opened and big_chest_in_locations(key_counter.free_locations): key_logic.sm_restricted.update(find_big_chest_locations(key_counter.free_locations)) @@ -318,6 +318,7 @@ def create_rule(key_counter, prev_counter, key_layout, world): available = raw_avail - key_counter.used_keys possible_smalls = count_unique_small_doors(key_counter, key_layout.flat_prop) required_keys = min(available, possible_smalls) + key_counter.used_keys + # required_keys = key_counter.used_keys + 1 # this sometimes makes more sense # if prev_avail < required_keys: # required_keys = prev_avail + prev_counter.used_keys # return DoorRules(required_keys) @@ -487,15 +488,16 @@ def count_unique_small_doors(key_counter, proposal): return cnt -def count_unique_big_doors(key_counter): - cnt = 0 - counted = set() - for door in key_counter.child_doors: - if door.bigKey and door not in counted: - cnt += 1 - counted.add(door) - counted.add(door.dest) - return cnt +def exist_relevant_big_doors(key_counter, key_layout): + bk_counter = find_counter(key_counter.open_doors, True, key_layout, False) + if bk_counter is not None: + diff = dict_difference(bk_counter.free_locations, key_counter.free_locations) + if len(diff) > 0: + return True + diff = dict_difference(bk_counter.key_only_locations, key_counter.key_only_locations) + if len(diff) > 0: + return True + return False def count_locations_big_optional(locations, bk=False): @@ -788,7 +790,7 @@ def state_id(state, flat_proposal): return s_id -def find_counter(opened_doors, bk_hint, key_layout): +def find_counter(opened_doors, bk_hint, key_layout, raise_on_error=True): counter = find_counter_hint(opened_doors, bk_hint, key_layout) if counter is not None: return counter @@ -801,7 +803,9 @@ def find_counter(opened_doors, bk_hint, key_layout): counter = find_counter_hint(dict.fromkeys(more_doors), bk_hint, key_layout) if counter is not None: return counter - raise Exception('Unable to find door permutation. Init CID: %s' % counter_id(opened_doors, bk_hint, key_layout.flat_prop)) + if raise_on_error: + raise Exception('Unable to find door permutation. Init CID: %s' % counter_id(opened_doors, bk_hint, key_layout.flat_prop)) + return None def find_counter_hint(opened_doors, bk_hint, key_layout): diff --git a/Main.py b/Main.py index 9ed5efc6..7630bfb5 100644 --- a/Main.py +++ b/Main.py @@ -325,6 +325,7 @@ def create_playthrough(world): while sphere_candidates: if not world.keysanity: 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 @@ -379,6 +380,7 @@ def create_playthrough(world): while required_locations: if not world.keysanity: 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/Rules.py b/Rules.py index 39647bbb..fd93c9f3 100644 --- a/Rules.py +++ b/Rules.py @@ -423,6 +423,55 @@ def global_rules(world, player): set_defeat_dungeon_boss_rule(world.get_location('Agahnim 2', player)) add_key_logic_rules(world, player) + + # crystal switch rules + set_rule(world.get_entrance('PoD Arena Crystal Path', player), lambda state: state.can_reach_blue(world.get_region('PoD Arena Crystal', player), player)) + set_rule(world.get_entrance('Swamp Trench 2 Pots Blue Barrier', player), lambda state: state.can_reach_blue(world.get_region('Swamp Trench 2 Pots', player), player)) + set_rule(world.get_entrance('Swamp Shortcut Blue Barrier', player), lambda state: state.can_reach_blue(world.get_region('Swamp Shortcut', player), player)) + set_rule(world.get_entrance('Thieves Attic ES', player), lambda state: state.can_reach_blue(world.get_region('Thieves Attic', player), player)) + set_rule(world.get_entrance('Thieves Hellway Blue Barrier', player), lambda state: state.can_reach_blue(world.get_region('Thieves Hellway', player), player)) + set_rule(world.get_entrance('Thieves Hellway Crystal Blue Barrier', player), lambda state: state.can_reach_blue(world.get_region('Thieves Hellway N Crystal', player), player)) + set_rule(world.get_entrance('Thieves Triple Bypass SE', player), lambda state: state.can_reach_blue(world.get_region('Thieves Triple Bypass', player), player)) + set_rule(world.get_entrance('Thieves Triple Bypass WN', player), lambda state: state.can_reach_blue(world.get_region('Thieves Triple Bypass', player), player)) + set_rule(world.get_entrance('Thieves Triple Bypass EN', player), lambda state: state.can_reach_blue(world.get_region('Thieves Triple Bypass', player), player)) + set_rule(world.get_entrance('Ice Crystal Right Blue Hole', player), lambda state: state.can_reach_blue(world.get_region('Ice Crystal Right', player), player)) + set_rule(world.get_entrance('Ice Crystal Left Blue Barrier', player), lambda state: state.can_reach_blue(world.get_region('Ice Crystal Left', player), player)) + set_rule(world.get_entrance('Ice Backwards Room Hole', player), lambda state: state.can_reach_blue(world.get_region('Ice Backwards Room', player), player)) + set_rule(world.get_entrance('Mire Hub Upper Blue Barrier', player), lambda state: state.can_reach_blue(world.get_region('Mire Hub', player), player)) + set_rule(world.get_entrance('Mire Hub Lower Blue Barrier', player), lambda state: state.can_reach_blue(world.get_region('Mire Hub', player), player)) + set_rule(world.get_entrance('Mire Hub Right Blue Barrier', player), lambda state: state.can_reach_blue(world.get_region('Mire Hub Right', player), player)) + set_rule(world.get_entrance('Mire Hub Top Blue Barrier', player), lambda state: state.can_reach_blue(world.get_region('Mire Hub Top', player), player)) + set_rule(world.get_entrance('Mire Map Spike Side Blue Barrier', player), lambda state: state.can_reach_blue(world.get_region('Mire Map Spike Side', player), player)) + set_rule(world.get_entrance('Mire Map Spot Blue Barrier', player), lambda state: state.can_reach_blue(world.get_region('Mire Map Spot', player), player)) + set_rule(world.get_entrance('Mire Crystal Dead End Left Barrier', player), lambda state: state.can_reach_blue(world.get_region('Mire Crystal Dead End', player), player)) + set_rule(world.get_entrance('Mire Crystal Dead End Right Barrier', player), lambda state: state.can_reach_blue(world.get_region('Mire Crystal Dead End', player), player)) + set_rule(world.get_entrance('Mire South Fish Blue Barrier', player), lambda state: state.can_reach_blue(world.get_region('Mire South Fish', player), player)) + set_rule(world.get_entrance('Mire Compass Blue Barrier', player), lambda state: state.can_reach_blue(world.get_region('Mire Compass Room', player), player)) + set_rule(world.get_entrance('Mire Crystal Mid Blue Barrier', player), lambda state: state.can_reach_blue(world.get_region('Mire Crystal Mid', player), player)) + set_rule(world.get_entrance('Mire Crystal Left Blue Barrier', player), lambda state: state.can_reach_blue(world.get_region('Mire Crystal Left', player), player)) + set_rule(world.get_entrance('TR Crystal Maze Blue Path', player), lambda state: state.can_reach_blue(world.get_region('TR Crystal Maze End', player), player)) + set_rule(world.get_entrance('GT Hookshot Entry Blue Barrier', player), lambda state: state.can_reach_blue(world.get_region('GT Hookshot South Entry', player), player)) + set_rule(world.get_entrance('GT Double Switch Key Blue Path', player), lambda state: state.can_reach_blue(world.get_region('GT Double Switch Key Spot', player), player)) + set_rule(world.get_entrance('GT Double Switch Blue Barrier', player), lambda state: state.can_reach_blue(world.get_region('GT Double Switch Switches', player), player)) + set_rule(world.get_entrance('GT Double Switch Transition Blue', player), lambda state: state.can_reach_blue(world.get_region('GT Double Switch Transition', player), player)) + + set_rule(world.get_entrance('Swamp Barrier Ledge - Orange', player), lambda state: state.can_reach_orange(world.get_region('Swamp Barrier Ledge', player), player)) + set_rule(world.get_entrance('Swamp Barrier - Orange', player), lambda state: state.can_reach_orange(world.get_region('Swamp Barrier', player), player)) + set_rule(world.get_entrance('Thieves Hellway Orange Barrier', player), lambda state: state.can_reach_orange(world.get_region('Thieves Hellway', player), player)) + set_rule(world.get_entrance('Thieves Hellway Crystal Orange Barrier', player), lambda state: state.can_reach_orange(world.get_region('Thieves Hellway S Crystal', player), player)) + set_rule(world.get_entrance('Ice Bomb Jump Ledge Orange Barrier', player), lambda state: state.can_reach_orange(world.get_region('Ice Bomb Jump Ledge', player), player)) + set_rule(world.get_entrance('Ice Bomb Jump Catwalk Orange Barrier', player), lambda state: state.can_reach_orange(world.get_region('Ice Bomb Jump Catwalk', player), player)) + set_rule(world.get_entrance('Ice Crystal Right Orange Barrier', player), lambda state: state.can_reach_orange(world.get_region('Ice Crystal Right', player), player)) + set_rule(world.get_entrance('Ice Crystal Left Orange Barrier', player), lambda state: state.can_reach_orange(world.get_region('Ice Crystal Left', player), player)) + set_rule(world.get_entrance('Mire Crystal Right Orange Barrier', player), lambda state: state.can_reach_orange(world.get_region('Mire Crystal Right', player), player)) + set_rule(world.get_entrance('Mire Crystal Mid Orange Barrier', player), lambda state: state.can_reach_orange(world.get_region('Mire Crystal Mid', player), player)) + set_rule(world.get_entrance('Mire Firesnake Skip Orange Barrier', player), lambda state: state.can_reach_orange(world.get_region('Mire Firesnake Skip', player), player)) + set_rule(world.get_entrance('Mire Antechamber Orange Barrier', player), lambda state: state.can_reach_orange(world.get_region('Mire Antechamber', player), player)) + set_rule(world.get_entrance('GT Double Switch Orange Barrier', player), lambda state: state.can_reach_orange(world.get_region('GT Double Switch Entry', player), player)) + set_rule(world.get_entrance('GT Double Switch Orange Barrier 2', player), lambda state: state.can_reach_orange(world.get_region('GT Double Switch Entry', player), player)) + set_rule(world.get_entrance('GT Double Switch Orange Path', player), lambda state: state.can_reach_orange(world.get_region('GT Double Switch Switches', player), player)) + set_rule(world.get_entrance('GT Double Switch Key Orange Path', player), lambda state: state.can_reach_orange(world.get_region('GT Double Switch Key Spot', player), player)) + # End of door rando rules. add_rule(world.get_location('Sunken Treasure', player), lambda state: state.has('Open Floodgate', player))