Files
dockipelago/worlds/apeescape/__init__.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

521 lines
26 KiB
Python

import math
import os
import json
from typing import ClassVar, Dict, List, Tuple, Optional, TextIO, Any
from BaseClasses import ItemClassification, MultiWorld, Tutorial, CollectionState
from logging import warning
from Options import OptionError
from worlds.AutoWorld import WebWorld, World
from .Items import item_table, ApeEscapeItem, GROUPED_ITEMS
from .Locations import location_table, base_location_id, GROUPED_LOCATIONS
from .Regions import create_regions, ApeEscapeLevel
from .Rules import set_rules, get_required_keys
from .Client import ApeEscapeClient
from .Strings import AEItem, AELocation
from .RAMAddress import RAM
from .Options import ApeEscapeOptions
class ApeEscapeWeb(WebWorld):
theme = "stone"
# Verify this placeholder text is accurate
setup_en = Tutorial(
"Ape Escape Multiworld Setup Guide",
"A guide to setting up Ape Escape in Archipelago.",
"English",
"setup_en.md",
"setup/en",
["CDRomatron, Thedragon005, IHNN"]
)
setup_fr = Tutorial(
setup_en.tutorial_name,
setup_en.description,
"Français",
"setup_fr.md",
"setup/fr",
["Thedragon005"]
)
tutorials = [setup_en, setup_fr]
class ApeEscapeWorld(World):
"""
Ape Escape is a platform game published and developed by Sony for the PlayStation, released in 1999.
The story revolves around the main protagonist, Spike, who has to prevent history from being changed
by an army of monkeys led by Specter, the main antagonist.
"""
game = "Ape Escape"
web: ClassVar[WebWorld] = ApeEscapeWeb()
topology_present = True
options_dataclass = ApeEscapeOptions
options: ApeEscapeOptions
item_name_to_id = item_table
for key, value in item_name_to_id.items():
item_name_to_id[key] = value + base_location_id
location_name_to_id = location_table
for key, value in location_name_to_id.items():
location_name_to_id[key] = value + base_location_id
item_name_groups = GROUPED_ITEMS
location_name_groups = GROUPED_LOCATIONS
glitches_item_name = AEItem.FAKE_OOL_ITEM.value
ut_can_gen_without_yaml = True # class var that tells it to ignore the player yaml
using_ut: bool # so we can check if we're using UT only once
passthrough: Dict[str, Any]
def __init__(self, multiworld: MultiWorld, player: int):
self.goal: Optional[int] = 0
self.requiredtokens: Optional[int] = 0
self.totaltokens: Optional[int] = 0
self.tokenlocations: Optional[int] = 0
self.fasttokengoal: Optional[int] = 0
self.logic: Optional[int] = 0
self.infinitejump: Optional[int] = 0
self.superflyer: Optional[int] = 0
self.entrance: Optional[int] = 0
self.randomizestartingroom: Optional[int] = 0
self.unlocksperkey: Optional[int] = 0
self.extrakeys: Optional[int] = 0
self.coin: Optional[int] = 0
self.mailbox: Optional[int] = 0
self.lamp: Optional[int] = 0
self.gadget: Optional[int] = 0
self.shufflenet: Optional[int] = 0
self.shufflewaternet: Optional[int] = 0
self.lowoxygensounds: Optional[int] = 0
self.trappercentage: Optional[int] = 0
self.itemdisplay: Optional[int] = 0
self.itempool: List[ApeEscapeItem] = []
self.levellist: List[ApeEscapeLevel] = []
self.entranceorder: List[ApeEscapeLevel] = []
self.firstrooms = []
super(ApeEscapeWorld, self).__init__(multiworld, player)
def generate_early(self) -> None:
self.goal = self.options.goal.value
self.requiredtokens = self.options.requiredtokens.value
self.totaltokens = self.options.totaltokens.value
self.tokenlocations = self.options.tokenlocations.value
self.fasttokengoal = self.options.fasttokengoal.value
self.logic = self.options.logic.value
self.infinitejump = self.options.infinitejump.value
self.superflyer = self.options.superflyer.value
self.entrance = self.options.entrance.value
self.randomizestartingroom = self.options.randomizestartingroom.value
self.unlocksperkey = self.options.unlocksperkey.value
self.extrakeys = self.options.extrakeys.value
self.coin = self.options.coin.value
self.mailbox = self.options.mailbox.value
self.lamp = self.options.lamp.value
self.gadget = self.options.gadget.value
self.shufflenet = self.options.shufflenet.value
self.shufflewaternet = self.options.shufflewaternet.value
self.lowoxygensounds = self.options.lowoxygensounds.value
self.trappercentage = self.options.trappercentage.value
self.itemdisplay = self.options.itemdisplay.value
self.itempool = []
# Universal tracker stuff, shouldn't do anything in standard gen
if hasattr(self.multiworld, "re_gen_passthrough"):
if "Ape Escape" in self.multiworld.re_gen_passthrough:
self.using_ut = True
self.passthrough = self.multiworld.re_gen_passthrough["Ape Escape"]
self.options.goal.value = self.passthrough["goal"]
self.options.fasttokengoal.value = self.passthrough["fasttokengoal"]
self.options.allowcollect.value = self.passthrough["allowcollect"]
self.options.requiredtokens.value = self.passthrough["requiredtokens"]
self.options.totaltokens.value = self.passthrough["totaltokens"]
self.options.tokenlocations.value = self.passthrough["tokenlocations"]
self.options.logic.value = self.passthrough["logic"]
self.options.infinitejump.value = self.passthrough["infinitejump"]
self.options.superflyer.value = self.passthrough["superflyer"]
self.options.entrance.value = self.passthrough["entrance"]
self.options.randomizestartingroom.value = self.passthrough["randomizestartingroom"]
self.options.unlocksperkey.value = self.passthrough["unlocksperkey"]
self.options.extrakeys.value = self.passthrough["extrakeys"]
self.options.coin.value = self.passthrough["coin"]
self.options.mailbox.value = self.passthrough["mailbox"]
self.options.lamp.value = self.passthrough["lamp"]
self.options.gadget.value = self.passthrough["gadget"]
self.options.shufflenet.value = self.passthrough["shufflenet"]
self.options.shufflewaternet.value = self.passthrough["shufflewaternet"]
self.options.lowoxygensounds.value = self.passthrough["lowoxygensounds"]
self.options.trappercentage.value = self.passthrough["trappercentage"]
self.options.itemdisplay.value = self.passthrough["itemdisplay"]
else:
self.using_ut = False
else:
self.using_ut = False
def create_regions(self):
create_regions(self)
def set_rules(self):
set_rules(self)
def create_item(self, name: str) -> ApeEscapeItem:
item_id = item_table[name]
classification = ItemClassification.progression
item = ApeEscapeItem(name, classification, item_id, self.player)
return item
def create_item_skipbalancing(self, name: str) -> ApeEscapeItem:
item_id = item_table[name]
classification = ItemClassification.progression_skip_balancing
item = ApeEscapeItem(name, classification, item_id, self.player)
return item
def create_item_useful(self, name: str) -> ApeEscapeItem:
item_id = item_table[name]
classification = ItemClassification.useful
item = ApeEscapeItem(name, classification, item_id, self.player)
return item
def create_item_filler(self, name: str) -> ApeEscapeItem:
item_id = item_table[name]
classification = ItemClassification.filler
item = ApeEscapeItem(name, classification, item_id, self.player)
return item
def create_item_trap(self, name: str) -> ApeEscapeItem:
item_id = item_table[name]
classification = ItemClassification.trap
item = ApeEscapeItem(name, classification, item_id, self.player)
return item
def create_event_item(self, name: str) -> ApeEscapeItem:
classification = ItemClassification.progression
item = ApeEscapeItem(name, classification, None, self.player)
return item
def create_items(self):
reservedlocations = 0
club = self.create_item(AEItem.Club.value)
net = self.create_item(AEItem.Net.value)
radar = self.create_item(AEItem.Radar.value)
shooter = self.create_item(AEItem.Sling.value)
hoop = self.create_item(AEItem.Hoop.value)
flyer = self.create_item(AEItem.Flyer.value)
car = self.create_item(AEItem.Car.value)
punch = self.create_item(AEItem.Punch.value)
victory = self.create_event_item("Victory")
waternet = self.create_item(AEItem.WaterNet.value)
# progwaternet = self.create_item(AEItem.ProgWaterNet.value)
watercatch = self.create_item(AEItem.WaterCatch.value)
CB_Lamp = self.create_item(AEItem.CB_Lamp.value)
DI_Lamp = self.create_item(AEItem.DI_Lamp.value)
CrC_Lamp = self.create_item(AEItem.CrC_Lamp.value)
CP_Lamp = self.create_item(AEItem.CP_Lamp.value)
SF_Lamp = self.create_item(AEItem.SF_Lamp.value)
TVT_Lobby_Lamp = self.create_item(AEItem.TVT_Lobby_Lamp.value)
TVT_Tank_Lamp = self.create_item(AEItem.TVT_Tank_Lamp.value)
MM_Lamp = self.create_item(AEItem.MM_Lamp.value)
MM_DoubleDoorKey = self.create_item(AEItem.MM_DoubleDoorKey.value)
self.itempool += [MM_DoubleDoorKey]
# Create the desired amount of Specter Tokens if the settings require them, and make them local if requested.
if self.options.goal == "tokenhunt" or self.options.goal == "mmtoken" or self.options.goal == "ppmtoken":
self.itempool += [self.create_item_skipbalancing(AEItem.Token.value) for _ in range(0, max(self.options.requiredtokens, self.options.totaltokens))]
if self.options.tokenlocations == "ownworld":
self.options.local_items.value.add("Specter Token")
# Create enough keys to access every level, if keys are on, plus the desired amount of extra keys.
if self.options.unlocksperkey != "none":
numkeys = get_required_keys(self.options.unlocksperkey.value, self.options.goal.value, self.options.coin.value)
self.itempool += [self.create_item(AEItem.Key.value) for _ in range(0, numkeys[21] + self.options.extrakeys.value)]
# Monkey Lamp shuffle - only add to the pool if the option is on (treat as vanilla otherwise)
if self.options.lamp == "true":
self.itempool += [CB_Lamp]
self.itempool += [DI_Lamp]
self.itempool += [CrC_Lamp]
self.itempool += [CP_Lamp]
self.itempool += [SF_Lamp]
self.itempool += [TVT_Lobby_Lamp]
self.itempool += [TVT_Tank_Lamp]
self.itempool += [MM_Lamp]
# Water Net shuffle handling
if self.options.shufflewaternet == 0x00 or self.options.gadget == 0x07: # Off or Starting Gadget
self.multiworld.push_precollected(waternet)
elif self.options.shufflewaternet == 0x01: # Progressive
self.itempool += [watercatch]
self.itempool += [self.create_item(AEItem.ProgWaterNet.value)]
self.itempool += [self.create_item(AEItem.ProgWaterNet.value)]
else: # On
self.itempool += [waternet]
# Net shuffle handling
if self.options.shufflenet == "false":
self.multiworld.push_precollected(net)
elif self.options.shufflenet == "true":
# If net shuffle is on, make sure there are locations that don't require net.
if self.options.coin == "true" or self.options.mailbox == "true":
self.itempool += [net]
else:
# All locations require net with these options, so throw a warning about incompatible options and just give the net anyway.
# if instead we want to error out and prevent generation, uncomment this line:
# raise OptionError(f"{self.player_name} has no sphere 1 locations!")
warning(
f"Warning: selected options for {self.player_name} have no sphere 1 locations. Giving Time Net.")
self.multiworld.push_precollected(net)
if self.options.gadget == "club":
self.multiworld.push_precollected(club)
self.itempool += [radar, shooter, hoop, flyer, car, punch]
elif self.options.gadget == "radar":
self.multiworld.push_precollected(radar)
self.itempool += [club, shooter, hoop, flyer, car, punch]
elif self.options.gadget == "sling":
self.multiworld.push_precollected(shooter)
self.itempool += [club, radar, hoop, flyer, car, punch]
elif self.options.gadget == "hoop":
self.multiworld.push_precollected(hoop)
self.itempool += [club, radar, shooter, flyer, car, punch]
elif self.options.gadget == "flyer":
self.multiworld.push_precollected(flyer)
self.itempool += [club, radar, shooter, hoop, car, punch]
elif self.options.gadget == "car":
self.multiworld.push_precollected(car)
self.itempool += [club, radar, shooter, hoop, flyer, punch]
elif self.options.gadget == "punch":
self.multiworld.push_precollected(punch)
self.itempool += [club, radar, shooter, hoop, flyer, car]
elif self.options.gadget == "none" or self.options.gadget == "waternet":
self.itempool += [club, radar, shooter, hoop, flyer, car, punch]
# Create "Victory" item for goals where the goal is at a location.
if self.options.goal == "mm" or self.options.goal == "mmtoken":
self.get_location(AELocation.Specter.value).place_locked_item(victory)
elif self.options.goal == "ppm" or self.options.goal == "ppmtoken":
self.get_location(AELocation.Specter2.value).place_locked_item(victory)
# This is where creating items for increasing special pellet maximums would go.
# Trap item fill: randomly pick items according to a set of weights.
# Trap weights: Banana Peel, Gadget Shuffle , Monkey Mash, Icy Hot Pants, Stun Trap
if self.options.trappercentage != 0:
custom_trapweights = [
self.options.trapweights[AEItem.BananaPeelTrap.value],
self.options.trapweights[AEItem.GadgetShuffleTrap.value],
self.options.trapweights[AEItem.MonkeyMashTrap.value],
self.options.trapweights[AEItem.IcyHotPantsTrap.value],
self.options.trapweights[AEItem.StunTrap.value],
self.options.trapweights[AEItem.CameraRotateTrap.value]
]
# If custom_trapweights are all zeros, reset to default values
if not any(y > 0 for y in custom_trapweights):
trap_weights = [15, 13, 5, 10, 7, 10]
else:
trap_weights = list(custom_trapweights)
trap_percentage = self.options.trappercentage / 100
trap_count = round((len(self.multiworld.get_unfilled_locations(self.player)) - len(self.itempool) - reservedlocations) * trap_percentage, None)
for x in range(1, len(trap_weights)):
trap_weights[x] = trap_weights[x] + trap_weights[x - 1]
for _ in range(trap_count):
randomTrap = self.random.randint(1, trap_weights[len(trap_weights) - 1])
if 0 < randomTrap <= trap_weights[0]:
self.itempool += [self.create_item_trap(AEItem.BananaPeelTrap.value)]
elif trap_weights[0] < randomTrap <= trap_weights[1]:
self.itempool += [self.create_item_trap(AEItem.GadgetShuffleTrap.value)]
elif trap_weights[1] < randomTrap <= trap_weights[2]:
self.itempool += [self.create_item_trap(AEItem.MonkeyMashTrap.value)]
elif trap_weights[2] < randomTrap <= trap_weights[3]:
self.itempool += [self.create_item_trap(AEItem.IcyHotPantsTrap.value)]
elif trap_weights[3] < randomTrap <= trap_weights[4]:
self.itempool += [self.create_item_trap(AEItem.CameraRotateTrap.value)]
else:
self.itempool += [self.create_item_trap(AEItem.StunTrap.value)]
# Junk item fill: randomly pick items according to a set of weights.
# Filler item weights are for 1 Jacket, 1/5 Cookies, 1/5/25 Energy Chips, 1/3 Explosive/Guided Pellets, Rainbow Cookie and Nothing, respectively.
custom_fillervalues = [
self.options.customfillerweights[AEItem.Shirt.value],
self.options.customfillerweights[AEItem.Cookie.value],
self.options.customfillerweights[AEItem.FiveCookies.value],
self.options.customfillerweights[AEItem.Triangle.value],
self.options.customfillerweights[AEItem.BigTriangle.value],
self.options.customfillerweights[AEItem.BiggerTriangle.value],
self.options.customfillerweights[AEItem.Flash.value],
self.options.customfillerweights[AEItem.ThreeFlash.value],
self.options.customfillerweights[AEItem.Rocket.value],
self.options.customfillerweights[AEItem.ThreeRocket.value],
self.options.customfillerweights[AEItem.RainbowCookie.value],
self.options.customfillerweights[AEItem.Nothing.value]
]
# Set filler item weights
allnothing = False
if self.options.fillerpreset == 0x00: # Normal
weights = [7, 16, 3, 31, 14, 4, 9, 3, 9, 3, 6, 0]
elif self.options.fillerpreset == 0x01: # Bountiful
weights = [11, 3, 8, 1, 4, 12, 2, 6, 2, 6, 5, 0] # Total of 60
elif self.options.fillerpreset == 0x02: # Stingy
weights = [3, 7, 1, 28, 7, 2, 5, 1, 5, 1, 3, 7] # Total of 70
elif self.options.fillerpreset == 0x03: # Nothing
allnothing = True
weights = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 99]
elif self.options.fillerpreset == 0x04: # Custom
# Failsafe: if the list is all zeroes, make every item a "Nothing"
if not any(y > 0 for y in custom_fillervalues):
allnothing = True
weights = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 99]
else:
weights = list(custom_fillervalues)
# Create filler items
for x in range(1, len(weights)):
weights[x] = weights[x] + weights[x - 1]
filler_count = len(self.multiworld.get_unfilled_locations(self.player)) - len(self.itempool) - reservedlocations
# Don't use weights if every filler item will be set to Nothing, as an optimization.
if allnothing == True:
for _ in range(filler_count):
self.itempool += [self.create_item_filler(AEItem.Nothing.value)]
else:
for _ in range(filler_count):
randomFiller = self.random.randint(1, weights[len(weights) - 1])
if 0 < randomFiller <= weights[0]:
self.itempool += [self.create_item_useful(AEItem.Shirt.value)]
elif weights[0] < randomFiller <= weights[1]:
self.itempool += [self.create_item_filler(AEItem.Cookie.value)]
elif weights[1] < randomFiller <= weights[2]:
self.itempool += [self.create_item_filler(AEItem.FiveCookies.value)]
elif weights[2] < randomFiller <= weights[3]:
self.itempool += [self.create_item_filler(AEItem.Triangle.value)]
elif weights[3] < randomFiller <= weights[4]:
self.itempool += [self.create_item_filler(AEItem.BigTriangle.value)]
elif weights[4] < randomFiller <= weights[5]:
self.itempool += [self.create_item_filler(AEItem.BiggerTriangle.value)]
elif weights[5] < randomFiller <= weights[6]:
self.itempool += [self.create_item_filler(AEItem.Flash.value)]
elif weights[6] < randomFiller <= weights[7]:
self.itempool += [self.create_item_useful(AEItem.ThreeFlash.value)]
elif weights[7] < randomFiller <= weights[8]:
self.itempool += [self.create_item_filler(AEItem.Rocket.value)]
elif weights[8] < randomFiller <= weights[9]:
self.itempool += [self.create_item_useful(AEItem.ThreeRocket.value)]
elif weights[9] < randomFiller <= weights[10]:
self.itempool += [self.create_item_useful(AEItem.RainbowCookie.value)]
else:
self.itempool += [self.create_item_filler(AEItem.Nothing.value)]
self.multiworld.itempool += self.itempool
def fill_slot_data(self):
bytestowrite = []
entranceids = []
newpositions = []
orderedfirstroomids = list(self.firstrooms)
for x in range(0, 22):
newpositions.append(self.levellist[x].newpos)
entranceids.append(self.entranceorder[x].entrance)
bytestowrite += self.entranceorder[x].bytes
bytestowrite.append(0) # We need a separator byte after each level name.
#self.firstrooms = orderedfirstroomids
return {
"goal": self.options.goal.value,
"fasttokengoal": self.options.fasttokengoal.value,
"allowcollect": self.options.allowcollect.value,
"requiredtokens": self.options.requiredtokens.value,
"totaltokens": self.options.totaltokens.value,
"tokenlocations": self.options.tokenlocations.value,
"logic": self.options.logic.value,
"infinitejump": self.options.infinitejump.value,
"superflyer": self.options.superflyer.value,
"entrance": self.options.entrance.value,
"randomizestartingroom": self.options.randomizestartingroom.value,
"unlocksperkey": self.options.unlocksperkey.value,
"extrakeys": self.options.extrakeys.value,
"coin": self.options.coin.value,
"mailbox": self.options.mailbox.value,
"lamp": self.options.lamp.value,
"gadget": self.options.gadget.value,
"shufflenet": self.options.shufflenet.value,
"shufflewaternet": self.options.shufflewaternet.value,
"lowoxygensounds": self.options.lowoxygensounds.value,
"fillerpreset": self.options.fillerpreset.value,
"customfillerweights": self.options.customfillerweights.value,
"trappercentage": self.options.trappercentage.value,
"trapweights": self.options.trapweights.value,
"trapsonreconnect": list(self.options.trapsonreconnect.value),
"trap_link": self.options.trap_link.value,
"itemdisplay": self.options.itemdisplay.value,
"kickoutprevention": self.options.kickoutprevention.value,
"autoequip": self.options.autoequip.value,
"spikecolor": self.options.spikecolor.value,
"customspikecolor": self.options.customspikecolor.value,
"levelnames": bytestowrite, # List of level names in entrance order. FF leads to the first.
"entranceids": entranceids, # Not used by the client. List of level ids in entrance order.
"newpositions": newpositions, # List of positions a level is moved to. The position of FF is first.
"firstrooms": orderedfirstroomids, # List of first rooms in entrance order.
"reqkeys": get_required_keys(self.options.unlocksperkey.value, self.options.goal.value, self.options.coin.value),
"death_link": self.options.death_link.value
}
# for the universal tracker, doesn't get called in standard gen
# docs: https://github.com/FarisTheAncient/Archipelago/blob/tracker/worlds/tracker/docs/re-gen-passthrough.md
@staticmethod
def interpret_slot_data(slot_data: Dict[str, Any]) -> Dict[str, Any]:
# returning slot_data so it regens, giving it back in multiworld.re_gen_passthrough
# we are using re_gen_passthrough over modifying the world here due to complexities with ER
return slot_data
def write_spoiler(self, spoiler_handle: TextIO):
if self.options.entrance.value != 0x00:
spoiler_handle.write(
f"\n\nApe Escape entrance connections for {self.multiworld.get_player_name(self.player)}:")
for x in range(0, 22):
spoiler_handle.write(f"\n {self.levellist[x].name} ==> {self.entranceorder[x].name}")
spoiler_handle.write(f"\n")
#def generate_output(self, output_directory: str):
#data = {
# "slot_data": self.fill_slot_data(),
# "location_to_item": {self.location_name_to_id[i.name] : item_table[i.item.name] for i in self.multiworld.get_locations() if not i.is_event},
# "data_package": {
# "data": {
# "games": {
# self.game: {
# "item_name_to_id": self.item_name_to_id,
# "location_name_to_id": self.location_name_to_id
# }
# }
# }
# }
#}
#filename = f"{self.multiworld.get_out_file_name_base(self.player)}.apae"
#with open(os.path.join(output_directory, filename), 'w') as f:
# json.dump(data, f)