Files
dockipelago/worlds/dk64/randomizer/ShuffleDoors.py
Jonathan Tinney 7971961166
Some checks failed
Analyze modified files / flake8 (push) Failing after 2m28s
Build / build-win (push) Has been cancelled
Build / build-ubuntu2204 (push) Has been cancelled
ctest / Test C++ ubuntu-latest (push) Has been cancelled
ctest / Test C++ windows-latest (push) Has been cancelled
Analyze modified files / mypy (push) Has been cancelled
Build and Publish Docker Images / Push Docker image to Docker Hub (push) Successful in 5m4s
Native Code Static Analysis / scan-build (push) Failing after 5m2s
type check / pyright (push) Successful in 1m7s
unittests / Test Python 3.11.2 ubuntu-latest (push) Failing after 16m23s
unittests / Test Python 3.12 ubuntu-latest (push) Failing after 28m19s
unittests / Test Python 3.13 ubuntu-latest (push) Failing after 14m49s
unittests / Test hosting with 3.13 on ubuntu-latest (push) Successful in 5m0s
unittests / Test Python 3.13 macos-latest (push) Has been cancelled
unittests / Test Python 3.11 windows-latest (push) Has been cancelled
unittests / Test Python 3.13 windows-latest (push) Has been cancelled
add schedule I, sonic 1/frontiers/heroes, spirit island
2026-04-02 23:46:36 -07:00

405 lines
27 KiB
Python

"""Shuffle Wrinkly and T&S Doors based on settings."""
from randomizer.Enums.DoorType import DoorType
from randomizer.Enums.Kongs import Kongs
from randomizer.Enums.Levels import Levels
from randomizer.Enums.Locations import Locations
from randomizer.Enums.Maps import Maps
from randomizer.Enums.Regions import Regions
from randomizer.Enums.Types import Types
from randomizer.Enums.Settings import DKPortalRando
from randomizer.Lists import Exceptions
from randomizer.Lists.DoorLocations import door_locations
from randomizer.LogicClasses import LocationLogic
from randomizer.Patching.Library.Generic import getHintRequirement
import randomizer.LogicFiles.AngryAztec
import randomizer.LogicFiles.CreepyCastle
import randomizer.LogicFiles.CrystalCaves
import randomizer.LogicFiles.FranticFactory
import randomizer.LogicFiles.FungiForest
import randomizer.LogicFiles.GloomyGalleon
import randomizer.LogicFiles.JungleJapes
level_list = [
"Jungle Japes",
"Angry Aztec",
"Frantic Factory",
"Gloomy Galleon",
"Fungi Forest",
"Crystal Caves",
"Creepy Castle",
]
level_to_name = {
Levels.DKIsles: "Isles",
Levels.JungleJapes: "Japes",
Levels.AngryAztec: "Aztec",
Levels.FranticFactory: "Factory",
Levels.GloomyGalleon: "Galleon",
Levels.FungiForest: "Forest",
Levels.CrystalCaves: "Caves",
Levels.CreepyCastle: "Castle",
}
def GetDoorLocationForKongAndLevel(kong, level):
"""For the Level and Kong enum values, return the generic Blueprint Location enum tied to it."""
baseOffset = int(Locations.JapesDonkeyDoor) # Japes/Donkey is the first door location and they're all grouped together
levelOffset = int(level)
return Locations(baseOffset + (5 * levelOffset) + int(kong))
def UpdateDoorLevels(spoiler):
"""Ensure that the location for each hint door counts towards the correct level."""
for region in spoiler.RegionList:
for location_logic in spoiler.RegionList[region].locations:
if spoiler.LocationList[location_logic.id].type == Types.Hint:
spoiler.LocationList[location_logic.id].level = spoiler.RegionList[region].level
def ShuffleDoors(spoiler, vanilla_doors_placed: bool):
"""Shuffle Wrinkly and T&S Doors based on settings."""
# Handle initial settings
shuffle_wrinkly = False if vanilla_doors_placed else spoiler.settings.wrinkly_location_rando
shuffle_tns = False if vanilla_doors_placed else spoiler.settings.tns_location_rando
shuffle_dkportal = spoiler.settings.dk_portal_location_rando_v2 != DKPortalRando.off
disable_wrinkly_puzzles = False if vanilla_doors_placed else spoiler.settings.remove_wrinkly_puzzles
# Prepare data structures that will hold the door info
human_hint_doors = {
"Jungle Japes": {},
"Angry Aztec": {},
"Frantic Factory": {},
"Gloomy Galleon": {},
"Fungi Forest": {},
"Crystal Caves": {},
"Creepy Castle": {},
}
human_portal_doors = {
"Jungle Japes": {},
"Angry Aztec": {},
"Frantic Factory": {},
"Gloomy Galleon": {},
"Fungi Forest": {},
"Crystal Caves": {},
"Creepy Castle": {},
}
human_entry_doors = {
"Jungle Japes": "Vanilla",
"Angry Aztec": "Vanilla",
"Frantic Factory": "Vanilla",
"Gloomy Galleon": "Vanilla",
"Fungi Forest": "Vanilla",
"Crystal Caves": "Vanilla",
"Creepy Castle": "Vanilla",
}
shuffled_door_data = {
Levels.JungleJapes: [],
Levels.AngryAztec: [],
Levels.FranticFactory: [],
Levels.GloomyGalleon: [],
Levels.FungiForest: [],
Levels.CrystalCaves: [],
Levels.CreepyCastle: [],
}
# Reset (unreserved) Doors
for level in door_locations:
for door in door_locations[level]:
if not spoiler.settings.dos_door_rando or not door.dos_door:
# In Dos' Doors, there are exactly two doors that need to not be undone by this process: the two extra Japes doors
door.placed = door.default_placed
if shuffle_wrinkly and not vanilla_doors_placed:
if door.placed == DoorType.wrinkly:
door.placed = DoorType.null
if shuffle_tns and not vanilla_doors_placed:
if door.placed == DoorType.boss:
door.placed = DoorType.null
if shuffle_dkportal:
if door.placed == DoorType.dk_portal:
door.placed = DoorType.null
# Reset door lists
door.door_type = door.default_door_list.copy()
door.updateDoorTypeLogic(spoiler)
# If we already placed vanilla doors, we've already saved some data we need to preserve post-reset
if vanilla_doors_placed:
shuffled_door_data = spoiler.shuffled_door_data
human_hint_doors = spoiler.human_hint_doors
human_portal_doors = spoiler.human_portal_doors
# Hint doors have Locations tied to them. If we're about to add new ones, then we must remove the old ones.
if shuffle_wrinkly:
ClearHintDoorLogic(spoiler)
# Assign Doors
for level in door_locations:
# Get all door locations that can be given a door
available_doors = []
for door_index, door in enumerate(door_locations[level]):
if door.placed == DoorType.null and (shuffle_wrinkly or shuffle_tns or shuffle_dkportal):
available_doors.append(door_index)
elif disable_wrinkly_puzzles and door.default_placed == DoorType.wrinkly:
available_doors.append(door_index)
# Prevent plandomized doors from being used as portals
plando_indexes = []
if spoiler.settings.enable_plandomizer and spoiler.settings.plandomizer_dict["plando_wrinkly_doors"] != -1:
plando_indexes = [x for x in available_doors if door_locations[level][x].name in spoiler.settings.plandomizer_dict["plando_wrinkly_doors"].values()]
for planned_door in plando_indexes:
available_doors.remove(planned_door)
spoiler.settings.random.shuffle(available_doors)
if shuffle_tns:
plando_portal_indexes = []
number_of_portals_in_level = spoiler.settings.random.choice([3, 4, 5])
allow_multiple_portals_per_group = False
# Make sure selected locations will be suitable to be a T&S portal
available_portals = [door for door in available_doors if DoorType.boss in door_locations[level][door].door_type]
if spoiler.settings.enable_plandomizer and spoiler.settings.plandomizer_dict["plando_tns_portals"] != -1:
level_to_string = str(level.value)
if level_to_string in spoiler.settings.plandomizer_dict["plando_tns_portals"].keys():
number_of_portals_in_level = min(3, len(spoiler.settings.plandomizer_dict["plando_tns_portals"][level_to_string]))
# Sanitize input, now that we don't need the -1's anymore
spoiler.settings.plandomizer_dict["plando_tns_portals"][level_to_string] = [x for x in spoiler.settings.plandomizer_dict["plando_tns_portals"][level_to_string] if x != -1]
plando_portal_indexes = [x for x in available_portals if door_locations[level][x].name in spoiler.settings.plandomizer_dict["plando_tns_portals"][level_to_string]]
if len(plando_portal_indexes) != len([x for x in spoiler.settings.plandomizer_dict["plando_tns_portals"][level_to_string]]):
raise Exceptions.PlandoIncompatibleException(f"Not every selected portal is available in level {level}")
for planned_portal in plando_portal_indexes:
available_portals.remove(planned_portal)
for new_portal in range(number_of_portals_in_level):
if len(available_portals) > 0: # Should only fail if we don't have enough door locations
if len(plando_portal_indexes) > 0:
selected_door_index = plando_portal_indexes.pop()
allow_multiple_portals_per_group = True
elif new_portal > 0:
selected_door_index = available_portals.pop()
else:
# On the first iteration, make sure at least 1 TnS portal is accessible without any moves
selected_door_index = spoiler.settings.random.choice([door for door in available_portals if door_locations[level][door].moveless is True])
available_portals.remove(selected_door_index)
selected_portal = door_locations[level][selected_door_index]
if not allow_multiple_portals_per_group:
# Only place one T&S portal per group so we don't stack portals too heavily
available_portals = [door for door in available_portals if door_locations[level][door].group != selected_portal.group]
# update available_doors separately as wrinkly doors should not be affected by the T&S grouping
available_doors.remove(selected_door_index)
selected_portal.assignPortal(spoiler)
human_portal_doors[level_list[level]]["T&S #" + str(new_portal + 1)] = selected_portal.name
shuffled_door_data[level].append((selected_door_index, DoorType.boss))
if shuffle_wrinkly:
# Place one hint door per kong
for kong in range(5): # NOTE: If testing all locations, replace "range(5) with range(len(available_doors))"
assignee = Kongs(kong % 5)
if len(available_doors) > 0: # Should only fail if we don't have enough door locations
# Give plandomizer an opportunity to get the final say
retry = True
location_var = str(GetDoorLocationForKongAndLevel(kong, level).value)
if (
spoiler.settings.enable_plandomizer
and spoiler.settings.plandomizer_dict["plando_wrinkly_doors"] != -1
and location_var in spoiler.settings.plandomizer_dict["plando_wrinkly_doors"].keys()
):
if spoiler.settings.plandomizer_dict["plando_wrinkly_doors"][location_var] not in ("", -1):
selected_door_index = [x for x in plando_indexes if door_locations[level][x].name == spoiler.settings.plandomizer_dict["plando_wrinkly_doors"][location_var]][0]
retry = False
else:
selected_door_index = available_doors.pop()
else:
selected_door_index = available_doors.pop(0) # Popping from the top of the list makes it possible to append the selected door back into the list, if it's a bad pick
# Make sure that the kong is eligible to be assigned to the selected door, and that the door location is suitable to be a hint door
while (assignee not in door_locations[level][selected_door_index].kongs) or (DoorType.wrinkly not in door_locations[level][selected_door_index].door_type):
# If testing all locations, add a break here
if retry:
available_doors.append(selected_door_index)
selected_door_index = available_doors.pop(0)
else:
name = spoiler.settings.plandomizer_dict["plando_wrinkly_doors"][location_var]
raise Exceptions.PlandoIncompatibleException(f"Bad door location: {name}.")
selected_door = door_locations[level][selected_door_index]
selected_door.assignDoor(assignee) # Clamp to within [0,4], preventing list index errors
human_hint_doors[level_list[level]][str(Kongs(kong % 5).name).capitalize()] = selected_door.name
shuffled_door_data[level].append((selected_door_index, DoorType.wrinkly, (kong % 5)))
# Add logic for the new door location
doorLocation = GetDoorLocationForKongAndLevel(kong, level) # If testing all locations, replace "kong" with "kong % 5"
region = spoiler.RegionList[selected_door.logicregion]
region.locations.append(LocationLogic(doorLocation, selected_door.logic))
spoiler.LocationList[doorLocation].name = f"{level_to_name[level]} Hint Door: {selected_door.name}"
elif disable_wrinkly_puzzles:
# place vanilla wrinkly doors
vanilla_wrinkly_doors = [door for door in available_doors if door_locations[level][door].default_placed == DoorType.wrinkly]
for kong in range(5):
if len(vanilla_wrinkly_doors) > 0: # Should only fail if we don't have enough door locations
selected_door_index = vanilla_wrinkly_doors.pop()
selected_door = door_locations[level][selected_door_index]
assignee = selected_door.default_kong
selected_door.assignDoor(assignee)
human_hint_doors[level_list[level]][str(assignee).capitalize()] = selected_door.name
shuffled_door_data[level].append((selected_door_index, DoorType.wrinkly, int(assignee)))
if shuffle_dkportal:
available_entries = [door for door in available_doors if DoorType.dk_portal in door_locations[level][door].door_type]
if len(available_entries) > 0: # Should only fail if we don't have enough door locations
selected_door_index = spoiler.settings.random.choice([door for door in available_entries])
available_entries.remove(selected_door_index)
selected_entry = door_locations[level][selected_door_index]
# update available_doors separately as wrinkly doors should not be affected by the T&S grouping
available_doors.remove(selected_door_index)
selected_entry.assignDKPortal(spoiler, level)
human_entry_doors[level_list[level]] = selected_entry.name
shuffled_door_data[level].append((selected_door_index, DoorType.dk_portal))
# Track all touched doors in a variable and put it in the spoiler because changes to the static list do not save
spoiler.shuffled_door_data = shuffled_door_data
# Give human text to spoiler log
if shuffle_wrinkly:
spoiler.human_hint_doors = human_hint_doors
if shuffle_tns:
spoiler.human_portal_doors = human_portal_doors
if shuffle_dkportal:
spoiler.human_entry_doors = human_entry_doors
def ShuffleVanillaDoors(spoiler):
"""Shuffle T&S and Wrinkly doors amongst the vanilla locations."""
ClearHintDoorLogic(spoiler)
# Prepare data structures that will hold the door info
human_hint_doors = {
"Jungle Japes": {},
"Angry Aztec": {},
"Frantic Factory": {},
"Gloomy Galleon": {},
"Fungi Forest": {},
"Crystal Caves": {},
"Creepy Castle": {},
}
human_portal_doors = {
"Jungle Japes": {},
"Angry Aztec": {},
"Frantic Factory": {},
"Gloomy Galleon": {},
"Fungi Forest": {},
"Crystal Caves": {},
"Creepy Castle": {},
}
shuffled_door_data = {
Levels.JungleJapes: [],
Levels.AngryAztec: [],
Levels.FranticFactory: [],
Levels.GloomyGalleon: [],
Levels.FungiForest: [],
Levels.CrystalCaves: [],
Levels.CreepyCastle: [],
}
for level in door_locations:
# Reset the data structures for door shuffling information sharing
shuffled_door_data[level] = []
human_hint_doors[level_list[level]] = {}
human_portal_doors[level_list[level]] = {}
# Find the vanilla doors that are valid hint locations and clear their door
# In Dos' Doors, this section finds exactly 6 doors per level
vanilla_door_indexes = []
for door_index, door in enumerate(door_locations[level]):
# This catches all default Wrinkly and T&S locations in addition to the additional doors needed for Dos' Doors as needed
if (door.default_placed != DoorType.null and door.default_placed != DoorType.dk_portal) or (door.dos_door and spoiler.settings.dos_door_rando):
# In Dos' Doors, we only need one lobby door, so ignore the rest
if spoiler.settings.dos_door_rando and door.default_placed == DoorType.wrinkly and not door.dos_door:
continue
door.placed = DoorType.null
vanilla_door_indexes.append(door_index)
spoiler.settings.random.shuffle(vanilla_door_indexes)
# One random vanilla T&S per level is locked to being a T&S - Two non-vanilla Japes locations are eligible in Dos' Doors (hence that DoorType.null eligibility)
locked_tns_options = [
idx for idx in vanilla_door_indexes if door_locations[level][idx].default_placed in (DoorType.boss, DoorType.null) and DoorType.boss in door_locations[level][idx].door_type
]
locked_tns_index = spoiler.settings.random.choice(locked_tns_options)
locked_tns = door_locations[level][locked_tns_index]
locked_tns.assignPortal(spoiler)
human_portal_doors[level_list[level]]["T&S #1"] = locked_tns.name
shuffled_door_data[level].append((locked_tns_index, DoorType.boss))
vanilla_door_indexes.remove(locked_tns_index)
# All other locations are fair game for hint doors - place one per kong
for kong in [0, 3, 4, 2, 1]: # The order of operations here matters because of our good friend Crystal Caves, which has restricted access to certain T&S locations
assignee = Kongs(kong % 5)
if level == Levels.CrystalCaves:
# Caves must consider the kong_lst for each door - there's one T&S that only Diddy can logically access as well as one only Diddy/Lanky can logically access
filtered_doors = [idx for idx in vanilla_door_indexes if assignee in door_locations[level][idx].kongs]
selected_door_index = spoiler.settings.random.choice(filtered_doors)
vanilla_door_indexes.remove(selected_door_index)
else:
# Everywhere else? Pick a door, any door
selected_door_index = vanilla_door_indexes.pop(0) # This should never fail
selected_door = door_locations[level][selected_door_index]
# Assign it to this Kong
selected_door.assignDoor(assignee)
human_hint_doors[level_list[level]][str(assignee.name).capitalize()] = selected_door.name
shuffled_door_data[level].append((selected_door_index, DoorType.wrinkly, int(assignee)))
# Add this hint door's location back to the logic
doorLocation = GetDoorLocationForKongAndLevel(kong, level)
region = spoiler.RegionList[selected_door.logicregion]
region.locations.append(LocationLogic(doorLocation, selected_door.logic))
spoiler.LocationList[doorLocation].name = f"{level_to_name[level]} Hint Door: {selected_door.name}"
# Any remaining vanilla door that isn't occupied and is a T&S door will get a T&S - the number of doors here will vary based on how many hints were placed in lobby vs level
placed_tns_count = 1
for door_index in vanilla_door_indexes:
vanilla_door = door_locations[level][door_index]
if vanilla_door.placed == DoorType.null and vanilla_door.default_placed == DoorType.boss and DoorType.boss in vanilla_door.door_type:
placed_tns_count += 1
vanilla_door.assignPortal(spoiler)
human_portal_doors[level_list[level]]["T&S #" + str(placed_tns_count)] = vanilla_door.name
shuffled_door_data[level].append((door_index, DoorType.boss))
# Track all touched doors in a variable and put it in the spoiler because changes to the static list do not save
spoiler.shuffled_door_data = shuffled_door_data
# Give human text to spoiler log
if spoiler.settings.wrinkly_location_rando:
spoiler.human_hint_doors = human_hint_doors
if spoiler.settings.tns_location_rando:
spoiler.human_portal_doors = human_portal_doors
def ClearHintDoorLogic(spoiler):
"""Remove existing hint door locations from the logic in preparation for custom door locations to be added."""
for id, region in spoiler.RegionList.items():
region.locations = [loclogic for loclogic in region.locations if loclogic.id < Locations.JapesDonkeyDoor or loclogic.id > Locations.CastleChunkyDoor]
def SetProgressiveHintDoorLogic(spoiler):
"""Set up hint door location logic for progressive hints to unlock them with GB amounts."""
dont_clear_hints = Types.Hint in spoiler.settings.shuffled_location_types and not (spoiler.settings.vanilla_door_rando or spoiler.settings.wrinkly_location_rando)
# Clear out old hint logic, including any custom logic that may have been placed. Don't need any of it.
if not dont_clear_hints:
ClearHintDoorLogic(spoiler)
hint_count = 35
hint_costs = []
for i in range(hint_count):
door_location = Locations.JapesDonkeyDoor + i # Hint door locations are ordered in their unlocking
hint_costs.append(getHintRequirement(i, spoiler.settings.progressive_hint_count))
# I probably hate this more than you do but lambda functions in python REALLY like to mutate apparently
spoiler.RegionList[Regions.GameStart].locations.append(LocationLogic(Locations.ProgressiveHint_01, lambda l: l.canFulfillProgHint(hint_costs[0])))
spoiler.RegionList[Regions.GameStart].locations.append(LocationLogic(Locations.ProgressiveHint_02, lambda l: l.canFulfillProgHint(hint_costs[1])))
spoiler.RegionList[Regions.GameStart].locations.append(LocationLogic(Locations.ProgressiveHint_03, lambda l: l.canFulfillProgHint(hint_costs[2])))
spoiler.RegionList[Regions.GameStart].locations.append(LocationLogic(Locations.ProgressiveHint_04, lambda l: l.canFulfillProgHint(hint_costs[3])))
spoiler.RegionList[Regions.GameStart].locations.append(LocationLogic(Locations.ProgressiveHint_05, lambda l: l.canFulfillProgHint(hint_costs[4])))
spoiler.RegionList[Regions.GameStart].locations.append(LocationLogic(Locations.ProgressiveHint_06, lambda l: l.canFulfillProgHint(hint_costs[5])))
spoiler.RegionList[Regions.GameStart].locations.append(LocationLogic(Locations.ProgressiveHint_07, lambda l: l.canFulfillProgHint(hint_costs[6])))
spoiler.RegionList[Regions.GameStart].locations.append(LocationLogic(Locations.ProgressiveHint_08, lambda l: l.canFulfillProgHint(hint_costs[7])))
spoiler.RegionList[Regions.GameStart].locations.append(LocationLogic(Locations.ProgressiveHint_09, lambda l: l.canFulfillProgHint(hint_costs[8])))
spoiler.RegionList[Regions.GameStart].locations.append(LocationLogic(Locations.ProgressiveHint_10, lambda l: l.canFulfillProgHint(hint_costs[9])))
spoiler.RegionList[Regions.GameStart].locations.append(LocationLogic(Locations.ProgressiveHint_11, lambda l: l.canFulfillProgHint(hint_costs[10])))
spoiler.RegionList[Regions.GameStart].locations.append(LocationLogic(Locations.ProgressiveHint_12, lambda l: l.canFulfillProgHint(hint_costs[11])))
spoiler.RegionList[Regions.GameStart].locations.append(LocationLogic(Locations.ProgressiveHint_13, lambda l: l.canFulfillProgHint(hint_costs[12])))
spoiler.RegionList[Regions.GameStart].locations.append(LocationLogic(Locations.ProgressiveHint_14, lambda l: l.canFulfillProgHint(hint_costs[13])))
spoiler.RegionList[Regions.GameStart].locations.append(LocationLogic(Locations.ProgressiveHint_15, lambda l: l.canFulfillProgHint(hint_costs[14])))
spoiler.RegionList[Regions.GameStart].locations.append(LocationLogic(Locations.ProgressiveHint_16, lambda l: l.canFulfillProgHint(hint_costs[15])))
spoiler.RegionList[Regions.GameStart].locations.append(LocationLogic(Locations.ProgressiveHint_17, lambda l: l.canFulfillProgHint(hint_costs[16])))
spoiler.RegionList[Regions.GameStart].locations.append(LocationLogic(Locations.ProgressiveHint_18, lambda l: l.canFulfillProgHint(hint_costs[17])))
spoiler.RegionList[Regions.GameStart].locations.append(LocationLogic(Locations.ProgressiveHint_19, lambda l: l.canFulfillProgHint(hint_costs[18])))
spoiler.RegionList[Regions.GameStart].locations.append(LocationLogic(Locations.ProgressiveHint_20, lambda l: l.canFulfillProgHint(hint_costs[19])))
spoiler.RegionList[Regions.GameStart].locations.append(LocationLogic(Locations.ProgressiveHint_21, lambda l: l.canFulfillProgHint(hint_costs[20])))
spoiler.RegionList[Regions.GameStart].locations.append(LocationLogic(Locations.ProgressiveHint_22, lambda l: l.canFulfillProgHint(hint_costs[21])))
spoiler.RegionList[Regions.GameStart].locations.append(LocationLogic(Locations.ProgressiveHint_23, lambda l: l.canFulfillProgHint(hint_costs[22])))
spoiler.RegionList[Regions.GameStart].locations.append(LocationLogic(Locations.ProgressiveHint_24, lambda l: l.canFulfillProgHint(hint_costs[23])))
spoiler.RegionList[Regions.GameStart].locations.append(LocationLogic(Locations.ProgressiveHint_25, lambda l: l.canFulfillProgHint(hint_costs[24])))
spoiler.RegionList[Regions.GameStart].locations.append(LocationLogic(Locations.ProgressiveHint_26, lambda l: l.canFulfillProgHint(hint_costs[25])))
spoiler.RegionList[Regions.GameStart].locations.append(LocationLogic(Locations.ProgressiveHint_27, lambda l: l.canFulfillProgHint(hint_costs[26])))
spoiler.RegionList[Regions.GameStart].locations.append(LocationLogic(Locations.ProgressiveHint_28, lambda l: l.canFulfillProgHint(hint_costs[27])))
spoiler.RegionList[Regions.GameStart].locations.append(LocationLogic(Locations.ProgressiveHint_29, lambda l: l.canFulfillProgHint(hint_costs[28])))
spoiler.RegionList[Regions.GameStart].locations.append(LocationLogic(Locations.ProgressiveHint_30, lambda l: l.canFulfillProgHint(hint_costs[29])))
spoiler.RegionList[Regions.GameStart].locations.append(LocationLogic(Locations.ProgressiveHint_31, lambda l: l.canFulfillProgHint(hint_costs[30])))
spoiler.RegionList[Regions.GameStart].locations.append(LocationLogic(Locations.ProgressiveHint_32, lambda l: l.canFulfillProgHint(hint_costs[31])))
spoiler.RegionList[Regions.GameStart].locations.append(LocationLogic(Locations.ProgressiveHint_33, lambda l: l.canFulfillProgHint(hint_costs[32])))
spoiler.RegionList[Regions.GameStart].locations.append(LocationLogic(Locations.ProgressiveHint_34, lambda l: l.canFulfillProgHint(hint_costs[33])))
spoiler.RegionList[Regions.GameStart].locations.append(LocationLogic(Locations.ProgressiveHint_35, lambda l: l.canFulfillProgHint(hint_costs[34])))