Files
dockipelago/worlds/papermario/ItemPool.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

573 lines
25 KiB
Python

# not entirely, but partially from https://github.com/icebound777/PMR-SeedGenerator/blob/main/rando_modules/logic.py
# follows examples in OoT's implementation
from collections import namedtuple
from itertools import chain
from .data.chapter_logic import get_bowser_castle_removed_locations, areas_by_chapter, \
get_locations_beyond_spirit_requirements
from .data.ItemList import taycet_items, item_table, progression_miscitems, item_groups, item_multiples_base_name
from .data.LocationsList import location_groups, location_table, missable_locations, dojo_location_order, ch8_locations
from .options import *
from .data.item_exclusion import exclude_due_to_settings, exclude_from_taycet_placement
from .modules.modify_itempool import get_randomized_itempool
from BaseClasses import ItemClassification as Ic, LocationProgressType
from .Locations import location_factory
from .data.chapter_logic import get_chapter_excluded_item_names, get_chapter_excluded_location_names
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from . import PaperMarioWorld
def generate_itempool(pm_world):
world = pm_world.multiworld
player = pm_world.player
(pool, placed_items, placed_items_excluded) = get_pool_core(pm_world)
pm_world.itempool = [pm_world.create_item(item) for item in pool]
for (location_name, item) in placed_items.items():
location = world.get_location(location_name, player)
location.place_locked_item(pm_world.create_item(item, allow_arbitrary_name=True))
for (location_name, item) in placed_items_excluded.items():
location = location_factory(location_name, player)
location.place_locked_item(pm_world.create_item(item, allow_arbitrary_name=True))
pm_world.ch_excluded_locations.append(location)
def get_pool_core(world: "PaperMarioWorld"):
pool_misc_progression_items = []
pool_other_items = []
pool_progression_items = []
pool_coins_only = []
pool_illogical_consumables = []
pool_badges = []
pool = []
placed_items = {}
bc_removed_locations = []
# items and locations excluded from chapters for LCL get handled differently from normal excluded locations
ch_excluded_locations = []
ch_excluded_items = []
placed_items_excluded = {}
if world.options.spirit_requirements.value == SpiritRequirements.option_Specific_And_Limit_Chapter_Logic:
ch_excluded_locations = get_chapter_excluded_location_names(world.excluded_spirits,
world.options.letter_rewards.value)
ch_excluded_items = get_chapter_excluded_item_names(world.excluded_spirits)
# remove chapter 8 locations if star way is the goal
# otherwise remove any bowser castle locations removed by shortened or boss rush modes
if world.options.seed_goal.value == SeedGoal.option_Open_Star_Way:
ch_excluded_locations.extend(ch8_locations)
ch_excluded_items.extend(get_chapter_excluded_item_names([8]))
else:
bc_removed_locations = get_bowser_castle_removed_locations(world.options.bowser_castle_mode.value)
# Exclude locations that are either missable or are going to be considered not in logic based on settings
excluded_locations = missable_locations + get_locations_to_exclude(world, bc_removed_locations)
# remove unused items from the pool
for loc_name in location_table:
if loc_name not in ch_excluded_locations and loc_name not in bc_removed_locations:
location = world.get_location(loc_name)
else:
location = location_factory(loc_name, world.player)
if location.vanilla_item is None:
continue
item = location.vanilla_item
itemdata = item_table[item]
shuffle_item = True
# Excluded locations
if location.name in excluded_locations:
location.progress_type = LocationProgressType.EXCLUDED
# Sometimes placed items
if location.name in location_groups["OverworldCoin"]:
shuffle_item = world.options.overworld_coins.value
if not shuffle_item:
location.disabled = True
location.show_in_spoiler = False
if location.name in location_groups["BlockCoin"]:
shuffle_item = world.options.coin_blocks.value
if not shuffle_item:
location.disabled = True
location.show_in_spoiler = False
if location.name in location_groups["FoliageCoin"]:
shuffle_item = world.options.foliage_coins.value
if not shuffle_item:
location.disabled = True
location.show_in_spoiler = False
if location.name in location_groups["ShopItem"]:
if location.identifier in ["DRO_01/ShopItemB", "DRO_01/ShopItemD", "DRO_01/ShopItemE"]:
shuffle_item = (world.options.random_puzzles.value and world.options.include_shops.value
and not (world.options.spirit_requirements.value ==
SpiritRequirements.option_Specific_And_Limit_Chapter_Logic and
2 in world.excluded_spirits))
else:
shuffle_item = world.options.include_shops.value
if not shuffle_item:
location.disabled = True
location.show_in_spoiler = False
if location.name in location_groups["HiddenPanel"]:
shuffle_item = world.options.shuffle_hidden_panels.value
if not shuffle_item:
location.disabled = True
location.show_in_spoiler = False
if location.name in location_groups["FavorReward"]:
# coins get shuffled only if other rewards are also shuffled
if location.name in location_groups["FavorCoin"]:
shuffle_item = (world.options.koot_coins.value and
(world.options.koot_favors.value != ShuffleKootFavors.option_Vanilla))
else:
shuffle_item = (world.options.koot_favors.value != ShuffleKootFavors.option_Vanilla)
if not shuffle_item:
location.disabled = True
location.show_in_spoiler = False
if location.name in location_groups["FavorItem"]:
shuffle_item = (world.options.koot_favors.value == ShuffleKootFavors.option_Full_Shuffle)
if not shuffle_item:
location.disabled = True
location.show_in_spoiler = False
if location.name in location_groups["LetterReward"]:
if location.name == "GR Goomba Village Goompapa Letter Reward 2":
shuffle_item = (world.options.letter_rewards.value in [ShuffleLetters.option_Final_Letter_Chain_Reward,
ShuffleLetters.option_Full_Shuffle])
elif location.name in location_groups["LetterChain"]:
shuffle_item = (world.options.letter_rewards.value == ShuffleLetters.option_Full_Shuffle)
else:
shuffle_item = (world.options.letter_rewards.value != ShuffleLetters.option_Vanilla)
if not shuffle_item:
location.disabled = True
location.show_in_spoiler = False
if location.name in location_groups["RadioTradeEvent"]:
shuffle_item = world.options.trading_events.value
if not shuffle_item:
location.disabled = True
location.show_in_spoiler = False
if location.name in location_groups["DojoReward"]:
reward_num = dojo_location_order.index(location.name)
shuffle_item = (reward_num < world.options.dojo.value)
if not shuffle_item:
location.disabled = True
if location.vanilla_item == "Forest Pass":
shuffle_item = (not world.options.open_forest.value)
if not shuffle_item:
location.disabled = True
if location.name in location_groups["Partner"]:
shuffle_item = (world.options.partners.value != ShufflePartners.option_Off)
if not shuffle_item:
location.disabled = True
if location.name in location_groups["MultiCoinBlock"]:
shuffle_item = (world.options.super_multi_blocks.value > ShuffleSuperMultiBlocks.option_Off)
if location.name in location_groups["SuperBlock"]:
shuffle_item = world.options.partner_upgrades.value > PartnerUpgradeShuffle.option_Vanilla
if location.name in location_groups["Gear"]:
# hammer 1 bush is special in that it is made to not be empty even if starting with hammer
if location.name == "GR Jr. Troopa's Playground In Hammer Bush":
shuffle_item = ((world.options.gear_shuffle_mode.value != GearShuffleMode.option_Vanilla) or
(world.options.starting_hammer.value == StartingHammer.option_Hammerless))
else:
shuffle_item = (world.options.gear_shuffle_mode.value != GearShuffleMode.option_Vanilla)
if not shuffle_item:
location.disabled = True
if location.name == "SSS Star Sanctuary Gift of the Stars":
shuffle_item = world.options.shuffle_star_beam.value
if not shuffle_item:
location.disabled = True
location.show_in_spoiler = False
if location.name in ch_excluded_locations and item in ch_excluded_items:
shuffle_item = False
# add it to the proper pool, or place the item
if shuffle_item:
# hammer bush gets shuffled as a Tayce T item if shuffling gear locations and not hammerless
if (location.name == "GR Jr. Troopa's Playground In Hammer Bush" and
(world.options.gear_shuffle_mode.value == GearShuffleMode.option_Gear_Location_Shuffle) and
(world.options.starting_hammer.value != StartingHammer.option_Hammerless)):
pool_progression_items.append(world.random.choice([x for x in taycet_items
if x not in exclude_from_taycet_placement]))
# some progression items need to be in replenishable locations, we only need one of each
elif item in progression_miscitems:
if item not in pool_misc_progression_items:
pool_misc_progression_items.append(item)
else:
pool_illogical_consumables.append(item)
# progression items are shuffled; include gear and star pieces from rip cheato
elif (itemdata[1] == Ic.progression or
(location.name in location_groups["ShopItem"] and
world.options.include_shops.value and "Star Piece" in item)) and item not in ch_excluded_items:
pool_progression_items.append(item)
# split other items into their own pools; these other pools get modified before being sent elsewhere
elif itemdata[0] == "COIN":
pool_coins_only.append(item)
elif itemdata[0] == "ITEM":
pool_illogical_consumables.append(item)
elif itemdata[0] == "BADGE":
pool_badges.append(item)
else:
pool_other_items.append(item)
elif loc_name in ch_excluded_locations or loc_name in bc_removed_locations:
# keep out of logic placed items separate, remove the location and item from remaining excluded lists
placed_items_excluded[location.name] = item
# remove locations with placed items from the respective lists so we can get the item pool count correct
if location.name in bc_removed_locations:
bc_removed_locations.remove(location.name)
if location.name in ch_excluded_locations:
ch_excluded_locations.remove(location.name)
# below only applies to key items in the chapter
if item in ch_excluded_items:
ch_excluded_items.remove(item)
else:
placed_items[location.name] = item
# end of location for loop
# at this point every location's item should be either left unshuffled or added to a pool
# we want to modify these pools according to settings and make sure to have the right number of items
target_itempool_size = (
len(pool_progression_items)
+ len(pool_misc_progression_items)
+ len(pool_coins_only)
+ len(pool_illogical_consumables)
+ len(pool_badges)
+ len(pool_other_items)
- len(bc_removed_locations)
)
# add power stars
if world.options.power_star_hunt.value and world.options.total_power_stars.value > 0:
for i in range(0, world.options.total_power_stars.value):
pool_progression_items.append("Power Star")
# add 5 item pouches
if world.options.item_pouches.value:
for i in range(0, 5):
pool_other_items.append("Pouch Upgrade")
# add beta items
if world.options.beta_items.value:
pool_other_items.extend(item_groups["ItemBeta"])
for badge in item_groups["BadgeBeta"]:
pool_badges.append(get_item_multiples_base_name(badge))
# add unused badge dupes
if world.options.unused_badge_dupes.value:
for badge in item_groups["BadgeDupe"]:
if not (world.options.beta_items.value and badge in item_groups["BadgeBeta"]):
pool_badges.append(get_item_multiples_base_name(badge))
# add progressive badges
if world.options.progressive_badges.value:
# 3 copies of each progressive badge
for name in item_groups["ProgBadge"]:
for i in range(0, 3):
pool_badges.append(name)
# add normal boots
if world.options.starting_boots.value == StartingBoots.option_Jumpless:
pool_progression_items.append("Progressive Boots")
# add two of each partner upgrade item, remove the generic ones
if world.options.partner_upgrades.value != PartnerUpgradeShuffle.option_Vanilla:
for i in range(0, 2):
for upgrade in item_groups["PartnerUpgrade"]:
if upgrade == "Partner Upgrade":
for _ in range(0, 8):
pool_other_items.remove(upgrade)
else:
pool_other_items.append(upgrade)
# add traps
max_traps = 0
match world.options.item_traps.value:
case ItemTraps.option_Sparse:
max_traps = 15
case ItemTraps.option_Moderate:
max_traps = 35
case ItemTraps.option_Plenty:
max_traps = 80
case _:
max_traps = 0
pool_other_items.extend(["Damage Trap"] * max_traps)
# adjust item pools based on settings
items_to_remove_from_pools = get_items_to_exclude(world)
while items_to_remove_from_pools:
item = items_to_remove_from_pools.pop()
if item in pool_progression_items:
pool_progression_items.remove(item)
continue
if item in pool_misc_progression_items:
pool_misc_progression_items.remove(item)
continue
if item in pool_badges:
pool_badges.remove(item)
continue
if item in pool_other_items:
pool_other_items.remove(item)
continue
# If we have set a badge pool limit and exceed that, remove random badges
# until that condition is satisfied
if len(pool_badges) > world.options.badge_pool_limit.value:
world.random.shuffle(pool_badges)
while len(pool_badges) > world.options.badge_pool_limit.value:
pool_badges.pop()
# If the item pool is the wrong size now, fix it by filling up or clearing out items
cur_itempool_size = (
len(pool_progression_items)
+ len(pool_misc_progression_items)
+ len(pool_coins_only)
+ len(pool_illogical_consumables)
+ len(pool_badges)
+ len(pool_other_items)
)
# add random tayce t items if we need to add items for some reason
while target_itempool_size > cur_itempool_size:
pool_illogical_consumables.append(world.random.choice([x for x in taycet_items
if x not in exclude_from_taycet_placement]))
cur_itempool_size += 1
# remove coins first, then consumables if we need to keep going
if target_itempool_size < cur_itempool_size:
world.random.shuffle(pool_illogical_consumables)
while target_itempool_size < cur_itempool_size:
if len(pool_coins_only) > 20 or len(pool_illogical_consumables) == 0:
trashable_items = pool_coins_only
else:
trashable_items = pool_illogical_consumables
if trashable_items:
trashable_items.pop()
cur_itempool_size -= 1
else:
raise ValueError(f"Paper Mario: {world.player} ({world.multiworld.player_name[world.player]}) has too "
f"large of an item pool for the number of locations; consider increasing the number "
f"of checks available or reducing the badge or power star pools.")
# Re-join the non-required items into one array
pool_other_items.extend(pool_coins_only)
pool_other_items.extend(pool_illogical_consumables)
pool_other_items.extend(pool_badges)
# Randomize consumables if needed
pool_other_items = get_randomized_itempool(
pool_other_items,
world.options.consumable_item_pool.value,
world.options.consumable_item_quality.value,
world.options.beta_items.value,
world.random
)
if ch_excluded_locations:
world.random.shuffle(ch_excluded_locations)
# shuffle items but sort to put useful items in front so that filler items go to out of logic locations first
world.random.shuffle(pool_other_items)
pool_other_items.sort(key=lambda item: 1 if item_table[item][1] == Ic.filler else 0)
# save some filler items for the excluded locations; not the chapter ones, but from get_locations_to_exclude
for _ in excluded_locations:
pool.append(pool_other_items.pop())
for loc in ch_excluded_locations:
placed_items_excluded[loc] = pool_other_items.pop()
# now we have the full pool
pool.extend(pool_progression_items)
pool.extend(pool_other_items)
pool.extend(pool_misc_progression_items)
return pool, placed_items, placed_items_excluded
def get_items_to_exclude(world: "PaperMarioWorld") -> list:
"""
Returns a list of items that should not be placed or given to Mario at the
start.
"""
excluded_items = []
if world.options.dojo.value:
for item_name in exclude_due_to_settings.get("do_randomize_dojo"):
excluded_items.append(item_name)
if world.options.start_with_goombario.value:
excluded_items.append("Goombario")
if world.options.start_with_kooper.value:
excluded_items.append("Kooper")
if world.options.start_with_bombette.value:
excluded_items.append("Bombette")
if world.options.start_with_parakarry.value:
excluded_items.append("Parakarry")
if world.options.start_with_bow.value:
excluded_items.append("Bow")
if world.options.start_with_watt.value:
excluded_items.append("Watt")
if world.options.start_with_sushie.value:
excluded_items.append("Sushie")
if world.options.start_with_lakilester.value:
excluded_items.append("Lakilester")
if world.options.open_blue_house.value:
for item_name in exclude_due_to_settings.get("startwith_bluehouse_open"):
excluded_items.append(item_name)
if world.options.open_forest.value:
for item_name in exclude_due_to_settings.get("startwith_forest_open"):
excluded_items.append(item_name)
if world.options.magical_seeds.value < 4:
for item_name in exclude_due_to_settings.get("magical_seeds_required").get(world.options.magical_seeds.value):
excluded_items.append(item_name)
if world.options.bowser_castle_mode.value > BowserCastleMode.option_Vanilla:
for item_name in exclude_due_to_settings.get("shorten_bowsers_castle"):
excluded_items.append(item_name)
if world.options.bowser_castle_mode.value == BowserCastleMode.option_Boss_Rush:
for item_name in exclude_due_to_settings.get("boss_rush"):
excluded_items.append(item_name)
if world.options.always_speedy_spin.value:
for item_name in exclude_due_to_settings.get("always_speedyspin"):
excluded_items.append(item_name)
if world.options.always_ispy.value:
for item_name in exclude_due_to_settings.get("always_ispy"):
excluded_items.append(item_name)
if world.options.always_peekaboo.value:
for item_name in exclude_due_to_settings.get("always_peekaboo"):
excluded_items.append(item_name)
if world.options.progressive_badges.value:
for item_name in exclude_due_to_settings.get("do_progressive_badges"):
excluded_items.append(item_name)
if world.options.starting_hammer.value == StartingHammer.option_Ultra:
excluded_items.append("Progressive Hammer")
if world.options.starting_hammer.value >= StartingHammer.option_Super:
excluded_items.append("Progressive Hammer")
if (world.options.starting_hammer.value >= StartingHammer.option_Normal and
world.options.gear_shuffle_mode.value != GearShuffleMode.option_Gear_Location_Shuffle):
excluded_items.append("Progressive Hammer")
if world.options.starting_boots.value == StartingBoots.option_Ultra:
excluded_items.append("Progressive Boots")
if world.options.starting_boots.value >= StartingBoots.option_Super:
excluded_items.append("Progressive Boots")
if world.options.partner_upgrades.value:
for item_name in exclude_due_to_settings.get("partner_upgrade_shuffle"):
excluded_items.append(item_name)
return excluded_items
def get_locations_to_exclude(world: "PaperMarioWorld", bc_removed_locations: list) -> list:
excluded_locations = []
# exclude locations which require more star spirits than are expected to be needed to beat the seed
if not world.options.power_star_hunt.value:
excluded_locations.extend(get_locations_beyond_spirit_requirements(world.options.star_way_spirits.value))
# below lines turned off to see if letting late game locations not be excluded is a problem or not
# exclude some amount of chapter 8 locations depending upon access requirements
# if world.options.seed_goal.value != SeedGoal.option_Open_Star_Way:
# late_game_locations = ch8_locations.copy()
# for bc_loc in bc_removed_locations:
# late_game_locations.remove(bc_loc)
#
# if not world.options.shuffle_star_beam.value:
# late_game_locations.remove("SSS Star Sanctuary Gift of the Stars")
#
# late_game_exclude_rate = get_star_haven_access_ratio(world.options) * 100
#
# for location in late_game_locations:
# if world.random.randint(1, 100) <= late_game_exclude_rate:
# excluded_locations.append(location)
# exclude merlow rewards
if not world.options.merlow_items.value:
excluded_locations.extend(location_groups["MerlowReward"])
# exclude rowf item locations
for location in location_groups["RowfShop"]:
set_number: int = int(location[34]) # Example string: "TT Plaza District Rowf's Shop Set 1 - 1"
if location not in excluded_locations and set_number > world.options.rowf_items.value:
excluded_locations.append(location)
# exclude rip cheato locations
for i in range(0, 11):
location_name = location_groups["RipCheato"][i]
if location_table[location_name][4] >= world.options.cheato_items.value:
excluded_locations.append(location_groups["RipCheato"][i])
# remove anything from the list that is already removed for LCL
for loc in excluded_locations:
if loc in world.ch_excluded_location_names:
excluded_locations.remove(loc)
return excluded_locations
def get_item_multiples_base_name(item_name: str) -> str:
if item_name in item_multiples_base_name.keys():
return item_multiples_base_name[item_name]
return item_name
def get_star_haven_access_ratio(options: PaperMarioOptions):
if options.seed_goal.value == SeedGoal.option_Open_Star_Way:
return 1
else:
if options.power_star_hunt.value:
return (options.star_way_power_stars.value / options.total_power_stars.value + options.star_way_spirits.value / 7) / 2
else:
return options.star_way_spirits.value / 7