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

448 lines
25 KiB
Python

"""This file contains the logic for creating and connecting regions in the Donkey Kong 64 world."""
import typing
from BaseClasses import CollectionState, ItemClassification, MultiWorld, Region, Entrance, Location
from worlds.AutoWorld import World
from randomizer import Spoiler
from randomizer import Settings
from randomizer.Enums.Collectibles import Collectibles
from randomizer.Enums.Events import Events
from randomizer.Enums.Items import Items
from randomizer.Enums.Kongs import Kongs
from randomizer.Enums.Levels import Levels
from randomizer.Enums.Locations import Locations
from randomizer.Enums.Regions import Regions
from randomizer.Enums.Settings import HelmSetting, FungiTimeSetting, FasterChecksSelected, ShuffleLoadingZones
from randomizer.Enums.Transitions import Transitions
from randomizer.Enums.Types import Types
from randomizer.Lists import Location as DK64RLocation, Item as DK64RItem
from randomizer.LogicClasses import Collectible, Event, LocationLogic, TransitionFront, Region as DK64Region
from randomizer.Patching.Library.Generic import IsItemSelected
from archipelago.Items import DK64Item
from worlds.generic.Rules import add_item_rule, add_rule, set_rule
from archipelago.Logic import LogicVarHolder
from randomizer.LogicFiles import (
AngryAztec,
CreepyCastle,
CrystalCaves,
DKIsles,
FungiForest,
HideoutHelm,
JungleJapes,
FranticFactory,
GloomyGalleon,
Shops,
)
from randomizer.CollectibleLogicFiles import (
AngryAztec as AztecCollectibles,
CreepyCastle as CastleCollectibles,
CrystalCaves as CavesCollectibles,
DKIsles as IslesCollectibles,
FungiForest as ForestCollectibles,
JungleJapes as JapesCollectibles,
FranticFactory as FactoryCollectibles,
GloomyGalleon as GalleonCollectibles,
)
BASE_ID = 0xD64000
class DK64Location(Location):
"""A class representing a location in Donkey Kong 64."""
game: str = "Donkey Kong 64"
def __init__(self, player: int, name: str = "", address: int = None, parent=None):
"""Initialize a new location."""
super().__init__(player, name, address, parent)
# Complete location table
all_locations = {DK64RLocation.LocationListOriginal[location].name: (BASE_ID + index) for index, location in enumerate(DK64RLocation.LocationListOriginal)}
all_locations.update({"Victory": 0x00}) # Temp for generating goal location
lookup_id_to_name: typing.Dict[int, str] = {id: name for name, id in all_locations.items()}
all_collectible_regions = {
**AztecCollectibles.LogicRegions,
**CastleCollectibles.LogicRegions,
**CavesCollectibles.LogicRegions,
**IslesCollectibles.LogicRegions,
**ForestCollectibles.LogicRegions,
**JapesCollectibles.LogicRegions,
**FactoryCollectibles.LogicRegions,
**GalleonCollectibles.LogicRegions,
}
all_logic_regions = {
**AngryAztec.LogicRegions,
**CreepyCastle.LogicRegions,
**CrystalCaves.LogicRegions,
**DKIsles.LogicRegions,
**FungiForest.LogicRegions,
**HideoutHelm.LogicRegions,
**JungleJapes.LogicRegions,
**FranticFactory.LogicRegions,
**GloomyGalleon.LogicRegions,
**Shops.LogicRegions,
}
gun_for_kong = {Kongs.donkey: "Coconut", Kongs.diddy: "Peanut", Kongs.lanky: "Grape", Kongs.tiny: "Feather", Kongs.chunky: "Pineapple"}
name_for_kong = {Kongs.donkey: "Donkey", Kongs.diddy: "Diddy", Kongs.lanky: "Lanky", Kongs.tiny: "Tiny", Kongs.chunky: "Chunky"}
def create_regions(multiworld: MultiWorld, player: int, spoiler: Spoiler):
"""Create the regions for the given player's world."""
menu_region = Region("Menu", player, multiworld)
multiworld.regions.append(menu_region)
# okay okay OKAY you get a logicVarHolder object for JUST THIS ONCE. Codes these days...
logic_holder = LogicVarHolder(spoiler, player)
# # Print contents of all_locations
# print("All Locations:")
# for location_name, location_id in all_locations.items():
# print(f"{location_name}: {location_id}")
for region_id in all_logic_regions:
region_obj = all_logic_regions[region_id]
# Filtering out auxiliary locations is detrimental to glitch logic, but is necessary to ensure each location placed exactly once
location_logics = [loc for loc in region_obj.locations if (not loc.isAuxiliaryLocation) or region_id.name == "FactoryBaboonBlast"]
# V1 LIMITATION: Helm must be skip_start
# Special exception time! The locations in HideoutHelmEntry cause more problems than they solve, and cannot exist in conjunction with other locations.
if region_obj.level == Levels.HideoutHelm:
# Carefully extract the duplicate Helm locations based on what Helm rooms are required per the settings.
# If the room is required, the HideoutHelmEntry region cannot have the locations (because you'll have to reach the room to complete the barrels)
# If the room is not required, the HideoutHelmKongRoom cannot have the locations (because they'll get completed by the Helm Entry Redirect)
if (region_id == Regions.HideoutHelmEntry and spoiler.settings.helm_donkey) or (region_id == Regions.HideoutHelmDonkeyRoom and not spoiler.settings.helm_donkey):
location_logics = [loc for loc in location_logics if loc.id not in (Locations.HelmDonkey1, Locations.HelmDonkey2)]
if (region_id == Regions.HideoutHelmEntry and spoiler.settings.helm_diddy) or (region_id == Regions.HideoutHelmDiddyRoom and not spoiler.settings.helm_diddy):
location_logics = [loc for loc in location_logics if loc.id not in (Locations.HelmDiddy1, Locations.HelmDiddy2)]
if (region_id == Regions.HideoutHelmEntry and spoiler.settings.helm_lanky) or (region_id == Regions.HideoutHelmLankyRoom and not spoiler.settings.helm_lanky):
location_logics = [loc for loc in location_logics if loc.id not in (Locations.HelmLanky1, Locations.HelmLanky2)]
if (region_id == Regions.HideoutHelmEntry and spoiler.settings.helm_tiny) or (region_id == Regions.HideoutHelmTinyRoom and not spoiler.settings.helm_tiny):
location_logics = [loc for loc in location_logics if loc.id not in (Locations.HelmTiny1, Locations.HelmTiny2)]
if (region_id == Regions.HideoutHelmEntry and spoiler.settings.helm_chunky) or (region_id == Regions.HideoutHelmChunkyRoom and not spoiler.settings.helm_chunky):
location_logics = [loc for loc in location_logics if loc.id not in (Locations.HelmChunky1, Locations.HelmChunky2)]
collectibles = []
if region_id in all_collectible_regions.keys():
collectibles = [col for col in all_collectible_regions[region_id] if col.type in (Collectibles.bunch, Collectibles.banana, Collectibles.balloon)]
events = [event for event in region_obj.events]
# if region_obj.level == Levels.Shops:
# multiworld.regions.append(create_shop_region(multiworld, player, region_id.name, region_obj, location_logics, spoiler.settings))
# else:
multiworld.regions.append(create_region(multiworld, player, region_id.name, region_obj.level, location_logics, collectibles, events, logic_holder))
def create_region(
multiworld: MultiWorld,
player: int,
region_name: str,
level: Levels,
location_logics: typing.List[LocationLogic],
collectibles: typing.List[Collectible],
events: typing.List[Event],
logic_holder: LogicVarHolder,
) -> Region:
"""Create a region for the given player's world."""
new_region = Region(region_name, player, multiworld)
# Two special cases - GameStart doesn't need any locations, as AP will handle starting items instead
if location_logics and region_name != "GameStart":
# And Isles Medals locations aren't real unless the setting is enabled.
if region_name == "DKIslesMedals" and not IsItemSelected(logic_holder.settings.cb_rando_enabled, logic_holder.settings.cb_rando_list_selected, Levels.DKIsles):
location_logics = []
for location_logic in location_logics:
location_obj = logic_holder.spoiler.LocationList[location_logic.id]
# DK Arcade Round 1 is dependent on a setting - don't create the inaccessible location depending on that Faster Checks toggle
if location_logic.id == Locations.FactoryDonkeyDKArcade:
if logic_holder.checkFastCheck(FasterChecksSelected.factory_arcade_round_1) and region_name == "FactoryArcadeTunnel":
continue
elif not logic_holder.checkFastCheck(FasterChecksSelected.factory_arcade_round_1) and region_name == "FactoryBaboonBlast":
continue
# Starting move locations and Kongs may be shuffled but their locations are not relevant ever due to item placement restrictions
# V1 LIMITATION: Kong locations are always empty because we can't put the vast majority of items (including AP items) there yet
if location_obj.type in (Types.TrainingBarrel, Types.PreGivenMove, Types.Kong):
continue
# Dropsanity would otherwise flood the world with irrelevant locked locations, greatly slowing seed gen
if location_obj.type == Types.Enemies and Types.Enemies not in logic_holder.settings.shuffled_location_types:
continue
# V1 LIMITATION: Shared shops cannot exist because we need the space in shops to guarantee enough locations for every item
# Because there's no shared shops, this may mean shared potions can end up in Kong shops. This is fine.
if location_obj.type == Types.Shop and location_obj.kong == Kongs.any:
continue
loc_id = all_locations.get(location_obj.name, 0)
# Universal Tracker: don't add this location if it has no item
if hasattr(multiworld, "generation_is_fake"):
if hasattr(multiworld, "re_gen_passthrough"):
if "Donkey Kong 64" in multiworld.re_gen_passthrough:
if location_obj.name in multiworld.re_gen_passthrough["Donkey Kong 64"]["JunkedLocations"]:
continue
location = DK64Location(player, location_obj.name, loc_id, new_region)
# If the location is not shuffled, lock in the default item on the location
if location_logic.id != Locations.BananaHoard and location_obj.type not in logic_holder.settings.shuffled_location_types and location_obj.default is not None:
location.address = None
location.place_locked_item(DK64Item(DK64RItem.ItemList[location_obj.default].name, ItemClassification.progression_skip_balancing, None, player))
# Otherwise, this is a location that can have items in it, and counts towards the number of locations available for items
else:
logic_holder.settings.location_pool_size += 1
# Quickly test and see if we can reach this location with zero items
quick_success = False
try:
quick_success = location.logic(None)
except Exception:
pass
# If we can, we can greatly simplify the logic at this location
if quick_success:
set_rule(location, lambda state: True)
# Otherwise we have to work our way through the logic proper
# V1 LIMITATION: this will ignore minigame logic, so bonus barrels and Helm barrels must be autocompleted
else:
set_rule(location, lambda state, player=player, location_logic=location_logic: hasDK64RLocation(state, player, location_logic))
# Our Fill checks for Shockwave independent of the location's logic, so we must do the same
if location_obj.type == Types.RainbowCoin:
add_rule(location, lambda state: state.has("Shockwave", player))
# Item placement limitations! These only apply to items in your own world, as other worlds' items will be AP items, and those can be anywhere.
# Fairy locations cannot have your own world's blueprints on them for technical reasons.
if location_obj.type == Types.Fairy:
add_item_rule(location, lambda item: not (item.player == player and "Blueprint" in item.name))
# Bosses and Crowns cannot have Junk due to technical reasons
if location_obj.type in (Types.Key, Types.Crown):
add_item_rule(location, lambda item: not (item.player == player and "Junk" in item.name))
new_region.locations.append(location)
# print("Adding location: " + location.name + " | " + str(loc_id))
collectible_id = 0
for collectible in collectibles:
collectible_id += 1
location_name = region_name + " Collectible " + str(collectible_id) + ": " + collectible.kong.name + " " + collectible.type.name
location = DK64Location(player, location_name, None, new_region)
# Quickly test and see if we can reach this location with zero items
quick_success = False
try:
quick_success = collectible.logic(None)
except Exception:
pass
# If we can, we can greatly simplify the logic at this location
if quick_success:
set_rule(location, lambda state: True)
else:
set_rule(location, lambda state, player=player, collectible=collectible: hasDK64RCollectible(state, player, collectible))
quantity = collectible.amount
if collectible.type == Collectibles.bunch:
quantity *= 5
elif collectible.type == Collectibles.balloon:
quantity *= 10
add_rule(location, lambda state, collectible_kong=collectible.kong: state.has(gun_for_kong[collectible_kong], player)) # We need to be sure we check for gun access for this balloon
add_rule(
location, lambda state, collectible_kong=collectible.kong: state.has(name_for_kong[collectible_kong], player)
) # There's no FTA for collectibles - you *must* own the right kong to collect it
location.place_locked_item(DK64Item("Collectible CBs, " + collectible.kong.name + ", " + level.name + ", " + str(quantity), ItemClassification.progression_skip_balancing, None, player))
# print("Collectible CBs, " + collectible.kong.name + ", " + level.name + ", " + str(quantity))
new_region.locations.append(location)
for event in events:
# Some events don't matter due to Archipelago settings
# Entering levels in weird spots can require a number of pre-completed events to be handled in Game Start
# We need to filter out any that will return False (because they will never not return False)
# V1 LIMITATION: We're not filtering out auto key turn ins, so that setting must be on (not really a problem for basically anyone)
if region_name == "GameStart" and event.name in (
Events.Night,
Events.Day,
Events.AztecIceMelted,
Events.MainCoreActivated,
Events.TestingGateOpened,
Events.LighthouseGateOpened,
Events.ShipyardGateOpened,
Events.ActivatedLighthouse,
Events.ShipyardTreasureRoomOpened,
Events.WormGatesOpened,
Events.HollowTreeGateOpened,
):
if not event.logic(logic_holder):
continue
# Most water level altering events are inaccessible, only the one specifically in LighthouseUnderwater is accessible
if event.name in (Events.WaterLowered, Events.WaterRaised) and region_name != "LighthouseUnderwater":
continue
# This event only matters if you enter galleon via the Treasure Room and it spawns open
if event.name == Events.ShipyardTreasureRoomOpened and region_name == "TreasureRoom":
if not event.logic(logic_holder):
continue
# This HelmFinished event is only necessary for skip all Helm
if event.name == Events.HelmFinished and region_name == "HideoutHelmEntry" and logic_holder.settings.helm_setting != HelmSetting.skip_all:
continue
location_name = region_name + " Event " + event.name.name
location = DK64Location(player, location_name, None, new_region)
# Quickly test and see if we can reach this location with zero items
quick_success = False
try:
quick_success = event.logic(None)
except Exception:
pass
# If we can, we can greatly simplify the logic at this location
if quick_success:
set_rule(location, lambda state: True)
else:
set_rule(location, lambda state, player=player, event=event: hasDK64REvent(state, player, event))
location.place_locked_item(DK64Item("Event, " + event.name.name, ItemClassification.progression_skip_balancing, None, player))
new_region.locations.append(location)
return new_region
# CURRENTLY UNUSED - for some reason some Lanky shops are inaccessible??
def create_shop_region(multiworld: MultiWorld, player: int, region_name: str, region_obj: DK64Region, location_logics: typing.List[LocationLogic], settings: Settings) -> Region:
"""Create a region for the given player's world."""
# Shop regions have relatively straightforward logic that can be streamlined for performance purposes
new_region = Region(region_name, player, multiworld)
# Snide and his blueprint locations are one-to-one every time
if "Snide" in region_name:
blueprint_id = Items.JungleJapesDonkeyBlueprint
for item in range(40):
blueprint_obj = DK64RItem.ItemList[blueprint_id]
location_name = "Turn In " + blueprint_obj.name
loc_id = all_locations.get(location_name, 0)
location = DK64Location(player, location_name, loc_id, new_region)
set_rule(location, lambda state, blueprint_name=blueprint_obj.name: state.has(blueprint_name, player))
location.place_locked_item(DK64Item(blueprint_obj.name, ItemClassification.progression_skip_balancing, None, player))
new_region.locations.append(location)
blueprint_id += 1
# The one special child here is Cranky Generic, home of Jetpac, the only shop location with any relevant logic
elif region_name == "Cranky Generic":
location = DK64Location(player, "Jetpac", all_locations.get("Jetpac", 0), new_region)
set_rule(location, lambda state, player=player, location_logic=location_logics[0]: hasDK64RLocation(state, player, location_logic))
new_region.locations.append(location)
settings.location_pool_size += 1
# All other shops are free because we are *not* touching shop logic with a 20000000000 ft pole (yet)
else:
for location_logic in location_logics:
location_obj = DK64RLocation.LocationListOriginal[location_logic.id]
if location_obj.kong == Kongs.any:
continue # We need to eliminate shared shop locations so shops don't have both a shared item and Kong items
loc_id = all_locations.get(location_obj.name, 0)
location = DK64Location(player, location_obj.name, loc_id, new_region)
required_kong_name = location_obj.kong.name.title()
set_rule(location, lambda state, required_kong_name=required_kong_name: state.has(required_kong_name, player))
new_region.locations.append(location)
settings.location_pool_size += 1
return new_region
def connect_regions(world: World, settings: Settings):
"""Connect the regions in the given world."""
connect(world, "Menu", "GameStart")
# # Example Region Connection
# connect(
# world,
# "DK Isles",
# "Test",
# lambda state: state.has(DK64RItem.ItemList[DK64RItems.GoldenBanana].name, world.player, 2),
# )
# Shuffling level order should be going off of our ShufflableExits dictionary, but that's not properly isolated to the spoiler object yet.
# For now, we have to pre-calculate what the destination region is for each of these transitions.
if settings.shuffle_loading_zones == ShuffleLoadingZones.levels:
lobby_transition_mapping = {}
enter_lobby_transitions = {
Transitions.IslesMainToJapesLobby: None,
Transitions.IslesMainToAztecLobby: None,
Transitions.IslesMainToFactoryLobby: None,
Transitions.IslesMainToGalleonLobby: None,
Transitions.IslesMainToForestLobby: None,
Transitions.IslesMainToCavesLobby: None,
Transitions.IslesMainToCastleLobby: None,
Transitions.IslesMainToHelmLobby: None,
}
exit_lobby_transitions = {
Transitions.IslesJapesLobbyToMain: None,
Transitions.IslesAztecLobbyToMain: None,
Transitions.IslesFactoryLobbyToMain: None,
Transitions.IslesGalleonLobbyToMain: None,
Transitions.IslesForestLobbyToMain: None,
Transitions.IslesCavesLobbyToMain: None,
Transitions.IslesCastleLobbyToMain: None,
Transitions.IslesHelmLobbyToMain: None,
}
# Identify which regions each lobby transition leads to in vanilla - this is as un-hard-coded as I can make it
for region_id, region_obj in DKIsles.LogicRegions.items():
for exit in region_obj.exits:
if exit.exitShuffleId in enter_lobby_transitions and not exit.isGlitchTransition:
enter_lobby_transitions[exit.exitShuffleId] = exit.dest.name
if exit.exitShuffleId in exit_lobby_transitions and not exit.isGlitchTransition:
exit_lobby_transitions[exit.exitShuffleId] = exit.dest.name
# Now we can map the transitions to the shuffled level order
enter_lobby_transitions_list = list(enter_lobby_transitions.keys())
exit_lobby_transitions_list = list(exit_lobby_transitions.keys())
for i in range(len(settings.level_order)):
level = settings.level_order[i + 1]
lobby_transition_mapping[enter_lobby_transitions_list[i]] = enter_lobby_transitions[enter_lobby_transitions_list[level]]
lobby_transition_mapping[exit_lobby_transitions_list[level]] = exit_lobby_transitions[exit_lobby_transitions_list[i]]
for region_id, region_obj in all_logic_regions.items():
for exit in region_obj.exits:
destination_name = exit.dest.name
# If this is a Isles <-> Lobby transition and we're shuffling levels, respect the dictionary built earlier
if settings.shuffle_loading_zones == ShuffleLoadingZones.levels and exit.exitShuffleId in lobby_transition_mapping.keys():
destination_name = lobby_transition_mapping[exit.exitShuffleId]
try:
# Quickly test and see if we can pass this exit with zero items
quick_success = False
try:
quick_success = exit.logic(None)
except Exception:
pass
# If we can, we can greatly simplify the logic for this exit
if quick_success:
converted_logic = lambda state: True
else:
converted_logic = lambda state, player=world.player, exit=exit: hasDK64RTransition(state, player, exit)
connect(world, region_id.name, destination_name, converted_logic)
# print("Connecting " + region_id.name + " to " + destination_name)
except Exception:
pass
# V1 LIMITATION: We have pre-activated Isles warps, so we need to make two extra connections to make sure the logic is correct
connect(world, "IslesMain", "IslesMainUpper", lambda state: True) # Pre-activated W2
connect(world, "IslesMain", "KremIsleBeyondLift", lambda state: True) # Pre-activated W4
pass
def connect(world: World, source: str, target: str, rule: typing.Optional[typing.Callable] = None):
"""Connect two regions in the given world."""
source_region = world.multiworld.get_region(source, world.player)
target_region = world.multiworld.get_region(target, world.player)
name = source + "->" + target
connection = Entrance(world.player, name, source_region)
if rule:
connection.access_rule = rule
source_region.exits.append(connection)
connection.connect(target_region)
def hasDK64RTransition(state: CollectionState, player: int, exit: TransitionFront):
"""Check if the given transition is accessible in the given state."""
return exit.logic(state.dk64_logic_holder[player])
def hasDK64RLocation(state: CollectionState, player: int, location: LocationLogic):
"""Check if the given location is accessible in the given state."""
return location.logic(state.dk64_logic_holder[player])
def hasDK64RCollectible(state: CollectionState, player: int, collectible: Collectible):
"""Check if the given collectible is accessible in the given state."""
return collectible.logic(state.dk64_logic_holder[player])
def hasDK64REvent(state: CollectionState, player: int, event: Event):
"""Check if the given event is accessible in the given state."""
return event.logic(state.dk64_logic_holder[player])