Files
dockipelago/worlds/tloz_oos/generation/GenerateEarly.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

233 lines
11 KiB
Python

import logging
from Options import OptionError
from ..Options import OracleOfSeasonsOldMenShuffle, OracleOfSeasonsLinkedHerosCave
from ..Util import get_old_man_values_pool
from ..World import OracleOfSeasonsWorld
from ..data import LOCATIONS_DATA, ITEMS_DATA
from ..data.Constants import DIRECTIONS, SEASONS, DIRECTION_LEFT, DIRECTION_UP, SEASON_NAMES, VALID_RUPEE_PRICE_VALUES, AVERAGE_PRICE_PER_LOCATION
def generate_early(world: OracleOfSeasonsWorld) -> None:
if world.options.randomize_ai:
world.options.golden_beasts_requirement.value = 0
conflicting_rings = world.options.required_rings.value & world.options.excluded_rings.value
if len(conflicting_rings) > 0:
raise OptionError("Required Rings and Excluded Rings contain the same element(s)", conflicting_rings)
world.remaining_progressive_gasha_seeds = world.options.deterministic_gasha_locations.value
pick_essences_in_game(world)
if len(world.essences_in_game) < world.options.treehouse_old_man_requirement:
world.options.treehouse_old_man_requirement.value = len(world.essences_in_game)
restrict_non_local_items(world)
randomize_default_seasons(world)
randomize_old_men(world)
if world.options.randomize_lost_woods_item_sequence:
# Pick 4 random seasons & directions (last one has to be "left")
world.lost_woods_item_sequence = []
for i in range(4):
world.lost_woods_item_sequence.append([
world.random.choice(DIRECTIONS) if i < 3 else DIRECTION_LEFT,
world.random.choice(SEASONS)
])
if world.options.randomize_lost_woods_main_sequence:
# Pick 4 random seasons & directions (last one has to be "up")
world.lost_woods_main_sequence = []
for i in range(4):
world.lost_woods_main_sequence.append([
world.random.choice(DIRECTIONS) if i < 3 else DIRECTION_UP,
world.random.choice(SEASONS)
])
if world.options.randomize_samasa_gate_code:
world.samasa_gate_code = []
for i in range(world.options.samasa_gate_code_length.value):
world.samasa_gate_code.append(world.random.randint(0, 3))
randomize_shop_order(world)
randomize_shop_prices(world)
compute_rupee_requirements(world)
create_random_rings_pool(world)
if world.options.linked_heros_cave.value & OracleOfSeasonsLinkedHerosCave.samasa:
world.dungeon_entrances["d11 entrance"] = "enter d11"
world.item_mapping_collect = {
"Rupees (1)": ("Rupees", 1),
"Rupees (5)": ("Rupees", 5),
"Rupees (10)": ("Rupees", 10),
"Rupees (20)": ("Rupees", 20),
"Rupees (30)": ("Rupees", 30),
"Rupees (50)": ("Rupees", 50),
"Rupees (100)": ("Rupees", 100),
"Rupees (200)": ("Rupees", 200),
"_reached_d2_rupee_room": ("Rupees", 150),
"_reached_d6_rupee_room": ("Rupees", 90),
"Ore Chunks (10)": ("Ore Chunks", 10),
"Ore Chunks (25)": ("Ore Chunks", 25),
"Ore Chunks (50)": ("Ore Chunks", 50),
"Bombs (10)": ("Bombs", 10),
"Bombs (20)": ("Bombs", 20),
"Bombchus (10)": ("Bombchus", 10),
"Bombchus (20)": ("Bombchus", 20),
}
for old_man in world.old_man_rupee_values:
rupees = world.old_man_rupee_values[old_man]
rupees = max(rupees, 0) # We ignore negative value because they will most often do nothing
# If this becomes an issue, state initialisation shall account for negative values
world.item_mapping_collect[f"rupees from {old_man}"] = ("Rupees", rupees)
def pick_essences_in_game(world: OracleOfSeasonsWorld) -> None:
# -1 is the named range to set the placed essences equal to the required essences
if world.options.placed_essences.value == -1:
world.options.placed_essences.value = world.options.placed_essences.value
# If the value for "Placed Essences" is lower than "Required Essences" (which can happen when using random
# values for both), a new random value is automatically picked in the valid range.
elif world.options.required_essences > world.options.placed_essences:
new_placed_essences = world.random.randint(world.options.required_essences.value, 8)
logging.warn(f"Essences placed for {world.player_name} required to be {world.options.placed_essences.value} "
f"but {world.options.required_essences} essences are required to beat the seed.\n"
f"Increased the value to {new_placed_essences}. "
f"You might want to set the range to 'included essences' or use triggers instead.")
world.options.placed_essences.value = new_placed_essences
# If some essence pedestal locations were excluded and essences are not shuffled,
# remove those essences in priority
if not world.options.shuffle_essences:
excluded_locations_data = {name: data for name, data in LOCATIONS_DATA.items() if name in world.options.exclude_locations.value}
for loc_name, loc_data in excluded_locations_data.items():
if "essence" in loc_data and loc_data["essence"] is True:
world.essences_in_game.remove(loc_data["vanilla_item"])
if len(world.essences_in_game) < world.options.required_essences:
raise ValueError("Too many essence pedestal locations were excluded, seed will be unbeatable")
# If we need to remove more essences, pick them randomly
world.random.shuffle(world.essences_in_game)
world.essences_in_game = world.essences_in_game[0:world.options.placed_essences]
def restrict_non_local_items(world: OracleOfSeasonsWorld) -> None:
# Restrict non_local_items option in cases where it's incompatible with other options that enforce items
# to be placed locally (e.g. dungeon items with keysanity off)
if not world.options.keysanity_small_keys:
world.options.non_local_items.value -= world.item_name_groups["Small Keys"]
world.options.non_local_items.value -= world.item_name_groups["Master Keys"]
if not world.options.keysanity_boss_keys:
world.options.non_local_items.value -= world.item_name_groups["Boss Keys"]
if not world.options.keysanity_maps_compasses:
world.options.non_local_items.value -= world.item_name_groups["Dungeon Maps"]
world.options.non_local_items.value -= world.item_name_groups["Compasses"]
def randomize_default_seasons(world: OracleOfSeasonsWorld) -> None:
if world.options.default_seasons == "randomized":
seasons_pool = SEASONS
elif world.options.default_seasons.current_key.endswith("singularity"):
single_season = world.options.default_seasons.current_key.replace("_singularity", "")
if single_season == "random":
single_season = world.random.choice(SEASONS)
else:
single_season = next(byte for byte, name in SEASON_NAMES.items() if name == single_season)
seasons_pool = [single_season]
else:
return
for region in world.default_seasons:
if region == "HORON_VILLAGE" and not world.options.normalize_horon_village_season:
continue
world.default_seasons[region] = world.random.choice(seasons_pool)
def randomize_old_men(world: OracleOfSeasonsWorld) -> None:
if world.options.shuffle_old_men == OracleOfSeasonsOldMenShuffle.option_shuffled_values:
shuffled_rupees = list(world.old_man_rupee_values.values())
world.random.shuffle(shuffled_rupees)
world.old_man_rupee_values = dict(zip(world.old_man_rupee_values, shuffled_rupees))
elif world.options.shuffle_old_men == OracleOfSeasonsOldMenShuffle.option_random_values:
for key in world.old_man_rupee_values.keys():
sign = world.random.choice([-1, 1])
world.old_man_rupee_values[key] = world.random.choice(get_old_man_values_pool()) * sign
elif world.options.shuffle_old_men == OracleOfSeasonsOldMenShuffle.option_random_positive_values:
for key in world.old_man_rupee_values.keys():
world.old_man_rupee_values[key] = world.random.choice(get_old_man_values_pool())
else:
# Remove the old man values from the pool so that they don't count negative when they are shuffled as items
world.old_man_rupee_values = {}
def randomize_shop_order(world: OracleOfSeasonsWorld) -> None:
world.shop_order = [
["horonShop1", "horonShop2", "horonShop3"],
["memberShop1", "memberShop2", "memberShop3"],
["syrupShop1", "syrupShop2", "syrupShop3"]
]
if world.options.advance_shop:
world.shop_order.append(["advanceShop1", "advanceShop2", "advanceShop3"])
if world.options.shuffle_business_scrubs:
world.shop_order.extend([["spoolSwampScrub"], ["samasaCaveScrub"], ["d2Scrub"], ["d4Scrub"]])
world.random.shuffle(world.shop_order)
def randomize_shop_prices(world: OracleOfSeasonsWorld) -> None:
if world.options.shop_prices == "vanilla":
if world.options.enforce_potion_in_shop:
world.shop_prices["horonShop3"] = 300
return
if world.options.shop_prices == "free":
world.shop_prices = {k: 0 for k in world.shop_prices}
return
# Prices are randomized, get a random price that follow set options for each shop location.
# Values must be rounded to nearest valid rupee amount.
average = AVERAGE_PRICE_PER_LOCATION[world.options.shop_prices.current_key]
deviation = min(19 * (average / 50), 100)
for i, shop in enumerate(world.shop_order):
shop_price_factor = (i / len(world.shop_order)) + 0.5
for location_code in shop:
value = world.random.gauss(average, deviation) * shop_price_factor
world.shop_prices[location_code] = min(VALID_RUPEE_PRICE_VALUES, key=lambda x: abs(x - value))
# Subrosia market special cases
for i in range(2, 6):
value = world.random.gauss(average, deviation) * 0.5
world.shop_prices[f"subrosianMarket{i}"] = min(VALID_RUPEE_PRICE_VALUES, key=lambda x: abs(x - value))
def compute_rupee_requirements(world: OracleOfSeasonsWorld) -> None:
# Compute global rupee requirements for each shop, based on shop order and item prices
cumulated_requirement = 0
for shop in world.shop_order:
if shop[0].startswith("advance") and not world.options.advance_shop:
continue
if shop[0].endswith("Scrub") and not world.options.shuffle_business_scrubs:
continue
# Add the price of each shop location in there to the requirement
for shop_location in shop:
cumulated_requirement += world.shop_prices[shop_location]
# Deduce the shop name from the code of the first location
shop_name = shop[0]
if not shop_name.endswith("Scrub"):
shop_name = shop_name[:-1]
world.shop_rupee_requirements[shop_name] = cumulated_requirement
def create_random_rings_pool(world: OracleOfSeasonsWorld) -> None:
# Get a subset of as many rings as needed, with a potential filter depending on chosen options
ring_names = [name for name, idata in ITEMS_DATA.items() if "ring" in idata]
# Remove required rings because they'll be added later anyway
ring_names = [name for name in ring_names if name not in world.options.required_rings.value and name not in world.options.excluded_rings.value]
world.random.shuffle(ring_names)
world.random_rings_pool = ring_names