files: import os
  from collections import Counter, OrderedDict
  from enum import Enum
  from math import sqrt
  
  import yaml
  
  from logic.item_types import CONSUMABLE_ITEMS, DUNGEON_NONPROGRESS_ITEMS, DUNGEON_PROGRESS_ITEMS
  from logic.logic import Logic
  from wwrando_paths import DATA_PATH
  import tweaks
  
  ITEM_LOCATION_NAME_TO_EXIT_ZONE_NAME_OVERRIDES = {
    "Pawprint Isle - Wizzrobe Cave": "Pawprint Isle Side Isle",
  }
  
  
  class HintType(Enum):
    PATH = 0
    BARREN = 1
    ITEM = 2
    LOCATION = 3
  
  
  class Hint:
    def __init__(self, type: HintType, place, reward=None):
      self.type = type
      self.place = place
      self.reward = reward
    
    def __str__(self):
      return "<HINT: %s, (%s, %s)>" % (str(self.type), self.place, self.reward)
  
  
  class Hints:
    # A dictionary mapping dungeon name to the dungeon boss.
    # The boss name is used as the path goal in the hint text.
    DUNGEON_NAME_TO_BOSS_NAME = {
      "Dragon Roost Cavern": "Gohma",
      "Forbidden Woods": "Kalle Demos",
      "Tower of the Gods": "Gohdan",
      "Forsaken Fortress": "Helmaroc King",
      "Earth Temple": "Jalhalla",
      "Wind Temple": "Molgera",
      "Hyrule": "Hyrule",
      "Ganon's Tower": "Ganondorf",
    }
    
    # A dictionary mapping dungeon name to the requirement name.
    # This dictionary is used when determining which items are on the path to a goal.
    DUNGEON_NAME_TO_REQUIREMENT_NAME = {
      "Dragon Roost Cavern": "Can Access Other Location \"Dragon Roost Cavern - Gohma Heart Container\"",
      "Forbidden Woods": "Can Access Other Location \"Forbidden Woods - Kalle Demos Heart Container\"",
      "Tower of the Gods": "Can Access Other Location \"Tower of the Gods - Gohdan Heart Container\"",
      "Forsaken Fortress": "Can Access Other Location \"Forsaken Fortress - Helmaroc King Heart Container\"",
      "Earth Temple": "Can Access Other Location \"Earth Temple - Jalhalla Heart Container\"",
      "Wind Temple": "Can Access Other Location \"Wind Temple - Molgera Heart Container\"",
      "Hyrule": "Can Access Hyrule",
      "Ganon's Tower": "Can Reach and Defeat Ganondorf",
    }
    
    def __init__(self, rando):
      self.rando = rando
      self.logic = rando.logic
      self.options = rando.options
      
      self.path_logic = Logic(self.rando)
      self.path_logic_initial_state = self.path_logic.save_simulated_playthrough_state()
      
      # Define instance variable shortcuts for hint distribution options.
      self.max_path_hints = int(self.options.get("num_path_hints", 0))
      self.max_barren_hints = int(self.options.get("num_barren_hints", 0))
      self.max_location_hints = int(self.options.get("num_location_hints", 0))
      self.max_item_hints = int(self.options.get("num_item_hints", 0))
      self.total_num_hints = self.max_path_hints + self.max_barren_hints + self.max_location_hints + self.max_item_hints
      
      self.clearer_hints = self.options.get("clearer_hints")
      self.use_always_hints = self.options.get("use_always_hints")
      
      # Import dictionaries used to build hints from files.
      with open(os.path.join(DATA_PATH, "progress_item_hints.txt"), "r") as f:
        self.progress_item_hints = yaml.safe_load(f)
      with open(os.path.join(DATA_PATH, "island_name_hints.txt"), "r") as f:
        self.island_name_hints = yaml.safe_load(f)
      with open(os.path.join(DATA_PATH, "location_hints.txt"), "r") as f:
        self.location_hints = yaml.safe_load(f)
      
      # Validate location names in location hints file.
      for location_name in self.location_hints:
        assert location_name in rando.logic.item_locations
      
      # Define a dictionary mapping charts to their sunken treasure.
      # This will be used to check whether or not the chart leads to a junk item. If so, the chart itself can be
      # considered junk.
      self.chart_name_to_sunken_treasure = {}
    
    @staticmethod
    def get_hint_item_name(item_name):
      if item_name.startswith("Triforce Chart"):
        return "Triforce Chart"
      if item_name.startswith("Triforce Shard"):
        return "Triforce Shard"
      if item_name.startswith("Treasure Chart"):
        return "Treasure Chart"
      if item_name.endswith("Tingle Statue"):
        return "Tingle Statue"
      if item_name.endswith("Small Key"):
        return "Small Key"
      if item_name.endswith("Big Key"):
        return "Big Key"
      return item_name
    
    @staticmethod
    def get_formatted_hint_text(hint, prefix="They say that ", suffix=".", delay=30):
      if hint.type == HintType.PATH:
        hint_string = (
          "%san item found at \\{1A 06 FF 00 00 05}%s\\{1A 06 FF 00 00 00} is on the path to \\{1A 06 FF 00 00 01}%s\\{1A 06 FF 00 00 00}%s"
          % (prefix, hint.place, hint.reward, suffix)
        )
      elif hint.type == HintType.BARREN:
        hint_string = (
          "%svisiting \\{1A 06 FF 00 00 03}%s\\{1A 06 FF 00 00 00} is a foolish choice%s"
          % (prefix, hint.place, suffix)
        )
      elif hint.type == HintType.LOCATION:
        hint_string = (
          "%s\\{1A 06 FF 00 00 01}%s\\{1A 06 FF 00 00 00} rewards \\{1A 06 FF 00 00 01}%s\\{1A 06 FF 00 00 00}%s"
          % (prefix, hint.place, hint.reward, suffix)
        )
      elif hint.type == HintType.ITEM:
        hint_string = (
          "%s\\{1A 06 FF 00 00 01}%s\\{1A 06 FF 00 00 00} is located in \\{1A 06 FF 00 00 01}%s\\{1A 06 FF 00 00 00}%s"
          % (prefix, hint.reward, hint.place, suffix)
        )
      else:
        hint_string = ""
      
      # Add a wait command (delay) to prevent the player from skipping over the hint accidentally.
      delay = max(0, min(0xFFFF, delay)) # Clamp within valid range.
      if delay > 0:
        hint_string += "\\{1A 07 00 00 07 %02X %02X}" % (delay >> 8, delay & 0xFF)
      
      return hint_string
    
    
    def get_entrance_zone(self, location_name):
      # Helper function to return the entrance zone name for the location.
      #
      # For non-dungeon and non-cave locations, the entrance zone name is simply the zone (island) name. However, when
      # entrances are randomized, the entrance zone name may differ from the zone name for dungeons and caves.
      # As a special case, if the entrance zone is Tower of the Gods or the location name is "Tower of the Gods - Sunken
      # Treasure", the entrance zone name is "Tower of the Gods Sector" to differentiate between the dungeon and the
      # entrance.
      
      zone_name, specific_location_name = self.logic.split_location_name_by_zone(location_name)
      
      if location_name in ITEM_LOCATION_NAME_TO_EXIT_ZONE_NAME_OVERRIDES:
        zone_name = ITEM_LOCATION_NAME_TO_EXIT_ZONE_NAME_OVERRIDES[location_name]
      
      if zone_name in self.rando.dungeon_and_cave_island_locations and self.logic.is_dungeon_or_cave(location_name):
        # If the location is in a dungeon or cave, use the hint for whatever island the dungeon/cave is located on.
        entrance_zone = self.rando.dungeon_and_cave_island_locations[zone_name]
        
        # Special case for Tower of the Gods to use Tower of the Gods Sector when refering to the entrance, not the
  
  [... the rest is omitted ...]
files: hints.py
files: import os
  from collections import Counter, OrderedDict
  from enum import Enum
  from math import sqrt
  
  import yaml
  
  from logic.item_types import CONSUMABLE_ITEMS, DUNGEON_NONPROGRESS_ITEMS, DUNGEON_PROGRESS_ITEMS
  from logic.logic import Logic
  from wwrando_paths import DATA_PATH
  import tweaks
  
  ITEM_LOCATION_NAME_TO_EXIT_ZONE_NAME_OVERRIDES = {
    "Pawprint Isle - Wizzrobe Cave": "Pawprint Isle Side Isle",
  }
  
  
  class HintType(Enum):
    PATH = 0
    BARREN = 1
    ITEM = 2
    LOCATION = 3
  
  
  class Hint:
    def __init__(self, type: HintType, place, reward=None):
      self.type = type
      self.place = place
      self.reward = reward
    
    def __str__(self):
      return "<HINT: %s, (%s, %s)>" % (str(self.type), self.place, self.reward)
  
  
  class Hints:
    # A dictionary mapping dungeon name to the dungeon boss.
    # The boss name is used as the path goal in the hint text.
    DUNGEON_NAME_TO_BOSS_NAME = {
      "Dragon Roost Cavern": "Gohma",
      "Forbidden Woods": "Kalle Demos",
      "Tower of the Gods": "Gohdan",
      "Forsaken Fortress": "Helmaroc King",
      "Earth Temple": "Jalhalla",
      "Wind Temple": "Molgera",
      "Hyrule": "Hyrule",
      "Ganon's Tower": "Ganondorf",
    }
    
    # A dictionary mapping dungeon name to the requirement name.
    # This dictionary is used when determining which items are on the path to a goal.
    DUNGEON_NAME_TO_REQUIREMENT_NAME = {
      "Dragon Roost Cavern": "Can Access Other Location \"Dragon Roost Cavern - Gohma Heart Container\"",
      "Forbidden Woods": "Can Access Other Location \"Forbidden Woods - Kalle Demos Heart Container\"",
      "Tower of the Gods": "Can Access Other Location \"Tower of the Gods - Gohdan Heart Container\"",
      "Forsaken Fortress": "Can Access Other Location \"Forsaken Fortress - Helmaroc King Heart Container\"",
      "Earth Temple": "Can Access Other Location \"Earth Temple - Jalhalla Heart Container\"",
      "Wind Temple": "Can Access Other Location \"Wind Temple - Molgera Heart Container\"",
      "Hyrule": "Can Access Hyrule",
      "Ganon's Tower": "Can Reach and Defeat Ganondorf",
    }
    
    def __init__(self, rando):
      self.rando = rando
      self.logic = rando.logic
      self.options = rando.options
      
      # Define instance variable shortcuts for hint distribution options.
      self.max_path_hints = int(self.options.get("num_path_hints", 0))
      self.max_barren_hints = int(self.options.get("num_barren_hints", 0))
      self.max_location_hints = int(self.options.get("num_location_hints", 0))
      self.max_item_hints = int(self.options.get("num_item_hints", 0))
      self.total_num_hints = self.max_path_hints + self.max_barren_hints + self.max_location_hints + self.max_item_hints
      
      self.clearer_hints = self.options.get("clearer_hints")
      self.use_always_hints = self.options.get("use_always_hints")
      
      # Import dictionaries used to build hints from files.
      with open(os.path.join(DATA_PATH, "progress_item_hints.txt"), "r") as f:
        self.progress_item_hints = yaml.safe_load(f)
      with open(os.path.join(DATA_PATH, "island_name_hints.txt"), "r") as f:
        self.island_name_hints = yaml.safe_load(f)
      with open(os.path.join(DATA_PATH, "location_hints.txt"), "r") as f:
        self.location_hints = yaml.safe_load(f)
      
      # Validate location names in location hints file.
      for location_name in self.location_hints:
        assert location_name in rando.logic.item_locations
      
      # Define a dictionary mapping charts to their sunken treasure.
      # This will be used to check whether or not the chart leads to a junk item. If so, the chart itself can be
      # considered junk.
      self.chart_name_to_sunken_treasure = {}
    
    @staticmethod
    def get_hint_item_name(item_name):
      if item_name.startswith("Triforce Chart"):
        return "Triforce Chart"
      if item_name.startswith("Triforce Shard"):
        return "Triforce Shard"
      if item_name.startswith("Treasure Chart"):
        return "Treasure Chart"
      if item_name.endswith("Tingle Statue"):
        return "Tingle Statue"
      if item_name.endswith("Small Key"):
        return "Small Key"
      if item_name.endswith("Big Key"):
        return "Big Key"
      return item_name
    
    @staticmethod
    def get_formatted_hint_text(hint, prefix="They say that ", suffix=".", delay=30):
      if hint.type == HintType.PATH:
        hint_string = (
          "%san item found at \\{1A 06 FF 00 00 05}%s\\{1A 06 FF 00 00 00} is on the path to \\{1A 06 FF 00 00 01}%s\\{1A 06 FF 00 00 00}%s"
          % (prefix, hint.place, hint.reward, suffix)
        )
      elif hint.type == HintType.BARREN:
        hint_string = (
          "%svisiting \\{1A 06 FF 00 00 03}%s\\{1A 06 FF 00 00 00} is a foolish choice%s"
          % (prefix, hint.place, suffix)
        )
      elif hint.type == HintType.LOCATION:
        hint_string = (
          "%s\\{1A 06 FF 00 00 01}%s\\{1A 06 FF 00 00 00} rewards \\{1A 06 FF 00 00 01}%s\\{1A 06 FF 00 00 00}%s"
          % (prefix, hint.place, hint.reward, suffix)
        )
      elif hint.type == HintType.ITEM:
        hint_string = (
          "%s\\{1A 06 FF 00 00 01}%s\\{1A 06 FF 00 00 00} is located in \\{1A 06 FF 00 00 01}%s\\{1A 06 FF 00 00 00}%s"
          % (prefix, hint.reward, hint.place, suffix)
        )
      else:
        hint_string = ""
      
      # Add a wait command (delay) to prevent the player from skipping over the hint accidentally.
      delay = max(0, min(0xFFFF, delay)) # Clamp within valid range.
      if delay > 0:
        hint_string += "\\{1A 07 00 00 07 %02X %02X}" % (delay >> 8, delay & 0xFF)
      
      return hint_string
    
    
    def get_entrance_zone(self, location_name):
      # Helper function to return the entrance zone name for the location.
      #
      # For non-dungeon and non-cave locations, the entrance zone name is simply the zone (island) name. However, when
      # entrances are randomized, the entrance zone name may differ from the zone name for dungeons and caves.
      # As a special case, if the entrance zone is Tower of the Gods or the location name is "Tower of the Gods - Sunken
      # Treasure", the entrance zone name is "Tower of the Gods Sector" to differentiate between the dungeon and the
      # entrance.
      
      zone_name, specific_location_name = self.logic.split_location_name_by_zone(location_name)
      
      if location_name in ITEM_LOCATION_NAME_TO_EXIT_ZONE_NAME_OVERRIDES:
        zone_name = ITEM_LOCATION_NAME_TO_EXIT_ZONE_NAME_OVERRIDES[location_name]
      
      if zone_name in self.rando.dungeon_and_cave_island_locations and self.logic.is_dungeon_or_cave(location_name):
        # If the location is in a dungeon or cave, use the hint for whatever island the dungeon/cave is located on.
        entrance_zone = self.rando.dungeon_and_cave_island_locations[zone_name]
        
        # Special case for Tower of the Gods to use Tower of the Gods Sector when refering to the entrance, not the dungeon
        if entrance_zone == "Tower of the Gods":
          entrance_zone = "Tower of the Gods Sector"
      else:
  
  
  [... the rest is omitted ...]
files: hints.py
files: 
  import yaml
  import re
  from collections import OrderedDict
  import copy
  
  import os
  
  from logic.item_types import PROGRESS_ITEMS, NONPROGRESS_ITEMS, CONSUMABLE_ITEMS, DUPLICATABLE_CONSUMABLE_ITEMS, DUNGEON_PROGRESS_ITEMS, DUNGEON_NONPROGRESS_ITEMS
  from wwrando_paths import LOGIC_PATH
  from randomizers import entrances
  
  class Logic:
    DUNGEON_NAMES = OrderedDict([
      ("DRC",  "Dragon Roost Cavern"),
      ("FW",   "Forbidden Woods"),
      ("TotG", "Tower of the Gods"),
      ("FF",   "Forsaken Fortress"),
      ("ET",   "Earth Temple"),
      ("WT",   "Wind Temple"),
    ])
    DUNGEON_NAME_TO_SHORT_DUNGEON_NAME = OrderedDict([v, k] for k, v in DUNGEON_NAMES.items())
    
    PROGRESS_ITEM_GROUPS = OrderedDict([
      ("Triforce Shards",  [
        "Triforce Shard 1",
        "Triforce Shard 2",
        "Triforce Shard 3",
        "Triforce Shard 4",
        "Triforce Shard 5",
        "Triforce Shard 6",
        "Triforce Shard 7",
        "Triforce Shard 8",
      ]),
      ("Goddess Pearls",  [
        "Nayru's Pearl",
        "Din's Pearl",
        "Farore's Pearl",
      ]),
      ("Tingle Statues",  [
        "Dragon Tingle Statue",
        "Forbidden Tingle Statue",
        "Goddess Tingle Statue",
        "Earth Tingle Statue",
        "Wind Tingle Statue",
      ]),
    ])
    
    initial_item_locations = None
    initial_macros = None
    
    def __init__(self, rando):
      self.rando = rando
      self.requirement_met_cache = {}
      
      
      # Initialize location related attributes.
      self.item_locations = Logic.load_and_parse_item_locations()
      self.load_and_parse_macros()
      
      self.locations_by_zone_name = OrderedDict()
      for location_name in self.item_locations:
        zone_name, specific_location_name = self.split_location_name_by_zone(location_name)
        if zone_name not in self.locations_by_zone_name:
          self.locations_by_zone_name[zone_name] = []
        self.locations_by_zone_name[zone_name].append(location_name)
      
      self.remaining_item_locations = list(self.item_locations.keys())
      self.prerandomization_item_locations = OrderedDict()
      
      self.done_item_locations = OrderedDict()
      for location_name in self.item_locations:
        self.done_item_locations[location_name] = None
      
      self.rock_spire_shop_ship_locations = []
      for location_name, location in self.item_locations.items():
        if location_name.startswith("Rock Spire Isle - Beedle's Special Shop Ship - "):
          self.rock_spire_shop_ship_locations.append(location_name)
      
      # Sync the logic macros with the randomizer.
      self.update_entrance_connection_macros()
      self.update_chart_macros()
      
      
      # Initialize item related attributes.
      self.all_progress_items = PROGRESS_ITEMS.copy()
      self.all_nonprogress_items = NONPROGRESS_ITEMS.copy()
      self.all_fixed_consumable_items = CONSUMABLE_ITEMS.copy()
      self.duplicatable_consumable_items = DUPLICATABLE_CONSUMABLE_ITEMS.copy()
      
      self.triforce_chart_names = []
      self.treasure_chart_names = []
      for i in range(1, 8+1):
        self.triforce_chart_names.append("Triforce Chart %d" % i)
      for i in range(1, 41+1):
        self.treasure_chart_names.append("Treasure Chart %d" % i)
      
      if self.rando.options.get("sword_mode") == "Swordless":
        self.all_progress_items = [
          item_name for item_name in self.all_progress_items
          if item_name != "Progressive Sword"
        ]
        self.all_nonprogress_items = [
          item_name for item_name in self.all_nonprogress_items
          if item_name != "Hurricane Spin"
        ]
      
      if self.rando.options.get("progression_triforce_charts"):
        self.all_progress_items += self.triforce_chart_names
      else:
        self.all_nonprogress_items += self.triforce_chart_names
      if self.rando.options.get("progression_treasure_charts"):
        self.all_progress_items += self.treasure_chart_names
      else:
        self.all_nonprogress_items += self.treasure_chart_names
      
      # Add dungeon items to the progress/nonprogress items lists.
      if self.rando.options.get("progression_dungeons"):
        self.all_progress_items += DUNGEON_PROGRESS_ITEMS
      else:
        self.all_nonprogress_items += DUNGEON_PROGRESS_ITEMS
      self.all_nonprogress_items += DUNGEON_NONPROGRESS_ITEMS
      
      # Tell the randomizer to register dungeon-specific item names as the normal items.
      for dungeon_item_name in (DUNGEON_PROGRESS_ITEMS + DUNGEON_NONPROGRESS_ITEMS):
        regular_item_name = dungeon_item_name.split(" ", 1)[1]
        self.rando.item_name_to_id[dungeon_item_name] = self.rando.item_name_to_id[regular_item_name]
      
      self.all_cleaned_item_names = []
      all_item_names = []
      all_item_names += self.all_progress_items
      all_item_names += self.all_nonprogress_items
      all_item_names += self.all_fixed_consumable_items
      all_item_names += self.duplicatable_consumable_items
      for item_name in all_item_names:
        cleaned_item_name = self.clean_item_name(item_name)
        if cleaned_item_name not in self.all_cleaned_item_names:
          self.all_cleaned_item_names.append(cleaned_item_name)
      
      self.unplaced_progress_items = self.all_progress_items.copy()
      self.unplaced_nonprogress_items = self.all_nonprogress_items.copy()
      self.unplaced_fixed_consumable_items = self.all_fixed_consumable_items.copy()
      
      self.progress_item_groups = copy.deepcopy(self.PROGRESS_ITEM_GROUPS)
      
      self.currently_owned_items = []
      
      for item_name in self.rando.starting_items:
        self.add_owned_item(item_name)
      
      # Decide what will count as a progress item on these settings.
      self.requirement_met_cache.clear()
      self.make_useless_progress_items_nonprogress()
      
      # Replace progress items that are part of a group with the group name instead.
      for group_name, item_names in self.progress_item_groups.items():
        if all(item_name in self.unplaced_progress_items for item_name in item_names):
          self.unplaced_progress_items.append(group_name)
          for item_name in item_names:
            self.unplaced_progress_items.remove(item_name)
      
      # Remove starting items from item groups.
      for group_name, group_item_names in self.progress_item_groups.items():
        items_to_remove_from_group = [
          item_name for item_name in group_item_names
          if item_name in self.rando.starting_items
        ]
        for item_name in items_to_remove_from_group:
          self.progress_item_groups[group_name].remove(item_name)
        if len(self.progress_item_groups[group_name]) == 0:
          if group_name in self.unplaced_progress_items:
            self.unplaced_progress_items.remove(group_name)
      
      self.requirement_met_cache.clear()
      self.cached_enemies_tested_for_reqs_tuple = OrderedDict()
    
    def save_simulated_playthrough_state(self):
      vars_backup = {}
      for attr_name in [
        "currently_owned_items",
        "unplaced_progress_items",
        "unplaced_nonprogress_items",
        "unplaced_fixed_consumable_items",
        "requirement_met_cache",
      ]:
        vars_backup[attr_name] = getattr(self, attr_name).copy()
      return vars_backup
    
    def load_simulated_playthrough_state(self, vars_backup):
      for attr_name, value in vars_backup.items():
        setattr(self, attr_name, value.copy())
    
    def is_dungeon_or_cave(self, location_name):
      # Look up the setting that the location name is under
      is_dungeon = "Dungeon" in self.item_locations[location_name]["Types"]
      is_puzzle_cave = "Puzzle Secret Cave" in self.item_locations[location_name]["Types"]
      is_combat_cave = "Combat Secret
  
  [... the rest is omitted ...]
files: logic/logic.py
files: 
  import yaml
  import re
  from collections import OrderedDict
  import copy
  
  import os
  
  from logic.item_types import PROGRESS_ITEMS, NONPROGRESS_ITEMS, CONSUMABLE_ITEMS, DUPLICATABLE_CONSUMABLE_ITEMS, DUNGEON_PROGRESS_ITEMS, DUNGEON_NONPROGRESS_ITEMS
  from wwrando_paths import LOGIC_PATH
  from randomizers import entrances
  
  class Logic:
    DUNGEON_NAMES = OrderedDict([
      ("DRC",  "Dragon Roost Cavern"),
      ("FW",   "Forbidden Woods"),
      ("TotG", "Tower of the Gods"),
      ("FF",   "Forsaken Fortress"),
      ("ET",   "Earth Temple"),
      ("WT",   "Wind Temple"),
    ])
    DUNGEON_NAME_TO_SHORT_DUNGEON_NAME = OrderedDict([v, k] for k, v in DUNGEON_NAMES.items())
    
    PROGRESS_ITEM_GROUPS = OrderedDict([
      ("Triforce Shards",  [
        "Triforce Shard 1",
        "Triforce Shard 2",
        "Triforce Shard 3",
        "Triforce Shard 4",
        "Triforce Shard 5",
        "Triforce Shard 6",
        "Triforce Shard 7",
        "Triforce Shard 8",
      ]),
      ("Goddess Pearls",  [
        "Nayru's Pearl",
        "Din's Pearl",
        "Farore's Pearl",
      ]),
      ("Tingle Statues",  [
        "Dragon Tingle Statue",
        "Forbidden Tingle Statue",
        "Goddess Tingle Statue",
        "Earth Tingle Statue",
        "Wind Tingle Statue",
      ]),
    ])
    
    initial_item_locations = None
    initial_macros = None
    
    def __init__(self, rando):
      self.rando = rando
      self.requirement_met_cache = {}
      
      
      # Initialize location related attributes.
      self.item_locations = Logic.load_and_parse_item_locations()
      self.load_and_parse_macros()
      
      self.locations_by_zone_name = OrderedDict()
      for location_name in self.item_locations:
        zone_name, specific_location_name = self.split_location_name_by_zone(location_name)
        if zone_name not in self.locations_by_zone_name:
          self.locations_by_zone_name[zone_name] = []
        self.locations_by_zone_name[zone_name].append(location_name)
      
      self.remaining_item_locations = list(self.item_locations.keys())
      self.prerandomization_item_locations = OrderedDict()
      
      self.done_item_locations = OrderedDict()
      for location_name in self.item_locations:
        self.done_item_locations[location_name] = None
      
      self.rock_spire_shop_ship_locations = []
      for location_name, location in self.item_locations.items():
        if location_name.startswith("Rock Spire Isle - Beedle's Special Shop Ship - "):
          self.rock_spire_shop_ship_locations.append(location_name)
      
      # Sync the logic macros with the randomizer.
      self.update_entrance_connection_macros()
      self.update_chart_macros()
      
      
      # Initialize item related attributes.
      self.all_progress_items = PROGRESS_ITEMS.copy()
      self.all_nonprogress_items = NONPROGRESS_ITEMS.copy()
      self.all_fixed_consumable_items = CONSUMABLE_ITEMS.copy()
      self.duplicatable_consumable_items = DUPLICATABLE_CONSUMABLE_ITEMS.copy()
      
      self.triforce_chart_names = []
      self.treasure_chart_names = []
      for i in range(1, 8+1):
        self.triforce_chart_names.append("Triforce Chart %d" % i)
      for i in range(1, 41+1):
        self.treasure_chart_names.append("Treasure Chart %d" % i)
      
      if self.rando.options.get("sword_mode") == "Swordless":
        self.all_progress_items = [
          item_name for item_name in self.all_progress_items
          if item_name != "Progressive Sword"
        ]
        self.all_nonprogress_items = [
          item_name for item_name in self.all_nonprogress_items
          if item_name != "Hurricane Spin"
        ]
      
      if self.rando.options.get("progression_triforce_charts"):
        self.all_progress_items += self.triforce_chart_names
      else:
        self.all_nonprogress_items += self.triforce_chart_names
      if self.rando.options.get("progression_treasure_charts"):
        self.all_progress_items += self.treasure_chart_names
      else:
        self.all_nonprogress_items += self.treasure_chart_names
      
      # Add dungeon items to the progress/nonprogress items lists.
      if self.rando.options.get("progression_dungeons"):
        self.all_progress_items += DUNGEON_PROGRESS_ITEMS
      else:
        self.all_nonprogress_items += DUNGEON_PROGRESS_ITEMS
      self.all_nonprogress_items += DUNGEON_NONPROGRESS_ITEMS
      
      # Tell the randomizer to register dungeon-specific item names as the normal items.
      for dungeon_item_name in (DUNGEON_PROGRESS_ITEMS + DUNGEON_NONPROGRESS_ITEMS):
        regular_item_name = dungeon_item_name.split(" ", 1)[1]
        self.rando.item_name_to_id[dungeon_item_name] = self.rando.item_name_to_id[regular_item_name]
      
      self.all_cleaned_item_names = []
      all_item_names = []
      all_item_names += self.all_progress_items
      all_item_names += self.all_nonprogress_items
      all_item_names += self.all_fixed_consumable_items
      all_item_names += self.duplicatable_consumable_items
      for item_name in all_item_names:
        cleaned_item_name = self.clean_item_name(item_name)
        if cleaned_item_name not in self.all_cleaned_item_names:
          self.all_cleaned_item_names.append(cleaned_item_name)
      
      self.unplaced_progress_items = self.all_progress_items.copy()
      self.unplaced_nonprogress_items = self.all_nonprogress_items.copy()
      self.unplaced_fixed_consumable_items = self.all_fixed_consumable_items.copy()
      
      self.progress_item_groups = copy.deepcopy(self.PROGRESS_ITEM_GROUPS)
      
      self.currently_owned_items = []
      
      for item_name in self.rando.starting_items:
        self.add_owned_item(item_name)
      
      # Decide what will count as a progress item on these settings.
      self.requirement_met_cache.clear()
      self.make_useless_progress_items_nonprogress()
      
      # Replace progress items that are part of a group with the group name instead.
      for group_name, item_names in self.progress_item_groups.items():
        if all(item_name in self.unplaced_progress_items for item_name in item_names):
          self.unplaced_progress_items.append(group_name)
          for item_name in item_names:
            self.unplaced_progress_items.remove(item_name)
      
      # Remove starting items from item groups.
      for group_name, group_item_names in self.progress_item_groups.items():
        items_to_remove_from_group = [
          item_name for item_name in group_item_names
          if item_name in self.rando.starting_items
        ]
        for item_name in items_to_remove_from_group:
          self.progress_item_groups[group_name].remove(item_name)
        if len(self.progress_item_groups[group_name]) == 0:
          if group_name in self.unplaced_progress_items:
            self.unplaced_progress_items.remove(group_name)
      
      self.requirement_met_cache.clear()
      self.cached_enemies_tested_for_reqs_tuple = OrderedDict()
    
    def is_dungeon_or_cave(self, location_name):
      # Look up the setting that the location name is under
      is_dungeon = "Dungeon" in self.item_locations[location_name]["Types"]
      is_puzzle_cave = "Puzzle Secret Cave" in self.item_locations[location_name]["Types"]
      is_combat_cave = "Combat Secret Cave" in self.item_locations[location_name]["Types"]
      is_savage = "Savage Labyrinth" in self.item_locations[location_name]["Types"]
      
      return (is_dungeon or is_puzzle_cave or is_combat_cave or is_savage)
    
    def set_location_to_item(self, location_name, item_name):
      #print("Setting %s to %s" % (location_name, item_name))
      
      if self.done_item_locations[location_name]:
        raise Exception("Location was used twice: " + location_name)
      
      self.done_item_locations[
  
  [... the rest is omitted ...]
files: logic/logic.py