AutoWorld: Add "stage" methods and implement LttP Dungeon fill as an example.
This commit is contained in:
parent
95350a1fa9
commit
01d88c362a
14
Main.py
14
Main.py
|
@ -14,7 +14,6 @@ from BaseClasses import MultiWorld, CollectionState, Region, Item
|
|||
from worlds.alttp.Items import item_name_groups
|
||||
from worlds.alttp.Regions import lookup_vanilla_location_to_entrance
|
||||
from worlds.alttp.Rom import patch_rom, patch_race_rom, patch_enemizer, apply_rom_settings, LocalRom, get_hash_string
|
||||
from worlds.alttp.Dungeons import fill_dungeons, fill_dungeons_restrictive
|
||||
from Fill import distribute_items_restrictive, flood_items, balance_multiworld_progression, distribute_planned
|
||||
from worlds.alttp.Shops import ShopSlotFill, SHOP_ID_START, total_shop_slots, FillDisabledShopSlots
|
||||
from worlds.alttp.ItemPool import difficulties, fill_prizes
|
||||
|
@ -218,18 +217,9 @@ def main(args, seed=None):
|
|||
|
||||
distribute_planned(world)
|
||||
|
||||
logger.info('Placing Dungeon Prizes.')
|
||||
logger.info('Running Pre Main Fill.')
|
||||
|
||||
fill_prizes(world)
|
||||
|
||||
logger.info('Placing Dungeon Items.')
|
||||
|
||||
if world.algorithm in ['balanced', 'vt26'] or any(
|
||||
list(args.mapshuffle.values()) + list(args.compassshuffle.values()) +
|
||||
list(args.keyshuffle.values()) + list(args.bigkeyshuffle.values())):
|
||||
fill_dungeons_restrictive(world)
|
||||
else:
|
||||
fill_dungeons(world)
|
||||
AutoWorld.call_all(world, "pre_fill")
|
||||
|
||||
logger.info('Fill the world.')
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ from BaseClasses import MultiWorld, Item, CollectionState
|
|||
|
||||
|
||||
class AutoWorldRegister(type):
|
||||
world_types:Dict[str, World] = {}
|
||||
world_types: Dict[str, World] = {}
|
||||
|
||||
def __new__(cls, name, bases, dct):
|
||||
# filter out any events
|
||||
|
@ -20,7 +20,6 @@ class AutoWorldRegister(type):
|
|||
dct["location_names"] = frozenset(dct["location_name_to_id"])
|
||||
dct["all_names"] = dct["item_names"] | dct["location_names"] | set(dct.get("item_name_groups", {}))
|
||||
|
||||
|
||||
# construct class
|
||||
new_class = super().__new__(cls, name, bases, dct)
|
||||
if "game" in dct:
|
||||
|
@ -45,8 +44,14 @@ def call_single(world: MultiWorld, method_name: str, player: int, *args):
|
|||
|
||||
|
||||
def call_all(world: MultiWorld, method_name: str, *args):
|
||||
world_types = set()
|
||||
for player in world.player_ids:
|
||||
world_types.add(world.worlds[player].__class__)
|
||||
call_single(world, method_name, player, *args)
|
||||
for world_type in world_types:
|
||||
stage_callable = getattr(world_type, f"stage_{method_name}", None)
|
||||
if stage_callable:
|
||||
stage_callable(world)
|
||||
|
||||
|
||||
class World(metaclass=AutoWorldRegister):
|
||||
|
@ -54,7 +59,7 @@ class World(metaclass=AutoWorldRegister):
|
|||
A Game should have its own subclass of World in which it defines the required data structures."""
|
||||
|
||||
options: dict = {} # link your Options mapping
|
||||
game: str # name the game
|
||||
game: str # name the game
|
||||
topology_present: bool = False # indicate if world type has any meaningful layout/pathing
|
||||
all_names: Set[str] = frozenset() # gets automatically populated with all item, item group and location names
|
||||
|
||||
|
@ -91,6 +96,9 @@ class World(metaclass=AutoWorldRegister):
|
|||
self.player = player
|
||||
|
||||
# overridable methods that get called by Main.py, sorted by execution order
|
||||
# can also be implemented as a classmethod and called "stage_<original_name",
|
||||
# in that case the MultiWorld object is passed as an argument and it gets called once for the entire multiworld.
|
||||
# An example of this can be found in alttp as stage_pre_fill
|
||||
def generate_early(self):
|
||||
pass
|
||||
|
||||
|
@ -106,6 +114,10 @@ class World(metaclass=AutoWorldRegister):
|
|||
def generate_basic(self):
|
||||
pass
|
||||
|
||||
def pre_fill(self):
|
||||
"""Optional method that is supposed to be used for special fill stages. This is run *after* plando."""
|
||||
pass
|
||||
|
||||
def generate_output(self, output_directory: str):
|
||||
"""This method gets called from a threadpool, do not use world.random here.
|
||||
If you need any last-second randomization, use MultiWorld.slot_seeds[slot] instead."""
|
||||
|
|
|
@ -44,75 +44,6 @@ def create_dungeons(world, player):
|
|||
|
||||
world.dungeons += [ES, EP, DP, ToH, AT, PoD, TT, SW, SP, IP, MM, TR, GT]
|
||||
|
||||
def fill_dungeons(world):
|
||||
#All chests on the freebes list locked behind a key in room with no other exit
|
||||
freebes = ['Ganons Tower - Map Chest', 'Palace of Darkness - Harmless Hellway', 'Palace of Darkness - Big Key Chest', 'Turtle Rock - Big Key Chest']
|
||||
|
||||
all_state_base = world.get_all_state()
|
||||
|
||||
dungeons = [(list(dungeon.regions), dungeon.big_key, list(dungeon.small_keys), list(dungeon.dungeon_items)) for dungeon in world.dungeons]
|
||||
|
||||
loopcnt = 0
|
||||
while dungeons:
|
||||
loopcnt += 1
|
||||
dungeon_regions, big_key, small_keys, dungeon_items = dungeons.pop(0)
|
||||
# this is what we need to fill
|
||||
dungeon_locations = [location for location in world.get_unfilled_locations() if location.parent_region.name in dungeon_regions]
|
||||
world.random.shuffle(dungeon_locations)
|
||||
|
||||
all_state = all_state_base.copy()
|
||||
|
||||
# first place big key
|
||||
if big_key is not None:
|
||||
bk_location = None
|
||||
for location in dungeon_locations:
|
||||
if location.item_rule(big_key):
|
||||
bk_location = location
|
||||
break
|
||||
|
||||
if bk_location is None:
|
||||
raise RuntimeError('No suitable location for %s' % big_key)
|
||||
|
||||
world.push_item(bk_location, big_key, False)
|
||||
bk_location.event = True
|
||||
bk_location.locked = True
|
||||
dungeon_locations.remove(bk_location)
|
||||
big_key = None
|
||||
|
||||
# next place small keys
|
||||
while small_keys:
|
||||
small_key = small_keys.pop()
|
||||
all_state.sweep_for_events()
|
||||
sk_location = None
|
||||
for location in dungeon_locations:
|
||||
if location.name in freebes or (location.can_reach(all_state) and location.item_rule(small_key)):
|
||||
sk_location = location
|
||||
break
|
||||
|
||||
if sk_location is None:
|
||||
# need to retry this later
|
||||
small_keys.append(small_key)
|
||||
dungeons.append((dungeon_regions, big_key, small_keys, dungeon_items))
|
||||
# infinite regression protection
|
||||
if loopcnt < (30 * world.players):
|
||||
break
|
||||
else:
|
||||
raise RuntimeError('No suitable location for %s' % small_key)
|
||||
|
||||
world.push_item(sk_location, small_key, False)
|
||||
sk_location.event = True
|
||||
sk_location.locked = True
|
||||
dungeon_locations.remove(sk_location)
|
||||
|
||||
if small_keys:
|
||||
# key placement not finished, loop again
|
||||
continue
|
||||
|
||||
# next place dungeon items
|
||||
for dungeon_item in dungeon_items:
|
||||
di_location = dungeon_locations.pop()
|
||||
world.push_item(di_location, dungeon_item, False)
|
||||
|
||||
|
||||
def get_dungeon_item_pool(world):
|
||||
items = [item for dungeon in world.dungeons for item in dungeon.all_items]
|
||||
|
@ -120,28 +51,27 @@ def get_dungeon_item_pool(world):
|
|||
item.world = world
|
||||
return items
|
||||
|
||||
|
||||
def fill_dungeons_restrictive(world):
|
||||
"""Places dungeon-native items into their dungeons, places nothing if everything is shuffled outside."""
|
||||
restricted_players = {player for player, restricted in world.restrict_dungeon_item_on_boss.items() if restricted}
|
||||
|
||||
locations = [location for location in world.get_unfilled_dungeon_locations()
|
||||
if not (location.player in restricted_players and location.name in lookup_boss_drops)] # filter boss
|
||||
|
||||
world.random.shuffle(locations)
|
||||
all_state_base = world.get_all_state()
|
||||
|
||||
# with shuffled dungeon items they are distributed as part of the normal item pool
|
||||
for item in world.get_items():
|
||||
if (item.smallkey and world.keyshuffle[item.player]) or (item.bigkey and world.bigkeyshuffle[item.player]):
|
||||
all_state_base.collect(item, True)
|
||||
item.advancement = True
|
||||
|
||||
dungeon_items = [item for item in get_dungeon_item_pool(world) if (((item.smallkey and not world.keyshuffle[item.player])
|
||||
or (item.bigkey and not world.bigkeyshuffle[item.player])
|
||||
or (item.map and not world.mapshuffle[item.player])
|
||||
or (item.compass and not world.compassshuffle[item.player])
|
||||
) and world.goal[item.player] != 'icerodhunt')] #
|
||||
dungeon_items = [item for item in get_dungeon_item_pool(world) if
|
||||
(((item.smallkey and not world.keyshuffle[item.player])
|
||||
or (item.bigkey and not world.bigkeyshuffle[item.player])
|
||||
or (item.map and not world.mapshuffle[item.player])
|
||||
or (item.compass and not world.compassshuffle[item.player])
|
||||
) and world.goal[item.player] != 'icerodhunt')]
|
||||
if dungeon_items:
|
||||
restricted_players = {player for player, restricted in world.restrict_dungeon_item_on_boss.items() if restricted}
|
||||
locations = [location for location in world.get_unfilled_dungeon_locations()
|
||||
if not (location.player in restricted_players and location.name in lookup_boss_drops)] # filter boss
|
||||
|
||||
world.random.shuffle(locations)
|
||||
all_state_base = world.get_all_state()
|
||||
# with shuffled dungeon items they are distributed as part of the normal item pool
|
||||
for item in world.get_items():
|
||||
if (item.smallkey and world.keyshuffle[item.player]) or (item.bigkey and world.bigkeyshuffle[item.player]):
|
||||
all_state_base.collect(item, True)
|
||||
item.advancement = True
|
||||
# sort in the order Big Key, Small Key, Other before placing dungeon items
|
||||
sort_order = {"BigKey": 3, "SmallKey": 2}
|
||||
dungeon_items.sort(key=lambda item: sort_order.get(item.type, 1))
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import random
|
||||
import logging
|
||||
|
||||
from BaseClasses import Item, CollectionState
|
||||
from .SubClasses import ALttPItem
|
||||
|
@ -14,6 +15,8 @@ from .Dungeons import create_dungeons
|
|||
from .InvertedRegions import create_inverted_regions, mark_dark_world_regions
|
||||
from .EntranceShuffle import link_entrances, link_inverted_entrances, plando_connect
|
||||
|
||||
lttp_logger = logging.getLogger("A Link to the Past")
|
||||
|
||||
class ALTTPWorld(World):
|
||||
game: str = "A Link to the Past"
|
||||
options = alttp_options
|
||||
|
@ -131,6 +134,47 @@ class ALTTPWorld(World):
|
|||
return True
|
||||
return False
|
||||
|
||||
def pre_fill(self):
|
||||
from Fill import fill_restrictive, FillError
|
||||
attempts = 5
|
||||
world = self.world
|
||||
player = self.player
|
||||
all_state = world.get_all_state(keys=True)
|
||||
crystals = [self.create_item(name) for name in ['Red Pendant', 'Blue Pendant', 'Green Pendant', 'Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 7', 'Crystal 5', 'Crystal 6']]
|
||||
crystal_locations = [world.get_location('Turtle Rock - Prize', player),
|
||||
world.get_location('Eastern Palace - Prize', player),
|
||||
world.get_location('Desert Palace - Prize', player),
|
||||
world.get_location('Tower of Hera - Prize', player),
|
||||
world.get_location('Palace of Darkness - Prize', player),
|
||||
world.get_location('Thieves\' Town - Prize', player),
|
||||
world.get_location('Skull Woods - Prize', player),
|
||||
world.get_location('Swamp Palace - Prize', player),
|
||||
world.get_location('Ice Palace - Prize', player),
|
||||
world.get_location('Misery Mire - Prize', player)]
|
||||
placed_prizes = {loc.item.name for loc in crystal_locations if loc.item}
|
||||
unplaced_prizes = [crystal for crystal in crystals if crystal.name not in placed_prizes]
|
||||
empty_crystal_locations = [loc for loc in crystal_locations if not loc.item]
|
||||
for attempt in range(attempts):
|
||||
try:
|
||||
prizepool = unplaced_prizes.copy()
|
||||
prize_locs = empty_crystal_locations.copy()
|
||||
world.random.shuffle(prize_locs)
|
||||
fill_restrictive(world, all_state, prize_locs, prizepool, True, lock=True)
|
||||
except FillError as e:
|
||||
lttp_logger.exception("Failed to place dungeon prizes (%s). Will retry %s more times", e,
|
||||
attempts - attempt)
|
||||
for location in empty_crystal_locations:
|
||||
location.item = None
|
||||
continue
|
||||
break
|
||||
else:
|
||||
raise FillError('Unable to place dungeon prizes')
|
||||
|
||||
@classmethod
|
||||
def stage_pre_fill(cls, world):
|
||||
from .Dungeons import fill_dungeons_restrictive
|
||||
fill_dungeons_restrictive(world)
|
||||
|
||||
def get_required_client_version(self) -> tuple:
|
||||
return max((0, 1, 4), super(ALTTPWorld, self).get_required_client_version())
|
||||
|
||||
|
|
Loading…
Reference in New Issue