Files
dockipelago/worlds/dk64/randomizer/ShufflePorts.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

378 lines
18 KiB
Python

"""Shuffle Bananaport Locations."""
from randomizer.Lists.MapsAndExits import RegionMapList
import randomizer.LogicFiles.AngryAztec
import randomizer.LogicFiles.CreepyCastle
import randomizer.LogicFiles.CrystalCaves
import randomizer.LogicFiles.DKIsles
import randomizer.LogicFiles.FranticFactory
import randomizer.LogicFiles.FungiForest
import randomizer.LogicFiles.GloomyGalleon
import randomizer.LogicFiles.JungleJapes
from randomizer.Enums.Levels import Levels
from randomizer.Enums.Events import Events
from randomizer.Enums.Maps import Maps
from randomizer.Enums.Regions import Regions
from randomizer.Enums.Settings import ActivateAllBananaports, ShufflePortLocations
from randomizer.Lists.CustomLocations import CustomLocation, CustomLocations, LocationTypes, getBannedWarps
from randomizer.Lists.Warps import BananaportVanilla
from randomizer.LogicClasses import Event
PortShufflerData = {
Maps.JungleJapes: {
"level": Levels.JungleJapes,
"starting_warp": Events.JapesW1aTagged,
"global_warp_count": 10,
},
Maps.AngryAztec: {
"level": Levels.AngryAztec,
"starting_warp": Events.AztecW1aTagged,
"global_warp_count": 10,
},
Maps.FranticFactory: {
"level": Levels.FranticFactory,
"starting_warp": Events.FactoryW1aTagged,
"global_warp_count": 10,
},
Maps.GloomyGalleon: {
"level": Levels.GloomyGalleon,
"starting_warp": Events.GalleonW1aTagged,
"global_warp_count": 10,
},
Maps.FungiForest: {
"level": Levels.FungiForest,
"starting_warp": Events.ForestW1aTagged,
"global_warp_count": 10,
},
Maps.CrystalCaves: {
"level": Levels.CrystalCaves,
"starting_warp": Events.CavesW1aTagged,
"global_warp_count": 10,
},
Maps.CreepyCastle: {
"level": Levels.CreepyCastle,
"starting_warp": Events.CastleW1aTagged,
"global_warp_count": 10,
},
Maps.Isles: {
"level": Levels.DKIsles,
"starting_warp": Events.IslesW1aTagged,
"global_warp_count": 10,
},
Maps.AztecLlamaTemple: {
"level": Levels.AngryAztec,
"starting_warp": Events.LlamaW1aTagged,
"global_warp_count": 4,
},
Maps.CastleCrypt: {
"level": Levels.CreepyCastle,
"starting_warp": Events.CryptW1aTagged,
"global_warp_count": 6,
},
}
def addPort(spoiler, warp: CustomLocation, event_enum: Events):
"""Add bananaport to relevant Logic Region."""
spoiler.RegionList[warp.logic_region].events.append(Event(event_enum, warp.logic))
for k in BananaportVanilla:
if BananaportVanilla[k].event == event_enum:
BananaportVanilla[k].region_id = warp.logic_region
def removePorts(spoiler, permitted_maps: list[Maps]):
"""Remove all bananaports from Logic regions."""
level_logic_regions = {
Levels.DKIsles: randomizer.LogicFiles.DKIsles.LogicRegions,
Levels.JungleJapes: randomizer.LogicFiles.JungleJapes.LogicRegions,
Levels.AngryAztec: randomizer.LogicFiles.AngryAztec.LogicRegions,
Levels.FranticFactory: randomizer.LogicFiles.FranticFactory.LogicRegions,
Levels.GloomyGalleon: randomizer.LogicFiles.GloomyGalleon.LogicRegions,
Levels.FungiForest: randomizer.LogicFiles.FungiForest.LogicRegions,
Levels.CrystalCaves: randomizer.LogicFiles.CrystalCaves.LogicRegions,
Levels.CreepyCastle: randomizer.LogicFiles.CreepyCastle.LogicRegions,
}
BANNED_PORT_SHUFFLE_EVENTS = getBannedWarps(spoiler)
persisted_events = []
for map_id in PortShufflerData:
if map_id not in permitted_maps:
start_event = PortShufflerData[map_id]["starting_warp"]
total_count = PortShufflerData[map_id]["global_warp_count"]
persisted_events.extend([start_event + i for i in range(total_count)])
for level_id in level_logic_regions:
level = level_logic_regions[level_id]
for region in level:
region_data = spoiler.RegionList[region]
region_data.events = [
x for x in region_data.events if x.name < Events.JapesW1aTagged or x.name > Events.IslesW5bTagged or x.name in BANNED_PORT_SHUFFLE_EVENTS or x.name in persisted_events
]
def ResetPorts():
"""Reset all bananaports to their vanilla state."""
for k in BananaportVanilla:
BananaportVanilla[k].reset()
def isCustomLocationValid(spoiler, location: CustomLocation, map_id: Maps, level: Levels) -> bool:
"""Determine whether a custom location is valid for a warp pad."""
if location.map != map_id:
# Has to be in the right map
return False
if location.has_access_logic:
# Locations that have logic to access them are banned from being warp locations when those warps are pre-activated
if spoiler.settings.activate_all_bananaports == ActivateAllBananaports.all:
return False
elif spoiler.settings.activate_all_bananaports != ActivateAllBananaports.off and map_id == Maps.Isles:
return False
BANNED_PORT_SHUFFLE_EVENTS = getBannedWarps(spoiler)
if location.tied_warp_event is not None:
if location.tied_warp_event in BANNED_PORT_SHUFFLE_EVENTS:
# Disable all locked warp locations
return False
if spoiler.settings.enable_plandomizer:
if location.name in spoiler.settings.plandomizer_dict["reserved_custom_locations"][level]:
return False
if location.is_galleon_floating_crate:
return False
if location.map in [Maps.FungiForestLobby, Maps.CavesRotatingCabin]:
if location.vanilla_crown:
return False
if spoiler.settings.bananaport_placement_rando == ShufflePortLocations.vanilla_only:
if not location.vanilla_port:
return False
else:
return True
if spoiler.settings.bananaport_placement_rando == ShufflePortLocations.half_vanilla or (
spoiler.settings.bananaport_placement_rando == ShufflePortLocations.on and not spoiler.settings.useful_bananaport_placement
):
if location.logic_region in ONE_KONG_REGIONS:
return False
return location.isValidLocation(LocationTypes.Bananaport)
REGION_KLUMPS = {
# A way to bias against zones of a map with a lot of logic regions
# Any entries in the list will sort regarding region dict based on the key rather than the normal value
Regions.IslesMainUpper: [Regions.IslesEar, Regions.IslesHill],
Regions.KremIsleBeyondLift: [Regions.KremIsleMouth, Regions.KremIsleTopLevel],
Regions.CabinIsle: [Regions.IslesAboveWaterfall],
Regions.JungleJapesStart: [Regions.JapesBlastPadPlatform],
Regions.JungleJapesMain: [Regions.JapesTnSAlcove, Regions.JapesPaintingRoomHill],
Regions.JapesHill: [Regions.JapesHillTop, Regions.JapesCannonPlatform, Regions.JapesTopOfMountain],
Regions.JapesBeyondCoconutGate2: [Regions.JapesLankyCave, Regions.JapesUselessSlope],
Regions.AztecTunnelBeforeOasis: [Regions.AngryAztecStart, Regions.BetweenVinesByPortal],
Regions.TempleStart: [Regions.TempleGuitarPad],
Regions.LlamaTemple: [Regions.LlamaTempleBack, Regions.LlamaTempleMatching],
Regions.DonkeyTemple: [Regions.DonkeyTempleDeadEndRight],
Regions.DiddyTemple: [Regions.DiddyTempleDeadEndRight],
Regions.LankyTemple: [Regions.LankyTempleEntrance],
Regions.TinyTemple: [Regions.TinyTempleEntrance],
Regions.ChunkyTemple: [Regions.ChunkyTempleEntrance],
Regions.BeyondHatch: [Regions.FactoryStoragePipe],
Regions.RandD: [Regions.RandDUpper],
Regions.MiddleCore: [Regions.SpinningCore, Regions.UpperCore],
Regions.Lighthouse: [Regions.LighthouseAboveLadder],
Regions.MushroomUpperExterior: [Regions.MushroomNightExterior],
Regions.MushroomLowerExterior: [Regions.MushroomUpperMidExterior, Regions.MushroomBlastLevelExterior, Regions.GiantMushroomArea],
Regions.MillArea: [Regions.ForestTopOfMill, Regions.ForestVeryTopOfMill, Regions.ForestMillTopOfNightCage],
Regions.CrystalCavesMain: [Regions.CavesBlueprintPillar, Regions.CavesBananaportSpire, Regions.CavesBonusCave],
Regions.CabinArea: [Regions.CavesGGRoom, Regions.CavesRotatingCabinRoof, Regions.CavesSprintCabinRoof],
Regions.CastleTree: [Regions.CastleTreePastPunch],
Regions.Crypt: [Regions.CryptChunkyRoom, Regions.CryptDiddyRoom, Regions.CryptDonkeyRoom],
}
ONE_KONG_REGIONS = [
# These regions are not accessible by every kong.
Regions.JapesTopOfMountain,
Regions.AztecDonkeyQuicksandCave,
Regions.LlamaTempleBack,
Regions.FactoryTinyRaceLobby,
Regions.TreasureRoomDiddyGoldTower,
Regions.CavesBonusCave,
Regions.CavesBlueprintCave,
Regions.CavesBlueprintPillar,
Regions.CavesBananaportSpire,
]
warp_event_pairs = {}
def populate_warp_event_pairs():
"""Populate the dict of warp_event_pairs."""
for k in BananaportVanilla:
warp = BananaportVanilla[k].event
if warp not in warp_event_pairs.keys():
other_warp = [
x.event for x in BananaportVanilla.values() if x.map_id == BananaportVanilla[k].map_id and x.vanilla_warp == BananaportVanilla[k].vanilla_warp and x.event != BananaportVanilla[k].event
][0]
warp_event_pairs[warp] = other_warp
warp_event_pairs[other_warp] = warp
def selectUsefulWarpFullShuffle(random, list_of_custom_locations, list_of_warps, warp: CustomLocation = None):
"""Find a useful warp to link to given warp."""
region = warp.logic_region
klumped_regions = []
if region in REGION_KLUMPS.keys():
klumped_regions = REGION_KLUMPS[region]
klumped_regions.append(Regions.CreepyCastleMain)
x = warp.coords[0]
y = warp.coords[1]
z = warp.coords[2]
big_logic_regions = [Regions.CrystalCavesMain, Regions.CreepyCastleMain]
possible_warps = [x for x in list_of_warps if list_of_custom_locations[x].logic_region != region or list_of_custom_locations[x].logic_region in big_logic_regions]
if warp.logic_region in ONE_KONG_REGIONS:
possible_warps = [x for x in possible_warps if list_of_custom_locations[x].logic_region not in ONE_KONG_REGIONS]
for range in [1400, 1000, 800]:
narrow_down = []
for loc in possible_warps:
warp_pad = list_of_custom_locations[loc]
if (
abs(abs(x - warp_pad.coords[0]) - abs(z - warp_pad.coords[2])) > range
or abs(y - warp_pad.coords[1]) > 200
or warp_pad.logic_region != region
or warp_pad.logic_region not in klumped_regions
):
narrow_down.append(loc)
if len(narrow_down) > 8:
possible_warps = narrow_down
break
return random.choice(possible_warps)
def EventToMap(event_id: Events) -> str:
"""Convert a warp event enum to a map name string."""
if event_id < Events.JapesW1aTagged or event_id > Events.IslesW5bTagged:
return None
init_name = event_id.name
for x in range(5):
search_str = f"W{x + 1}"
if search_str in init_name:
return init_name.split(search_str)[0]
return None
def EventToName(spoiler, event_id: Events) -> str:
"""Convert a warp event enum to a string."""
if event_id < Events.JapesW1aTagged or event_id > Events.IslesW5bTagged:
return None
init_name = event_id.name
for x in range(5):
search_str = f"W{x + 1}"
end_str = f"Warp {x + 1}"
if search_str in init_name:
if spoiler.settings.bananaport_placement_rando == ShufflePortLocations.half_vanilla:
return end_str
return f"{end_str} ({init_name.split(search_str)[1][0]})"
return None
def ShufflePorts(spoiler, port_selection, human_ports):
"""Shuffle the location of bananaports."""
port_list = spoiler.settings.warp_level_list_selected
maps_to_check = [
Maps.Isles,
Maps.JungleJapes,
Maps.AngryAztec,
Maps.AztecLlamaTemple,
Maps.FranticFactory,
Maps.GloomyGalleon,
Maps.FungiForest,
Maps.CrystalCaves,
Maps.CreepyCastle,
Maps.CastleCrypt,
]
if len(port_list) > 0:
maps_to_check = port_list.copy()
removePorts(spoiler, maps_to_check)
BANNED_PORT_SHUFFLE_EVENTS = getBannedWarps(spoiler)
for level in CustomLocations:
level_lst = CustomLocations[level]
for map in PortShufflerData:
if PortShufflerData[map]["level"] == level and map in maps_to_check:
index_lst = list(range(len(level_lst)))
index_lst = [x for x in index_lst if isCustomLocationValid(spoiler, level_lst[x], map, level)]
global_count = PortShufflerData[map]["global_warp_count"]
start_event = PortShufflerData[map]["starting_warp"]
end_event = start_event + PortShufflerData[map]["global_warp_count"]
pick_count = global_count - len([x for x in BANNED_PORT_SHUFFLE_EVENTS if x >= start_event and x < end_event])
if len(index_lst) < pick_count:
raise Exception(f"Insufficient custom location count for {map.name}. Expected: {pick_count}. Actual: {len(index_lst)}")
pick_count = min(pick_count, len(index_lst))
warps = []
if spoiler.settings.useful_bananaport_placement and spoiler.settings.bananaport_placement_rando != ShufflePortLocations.vanilla_only:
spoiler.settings.random.shuffle(index_lst)
# Populate the region dict with custom locations in each region
region_dict = {}
for x in index_lst:
# Populate dict
if map == Maps.CreepyCastle:
# Castle is all 1 logic region, and it's usefulness is solely based on height
# As such, set the region as it's height component
y_val = level_lst[x].coords[1]
# Castle bottom = 400 (roughly), top is 2000 (roughly)
# 320 is deduced by (2000 - 400) / 5, splitting castle into 5 sections
region = int(y_val / 320)
else:
region = level_lst[x].logic_region
# Calculate the region based on klumping
for prop_region in REGION_KLUMPS:
if region in REGION_KLUMPS[prop_region]:
region = prop_region
break
if region not in region_dict:
region_dict[region] = []
region_dict[region].append(x)
# For all regions, push the first location in each region. Loop through regions repeatedly until warp list is filled
counter = pick_count
while counter > 0:
region_lst = [x for xi, x in enumerate(list(region_dict.keys())) if xi < counter]
for region in region_lst:
selected_warp = region_dict[region].pop(0)
warps.append(selected_warp)
counter -= 1
del_lst = []
for region in region_dict:
if len(region_dict[region]) == 0: # delete any empty region
del_lst.append(region)
for region in del_lst:
del region_dict[region]
else:
if spoiler.settings.bananaport_placement_rando == ShufflePortLocations.vanilla_only:
# Useful warps don't impact vanilla shuffle (yet). It's simpler and faster to just shuffle them
warps = index_lst.copy()
spoiler.settings.random.shuffle(warps)
else:
warps = spoiler.settings.random.sample(index_lst, pick_count)
if pick_count > 0:
for k in BananaportVanilla:
event_id = BananaportVanilla[k].event
if event_id >= start_event and event_id < end_event and event_id not in BANNED_PORT_SHUFFLE_EVENTS:
if not (spoiler.settings.bananaport_placement_rando == ShufflePortLocations.on and spoiler.settings.useful_bananaport_placement):
selected_port = warps.pop(0)
port_selection[k] = selected_port
else:
populate_warp_event_pairs()
if warp_event_pairs[event_id] in BANNED_PORT_SHUFFLE_EVENTS or warp_event_pairs[event_id] in port_selection.keys():
if warp_event_pairs[event_id] in port_selection.keys():
warp = level_lst[port_selection[warp_event_pairs[event_id]]]
else:
warp = [x for x in level_lst if x.tied_warp_event == warp_event_pairs[event_id]][0]
selected_port = selectUsefulWarpFullShuffle(spoiler.settings.random, level_lst, index_lst, warp)
warps = [x for x in warps if x != selected_port]
index_lst = [x for x in warps if x != selected_port]
port_selection[k] = selected_port
else:
selected_port = warps.pop(0)
index_lst.remove(selected_port)
port_selection[k] = selected_port
addPort(spoiler, level_lst[selected_port], event_id)
CustomLocations[level][selected_port].setCustomLocation(True)
map_name_spoiler = EventToMap(event_id)
if map_name_spoiler not in human_ports:
human_ports[map_name_spoiler] = {}
human_ports[map_name_spoiler][EventToName(spoiler, event_id)] = level_lst[selected_port].name
if len(warps) == 0:
break