forked from mirror/Archipelago
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
306 lines
11 KiB
Python
306 lines
11 KiB
Python
"""Contains classes used in the logic system."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING, Any, Callable, List, Optional, Tuple, Union
|
|
|
|
from randomizer.Enums.Kongs import Kongs
|
|
from randomizer.Enums.Levels import Levels
|
|
from randomizer.Enums.Regions import Regions
|
|
from randomizer.Enums.Time import Time
|
|
from randomizer.Enums.Locations import Locations
|
|
from randomizer.Enums.HintRegion import HintRegion, HINT_REGION_PAIRING, MEDAL_REWARD_REGIONS, SHOP_REGIONS
|
|
from randomizer.Lists.EnemyTypes import enemy_location_list
|
|
|
|
if TYPE_CHECKING:
|
|
from randomizer.Enums.Collectibles import Collectibles
|
|
from randomizer.Enums.Events import Events
|
|
from randomizer.Enums.Locations import Locations
|
|
from randomizer.Enums.MinigameType import MinigameType
|
|
from randomizer.Enums.Transitions import Transitions
|
|
from randomizer.Logic import LogicVarHolder
|
|
|
|
|
|
class LocationLogic:
|
|
"""Logic for a location."""
|
|
|
|
def __init__(
|
|
self,
|
|
id: Union[int, Locations],
|
|
logic: Callable,
|
|
bonusBarrel: Optional[MinigameType] = None,
|
|
isAuxiliary: bool = False,
|
|
) -> None:
|
|
"""Initialize with given parameters."""
|
|
self.id = id
|
|
self.logic = logic # Lambda function for accessibility
|
|
if id >= Locations.JapesMainEnemy_Start and id <= Locations.IslesMainEnemy_LowerFactoryPath1:
|
|
# Handle enemy logic
|
|
self.logic = lambda l: logic(l) and enemy_location_list[id].canDropItem(l)
|
|
self.bonusBarrel = bonusBarrel # Uses MinigameType enum
|
|
self.isAuxiliaryLocation = (
|
|
isAuxiliary # For when the Location needs to be in a region but not count as in the region (used for locations that need to be accessible in different regions depending on settings)
|
|
)
|
|
|
|
|
|
class Event:
|
|
"""Event within a region.
|
|
|
|
Events act as statically placed items
|
|
For example, if Lanky must press a button in region x to open something in region y,
|
|
that can be represented as a button press event in region x which is checked for in region y.
|
|
"""
|
|
|
|
def __init__(self, name: Events, logic: Callable) -> None:
|
|
"""Initialize with given parameters."""
|
|
self.name = name
|
|
self.logic = logic # Lambda function for accessibility
|
|
|
|
|
|
class Collectible:
|
|
"""Class used for colored bananas and banana coins."""
|
|
|
|
def __init__(
|
|
self,
|
|
type: Collectibles,
|
|
kong: Kongs,
|
|
logic: Callable,
|
|
coords: Optional[Tuple[float, float, float]] = None,
|
|
amount: int = 1,
|
|
enabled: bool = True,
|
|
vanilla: bool = True,
|
|
name: str = "vanilla",
|
|
locked: bool = False,
|
|
) -> None:
|
|
"""Initialize with given parameters."""
|
|
self.type = type
|
|
self.kong = kong
|
|
self.logic = logic
|
|
self.amount = amount
|
|
self.coords = coords # None for vanilla collectibles for now. For custom, use (x,y,z) format
|
|
self.added = False
|
|
self.enabled = enabled
|
|
self.vanilla = vanilla
|
|
self.name = name
|
|
self.locked = locked
|
|
|
|
|
|
class Region:
|
|
"""Region contains shufflable locations, events, and transitions to other regions."""
|
|
|
|
def __init__(
|
|
self,
|
|
name: str,
|
|
hint_name: HintRegion,
|
|
level: Levels,
|
|
tagbarrel: bool,
|
|
deathwarp: Optional[Union[int, TransitionFront, Regions]],
|
|
locations: List[Union[LocationLogic, Any]],
|
|
events: List[Union[Event, Any]],
|
|
transitionFronts: List[Union[TransitionFront, Any]],
|
|
restart: Optional[Union[Transitions, int]] = None,
|
|
) -> None:
|
|
"""Initialize with given parameters."""
|
|
self.name = name
|
|
self.hint_name = hint_name
|
|
self.level = level
|
|
self.tagbarrel = tagbarrel
|
|
self.deathwarp = None
|
|
self.locations = locations
|
|
self.events = events
|
|
self.exits = transitionFronts # In the context of a region, exits are how you leave the region
|
|
self.restart = restart
|
|
|
|
self.dayAccess = [False] * 5
|
|
self.nightAccess = [False] * 5
|
|
|
|
# If possible to die in this region, add an exit to where dying will take you
|
|
# deathwarp is also set to none in regions in which a deathwarp would take you to itself
|
|
# Or if there is loading-zone-less free access to the region it would take you to already
|
|
if deathwarp is not None:
|
|
# If deathwarp is itself an exit class (necessary when deathwarp requires custom logic) just add it directly
|
|
if isinstance(deathwarp, TransitionFront):
|
|
self.deathwarp = deathwarp
|
|
else:
|
|
# If deathwarp is -1, indicates to use the default value for it, which is the starting area of the level
|
|
if deathwarp == -1:
|
|
deathwarp = self.GetDefaultDeathwarp()
|
|
if deathwarp is not None:
|
|
if isinstance(deathwarp, Regions):
|
|
self.deathwarp = TransitionFront(deathwarp, lambda l: True)
|
|
else:
|
|
self.deathwarp = TransitionFront(Regions(deathwarp), lambda l: True)
|
|
|
|
self.ResetAccess()
|
|
|
|
def ResetAccess(self) -> None:
|
|
"""Clear access variables set during search."""
|
|
# Time access
|
|
self.dayAccess = [False] * 5
|
|
self.nightAccess = [False] * 5
|
|
|
|
def GetDefaultDeathwarp(self) -> Regions:
|
|
"""Get the default deathwarp depending on the region's level."""
|
|
if self.level == Levels.DKIsles:
|
|
return Regions.IslesMain
|
|
elif self.level == Levels.JungleJapes:
|
|
return Regions.JungleJapesEntryHandler
|
|
elif self.level == Levels.AngryAztec:
|
|
return Regions.AngryAztecEntryHandler
|
|
elif self.level == Levels.FranticFactory:
|
|
return Regions.FranticFactoryEntryHandler
|
|
elif self.level == Levels.GloomyGalleon:
|
|
return Regions.GloomyGalleonEntryHandler
|
|
elif self.level == Levels.FungiForest:
|
|
return Regions.FungiForestEntryHandler
|
|
elif self.level == Levels.CrystalCaves:
|
|
return Regions.CrystalCavesEntryHandler
|
|
elif self.level == Levels.CreepyCastle:
|
|
return Regions.CreepyCastleEntryHandler
|
|
elif self.level == Levels.HideoutHelm:
|
|
return Regions.HideoutHelmEntry
|
|
return Regions.GameStart
|
|
|
|
def getHintRegionName(self) -> str:
|
|
"""Convert hint region enum to the name."""
|
|
return HINT_REGION_PAIRING.get(self.hint_name, "Unknown Region")
|
|
|
|
def isMedalRegion(self) -> bool:
|
|
"""Return whether the associated hint region is a medal reward region."""
|
|
return self.hint_name in MEDAL_REWARD_REGIONS
|
|
|
|
def isCBRegion(self) -> bool:
|
|
"""Return whether the associated hint region requires CBs to access (Bosses and medal rewards)."""
|
|
return self.hint_name in MEDAL_REWARD_REGIONS or self.hint_name == HintRegion.Bosses
|
|
|
|
def isShopRegion(self) -> bool:
|
|
"""Return whether the associated hint region is a shop region."""
|
|
return self.hint_name in SHOP_REGIONS
|
|
|
|
|
|
class TransitionBack:
|
|
"""The exited side of a transition between regions."""
|
|
|
|
def __init__(self, regionId: Regions, exitName: str, spoilerName: str, reverse: Optional[Transitions] = None) -> None:
|
|
"""Initialize with given parameters."""
|
|
self.regionId = regionId # Destination region
|
|
self.name = exitName
|
|
self.spoilerName = spoilerName
|
|
self.reverse = reverse # Indicates a reverse direction transition, if one exists
|
|
|
|
|
|
class TransitionFront:
|
|
"""The entered side of a transition between regions."""
|
|
|
|
def __init__(
|
|
self,
|
|
dest: Regions,
|
|
logic: Callable,
|
|
exitShuffleId: Optional[Transitions] = None,
|
|
assumed: bool = False,
|
|
time: Time = Time.Both,
|
|
isGlitchTransition: bool = False,
|
|
isBananaportTransition: bool = False,
|
|
) -> None:
|
|
"""Initialize with given parameters."""
|
|
self.dest = dest
|
|
self.logic = logic # Lambda function for accessibility
|
|
self.exitShuffleId = exitShuffleId
|
|
self.time = time
|
|
self.assumed = assumed # Indicates this is an assumed exit attached to the root
|
|
self.isGlitchTransition = isGlitchTransition # Indicates if this is a glitch-logic transition for this entrance
|
|
self.isBananaportTransition = isBananaportTransition # Indicates if this transition is due to a Bananaport
|
|
|
|
|
|
class Sphere:
|
|
"""A randomizer concept often used in spoiler logs.
|
|
|
|
A 'sphere' is a collection of locations and items that are accessible
|
|
or obtainable with only the items available from earlier, smaller spheres.
|
|
Sphere 0 items are what you start with in a seed, sphere 1 items can be
|
|
obtained with those items, sphere 2 items can be obtained with sphere 0
|
|
and sphere 1 items, and so on.
|
|
"""
|
|
|
|
def __init__(self) -> None:
|
|
"""Initialize with given parameters."""
|
|
self.seedBeaten = False
|
|
self.availableGBs = 0
|
|
self.locations: List[Union[LocationLogic, Any]] = []
|
|
|
|
|
|
class ColoredBananaGroup:
|
|
"""Stores data for each group of colored bananas."""
|
|
|
|
def __init__(
|
|
self,
|
|
*,
|
|
group=0,
|
|
name="No Location",
|
|
map_id=0,
|
|
konglist=[],
|
|
region=None,
|
|
logic=None,
|
|
vanilla=False,
|
|
locations=[],
|
|
) -> None:
|
|
"""Initialize with given parameters."""
|
|
self.group = group
|
|
self.name = name
|
|
self.map = map_id
|
|
self.kongs = konglist
|
|
self.locations = locations # 5 numbers: {int amount, float scale, int x, y, z}
|
|
self.region = region
|
|
if logic is None:
|
|
self.logic = lambda l: True
|
|
else:
|
|
self.logic = logic
|
|
self.selected = False
|
|
|
|
|
|
class Balloon:
|
|
"""Stores data for each balloon."""
|
|
|
|
def __init__(
|
|
self,
|
|
*,
|
|
id=0,
|
|
name="No Location",
|
|
map_id=0,
|
|
speed=0,
|
|
konglist=[],
|
|
region=None,
|
|
logic=None,
|
|
vanilla=False,
|
|
points=[],
|
|
) -> None:
|
|
"""Initialize with given parameters."""
|
|
self.id = id
|
|
self.name = name
|
|
self.map = map_id
|
|
self.speed = speed
|
|
self.kongs = konglist
|
|
self.points = points # 3 numbers: [int x, y, z]
|
|
self.region = region
|
|
if logic is None:
|
|
self.logic = lambda l: True
|
|
else:
|
|
self.logic = logic
|
|
self.spawnPoint = self.setSpawnPoint(points)
|
|
self.selected = False
|
|
|
|
def setSpawnPoint(self, points: List[List[int]] = []) -> List[int]:
|
|
"""Set the spawn point of a balloon based on its path."""
|
|
spawnX = 0.0
|
|
spawnY = 0.0
|
|
spawnZ = 0.0
|
|
for p in points:
|
|
spawnX += p[0]
|
|
spawnY += p[1]
|
|
spawnZ += p[2]
|
|
spawnX /= len(points)
|
|
spawnY /= len(points)
|
|
spawnY -= 100.0 # Most balloons are at least 100 units off the ground
|
|
spawnZ /= len(points)
|
|
return [int(spawnX), int(spawnY), int(spawnZ)]
|