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

420 lines
22 KiB
Python

"""Randomize Boss Locations."""
from array import array
from randomizer.Enums.Items import Items
from randomizer.Enums.Kongs import Kongs
from randomizer.Enums.Levels import Levels
from randomizer.Enums.Locations import Locations
from randomizer.Enums.Settings import SlamRequirement, HardBossesSelected
from randomizer.Lists.Exceptions import BossOutOfLocationsException, PlandoIncompatibleException
from randomizer.Enums.Maps import Maps
from randomizer.Patching.Library.Generic import IsItemSelected
BossMapList = [
Maps.JapesBoss,
Maps.AztecBoss,
Maps.FactoryBoss,
Maps.GalleonBoss,
Maps.FungiBoss,
Maps.CavesBoss,
Maps.CastleBoss,
]
KRoolMaps = [
Maps.KroolDonkeyPhase,
Maps.KroolDiddyPhase,
Maps.KroolLankyPhase,
Maps.KroolTinyPhase,
Maps.KroolChunkyPhase,
]
def getBosses(settings) -> list:
"""Get list of bosses."""
boss_maps = BossMapList.copy()
if settings.krool_in_boss_pool:
boss_maps.extend(KRoolMaps.copy())
return [x for x in boss_maps if x not in settings.krool_order]
def ShuffleBosses(boss_location_rando: bool, settings):
"""Shuffle boss locations."""
boss_maps = getBosses(settings)
if boss_location_rando:
settings.random.shuffle(boss_maps)
return boss_maps
def HardBossesEnabled(settings, check: HardBossesSelected) -> bool:
"""Return whether the hard bosses setting is on."""
return IsItemSelected(settings.hard_bosses, settings.hard_bosses_selected, check, False)
def ShuffleBossKongs(settings, locked_levels=[]):
"""Shuffle the kongs required for the bosses."""
vanillaBossKongs = {
Maps.JapesBoss: Kongs.donkey,
Maps.AztecBoss: Kongs.diddy,
Maps.FactoryBoss: Kongs.tiny,
Maps.GalleonBoss: Kongs.lanky,
Maps.FungiBoss: Kongs.chunky,
Maps.CavesBoss: Kongs.donkey,
Maps.CastleBoss: Kongs.lanky,
Maps.KroolDonkeyPhase: Kongs.donkey,
Maps.KroolDiddyPhase: Kongs.diddy,
Maps.KroolLankyPhase: Kongs.lanky,
Maps.KroolTinyPhase: Kongs.tiny,
Maps.KroolChunkyPhase: Kongs.chunky,
}
boss_kongs = []
for level in range(7):
if level in locked_levels:
boss_kongs.append(settings.boss_kongs[level])
continue
boss_map = settings.boss_maps[level]
if settings.boss_kong_rando:
kong = settings.random.choice(GetKongOptionsForBoss(boss_map, HardBossesEnabled(settings, HardBossesSelected.alternative_mad_jack_kongs)))
else:
kong = vanillaBossKongs[boss_map]
boss_kongs.append(kong)
return boss_kongs
def GetKongOptionsForBoss(boss_map: Maps, alt_mj_kongs: bool):
"""Randomly choses from the allowed list for the boss."""
possibleKongs = []
if boss_map == Maps.JapesBoss:
possibleKongs = [Kongs.donkey, Kongs.diddy, Kongs.lanky, Kongs.tiny, Kongs.chunky]
elif boss_map == Maps.AztecBoss:
possibleKongs = [Kongs.donkey, Kongs.diddy, Kongs.lanky, Kongs.tiny, Kongs.chunky]
elif boss_map == Maps.FactoryBoss:
if alt_mj_kongs:
possibleKongs = [Kongs.donkey, Kongs.tiny, Kongs.chunky]
else:
possibleKongs = [Kongs.tiny]
elif boss_map == Maps.GalleonBoss:
possibleKongs = [Kongs.donkey, Kongs.diddy, Kongs.lanky, Kongs.tiny, Kongs.chunky]
elif boss_map == Maps.FungiBoss:
possibleKongs = [Kongs.chunky]
elif boss_map == Maps.CavesBoss:
possibleKongs = [Kongs.donkey, Kongs.diddy, Kongs.lanky, Kongs.tiny, Kongs.chunky]
elif boss_map == Maps.CastleBoss:
possibleKongs = [Kongs.donkey, Kongs.diddy, Kongs.lanky, Kongs.tiny, Kongs.chunky]
elif boss_map in (
Maps.KroolDonkeyPhase,
Maps.KroolDiddyPhase,
Maps.KroolLankyPhase,
Maps.KroolTinyPhase,
Maps.KroolChunkyPhase,
):
# Not possible right now to make any other kong beat another's phase, however, if we were to do so:
# DK Phase - Make all kongs enter cannon barrels
# Other phases - ????????
possibleKongs = [Kongs(Kongs.donkey + (boss_map - Maps.KroolDonkeyPhase))]
return possibleKongs
def ShuffleKutoutKongs(random, boss_maps: array, boss_kongs: array, boss_kong_rando: bool):
"""Shuffle the Kutout kong order."""
vanillaKutoutKongs = [Kongs.lanky, Kongs.tiny, Kongs.chunky, Kongs.donkey, Kongs.diddy]
kutout_kongs = []
if boss_kong_rando:
if Maps.CastleBoss in boss_maps:
kutoutLocation = boss_maps.index(Maps.CastleBoss)
if kutoutLocation < 0 or kutoutLocation >= len(boss_kongs):
starting_kong = random.choice(vanillaKutoutKongs)
else:
starting_kong = boss_kongs[kutoutLocation]
else:
starting_kong = random.choice(vanillaKutoutKongs)
kongPool = vanillaKutoutKongs.copy()
kongPool.remove(starting_kong)
random.shuffle(kongPool)
kutout_kongs.append(starting_kong)
kutout_kongs.extend(kongPool)
else:
kutout_kongs = vanillaKutoutKongs
return kutout_kongs
def ShuffleKKOPhaseOrder(settings):
"""Shuffle the phase order in King Kut Out."""
kko_phases = [0, 1, 2, 3]
settings.random.shuffle(kko_phases)
kko_phase_subset = []
for phase_slot in range(3):
kko_phase_subset.append(kko_phases[phase_slot])
return kko_phase_subset.copy()
def ShuffleBossesBasedOnOwnedItems(spoiler, ownedKongs: dict, ownedMoves: dict):
"""Identify what levels can hold which bosses and place bosses accordingly, based on available Kongs and moves in each leve."""
bossOptions = {
Levels.JungleJapes: [],
Levels.AngryAztec: [],
Levels.FranticFactory: [],
Levels.GloomyGalleon: [],
Levels.FungiForest: [],
Levels.CrystalCaves: [],
Levels.CreepyCastle: [],
}
for level in bossOptions.keys():
# Pufftoss is always accessible
bossOptions[level].append(Maps.GalleonBoss)
# King Kut Out is usually accessible but does care about lava water
if not spoiler.LogicVariables.IsLavaWater() or ownedMoves[level].count(Items.ProgressiveInstrumentUpgrade) >= 3:
bossOptions[level].append(Maps.CastleBoss)
if Items.Barrels in ownedMoves[level]:
# These bosses only care about barrels
bossOptions[level].extend([Maps.JapesBoss, Maps.AztecBoss, Maps.CavesBoss])
# Dogadon 2 also needs Hunky and Chunky
if Kongs.chunky in ownedKongs[level] and Items.HunkyChunky in ownedMoves[level]:
bossOptions[level].append(Maps.FungiBoss)
# Lanky Phase also needs Lanky and Trombone
is_beta_lanky = IsItemSelected(spoiler.settings.hard_bosses, spoiler.settings.hard_bosses_selected, HardBossesSelected.beta_lanky_phase, False)
required_additional_item_lankyphase = Items.Grape if is_beta_lanky else Items.Trombone
if spoiler.settings.krool_in_boss_pool and Kongs.lanky in ownedKongs[level] and required_additional_item_lankyphase in ownedMoves[level]:
bossOptions[level].append(Maps.KroolLankyPhase)
# Mad Jack always requires a slam
if Items.ProgressiveSlam in ownedMoves[level]:
# In hard bosses any of Donkey, Tiny, or Chunky is sufficient
if HardBossesEnabled(spoiler.settings, HardBossesSelected.alternative_mad_jack_kongs) and any([kong for kong in ownedKongs[level] if kong in [Kongs.donkey, Kongs.tiny, Kongs.chunky]]):
bossOptions[level].append(Maps.FactoryBoss)
# Outside of hard bosses, you need exactly Tiny and Twirl
elif Kongs.tiny in ownedKongs[level] and Items.PonyTailTwirl in ownedMoves[level]:
bossOptions[level].append(Maps.FactoryBoss)
if spoiler.settings.krool_in_boss_pool:
# Donkey Phase may or may not need Blast
if Kongs.donkey in ownedKongs[level] and Items.Climbing in ownedMoves[level] and (not spoiler.settings.cannons_require_blast or Items.BaboonBlast in ownedMoves[level]):
bossOptions[level].append(Maps.KroolDonkeyPhase)
# Diddy Phase needs Peanut and Rocket
if Kongs.diddy in ownedKongs[level] and Items.RocketbarrelBoost in ownedMoves[level] and Items.Peanut in ownedMoves[level]:
bossOptions[level].append(Maps.KroolDiddyPhase)
# Tiny Phase needs Mini and Feather (no Orange logic yet (or ever?))
if Kongs.tiny in ownedKongs[level] and Items.MiniMonkey in ownedMoves[level] and Items.Feather in ownedMoves[level]:
bossOptions[level].append(Maps.KroolTinyPhase)
# Chunky Phase always needs Punch, Hunky, and Gorilla Gone
if Kongs.chunky in ownedKongs[level] and Items.PrimatePunch in ownedMoves[level] and Items.HunkyChunky in ownedMoves[level] and Items.GorillaGone in ownedMoves[level]:
# It also needs some number of slams - reference the settings for that number
slamsRequired = 1
spoiler.settings.chunky_phase_slam_req_internal
if spoiler.settings.chunky_phase_slam_req_internal == SlamRequirement.blue:
slamsRequired = 2
elif spoiler.settings.chunky_phase_slam_req_internal == SlamRequirement.red:
slamsRequired = 3
if ownedMoves[level].count(Items.ProgressiveSlam) >= slamsRequired:
bossOptions[level].append(Maps.KroolChunkyPhase)
placedLevels = []
placedBossMaps = []
placedBossKongs = []
while len(placedBossMaps) < 7:
# The number of options are the boss options for each level not already placed
levelsSortedByNumberOfOptions = [level for level in bossOptions.keys() if level not in placedLevels]
# This can include endgame phases, and this is intentional. If you have to squeeze in every boss/phase into this, you need every inch of leeway you can get.
# By sorting by the raw amount of available boss space, you won't ever place bosses in a bad order (outside of utterly egregious circumstances).
levelsSortedByNumberOfOptions.sort(
key=lambda x: len([map for map in bossOptions[x] if map not in placedBossMaps]) + spoiler.settings.random.random()
) # The random factor here is so there's randomness among tied counts (but never greater than 1)
# Always pick the level with the least options - this guarantees we never orphan a level with no options (unless it was forced anyway)
mostRestrictiveLevel = levelsSortedByNumberOfOptions[0]
notTakenBossOptions = [map for map in bossOptions[mostRestrictiveLevel] if map not in placedBossMaps and map not in spoiler.settings.krool_order]
chosenBoss = None
# If we don't have an option available for this very restrictive level
# OR if we haven't placed many bosses yet, we'll take a peek at the endgame and see if any of those could fit
if not any(notTakenBossOptions) or (spoiler.settings.krool_in_boss_pool and len(placedLevels) < 2):
# If we don't have an option available, desperate times call for desperate measures
# If T&S Bosses could be on endgame fights, we might have to go steal one
if spoiler.settings.krool_in_boss_pool:
# It's possible we can take a boss out of the endgame rotation and put it here instead
possibleEndgameBossSwaps = [map for map in bossOptions[mostRestrictiveLevel] if map in spoiler.settings.krool_order]
# If we can't do that, then there exists no combination of bosses that could make this fill work
if not any(possibleEndgameBossSwaps) and not any(notTakenBossOptions):
# I *really* hope this is infrequent or limited to lava water shenanigans
raise BossOutOfLocationsException("Fill has no valid boss/phase placement combinations.")
expandedBossOptions = notTakenBossOptions + possibleEndgameBossSwaps
chosenBoss = spoiler.settings.random.choice(expandedBossOptions)
if chosenBoss in possibleEndgameBossSwaps:
spoiler.settings.krool_order.remove(chosenBoss)
updateKRoolSettings(spoiler, chosenBoss)
else:
# This is likely limited to lava water shenanigans
raise BossOutOfLocationsException("Fill has no valid boss placement combinations.")
else:
chosenBoss = spoiler.settings.random.choice(notTakenBossOptions)
kongOptions = [
kong for kong in GetKongOptionsForBoss(chosenBoss, HardBossesEnabled(spoiler.settings, HardBossesSelected.alternative_mad_jack_kongs)) if kong in ownedKongs[mostRestrictiveLevel]
]
# This should be impossible, as bossOptions is populated referencing the ownedKongs dict
# This could become possible if a boss becomes beatable with unique combinations of Kong + move (e.g. a boss is beatable with (Kong A + Move B) OR (Kong C + Move D))
if len(kongOptions) == 0:
# I'll just hedge my bets anyway - remove this boss from eligiblility for this level and try again
bossOptions[mostRestrictiveLevel].remove(chosenBoss)
continue
# These will be the same index which will be convenient later
placedLevels.append(mostRestrictiveLevel)
placedBossMaps.append(chosenBoss)
placedBossKongs.append(Kongs(spoiler.settings.random.choice(kongOptions)))
# If we did steal a boss from the endgame, we need to put one back in.
# Note that this has zero impact on logic: you should have all items by the time you reach the endgame
while len(spoiler.settings.krool_order) < spoiler.settings.krool_phase_count:
possibleBosses = [map for map in getBosses(spoiler.settings) if map not in placedBossMaps and map not in spoiler.settings.krool_order]
newEndgameBoss = spoiler.settings.random.choice(possibleBosses)
spoiler.settings.krool_order.append(newEndgameBoss)
updateKRoolSettings(spoiler, newEndgameBoss)
spoiler.settings.random.shuffle(spoiler.settings.krool_order)
# UHHHH does this fuck with kongs assigned to phases? SURE HOPE NOT!
newBossMaps = [None, None, None, None, None, None, None]
newBossKongs = [None, None, None, None, None, None, None]
for level in bossOptions.keys():
index = placedLevels.index(level)
newBossMaps[level] = placedBossMaps[index]
newBossKongs[level] = placedBossKongs[index]
# Only apply this shuffle if the settings permit it
# If kongs are random we have to shuffle bosses and locations or else we might break logic
if spoiler.settings.kong_rando or spoiler.settings.boss_location_rando:
spoiler.settings.boss_maps = newBossMaps
else:
spoiler.settings.boss_maps = getBosses(spoiler.settings)
if spoiler.settings.kong_rando or spoiler.settings.boss_kong_rando:
# If we shuffle kongs but not locations, we must forcibly sort the array with the known valid kongs
if not spoiler.settings.boss_location_rando:
# This is outrageously niche, I sure hope it doesn't break
spoiler.settings.boss_kongs = [
spoiler.settings.random.choice([kong for kong in GetKongOptionsForBoss(Maps.JapesBoss, False) if kong in bossOptions[Levels.JungleJapes]]),
spoiler.settings.random.choice([kong for kong in GetKongOptionsForBoss(Maps.AztecBoss, False) if kong in bossOptions[Levels.AngryAztec]]),
spoiler.settings.random.choice(
[
kong
for kong in GetKongOptionsForBoss(
Maps.FactoryBoss,
HardBossesEnabled(spoiler.settings, HardBossesSelected.alternative_mad_jack_kongs),
)
if kong in bossOptions[Levels.FranticFactory]
]
),
spoiler.settings.random.choice([kong for kong in GetKongOptionsForBoss(Maps.GalleonBoss, False) if kong in bossOptions[Levels.GloomyGalleon]]),
spoiler.settings.random.choice([kong for kong in GetKongOptionsForBoss(Maps.FungiBoss, False) if kong in bossOptions[Levels.FungiForest]]),
spoiler.settings.random.choice([kong for kong in GetKongOptionsForBoss(Maps.CavesBoss, False) if kong in bossOptions[Levels.CrystalCaves]]),
spoiler.settings.random.choice([kong for kong in GetKongOptionsForBoss(Maps.CastleBoss, False) if kong in bossOptions[Levels.CreepyCastle]]),
]
else:
spoiler.settings.boss_kongs = newBossKongs
else:
spoiler.settings.boss_kongs = ShuffleBossKongs(spoiler.settings)
spoiler.settings.kutout_kongs = ShuffleKutoutKongs(spoiler.settings.random, spoiler.settings.boss_maps, spoiler.settings.boss_kongs, spoiler.settings.boss_kong_rando)
def ShuffleTinyPhaseToes(random):
"""Generate random assortment of toes for Tiny Phase."""
toe_sequence = []
previous_toe = 1 # Use 1 as the index as it's within distance of all toes, so all toes for the first in the sequence will be valid
for toe in range(10):
mode = random.randint(0, 10)
if (toe % 5) == 0:
# Prevent player position mode on the first toe
mode += 1
if mode == 0:
# Use player position
toe_sequence.append(0xFF)
previous_toe = 1
else:
# Determine random assortment
toe_list = list(range(4))
if (toe % 5) == 0:
# First toe
toe_list = [0, 2, 3]
toe_list = [x for x in toe_list if abs(x - previous_toe) < 3] # Prevent toes being selected that
toe_count = random.randint(1, min(3, len(toe_list) - 1))
if len(toe_list) <= 1:
toe_bitfield = 0
else:
activated_toes = random.sample(toe_list, toe_count)
unactivated_toes = [x for x in list(range(4)) if x not in activated_toes]
picked_toe = False
for t in (1, 2):
if t in unactivated_toes:
previous_toe = t
picked_toe = True
if not picked_toe:
previous_toe = random.choice(unactivated_toes)
toe_bitfield = 0
for toe in activated_toes:
toe_bitfield |= 1 << toe
toe_sequence.append(toe_bitfield)
return toe_sequence.copy()
def CorrectBossKongLocations(spoiler):
"""Correct the Kong assigned to each boss Location for more accurate hints."""
spoiler.LocationList[Locations.JapesKey].kong = spoiler.settings.boss_kongs[0]
spoiler.LocationList[Locations.AztecKey].kong = spoiler.settings.boss_kongs[1]
spoiler.LocationList[Locations.FactoryKey].kong = spoiler.settings.boss_kongs[2]
spoiler.LocationList[Locations.GalleonKey].kong = spoiler.settings.boss_kongs[3]
spoiler.LocationList[Locations.ForestKey].kong = spoiler.settings.boss_kongs[4]
spoiler.LocationList[Locations.CavesKey].kong = spoiler.settings.boss_kongs[5]
spoiler.LocationList[Locations.CastleKey].kong = spoiler.settings.boss_kongs[6]
def updateKRoolSettings(spoiler, phase):
"""Make sure the settings match the phases in the K. Rool order."""
if phase == Maps.KroolDonkeyPhase:
spoiler.settings.krool_donkey = not spoiler.settings.krool_donkey
elif phase == Maps.KroolDiddyPhase:
spoiler.settings.krool_diddy = not spoiler.settings.krool_diddy
elif phase == Maps.KroolLankyPhase:
spoiler.settings.krool_lanky = not spoiler.settings.krool_lanky
elif phase == Maps.KroolTinyPhase:
spoiler.settings.krool_tiny = not spoiler.settings.krool_tiny
elif phase == Maps.KroolChunkyPhase:
spoiler.settings.krool_chunky = not spoiler.settings.krool_chunky
elif phase == Maps.JapesBoss:
spoiler.settings.krool_dillo1 = not spoiler.settings.krool_dillo1
elif phase == Maps.AztecBoss:
spoiler.settings.krool_dog1 = not spoiler.settings.krool_dog1
elif phase == Maps.FactoryBoss:
spoiler.settings.krool_madjack = not spoiler.settings.krool_madjack
elif phase == Maps.GalleonBoss:
spoiler.settings.krool_pufftoss = not spoiler.settings.krool_pufftoss
elif phase == Maps.FungiBoss:
spoiler.settings.krool_dog2 = not spoiler.settings.krool_dog2
elif phase == Maps.CavesBoss:
spoiler.settings.krool_dillo2 = not spoiler.settings.krool_dillo2
elif phase == Maps.CastleBoss:
spoiler.settings.krool_kutout = not spoiler.settings.krool_kutout
def PlandoBosses(spoiler):
"""Fix bosses into their plando'd positions and then randomly fill the rest. This fill supercedes the standard boss placement algorithm."""
if spoiler.settings.boss_plando:
filledBosses = []
filledLevels = []
for level in range(7):
# If we intend to plando this boss, send it in there
if spoiler.settings.plandomizer_dict["plando_boss_order_" + str(level)] != -1:
bossMap = Maps(spoiler.settings.plandomizer_dict["plando_boss_order_" + str(level)])
plannedKong = spoiler.settings.plandomizer_dict["plando_boss_kong_" + str(level)]
eligibleKongs = GetKongOptionsForBoss(bossMap, HardBossesEnabled(spoiler.settings, HardBossesSelected.alternative_mad_jack_kongs))
if plannedKong != -1:
# If the planned Kong is incompatible with this boss, ship it back
if plannedKong not in eligibleKongs:
raise PlandoIncompatibleException(str(Kongs(plannedKong).name) + " is not eligible to fight " + str(bossMap.name))
spoiler.settings.boss_kongs[level] = Kongs(plannedKong)
else:
spoiler.settings.boss_kongs[level] = spoiler.settings.random.choice(eligibleKongs)
spoiler.settings.boss_maps[level] = bossMap
filledBosses.append(bossMap)
filledLevels.append(level)
remainingBosses = [boss for boss in getBosses(spoiler.settings) if boss not in filledBosses]
for level in range(7):
if level in filledLevels:
continue
bossMap = spoiler.settings.random.choice(remainingBosses)
spoiler.settings.boss_maps[level] = bossMap
remainingBosses.remove(bossMap)
spoiler.settings.boss_kongs = ShuffleBossKongs(spoiler.settings, locked_levels=filledLevels)
spoiler.settings.kutout_kongs = ShuffleKutoutKongs(spoiler.settings.random, spoiler.settings.boss_maps, spoiler.settings.boss_kongs, spoiler.settings.boss_kong_rando)