Files
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

3854 lines
230 KiB
Python

"""Module used to distribute items randomly."""
from __future__ import annotations
from math import ceil
from typing import TYPE_CHECKING, Any, List, Optional, Set, Tuple, Union
from functools import lru_cache
import js
import randomizer.ItemPool as ItemPool
import randomizer.Lists.Exceptions as Ex
import randomizer.ShuffleExits as ShuffleExits
from randomizer.CompileHints import compileHints, compileMicrohints, compileSpoilerHints, getDoorRestrictionsForItem
from randomizer.Enums.Events import Events
from randomizer.Enums.Items import Items
from randomizer.Enums.Kongs import GetKongs, Kongs
from randomizer.Enums.Levels import Levels
from randomizer.Enums.Locations import Locations
from randomizer.Enums.MinigameType import MinigameType
from randomizer.Enums.Regions import Regions
from randomizer.Enums.HintRegion import HintRegion
from randomizer.Enums.SearchMode import SearchMode
from randomizer.Enums.Settings import (
ActivateAllBananaports,
BananaportRando,
CBRando,
ClimbingStatus,
DamageAmount,
DKPortalRando,
FasterChecksSelected,
FillAlgorithm,
FungiTimeSetting,
HardModeSelected,
HelmBonuses,
LogicType,
MinigameBarrels,
MoveRando,
ProgressiveHintItem,
RandomPrices,
RemovedBarriersSelected,
ShockwaveStatus,
ShuffleLoadingZones,
ShufflePortLocations,
SpoilerHints,
TrainingBarrels,
WinConditionComplex,
WrinklyHints,
)
from randomizer.Enums.Time import Time
from randomizer.Enums.Transitions import Transitions
from randomizer.Enums.Types import Types, BarrierItems
from randomizer.Lists.CustomLocations import resetCustomLocations
from randomizer.Enums.Maps import Maps
from randomizer.Lists.Item import ItemList
from randomizer.Lists.Location import SharedMoveLocations, SharedShopLocations
from randomizer.Lists.Minigame import BarrelMetaData, MinigameRequirements
from randomizer.Lists.ShufflableExit import GetLevelShuffledToIndex
from randomizer.LogicClasses import Sphere, TransitionFront
from randomizer.Patching import ApplyRandomizer
from randomizer.Patching.EnemyRando import randomize_enemies_0
from randomizer.Patching.Library.Generic import IsItemSelected
from randomizer.Prices import GetMaxForKong
from randomizer.Settings import Settings
from randomizer.ShuffleBarrels import BarrelShuffle
from randomizer.ShuffleBosses import CorrectBossKongLocations, ShuffleBossesBasedOnOwnedItems
from randomizer.ShuffleCBs import ShuffleCBs
from randomizer.ShuffleCoins import ShuffleCoins
from randomizer.ShuffleCrates import ShuffleMelonCrates
from randomizer.ShuffleCrowns import ShuffleCrowns
from randomizer.ShuffleDoors import SetProgressiveHintDoorLogic, ShuffleDoors, ShuffleVanillaDoors, UpdateDoorLevels
from randomizer.ShuffleFairies import ShuffleFairyLocations
from randomizer.ShuffleItems import ShuffleItems
from randomizer.ShuffleKasplats import (
ResetShuffledKasplatLocations,
ShuffleKasplatsAndLocations,
ShuffleKasplatsInVanillaLocations,
constants,
shufflable,
)
from randomizer.ShufflePatches import ShufflePatches
from randomizer.ShufflePorts import ShufflePorts, ResetPorts
from randomizer.ShuffleShopLocations import ShuffleShopLocations
from randomizer.ShuffleWarps import LinkWarps, ShuffleWarpsCrossMap
if TYPE_CHECKING:
from randomizer.LogicClasses import LogicVarHolder, Region
from randomizer.Spoiler import Spoiler
def KasplatShuffle(spoiler: Spoiler, LogicVariables: LogicVarHolder) -> None:
"""Facilitate the shuffling of kasplat types."""
# If these were ever set at any prior point (likely only relevant running locally) then reset them - the upcoming methods will handle this TODO: maybe do this on other shufflers
for location in shufflable:
spoiler.LocationList[location].inaccessible = False
for location in constants:
spoiler.LocationList[location].inaccessible = False
if spoiler.settings.kasplat_rando:
retries = 0
while True:
try:
# Clear any existing logic
ResetShuffledKasplatLocations(spoiler)
# Shuffle kasplats
if spoiler.settings.kasplat_location_rando:
ShuffleKasplatsAndLocations(spoiler, LogicVariables)
else:
ShuffleKasplatsInVanillaLocations(spoiler, LogicVariables)
# Verify world by assuring all locations are still reachable
spoiler.Reset()
if not VerifyWorld(spoiler):
if retries < 10:
raise Ex.KasplatPlacementException
else:
# This is the first VerifyWorld check, and serves as the canary in the coal mine
# If we get to this point in the code, the world itself is likely unstable from some combination of settings or bugs
js.postMessage("Not all locations reached.")
ResetShuffledKasplatLocations(spoiler)
raise Ex.LocationsFailureException("Unexpected failure to reach all locations - report this to the devs!")
return
except Ex.KasplatPlacementException:
retries += 1
js.postMessage("Kasplat placement failed. Retrying. Tries: " + str(retries))
# @lru_cache(maxsize=None) - Cache is disabled for now, as it's causing issues with certain bits of logic in LZR
def GetExitLevelExit(level, restart) -> Optional[Transitions]:
"""Get the exit that using the "Exit Level" button will take you to."""
# If you have option to restart, means there is no Exit Level option
if restart is not None:
return None
# Otherwise identify and return the exit that the "Exit Level" button will take you to
if level == Levels.JungleJapes:
return ShuffleExits.ShufflableExits[Transitions.JapesToIsles].shuffledId
elif level == Levels.AngryAztec:
return ShuffleExits.ShufflableExits[Transitions.AztecToIsles].shuffledId
elif level == Levels.FranticFactory:
return ShuffleExits.ShufflableExits[Transitions.FactoryToIsles].shuffledId
elif level == Levels.GloomyGalleon:
return ShuffleExits.ShufflableExits[Transitions.GalleonToIsles].shuffledId
elif level == Levels.FungiForest:
return ShuffleExits.ShufflableExits[Transitions.ForestToIsles].shuffledId
elif level == Levels.CrystalCaves:
return ShuffleExits.ShufflableExits[Transitions.CavesToIsles].shuffledId
elif level == Levels.CreepyCastle:
return ShuffleExits.ShufflableExits[Transitions.CastleToIsles].shuffledId
@lru_cache(maxsize=None)
def GetLobbyOfRegion(level):
"""Get the lobby region for the parameter's region."""
if level == Levels.JungleJapes:
return Regions.JungleJapesLobby
elif level == Levels.AngryAztec:
return Regions.AngryAztecLobby
elif level == Levels.FranticFactory:
return Regions.FranticFactoryLobby
elif level == Levels.GloomyGalleon:
return Regions.GloomyGalleonLobby
elif level == Levels.FungiForest:
return Regions.FungiForestLobby
elif level == Levels.CrystalCaves:
return Regions.CrystalCavesLobby
elif level == Levels.CreepyCastle:
return Regions.CreepyCastleLobby
else:
return None
@lru_cache(maxsize=None)
def GetLevelExitTransition(level):
"""Get the exit level transition for the parameter region's level."""
if level == Levels.JungleJapes:
return Transitions.JapesToIsles
elif level == Levels.AngryAztec:
return Transitions.AztecToIsles
elif level == Levels.FranticFactory:
return Transitions.FactoryToIsles
elif level == Levels.GloomyGalleon:
return Transitions.GalleonToIsles
elif level == Levels.FungiForest:
return Transitions.ForestToIsles
elif level == Levels.CrystalCaves:
return Transitions.CavesToIsles
elif level == Levels.CreepyCastle:
return Transitions.CastleToIsles
else:
return None
def should_skip_location(location, location_obj, spoiler, settings, region):
"""Check if a location should be skipped based on various criteria."""
# Skip if location is inaccessible
if location_obj.inaccessible:
return True
# Skip bonus barrels based on settings and logic
if location.bonusBarrel:
if not (
(location.bonusBarrel is MinigameType.BonusBarrel and settings.bonus_barrels == MinigameBarrels.skip)
or (location.bonusBarrel is MinigameType.HelmBarrelFirst and (settings.helm_barrels == MinigameBarrels.skip or settings.helm_room_bonus_count == HelmBonuses.zero))
or (location.bonusBarrel is MinigameType.HelmBarrelSecond and (settings.helm_barrels == MinigameBarrels.skip or settings.helm_room_bonus_count != HelmBonuses.two))
or (location.bonusBarrel is MinigameType.TrainingBarrel and settings.training_barrels_minigames == MinigameBarrels.skip)
):
if not MinigameRequirements[BarrelMetaData[location.id].minigame].logic(spoiler.LogicVariables):
return True
# Skip hint doors if the wrong Kong
if location_obj.type == Types.Hint and not spoiler.LogicVariables.HintAccess(location_obj, region.id):
return True
# Skip blueprint locations if the wrong Kong
if location_obj.item and ItemList[location_obj.item].type == Types.Blueprint:
if not spoiler.LogicVariables.BlueprintAccess(ItemList[location_obj.item]):
return True
# Skip Kasplats with no blueprint if the wrong Kong
if location_obj.type == Types.Blueprint:
if not spoiler.LogicVariables.IsKong(location_obj.kong) and not settings.free_trade_items:
return True
# Skip dirt patches if no shockwave
if location_obj.type == Types.RainbowCoin and not spoiler.LogicVariables.shockwave:
return True
# Skip Helm crowns if Helm isn't finished
if location_obj.type == Types.Crown and location_obj.level == Levels.HideoutHelm:
if Events.HelmFinished not in spoiler.LogicVariables.Events:
return True
return False
def GetAccessibleLocations(
spoiler: Spoiler,
startingOwnedItems: List[Union[Any, Items]],
searchType: SearchMode,
purchaseList: Optional[List[Locations]] = None,
targetItemId: None = None,
) -> Union[List[Sphere], List[Locations], bool, Set[Union[Locations, int]]]:
"""Search to find all reachable locations given owned items."""
settings = spoiler.settings
# No logic? Calls to this method that are checking things just return True
if settings.logic_type == LogicType.nologic and searchType in [
SearchMode.CheckAllReachable,
SearchMode.CheckBeatable,
SearchMode.CheckSpecificItemReachable,
]:
return True
if purchaseList is None:
purchaseList = []
accessible = set()
newLocations = set()
ownedItems = startingOwnedItems[:]
newItems = [] # debug code utility
if searchType == SearchMode.GeneratePlaythrough:
spoiler.playthroughTransitionOrder = []
playthroughLocations = []
unpurchasedEmptyShopLocationIds = []
kongAccessibleRegions = [
{Regions.GameStart},
{Regions.GameStart},
{Regions.GameStart},
{Regions.GameStart},
{Regions.GameStart},
]
eventAdded = True
UnderwaterRegions = {
Regions.LighthouseUnderwater,
Regions.ShipyardUnderwater,
Regions.TreasureRoom,
Regions.MermaidRoom,
Regions.Submarine,
Regions.LankyShip,
Regions.TinyShip,
Regions.BongosShip,
Regions.GuitarShip,
Regions.TromboneShip,
Regions.SaxophoneShip,
Regions.TriangleShip,
}
SurfaceWaterRegions = {Regions.Shipyard}
# Continue doing searches until nothing new is found
while len(newLocations) > 0 or eventAdded:
# Add items and events from the last search iteration
sphere = Sphere()
if playthroughLocations:
sphere.availableGBs = playthroughLocations[-1].availableGBs
for locationId in newLocations:
accessible.add(locationId)
location = spoiler.LocationList[locationId]
if location.logically_relevant:
spoiler.LogicVariables.SpecialLocationsReached.append(locationId)
# If this location has an item placed, add it to owned items
if location.item is not None:
# In search mode GetReachableWithControlledPurchases, only allowed to purchase items as prescribed by purchaseOrder
if location.type == Types.Shop and searchType == SearchMode.GetReachableWithControlledPurchases and locationId not in purchaseList:
continue
ownedItems.append(location.item)
newItems.append(location.item)
# If we want to generate the playthrough and the item is a playthrough item, add it to the sphere
if searchType == SearchMode.GeneratePlaythrough and ItemList[location.item].playthrough:
if location.item == Items.GoldenBanana:
sphere.availableGBs += 1
sphere.locations.append(locationId)
continue
# Banana hoard in a sphere by itself
elif location.item == Items.BananaHoard:
sphere.locations = [locationId]
break
# The starting shop owner locations are not eligible for the playthrough
if location.type not in (Types.Cranky, Types.Funky, Types.Candy, Types.Snide):
sphere.locations.append(locationId)
# If we're looking for one item and we find it, we're done
elif searchType == SearchMode.CheckSpecificItemReachable and location.item == targetItemId:
return True
eventAdded = False
# Reset new lists
newLocations = set()
# Update based on new items
spoiler.LogicVariables.Update(ownedItems)
newItems = []
if len(sphere.locations) > 0:
if searchType == SearchMode.GeneratePlaythrough:
sphere.seedBeaten = spoiler.LogicVariables.bananaHoard
playthroughLocations.append(sphere)
# If we're checking beatability, check for the Banana Hoard after updating the last set of locations
if searchType == SearchMode.CheckBeatable and spoiler.LogicVariables.bananaHoard:
return True
# Do a search for each owned kong
for kong in set(spoiler.LogicVariables.GetKongs()):
spoiler.LogicVariables.SetKong(kong)
startRegion = spoiler.RegionList[Regions.GameStart]
startRegion.id = Regions.GameStart
startRegion.dayAccess = [Events.Day in spoiler.LogicVariables.Events] * 5
startRegion.nightAccess = [Events.Night in spoiler.LogicVariables.Events] * 5
regionPool = list(kongAccessibleRegions[kong])
# Loop for each region until no more accessible regions found
while len(regionPool) > 0:
regionId = regionPool.pop()
region = spoiler.RegionList[regionId]
# If this region has a tag barrel, everyone can access this region now
if region.tagbarrel:
if region.dayAccess[kong]:
region.dayAccess = [True] * 5
if region.nightAccess[kong]:
region.nightAccess = [True] * 5
for i in range(5):
kongAccessibleRegions[i].add(regionId)
# Check accessibility for each event in this region
for event in region.events:
if event.name not in spoiler.LogicVariables.Events and event.logic(spoiler.LogicVariables):
# Add the event if it's not already in the list and its logic is satisfied
eventAdded = True
spoiler.LogicVariables.Events.append(event.name)
# Update region access based on specific events
if event.name == Events.Night:
if event.logic(spoiler.LogicVariables):
region.nightAccess[kong] = True
elif event.name == Events.Day:
if event.logic(spoiler.LogicVariables):
region.dayAccess[kong] = True
# Check accessibility for collectibles
if region.id in spoiler.CollectibleRegions.keys():
for collectible in spoiler.CollectibleRegions[region.id]:
if not collectible.added and collectible.kong in (kong, Kongs.any) and collectible.enabled and collectible.logic(spoiler.LogicVariables):
spoiler.LogicVariables.AddCollectible(collectible, region.level)
# Check accessibility for each location in this region
for location in region.locations:
# Skip locations already accessible or not meeting logic
if location.id in newLocations or location.id in accessible or not location.logic(spoiler.LogicVariables):
continue
location_obj = spoiler.LocationList[location.id]
if should_skip_location(location, location_obj, spoiler, settings, region):
continue
# Handle shop logic
if location_obj.type == Types.Shop:
shop_is_empty = location_obj.item is None or location_obj.item == Items.NoItem
location_can_be_bought = searchType != SearchMode.GetReachableWithControlledPurchases or location.id in purchaseList
if not shop_is_empty and location_can_be_bought:
spoiler.LogicVariables.PurchaseShopItem(location.id)
elif shop_is_empty:
unpurchasedEmptyShopLocationIds.append(location.id)
elif location.id == Locations.NintendoCoin:
# Spend Two Coins for arcade lever
spoiler.LogicVariables.Coins[Kongs.donkey] -= 2
spoiler.LogicVariables.SpentCoins[Kongs.donkey] += 2
newLocations.add(location.id)
# Check accessibility for each exit in this region
exits = region.exits[:]
# If loading zones are shuffled, the "Exit Level" button in the pause menu could potentially take you somewhere new
if settings.shuffle_loading_zones == ShuffleLoadingZones.all and region.level != Levels.DKIsles and region.level != Levels.Shops:
levelExit = GetExitLevelExit(region.level, region.restart)
# When shuffling levels, unplaced level entrances will have no destination yet
if levelExit is not None:
dest = ShuffleExits.ShufflableExits[levelExit].back.regionId
exits.append(TransitionFront(dest, lambda l: True))
# If we're generating the final playthrough, note down the order in which we access entrances for LZR purposes
# No need to check access on this transition, it's always accessible
if searchType == SearchMode.GeneratePlaythrough:
levelExitTransitionId = GetLevelExitTransition(region.level)
if levelExitTransitionId not in spoiler.playthroughTransitionOrder:
spoiler.playthroughTransitionOrder.append(levelExitTransitionId)
# If loading zones are not shuffled but you have a random starting location, you may need to exit level to escape some regions
elif settings.random_starting_region and region.level != Levels.DKIsles and region.level != Levels.Shops and region.restart is None:
levelLobby = GetLobbyOfRegion(region.level)
if levelLobby is not None and levelLobby not in kongAccessibleRegions[kong]:
exits.append(TransitionFront(levelLobby, lambda l: True))
for exit in exits:
destination = exit.dest
shuffle_id = exit.exitShuffleId
is_shuffled = shuffle_id is not None and not exit.assumed
# Handle shuffled exits
if is_shuffled:
shuffled_exit = ShuffleExits.ShufflableExits[shuffle_id]
if shuffled_exit.shuffled:
destination = ShuffleExits.ShufflableExits[shuffled_exit.shuffledId].back.regionId
elif shuffled_exit.toBeShuffled:
continue
# Check if the transition is accessible
if not exit.logic(spoiler.LogicVariables):
continue
# Handle water/lava restrictions
is_lava_water = spoiler.LogicVariables.IsLavaWater()
if is_lava_water and (settings.shuffle_loading_zones == ShuffleLoadingZones.all or settings.random_starting_region):
if destination in UnderwaterRegions and spoiler.LogicVariables.Melons < 3:
continue
if destination in SurfaceWaterRegions and spoiler.LogicVariables.Melons < 2:
continue
# Check time of day access
time_access = True
if exit.time == Time.Night and not region.nightAccess[kong]:
time_access = False
elif exit.time == Time.Day and not region.dayAccess[kong]:
time_access = False
if not time_access:
continue
# Track playthrough transitions if needed
if searchType == SearchMode.GeneratePlaythrough and settings.shuffle_loading_zones == ShuffleLoadingZones.all:
if shuffle_id and shuffle_id not in spoiler.playthroughTransitionOrder:
spoiler.playthroughTransitionOrder.append(shuffle_id)
# Add new regions to the queue
if destination not in kongAccessibleRegions[kong]:
kongAccessibleRegions[kong].add(destination)
new_region = spoiler.RegionList[destination]
new_region.id = destination
regionPool.append(destination)
# Update day/night access
region_list_dest = spoiler.RegionList[destination]
if region.dayAccess[kong] and exit.time != Time.Night and not region_list_dest.dayAccess[kong]:
region_list_dest.dayAccess[kong] = True
eventAdded = True
if region.nightAccess[kong] and exit.time != Time.Day and not region_list_dest.nightAccess[kong]:
region_list_dest.nightAccess[kong] = True
eventAdded = True
# Handle dusk time setting
if settings.fungi_time == FungiTimeSetting.dusk:
region_list_dest.dayAccess[kong] = True
region_list_dest.nightAccess[kong] = True
# Deathwarps currently send to the vanilla destination
if region.deathwarp and not settings.perma_death:
destination = region.deathwarp.dest
# If a region is accessible through this exit and has not yet been added, add it to the queue to be visited eventually
if destination not in kongAccessibleRegions[kong] and region.deathwarp.logic(spoiler.LogicVariables):
kongAccessibleRegions[kong].add(destination)
newRegion = spoiler.RegionList[destination]
newRegion.id = destination
regionPool.append(destination)
# If this region has day access, the deathwarp will occur on the same time of day
# Note that no deathwarps are dependent on time of day
if region.dayAccess[kong]:
spoiler.RegionList[destination].dayAccess[kong] = True
# Count as event added so search doesn't get stuck if region is searched,
# then later a new time of day access is found so it should be re-visited
eventAdded = True
# And vice versa
if region.nightAccess[kong]:
spoiler.RegionList[destination].nightAccess[kong] = True
eventAdded = True
# If we're here to get accessible locations for fill purposes, we need to take a harder look at all the empty shops we didn't buy
if searchType == SearchMode.GetReachableForFilling:
spoiler.settings.random.shuffle(unpurchasedEmptyShopLocationIds) # This shuffle is to not bias fills towards earlier shops
# For each location...
for location_id in unpurchasedEmptyShopLocationIds:
# If we can, "buy" the empty location. This will affect our ability to buy future locations. It's not a guarantee we'll be able to buy all of these locations.
if (location_id in SharedShopLocations and spoiler.LogicVariables.AnyKongCanBuy(location_id, buy_empty=True)) or (
location_id not in SharedShopLocations and spoiler.LogicVariables.CanBuy(location_id, buy_empty=True)
):
spoiler.LogicVariables.PurchaseShopItem(location_id)
# If we can't, treat the location as inaccessible
else:
accessible.remove(location_id)
if searchType in (
SearchMode.GetReachable,
SearchMode.GetReachableForFilling,
SearchMode.GetReachableWithControlledPurchases,
):
return accessible
elif searchType == SearchMode.CheckBeatable or searchType == SearchMode.CheckSpecificItemReachable:
# If the search has completed and the target item has not been found, then we failed to find it
# settings.debug_accessible = accessible
return False
elif searchType == SearchMode.GeneratePlaythrough:
return playthroughLocations
elif searchType == SearchMode.CheckAllReachable:
expected_accessible_locations = [x for x in spoiler.LocationList if not spoiler.LocationList[x].inaccessible]
if settings.extreme_debugging:
# Debugging variables: they are unaccessed but certainly useful. Do not touch!
incorrectly_accessible = [x for x in accessible if x not in expected_accessible_locations]
incorrectly_inaccessible = [x for x in expected_accessible_locations if x not in accessible]
always_inaccessible_locations = [x for x in spoiler.LocationList if spoiler.LocationList[x].inaccessible]
settings.debug_accessible = accessible
settings.debug_accessible_not = [location for location in spoiler.LocationList if location not in accessible]
settings.debug_enormous_pain_1 = [spoiler.LocationList[location] for location in settings.debug_accessible]
settings.debug_enormous_pain_3 = [spoiler.LocationList[location] for location in settings.debug_accessible_not]
if len(accessible) != len(expected_accessible_locations):
return False
return True
return len(accessible) == len(expected_accessible_locations)
elif searchType == SearchMode.GetUnreachable:
return [x for x in spoiler.LocationList if x not in accessible and not spoiler.LocationList[x].inaccessible]
def VerifyWorld(spoiler: Spoiler) -> bool:
"""Make sure all item locations are reachable on current world graph with no items placed and all items owned."""
settings = spoiler.settings
if settings.logic_type == LogicType.nologic:
return True # Don't need to verify world in no logic
unreachables = GetAccessibleLocations(spoiler, ItemPool.AllItemsUnrestricted(settings), SearchMode.GetUnreachable)
if len(spoiler.cb_placements) == 0:
unreachables = [
x
for x in unreachables
if x
not in [
Locations.IslesDonkeyMedal,
Locations.IslesDiddyMedal,
Locations.IslesLankyMedal,
Locations.IslesTinyMedal,
Locations.IslesChunkyMedal,
]
]
allLocationsReached = len(unreachables) == 0
allCBsFound = True
for level_index in range(9):
isles_cb_rando_enabled = IsItemSelected(spoiler.settings.cb_rando_enabled, spoiler.settings.cb_rando_list_selected, Levels.DKIsles)
if level_index == Levels.HideoutHelm:
continue
elif level_index == Levels.DKIsles and (not isles_cb_rando_enabled) or len(spoiler.cb_placements) == 0:
continue
if sum(spoiler.LogicVariables.ColoredBananas[level_index]) != 500:
missingCBs = []
for region_collectible_list in spoiler.CollectibleRegions.values():
for collectible in region_collectible_list:
if collectible.enabled and not collectible.added:
missingCBs.append(collectible)
allCBsFound = False
if spoiler.settings.extreme_debugging and not allLocationsReached:
print(f"Unable to reach all locations: {unreachables}")
if spoiler.settings.extreme_debugging and not allCBsFound:
print(f"Unable to reach all CBs: {spoiler.LogicVariables.ColoredBananas}")
spoiler.Reset()
return allLocationsReached and allCBsFound
def VerifyWorldWithWorstCoinUsage(spoiler: Spoiler) -> bool:
"""Make sure the game is beatable without it being possible to run out of coins for required moves."""
settings = spoiler.settings
if settings.logic_type == LogicType.nologic:
return True # Don't verify world in no logic
locationsToPurchase = []
reachable = []
maxCoins = [
GetMaxForKong(spoiler, Kongs.donkey),
GetMaxForKong(spoiler, Kongs.diddy),
GetMaxForKong(spoiler, Kongs.lanky),
GetMaxForKong(spoiler, Kongs.tiny),
GetMaxForKong(spoiler, Kongs.chunky),
]
# Set up some thresholds for speeding this method up
medalThreshold = settings.medal_requirement
fairyThreshold = settings.rareware_gb_fairies
pearlThreshold = settings.mermaid_gb_pearls
while 1:
spoiler.Reset()
reachable = GetAccessibleLocations(spoiler, [], SearchMode.GetReachableWithControlledPurchases, locationsToPurchase)
# Subtract the price of the chosen location from maxCoinsNeeded
coinsSpent = GetMaxCoinsSpent(spoiler, locationsToPurchase)
coinsNeeded = [maxCoins[kong] - coinsSpent[kong] for kong in range(0, 5)]
spoiler.LogicVariables.UpdateCoins()
coinsBefore = spoiler.LogicVariables.Coins.copy()
# print("Coins owned during search: " + str(coinsBefore))
# print("Coins needed during search: " + str(coinsNeeded))
# If we found enough coins that every kong can buy all their moves, world is valid!
if (
coinsBefore[Kongs.donkey] >= coinsNeeded[Kongs.donkey]
and coinsBefore[Kongs.diddy] >= coinsNeeded[Kongs.diddy]
and coinsBefore[Kongs.lanky] >= coinsNeeded[Kongs.lanky]
and coinsBefore[Kongs.tiny] >= coinsNeeded[Kongs.tiny]
and coinsBefore[Kongs.chunky] >= coinsNeeded[Kongs.chunky]
):
# print("Seed is valid, found enough coins with worst purchase order: " + str([LocationList[x].name + ": " + LocationList[x].item.name + ", " for x in locationsToPurchase]))
spoiler.Reset()
return True
# If we found the Banana Hoard, world is valid!
if spoiler.LogicVariables.bananaHoard:
# print("Seed is valid, found banana hoard with worst purchase order: " + str([LocationList[x].name + ": " + LocationList[x].item.name + ", " for x in locationsToPurchase]))
spoiler.Reset()
return True
# For each accessible shop location
newReachableShops = [
x
for x in reachable
if spoiler.LocationList[x].type == Types.Shop
and spoiler.LocationList[x].item is not None
and spoiler.LocationList[x].item != Items.NoItem
and x not in locationsToPurchase
and spoiler.LogicVariables.CanBuy(x)
]
shopDifferentials = {}
shopUnlocksItems = {}
# If no accessible shop locations found, means you got coin locked and the seed is not valid
if len(newReachableShops) == 0:
print("Seed is invalid, coin locked with purchase order: " + str([spoiler.LocationList[x].name + ": " + spoiler.LocationList[x].item.name + ", " for x in locationsToPurchase]))
spoiler.Reset()
return False
# We can cheat some - here we calculate things we know we can add to the purchase order for free
# All we have to do is ensure that these items are not progressive in ANY way
# If we manage to add anything to the purchase order, we cut N GetAccessibleLocation calls where N is the length of newReachableShops
anythingAddedToPurchaseOrder = False
# Thresholds are the values that would cause the next item of that type to give you access to more locations
# The GB threshold is the next B. Locker from what we've previously found - opening a B. Locker likely gives you access to more coins
currentGBCount = spoiler.LogicVariables.GoldenBananas
gbThreshold = 1000
for blocker in range(0, 8):
if settings.BLockerEntryCount[blocker] > currentGBCount and settings.BLockerEntryCount[blocker] < gbThreshold:
gbThreshold = settings.BLockerEntryCount[blocker]
currentMedalCount = spoiler.LogicVariables.BananaMedals # Jetpac access might give you another item that gives you access to more coins
currentFairyCount = spoiler.LogicVariables.BananaFairies # Rareware GB access might do the same
currentPearlCount = spoiler.LogicVariables.Pearls # Mermaid GB access might do the same
for shopLocationId in newReachableShops:
# Check all of the newly reachable shops' items
shopItem = spoiler.LocationList[shopLocationId].item
# If the item is not going to exactly meet that item type's threshold, we can freely purchase it knowing it will never be progression
if shopItem == Items.Pearl and (currentPearlCount < (pearlThreshold - 1) or currentPearlCount >= pearlThreshold):
currentPearlCount += 1 # Treat the item as collected for future calculations, we might approach the threshold during this process
locationsToPurchase.append(shopLocationId)
anythingAddedToPurchaseOrder = True
if shopItem == Items.BananaMedal and (currentMedalCount < (medalThreshold - 1) or currentMedalCount >= medalThreshold):
currentMedalCount += 1
locationsToPurchase.append(shopLocationId)
anythingAddedToPurchaseOrder = True
if shopItem == Items.BananaFairy and (currentFairyCount < (fairyThreshold - 1) or currentFairyCount >= fairyThreshold):
currentFairyCount += 1
locationsToPurchase.append(shopLocationId)
anythingAddedToPurchaseOrder = True
# Treat GBs and Blueprints as identical
if (shopItem == Items.GoldenBanana or shopItem in ItemPool.Blueprints()) and (currentGBCount < (gbThreshold - 1) or currentGBCount > gbThreshold):
currentGBCount += 1
locationsToPurchase.append(shopLocationId)
anythingAddedToPurchaseOrder = True
# These items will never practically give progression. Helm doors are not really relevant here, as any theoretical coin lock will happen WELL before this point.
if shopItem in (Items.BattleCrown, Items.IceTrapBubble, Items.RarewareCoin, Items.NintendoCoin):
locationsToPurchase.append(shopLocationId)
anythingAddedToPurchaseOrder = True
# If we added anything to the purchase order, short-circuit back to the top of the loop and keep going with a (hopefully) greatly expanded purchase list
if anythingAddedToPurchaseOrder:
continue
# Now that we know our next item has to give us progression in some form, we can consolidate our "worst location candidates" into the worst options among each type
# Find the most expensive location of each type (it may not exist)
mostExpensivePearl = None
pearlShops = [location for location in newReachableShops if spoiler.LocationList[location].item == Items.Pearl]
if settings.random_prices == RandomPrices.vanilla and len(pearlShops) > 0: # In vanilla prices, prices are by item so we know all these locations have the same price (0)
mostExpensivePearl = pearlShops[0]
else:
for shop in pearlShops:
if mostExpensivePearl is None or settings.prices[shop] > settings.prices[mostExpensivePearl]:
mostExpensivePearl = shop
mostExpensiveMedal = None
medalShops = [location for location in newReachableShops if spoiler.LocationList[location].item == Items.BananaMedal]
if settings.random_prices == RandomPrices.vanilla and len(medalShops) > 0: # Same vanilla price logic applies to all of the threshold types (they all cost 0)
mostExpensiveMedal = medalShops[0]
else:
for shop in medalShops:
if mostExpensiveMedal is None or settings.prices[shop] > settings.prices[mostExpensiveMedal]:
mostExpensiveMedal = shop
mostExpensiveFairy = None
fairyShops = [location for location in newReachableShops if spoiler.LocationList[location].item == Items.BananaFairy]
if settings.random_prices == RandomPrices.vanilla and len(fairyShops) > 0:
mostExpensiveFairy = fairyShops[0]
else:
for shop in fairyShops:
if mostExpensiveFairy is None or settings.prices[shop] > settings.prices[mostExpensiveFairy]:
mostExpensiveFairy = shop
mostExpensiveGB = None
gbShops = [location for location in newReachableShops if (spoiler.LocationList[location].item == Items.GoldenBanana or spoiler.LocationList[location].item in ItemPool.Blueprints())]
if settings.random_prices == RandomPrices.vanilla and len(gbShops) > 0: # While GBs and Blueprints aren't the same item, they both always cost 0 in vanilla
mostExpensiveGB = gbShops[0]
else:
for shop in gbShops:
if mostExpensiveGB is None or settings.prices[shop] > settings.prices[mostExpensiveGB]:
mostExpensiveGB = shop
# Prepare the candidates for "worst location" - exclude any of the threshold items that we know the worst of
thresholdItems = ItemPool.Blueprints().copy()
thresholdItems.extend([Items.Pearl, Items.BananaMedal, Items.BananaFairy, Items.GoldenBanana])
worstLocationCandidates = [shop for shop in newReachableShops if spoiler.LocationList[shop].item not in thresholdItems]
# If there exists a spot of this type, then we add the worst of this type to our list of candidates
if mostExpensivePearl is not None:
worstLocationCandidates.append(mostExpensivePearl)
if mostExpensiveMedal is not None:
worstLocationCandidates.append(mostExpensiveMedal)
if mostExpensiveFairy is not None:
worstLocationCandidates.append(mostExpensiveFairy)
if mostExpensiveGB is not None:
worstLocationCandidates.append(mostExpensiveGB)
locationToBuy = worstLocationCandidates[0]
if len(worstLocationCandidates) > 1: # Things can be sped up if there's only one option (this tends to happen)
for shopLocation in worstLocationCandidates:
# Recheck accessible to see how many coins will be available afterward
tempLocationsToPurchase = locationsToPurchase.copy()
tempLocationsToPurchase.append(shopLocation)
spoiler.Reset()
reachableAfter: list = GetAccessibleLocations(spoiler, [], SearchMode.GetReachableWithControlledPurchases, tempLocationsToPurchase)
spoiler.LogicVariables.UpdateCoins()
coinsAfter = spoiler.LogicVariables.Coins.copy()
# Calculate the coin differential
coinDifferential = [0, 0, 0, 0, 0]
for kong in spoiler.LogicVariables.GetKongs():
coinDifferential[kong] = coinsAfter[kong] - coinsBefore[kong]
# print("Coin differential: " + str(coinDifferential))
shopDifferentials[shopLocation] = coinDifferential
shopUnlocksItems[shopLocation] = [spoiler.LocationList[x].item for x in reachableAfter if x not in reachable and spoiler.LocationList[x].item is not None]
# Determine if this is the new worst move
if locationToBuy is None:
locationToBuy = shopLocation
continue
# Coin differential must be negative for at least one kong to be considered new worst
if len([x for x in shopDifferentials[shopLocation] if x < 0]) == 0:
continue
# If a move unlocks new kongs it is more useful than others, even if it has a worse coin differential
existingMoveKongsUnlocked = len([x for x in shopUnlocksItems[locationToBuy] if ItemList[x].type == Types.Kong])
currentMoveKongsUnlocked = len([x for x in shopUnlocksItems[shopLocation] if ItemList[x].type == Types.Kong])
if currentMoveKongsUnlocked > existingMoveKongsUnlocked:
continue
# If a move unlocks a new boss key it is more useful than others, even if it has a worse coin differential
existingMoveKeysUnlocked = len([x for x in shopUnlocksItems[locationToBuy] if ItemList[x].type == Types.Key])
currentMoveKeysUnlocked = len([x for x in shopUnlocksItems[shopLocation] if ItemList[x].type == Types.Key])
if currentMoveKeysUnlocked > existingMoveKeysUnlocked:
continue
# All else equal, pick the move with the lowest overall coin differential
existingMoveCoinDiff = sum(list(shopDifferentials[locationToBuy]))
currentMoveCoinDiff = sum(list(shopDifferentials[shopLocation]))
if currentMoveCoinDiff < existingMoveCoinDiff:
locationToBuy = shopLocation
# Purchase the "least helpful" move & add to owned Items
# print("Choosing to buy " + LocationList[locationToBuy].item.name + " from " + LocationList[locationToBuy].name)
locationsToPurchase.append(locationToBuy)
def ParePlaythrough(spoiler: Spoiler, PlaythroughLocations: List[Sphere]) -> None:
"""Pare playthrough down to only the essential elements."""
settings = spoiler.settings
AccessibleHintsForLocation = {}
locationsToAddBack = []
mostExpensiveBLocker = max(
[
settings.blocker_0,
settings.blocker_1,
settings.blocker_2,
settings.blocker_3,
settings.blocker_4,
settings.blocker_5,
settings.blocker_6,
settings.blocker_7,
]
)
# Check every location in the list of spheres.
for i in range(len(PlaythroughLocations) - 1, -1, -1):
# We can immediately ignore spheres past the first sphere that is beaten
if i > 0 and PlaythroughLocations[i - 1].seedBeaten:
PlaythroughLocations.remove(PlaythroughLocations[i])
continue
sphere = PlaythroughLocations[i]
# We want to track specific GBs in each sphere of the spoiler log up to and including the sphere where the last B. Locker becomes openable
if i > 0 and PlaythroughLocations[i - 1].availableGBs > mostExpensiveBLocker:
sphere.locations = [locationId for locationId in sphere.locations if spoiler.LocationList[locationId].item != Items.GoldenBanana]
for locationId in sphere.locations.copy():
location = spoiler.LocationList[locationId]
# All GBs that make it here are logically required
if location.item == Items.GoldenBanana:
continue
# These items aren't usually that important - to make it here they have to be part of the win condition or one of the Helm doors
# They'll be part of the Playthrough but aren't candidates for the WotH, so we don't have to do any calculations on them
if location.item in (Items.BananaFairy, Items.BananaMedal, Items.RainbowCoin, Items.BattleCrown):
continue
if location.item is not None and ItemList[location.item].type == Types.Blueprint:
continue
# Copy out item from location
item = location.item
location.item = None
# Check if the game is still beatable
spoiler.Reset()
gameIsBeatable = GetAccessibleLocations(spoiler, [], SearchMode.CheckBeatable)
# Make note of what hints are accessible without this WotH candidate in case it gets hinted later.
# This may miss hints available after the win condition is met, but those hints are never practically getting seen anyway.
AccessibleHintsForLocation[locationId] = spoiler.LogicVariables.Hints.copy()
if gameIsBeatable:
# If the game is still beatable, this is an unnecessary location. We remove it from the playthrough, as it is not strictly required.
sphere.locations.remove(locationId)
# In non-item rando, put back the items on a delay
if not spoiler.settings.shuffle_items:
# We delay the item to ensure future locations which may rely on this one do not give a false positive for beatability.
# This is legacy behavior I'm not convinced needs to exist. It stays in non-item rando because the performance cost is negligible there.
location.SetDelayedItem(item)
locationsToAddBack.append(locationId)
# In item rando, we do additional WotH paring via paths later, so we don't need to worry about getting it perfect here
else:
location.PlaceItem(spoiler, item)
else:
# If the game is not beatable without this item, don't remove it from the playthrough and add the item back. This is now a WotH candidate.
location.PlaceItem(spoiler, item)
# Some important items have inherent door restrictions depending on the settings
restrictions = getDoorRestrictionsForItem(spoiler, item)
if len(restrictions) > 0:
AccessibleHintsForLocation[locationId] = [hint for hint in AccessibleHintsForLocation[locationId] if hint in restrictions]
# Record that dictionary of hint access for when we compile hints
spoiler.accessible_hints_for_location = AccessibleHintsForLocation
# Check if there are any empty spheres, if so remove them
for i in range(len(PlaythroughLocations) - 1, -1, -1):
sphere = PlaythroughLocations[i]
if len(sphere.locations) == 0:
PlaythroughLocations.remove(sphere)
# Re-place those items which were delayed earlier.
for locationId in locationsToAddBack:
spoiler.LocationList[locationId].PlaceDelayedItem(spoiler)
def PareWoth(spoiler: Spoiler, PlaythroughLocations: List[Sphere]) -> List[Union[Locations, int]]:
"""Pare playthrough to locations which are Way of the Hoard (hard required by logic)."""
# The functionality is similar to ParePlaythrough, but we want to see if individual locations are
# hard required, so items are added back after checking regardless of the outcome.
WothLocations = []
for sphere in PlaythroughLocations:
# Don't want constant locations in woth and we can filter out some types of items as not being essential to the woth
for loc in [
loc
for loc in sphere.locations # If Keys are constant, we may still want path hints for them.
if (not spoiler.LocationList[loc].constant or ItemList[spoiler.LocationList[loc].item].type == Types.Key)
and ItemList[spoiler.LocationList[loc].item].type
not in (
Types.Banana,
Types.BlueprintBanana,
Types.Crown,
Types.Medal,
Types.Blueprint,
Types.Fairy,
Types.RainbowCoin,
Types.CrateItem,
Types.Enemies,
)
]:
WothLocations.append(loc)
WothLocations.append(Locations.BananaHoard) # The Banana Hoard is the endpoint of the Way of the Hoard
spoiler.majorItems = [] # Initialize in case something dumb happens
# Only need to build paths for item rando
if spoiler.settings.shuffle_items:
majorItems = IdentifyMajorItems(spoiler)
spoiler.majorItems = majorItems
CalculateWothPaths(spoiler, WothLocations, majorItems)
CalculateFoolish(spoiler, WothLocations, majorItems)
# Non-item rando needs additional WotH paring due to the delayed item re-placing done when paring the playthrough
else:
# Check every item location to see if removing it by itself makes the game unbeatable
for i in range(len(WothLocations) - 1, -1, -1):
locationId = WothLocations[i]
location = spoiler.LocationList[locationId]
item = location.item
location.item = None
# Check if game is still beatable
spoiler.Reset()
if GetAccessibleLocations(spoiler, [], SearchMode.CheckBeatable):
# If game is still beatable, this location is not hard required
WothLocations.remove(locationId)
# Either way, add location back
location.PlaceItem(spoiler, item)
# We kept Keys around to generate paths better, but we don't need them in the spoiler log or being hinted (except for the Helm Key if it's there and also keep the Banana Hoard path)
WothLocations = [loc for loc in WothLocations if not spoiler.LocationList[loc].constant or loc == Locations.HelmKey or loc == Locations.BananaHoard]
if spoiler.settings.shuffle_items:
# The non-key 8 paths are a bit misleading, so it's best not to show them
for path_loc in [key for key in spoiler.woth_paths.keys()]:
if path_loc not in WothLocations:
del spoiler.woth_paths[path_loc]
return WothLocations
def checkCommonBarriers(settings: Settings, target_item: BarrierItems, target_win_con: WinConditionComplex) -> bool:
"""Check common barriers which would make an item a major one."""
if settings.coin_door_item == target_item:
return True
if settings.crown_door_item == target_item:
return True
if target_item in settings.BLockerEntryItems:
return True
return settings.win_condition_item == target_win_con
def IdentifyMajorItems(spoiler: Spoiler) -> List[Locations]:
"""Identify the Major Items in this seed based on the item placement and the settings."""
# Use the settings to determine non-progression Major Items
majorItems = ItemPool.AllKongMoves()
if Types.Cranky in spoiler.settings.shuffled_location_types:
majorItems.extend(ItemPool.CrankyItems())
if Types.Funky in spoiler.settings.shuffled_location_types:
majorItems.extend(ItemPool.FunkyItems())
if Types.Candy in spoiler.settings.shuffled_location_types:
majorItems.extend(ItemPool.CandyItems())
if Types.Snide in spoiler.settings.shuffled_location_types:
majorItems.extend(ItemPool.SnideItems())
if spoiler.settings.training_barrels != TrainingBarrels.normal:
majorItems.extend(ItemPool.TrainingBarrelAbilities())
if spoiler.settings.climbing_status != ClimbingStatus.normal:
majorItems.extend(ItemPool.ClimbingAbilities())
if Types.Shockwave in spoiler.settings.shuffled_location_types:
majorItems.extend(ItemPool.ShockwaveTypeItems(spoiler.settings))
majorItems.extend(ItemPool.Keys())
majorItems.extend(ItemPool.Kongs(spoiler.settings))
if checkCommonBarriers(spoiler.settings, BarrierItems.CompanyCoin, WinConditionComplex.req_companycoins):
majorItems.append(Items.RarewareCoin) # A vanilla Rareware Coin should be considered a major item so medals will not be foolish
majorItems.append(Items.NintendoCoin) # A vanilla Nintendo Coin should be considered a major item
if checkCommonBarriers(spoiler.settings, BarrierItems.Blueprint, WinConditionComplex.req_bp):
majorItems.extend(ItemPool.Blueprints())
if checkCommonBarriers(spoiler.settings, BarrierItems.Medal, WinConditionComplex.req_medal):
majorItems.append(Items.BananaMedal)
if checkCommonBarriers(spoiler.settings, BarrierItems.Fairy, WinConditionComplex.req_fairy):
majorItems.append(Items.BananaFairy)
if checkCommonBarriers(spoiler.settings, BarrierItems.Crown, WinConditionComplex.req_crown):
majorItems.append(Items.BattleCrown)
if checkCommonBarriers(spoiler.settings, BarrierItems.Pearl, WinConditionComplex.req_pearl):
majorItems.append(Items.Pearl)
if checkCommonBarriers(spoiler.settings, BarrierItems.Bean, WinConditionComplex.req_bean):
majorItems.append(Items.Bean)
if checkCommonBarriers(spoiler.settings, BarrierItems.RainbowCoin, WinConditionComplex.req_rainbowcoin):
majorItems.append(Items.RainbowCoin)
# The contents of some locations can make entire classes of items not foolish
# Loop through these locations until no new items are added to the list of major items
newFoolishItems = True
while newFoolishItems:
newFoolishItems = False
if spoiler.LocationList[Locations.RarewareCoin].item in majorItems and Items.BananaMedal not in majorItems:
majorItems.append(Items.BananaMedal)
newFoolishItems = True
if spoiler.LocationList[Locations.RarewareBanana].item in majorItems and Items.BananaFairy not in majorItems:
majorItems.append(Items.BananaFairy)
newFoolishItems = True
if spoiler.LocationList[Locations.GalleonTinyPearls].item in majorItems and Items.Pearl not in majorItems:
majorItems.append(Items.Pearl)
newFoolishItems = True
if spoiler.LocationList[Locations.ForestTinyBeanstalk].item in majorItems and Items.Bean not in majorItems:
majorItems.append(Items.Bean)
newFoolishItems = True
return majorItems
def CalculateWothPaths(spoiler: Spoiler, WothLocations: List[Union[Locations, int]], MajorItems: List[Items]) -> None:
"""Calculate the Paths (dependencies) for each Way of the Hoard item."""
# Helps get more accurate paths by removing important obstacles to level entry
# Removes the following:
# - The need for GBs and coins to reach locations
# - The need for vines to progress in Aztec
# - The need for swim to get into level 4
# - The need for vines to get to upper Isles
# - The need for all keys to access K. Rool
# - The need for keys to open lobbies (this is done with open_lobbies)
old_open_lobbies_temp = spoiler.settings.open_lobbies # It's far less likely for a key to be a prerequisite
spoiler.LogicVariables.assumePaidBLockers = True # This means we don't have to worry about moves required to pay B. Lockers - we already know we can clear all B. Lockers
spoiler.LogicVariables.assumeInfiniteCoins = True # This means we don't have to worry about moves required to get coins - we already know there is no breaking purchase order
spoiler.LogicVariables.assumeKRoolAccess = True # This makes the K. Rool path better if we need it
if spoiler.settings.shuffle_loading_zones != ShuffleLoadingZones.all:
# These assumptions are only good in level order because entrances can matter more in LZR
spoiler.LogicVariables.assumeAztecEntry = True
spoiler.LogicVariables.assumeLevel4Entry = True
spoiler.LogicVariables.assumeUpperIslesAccess = True
spoiler.settings.open_lobbies = True
# If it's not Helm in upper Krem Isle, then assume you have access to this level as soon as you have the keys, bypassing the move needed to get up there.
if spoiler.settings.shuffle_helm_location:
spoiler.LogicVariables.assumeLevel8Entry = True # Do not ever assume this in LZR! It makes a specific entrance one-way to work.
# Identify important locations we might want to find the paths to
# Filter out the items that are never WotH
filtered_major_items = [
item
for item in MajorItems
if ItemList[item].type
not in (
Types.Banana,
Types.BlueprintBanana,
Types.Crown,
Types.Medal,
Types.Blueprint,
Types.Fairy,
Types.RainbowCoin,
Types.CrateItem,
Types.Enemies,
)
]
interesting_locations = []
for id, location in spoiler.LocationList.items():
if not location.inaccessible and location.item in filtered_major_items:
interesting_locations.append(id)
interesting_locations.append(Locations.BananaHoard)
# If intentionally starting with a slam, don't count it for paths
if spoiler.settings.start_with_slam:
interesting_locations.remove(Locations.IslesFirstMove)
# Starting shop owners should not be on paths - only shop owners can be in these locations
if Locations.ShopOwner_Location00 in interesting_locations:
interesting_locations.remove(Locations.ShopOwner_Location00)
if Locations.ShopOwner_Location01 in interesting_locations:
interesting_locations.remove(Locations.ShopOwner_Location01)
if Locations.ShopOwner_Location02 in interesting_locations:
interesting_locations.remove(Locations.ShopOwner_Location02)
if Locations.ShopOwner_Location03 in interesting_locations:
interesting_locations.remove(Locations.ShopOwner_Location03)
ordered_interesting_locations = []
# Prep the dictionaries that will contain the paths to our interesting locations
for locationId in WothLocations:
spoiler.woth_paths[locationId] = [locationId] # The endpoint is on its own path
ordered_interesting_locations.append(locationId) # Keeping WotH locations in order makes paths MUCH easier to read
for locationId in interesting_locations:
if locationId not in WothLocations:
spoiler.other_paths[locationId] = [locationId]
ordered_interesting_locations.append(locationId)
# If K. Rool is the win condition, prepare phase-specific paths as well
if spoiler.settings.win_condition_item == WinConditionComplex.beat_krool:
for phase in spoiler.settings.krool_order:
spoiler.krool_paths[phase] = []
for locationId in ordered_interesting_locations:
# Remove the item from the location
location = spoiler.LocationList[locationId]
item_id = location.item
location.item = None
# We also need to assume Kongs in order to get a "pure" path instead of Kong paths being a subset of most later paths.
# Anything locked behind a a Kong will then require everything that Kong requires.
# This sort of defeats the purpose of paths, as it would put everything in a Kong's path into the path of many, many items.
assumedItems = ItemPool.Kongs(spoiler.settings)
# Find all accessible locations without this item placed
spoiler.Reset()
accessible = GetAccessibleLocations(spoiler, assumedItems, SearchMode.GetReachable)
# Then check every other WotH location for accessibility
for other_location in WothLocations:
# If it is no longer accessible, then this location is on the path of that other location
if other_location not in accessible:
spoiler.woth_paths[other_location].append(locationId)
for other_location in spoiler.other_paths.keys():
if other_location not in accessible:
spoiler.other_paths[other_location].append(locationId)
# If the win condition is K. Rool, also add this location to those paths as applicable
if spoiler.settings.win_condition_item == WinConditionComplex.beat_krool:
final_boss_associated_event = {
Maps.JapesBoss: Events.KRoolDillo1,
Maps.AztecBoss: Events.KRoolDog1,
Maps.FactoryBoss: Events.KRoolJack,
Maps.GalleonBoss: Events.KRoolPufftoss,
Maps.FungiBoss: Events.KRoolDog2,
Maps.CavesBoss: Events.KRoolDillo2,
Maps.CastleBoss: Events.KRoolKKO,
Maps.KroolDonkeyPhase: Events.KRoolDonkey,
Maps.KroolDiddyPhase: Events.KRoolDiddy,
Maps.KroolLankyPhase: Events.KRoolLanky,
Maps.KroolTinyPhase: Events.KRoolTiny,
Maps.KroolChunkyPhase: Events.KRoolChunky,
}
for map_id in final_boss_associated_event:
if map_id in spoiler.settings.krool_order and final_boss_associated_event[map_id] not in spoiler.LogicVariables.Events:
spoiler.krool_paths[map_id].append(locationId)
elif spoiler.settings.win_condition_item == WinConditionComplex.dk_rap_items:
rap_assoc_name = {
"Donkey Verse": Events.DonkeyVerse,
"Diddy Verse": Events.DiddyVerse,
"Lanky Verse": Events.LankyVerse,
"Tiny Verse": Events.TinyVerse,
"Chunky Verse": Events.ChunkyVerse,
"The Fridge": Events.FridgeVerse,
}
for verse_name in rap_assoc_name:
if verse_name not in spoiler.rap_win_con_paths:
spoiler.rap_win_con_paths[verse_name] = []
if rap_assoc_name[verse_name] not in spoiler.LogicVariables.Events:
spoiler.rap_win_con_paths[verse_name].append(locationId)
# Put the item back for future calculations
location.PlaceItem(spoiler, item_id)
# After everything is calculated, get rid of paths for false WotH locations
# If an item doesn't show up on any other paths, it's not actually WotH
# This is rare, but could happen if the item at the location is needed for coins or B. Lockers - it's often required, but not helpful to hint at all
anything_removed = True
while anything_removed:
anything_removed = False
# Check every WotH location
for locationId in WothLocations:
location = spoiler.LocationList[locationId]
# If this item doesn't normally show up on paths but is definitely needed, no need to calculate it, it's definitely WotH
if location.item in assumedItems or location.item == Items.BananaHoard:
continue
# Check every other path to see if this location is on any other path
inAnotherPath = False
for otherLocationId in [loc for loc in WothLocations if loc != locationId]:
if locationId in spoiler.woth_paths[otherLocationId]:
inAnotherPath = True
break
# If it's not on any other path, it's not WotH
if not inAnotherPath:
# In Chaos B. Lockers, you may need certain items purely to pass B. Locker
if spoiler.settings.chaos_blockers:
# Most likely: The Bean is always required to pass the Bean Locker - if it gets here, that means you need it and it should be WotH
if location.item == Items.Bean and BarrierItems.Bean in spoiler.settings.BLockerEntryItems:
continue
# Less likely: Either of the two company coins could be strictly required due to items inside the levels they lock leading to the other one
if location.item in (Items.NintendoCoin, Items.RarewareCoin) and BarrierItems.CompanyCoin in spoiler.settings.BLockerEntryItems:
continue
# Even less likely: Pearls are in the same boat as the company coins, but there's 5 of them so it's considerably less likely to get here
if location.item == Items.Pearl and BarrierItems.Pearl in spoiler.settings.BLockerEntryItems:
continue
# Keys that make it here are also always WotH
if location.item in ItemPool.Keys():
continue
# We do need to double check our work sometimes - this item might be required to beat the game if it's needed to get into a level
# Only do this double-checking outside of LZR. The assumptions blocking level entry in LZR are less burdened with assumptions, so it's more likely to be accurate on the first pass.
skipDoubleCheck = spoiler.settings.shuffle_loading_zones == ShuffleLoadingZones.all
doubleCheckBeatsGame = False
if not skipDoubleCheck:
spoiler.LogicVariables.assumeAztecEntry = False
spoiler.LogicVariables.assumeLevel4Entry = False
spoiler.LogicVariables.assumeLevel8Entry = False
spoiler.LogicVariables.assumeUpperIslesAccess = False
spoiler.LogicVariables.assumeKRoolAccess = False # This item may also be needed to access K. Rool because of the aforementioned level entry
# Quickly check beatability after deleting this item - does removing this item make the game unbeatable?
spoiler.Reset()
item = location.item
location.item = None
# We still assume Kongs here!
assumedItems = ItemPool.Kongs(spoiler.settings)
if item in assumedItems: # Except for if it's the item we're testing
assumedItems.remove(item)
# Check if we still have every woth location
accessibleItems = GetAccessibleLocations(spoiler, assumedItems, SearchMode.GetReachable)
inaccessibleWoths = [loc for loc in WothLocations if loc not in accessibleItems]
if not any(inaccessibleWoths):
doubleCheckBeatsGame = True
location.PlaceItem(spoiler, item)
# If the game is still beatable when banned with the other assumptions (or if we're skipping the double checking), this item is definitely not WotH
if skipDoubleCheck or doubleCheckBeatsGame:
WothLocations.remove(locationId)
spoiler.other_paths[locationId] = spoiler.woth_paths[locationId]
del spoiler.woth_paths[locationId]
# If we remove anything, we have to check the whole list again
anything_removed = True
break
# None of these assumptions should ever make it out of this method
spoiler.LogicVariables.assumePaidBLockers = False
spoiler.LogicVariables.assumeInfiniteCoins = False
spoiler.LogicVariables.assumeAztecEntry = False
spoiler.LogicVariables.assumeLevel4Entry = False
spoiler.LogicVariables.assumeLevel8Entry = False
spoiler.LogicVariables.assumeUpperIslesAccess = False
spoiler.LogicVariables.assumeKRoolAccess = False
spoiler.settings.open_lobbies = old_open_lobbies_temp # Undo the open lobbies setting change as needed
# Confirm that all paths that should have at least one item on them do. This sidesteps an unsolved mystery issue where sometimes paths inexplicably end up empty.
# This should never happen, but it's *really* bad if it does, so this quickly double checks the work.
for goal, path in spoiler.woth_paths.items():
if len(path) < 1:
raise Ex.FillException("Rare path calculation error - report this to the devs with your settings string. Error code PL-1")
for goal, path in spoiler.krool_paths.items():
if len(path) < 1:
# Some K. Rool fights are expected to have no items on the path
expectedEmptyPathPhases = [Maps.GalleonBoss] # Galleon boss never requires an item
# Castle boss requires items only with lava water
if not spoiler.LogicVariables.IsLavaWater():
expectedEmptyPathPhases.append(Maps.CastleBoss)
# DK Phase sometimes needs blast and always needs climbing, but the climbing isn't relevant unless it's shuffled
if not spoiler.settings.cannons_require_blast and Types.Climbing not in spoiler.settings.shuffled_location_types:
expectedEmptyPathPhases.append(Maps.KroolDonkeyPhase)
# If your training moves are unshuffled, they don't end up on paths. All the Barrels-only bosses no longer have any path requirements.
if spoiler.settings.training_barrels == TrainingBarrels.normal:
expectedEmptyPathPhases.extend([Maps.JapesBoss, Maps.AztecBoss, Maps.CavesBoss])
if goal not in expectedEmptyPathPhases:
raise Ex.FillException("Rare path calculation error - report this to the devs with your settings string. Error code PL-2")
for goal, path in spoiler.rap_win_con_paths.items():
if len(path) < 1:
# One verse is sometimes expected to have no items on the path
expectedEmptyPathVerses = []
# If your training moves are unshuffled, they don't end up on paths. Chunky Verse only requires barrels, so it no longer has any path requirements.
if spoiler.settings.training_barrels == TrainingBarrels.normal:
expectedEmptyPathVerses.append(Events.ChunkyVerse)
if rap_assoc_name[goal] not in expectedEmptyPathVerses:
raise Ex.FillException("Rare path calculation error - report this to the devs with your settings string. Error code PL-3")
def CalculateFoolish(spoiler: Spoiler, WothLocations: List[Union[Locations, int]], MajorItems: List[Items]) -> None:
"""Calculate the items and regions that are foolish (blocking no major items)."""
# Identify the items that count for potion hinting hints
regionCountHintableItems = ItemPool.AllKongMoves()
regionCountHintableItems.extend(ItemPool.JunkSharedMoves)
if spoiler.settings.training_barrels != TrainingBarrels.normal:
regionCountHintableItems.extend(ItemPool.TrainingBarrelAbilities())
if spoiler.settings.climbing_status != ClimbingStatus.normal:
regionCountHintableItems.extend(ItemPool.ClimbingAbilities())
if spoiler.settings.shockwave_status != ShockwaveStatus.shuffled_decoupled and spoiler.settings.shockwave_status != ShockwaveStatus.start_with:
regionCountHintableItems.append(Items.CameraAndShockwave)
if spoiler.settings.shockwave_status == ShockwaveStatus.shuffled_decoupled and spoiler.settings.shockwave_status != ShockwaveStatus.start_with:
regionCountHintableItems.append(Items.Shockwave)
regionCountHintableItems.append(Items.Camera)
# These regions never have anything useful or are otherwise accounted for in the hints and shouldn't be hinted
neverHintableNames = {
HintRegion.GameStart,
HintRegion.KRool,
HintRegion.Error,
HintRegion.Credits,
HintRegion.Jetpac,
}
nonHintableNames = {HintRegion.GameStart, HintRegion.KRool, HintRegion.Error, HintRegion.Credits, HintRegion.Jetpac}
if not IsItemSelected(spoiler.settings.cb_rando_enabled, spoiler.settings.cb_rando_list_selected, Levels.DKIsles):
# Disable hinting this if CBs aren't in Isles. Obviously Isles CBs would be foolish if there's no CBs to get
nonHintableNames.add(HintRegion.IslesCBs)
spoiler.region_hintable_count = {}
bossLocations = [location for id, location in spoiler.LocationList.items() if location.type == Types.Key]
# In order for a region to be foolish, it can contain none of these Major Items
for id, region in spoiler.RegionList.items():
locations = [spoiler.LocationList[loc.id] for loc in region.locations if loc.id in spoiler.LocationList.keys() and not loc.isAuxiliaryLocation]
# If this region's valid locations (exclude starting moves) DO contain a major item, add it the name to the set of non-hintable hint regions
if any([loc for loc in locations if loc.type not in (Types.TrainingBarrel, Types.PreGivenMove, Types.Climbing) and loc.item in MajorItems]):
nonHintableNames.add(region.hint_name)
# In addition to being empty, medal regions need the corresponding boss location to be empty to be hinted foolish - this lets us say "CBs are foolish" which is more helpful
elif region.isMedalRegion() and region.level not in (Levels.DKIsles, Levels.HideoutHelm):
bossLocation = [location for location in bossLocations if location.level == region.level][0] # Matches only one
if bossLocation.item in MajorItems:
nonHintableNames.add(region.hint_name)
# Ban shops from region count hinting. These are significantly worse regions to hint than any others.
if not region.isShopRegion() and region.hint_name not in neverHintableNames:
# Count the number of region count hintable items in the region (again, ignore training moves)
regionItemCount = sum(1 for loc in locations if loc.type not in (Types.TrainingBarrel, Types.PreGivenMove, Types.Climbing) and loc.item in regionCountHintableItems)
if regionItemCount > 0:
# If we need to create a new entry due to this region, do so
if region.hint_name not in spoiler.region_hintable_count.keys():
spoiler.region_hintable_count[region.hint_name] = 0
# Keep a running tally of found vials in each region
spoiler.region_hintable_count[region.hint_name] += regionItemCount
# The regions that are foolish are all regions not in this list (that have locations in them!)
spoiler.foolish_region_names = list(set([region.hint_name for id, region in spoiler.RegionList.items() if any(region.locations) and region.hint_name not in nonHintableNames]))
# Determine non-path items (foolish v2)
# Non-path items are items that are not on the path to anything. This is similar but different to a foolish hint, so the phrasing on the hint will be different.
wothItems = [spoiler.LocationList[loc].item for loc in WothLocations]
# First we need to determine what Major Items are interesting - this is basically just all Kong moves
spoiler.pathless_moves = []
shuffledPotionItems = set(ItemPool.AllKongMoves())
if spoiler.settings.training_barrels != TrainingBarrels.normal: # If the training barrels aren't shuffled, they don't end up in the WotH so watch out
shuffledPotionItems.update(ItemPool.TrainingBarrelAbilities())
if spoiler.settings.climbing_status != ClimbingStatus.normal:
shuffledPotionItems.update(ItemPool.ClimbingAbilities())
if spoiler.settings.shockwave_status not in (ShockwaveStatus.start_with, ShockwaveStatus.shuffled_decoupled):
shuffledPotionItems.add(Items.CameraAndShockwave)
elif spoiler.settings.shockwave_status == ShockwaveStatus.shuffled_decoupled:
shuffledPotionItems.add(Items.Shockwave)
shuffledPotionItems.add(Items.Camera)
# Some items aren't WotH but are frequently a part of either/or scenarios. The paths to these items should also be considered by "pathless" hints.
interesting_non_woth_items = [Items.Bean, Items.Pearl, Items.NintendoCoin, Items.RarewareCoin]
# If you start with a slam and have 0 WotH slams OR you don't start with a slam and have 0-1 WotH slams
if (spoiler.settings.start_with_slam and Items.ProgressiveSlam not in wothItems) or (not spoiler.settings.start_with_slam and wothItems.count(Items.ProgressiveSlam) <= 1):
# That means two slams are unhintable and we must account for the paths to the unhinted slams
interesting_non_woth_items.append(Items.ProgressiveSlam)
# With lava water, 3rd melon is very often required but falls into the same pitfalls as progressive slams
if IsItemSelected(spoiler.settings.hard_mode, spoiler.settings.hard_mode_selected, HardModeSelected.water_is_lava, False):
interesting_non_woth_items.append(Items.ProgressiveInstrumentUpgrade)
# Note down all the items on these interesting non-WotH paths
items_on_interesting_non_woth_paths = set()
for path_location in spoiler.other_paths.keys():
# If this path is to an interesting non-WotH item, note down every item on this path
if spoiler.LocationList[path_location].item in interesting_non_woth_items:
items_on_interesting_non_woth_paths.update(set([spoiler.LocationList[loc].item for loc in spoiler.other_paths[path_location]]))
for item in shuffledPotionItems:
# If this item is in the WotH, it can't possibly be foolish
if item in wothItems:
continue
# If this item is on an interesting non-WotH path, it is treated as not pathless
elif item in items_on_interesting_non_woth_paths:
continue
spoiler.pathless_moves.append(item)
# Saying slams aren't on the path to anything is usually utterly useless due to the progressive nature. I'm not even gonna try to pretend to make these work.
while Items.ProgressiveSlam in spoiler.pathless_moves:
spoiler.pathless_moves.remove(Items.ProgressiveSlam)
# Similarly, progressive instrument upgrades are also a nightmare for pathless - BEGONE
if IsItemSelected(spoiler.settings.hard_mode, spoiler.settings.hard_mode_selected, HardModeSelected.water_is_lava, False):
while Items.ProgressiveInstrumentUpgrade in spoiler.pathless_moves:
spoiler.pathless_moves.remove(Items.ProgressiveInstrumentUpgrade)
def RandomFill(spoiler: Spoiler, itemsToPlace: List[Items], inOrder: bool = False) -> int:
"""Randomly place given items in any location disregarding logic."""
settings = spoiler.settings
if not inOrder:
spoiler.settings.random.shuffle(itemsToPlace)
# Get all remaining empty locations
empty = []
for id, location in spoiler.LocationList.items():
if location.item is None:
empty.append(id)
# Place item in random locations
while len(itemsToPlace) > 0:
item = itemsToPlace.pop()
validLocations = settings.GetValidLocationsForItem(item)
itemEmpty = [x for x in empty if x in validLocations and spoiler.LocationList[x].item is None and not spoiler.LocationList[x].inaccessible]
if len(itemEmpty) == 0:
if settings.extreme_debugging:
# Debugging variables: they are unaccessed but certainly useful. Do not touch!
invalid_empty_reachable = [x for x in itemEmpty if x not in validLocations]
empty_locations = [x for x in spoiler.LocationList.values() if x.item is None]
accessible_empty_locations = [x for x in empty_locations if not x.inaccessible]
noitem_locations = [x for x in spoiler.LocationList.values() if x.type != Types.Shop and x.item is Items.NoItem]
return len(itemsToPlace) + 1
spoiler.settings.random.shuffle(itemEmpty)
locationId = itemEmpty.pop()
spoiler.LocationList[locationId].PlaceItem(spoiler, item)
empty.remove(locationId)
if locationId in SharedShopLocations:
settings.placed_shared_shops += 1
if settings.placed_shared_shops >= settings.max_shared_shops:
BanAllRemainingSharedShops(spoiler)
# Have to recalculate empty after filling additional locations
empty = []
for id, location in spoiler.LocationList.items():
if location.item is None:
empty.append(id)
return 0
def CarefulRandomFill(spoiler: Spoiler, itemsToPlace: List[Union[Any, Items]], ownedItems: Optional[List[Union[Any, Items]]] = None) -> int:
"""Randomly place items, but try to keep shops in mind. Expected to be faster than forward fill for large quantities of items but slower than random fill."""
spoiler.Reset()
settings = spoiler.settings
# This method assumes you know that whatever you're trying to place can be placed nearly entirely randomly.
owned = ownedItems.copy()
# That means you can assume you own all the things you're about to place with no consequence
owned.extend(itemsToPlace)
reachable = GetAccessibleLocations(spoiler, owned, SearchMode.GetReachableForFilling)
# Place items randomly in the accessible locations
spoiler.settings.random.shuffle(itemsToPlace)
while len(itemsToPlace) > 0:
item = itemsToPlace.pop()
validLocations = settings.GetValidLocationsForItem(item)
itemEmpty = [x for x in reachable if x in validLocations and spoiler.LocationList[x].item is None and not spoiler.LocationList[x].inaccessible]
if len(itemEmpty) == 0:
if settings.extreme_debugging:
# Debugging variables: they are unaccessed but certainly useful. Do not touch!
invalid_empty_reachable = [x for x in itemEmpty if x not in validLocations]
empty_locations = [x for x in spoiler.LocationList.values() if x.item is None]
accessible_empty_locations = [x for x in empty_locations if not x.inaccessible]
noitem_locations = [x for x in spoiler.LocationList.values() if x.type != Types.Shop and x.item is Items.NoItem]
return len(itemsToPlace) + 1
spoiler.settings.random.shuffle(itemEmpty)
spoiler.settings.random.shuffle(itemEmpty)
locationId = itemEmpty.pop()
spoiler.LocationList[locationId].PlaceItem(spoiler, item)
# If you hit a shop location, we have to do some stuff
if spoiler.LocationList[locationId].type == Types.Shop:
# Recalculate assumed items for what we've placed so far
spoiler.Reset()
owned = ownedItems.copy()
owned.extend(itemsToPlace)
# In higher price settings, we have to be extra careful with putting things in shops - adding a new item to a shop can disturb a fragile required purchase order
if settings.random_prices in (RandomPrices.high, RandomPrices.extreme):
# Check if we can still 101% the seed - this is costly to do for every shop location, but should improve the consistency of seed generation for higher-priced settings
reached_all = GetAccessibleLocations(spoiler, owned, SearchMode.CheckAllReachable)
if not reached_all:
# If we can't, we've established that this location cannot hold any items without causing a coin logic break
# This does not preclude the possibility of a coin logic failure later, just heavily reduces the odds
# This will, however, prevent failed 101% checks because of purchase orders
spoiler.LocationList[locationId].UnplaceItem(spoiler)
spoiler.LocationList[locationId].inaccessible = True
spoiler.LocationList[locationId].tooExpensiveInaccessible = True
itemsToPlace.append(item)
continue
# Shared shops have to respect the shared shop limit
if locationId in SharedShopLocations:
settings.placed_shared_shops += 1
if settings.placed_shared_shops >= settings.max_shared_shops:
BanAllRemainingSharedShops(spoiler)
spoiler.Reset()
reachable = GetAccessibleLocations(spoiler, owned, SearchMode.GetReachableForFilling)
if settings.extreme_debugging:
spoiler.Reset()
reached_all = GetAccessibleLocations(spoiler, owned, SearchMode.CheckAllReachable)
if not reached_all:
print("red alert - this item placement lost 101% somehow?")
# Debugging variables
item_placed_before_this_one = item
location_placed_before_this_one = spoiler.LocationList[locationId]
return 0
def ForwardFill(
spoiler: Spoiler,
itemsToPlace: List[Items],
ownedItems: Optional[List[Items]] = None,
inOrder: bool = False,
doubleTime: bool = False,
) -> int:
"""Forward fill algorithm for item placement."""
settings = spoiler.settings
if ownedItems is None:
ownedItems = []
if not inOrder:
spoiler.settings.random.shuffle(itemsToPlace)
needToRefreshReachable = True
# While there are items to place
while len(itemsToPlace) > 0:
# Get a random item
item = itemsToPlace.pop(0)
# In "doubleTime", only refresh the list of reachable locations every other item to reduce calls to this method - this should have minimal impact on randomization depending on the item filling here
if not doubleTime or needToRefreshReachable:
# Find a random empty location which is reachable with current items
spoiler.Reset()
reachable = GetAccessibleLocations(spoiler, ownedItems.copy(), SearchMode.GetReachableForFilling)
validLocations = settings.GetValidLocationsForItem(item)
validReachable = [x for x in reachable if spoiler.LocationList[x].item is None and x in validLocations]
if len(validReachable) == 0: # If there are no empty reachable locations, reached a dead end
if settings.extreme_debugging:
invalid_empty_reachable = [x for x in reachable if spoiler.LocationList[x].item is None and x not in validLocations]
valid_empty = [x for x in spoiler.LocationList.keys() if spoiler.LocationList[x].item is None and x in validLocations]
return len(itemsToPlace) + 1
spoiler.settings.random.shuffle(validReachable)
locationId = validReachable.pop()
# Place the item
spoiler.LocationList[locationId].PlaceItem(spoiler, item)
# Debug code utility for very important items
if item in ItemPool.HighPriorityItems(settings):
settings.debug_fill[spoiler.LocationList[locationId].name] = item
if item in ItemPool.Keys():
settings.debug_fill[spoiler.LocationList[locationId].name] = item
needToRefreshReachable = not needToRefreshReachable # Alternate this variable every item for doubleTime
# If you hit a shop location, we have to do some stuff
if spoiler.LocationList[locationId].type == Types.Shop:
# In higher price settings, we have to be extra careful with putting things in shops - adding a new item to a shop can disturb a fragile required purchase order
if settings.random_prices in (RandomPrices.high, RandomPrices.extreme):
# Check if we can still 101% the seed - this is costly to do for every shop location, but should improve the consistency of seed generation for higher-priced settings
assumedItems = ownedItems.copy()
assumedItems.extend(itemsToPlace)
reached_all = GetAccessibleLocations(spoiler, assumedItems, SearchMode.CheckAllReachable)
if not reached_all:
# If we can't, we've established that this location cannot hold any items without causing a coin logic break
# This does not preclude the possibility of a coin logic failure later, just heavily reduces the odds
# This will, however, prevent failed 101% checks because of purchase orders
spoiler.LocationList[locationId].UnplaceItem(spoiler)
spoiler.LocationList[locationId].inaccessible = True
spoiler.LocationList[locationId].tooExpensiveInaccessible = True
itemsToPlace.append(item)
continue
# Shared shops must abide by the shared shop limit
if locationId in SharedShopLocations:
settings.placed_shared_shops += 1
if settings.placed_shared_shops >= settings.max_shared_shops:
BanAllRemainingSharedShops(spoiler)
needToRefreshReachable = True
if settings.extreme_debugging:
spoiler.Reset()
assumedItems = ownedItems.copy()
assumedItems.extend(itemsToPlace)
reached_all = GetAccessibleLocations(spoiler, assumedItems, SearchMode.CheckAllReachable)
if not reached_all:
print("red alert - this item placement lost 101% somehow?")
item_placed_before_this_one = item
location_placed_before_this_one = spoiler.LocationList[locationId]
return 0
def GetItemValidLocations(spoiler: Spoiler, validLocations, item):
"""Get the list of valid locations for this item."""
# If validLocations is a dictionary, check for this item's value
itemValidLocations = validLocations
if isinstance(validLocations, dict):
for itemKey in validLocations.keys():
if item == itemKey:
itemValidLocations = validLocations[itemKey]
break
# Valid locations entry wasn't found
itemValidLocations = list(spoiler.LocationList)
return itemValidLocations
def AssumedFill(spoiler: Spoiler, itemsToPlace: List[Items], ownedItems: Optional[List[Items]] = None, inOrder: bool = False) -> int:
"""Assumed fill algorithm for item placement."""
settings = spoiler.settings
if ownedItems is None:
ownedItems = []
# While there are items to place
if not inOrder:
spoiler.settings.random.shuffle(itemsToPlace)
while len(itemsToPlace) > 0:
# Get a random item, check which empty locations are still accessible without owning it
item = itemsToPlace.pop(0)
itemShuffled = False
owned = itemsToPlace.copy()
owned.extend(ownedItems)
itemValidLocations = settings.GetValidLocationsForItem(item)
# Find all valid reachable locations for this item
spoiler.Reset()
reachable = GetAccessibleLocations(spoiler, owned, SearchMode.GetReachableForFilling)
validReachable = [x for x in reachable if spoiler.LocationList[x].item is None and x in itemValidLocations]
# If there are no empty reachable locations, reached a dead end
if len(validReachable) == 0:
print("Failed placing item " + ItemList[item].name + ", no valid reachable locations without this item.")
currentKongsFreed = [ItemList[x].name for x in owned if ItemList[x].type == Types.Kong]
startKongList = []
for x in settings.starting_kong_list:
startKongList.append(x.name.capitalize())
for i, kong in enumerate(startKongList):
currentKongsFreed.insert(i, kong)
currentMovesOwned = [ItemList[x].name for x in owned if ItemList[x].type in (Types.Shop, Types.TrainingBarrel, Types.Shockwave, Types.Climbing)]
currentGbCount = len([x for x in owned if ItemList[x].type == Types.Banana])
js.postMessage("Current Moves owned at failure: " + str(currentMovesOwned) + " with GB count: " + str(currentGbCount) + " and kongs freed: " + str(currentKongsFreed))
return len(itemsToPlace) + 1
spoiler.settings.random.shuffle(validReachable)
# Get a random, empty, reachable location
for locationId in validReachable:
# Atempt to place the item here
spoiler.LocationList[locationId].PlaceItem(spoiler, item)
if len(itemsToPlace) > 0:
# If we have more items to placed, check valid reachable after placing to see if placing it here causes problems
# Need to re-assign owned items since the search adds a bunch of extras
owned = itemsToPlace.copy()
owned.extend(ownedItems)
spoiler.Reset()
reachable = GetAccessibleLocations(spoiler, owned, SearchMode.GetReachableForFilling)
valid = True
# For each remaining item, ensure that it has a valid location reachable after placing this item
for checkItem in itemsToPlace:
itemValid = settings.GetValidLocationsForItem(checkItem)
validReachable = [x for x in reachable if x in itemValid and x != locationId]
if len(validReachable) == 0:
js.postMessage("Failed placing item " + ItemList[item].name + " in location " + spoiler.LocationList[locationId].name + ", due to too few remaining locations in play")
valid = False
break
reachable.remove(validReachable[0]) # Remove one so same location can't be "used" twice
# If world is not valid, undo item placement and try next location
if not valid:
spoiler.LocationList[locationId].UnplaceItem(spoiler)
itemShuffled = False
continue
# Debug code utility for very important items
if item in ItemPool.HighPriorityItems(settings):
settings.debug_fill[spoiler.LocationList[locationId].name] = item
if item in ItemPool.Keys():
settings.debug_fill[spoiler.LocationList[locationId].name] = item
itemShuffled = True
if locationId in SharedShopLocations:
settings.placed_shared_shops += 1
if settings.placed_shared_shops >= settings.max_shared_shops:
BanAllRemainingSharedShops(spoiler)
break
if not itemShuffled:
js.postMessage("Failed placing item " + ItemList[item].name + " in any of remaining " + str(ItemList[item].type) + " type possible locations")
return len(itemsToPlace) + 1
elif settings.extreme_debugging:
spoiler.Reset()
reached_all = GetAccessibleLocations(spoiler, owned, SearchMode.CheckAllReachable)
if not reached_all:
print("red alert - this item placement lost 101% somehow?")
item_placed_before_this_one = item
return 0
def BanAllRemainingSharedShops(spoiler: Spoiler):
"""Fill all empty shared shops with a NoItem."""
for location in SharedShopLocations:
if not spoiler.LocationList[location].inaccessible and spoiler.LocationList[location].item is None:
spoiler.LocationList[location].PlaceItem(spoiler, Items.NoItem)
def GetMaxCoinsSpent(spoiler: Spoiler, purchasedShops: List[Union[Any, Locations]]) -> List[int]:
"""Calculate the max number of coins each kong could have spent given the ownedItems and the price settings."""
settings = spoiler.settings
MaxCoinsSpent = [0, 0, 0, 0, 0, 0]
slamLevel = 0
ammoBelts = 0
instUpgrades = 0
for location_id in purchasedShops:
location = spoiler.LocationList[location_id]
if location.item == Items.ProgressiveSlam:
movePrice = settings.prices[location.item][slamLevel]
slamLevel += 1
elif location.item == Items.ProgressiveAmmoBelt:
movePrice = settings.prices[location.item][ammoBelts]
ammoBelts += 1
elif location.item == Items.ProgressiveInstrumentUpgrade:
movePrice = settings.prices[location.item][instUpgrades]
instUpgrades += 1
elif settings.random_prices == RandomPrices.vanilla:
movePrice = settings.prices[location.item]
else:
movePrice = settings.prices[location_id]
if movePrice is not None:
MaxCoinsSpent[location.kong] += movePrice
# All shared moves add to the cost of each Kong
for kong_index in range(5):
MaxCoinsSpent[kong_index] += MaxCoinsSpent[int(Kongs.any)]
MaxCoinsSpent.pop() # Remove the shared total, as it was just for numbers keeping
return MaxCoinsSpent
# @pp.profile_by_line()
def GetUnplacedItemPrerequisites(spoiler: Spoiler, targetItemId, placedMoves, ownedKongs=[]):
"""Given the target item and the current world state, find a valid, minimal, unplaced set of items required to reach the location it is in."""
# Settings-required moves are always owned in order to complete this method based on the settings
settingsRequiredMoves = ItemPool.AllItemsForMovePlacement(spoiler.settings)
# The most likely case - if no moves are needed, get out of here quickly
spoiler.Reset()
if GetAccessibleLocations(spoiler, settingsRequiredMoves.copy(), SearchMode.CheckSpecificItemReachable, targetItemId=targetItemId):
return []
requiredMoves = []
# Some locations can be accessed by multiple items, so we'll shuffle the order we check the items to randomly pick one of them first
# We should have just placed this item, so it should be available with the provided list of owned kongs
# We don't want to find requirements for Kongs we don't own, as we shouldn't need them
# e.g. You own DK, Diddy, and Tiny but want to find the prerequisites for an item found in the Llama temple
# You intentionally only look at DK/Diddy/Tiny moves so you don't find Grape as a prerequisite because you don't have Lanky
# In this example (with no other shuffles), there are two possible return values depending on the shuffle order.
# Either [Items.Guitar, Items.Coconut] OR [Items.Guitar, Items.Feather]
moveList = [move for move in ItemPool.AllKongMoves()] # I really want to pare this down quickly with ownedKongs, but it hurts the fill to do so
# Sometimes a move requires camera or shockwave as a prerequisite
if spoiler.settings.shockwave_status != ShockwaveStatus.vanilla:
if spoiler.settings.shockwave_status == ShockwaveStatus.shuffled_decoupled:
moveList.append(Items.Camera)
moveList.append(Items.Shockwave)
else:
moveList.append(Items.CameraAndShockwave)
# Often moves require training barrels as prerequisites
if spoiler.settings.training_barrels != TrainingBarrels.normal:
moveList.extend(ItemPool.TrainingBarrelAbilities())
if spoiler.settings.climbing_status != ClimbingStatus.normal:
moveList.extend(ItemPool.ClimbingAbilities())
# We only want *unplaced* prerequisites, cull all placed moves from the move list
for move in placedMoves:
if move in moveList:
moveList.remove(move)
spoiler.settings.random.shuffle(moveList)
itemWasFound = False
# Every item in moveList could be a required item
for i in range(0, len(moveList)):
# Remove one item from the moveList
possiblyUnnecessaryItem = moveList[i]
moveList[i] = Items.NoItem
# Check if the target is still accessible without this item
spoiler.Reset()
if not GetAccessibleLocations(
spoiler,
settingsRequiredMoves.copy() + moveList.copy(),
SearchMode.CheckSpecificItemReachable,
targetItemId=targetItemId,
):
# If it's no longer accessible, then this item is required
requiredMoves.append(possiblyUnnecessaryItem)
# Restore the item to the move list ONLY if it's required - this will cover either/or items
moveList[i] = possiblyUnnecessaryItem
else:
itemWasFound = True
# If we didn't find a required move, the item was placed improperly somehow (this shouldn't happen)
if not itemWasFound:
# DEBUG CODE - This helps find where items are being placed
mysteryLocation = None
itemobj = ItemList[targetItemId]
if type(spoiler.settings.valid_locations[itemobj.type]) is dict:
for possibleLocationThisItemGotPlaced in spoiler.settings.valid_locations[itemobj.type][itemobj.kong]:
if spoiler.LocationList[possibleLocationThisItemGotPlaced].item == targetItemId:
mysteryLocation = spoiler.LocationList[possibleLocationThisItemGotPlaced]
break
else:
for possibleLocationThisItemGotPlaced in spoiler.settings.valid_locations[itemobj.type]:
if spoiler.LocationList[possibleLocationThisItemGotPlaced].item == targetItemId:
mysteryLocation = spoiler.LocationList[possibleLocationThisItemGotPlaced]
break
if mysteryLocation is None:
raise Ex.ItemPlacementException("Target item not placed??")
# debug_reachable = GetAccessibleLocations(spoiler, settingsRequiredMoves.copy() + moveList.copy(), SearchMode.GetReachable)
print("Item placed in an inaccessible location: " + str(mysteryLocation.name))
raise Ex.ItemPlacementException("Item placed in an inaccessible location: " + str(mysteryLocation.name))
spoiler.settings.debug_prerequisites[targetItemId] = requiredMoves
return requiredMoves
def PlaceItems(
spoiler: Spoiler,
algorithm: FillAlgorithm,
itemsToPlace: List[Union[Any, Items]],
ownedItems: Optional[List[Union[Any, Items]]] = None,
inOrder: bool = False,
doubleTime: bool = False,
) -> int:
"""Places items using given algorithm."""
if ownedItems is None:
ownedItems = []
# Always use random fill with no logic
if spoiler.settings.logic_type == LogicType.nologic:
algorithm = FillAlgorithm.random
if algorithm == FillAlgorithm.assumed:
return AssumedFill(spoiler, itemsToPlace, ownedItems, inOrder)
elif algorithm == FillAlgorithm.forward:
return ForwardFill(spoiler, itemsToPlace, ownedItems, inOrder, doubleTime)
elif algorithm == FillAlgorithm.random:
return RandomFill(spoiler, itemsToPlace, inOrder)
elif algorithm == FillAlgorithm.careful_random:
return CarefulRandomFill(spoiler, itemsToPlace, ownedItems)
def FillShuffledKeys(spoiler: Spoiler, placed_types: List[Types], placed_items: List[Items]) -> None:
"""Fill Keys in shuffled locations based on the settings."""
keysToPlace = ItemPool.KeysToPlace(spoiler.settings)
# Don't double-place keys
for item in placed_items:
if item in keysToPlace:
keysToPlace.remove(item)
# Level-agnostic key placement settings include...
# - No logic (totally random)
# - Loading Zone randomizer (key unlocks are typically of lesser importance)
# - Complex level progression (key order is non-linear)
if spoiler.settings.logic_type == LogicType.nologic or spoiler.settings.shuffle_loading_zones == ShuffleLoadingZones.all or spoiler.settings.hard_level_progression:
# Assumed fills tend to place multiple keys at once better
keyAlgorithm = FillAlgorithm.assumed
if spoiler.settings.logic_type == LogicType.nologic: # Obviously no logic gets random fills
keyAlgorithm = FillAlgorithm.random
# Place all the keys
keysUnplaced = PlaceItems(spoiler, keyAlgorithm, keysToPlace, ItemPool.GetItemsNeedingToBeAssumed(spoiler.settings, placed_types))
if keysUnplaced > 0:
raise Ex.ItemPlacementException(str(keysUnplaced) + " unplaced keys.")
# # Simple linear level order progression leads to straightforward key placement
else:
# If Helm is not last, and we're locking key 8 and we're using the SLO ruleset,
# place Key 8 in the 8th level somewhere
if Items.HideoutHelmKey in keysToPlace and spoiler.settings.key_8_helm:
last_level = spoiler.settings.level_order[8]
if last_level != Levels.HideoutHelm:
if spoiler.settings.shuffle_items:
potential_locations = [
loc
for loc in spoiler.LocationList
if spoiler.LocationList[loc].level == last_level
and spoiler.LocationList[loc].type in spoiler.settings.shuffled_location_types
and not spoiler.LocationList[loc].inaccessible
and loc in spoiler.settings.GetValidLocationsForItem(Items.HideoutHelmKey)
]
# Outside of item rando, the only eligible location is the boss in level 8. This should filter down to only one location.
else:
potential_locations = [loc for loc in spoiler.LocationList if spoiler.LocationList[loc].level == last_level and spoiler.LocationList[loc].type == Types.Key]
selected_location = spoiler.settings.random.choice(potential_locations)
spoiler.LocationList[selected_location].PlaceItem(spoiler, Items.HideoutHelmKey)
keysToPlace.remove(Items.HideoutHelmKey)
# Place the keys in order
keysToPlace.sort()
keysUnplaced = PlaceItems(
spoiler,
spoiler.settings.algorithm,
keysToPlace,
ItemPool.GetItemsNeedingToBeAssumed(spoiler.settings, placed_types),
inOrder=True,
)
if keysUnplaced > 0:
raise Ex.ItemPlacementException(str(keysUnplaced) + " unplaced keys.")
def FillHelmLocations(spoiler: Spoiler, placed_types: List[Types], placed_items: List[Items]) -> List[Items]:
"""Fill all currently empty (non-enemy!) Helm locations with eligible unplaced items."""
placed_in_helm = []
# Get all the empty Helm locations
empty_helm_locations = [
loc_id
for loc_id in spoiler.LocationList.keys()
if spoiler.LocationList[loc_id].level == Levels.HideoutHelm and spoiler.LocationList[loc_id].type not in (Types.Constant, Types.Enemies) and spoiler.LocationList[loc_id].item is None
]
# Make sure hints don't get placed, if progressive hints are enabled
if spoiler.settings.progressive_hint_item != ProgressiveHintItem.off:
placed_types.append(Types.Hint)
# Rig the valid_locations for all relevant items to only be able to place things in Helm
for typ in [x for x in spoiler.settings.shuffled_location_types if x not in placed_types]: # Shops would already be placed
# Filter the valid locations down to only Helm locations
# Blueprints are tricky - their valid locations are organized by Kong
if typ == Types.Blueprint:
for kong in GetKongs():
spoiler.settings.valid_locations[Types.Blueprint][kong] = [
loc for loc in spoiler.settings.valid_locations[Types.Blueprint][kong] if spoiler.LocationList[loc].level == Levels.HideoutHelm and loc in empty_helm_locations
]
# Everything else can be in any Helm location they already could have been in depending on their type
elif typ in spoiler.settings.valid_locations.keys():
spoiler.settings.valid_locations[typ] = [loc for loc in spoiler.settings.valid_locations[typ] if spoiler.LocationList[loc].level == Levels.HideoutHelm and loc in empty_helm_locations]
# Anything that falls out of this else is a type that doesn't have valid locations (ToughBanana, etc.)
# Now we get the full list of items we could place here
unplaced_items = ItemPool.GetItemsNeedingToBeAssumed(spoiler.settings, placed_types)
for item in placed_items:
if item in unplaced_items:
unplaced_items.remove(item)
debug_failed_to_place_items = []
possible_items = [item for item in unplaced_items if item != Items.GoldenBanana] # To save some time, we know GBs can't be in Helm
spoiler.settings.random.shuffle(possible_items)
# Until we have placed enough items...
while len(placed_in_helm) < len(empty_helm_locations):
if len(possible_items) == 0:
spoiler.settings.update_valid_locations(spoiler)
raise Ex.FillException("Unable to fill Helm.")
# Grab the next one from the pile and attempt to place it
item_to_attempt_placement = possible_items.pop()
unplaced_items.remove(item_to_attempt_placement)
spoiler.Reset()
unplaced = PlaceItems(spoiler, FillAlgorithm.forward, [item_to_attempt_placement], unplaced_items)
# If we succeed, mark this item as being placed in Helm
if unplaced == 0:
placed_in_helm.append(item_to_attempt_placement)
# If we failed, go again. This is most likely either a location conflict inherent to the item or a logical restriction based on a huge Fairy/Medal requirement
else:
debug_failed_to_place_items.append(item_to_attempt_placement) # This item we failed to place could be important earlier, so we need to assume it going forward
unplaced_items.append(item_to_attempt_placement)
# Very important - we have to reset valid_locations to the correct state after this
spoiler.settings.update_valid_locations(spoiler)
# Return all items we placed, all future methods must consider these when placing (and assuming) items
return placed_in_helm
def FillBossLocations(spoiler: Spoiler, placed_types: List[Types], placed_items: List[Items]) -> List[Items]:
"""Fill all currently empty Boss locations with eligible unplaced items."""
placed_on_bosses = []
# Get all the empty boss locations
empty_boss_locations = [
loc_id
for loc_id in spoiler.LocationList.keys()
if spoiler.LocationList[loc_id].level != Levels.HideoutHelm and spoiler.LocationList[loc_id].type == Types.Key and spoiler.LocationList[loc_id].item is None
]
# Make sure hints don't get placed, if progressive hints are enabled
if spoiler.settings.progressive_hint_item != ProgressiveHintItem.off:
placed_types.append(Types.Hint)
# Rig the valid_locations for all relevant items to only be able to place things on bosses
for typ in [x for x in spoiler.settings.shuffled_location_types if x not in placed_types]: # Shops would already be placed
# Any item eligible to be on a boss can be on any boss
spoiler.settings.valid_locations[typ] = empty_boss_locations
# Now we get the full list of items we could place here
unplaced_items = ItemPool.GetItemsNeedingToBeAssumed(spoiler.settings, placed_types)
# Checkless can be on bosses, but we need shops in the pool in order to have room to do this reliably
if Types.Shop in spoiler.settings.shuffled_location_types and Types.FakeItem in spoiler.settings.shuffled_location_types:
unplaced_items.extend(ItemPool.FakeItems(spoiler.settings))
for item in placed_items:
if item in unplaced_items:
unplaced_items.remove(item)
debug_failed_to_place_items = []
possible_items = [item for item in unplaced_items if item < Items.JungleJapesDonkeyBlueprint or item > Items.DKIslesChunkyBlueprint] # To save some time, we know blueprints can't be on bosses
spoiler.settings.random.shuffle(possible_items)
# Until we have placed enough items...
while len(placed_on_bosses) < len(empty_boss_locations):
if len(possible_items) == 0:
spoiler.settings.update_valid_locations(spoiler)
raise Ex.FillException("Unable to find all locations during the fill. Error code: BS-1")
# Grab the next one from the pile and attempt to place it
item_to_attempt_placement = possible_items.pop()
unplaced_items.remove(item_to_attempt_placement)
spoiler.Reset()
unplaced = PlaceItems(spoiler, FillAlgorithm.forward, [item_to_attempt_placement], unplaced_items)
# If we succeed, mark this item as being placed in Helm
if unplaced == 0:
placed_on_bosses.append(item_to_attempt_placement)
# If we failed, go again. This would be really surprising to ever happen, as boss accessibility is only calculated post-fill. Maybe in plando?
else:
debug_failed_to_place_items.append(item_to_attempt_placement) # Apparently the item we failed to place is important earlier, so we need to assume it going forward
unplaced_items.append(item_to_attempt_placement)
# Very important - we have to reset valid_locations to the correct state after this
spoiler.settings.update_valid_locations(spoiler)
# Return all items we placed, all future methods must consider these when placing (and assuming) items
return placed_on_bosses
def Fill(spoiler: Spoiler) -> None:
"""Fully randomizes and places all items."""
placed_types = []
spoiler.settings.debug_fill = {}
spoiler.settings.debug_prerequisites = {}
spoiler.settings.debug_fill_blueprints = {}
spoiler.settings.placed_shared_shops = 0
# First place constant items - these will never vary and need to be in place for all other fills to know that
ItemPool.PlaceConstants(spoiler)
preplaced_items = spoiler.settings.plandomizer_items_placed
# Place rainbow coins before all randomly placed items so that we have coins set in stone for the other fills
# It's possible that shops could overload if we continue assuming rainbow coins for too many fills
if Types.RainbowCoin in spoiler.settings.shuffled_location_types:
placed_types.append(Types.RainbowCoin)
spoiler.Reset()
rainbowCoinsToPlace = ItemPool.RainbowCoinItems().copy()
for item in preplaced_items:
if item in rainbowCoinsToPlace:
rainbowCoinsToPlace.remove(item)
rcoinUnplaced = PlaceItems(
spoiler,
FillAlgorithm.random,
rainbowCoinsToPlace,
ItemPool.GetItemsNeedingToBeAssumed(spoiler.settings, placed_types, placed_items=preplaced_items),
)
if rcoinUnplaced > 0:
raise Ex.ItemPlacementException("Unable to find all locations during the fill. Error code: RC-" + str(rcoinUnplaced))
if spoiler.settings.extreme_debugging:
DebugCheckAllReachable(
spoiler,
ItemPool.GetItemsNeedingToBeAssumed(spoiler.settings, placed_types, placed_items=preplaced_items),
"Rainbow Coins",
)
if spoiler.settings.shuffle_items and Types.Shop in spoiler.settings.shuffled_location_types:
if spoiler.settings.kong_rando:
FillKongs(spoiler, placed_types, preplaced_items)
if spoiler.settings.extreme_debugging:
DebugCheckAllReachable(
spoiler,
ItemPool.GetItemsNeedingToBeAssumed(spoiler.settings, placed_types, placed_items=preplaced_items),
"Kongs",
)
preplaced_items.extend([Items.Donkey, Items.Diddy, Items.Lanky, Items.Tiny, Items.Chunky])
preplaced_items.extend(FillTrainingMoves(spoiler, preplaced_items))
placed_types.append(Types.Shop)
placed_types.append(Types.TrainingBarrel)
placed_types.append(Types.Climbing)
placed_types.append(Types.Shockwave)
placed_types.append(Types.Key)
bigListOfItemsToPlace = []
if Types.Shop in spoiler.settings.shuffled_location_types:
bigListOfItemsToPlace.extend(ItemPool.ImportantSharedMoves.copy())
bigListOfItemsToPlace.extend(ItemPool.JunkSharedMoves.copy())
bigListOfItemsToPlace.extend(ItemPool.DonkeyMoves)
bigListOfItemsToPlace.extend(ItemPool.DiddyMoves)
bigListOfItemsToPlace.extend(ItemPool.LankyMoves)
bigListOfItemsToPlace.extend(ItemPool.TinyMoves)
bigListOfItemsToPlace.extend(ItemPool.ChunkyMoves)
if spoiler.settings.training_barrels != TrainingBarrels.normal:
bigListOfItemsToPlace.extend(ItemPool.TrainingBarrelAbilities())
if spoiler.settings.climbing_status != ClimbingStatus.normal:
bigListOfItemsToPlace.extend(ItemPool.ClimbingAbilities())
if spoiler.settings.shockwave_status != ShockwaveStatus.start_with and Types.Shockwave in spoiler.settings.shuffled_location_types:
bigListOfItemsToPlace.extend(ItemPool.ShockwaveTypeItems(spoiler.settings))
if Types.Key in spoiler.settings.shuffled_location_types:
bigListOfItemsToPlace.extend(ItemPool.KeysToPlace(spoiler.settings))
if Types.Cranky in spoiler.settings.shuffled_location_types:
placed_types.append(Types.Cranky)
bigListOfItemsToPlace.extend(ItemPool.CrankyItems())
if Types.Funky in spoiler.settings.shuffled_location_types:
placed_types.append(Types.Funky)
bigListOfItemsToPlace.extend(ItemPool.FunkyItems())
if Types.Candy in spoiler.settings.shuffled_location_types:
placed_types.append(Types.Candy)
bigListOfItemsToPlace.extend(ItemPool.CandyItems())
if Types.Snide in spoiler.settings.shuffled_location_types:
placed_types.append(Types.Snide)
bigListOfItemsToPlace.extend(ItemPool.SnideItems())
for item in preplaced_items:
if item in bigListOfItemsToPlace:
bigListOfItemsToPlace.remove(item)
# If Helm is not last, and we're locking key 8 and we're using the SLO ruleset, place Key 8 in the 8th level somewhere
if (
Items.HideoutHelmKey in bigListOfItemsToPlace
and spoiler.settings.key_8_helm
and spoiler.settings.shuffle_loading_zones == ShuffleLoadingZones.levels
and not spoiler.settings.hard_level_progression
):
last_level = spoiler.settings.level_order[8]
if last_level != Levels.HideoutHelm:
potential_locations = [
loc
for loc in spoiler.LocationList
if spoiler.LocationList[loc].level == last_level
and spoiler.LocationList[loc].type in spoiler.settings.shuffled_location_types
and not spoiler.LocationList[loc].inaccessible
and loc in spoiler.settings.GetValidLocationsForItem(Items.HideoutHelmKey)
]
selected_location = spoiler.settings.random.choice(potential_locations)
spoiler.LocationList[selected_location].PlaceItem(spoiler, Items.HideoutHelmKey)
bigListOfItemsToPlace.remove(Items.HideoutHelmKey)
unplaced = PlaceItems(
spoiler,
FillAlgorithm.assumed,
bigListOfItemsToPlace,
ItemPool.GetItemsNeedingToBeAssumed(spoiler.settings, placed_types, placed_items=preplaced_items),
)
if unplaced > 0:
raise Ex.ItemPlacementException("Unable to find all locations during the fill. Error code: BIG-" + str(unplaced))
if spoiler.settings.extreme_debugging:
DebugCheckAllReachable(
spoiler,
ItemPool.GetItemsNeedingToBeAssumed(spoiler.settings, placed_types, placed_items=preplaced_items),
"The Fill",
)
else:
# Now we place all logically-relevant low-quantity items
# Then fill Kongs and Moves - this should be a very early fill type for hopefully obvious reasons
FillKongsAndMoves(spoiler, placed_types, preplaced_items)
if spoiler.settings.extreme_debugging:
DebugCheckAllReachable(
spoiler,
ItemPool.GetItemsNeedingToBeAssumed(spoiler.settings, placed_types, placed_items=preplaced_items),
"all moves",
)
# Then place Keys
if Types.Key in spoiler.settings.shuffled_location_types:
placed_types.append(Types.Key)
FillShuffledKeys(spoiler, placed_types, preplaced_items)
if spoiler.settings.extreme_debugging:
DebugCheckAllReachable(
spoiler,
ItemPool.GetItemsNeedingToBeAssumed(spoiler.settings, placed_types, placed_items=preplaced_items),
"Keys",
)
# Then place misc progression items
if Types.Bean in spoiler.settings.shuffled_location_types:
placed_types.append(Types.Bean)
placed_types.append(Types.Pearl)
spoiler.Reset()
miscItemsToPlace = ItemPool.MiscItemRandoItems().copy()
for item in preplaced_items:
if item in miscItemsToPlace:
miscItemsToPlace.remove(item)
miscUnplaced = PlaceItems(
spoiler,
spoiler.settings.algorithm,
miscItemsToPlace,
ItemPool.GetItemsNeedingToBeAssumed(spoiler.settings, placed_types, placed_items=preplaced_items),
)
if miscUnplaced > 0:
raise Ex.ItemPlacementException("Unable to find all locations during the fill. Error code: MI-" + str(miscUnplaced))
if spoiler.settings.extreme_debugging:
DebugCheckAllReachable(
spoiler,
ItemPool.GetItemsNeedingToBeAssumed(spoiler.settings, placed_types, placed_items=preplaced_items),
"Miscellaneous Items",
)
# # Now we place the (generally) filler items
# # If Helm is having locations shuffled and we're shuffling GBs, we have to fill Helm now.
# # This is because GBs can't be in Helm, so we might run out of locations to place them if these spots aren't filled
# if Types.Banana in spoiler.settings.shuffled_location_types and (
# Types.Medal in spoiler.settings.shuffled_location_types
# or Types.Crown in spoiler.settings.shuffled_location_types
# or Types.Fairy in spoiler.settings.shuffled_location_types
# or Types.Key in spoiler.settings.shuffled_location_types
# ):
# preplaced_items.extend(FillHelmLocations(spoiler, placed_types.copy(), preplaced_items))
# if spoiler.settings.extreme_debugging:
# DebugCheckAllReachable(spoiler, ItemPool.GetItemsNeedingToBeAssumed(spoiler.settings, placed_types, placed_items=preplaced_items), "things in Helm")
# If keys are shuffled in the pool we want to ensure an item is on every boss
# This is to support broader settings that rely on boss kills and to enable reads on the boss fill algorithm
if Types.Key in spoiler.settings.shuffled_location_types:
preplaced_items.extend(FillBossLocations(spoiler, placed_types.copy(), preplaced_items))
if spoiler.settings.extreme_debugging:
DebugCheckAllReachable(
spoiler,
ItemPool.GetItemsNeedingToBeAssumed(spoiler.settings, placed_types, placed_items=preplaced_items),
"things on Bosses",
)
# Then place Blueprints - these are moderately restrictive in their placement
if Types.Blueprint in spoiler.settings.shuffled_location_types:
placed_types.append(Types.Blueprint)
spoiler.Reset()
blueprintsToPlace = ItemPool.Blueprints().copy()
for item in preplaced_items:
if item in blueprintsToPlace:
blueprintsToPlace.remove(item)
# Blueprints can be placed largely randomly - there's no location (yet) that can cause blueprints to lock themselves
blueprintsUnplaced = PlaceItems(
spoiler,
FillAlgorithm.careful_random,
blueprintsToPlace,
ItemPool.GetItemsNeedingToBeAssumed(spoiler.settings, placed_types, placed_items=preplaced_items),
)
if blueprintsUnplaced > 0:
raise Ex.ItemPlacementException("Unable to find all locations during the fill. Error code: BP-" + str(blueprintsUnplaced))
if spoiler.settings.extreme_debugging:
DebugCheckAllReachable(
spoiler,
ItemPool.GetItemsNeedingToBeAssumed(spoiler.settings, placed_types, placed_items=preplaced_items),
"Blueprints",
)
# Then place Nintendo & Rareware Coins
for coin in (Types.NintendoCoin, Types.RarewareCoin):
if coin in spoiler.settings.shuffled_location_types:
placed_types.append(coin)
spoiler.Reset()
coinsToPlace = ItemPool.NintendoCoinItems() if coin == Types.NintendoCoin else ItemPool.RarewareCoinItems()
for item in preplaced_items:
if item in coinsToPlace:
coinsToPlace.remove(item)
coinsUnplaced = PlaceItems(
spoiler,
spoiler.settings.algorithm,
coinsToPlace,
ItemPool.GetItemsNeedingToBeAssumed(spoiler.settings, placed_types, placed_items=preplaced_items),
)
if coinsUnplaced > 0:
raise Ex.ItemPlacementException("Unable to find all locations during the fill. Error code: CC-" + str(coinsUnplaced))
if spoiler.settings.extreme_debugging:
DebugCheckAllReachable(
spoiler,
ItemPool.GetItemsNeedingToBeAssumed(spoiler.settings, placed_types, placed_items=preplaced_items),
"Company Coins",
)
# Then place Battle Crowns
if Types.Crown in spoiler.settings.shuffled_location_types:
placed_types.append(Types.Crown)
spoiler.Reset()
crownsToPlace = ItemPool.BattleCrownItems()
for item in preplaced_items:
if item in crownsToPlace:
crownsToPlace.remove(item)
# Crowns can be placed randomly, but only if the helm doors don't need any
algo = FillAlgorithm.careful_random
if spoiler.settings.coin_door_item == BarrierItems.Crown or spoiler.settings.crown_door_item == BarrierItems.Crown:
algo = spoiler.settings.algorithm
crownsUnplaced = PlaceItems(
spoiler,
algo,
crownsToPlace,
ItemPool.GetItemsNeedingToBeAssumed(spoiler.settings, placed_types, placed_items=preplaced_items),
doubleTime=True,
)
if crownsUnplaced > 0:
raise Ex.ItemPlacementException("Unable to find all locations during the fill. Error code: CR-" + str(crownsUnplaced))
if spoiler.settings.extreme_debugging:
DebugCheckAllReachable(
spoiler,
ItemPool.GetItemsNeedingToBeAssumed(spoiler.settings, placed_types, placed_items=preplaced_items),
"Crowns",
)
# Then place Banana Medals
if Types.Medal in spoiler.settings.shuffled_location_types:
placed_types.append(Types.Medal)
spoiler.Reset()
medalsToBePlaced = ItemPool.BananaMedalItems(spoiler.settings)
for item in preplaced_items:
if item in medalsToBePlaced:
medalsToBePlaced.remove(item)
medalAssumedItems = ItemPool.GetItemsNeedingToBeAssumed(spoiler.settings, placed_types, placed_items=preplaced_items)
# Medals up to the logical Jetpac requirement must be placed carefully
jetpacRequiredMedals = medalsToBePlaced[: spoiler.settings.logical_medal_requirement]
medalsUnplaced = PlaceItems(spoiler, spoiler.settings.algorithm, jetpacRequiredMedals, medalAssumedItems)
if medalsUnplaced > 0:
raise Ex.ItemPlacementException("Unable to find all locations during the fill. Error code: LM-" + str(medalsUnplaced))
# The remaining medals can be placed randomly
medalsUnplaced = PlaceItems(
spoiler,
FillAlgorithm.careful_random,
medalsToBePlaced[spoiler.settings.logical_medal_requirement :],
medalAssumedItems,
)
if medalsUnplaced > 0:
raise Ex.ItemPlacementException("Unable to find all locations during the fill. Error code: RM-" + str(medalsUnplaced))
if spoiler.settings.extreme_debugging:
DebugCheckAllReachable(
spoiler,
ItemPool.GetItemsNeedingToBeAssumed(spoiler.settings, placed_types, placed_items=preplaced_items),
"Banana Medals",
)
# Then place Fairies
if Types.Fairy in spoiler.settings.shuffled_location_types:
placed_types.append(Types.Fairy)
spoiler.Reset()
fairiesToBePlaced = ItemPool.FairyItems()
for item in preplaced_items:
if item in fairiesToBePlaced:
fairiesToBePlaced.remove(item)
fairyAssumedItems = ItemPool.GetItemsNeedingToBeAssumed(spoiler.settings, placed_types, placed_items=preplaced_items)
# Fairies up to the logical Rareware GB requirement must be placed carefully
rarewareRequiredFairies = fairiesToBePlaced[: spoiler.settings.logical_fairy_requirement]
fairyUnplaced = PlaceItems(spoiler, spoiler.settings.algorithm, rarewareRequiredFairies, fairyAssumedItems)
if fairyUnplaced > 0:
raise Ex.ItemPlacementException("Unable to find all locations during the fill. Error code: LF-" + str(fairyUnplaced))
# The remaining fairies can be placed randomly
fairyUnplaced = PlaceItems(
spoiler,
FillAlgorithm.careful_random,
fairiesToBePlaced[spoiler.settings.logical_fairy_requirement :],
fairyAssumedItems,
)
if fairyUnplaced > 0:
raise Ex.ItemPlacementException("Unable to find all locations during the fill. Error code: RF-" + str(fairyUnplaced))
if spoiler.settings.extreme_debugging:
DebugCheckAllReachable(
spoiler,
ItemPool.GetItemsNeedingToBeAssumed(spoiler.settings, placed_types, placed_items=preplaced_items),
"Fairies",
)
# Then fill remaining locations with GBs
preplaced_gbs_accounted_for = [] # Because GBs are placed in two parts, we may have to account for preplaced GBs in either section
if Types.Banana in spoiler.settings.shuffled_location_types:
placed_types.append(Types.Banana)
spoiler.Reset()
gbsToBePlaced = ItemPool.GoldenBananaItems()
for item in preplaced_items:
if item in gbsToBePlaced:
gbsToBePlaced.remove(item)
# Mark this preplaced GB as accounted for
preplaced_gbs_accounted_for.append(item)
# After checking all preplaced items, we can treat the accounted for GBs as no longer preplaced
# This way the upcoming ToughBanana GB fill will not double-account for them
for item in preplaced_gbs_accounted_for:
preplaced_items.remove(item)
gbsUnplaced = PlaceItems(spoiler, FillAlgorithm.careful_random, gbsToBePlaced, [])
if gbsUnplaced > 0:
raise Ex.ItemPlacementException("Unable to find all locations during the fill. Error code: GB-" + str(gbsUnplaced))
if spoiler.settings.extreme_debugging:
DebugCheckAllReachable(
spoiler,
ItemPool.GetItemsNeedingToBeAssumed(spoiler.settings, placed_types, placed_items=preplaced_items),
"GBs",
)
if Types.ToughBanana in spoiler.settings.shuffled_location_types:
placed_types.append(Types.ToughBanana)
spoiler.Reset()
toughGbsToBePlaced = ItemPool.ToughGoldenBananaItems()
for item in preplaced_items:
if item in toughGbsToBePlaced:
toughGbsToBePlaced.remove(item)
gbsUnplaced = PlaceItems(spoiler, FillAlgorithm.careful_random, toughGbsToBePlaced, [])
if gbsUnplaced > 0:
raise Ex.ItemPlacementException("Unable to find all locations during the fill. Error code: TB-" + str(gbsUnplaced))
if spoiler.settings.extreme_debugging:
DebugCheckAllReachable(
spoiler,
ItemPool.GetItemsNeedingToBeAssumed(spoiler.settings, placed_types, placed_items=preplaced_items),
"Tough GBs",
)
# Place Hints
if Types.Hint in spoiler.settings.shuffled_location_types and spoiler.settings.progressive_hint_item == ProgressiveHintItem.off:
placed_types.append(Types.Hint)
spoiler.Reset()
hintItemsToBePlaced = ItemPool.HintItems()
for item in preplaced_items:
if item in hintItemsToBePlaced:
hintItemsToBePlaced.remove(item)
hintsUnplaced = PlaceItems(spoiler, FillAlgorithm.careful_random, hintItemsToBePlaced, [])
if hintsUnplaced > 0:
raise Ex.ItemPlacementException("Unable to find all locations during the fill. Error code: HN-" + str(hintsUnplaced))
if spoiler.settings.extreme_debugging:
DebugCheckAllReachable(
spoiler,
ItemPool.GetItemsNeedingToBeAssumed(spoiler.settings, placed_types, placed_items=preplaced_items),
"Tough GBs",
)
# Fill in fake items
if Types.FakeItem in spoiler.settings.shuffled_location_types:
placed_types.append(Types.FakeItem)
spoiler.Reset()
fakeItemsToBePlaced = ItemPool.FakeItems(spoiler.settings)
for item in preplaced_items:
if item in fakeItemsToBePlaced:
fakeItemsToBePlaced.remove(item)
PlaceItems(spoiler, FillAlgorithm.careful_random, fakeItemsToBePlaced, [])
# Don't raise exception if unplaced fake items
if spoiler.settings.extreme_debugging:
DebugCheckAllReachable(spoiler, ItemPool.GetItemsNeedingToBeAssumed(spoiler.settings, placed_types), "Fake Items")
# Fill in junk items
if Types.JunkItem in spoiler.settings.shuffled_location_types:
placed_types.append(Types.JunkItem)
spoiler.Reset()
PlaceItems(spoiler, FillAlgorithm.random, ItemPool.JunkItems(spoiler.settings), [])
# Don't raise exception if unplaced junk items
if Types.CrateItem in spoiler.settings.shuffled_location_types:
placed_types.append(Types.CrateItem)
# Crates hold nothing, so leave this one empty
if Types.Enemies in spoiler.settings.shuffled_location_types:
placed_types.append(Types.Enemies)
# Enemies hold nothing, so leave this one empty
# Some locations require special care to make logic work correctly
# This is the only location that cares about None vs NoItem - it needs to be None so it fills correctly but NoItem for logic to generate progression correctly
if spoiler.LocationList[Locations.JapesDonkeyFreeDiddy].item is None:
spoiler.LocationList[Locations.JapesDonkeyFreeDiddy].PlaceItem(spoiler, Items.NoItem)
# Shopkeepers have been either placed in the world or set to vanilla. In case of the former, empty out their vanilla "locations" as needed
for x in range(4):
if spoiler.LocationList[Locations.ShopOwner_Location00 + x].item is None:
spoiler.LocationList[Locations.ShopOwner_Location00 + x].PlaceItem(spoiler, Items.NoItem)
# Finally, check if game is beatable
spoiler.Reset()
if not GetAccessibleLocations(spoiler, [], SearchMode.CheckAllReachable):
print("Failed 101% check")
raise Ex.GameNotBeatableException("Game not able to complete 101% after placing all items.")
# We have successfully filled the seed by this point. All that is left is to confirm there are no purchase order locks
return
def FillTrainingMoves(spoiler: Spoiler, preplacedMoves: List[Items]):
"""Fill training barrels with your starting moves."""
# Convenient assumptions made by this method:
# - No list is expected to pull more items than are contained in the list.
# - Training locations that are made Constant will already have been made so. Essentially, do this after placing constants.
# - No non-progressive item is in multiple lists. The correct quantity of progressive items are cumulatively in all lists.
# To account for plando things, remove the possibility for plando'd items to get chosen.
# This isn't foolproof, particularly around progressive items!
for item in preplacedMoves:
for i in range(len(spoiler.settings.starting_moves_lists)):
if item in spoiler.settings.starting_moves_lists[i]:
spoiler.settings.starting_moves_lists[i].remove(item)
# Determine what moves will be placed in these locations
movesToPlace = []
# Check for if we've found what would be the vanilla starting slam - this is only necessary if the settings dictate that we should find one
startingSlamIdentified = not spoiler.settings.start_with_slam
for i in range(len(spoiler.settings.starting_moves_lists)):
# If every item in this list is guaranteed to be placed, we have to watch out for vanilla items that should already be placed
if len(spoiler.settings.starting_moves_lists[i]) <= spoiler.settings.starting_moves_list_counts[i]:
for item in spoiler.settings.starting_moves_lists[i]:
# Avoid vanilla training moves, the first starting slam, and climbing
if item in ItemPool.TrainingBarrelAbilities() and spoiler.settings.training_barrels == TrainingBarrels.normal:
continue
if item == Items.ProgressiveSlam and not startingSlamIdentified:
startingSlamIdentified = True
continue
if item == Items.Climbing and spoiler.settings.climbing_status == ClimbingStatus.normal:
continue
# Avoid anything that isn't supposed to be shuffled but could be in the starting move selector (Shopkeepers, Camera/Shockwave)
if ItemList[item].type not in spoiler.settings.shuffled_location_types:
continue
# Otherwise everything else in this pool is guaranteed to be placed on a starting location
movesToPlace.append(item)
# Otherwise we take a random selection of items in this pool equal to the corresponding desired amount
else:
movesToPlace.extend(spoiler.settings.random.sample(spoiler.settings.starting_moves_lists[i], k=spoiler.settings.starting_moves_list_counts[i]))
placedMoves = []
if spoiler.settings.start_with_slam:
placedMoves.append(Items.ProgressiveSlam)
# Shopkeepers can be placed in their special starting locations
if Items.Cranky in movesToPlace:
spoiler.LocationList[Locations.ShopOwner_Location00].PlaceItem(spoiler, Items.Cranky)
spoiler.LocationList[Locations.ShopOwner_Location00].inaccessible = False
placedMoves.append(Items.Cranky)
movesToPlace.remove(Items.Cranky)
if Items.Funky in movesToPlace:
spoiler.LocationList[Locations.ShopOwner_Location01].PlaceItem(spoiler, Items.Funky)
spoiler.LocationList[Locations.ShopOwner_Location01].inaccessible = False
placedMoves.append(Items.Funky)
movesToPlace.remove(Items.Funky)
if Items.Candy in movesToPlace:
spoiler.LocationList[Locations.ShopOwner_Location02].PlaceItem(spoiler, Items.Candy)
spoiler.LocationList[Locations.ShopOwner_Location02].inaccessible = False
placedMoves.append(Items.Candy)
movesToPlace.remove(Items.Candy)
if Items.Snide in movesToPlace:
spoiler.LocationList[Locations.ShopOwner_Location03].PlaceItem(spoiler, Items.Snide)
spoiler.LocationList[Locations.ShopOwner_Location03].inaccessible = False
placedMoves.append(Items.Snide)
movesToPlace.remove(Items.Snide)
if len(movesToPlace) > 0:
# We can expect that all locations in this region are starting move locations, Training Barrels, or starting shopkeeper locations
for locationLogic in spoiler.RegionList[Regions.GameStart].locations:
location = spoiler.LocationList[locationLogic.id]
if location.item is None and location.type not in (Types.Cranky, Types.Funky, Types.Candy, Types.Snide): # Don't put moves in shopkeeper locations!
placedMove = movesToPlace.pop()
location.inaccessible = False
location.PlaceItem(spoiler, placedMove)
placedMoves.append(placedMove)
if len(movesToPlace) <= 0:
break
return placedMoves
def ShuffleSharedMoves(spoiler: Spoiler, placedMoves: List[Items], placedTypes: List[Types]) -> None:
"""Shuffles shared kong moves into shops and then returns the remaining ones and their valid locations."""
# If we start with a slam as the training grounds reward, it counts as placed for fill purposes
if spoiler.settings.start_with_slam:
placedMoves.append(Items.ProgressiveSlam)
# If shared moves have to be in shared shops, confirm there are enough locations available for each remaining shared move
if not spoiler.settings.shuffle_items or Types.Shop not in spoiler.settings.shuffled_location_types:
availableSharedShops = [location for location in SharedMoveLocations if spoiler.LocationList[location].item is None]
placedSharedMoves = [move for move in placedMoves if move in ItemPool.ImportantSharedMoves or move in ItemPool.JunkSharedMoves]
if len(availableSharedShops) < len(ItemPool.ImportantSharedMoves) + len(ItemPool.JunkSharedMoves) - len(placedSharedMoves):
raise Ex.ItemPlacementException(
"Too many kong moves placed before shared moves. Only "
+ str(len(availableSharedShops))
+ " available for "
+ str(len(ItemPool.ImportantSharedMoves))
+ str(len(ItemPool.JunkSharedMoves))
+ str(len(placedSharedMoves))
+ " remaining shared moves."
)
# When a shared move is assigned to a shop in any particular level, that shop cannot also hold any kong-specific moves.
# To avoid conflicts, first determine which level shops will have shared moves then remove these shops from each kong's valid locations list
training = []
if spoiler.settings.training_barrels != TrainingBarrels.normal:
# First place training moves that are not placed. These should be the first moves placed outside of starting moves. Placement order is in relative importance.
training = [Items.Barrels, Items.Vines, Items.Swim, Items.Oranges]
if spoiler.settings.climbing_status != ClimbingStatus.normal:
training.append(Items.Climbing)
trainingMovesToPlace = [move for move in training if move not in placedMoves]
assumedItems = [x for x in ItemPool.GetItemsNeedingToBeAssumed(spoiler.settings, placedTypes, placedMoves) if x not in trainingMovesToPlace]
trainingMovesUnplaced = PlaceItems(spoiler, FillAlgorithm.assumed, trainingMovesToPlace, assumedItems, inOrder=True)
if trainingMovesUnplaced > 0:
raise Ex.ItemPlacementException("Unable to find enough locations to place training barrel moves.")
placedMoves.extend(training)
importantSharedToPlace = ItemPool.ImportantSharedMoves.copy()
# Next place any fairy moves that need placing, settings dependent
if spoiler.settings.shockwave_status == ShockwaveStatus.shuffled and Items.CameraAndShockwave not in placedMoves:
importantSharedToPlace.append(Items.CameraAndShockwave)
elif spoiler.settings.shockwave_status == ShockwaveStatus.shuffled_decoupled and (Items.Camera not in placedMoves or Items.Shockwave not in placedMoves):
importantSharedToPlace.append(Items.Camera)
importantSharedToPlace.append(Items.Shockwave)
for item in placedMoves:
if item in importantSharedToPlace:
importantSharedToPlace.remove(item)
placedMoves.extend(importantSharedToPlace)
importantSharedUnplaced = PlaceItems(
spoiler,
FillAlgorithm.assumed,
importantSharedToPlace,
ItemPool.GetItemsNeedingToBeAssumed(spoiler.settings, placedTypes, placedMoves),
)
if importantSharedUnplaced > 0:
raise Ex.ItemPlacementException("Unable to find enough locations to place " + str(importantSharedUnplaced) + " shared important items.")
junkSharedToPlace = ItemPool.JunkSharedMoves.copy()
for item in placedMoves:
if item in junkSharedToPlace:
junkSharedToPlace.remove(item)
placedMoves.extend(junkSharedToPlace)
junkSharedUnplaced = PlaceItems(
spoiler,
FillAlgorithm.random,
junkSharedToPlace,
[x for x in ItemPool.GetItemsNeedingToBeAssumed(spoiler.settings, placedTypes) if x not in junkSharedToPlace],
)
if junkSharedUnplaced > 0:
raise Ex.ItemPlacementException("Unable to find enough locations to place " + str(junkSharedUnplaced) + " shared junk items.")
def GeneratePlaythrough(spoiler: Spoiler) -> None:
"""Generate playthrough and way of the hoard and update spoiler."""
js.postMessage("Seed generated! Finalizing spoiler...")
spoiler.LogicVariables.assumeFillSuccess = True # Now that we know the seed is valid, we can assume fill success for the sake of generating the playthrough and WotH
# Generate and display the playthrough
spoiler.Reset()
PlaythroughLocations = GetAccessibleLocations(spoiler, [], SearchMode.GeneratePlaythrough) # identify in the spheres where the win condition is met
if not spoiler.LogicVariables.bananaHoard and spoiler.settings.logic_type != LogicType.nologic:
raise Ex.FillException("Woah, you hit an EXTREMELY rare error! Please post your settings string to the discord. It's probably a freak accident so you're safe to try again.")
ParePlaythrough(spoiler, PlaythroughLocations)
# Generate and display woth
WothLocations = PareWoth(spoiler, PlaythroughLocations)
# Write data to spoiler and return
spoiler.UpdateLocations(spoiler.LocationList)
if any(spoiler.settings.shuffled_location_types):
ShuffleItems(spoiler)
spoiler.UpdatePlaythrough(spoiler.LocationList, PlaythroughLocations)
spoiler.UpdateWoth(spoiler.LocationList, WothLocations)
def GetLogicallyAccessibleKongLocations(spoiler: Spoiler, kongLocations, ownedKongs, latestLevel):
"""Find the logically accessible Kong Locations given the current state of Kong unlocking."""
logicallyAccessibleKongLocations = []
for level in range(1, latestLevel + 1):
if spoiler.settings.level_order[level] == Levels.JungleJapes and Locations.DiddyKong in kongLocations:
logicallyAccessibleKongLocations.append(Locations.DiddyKong)
if spoiler.settings.level_order[level] == Levels.FranticFactory and Locations.ChunkyKong in kongLocations:
logicallyAccessibleKongLocations.append(Locations.ChunkyKong)
if spoiler.settings.level_order[level] == Levels.AngryAztec and Locations.TinyKong in kongLocations and (Kongs.diddy in ownedKongs or Kongs.chunky in ownedKongs):
logicallyAccessibleKongLocations.append(Locations.TinyKong)
if (
spoiler.settings.level_order[level] == Levels.AngryAztec
and Locations.LankyKong in kongLocations
# Must be able to bypass Guitar door - the active bananaports condition is in case your only Llama Temple access is through the quicksand cave
and (
Kongs.diddy in ownedKongs
or IsItemSelected(
spoiler.settings.remove_barriers_enabled,
spoiler.settings.remove_barriers_selected,
RemovedBarriersSelected.aztec_tunnel_door,
)
or (Kongs.donkey in ownedKongs and spoiler.settings.activate_all_bananaports == ActivateAllBananaports.all)
)
and (Kongs.donkey in ownedKongs or Kongs.lanky in ownedKongs or Kongs.tiny in ownedKongs)
): # Must be able to open Llama Temple
logicallyAccessibleKongLocations.append(Locations.LankyKong)
return logicallyAccessibleKongLocations
def PlacePriorityItems(spoiler: Spoiler, itemsToPlace, beforePlacedItems, placedTypes, levelBlock=None):
"""Place the given items with priority, also placing all dependencies depending on where they got placed. Returns a list of all items newly placed by this function."""
if itemsToPlace == []: # Base case of recursion - when priority items no longer have dependencies, they'll hit this method placing zero items
return []
# Prevent reference shenanigans because I'm too lazy to do it properly
priorityItemsToPlace = itemsToPlace.copy()
placedItems = beforePlacedItems.copy()
# If we're blocking past a certain level, ban keys that would unlock anything beyond those levels
bannedKeys = []
if levelBlock is not None:
bannedKeys = [key for key in ItemPool.Keys() if ItemList[key].index >= levelBlock]
allOtherItems = ItemPool.GetItemsNeedingToBeAssumed(spoiler.settings, placedTypes)
if Types.Key in spoiler.settings.shuffled_location_types:
# However we don't want all keys - don't assume keys for or beyond the latest logically allowed level's key
for key in bannedKeys:
allOtherItems.remove(key)
# Other exceptions: we don't assume we have the items to be placed, as then they could lock themselves
for item in priorityItemsToPlace:
allOtherItems.remove(item)
# We also don't assume we have any placed items. If these unlock locations we should find them as we go.
# This should prevent circular logic (e.g. the diddy-unlocking-gun being locked behind guitar which is already priority placed in Japes Cranky)
for item in placedItems:
if item in allOtherItems:
allOtherItems.remove(item)
# At last, place all the items
failedToPlace = PlaceItems(spoiler, FillAlgorithm.assumed, priorityItemsToPlace.copy(), ownedItems=allOtherItems)
if failedToPlace > 0:
item_names = ", ".join([ItemList[item].name for item in priorityItemsToPlace])
raise Ex.ItemPlacementException(f"Failed to priority place {item_names}")
# Note down the latest known list of owned kongs - I don't think this is necessary, but if it is less than 5 it is accurate and should speed up GetUnplacedItemPrerequisites
ownedKongs = spoiler.LogicVariables.GetKongs()
# The items we just placed can now be treated as such
placedItems.extend(priorityItemsToPlace)
unplacedDependencies = []
for item in priorityItemsToPlace:
# Find what items are needed to get this item
unplacedItems = GetUnplacedItemPrerequisites(spoiler, item, placedItems, ownedKongs)
slamsRequired = unplacedItems.count(Items.ProgressiveSlam)
# Add each unplaced item to the list of items that now need to be placed, making sure not to add duplicates or third slams
for item in unplacedItems:
if item not in unplacedDependencies or (item == Items.ProgressiveSlam and slamsRequired > 1 and unplacedDependencies.count(Items.ProgressiveSlam) < 2):
unplacedDependencies.append(item)
# Recursively place priority items with the dependencies - anything this method places will also need to be returned by the outermost call
priorityItemsToPlace.extend(PlacePriorityItems(spoiler, unplacedDependencies, placedItems, placedTypes, levelBlock))
return priorityItemsToPlace
def PlaceKongsInKongLocations(spoiler: Spoiler, kongItems, kongLocations):
"""For these settings, Kongs to place, and locations to place them in, place the Kongs in such a way the generation will never error here."""
ownedKongs = [kong for kong in spoiler.settings.starting_kong_list]
# In entrance randomizer, it's too complicated to quickly determine kong accessibility.
# Instead, we place Kongs in a specific order to guarantee we'll at least have an eligible freer.
# To be at least somewhat nice to no logic users, we also use this section here so kongs don't lock each other.
if spoiler.settings.shuffle_loading_zones == ShuffleLoadingZones.all or spoiler.settings.logic_type == LogicType.nologic:
spoiler.settings.random.shuffle(kongItems)
if Locations.ChunkyKong in kongLocations:
kongItemToBeFreed = kongItems.pop()
spoiler.LocationList[Locations.ChunkyKong].PlaceItem(spoiler, kongItemToBeFreed)
spoiler.settings.chunky_freeing_kong = spoiler.settings.random.choice(ownedKongs)
ownedKongs.append(ItemPool.GetKongForItem(kongItemToBeFreed))
if Locations.DiddyKong in kongLocations:
kongItemToBeFreed = kongItems.pop()
spoiler.LocationList[Locations.DiddyKong].PlaceItem(spoiler, kongItemToBeFreed)
spoiler.settings.diddy_freeing_kong = spoiler.settings.random.choice(ownedKongs)
ownedKongs.append(ItemPool.GetKongForItem(kongItemToBeFreed))
# The Lanky location can't be your first in cases where the Lanky freeing Kong can't get into the llama temple and you need a second Kong
if Locations.LankyKong in kongLocations:
kongItemToBeFreed = kongItems.pop()
spoiler.LocationList[Locations.LankyKong].PlaceItem(spoiler, kongItemToBeFreed)
spoiler.settings.lanky_freeing_kong = spoiler.settings.random.choice(ownedKongs)
ownedKongs.append(ItemPool.GetKongForItem(kongItemToBeFreed))
# Placing the Tiny location last guarantees we have one of Diddy or Chunky
if Locations.TinyKong in kongLocations:
kongItemToBeFreed = kongItems.pop()
spoiler.LocationList[Locations.TinyKong].PlaceItem(spoiler, kongItemToBeFreed)
eligibleFreers = list(set(ownedKongs).intersection([Kongs.diddy, Kongs.chunky]))
spoiler.settings.tiny_freeing_kong = spoiler.settings.random.choice(eligibleFreers)
ownedKongs.append(ItemPool.GetKongForItem(kongItemToBeFreed))
# In level order shuffling, we need to be very particular about who we unlock and in what order so as to guarantee completion
# Vanilla levels can be treated as if the level shuffler randomly placed all the levels in the same order
elif spoiler.settings.shuffle_loading_zones in (ShuffleLoadingZones.levels, ShuffleLoadingZones.none):
latestLogicallyAllowedLevel = len(ownedKongs) + 1
# Logically we can always enter any level on hard level progression
if spoiler.settings.hard_level_progression:
latestLogicallyAllowedLevel = 8
logicallyAccessibleKongLocations = GetLogicallyAccessibleKongLocations(spoiler, kongLocations, ownedKongs, latestLogicallyAllowedLevel)
while len(ownedKongs) != 5:
# If there aren't any accessible Kong locations, then the level order shuffler has a bug (this shouldn't happen)
if not any(logicallyAccessibleKongLocations):
raise Ex.EntrancePlacementException("Levels shuffled in a way that makes Kong unlocks impossible. SEND THIS TO THE DEVS!")
# Begin by finding the currently accessible Kong locations
# Randomly pick an accessible location
progressionLocation = spoiler.settings.random.choice(logicallyAccessibleKongLocations)
logicallyAccessibleKongLocations.remove(progressionLocation)
# Pick a Kong to free this location from the Kongs we currently have
if progressionLocation == Locations.DiddyKong:
spoiler.settings.diddy_freeing_kong = spoiler.settings.random.choice(ownedKongs)
elif progressionLocation == Locations.LankyKong:
spoiler.settings.lanky_freeing_kong = spoiler.settings.random.choice(ownedKongs)
elif progressionLocation == Locations.TinyKong:
eligibleFreers = list(set(ownedKongs).intersection([Kongs.diddy, Kongs.chunky]))
spoiler.settings.tiny_freeing_kong = spoiler.settings.random.choice(eligibleFreers)
elif progressionLocation == Locations.ChunkyKong:
spoiler.settings.chunky_freeing_kong = spoiler.settings.random.choice(ownedKongs)
# Remove this location from any considerations
kongLocations.remove(progressionLocation)
# Pick a Kong to unlock from the locked Kongs
kongToBeFreed = spoiler.settings.random.choice(kongItems)
# With this kong, we can progress one level further (if we care about this logic)
if not spoiler.settings.hard_level_progression:
latestLogicallyAllowedLevel += 1
# If this Kong must unlock more locked Kong locations, we have to be more careful
# The second condition here because we don't need to worry about the last placed Kong
if len(logicallyAccessibleKongLocations) == 0 and len(kongItems) > 1:
# First check if that newly accessible level adds a location. If it does, then it doesn't matter who we free here
logicallyAccessibleKongLocations = GetLogicallyAccessibleKongLocations(spoiler, kongLocations, ownedKongs, latestLogicallyAllowedLevel)
if not any(logicallyAccessibleKongLocations):
# If it doesn't, then we need to see which Kongs will open more Kongs
progressionKongItems = []
for kongItem in kongItems:
# Test each Kong by temporarily owning them and seeing what we can now reach
tempOwnedKongs = [x for x in ownedKongs]
tempOwnedKongs.append(ItemPool.GetKongForItem(kongItem))
newlyAccessibleKongLocations = GetLogicallyAccessibleKongLocations(spoiler, kongLocations, tempOwnedKongs, latestLogicallyAllowedLevel)
if any(newlyAccessibleKongLocations):
progressionKongItems.append(kongItem)
if len(progressionKongItems) == 0:
raise Ex.FillException("Kongs placed in a way that is impossible to unlock everyone. SEND THIS TO THE DEVS!")
# Pick a random Kong from the Kongs that guarantee progression
kongToBeFreed = spoiler.settings.random.choice(progressionKongItems)
# Now that we have a combination guaranteed to not break the seed or logic, lock it in
spoiler.LocationList[progressionLocation].PlaceItem(spoiler, kongToBeFreed)
spoiler.settings.debug_fill[spoiler.LocationList[progressionLocation].name] = kongToBeFreed
kongItems.remove(kongToBeFreed)
ownedKongs.append(ItemPool.GetKongForItem(kongToBeFreed))
# Refresh the location list and repeat until all Kongs are free
logicallyAccessibleKongLocations = GetLogicallyAccessibleKongLocations(spoiler, kongLocations, ownedKongs, latestLogicallyAllowedLevel)
# Pick freeing kongs for any that are still "any" with no restrictions.
if spoiler.settings.diddy_freeing_kong == Kongs.any:
spoiler.settings.diddy_freeing_kong = spoiler.settings.random.choice(GetKongs())
if spoiler.settings.lanky_freeing_kong == Kongs.any:
spoiler.settings.lanky_freeing_kong = spoiler.settings.random.choice(GetKongs())
if spoiler.settings.tiny_freeing_kong == Kongs.any:
spoiler.settings.tiny_freeing_kong = spoiler.settings.random.choice([Kongs.diddy, Kongs.chunky])
if spoiler.settings.chunky_freeing_kong == Kongs.any:
spoiler.settings.chunky_freeing_kong = spoiler.settings.random.choice(GetKongs())
# Update the locations' assigned kong with the set freeing kong list
spoiler.LocationList[Locations.DiddyKong].kong = spoiler.settings.diddy_freeing_kong
spoiler.LocationList[Locations.JapesDonkeyFrontofCage].kong = spoiler.settings.diddy_freeing_kong
spoiler.LocationList[Locations.JapesDonkeyFreeDiddy].kong = spoiler.settings.diddy_freeing_kong
spoiler.LocationList[Locations.LankyKong].kong = spoiler.settings.lanky_freeing_kong
spoiler.LocationList[Locations.AztecDonkeyFreeLanky].kong = spoiler.settings.lanky_freeing_kong
spoiler.LocationList[Locations.TinyKong].kong = spoiler.settings.tiny_freeing_kong
spoiler.LocationList[Locations.AztecDiddyFreeTiny].kong = spoiler.settings.tiny_freeing_kong
spoiler.LocationList[Locations.ChunkyKong].kong = spoiler.settings.chunky_freeing_kong
spoiler.LocationList[Locations.FactoryLankyFreeChunky].kong = spoiler.settings.chunky_freeing_kong
spoiler.settings.update_valid_locations(spoiler)
def FillKongs(spoiler: Spoiler, placedTypes: List[Types], placedItems: List[Items]) -> None:
"""Place Kongs in valid locations."""
placedTypes.append(Types.Kong)
# Determine what kong items need to be placed
startingKongItems = [ItemPool.ItemFromKong(kong) for kong in spoiler.settings.starting_kong_list]
kongItems = [item for item in ItemPool.Kongs(spoiler.settings) if item not in startingKongItems and item not in placedItems]
# If Kongs can be placed anywhere, we don't need anything special
if spoiler.settings.shuffle_items and Types.Kong in spoiler.settings.shuffled_location_types:
# First, randomly pick who opens what cage - this prevents cases where a Kong locks themselves
spoiler.settings.diddy_freeing_kong = spoiler.settings.random.choice(GetKongs())
spoiler.settings.lanky_freeing_kong = spoiler.settings.random.choice(GetKongs())
spoiler.settings.tiny_freeing_kong = spoiler.settings.random.choice([Kongs.diddy, Kongs.chunky])
spoiler.settings.chunky_freeing_kong = spoiler.settings.random.choice(GetKongs())
if spoiler.settings.enable_plandomizer:
if spoiler.settings.plandomizer_dict["plando_kong_rescue_diddy"] != -1:
spoiler.settings.diddy_freeing_kong = Kongs(spoiler.settings.plandomizer_dict["plando_kong_rescue_diddy"])
if spoiler.settings.plandomizer_dict["plando_kong_rescue_lanky"] != -1:
spoiler.settings.lanky_freeing_kong = Kongs(spoiler.settings.plandomizer_dict["plando_kong_rescue_lanky"])
if spoiler.settings.plandomizer_dict["plando_kong_rescue_tiny"] != -1:
spoiler.settings.tiny_freeing_kong = Kongs(spoiler.settings.plandomizer_dict["plando_kong_rescue_tiny"])
if spoiler.settings.plandomizer_dict["plando_kong_rescue_chunky"] != -1:
spoiler.settings.chunky_freeing_kong = Kongs(spoiler.settings.plandomizer_dict["plando_kong_rescue_chunky"])
# Update the locations' assigned kong with the set freeing kong list
spoiler.LocationList[Locations.JapesDonkeyFrontofCage].kong = spoiler.settings.diddy_freeing_kong
spoiler.LocationList[Locations.JapesDonkeyFreeDiddy].kong = spoiler.settings.diddy_freeing_kong
spoiler.LocationList[Locations.AztecDonkeyFreeLanky].kong = spoiler.settings.lanky_freeing_kong
spoiler.LocationList[Locations.AztecDiddyFreeTiny].kong = spoiler.settings.tiny_freeing_kong
spoiler.LocationList[Locations.FactoryLankyFreeChunky].kong = spoiler.settings.chunky_freeing_kong
assumedItems = ItemPool.GetItemsNeedingToBeAssumed(spoiler.settings, placedTypes, placedItems)
spoiler.Reset()
PlaceItems(spoiler, FillAlgorithm.assumed, kongItems, assumedItems)
# If we didn't put an item in a kong location, then it gets a NoItem
# This matters specifically so the logic around items inside Kong cages (VERY important for Diddy's cage) behaves properly
if spoiler.LocationList[Locations.DiddyKong].item is None:
spoiler.LocationList[Locations.DiddyKong].PlaceItem(spoiler, Items.NoItem)
else:
spoiler.LocationList[Locations.DiddyKong].kong = spoiler.settings.diddy_freeing_kong # If any Kong cage DOES have a kong, update the location's assigned Kong
if spoiler.LocationList[Locations.TinyKong].item is None:
spoiler.LocationList[Locations.TinyKong].PlaceItem(spoiler, Items.NoItem)
else:
spoiler.LocationList[Locations.TinyKong].kong = spoiler.settings.tiny_freeing_kong
if spoiler.LocationList[Locations.LankyKong].item is None:
spoiler.LocationList[Locations.LankyKong].PlaceItem(spoiler, Items.NoItem)
else:
spoiler.LocationList[Locations.LankyKong].kong = spoiler.settings.lanky_freeing_kong
if spoiler.LocationList[Locations.ChunkyKong].item is None:
spoiler.LocationList[Locations.ChunkyKong].PlaceItem(spoiler, Items.NoItem)
else:
spoiler.LocationList[Locations.ChunkyKong].kong = spoiler.settings.chunky_freeing_kong
spoiler.settings.update_valid_locations(spoiler)
# If kongs must be in Kong cages, we need to be more careful
else:
# Plando causes problems here. Let's just not.
if any([item for item in placedItems if ItemList[item].type == Types.Kong]):
raise Ex.PlandoIncompatibleException("Cannot plando Kong placement if Kongs are not in the pool.")
if spoiler.settings.enable_plandomizer and (
spoiler.settings.plandomizer_dict["plando_kong_rescue_diddy"] != -1
or spoiler.settings.plandomizer_dict["plando_kong_rescue_lanky"] != -1
or spoiler.settings.plandomizer_dict["plando_kong_rescue_tiny"] != -1
or spoiler.settings.plandomizer_dict["plando_kong_rescue_chunky"] != -1
):
raise Ex.PlandoIncompatibleException("Cannot plando Kong cage openers if Kongs are not in the pool.")
# Determine what locations the kong items need to be placed in
if any(spoiler.settings.kong_locations):
emptyKongLocations = [location for location in [Locations.DiddyKong, Locations.LankyKong, Locations.TinyKong, Locations.ChunkyKong] if location not in spoiler.settings.kong_locations]
for locationId in emptyKongLocations:
spoiler.LocationList[locationId].PlaceItem(spoiler, Items.NoItem)
spoiler.Reset()
# Specialized Kong placement function that will never fail to find a beatable combination of Kong unlocks for the vanilla locations
PlaceKongsInKongLocations(spoiler, kongItems, spoiler.settings.kong_locations.copy())
def FillKongsAndMoves(spoiler: Spoiler, placedTypes: List[Types], placedItems: List[Items]) -> None:
"""Fill kongs, then progression moves, then shared moves, then rest of moves."""
itemsToPlace = []
# Handle kong rando first so we know what moves are most important to place
if spoiler.settings.kong_rando:
FillKongs(spoiler, placedTypes, placedItems)
placedMoves = [
Items.Donkey,
Items.Diddy,
Items.Lanky,
Items.Tiny,
Items.Chunky,
] # Kongs are now placed, either in the above method or by default
placedMoves.extend(placedItems)
# Place Training Moves
placedMoves.extend(FillTrainingMoves(spoiler, placedItems))
# Fill in Shop Owners
shop_owner_items = {
Types.Cranky: ItemPool.CrankyItems(),
Types.Funky: ItemPool.FunkyItems(),
Types.Candy: ItemPool.CandyItems(),
Types.Snide: ItemPool.SnideItems(),
}
for item_type in shop_owner_items:
if item_type in spoiler.settings.shuffled_location_types:
placedTypes.append(item_type)
spoiler.Reset()
shopOwnerItemsToBePlaced = shop_owner_items[item_type]
for item in placedMoves:
if item in shopOwnerItemsToBePlaced:
shopOwnerItemsToBePlaced.remove(item)
unplacedShopOwners = PlaceItems(
spoiler,
FillAlgorithm.random,
shopOwnerItemsToBePlaced,
ItemPool.GetItemsNeedingToBeAssumed(spoiler.settings, placedTypes, placedItems),
)
if unplacedShopOwners > 0:
raise Ex.ItemPlacementException("Unable to find locations to place " + str(unplacedShopOwners) + " shop owners.")
# Handle shared moves before other moves in move rando
if spoiler.settings.move_rando != MoveRando.off:
# Shuffle the shared move locations since they must be done first
ShuffleSharedMoves(spoiler, placedMoves.copy(), placedTypes)
# Set up remaining kong moves to be shuffled
itemsToPlace.extend(ItemPool.DonkeyMoves)
itemsToPlace.extend(ItemPool.DiddyMoves)
itemsToPlace.extend(ItemPool.LankyMoves)
itemsToPlace.extend(ItemPool.TinyMoves)
itemsToPlace.extend(ItemPool.ChunkyMoves)
# Handle remaining moves/items
placedTypes.append(Types.Shop)
placedTypes.append(Types.TrainingBarrel)
placedTypes.append(Types.Climbing)
placedTypes.append(Types.Shockwave)
spoiler.Reset()
itemsToPlace = [item for item in itemsToPlace if item not in placedMoves]
unplaced = PlaceItems(
spoiler,
FillAlgorithm.assumed,
itemsToPlace,
ItemPool.GetItemsNeedingToBeAssumed(spoiler.settings, placedTypes, placedItems),
)
if unplaced > 0:
# debug code - outputs all preplaced and shared items in an attempt to find where things are going wrong
locationsAndMoves = {}
emptyShops = []
emptySharedShops = []
for locationId in spoiler.LocationList:
location = spoiler.LocationList[locationId]
if location.item is not None and location.item != Items.NoItem and location.item <= Items.CameraAndShockwave:
locationsAndMoves[locationId] = location.item
if location.type == Types.Shop and location.item is None:
emptyShops.append(location)
if locationId in SharedMoveLocations:
emptySharedShops.append(location)
raise Ex.ItemPlacementException(str(unplaced) + " unplaced items.")
def FillWorld(spoiler: Spoiler) -> None:
"""Fill all locations with Kongs, moves, items, and etc."""
# Level order rando may have to affect the progression to be fillable - no logic doesn't care about your silly progression, however
wipe_progression = spoiler.settings.shuffle_loading_zones != ShuffleLoadingZones.all and spoiler.settings.logic_type != LogicType.nologic
retries = 0
error_log = []
while 1:
try:
if wipe_progression:
# Assume we can progress through the levels so long as we have enough kongs
spoiler.settings.kongs_for_progression = True
WipeBLockerRequirements(spoiler.settings)
# If we're in CLO and keys are not in the pool, don't wipe boss requirements
if not (spoiler.settings.hard_level_progression and spoiler.settings.shuffle_items and Types.Key in spoiler.settings.shuffled_location_types):
WipeBossRequirements(spoiler.settings)
# To aid in finding these locations, treat Rareware Coin and Rareware GB as being ~15-20% more expensive for fill purposes (unless it's already very expensive)
spoiler.settings.medal_requirement = spoiler.settings.logical_medal_requirement
spoiler.settings.rareware_gb_fairies = spoiler.settings.logical_fairy_requirement
# Fill locations
Fill(spoiler)
if wipe_progression:
# Update progression requirements based on what is now accessible after all shuffles are done
if spoiler.settings.hard_level_progression:
SetNewProgressionRequirementsUnordered(spoiler)
else:
SetNewProgressionRequirements(spoiler)
# After setting B. Lockers and bosses, make sure the game is still 101%-able
spoiler.Reset()
if not GetAccessibleLocations(spoiler, [], SearchMode.CheckAllReachable):
print("Failed post-progression 101% check?")
raise Ex.GameNotBeatableException("Game not able to complete 101% after setting progression.")
# Once progression requirements updated, no longer assume we need kongs freed for level progression
spoiler.settings.kongs_for_progression = False
# Reset the adjustments made for fill purposes
spoiler.settings.medal_requirement = spoiler.settings.original_medal_requirement
spoiler.settings.rareware_gb_fairies = spoiler.settings.original_fairy_requirement
# Check if game is beatable
if not VerifyWorldWithWorstCoinUsage(spoiler):
raise Ex.GameNotBeatableException("Game potentially unbeatable after placing all items.")
return
except Ex.FillException as ex:
error_log.append(ex)
spoiler.Reset()
spoiler.ClearAllLocations()
retries += 1
if retries == 2:
js.postMessage("Fill failed, out of retries.")
raise ex
spoiler.settings.shuffle_prices(spoiler)
# We don't really reach this block anymore now that we moved this to the server
# Every 3rd fill, retry more aggressively by reshuffling level order, move prices, and starting location as applicable
if retries % 3 == 0:
js.postMessage("Retrying fill really hard. Tries: " + str(retries))
if spoiler.settings.random_starting_region:
spoiler.settings.RandomizeStartingLocation(spoiler)
if spoiler.settings.shuffle_loading_zones == ShuffleLoadingZones.levels: # TODO: Reshuffling LZR doesn't work yet, but it might be nice? Not sure how necessary it is
ShuffleExits.ShuffleExits(spoiler)
spoiler.UpdateExits()
else:
js.postMessage("Retrying fill. Tries: " + str(retries))
def GetAccessibleKongLocations(levels: list, ownedKongs: list):
"""Get all kong locations within the provided levels which are reachable by owned kongs."""
kongLocations = []
for level in levels:
if level == Levels.JungleJapes:
kongLocations.append(Locations.DiddyKong)
elif level == Levels.AngryAztec:
if Kongs.donkey in ownedKongs or Kongs.lanky in ownedKongs or Kongs.tiny in ownedKongs:
kongLocations.append(Locations.LankyKong)
if Kongs.diddy in ownedKongs:
kongLocations.append(Locations.TinyKong)
elif level == Levels.FranticFactory:
if Kongs.lanky in ownedKongs or Kongs.tiny in ownedKongs:
kongLocations.append(Locations.ChunkyKong)
return kongLocations
def WipeBLockerRequirements(settings: Settings) -> None:
"""Wipe out progression requirements to assume access through main 7 levels."""
for i in range(0, 7):
# Assume B.Locker amounts will be attainable for now
settings.BLockerEntryCount[i] = 0
# Iff Helm is shuffled amongst other levels, also wipe the Helm B. Locker
if settings.shuffle_helm_location:
settings.BLockerEntryCount[7] = 0
def WipeBossRequirements(settings: Settings) -> None:
"""Wipe out progression requirements to beat bosses in the main 7 levels."""
for i in range(0, 7):
# Assume T&S amounts will be attainable for now
settings.BossBananas[i] = 0
# The standard boss placement algorithm assumes the starting kong can beat all the bosses for now
# If we are plandoing bosses, then we can't undo boss choices here because they've already been made
if not settings.boss_plando:
settings.boss_kongs[i] = settings.starting_kong
settings.boss_maps[i] = Maps.GalleonBoss # This requires nothing, allowing the fill to proceed as normal
def SetNewProgressionRequirements(spoiler: Spoiler) -> None:
"""Set new progression requirements based on what is owned or accessible heading into each level."""
# Find for each level: # of accessible bananas, total GBs, owned kongs & owned moves
settings = spoiler.settings
coloredBananaCounts = []
# goldenBananaTotals = []
ownedKongs = {}
ownedMoves = {}
# Cap the B. Locker amounts based on a random fraction of accessible bananas & GBs
BLOCKER_MIN = 0.4
BLOCKER_MAX = 0.7
if settings.hard_blockers:
BLOCKER_MIN = 0.6
BLOCKER_MAX = 0.95
blocker_variable_mapping = {
0: settings.blocker_0,
1: settings.blocker_1,
2: settings.blocker_2,
3: settings.blocker_3,
4: settings.blocker_4,
5: settings.blocker_5,
6: settings.blocker_6,
7: settings.blocker_7,
}
blocker_value_projection = [0, 0, 0, 0, 0, 0, 0, 0]
blocker_item_projection = [item for item in settings.BLockerEntryItems]
# Get sphere 0 GB count
BlockAccessToLevel(settings, 0)
spoiler.Reset()
accessible = GetAccessibleLocations(spoiler, [], SearchMode.GetReachable)
# Find all items that could be our first B. Locker's requirement
accessibleItems = spoiler.LogicVariables.ItemCounts()
# In Chaos B. Lockers, we should try our best to avoid a 0
if settings.chaos_blockers and accessibleItems[blocker_item_projection[0]] == 0:
# Determine which items have been found and could be eligible for this door
eligibleTypes = [item for item in settings.blocker_limits.keys() if accessibleItems[item] > 0]
# There can be only one Bean Locker, P. Locker, and C.C. Locker so they are not eligible if it already exists
if BarrierItems.Bean in eligibleTypes and BarrierItems.Bean in blocker_item_projection:
eligibleTypes.remove(BarrierItems.Bean)
if BarrierItems.Pearl in eligibleTypes and BarrierItems.Pearl in blocker_item_projection:
eligibleTypes.remove(BarrierItems.Pearl)
if BarrierItems.CompanyCoin in eligibleTypes and BarrierItems.CompanyCoin in blocker_item_projection:
eligibleTypes.remove(BarrierItems.CompanyCoin)
# If there are no eligible items (most likely on B. Locker 1) then we'll have to settle for 0 GBs
if len(eligibleTypes) == 0:
blocker_item_projection[0] = BarrierItems.GoldenBanana
else:
blocker_item_projection[0] = spoiler.settings.random.choice(eligibleTypes)
blocker_value_projection[0] = min(1, accessibleItems[blocker_item_projection[0]]) # This should limit the first B. Locker to 1 item, no matter what it is
if not settings.chaos_blockers:
blocker_value_projection[0] = min(blocker_variable_mapping[0], blocker_value_projection[0])
# For each level, calculate the available moves and number of bananas
for level in range(1, 9):
thisLevel = GetLevelShuffledToIndex(level - 1)
# Block access to future levels
BlockAccessToLevel(settings, level + 1)
settings.BossBananas[thisLevel] = 1000 # also block this level's boss
# Set up the logic variables with the available locations and items
spoiler.Reset()
accessible = GetAccessibleLocations(spoiler, [], SearchMode.GetReachable)
# Save the available counts for this level
coloredBananaCounts.append(spoiler.LogicVariables.ColoredBananas[thisLevel])
# Calculate the available quantity of items for the B. Locker
accessibleItems = spoiler.LogicVariables.ItemCounts()
# Calculate the next level's B. Locker value based on what's available (if there is a next level)
if level < 8:
# In Chaos B. Lockers, we should try our best to avoid a 0
if settings.chaos_blockers and accessibleItems[blocker_item_projection[level]] == 0:
# Determine which items have been found and could be eligible for this door
eligibleTypes = [item for item in settings.blocker_limits.keys() if accessibleItems[item] > 0]
# There can be only one Bean Locker, P. Locker, and C.C. Locker so they are not eligible if it already exists
if BarrierItems.Bean in eligibleTypes and BarrierItems.Bean in blocker_item_projection:
eligibleTypes.remove(BarrierItems.Bean)
if BarrierItems.Pearl in eligibleTypes and BarrierItems.Pearl in blocker_item_projection:
eligibleTypes.remove(BarrierItems.Pearl)
if BarrierItems.CompanyCoin in eligibleTypes and BarrierItems.CompanyCoin in blocker_item_projection:
eligibleTypes.remove(BarrierItems.CompanyCoin)
# If there are no eligible items (staggeringly unlikely past level 1) then we'll have to settle for 0 GBs
if len(eligibleTypes) == 0:
blocker_item_projection[level] = BarrierItems.GoldenBanana
else:
blocker_item_projection[level] = spoiler.settings.random.choice(eligibleTypes)
blocker_value_projection[level] = max(1, round(spoiler.settings.random.uniform(BLOCKER_MIN, BLOCKER_MAX) * accessibleItems[blocker_item_projection[level]]))
# If we're on Chaos B. Lockers, we need a random value to compare against so we don't only follow the item availability heuristic - if we did, we'd get really expensive B. Lockers
if settings.chaos_blockers:
# Roll 8 random values and take the levelth one to get an approximation of what the levelth most expensive random B. Locker might be if all of them were of this item
# This is functionally equivalent to what non-chaos B. Lockers does with GBs during settings initialization (blocker_0, blocker_1, etc.)
# This also prevents the item availability-based values from overtaking the maximum value
assorted_random_values = []
for i in range(8):
assorted_random_values.append(spoiler.settings.random.randint(1, ceil(settings.blocker_limits[blocker_item_projection[level]] * settings.chaos_ratio)))
assorted_random_values.sort()
blocker_value_projection[level] = min(assorted_random_values[level], blocker_value_projection[level])
# If we're not on Chaos B. Lockers we need to respect the UI input or the randomly generated value from earlier so the item availability calc doesn't overtake the max
else:
blocker_value_projection[level] = min(blocker_variable_mapping[level], blocker_value_projection[level])
ownedKongs[thisLevel] = spoiler.LogicVariables.GetKongs()
accessibleMoves = [
spoiler.LocationList[x].item
for x in accessible
if spoiler.LocationList[x].item != Items.NoItem
and spoiler.LocationList[x].item is not None
and ItemList[spoiler.LocationList[x].item].type in (Types.TrainingBarrel, Types.Shop, Types.Shockwave, Types.Climbing)
]
ownedMoves[thisLevel] = accessibleMoves
settings.BLockerEntryCount = blocker_value_projection
# Without Chaos B. Lockers, the last B. Locker is unchanged from what was generated earlier
if not settings.chaos_blockers:
settings.BLockerEntryCount[7] = settings.blocker_7
# With Chaos B. Lockers, we give the last level a the maximum value for that item proportional to the chaos ratio input
else:
settings.BLockerEntryCount[7] = ceil(settings.chaos_ratio * settings.blocker_limits[blocker_item_projection[7]])
settings.BLockerEntryItems = blocker_item_projection
# Prevent scenario where B. Lockers randomize to not-always-increasing values
if settings.randomize_blocker_required_amounts:
for i in range(1, 7):
for j in range(i + 1, 7):
# If any later level j is cheaper than this level i, swap the B. Lockers
# This will never break logic - if you could get into a more expensive level 3, you could get into an equally expensive level 4
# This only applies if the levels have the same item required by B. Locker
if settings.BLockerEntryItems[i] == settings.BLockerEntryItems[j] and settings.BLockerEntryCount[i] > settings.BLockerEntryCount[j]:
temp = settings.BLockerEntryCount[i]
settings.BLockerEntryCount[i] = settings.BLockerEntryCount[j]
settings.BLockerEntryCount[j] = temp
if settings.troff_max > 0:
settings.BossBananas = [
min(
settings.troff_0,
sum(coloredBananaCounts[0]),
round(settings.troff_0 / (settings.troff_max * settings.troff_weight_0) * sum(coloredBananaCounts[0])),
),
min(
settings.troff_1,
sum(coloredBananaCounts[1]),
round(settings.troff_1 / (settings.troff_max * settings.troff_weight_1) * sum(coloredBananaCounts[1])),
),
min(
settings.troff_2,
sum(coloredBananaCounts[2]),
round(settings.troff_2 / (settings.troff_max * settings.troff_weight_2) * sum(coloredBananaCounts[2])),
),
min(
settings.troff_3,
sum(coloredBananaCounts[3]),
round(settings.troff_3 / (settings.troff_max * settings.troff_weight_3) * sum(coloredBananaCounts[3])),
),
min(
settings.troff_4,
sum(coloredBananaCounts[4]),
round(settings.troff_4 / (settings.troff_max * settings.troff_weight_4) * sum(coloredBananaCounts[4])),
),
min(
settings.troff_5,
sum(coloredBananaCounts[5]),
round(settings.troff_5 / (settings.troff_max * settings.troff_weight_5) * sum(coloredBananaCounts[5])),
),
min(
settings.troff_6,
sum(coloredBananaCounts[6]),
round(settings.troff_6 / (settings.troff_max * settings.troff_weight_6) * sum(coloredBananaCounts[6])),
),
min(
settings.troff_7,
sum(coloredBananaCounts[7]),
round(settings.troff_7 / (settings.troff_max * settings.troff_weight_7) * sum(coloredBananaCounts[7])),
),
]
else:
settings.BossBananas = [0, 0, 0, 0, 0, 0, 0, 0]
# Update values based on actual level progression
ShuffleExits.UpdateLevelProgression(settings)
# We only need to shuffle bosses based on these calculated items if they aren't placed already
if not settings.boss_plando:
ShuffleBossesBasedOnOwnedItems(spoiler, ownedKongs, ownedMoves)
settings.owned_kongs_by_level = ownedKongs
settings.owned_moves_by_level = ownedMoves
def SetNewProgressionRequirementsUnordered(spoiler: Spoiler) -> None:
"""Set level progression requirements based on a random path of accessible levels."""
settings = spoiler.settings
isKeyItemRando = settings.shuffle_items and Types.Key in settings.shuffled_location_types
ownedKongs = {}
ownedMoves = {}
allMoves = ItemPool.DonkeyMoves.copy()
allMoves.extend(ItemPool.DiddyMoves)
allMoves.extend(ItemPool.LankyMoves)
allMoves.extend(ItemPool.TinyMoves)
allMoves.extend(ItemPool.ChunkyMoves)
allMoves.extend(ItemPool.ImportantSharedMoves)
allMoves.extend(ItemPool.TrainingBarrelAbilities())
allMoves.extend(ItemPool.ClimbingAbilities())
KeyEvents = [
Events.JapesKeyTurnedIn,
Events.AztecKeyTurnedIn,
Events.FactoryKeyTurnedIn,
Events.GalleonKeyTurnedIn,
Events.ForestKeyTurnedIn,
Events.CavesKeyTurnedIn,
Events.CastleKeyTurnedIn,
Events.HelmKeyTurnedIn,
]
# Limit the B. Locker amounts based on a fraction of the accessible GBs
BLOCKER_MIN = 0.4
BLOCKER_MAX = 0.7
if settings.hard_blockers:
BLOCKER_MIN = 0.6
BLOCKER_MAX = 0.95
# Before doing anything else, determine how many GBs we can access without entering any levels
# This is likely to be 1, but depending on the settings there are pretty good odds more are available
BlockAccessToLevel(settings, 0)
spoiler.Reset()
accessible = GetAccessibleLocations(spoiler, [], SearchMode.GetReachable)
runningGBTotal = spoiler.LogicVariables.GoldenBananas
# Reset B. Lockers and T&S to initial values
settings.BLockerEntryCount = [
settings.blocker_0,
settings.blocker_1,
settings.blocker_2,
settings.blocker_3,
settings.blocker_4,
settings.blocker_5,
settings.blocker_6,
settings.blocker_7,
]
settings.BossBananas = [
settings.troff_0,
settings.troff_1,
settings.troff_2,
settings.troff_3,
settings.troff_4,
settings.troff_5,
settings.troff_6,
settings.troff_7,
]
if settings.randomize_blocker_required_amounts or settings.chaos_blockers: # If amounts are random, they need to be maxed out to properly generate random values
settings.BLockerEntryCount = [1000, 1000, 1000, 1000, 1000, 1000, 1000, settings.blocker_7]
# If Helm is shuffled with the rest of the levels, this algorithm should settle its value
if settings.shuffle_helm_location:
settings.BLockerEntryCount[7] = 1000
# Chaos B. Lockers will be determined as we arrive at them - blank all of them out except for Helm for now
if settings.chaos_blockers:
settings.BLockerEntryItems[0] = BarrierItems.GoldenBanana
settings.BLockerEntryItems[1] = BarrierItems.GoldenBanana
settings.BLockerEntryItems[2] = BarrierItems.GoldenBanana
settings.BLockerEntryItems[3] = BarrierItems.GoldenBanana
settings.BLockerEntryItems[4] = BarrierItems.GoldenBanana
settings.BLockerEntryItems[5] = BarrierItems.GoldenBanana
settings.BLockerEntryItems[6] = BarrierItems.GoldenBanana
if settings.shuffle_helm_location:
settings.BLockerEntryItems[7] = BarrierItems.GoldenBanana
else:
# If guaranteed to be 8, Helm will be a max roll of a random item
settings.BLockerEntryCount[7] = ceil(settings.blocker_limits[settings.BLockerEntryItems[7]] * settings.chaos_ratio)
# Add a buffer to the Helm B. Locker value if it's your last B. Locker. This should make it less likely you need every item available to enter Helm.
# This protection does not apply if your last B. Locker could be a huge number. You'll have to face the music then.
settings.BLockerEntryCount[7] = min(
ceil(settings.BLockerEntryCount[7] / BLOCKER_MAX), max(settings.BLockerEntryCount[7], ceil(settings.blocker_limits[settings.BLockerEntryItems[7]] * BLOCKER_MAX))
)
# We also need to remember T&S values in an array as we'll overwrite the settings value in the process of determining location availability
initialTNS = [
settings.troff_0,
settings.troff_1,
settings.troff_2,
settings.troff_3,
settings.troff_4,
settings.troff_5,
settings.troff_6,
settings.troff_7,
]
# Reshuffle these values to the correct level index
ShuffleExits.UpdateLevelProgression(settings)
maximumMinRoll = round((settings.blocker_max / BLOCKER_MAX) * BLOCKER_MIN)
levelsProgressed = []
foundProgressionKeyEvents = []
number_of_progressable_levels = 8 if settings.shuffle_helm_location else 7
# Until we've completed every level...
while len(levelsProgressed) < number_of_progressable_levels:
openLevels = GetAccessibleOpenLevels(spoiler)
if not settings.chaos_blockers:
# Pick a random accessible B. Locker
maxEnterableBlocker = round(runningGBTotal * BLOCKER_MAX)
accessibleIncompleteLevels = [level for level in openLevels if level not in levelsProgressed and settings.BLockerEntryCount[level] <= maxEnterableBlocker]
# If we have no levels accessible, we need to lower a B. Locker count to make one accessible
if len(accessibleIncompleteLevels) == 0:
openUnprogressedLevels = [level for level in openLevels if level not in levelsProgressed]
if len(openUnprogressedLevels) == 0:
raise Ex.FillException("E1: Hard level order shuffler failed to progress through levels.")
# Next level chosen randomly (possible room for improvement here?) from accessible levels
nextLevelToBeat = spoiler.settings.random.choice(openUnprogressedLevels)
# If the level still isn't accessible, we have to truncate the required amount
if settings.BLockerEntryCount[nextLevelToBeat] > maxEnterableBlocker:
# Each B. Locker must be greater than the previous one and at least a specified percentage of available GBs
highroll = min(settings.blocker_max, maxEnterableBlocker) # Max max roll vs max progression roll
lowroll = min(maximumMinRoll, round(runningGBTotal * BLOCKER_MIN)) # Max min roll vs min progression roll
if lowroll > highroll: # I think this impossible? It probably takes insane rng and very specific numbers
lowroll = highroll
settings.BLockerEntryCount[nextLevelToBeat] = spoiler.settings.random.randint(lowroll, highroll)
accessibleIncompleteLevels = [nextLevelToBeat]
else:
nextLevelToBeat = spoiler.settings.random.choice(accessibleIncompleteLevels)
# If this is the last level in a world where Helm is shuffled into the order and we need to maximize the last B. Locker...
if settings.maximize_helm_blocker and settings.shuffle_helm_location and len(levelsProgressed) == (number_of_progressable_levels - 1):
# Set the B. Locker value equal to the cap. If we aren't shuffling Helm, we'll sort it out at the end.
settings.BLockerEntryCount[nextLevelToBeat] = min(settings.blocker_max, maxEnterableBlocker)
# Chaos B. Lockers will always have to update the B. Locker
else:
accessibleIncompleteLevels = [level for level in openLevels if level not in levelsProgressed]
if len(accessibleIncompleteLevels) == 0:
raise Ex.FillException("E1-C: Hard level order shuffler failed to progress through levels.")
nextLevelToBeat = spoiler.settings.random.choice(accessibleIncompleteLevels)
# In CLO, we always recalculate the B. Locker items
# Calculate the available quantity of the item for the B. Locker
accessibleItems = spoiler.LogicVariables.ItemCounts()
# In Chaos B. Lockers, we should try our best to avoid a 0
# Determine which items have been found and could be eligible for this door
eligibleTypes = [item for item in settings.blocker_limits.keys() if accessibleItems[item] > 0]
# There can be only one Bean Locker, P. Locker, and C.C. Locker so they are not eligible if it already exists
if BarrierItems.Bean in eligibleTypes and BarrierItems.Bean in settings.BLockerEntryItems:
eligibleTypes.remove(BarrierItems.Bean)
if BarrierItems.Pearl in eligibleTypes and BarrierItems.Pearl in settings.BLockerEntryItems:
eligibleTypes.remove(BarrierItems.Pearl)
if BarrierItems.CompanyCoin in eligibleTypes and BarrierItems.CompanyCoin in settings.BLockerEntryItems:
eligibleTypes.remove(BarrierItems.CompanyCoin)
# If there are no eligible items (staggeringly unlikely past the first level) then we'll have to settle for 0 GBs
if len(eligibleTypes) == 0:
settings.BLockerEntryItems[nextLevelToBeat] = BarrierItems.GoldenBanana
progression_roll = 0
else:
settings.BLockerEntryItems[nextLevelToBeat] = spoiler.settings.random.choice(eligibleTypes)
progression_roll = max(1, round(spoiler.settings.random.uniform(BLOCKER_MIN, BLOCKER_MAX) * accessibleItems[settings.BLockerEntryItems[nextLevelToBeat]]))
# If this is the last level in a world where Helm is shuffled into the order, set the B. Locker value equal to the cap. If we aren't shuffling Helm, we'll sort it out at the end.
if settings.shuffle_helm_location and len(levelsProgressed) == (number_of_progressable_levels - 1):
# This will likely be a max roll but in some rare circumstances it may not be due to needing something out of your last progression level extremely early
settings.BLockerEntryCount[nextLevelToBeat] = min(
ceil(settings.blocker_limits[settings.BLockerEntryItems[nextLevelToBeat]] * settings.chaos_ratio), round(BLOCKER_MAX * accessibleItems[settings.BLockerEntryItems[nextLevelToBeat]])
)
# Otherwise, generate a random value
else:
# Roll 8 random values and take the nth one to get an approximation of what the nth most expensive random B. Locker might be if all of them were of this item
# n in this scenario is the nth level to be entered
# This also prevents the item availability-based values from overtaking the maximum value
assorted_random_values = []
for i in range(9):
assorted_random_values.append(spoiler.settings.random.randint(1, ceil(settings.blocker_limits[settings.BLockerEntryItems[nextLevelToBeat]] * settings.chaos_ratio)))
assorted_random_values.sort()
settings.BLockerEntryCount[nextLevelToBeat] = min(progression_roll, assorted_random_values[len(levelsProgressed)])
levelsProgressed.append(nextLevelToBeat)
# Determine the Kong, GB, and Move accessibility from this level
# If we get keys (and thus level progression) from the boss...
if not isKeyItemRando:
# Block the ability to complete the boss of every level we could complete but haven't yet (including this one)
# This allows logic to get items from any other accessible level to beat this one
BlockCompletionOfLevelSet(settings, accessibleIncompleteLevels)
spoiler.Reset()
accessible = GetAccessibleLocations(spoiler, [], SearchMode.GetReachable)
runningGBTotal = spoiler.LogicVariables.GoldenBananas
# -------------------------------------------------------------------------------------------------------------------------------------------
# This chunk of code is here if we need to lower T&S values for whatever reason. This was the original attempt for CLO + item rando w/ keys.
# This is no longer believed to be necessary, but preserved here in case we need to revert because it mostly worked.
# -------------------------------------------------------------------------------------------------------------------------------------------
# If at any moment we can get keys, let's see if we found any here
# if isKeyItemRando:
# # Until we know a new level is accessible...
# while 1:
# openLevels = GetAccessibleOpenLevels(spoiler)
# # If we haven't found all the levels and have progressed through all open levels, we need to lower the CB requirement of one or more bosses for progression
# if len(openLevels) < 7 and len(openLevels) == len(levelsProgressed):
# bossLocations = [location for id, location in spoiler.LocationList.items() if location.type == Types.Key and location.level in levelsProgressed]
# shuffle(bossLocations)
# priorityBossLocation = None
# priorityStrength = -1
# # Loop through the boss locations, looking for the most likely progression candidate
# for bossLocation in bossLocations:
# # If this location has nothing, don't even pretend to consider it
# if bossLocation.item is None or bossLocation.item == Items.NoItem:
# continue
# # If this one is already reachable, skip
# availableCBs = sum(spoiler.LogicVariables.ColoredBananas[bossLocation.level])
# if availableCBs < settings.BossBananas[bossLocation.level]: # Note we track against current values so we take into account already-lowered ones
# # Absolute top priority for boss rewards is barrels - this can lock other bosses
# if bossLocation.item == Items.Barrels:
# priorityBossLocation = bossLocation
# priorityStrength = 1000
# # Next up is Keys - these can directly lock lobbies
# itemOnBoss = ItemList[bossLocation.item]
# if itemOnBoss.type == Types.Key and priorityStrength < 100:
# priorityBossLocation = bossLocation
# priorityStrength = 100
# # Next up is Swim - if this is shuffled it locks a lobby
# if bossLocation.item == Items.Swim and priorityStrength < 99:
# priorityBossLocation = bossLocation
# priorityStrength = 99
# # Next up is Vines - if this is shuffled it sometimes locks a lobby but is also often locking a lot of things
# if bossLocation.item == Items.Vines and priorityStrength < 98:
# priorityBossLocation = bossLocation
# priorityStrength = 98
# # Next up is Guns/Instruments - these are more likely to lock Kongs which unlock Keys
# if bossLocation.item in ItemPool.Guns(settings) or bossLocation.item in ItemPool.Instruments(settings):
# priorityBossLocation = bossLocation
# priorityStrength = 50
# # Other boss rewards of interest would be moves with no particular priority
# elif itemOnBoss.type == Types.Shop and priorityStrength < 10:
# priorityBossLocation = bossLocation
# priorityStrength = 10
# # Very low priority reward moves are Oranges and Shockwave/Camera
# elif itemOnBoss.type in (Types.TrainingBarrel, Types.Shockwave) and priorityStrength < 9:
# priorityBossLocation = bossLocation
# priorityStrength = 9
# # Zero priority rewards is basically everything else
# elif priorityStrength < 0:
# priorityBossLocation = bossLocation
# priorityStrength = 0
# # The rest won't be locking progression so don't need to be lowered
# if priorityBossLocation is None:
# # If we've already lowered all the T&S we can, then that's a fill error
# raise Ex.FillException("E2: Hard level order shuffler failed to progress through levels.")
# randomlyRolledRatio = initialTNS[priorityBossLocation.level] / settings.troff_max
# settings.BossBananas[priorityBossLocation.level] = round(availableCBs * randomlyRolledRatio)
# accessibleMoves = [
# spoiler.LocationList[x].item
# for x in accessible
# if spoiler.LocationList[x].item != Items.NoItem
# and spoiler.LocationList[x].item is not None
# and ItemList[spoiler.LocationList[x].item].type in (Types.TrainingBarrel, Types.Shop, Types.Shockwave)
# ]
# if priorityBossLocation.item in accessibleMoves:
# accessibleMoves.remove(priorityBossLocation.item)
# ownedMoves[priorityBossLocation.level] = accessibleMoves
# ownedKongs[priorityBossLocation.level] = spoiler.LogicVariables.GetKongs()
# # Now that this boss location is accessible, let's see what's new and then repeat this loop in case we didn't find a new key
# spoiler.Reset()
# accessible = GetAccessibleLocations(spoiler, [], SearchMode.GetReachable)
# else:
# # To break out of this loop, we either have a level we can progress to or we've just found all the levels
# break
# If we acquire keys in the traditional way, we go get this level's boss key
if not isKeyItemRando:
# Determine if the level we picked was a level progression key
if not settings.open_lobbies:
lobbyIndex = -1
for key in settings.level_order.keys():
if settings.level_order[key] == nextLevelToBeat:
lobbyIndex = key - 1
break
foundKeyEvent = KeyEvents[lobbyIndex]
# If we need this key to open new lobbies, it's a progression key
if foundKeyEvent in settings.krool_keys_required and foundKeyEvent != Events.FactoryKeyTurnedIn:
# If Helm is shuffled among other levels, Keys 6 and 7 become progression keys
if settings.shuffle_helm_location or foundKeyEvent not in [
Events.CavesKeyTurnedIn,
Events.CastleKeyTurnedIn,
]:
foundProgressionKeyEvents.append(foundKeyEvent)
eligibleProgressionKeyEvents = [event for event in foundProgressionKeyEvents]
# Keys 6 and 7 are useless in isolation - they aren't progression unless you have both
if Events.CavesKeyTurnedIn in eligibleProgressionKeyEvents and Events.CastleKeyTurnedIn not in eligibleProgressionKeyEvents:
eligibleProgressionKeyEvents.remove(Events.CavesKeyTurnedIn)
if Events.CastleKeyTurnedIn in eligibleProgressionKeyEvents and Events.CavesKeyTurnedIn not in eligibleProgressionKeyEvents:
eligibleProgressionKeyEvents.remove(Events.CastleKeyTurnedIn)
# If we've progressed through all open levels, then we need to pick a progression key we've found to acquire and set that level's Troff n Scoff
if len(openLevels) == len(levelsProgressed) and any(eligibleProgressionKeyEvents):
chosenKeyEvent = spoiler.settings.random.choice(eligibleProgressionKeyEvents)
foundProgressionKeyEvents.remove(chosenKeyEvent)
# Determine what levels need to be completed
levelsToCompleteBoss = []
if chosenKeyEvent == Events.JapesKeyTurnedIn:
spoiler.LogicVariables.Events.append(Events.JapesKeyTurnedIn)
levelsToCompleteBoss.append(settings.level_order[1])
elif chosenKeyEvent == Events.AztecKeyTurnedIn:
spoiler.LogicVariables.Events.append(Events.AztecKeyTurnedIn)
levelsToCompleteBoss.append(settings.level_order[2])
elif chosenKeyEvent == Events.GalleonKeyTurnedIn:
spoiler.LogicVariables.Events.append(Events.GalleonKeyTurnedIn)
levelsToCompleteBoss.append(settings.level_order[4])
elif chosenKeyEvent == Events.ForestKeyTurnedIn:
spoiler.LogicVariables.Events.append(Events.ForestKeyTurnedIn)
levelsToCompleteBoss.append(settings.level_order[5])
elif chosenKeyEvent in [Events.CavesKeyTurnedIn, Events.CastleKeyTurnedIn]:
spoiler.LogicVariables.Events.append(Events.CavesKeyTurnedIn)
spoiler.LogicVariables.Events.append(Events.CastleKeyTurnedIn)
levelsToCompleteBoss.append(settings.level_order[6])
levelsToCompleteBoss.append(settings.level_order[7])
for bossCompletedLevel in levelsToCompleteBoss:
availableCBs = sum(spoiler.LogicVariables.ColoredBananas[bossCompletedLevel])
# If we don't have enough CBs to beat the boss per the settings-determined value
if availableCBs < initialTNS[bossCompletedLevel]:
# Reduce the requirement to an amount guaranteed to be available, based on the ratio of the initial T&S roll
randomlyRolledRatio = initialTNS[bossCompletedLevel] / settings.troff_max
settings.BossBananas[bossCompletedLevel] = round(availableCBs * randomlyRolledRatio)
else:
settings.BossBananas[bossCompletedLevel] = initialTNS[bossCompletedLevel]
ownedKongs[bossCompletedLevel] = spoiler.LogicVariables.GetKongs()
accessibleMoves = [
spoiler.LocationList[x].item
for x in accessible
if spoiler.LocationList[x].item != Items.NoItem
and spoiler.LocationList[x].item is not None
and ItemList[spoiler.LocationList[x].item].type in (Types.TrainingBarrel, Types.Shop, Types.Shockwave, Types.Climbing)
]
ownedMoves[bossCompletedLevel] = accessibleMoves
# After unblocking at least one T&S, the next loop needs the logic variables to know new lobbies are accessible
# We've now made the key on this boss accessible, so this iteration should be identical plus keys from the unblocked bosses
spoiler.Reset()
GetAccessibleLocations(spoiler, [], SearchMode.GetReachable)
# For any boss location behind a T&S we didn't lower...
bossLocations = [
location
for id, location in spoiler.LocationList.items()
if location.type == Types.Key and location.level in levelsProgressed and location.level != Levels.HideoutHelm and settings.BossBananas[location.level] >= initialTNS[location.level]
]
for bossLocation in bossLocations:
# For any level we explicitly blocked, undo the blocking
if settings.BossBananas[bossLocation.level] > 500:
# We should have access to everything by this point
settings.BossBananas[bossLocation.level] = initialTNS[bossLocation.level]
# For any level we haven't lowered yet, assume we own everything
if bossLocation.level not in ownedKongs.keys():
ownedKongs[bossLocation.level] = [Kongs.donkey, Kongs.diddy, Kongs.lanky, Kongs.tiny, Kongs.chunky]
ownedMoves[bossLocation.level] = allMoves
# -------------------------------------------------------------------------------------------------------------------------------------------
# This chunk of code is here if we need to lower T&S values for whatever reason. This was the original attempt for CLO + item rando w/ keys.
# This is no longer believed to be necessary, but preserved here in case we need to revert because it mostly worked.
# -------------------------------------------------------------------------------------------------------------------------------------------
# If boss rewards could be anything, we have to make sure they're accessible independent of all else
# if isKeyItemRando:
# bossReward = bossLocation.item
# # If the boss reward doesn't contain progression, it's fine
# if bossReward is None or ItemList[bossReward].type not in (Types.TrainingBarrel, Types.Shop, Types.Shockwave, Types.Key):
# continue
# # You never have the boss reward when fighting it, so remove it from consideration for boss placement
# if bossReward in ownedMoves[bossLocation.level]:
# ownedMoves[bossLocation.level].remove(bossReward)
# # If it could contain progression, place a dummy item there and see if we can reach it
# bossLocation.PlaceItem(spoiler, Items.TestItem)
# spoiler.Reset()
# accessible = GetAccessibleLocations(spoiler, [], SearchMode.GetReachable)
# if not spoiler.LogicVariables.found_test_item:
# # If we can't reach it eventually in this world state, then we need to lower this T&S
# randomlyRolledRatio = initialTNS[bossLocation.level] / settings.troff_max
# availableCBs = sum(spoiler.LogicVariables.ColoredBananas[bossLocation.level])
# settings.BossBananas[bossLocation.level] = round(availableCBs * randomlyRolledRatio)
# accessibleMoves = [
# spoiler.LocationList[x].item
# for x in accessible
# if spoiler.LocationList[x].item != Items.NoItem
# and spoiler.LocationList[x].item is not None
# and ItemList[spoiler.LocationList[x].item].type in (Types.TrainingBarrel, Types.Shop, Types.Shockwave)
# ]
# ownedMoves[bossLocation.level] = accessibleMoves
# ownedKongs[bossLocation.level] = spoiler.LogicVariables.GetKongs()
# # Put it back so we don't accidentally an item
# bossLocation.PlaceItem(spoiler, bossReward)
# If we're not shuffling Helm with the rest of the levels, we need to sort out its B. Locker value here
if not settings.shuffle_helm_location:
mostExpensiveBLocker = max(settings.BLockerEntryCount[0:7])
settings.BLockerEntryCount[7] = settings.blocker_7 # Reset the buffer that we applied earlier (Chaos B. Lockers is about to ignore this and that's good)
# Chaos B. Lockers needs to also update the Helm B. Locker - max roll whatever the item there is
if settings.chaos_blockers:
settings.BLockerEntryCount[7] = ceil(settings.blocker_limits[settings.BLockerEntryItems[7]] * settings.chaos_ratio)
# Because we might not have sorted the B. Lockers when they're randomly generated, Helm might be a surprisingly low number if it's not maximized
elif settings.randomize_blocker_required_amounts and not settings.maximize_helm_blocker and settings.BLockerEntryCount[7] < mostExpensiveBLocker:
# Ensure that Helm is the most expensive B. Locker
# This may raise the Helm B. Locker and in some ***rare*** scenarios violate the B. Locker buffer, but it'll be a relatively small violation.
settings.BLockerEntryCount[7] = spoiler.settings.random.randint(mostExpensiveBLocker, settings.blocker_max)
# Only if keys are shuffled off of bosses do we need to reshuffle the bosses
if not isKeyItemRando:
# Place boss locations based on kongs and moves found for each level
# We only need to shuffle bosses based on these calculated items if they aren't placed already
if not spoiler.settings.boss_plando:
ShuffleBossesBasedOnOwnedItems(spoiler, ownedKongs, ownedMoves)
settings.owned_kongs_by_level = ownedKongs
settings.owned_moves_by_level = ownedMoves
# After setting all the progression, make sure we did it right
# Technically the coin logic check after this will cover it, but this will help identify issues better
spoiler.Reset()
if not GetAccessibleLocations(spoiler, [], SearchMode.CheckAllReachable):
raise Ex.GameNotBeatableException("Complex progression generation prevented 101%.")
def GetAccessibleOpenLevels(spoiler: Spoiler) -> List[int]:
"""Return the list of levels (not lobbies) you have access to after running GetAccessibleLocations()."""
lobbyAccessEvents = [event for event in spoiler.LogicVariables.Events if event >= Events.JapesLobbyAccessed and event <= Events.HelmLobbyTraversable]
accessibleOpenLevels = []
if Events.JapesLobbyAccessed in lobbyAccessEvents:
accessibleOpenLevels.append(Levels.JungleJapes)
if Events.AztecLobbyAccessed in lobbyAccessEvents:
# Also make sure we can do anything in Aztec. BONUS: if your DK portal is random the odds are very high it's not behind the vines, so this will suffice.
if (
spoiler.LogicVariables.vines
or (spoiler.LogicVariables.tiny and spoiler.LogicVariables.twirl)
or spoiler.LogicVariables.CanPhase()
or spoiler.settings.dk_portal_location_rando_v2 != DKPortalRando.off
):
accessibleOpenLevels.append(Levels.AngryAztec)
if Events.FactoryLobbyAccessed in lobbyAccessEvents:
accessibleOpenLevels.append(Levels.FranticFactory)
if Events.GalleonLobbyAccessed in lobbyAccessEvents:
accessibleOpenLevels.append(Levels.GloomyGalleon)
if Events.ForestLobbyAccessed in lobbyAccessEvents:
accessibleOpenLevels.append(Levels.FungiForest)
if Events.CavesLobbyAccessed in lobbyAccessEvents:
accessibleOpenLevels.append(Levels.CrystalCaves)
if Events.CastleLobbyAccessed in lobbyAccessEvents:
accessibleOpenLevels.append(Levels.CreepyCastle)
# Not only do we need Helm Lobby access, we also need to make sure we can traverse it to logically enter it
if spoiler.settings.shuffle_helm_location and Events.HelmLobbyAccessed in lobbyAccessEvents and Events.HelmLobbyTraversable in lobbyAccessEvents:
accessibleOpenLevels.append(Levels.HideoutHelm)
return accessibleOpenLevels
def BlockAccessToLevel(settings: Settings, level: int) -> None:
"""Assume the level index passed in is the furthest level you have access to in the level order."""
for i in range(0, 8):
if i >= level - 1:
# This level and those after it are locked out
settings.BLockerEntryCount[i] = 1000
if i < 7:
settings.BossBananas[i] = 1000
else:
# Previous levels assumed accessible
settings.BLockerEntryCount[i] = 0
if i < 7:
settings.BossBananas[i] = 0
# Update values based on actual level progression
ShuffleExits.UpdateLevelProgression(settings)
def BlockCompletionOfLevelSet(settings: Settings, lockedLevels):
"""Prevent acquiring the keys of the levels provided."""
for i in range(0, 7):
if i in lockedLevels:
# This level's boss is incompletable
settings.BossBananas[i] = 1000
def Generate_Spoiler(spoiler: Spoiler) -> Tuple[bytes, Spoiler]:
"""Generate a complete spoiler based on input settings."""
# Check for settings incompatibilities
CheckForIncompatibleSettings(spoiler.settings)
if spoiler.settings.wrinkly_hints == WrinklyHints.fixed_racing:
ValidateFixedHints(spoiler.settings)
# Reset LocationList for a new fill
if not spoiler.settings.archipelago:
spoiler.ResetLocationList()
# Initiate kasplat map with default
spoiler.InitKasplatMap()
# Handle misc randomizations
ShuffleMisc(spoiler)
if spoiler.settings.archipelago:
return
# Handle Loading Zones - this will handle LO and LZR appropriately
if spoiler.settings.shuffle_loading_zones != ShuffleLoadingZones.none:
ShuffleExits.ExitShuffle(spoiler)
spoiler.UpdateExits()
# Handle Item Fill
if spoiler.settings.move_rando != MoveRando.off or spoiler.settings.kong_rando or any(spoiler.settings.shuffled_location_types):
FillWorld(spoiler)
else:
# Just check if normal item locations are beatable with given settings
ItemPool.PlaceConstants(spoiler)
if not GetAccessibleLocations(spoiler, [], SearchMode.CheckBeatable):
raise Ex.VanillaItemsGameNotBeatableException("Game unbeatable.")
CorrectBossKongLocations(spoiler)
GeneratePlaythrough(spoiler)
compileMicrohints(spoiler)
if spoiler.settings.wrinkly_hints != WrinklyHints.off:
compileHints(spoiler)
if spoiler.settings.spoiler_hints != SpoilerHints.off:
compileSpoilerHints(spoiler)
spoiler.Reset()
ShuffleExits.Reset(spoiler)
spoiler.createJson()
js.postMessage("Patching ROM...")
# print(spoiler)
# print(spoiler.json)
patch_data, password = ApplyRandomizer.patching_response(spoiler)
return patch_data, spoiler, password
class ItemReference:
"""Class to store information regarding an item's location."""
def __init__(self, item: Items, item_name: str, locations):
"""Initialize with given parameters."""
self.item = item
self.item_name = item_name
self.locations = [locations] if isinstance(locations, str) else locations
def setLocation(self, index: int, new_name: str):
"""Set new name for location."""
self.locations[index] = new_name
def ShuffleMisc(spoiler: Spoiler) -> None:
"""Shuffle miscellaneous objects outside of main fill algorithm, including Kasplats, Bonus barrels, and bananaport warps."""
resetCustomLocations(spoiler)
ResetPorts()
if spoiler.settings.bananaport_placement_rando != ShufflePortLocations.off:
port_replacements = {}
port_human_replacements = {}
ShufflePorts(spoiler, port_replacements, port_human_replacements)
spoiler.warp_locations = port_replacements
spoiler.human_warps = port_human_replacements
if spoiler.settings.progressive_hint_item != ProgressiveHintItem.off:
SetProgressiveHintDoorLogic(spoiler)
# T&S and Wrinkly Door Shuffle
if spoiler.settings.vanilla_door_rando: # Includes Dos' Doors
ShuffleVanillaDoors(spoiler)
if spoiler.settings.dk_portal_location_rando_v2 != DKPortalRando.off:
ShuffleDoors(spoiler, True)
elif (
spoiler.settings.wrinkly_location_rando
or spoiler.settings.tns_location_rando
or spoiler.settings.remove_wrinkly_puzzles
or spoiler.settings.dk_portal_location_rando_v2 != DKPortalRando.off
or (spoiler.settings.progressive_hint_item != ProgressiveHintItem.off and Types.Hint in spoiler.settings.shuffled_location_types)
):
ShuffleDoors(spoiler, False)
if Types.Hint in spoiler.settings.shuffled_location_types:
UpdateDoorLevels(spoiler)
# Handle Crown Placement
if spoiler.settings.crown_placement_rando:
crown_replacements = {}
crown_human_replacements = {}
ShuffleCrowns(spoiler, crown_replacements, crown_human_replacements)
spoiler.crown_locations = crown_replacements
spoiler.human_crowns = dict(sorted(crown_human_replacements.items()))
# Handle Bananaports
if spoiler.settings.bananaport_rando in (BananaportRando.crossmap_coupled, BananaportRando.crossmap_decoupled):
replacements = []
human_replacements = {}
ShuffleWarpsCrossMap(
spoiler,
replacements,
human_replacements,
spoiler.settings.bananaport_rando == BananaportRando.crossmap_coupled,
spoiler.settings.warp_level_list_selected,
)
spoiler.bananaport_replacements = replacements.copy()
spoiler.human_warp_locations = human_replacements
LinkWarps(spoiler)
# Handle kasplats - this is the first VerifyWorld check, all shuffles affecting Locations must be before this one
KasplatShuffle(spoiler, spoiler.LogicVariables)
spoiler.human_kasplats = {}
spoiler.UpdateKasplats(spoiler.LogicVariables.kasplat_map)
# Enemy Rando
spoiler.enemy_rando_data = {}
spoiler.pkmn_snap_data = []
if spoiler.settings.enemy_rando:
randomize_enemies_0(spoiler)
# Handle bonus barrels
if (
spoiler.settings.bonus_barrels in (MinigameBarrels.random, MinigameBarrels.selected)
or spoiler.settings.helm_barrels == MinigameBarrels.random
or spoiler.settings.training_barrels_minigames == MinigameBarrels.random
):
BarrelShuffle(spoiler.settings)
spoiler.UpdateBarrels()
# CB Shuffle
if spoiler.settings.cb_rando_enabled:
ShuffleCBs(spoiler)
# Coin Shuffle
if spoiler.settings.coin_rando:
ShuffleCoins(spoiler)
# Random Patches
if spoiler.settings.random_patches:
human_patches = {}
spoiler.human_patches = ShufflePatches(spoiler, human_patches).copy()
if spoiler.settings.random_fairies:
ShuffleFairyLocations(spoiler)
if spoiler.settings.shuffle_shops:
ShuffleShopLocations(spoiler)
# Crate Shuffle
if spoiler.settings.random_crates:
human_crates = {}
spoiler.human_crates = ShuffleMelonCrates(spoiler, human_crates).copy()
# Populate location references
spoiler.location_references = [
# DK Moves
ItemReference(Items.BaboonBlast, "Baboon Blast", "DK Japes Cranky"),
ItemReference(Items.StrongKong, "Strong Kong", "DK Aztec Cranky"),
ItemReference(Items.GorillaGrab, "Gorilla Grab", "DK Factory Cranky"),
ItemReference(Items.Coconut, "Coconut Gun", "DK Japes Funky"),
ItemReference(Items.Bongos, "Bongo Blast", "DK Aztec Candy"),
# Diddy Moves
ItemReference(Items.ChimpyCharge, "Chimpy Charge", "Diddy Japes Cranky"),
ItemReference(Items.RocketbarrelBoost, "Rocketbarrel Boost", "Diddy Aztec Cranky"),
ItemReference(Items.SimianSpring, "Simian Spring", "Diddy Factory Cranky"),
ItemReference(Items.Peanut, "Peanut Popguns", "Diddy Japes Funky"),
ItemReference(Items.Guitar, "Guitar Gazump", "Diddy Aztec Candy"),
# Lanky Moves
ItemReference(Items.Orangstand, "Orangstand", "Lanky Japes Cranky"),
ItemReference(Items.BaboonBalloon, "Baboon Balloon", "Lanky Factory Cranky"),
ItemReference(Items.OrangstandSprint, "Orangstand Sprint", "Lanky Caves Cranky"),
ItemReference(Items.Grape, "Grape Shooter", "Lanky Japes Funky"),
ItemReference(Items.Trombone, "Trombone Tremor", "Lanky Aztec Candy"),
# Tiny Moves
ItemReference(Items.MiniMonkey, "Mini Monkey", "Tiny Japes Cranky"),
ItemReference(Items.PonyTailTwirl, "Pony Tail Twirl", "Tiny Factory Cranky"),
ItemReference(Items.Monkeyport, "Monkeyport", "Tiny Caves Cranky"),
ItemReference(Items.Feather, "Feather Bow", "Tiny Japes Funky"),
ItemReference(Items.Saxophone, "Saxophone Slam", "Tiny Aztec Candy"),
# Chunky Moves
ItemReference(Items.HunkyChunky, "Hunky Chunky", "Chunky Japes Cranky"),
ItemReference(Items.PrimatePunch, "Primate Punch", "Chunky Factory Cranky"),
ItemReference(Items.GorillaGone, "Gorilla Gone", "Chunky Caves Cranky"),
ItemReference(Items.Pineapple, "Pineapple Launcher", "Chunky Japes Funky"),
ItemReference(Items.Triangle, "Triangle Trample", "Chunky Aztec Candy"),
# Gun Upgrades
ItemReference(Items.HomingAmmo, "Homing Ammo", "Shared Forest Funky"),
ItemReference(Items.SniperSight, "Sniper Scope", "Shared Castle Funky"),
ItemReference(Items.ProgressiveAmmoBelt, "Progressive Ammo Belt", ["Shared Factory Funky", "Shared Caves Funky"]),
ItemReference(Items.Camera, "Fairy Camera", "Banana Fairy Gift"),
ItemReference(Items.Shockwave, "Shockwave", "Banana Fairy Gift"),
# Basic Moves
ItemReference(Items.Swim, "Diving", "Dive Barrel"),
ItemReference(Items.Oranges, "Orange Throwing", "Orange Barrel"),
ItemReference(Items.Barrels, "Barrel Throwing", "Barrel Barrel"),
ItemReference(Items.Vines, "Vine Swinging", "Vine Barrel"),
ItemReference(Items.Climbing, "Climbing", "Starting Move"),
# Instrument Upgrades & Slams
ItemReference(
Items.ProgressiveInstrumentUpgrade,
"Progressive Instrument Upgrade",
["Shared Galleon Candy", "Shared Caves Candy", "Shared Castle Candy"],
),
ItemReference(
Items.ProgressiveSlam,
"Progressive Slam",
["Shared Isles Cranky", "Shared Forest Cranky", "Shared Castle Cranky"],
),
# Kongs
ItemReference(Items.Donkey, "Donkey Kong", "Starting Kong"),
ItemReference(Items.Diddy, "Diddy Kong", "Japes Diddy Cage"),
ItemReference(Items.Lanky, "Lanky Kong", "Llama Lanky Cage"),
ItemReference(Items.Tiny, "Tiny Kong", "Aztec Tiny Cage"),
ItemReference(Items.Chunky, "Chunky Kong", "Factory Chunky Cage"),
# Shopkeepers
ItemReference(Items.Cranky, "Cranky Kong", "Starting Item"),
ItemReference(Items.Candy, "Candy Kong", "Starting Item"),
ItemReference(Items.Funky, "Funky Kong", "Starting Item"),
ItemReference(Items.Snide, "Snide", "Starting Item"),
# Early Keys
ItemReference(Items.JungleJapesKey, "Key 1", "Starting Key"),
ItemReference(Items.AngryAztecKey, "Key 2", "Starting Key"),
ItemReference(Items.FranticFactoryKey, "Key 3", "Starting Key"),
ItemReference(Items.GloomyGalleonKey, "Key 4", "Starting Key"),
# Late Keys
ItemReference(Items.FungiForestKey, "Key 5", "Starting Key"),
ItemReference(Items.CrystalCavesKey, "Key 6", "Starting Key"),
ItemReference(Items.CreepyCastleKey, "Key 7", "Starting Key"),
ItemReference(Items.HideoutHelmKey, "Key 8", "Starting Key"),
]
# Item Rando
spoiler.human_item_assignment = {}
spoiler.settings.update_valid_locations(spoiler)
def ValidateFixedHints(settings: Settings) -> None:
"""Check for some known incompatibilities with the Fixed hint system ASAP so we don't waste time genning this seed."""
if settings.logic_type == LogicType.nologic:
raise Ex.SettingsIncompatibleException("No Logic is not compatible with fixed hints.")
if not settings.shuffle_items:
raise Ex.SettingsIncompatibleException("Item Randomizer must be enabled with Fixed hints.")
if settings.win_condition_item != WinConditionComplex.beat_krool:
raise Ex.SettingsIncompatibleException("Alternate win conditions will not work with Fixed hints.")
if len(settings.starting_kong_list) != 2:
raise Ex.SettingsIncompatibleException("Fixed hints require starting with exactly 2 Kongs.")
if settings.enable_plandomizer and len(settings.plandomizer_dict["hints"]) > 5:
raise Ex.SettingsIncompatibleException("Fixed hints are incompatible with more than 5 plandomized hints.")
def CheckForIncompatibleSettings(settings: Settings) -> None:
"""Check for known settings conflicts and throw an exception immediately."""
found_incompatibilities = ""
if not settings.fast_start_beginning_of_game:
if settings.shuffle_loading_zones == ShuffleLoadingZones.all:
found_incompatibilities += "Cannot turn off Fast Start with Loading Zones Randomized. "
if settings.random_starting_region:
found_incompatibilities += "Cannot turn off Fast Start with a Random Starting Location. "
if not settings.start_with_slam:
found_incompatibilities += "Cannot turn off Fast Start unless you are guaranteed to start with a Progressive Slam. "
if Types.Cranky in settings.shuffled_location_types:
found_incompatibilities += "Cannot turn off Fast start unless you start with Cranky. "
if not settings.shuffle_items:
if not settings.start_with_slam:
found_incompatibilities += "Cannot turn off Item Randomizer without starting with a Progressive Slam. "
if settings.training_barrels != TrainingBarrels.normal:
found_incompatibilities += "Cannot turn off Item Randomizer without starting with all Training Moves. "
if settings.climbing_status != ClimbingStatus.normal:
found_incompatibilities += "Cannot turn off Item Randomizer without starting with Climbing. "
if IsItemSelected(settings.hard_mode, settings.hard_mode_selected, HardModeSelected.water_is_lava, False):
if settings.no_healing:
found_incompatibilities += "Cannot turn on 'Water is Lava' whilst disabling healing. "
if IsItemSelected(settings.hard_mode, settings.hard_mode_selected, HardModeSelected.angry_caves, False):
if settings.perma_death:
if settings.damage_amount == DamageAmount.quad or settings.damage_amount == DamageAmount.ohko:
found_incompatibilities += "Cannot turn on 'Angry Caves' with a damage modifier higher than double damage with Irondonk enabled. "
if not settings.is_valid_item_pool():
found_incompatibilities += "Item pool is not a valid combination of items and cannot successfully fill the world. "
if settings.krool_access and Items.HideoutHelmKey in settings.starting_keys_list_selected:
found_incompatibilities += "Cannot start with Key 8 and guarantee Key 8 to be required at the same time. "
if found_incompatibilities != "":
raise Ex.SettingsIncompatibleException(found_incompatibilities)
def DebugCheckAllReachable(spoiler: Spoiler, owned, what_just_got_placed):
"""Immediately check if the world is 101%-able. Only used with extreme_debugging."""
spoiler.Reset()
reached_all = GetAccessibleLocations(spoiler, owned, SearchMode.CheckAllReachable)
if not reached_all:
print("red alert - we have just lost 101% after placing " + what_just_got_placed + "!")