mirror of
https://github.com/MarioSpore/Grinch-AP.git
synced 2025-10-21 12:11:33 -06:00
Core: Plando Items "Rewrite" (#3046)
This commit is contained in:
@@ -9,8 +9,9 @@ from argparse import Namespace
|
|||||||
from collections import Counter, deque
|
from collections import Counter, deque
|
||||||
from collections.abc import Collection, MutableSequence
|
from collections.abc import Collection, MutableSequence
|
||||||
from enum import IntEnum, IntFlag
|
from enum import IntEnum, IntFlag
|
||||||
from typing import (AbstractSet, Any, Callable, ClassVar, Dict, Iterable, Iterator, List, Mapping, NamedTuple,
|
from typing import (AbstractSet, Any, Callable, ClassVar, Dict, Iterable, Iterator, List, Literal, Mapping, NamedTuple,
|
||||||
Optional, Protocol, Set, Tuple, Union, TYPE_CHECKING)
|
Optional, Protocol, Set, Tuple, Union, TYPE_CHECKING)
|
||||||
|
import dataclasses
|
||||||
|
|
||||||
from typing_extensions import NotRequired, TypedDict
|
from typing_extensions import NotRequired, TypedDict
|
||||||
|
|
||||||
@@ -54,12 +55,21 @@ class HasNameAndPlayer(Protocol):
|
|||||||
player: int
|
player: int
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class PlandoItemBlock:
|
||||||
|
player: int
|
||||||
|
from_pool: bool
|
||||||
|
force: bool | Literal["silent"]
|
||||||
|
worlds: set[int] = dataclasses.field(default_factory=set)
|
||||||
|
items: list[str] = dataclasses.field(default_factory=list)
|
||||||
|
locations: list[str] = dataclasses.field(default_factory=list)
|
||||||
|
resolved_locations: list[Location] = dataclasses.field(default_factory=list)
|
||||||
|
count: dict[str, int] = dataclasses.field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
class MultiWorld():
|
class MultiWorld():
|
||||||
debug_types = False
|
debug_types = False
|
||||||
player_name: Dict[int, str]
|
player_name: Dict[int, str]
|
||||||
plando_texts: List[Dict[str, str]]
|
|
||||||
plando_items: List[List[Dict[str, Any]]]
|
|
||||||
plando_connections: List
|
|
||||||
worlds: Dict[int, "AutoWorld.World"]
|
worlds: Dict[int, "AutoWorld.World"]
|
||||||
groups: Dict[int, Group]
|
groups: Dict[int, Group]
|
||||||
regions: RegionManager
|
regions: RegionManager
|
||||||
@@ -83,6 +93,8 @@ class MultiWorld():
|
|||||||
start_location_hints: Dict[int, Options.StartLocationHints]
|
start_location_hints: Dict[int, Options.StartLocationHints]
|
||||||
item_links: Dict[int, Options.ItemLinks]
|
item_links: Dict[int, Options.ItemLinks]
|
||||||
|
|
||||||
|
plando_item_blocks: Dict[int, List[PlandoItemBlock]]
|
||||||
|
|
||||||
game: Dict[int, str]
|
game: Dict[int, str]
|
||||||
|
|
||||||
random: random.Random
|
random: random.Random
|
||||||
@@ -160,13 +172,12 @@ class MultiWorld():
|
|||||||
self.local_early_items = {player: {} for player in self.player_ids}
|
self.local_early_items = {player: {} for player in self.player_ids}
|
||||||
self.indirect_connections = {}
|
self.indirect_connections = {}
|
||||||
self.start_inventory_from_pool: Dict[int, Options.StartInventoryPool] = {}
|
self.start_inventory_from_pool: Dict[int, Options.StartInventoryPool] = {}
|
||||||
|
self.plando_item_blocks = {}
|
||||||
|
|
||||||
for player in range(1, players + 1):
|
for player in range(1, players + 1):
|
||||||
def set_player_attr(attr: str, val) -> None:
|
def set_player_attr(attr: str, val) -> None:
|
||||||
self.__dict__.setdefault(attr, {})[player] = val
|
self.__dict__.setdefault(attr, {})[player] = val
|
||||||
set_player_attr('plando_items', [])
|
set_player_attr('plando_item_blocks', [])
|
||||||
set_player_attr('plando_texts', {})
|
|
||||||
set_player_attr('plando_connections', [])
|
|
||||||
set_player_attr('game', "Archipelago")
|
set_player_attr('game', "Archipelago")
|
||||||
set_player_attr('completion_condition', lambda state: True)
|
set_player_attr('completion_condition', lambda state: True)
|
||||||
self.worlds = {}
|
self.worlds = {}
|
||||||
@@ -427,7 +438,8 @@ class MultiWorld():
|
|||||||
def get_location(self, location_name: str, player: int) -> Location:
|
def get_location(self, location_name: str, player: int) -> Location:
|
||||||
return self.regions.location_cache[player][location_name]
|
return self.regions.location_cache[player][location_name]
|
||||||
|
|
||||||
def get_all_state(self, use_cache: bool, allow_partial_entrances: bool = False) -> CollectionState:
|
def get_all_state(self, use_cache: bool, allow_partial_entrances: bool = False,
|
||||||
|
collect_pre_fill_items: bool = True) -> CollectionState:
|
||||||
cached = getattr(self, "_all_state", None)
|
cached = getattr(self, "_all_state", None)
|
||||||
if use_cache and cached:
|
if use_cache and cached:
|
||||||
return cached.copy()
|
return cached.copy()
|
||||||
@@ -436,10 +448,11 @@ class MultiWorld():
|
|||||||
|
|
||||||
for item in self.itempool:
|
for item in self.itempool:
|
||||||
self.worlds[item.player].collect(ret, item)
|
self.worlds[item.player].collect(ret, item)
|
||||||
for player in self.player_ids:
|
if collect_pre_fill_items:
|
||||||
subworld = self.worlds[player]
|
for player in self.player_ids:
|
||||||
for item in subworld.get_pre_fill_items():
|
subworld = self.worlds[player]
|
||||||
subworld.collect(ret, item)
|
for item in subworld.get_pre_fill_items():
|
||||||
|
subworld.collect(ret, item)
|
||||||
ret.sweep_for_advancements()
|
ret.sweep_for_advancements()
|
||||||
|
|
||||||
if use_cache:
|
if use_cache:
|
||||||
|
349
Fill.py
349
Fill.py
@@ -4,7 +4,7 @@ import logging
|
|||||||
import typing
|
import typing
|
||||||
from collections import Counter, deque
|
from collections import Counter, deque
|
||||||
|
|
||||||
from BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld
|
from BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld, PlandoItemBlock
|
||||||
from Options import Accessibility
|
from Options import Accessibility
|
||||||
|
|
||||||
from worlds.AutoWorld import call_all
|
from worlds.AutoWorld import call_all
|
||||||
@@ -100,7 +100,7 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
|
|||||||
# if minimal accessibility, only check whether location is reachable if game not beatable
|
# if minimal accessibility, only check whether location is reachable if game not beatable
|
||||||
if multiworld.worlds[item_to_place.player].options.accessibility == Accessibility.option_minimal:
|
if multiworld.worlds[item_to_place.player].options.accessibility == Accessibility.option_minimal:
|
||||||
perform_access_check = not multiworld.has_beaten_game(maximum_exploration_state,
|
perform_access_check = not multiworld.has_beaten_game(maximum_exploration_state,
|
||||||
item_to_place.player) \
|
item_to_place.player) \
|
||||||
if single_player_placement else not has_beaten_game
|
if single_player_placement else not has_beaten_game
|
||||||
else:
|
else:
|
||||||
perform_access_check = True
|
perform_access_check = True
|
||||||
@@ -242,7 +242,7 @@ def remaining_fill(multiworld: MultiWorld,
|
|||||||
unplaced_items: typing.List[Item] = []
|
unplaced_items: typing.List[Item] = []
|
||||||
placements: typing.List[Location] = []
|
placements: typing.List[Location] = []
|
||||||
swapped_items: typing.Counter[typing.Tuple[int, str]] = Counter()
|
swapped_items: typing.Counter[typing.Tuple[int, str]] = Counter()
|
||||||
total = min(len(itempool), len(locations))
|
total = min(len(itempool), len(locations))
|
||||||
placed = 0
|
placed = 0
|
||||||
|
|
||||||
# Optimisation: Decide whether to do full location.can_fill check (respect excluded), or only check the item rule
|
# Optimisation: Decide whether to do full location.can_fill check (respect excluded), or only check the item rule
|
||||||
@@ -343,8 +343,10 @@ def fast_fill(multiworld: MultiWorld,
|
|||||||
|
|
||||||
def accessibility_corrections(multiworld: MultiWorld, state: CollectionState, locations, pool=[]):
|
def accessibility_corrections(multiworld: MultiWorld, state: CollectionState, locations, pool=[]):
|
||||||
maximum_exploration_state = sweep_from_pool(state, pool)
|
maximum_exploration_state = sweep_from_pool(state, pool)
|
||||||
minimal_players = {player for player in multiworld.player_ids if multiworld.worlds[player].options.accessibility == "minimal"}
|
minimal_players = {player for player in multiworld.player_ids if
|
||||||
unreachable_locations = [location for location in multiworld.get_locations() if location.player in minimal_players and
|
multiworld.worlds[player].options.accessibility == "minimal"}
|
||||||
|
unreachable_locations = [location for location in multiworld.get_locations() if
|
||||||
|
location.player in minimal_players and
|
||||||
not location.can_reach(maximum_exploration_state)]
|
not location.can_reach(maximum_exploration_state)]
|
||||||
for location in unreachable_locations:
|
for location in unreachable_locations:
|
||||||
if (location.item is not None and location.item.advancement and location.address is not None and not
|
if (location.item is not None and location.item.advancement and location.address is not None and not
|
||||||
@@ -365,7 +367,7 @@ def inaccessible_location_rules(multiworld: MultiWorld, state: CollectionState,
|
|||||||
unreachable_locations = [location for location in locations if not location.can_reach(maximum_exploration_state)]
|
unreachable_locations = [location for location in locations if not location.can_reach(maximum_exploration_state)]
|
||||||
if unreachable_locations:
|
if unreachable_locations:
|
||||||
def forbid_important_item_rule(item: Item):
|
def forbid_important_item_rule(item: Item):
|
||||||
return not ((item.classification & 0b0011) and multiworld.worlds[item.player].options.accessibility != 'minimal')
|
return not ((item.classification & 0b0011) and multiworld.worlds[item.player].options.accessibility != "minimal")
|
||||||
|
|
||||||
for location in unreachable_locations:
|
for location in unreachable_locations:
|
||||||
add_item_rule(location, forbid_important_item_rule)
|
add_item_rule(location, forbid_important_item_rule)
|
||||||
@@ -677,9 +679,9 @@ def balance_multiworld_progression(multiworld: MultiWorld) -> None:
|
|||||||
if multiworld.worlds[player].options.progression_balancing > 0
|
if multiworld.worlds[player].options.progression_balancing > 0
|
||||||
}
|
}
|
||||||
if not balanceable_players:
|
if not balanceable_players:
|
||||||
logging.info('Skipping multiworld progression balancing.')
|
logging.info("Skipping multiworld progression balancing.")
|
||||||
else:
|
else:
|
||||||
logging.info(f'Balancing multiworld progression for {len(balanceable_players)} Players.')
|
logging.info(f"Balancing multiworld progression for {len(balanceable_players)} Players.")
|
||||||
logging.debug(balanceable_players)
|
logging.debug(balanceable_players)
|
||||||
state: CollectionState = CollectionState(multiworld)
|
state: CollectionState = CollectionState(multiworld)
|
||||||
checked_locations: typing.Set[Location] = set()
|
checked_locations: typing.Set[Location] = set()
|
||||||
@@ -777,7 +779,7 @@ def balance_multiworld_progression(multiworld: MultiWorld) -> None:
|
|||||||
if player in threshold_percentages):
|
if player in threshold_percentages):
|
||||||
break
|
break
|
||||||
elif not balancing_sphere:
|
elif not balancing_sphere:
|
||||||
raise RuntimeError('Not all required items reachable. Something went terribly wrong here.')
|
raise RuntimeError("Not all required items reachable. Something went terribly wrong here.")
|
||||||
# Gather a set of locations which we can swap items into
|
# Gather a set of locations which we can swap items into
|
||||||
unlocked_locations: typing.Dict[int, typing.Set[Location]] = collections.defaultdict(set)
|
unlocked_locations: typing.Dict[int, typing.Set[Location]] = collections.defaultdict(set)
|
||||||
for l in unchecked_locations:
|
for l in unchecked_locations:
|
||||||
@@ -793,8 +795,8 @@ def balance_multiworld_progression(multiworld: MultiWorld) -> None:
|
|||||||
testing = items_to_test.pop()
|
testing = items_to_test.pop()
|
||||||
reducing_state = state.copy()
|
reducing_state = state.copy()
|
||||||
for location in itertools.chain((
|
for location in itertools.chain((
|
||||||
l for l in items_to_replace
|
l for l in items_to_replace
|
||||||
if l.item.player == player
|
if l.item.player == player
|
||||||
), items_to_test):
|
), items_to_test):
|
||||||
reducing_state.collect(location.item, True, location)
|
reducing_state.collect(location.item, True, location)
|
||||||
|
|
||||||
@@ -867,52 +869,30 @@ def swap_location_item(location_1: Location, location_2: Location, check_locked:
|
|||||||
location_2.item.location = location_2
|
location_2.item.location = location_2
|
||||||
|
|
||||||
|
|
||||||
def distribute_planned(multiworld: MultiWorld) -> None:
|
def parse_planned_blocks(multiworld: MultiWorld) -> dict[int, list[PlandoItemBlock]]:
|
||||||
def warn(warning: str, force: typing.Union[bool, str]) -> None:
|
def warn(warning: str, force: bool | str) -> None:
|
||||||
if force in [True, 'fail', 'failure', 'none', False, 'warn', 'warning']:
|
if isinstance(force, bool):
|
||||||
logging.warning(f'{warning}')
|
logging.warning(f"{warning}")
|
||||||
else:
|
else:
|
||||||
logging.debug(f'{warning}')
|
logging.debug(f"{warning}")
|
||||||
|
|
||||||
def failed(warning: str, force: typing.Union[bool, str]) -> None:
|
def failed(warning: str, force: bool | str) -> None:
|
||||||
if force in [True, 'fail', 'failure']:
|
if force is True:
|
||||||
raise Exception(warning)
|
raise Exception(warning)
|
||||||
else:
|
else:
|
||||||
warn(warning, force)
|
warn(warning, force)
|
||||||
|
|
||||||
swept_state = multiworld.state.copy()
|
|
||||||
swept_state.sweep_for_advancements()
|
|
||||||
reachable = frozenset(multiworld.get_reachable_locations(swept_state))
|
|
||||||
early_locations: typing.Dict[int, typing.List[str]] = collections.defaultdict(list)
|
|
||||||
non_early_locations: typing.Dict[int, typing.List[str]] = collections.defaultdict(list)
|
|
||||||
for loc in multiworld.get_unfilled_locations():
|
|
||||||
if loc in reachable:
|
|
||||||
early_locations[loc.player].append(loc.name)
|
|
||||||
else: # not reachable with swept state
|
|
||||||
non_early_locations[loc.player].append(loc.name)
|
|
||||||
|
|
||||||
world_name_lookup = multiworld.world_name_lookup
|
world_name_lookup = multiworld.world_name_lookup
|
||||||
|
|
||||||
block_value = typing.Union[typing.List[str], typing.Dict[str, typing.Any], str]
|
plando_blocks: dict[int, list[PlandoItemBlock]] = dict()
|
||||||
plando_blocks: typing.List[typing.Dict[str, typing.Any]] = []
|
player_ids: set[int] = set(multiworld.player_ids)
|
||||||
player_ids = set(multiworld.player_ids)
|
|
||||||
for player in player_ids:
|
for player in player_ids:
|
||||||
for block in multiworld.plando_items[player]:
|
plando_blocks[player] = []
|
||||||
block['player'] = player
|
for block in multiworld.worlds[player].options.plando_items:
|
||||||
if 'force' not in block:
|
new_block: PlandoItemBlock = PlandoItemBlock(player, block.from_pool, block.force)
|
||||||
block['force'] = 'silent'
|
target_world = block.world
|
||||||
if 'from_pool' not in block:
|
|
||||||
block['from_pool'] = True
|
|
||||||
elif not isinstance(block['from_pool'], bool):
|
|
||||||
from_pool_type = type(block['from_pool'])
|
|
||||||
raise Exception(f'Plando "from_pool" has to be boolean, not {from_pool_type} for player {player}.')
|
|
||||||
if 'world' not in block:
|
|
||||||
target_world = False
|
|
||||||
else:
|
|
||||||
target_world = block['world']
|
|
||||||
|
|
||||||
if target_world is False or multiworld.players == 1: # target own world
|
if target_world is False or multiworld.players == 1: # target own world
|
||||||
worlds: typing.Set[int] = {player}
|
worlds: set[int] = {player}
|
||||||
elif target_world is True: # target any worlds besides own
|
elif target_world is True: # target any worlds besides own
|
||||||
worlds = set(multiworld.player_ids) - {player}
|
worlds = set(multiworld.player_ids) - {player}
|
||||||
elif target_world is None: # target all worlds
|
elif target_world is None: # target all worlds
|
||||||
@@ -922,172 +902,197 @@ def distribute_planned(multiworld: MultiWorld) -> None:
|
|||||||
for listed_world in target_world:
|
for listed_world in target_world:
|
||||||
if listed_world not in world_name_lookup:
|
if listed_world not in world_name_lookup:
|
||||||
failed(f"Cannot place item to {target_world}'s world as that world does not exist.",
|
failed(f"Cannot place item to {target_world}'s world as that world does not exist.",
|
||||||
block['force'])
|
block.force)
|
||||||
continue
|
continue
|
||||||
worlds.add(world_name_lookup[listed_world])
|
worlds.add(world_name_lookup[listed_world])
|
||||||
elif type(target_world) == int: # target world by slot number
|
elif type(target_world) == int: # target world by slot number
|
||||||
if target_world not in range(1, multiworld.players + 1):
|
if target_world not in range(1, multiworld.players + 1):
|
||||||
failed(
|
failed(
|
||||||
f"Cannot place item in world {target_world} as it is not in range of (1, {multiworld.players})",
|
f"Cannot place item in world {target_world} as it is not in range of (1, {multiworld.players})",
|
||||||
block['force'])
|
block.force)
|
||||||
continue
|
continue
|
||||||
worlds = {target_world}
|
worlds = {target_world}
|
||||||
else: # target world by slot name
|
else: # target world by slot name
|
||||||
if target_world not in world_name_lookup:
|
if target_world not in world_name_lookup:
|
||||||
failed(f"Cannot place item to {target_world}'s world as that world does not exist.",
|
failed(f"Cannot place item to {target_world}'s world as that world does not exist.",
|
||||||
block['force'])
|
block.force)
|
||||||
continue
|
continue
|
||||||
worlds = {world_name_lookup[target_world]}
|
worlds = {world_name_lookup[target_world]}
|
||||||
block['world'] = worlds
|
new_block.worlds = worlds
|
||||||
|
|
||||||
items: block_value = []
|
items: list[str] | dict[str, typing.Any] = block.items
|
||||||
if "items" in block:
|
|
||||||
items = block["items"]
|
|
||||||
if 'count' not in block:
|
|
||||||
block['count'] = False
|
|
||||||
elif "item" in block:
|
|
||||||
items = block["item"]
|
|
||||||
if 'count' not in block:
|
|
||||||
block['count'] = 1
|
|
||||||
else:
|
|
||||||
failed("You must specify at least one item to place items with plando.", block['force'])
|
|
||||||
continue
|
|
||||||
if isinstance(items, dict):
|
if isinstance(items, dict):
|
||||||
item_list: typing.List[str] = []
|
item_list: list[str] = []
|
||||||
for key, value in items.items():
|
for key, value in items.items():
|
||||||
if value is True:
|
if value is True:
|
||||||
value = multiworld.itempool.count(multiworld.worlds[player].create_item(key))
|
value = multiworld.itempool.count(multiworld.worlds[player].create_item(key))
|
||||||
item_list += [key] * value
|
item_list += [key] * value
|
||||||
items = item_list
|
items = item_list
|
||||||
if isinstance(items, str):
|
new_block.items = items
|
||||||
items = [items]
|
|
||||||
block['items'] = items
|
|
||||||
|
|
||||||
locations: block_value = []
|
locations: list[str] = block.locations
|
||||||
if 'location' in block:
|
|
||||||
locations = block['location'] # just allow 'location' to keep old yamls compatible
|
|
||||||
elif 'locations' in block:
|
|
||||||
locations = block['locations']
|
|
||||||
if isinstance(locations, str):
|
if isinstance(locations, str):
|
||||||
locations = [locations]
|
locations = [locations]
|
||||||
|
|
||||||
if isinstance(locations, dict):
|
locations_from_groups: list[str] = []
|
||||||
location_list = []
|
resolved_locations: list[Location] = []
|
||||||
for key, value in locations.items():
|
for target_player in worlds:
|
||||||
location_list += [key] * value
|
world_locations = multiworld.get_unfilled_locations(target_player)
|
||||||
locations = location_list
|
for group in multiworld.worlds[target_player].location_name_groups:
|
||||||
|
if group in locations:
|
||||||
|
locations_from_groups.extend(multiworld.worlds[target_player].location_name_groups[group])
|
||||||
|
resolved_locations.extend(location for location in world_locations
|
||||||
|
if location.name in [*locations, *locations_from_groups])
|
||||||
|
new_block.locations = sorted(dict.fromkeys(locations))
|
||||||
|
new_block.resolved_locations = sorted(set(resolved_locations))
|
||||||
|
|
||||||
|
count = block.count
|
||||||
|
if not count:
|
||||||
|
count = len(new_block.items)
|
||||||
|
if isinstance(count, int):
|
||||||
|
count = {"min": count, "max": count}
|
||||||
|
if "min" not in count:
|
||||||
|
count["min"] = 0
|
||||||
|
if "max" not in count:
|
||||||
|
count["max"] = len(new_block.items)
|
||||||
|
|
||||||
|
new_block.count = count
|
||||||
|
plando_blocks[player].append(new_block)
|
||||||
|
|
||||||
|
return plando_blocks
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_early_locations_for_planned(multiworld: MultiWorld):
|
||||||
|
def warn(warning: str, force: bool | str) -> None:
|
||||||
|
if isinstance(force, bool):
|
||||||
|
logging.warning(f"{warning}")
|
||||||
|
else:
|
||||||
|
logging.debug(f"{warning}")
|
||||||
|
|
||||||
|
def failed(warning: str, force: bool | str) -> None:
|
||||||
|
if force is True:
|
||||||
|
raise Exception(warning)
|
||||||
|
else:
|
||||||
|
warn(warning, force)
|
||||||
|
|
||||||
|
swept_state = multiworld.state.copy()
|
||||||
|
swept_state.sweep_for_advancements()
|
||||||
|
reachable = frozenset(multiworld.get_reachable_locations(swept_state))
|
||||||
|
early_locations: dict[int, list[Location]] = collections.defaultdict(list)
|
||||||
|
non_early_locations: dict[int, list[Location]] = collections.defaultdict(list)
|
||||||
|
for loc in multiworld.get_unfilled_locations():
|
||||||
|
if loc in reachable:
|
||||||
|
early_locations[loc.player].append(loc)
|
||||||
|
else: # not reachable with swept state
|
||||||
|
non_early_locations[loc.player].append(loc)
|
||||||
|
|
||||||
|
for player in multiworld.plando_item_blocks:
|
||||||
|
removed = []
|
||||||
|
for block in multiworld.plando_item_blocks[player]:
|
||||||
|
locations = block.locations
|
||||||
|
resolved_locations = block.resolved_locations
|
||||||
|
worlds = block.worlds
|
||||||
if "early_locations" in locations:
|
if "early_locations" in locations:
|
||||||
locations.remove("early_locations")
|
|
||||||
for target_player in worlds:
|
for target_player in worlds:
|
||||||
locations += early_locations[target_player]
|
resolved_locations += early_locations[target_player]
|
||||||
if "non_early_locations" in locations:
|
if "non_early_locations" in locations:
|
||||||
locations.remove("non_early_locations")
|
|
||||||
for target_player in worlds:
|
for target_player in worlds:
|
||||||
locations += non_early_locations[target_player]
|
resolved_locations += non_early_locations[target_player]
|
||||||
|
|
||||||
block['locations'] = list(dict.fromkeys(locations))
|
if block.count["max"] > len(block.items):
|
||||||
|
count = block.count["max"]
|
||||||
|
failed(f"Plando count {count} greater than items specified", block.force)
|
||||||
|
block.count["max"] = len(block.items)
|
||||||
|
if block.count["min"] > len(block.items):
|
||||||
|
block.count["min"] = len(block.items)
|
||||||
|
if block.count["max"] > len(block.resolved_locations) > 0:
|
||||||
|
count = block.count["max"]
|
||||||
|
failed(f"Plando count {count} greater than locations specified", block.force)
|
||||||
|
block.count["max"] = len(block.resolved_locations)
|
||||||
|
if block.count["min"] > len(block.resolved_locations):
|
||||||
|
block.count["min"] = len(block.resolved_locations)
|
||||||
|
block.count["target"] = multiworld.random.randint(block.count["min"],
|
||||||
|
block.count["max"])
|
||||||
|
|
||||||
if not block['count']:
|
if not block.count["target"]:
|
||||||
block['count'] = (min(len(block['items']), len(block['locations'])) if
|
removed.append(block)
|
||||||
len(block['locations']) > 0 else len(block['items']))
|
|
||||||
if isinstance(block['count'], int):
|
|
||||||
block['count'] = {'min': block['count'], 'max': block['count']}
|
|
||||||
if 'min' not in block['count']:
|
|
||||||
block['count']['min'] = 0
|
|
||||||
if 'max' not in block['count']:
|
|
||||||
block['count']['max'] = (min(len(block['items']), len(block['locations'])) if
|
|
||||||
len(block['locations']) > 0 else len(block['items']))
|
|
||||||
if block['count']['max'] > len(block['items']):
|
|
||||||
count = block['count']
|
|
||||||
failed(f"Plando count {count} greater than items specified", block['force'])
|
|
||||||
block['count'] = len(block['items'])
|
|
||||||
if block['count']['max'] > len(block['locations']) > 0:
|
|
||||||
count = block['count']
|
|
||||||
failed(f"Plando count {count} greater than locations specified", block['force'])
|
|
||||||
block['count'] = len(block['locations'])
|
|
||||||
block['count']['target'] = multiworld.random.randint(block['count']['min'], block['count']['max'])
|
|
||||||
|
|
||||||
if block['count']['target'] > 0:
|
for block in removed:
|
||||||
plando_blocks.append(block)
|
multiworld.plando_item_blocks[player].remove(block)
|
||||||
|
|
||||||
|
|
||||||
|
def distribute_planned_blocks(multiworld: MultiWorld, plando_blocks: list[PlandoItemBlock]):
|
||||||
|
def warn(warning: str, force: bool | str) -> None:
|
||||||
|
if isinstance(force, bool):
|
||||||
|
logging.warning(f"{warning}")
|
||||||
|
else:
|
||||||
|
logging.debug(f"{warning}")
|
||||||
|
|
||||||
|
def failed(warning: str, force: bool | str) -> None:
|
||||||
|
if force is True:
|
||||||
|
raise Exception(warning)
|
||||||
|
else:
|
||||||
|
warn(warning, force)
|
||||||
|
|
||||||
# shuffle, but then sort blocks by number of locations minus number of items,
|
# shuffle, but then sort blocks by number of locations minus number of items,
|
||||||
# so less-flexible blocks get priority
|
# so less-flexible blocks get priority
|
||||||
multiworld.random.shuffle(plando_blocks)
|
multiworld.random.shuffle(plando_blocks)
|
||||||
plando_blocks.sort(key=lambda block: (len(block['locations']) - block['count']['target']
|
plando_blocks.sort(key=lambda block: (len(block.resolved_locations) - block.count["target"]
|
||||||
if len(block['locations']) > 0
|
if len(block.resolved_locations) > 0
|
||||||
else len(multiworld.get_unfilled_locations(player)) - block['count']['target']))
|
else len(multiworld.get_unfilled_locations(block.player)) -
|
||||||
|
block.count["target"]))
|
||||||
for placement in plando_blocks:
|
for placement in plando_blocks:
|
||||||
player = placement['player']
|
player = placement.player
|
||||||
try:
|
try:
|
||||||
worlds = placement['world']
|
worlds = placement.worlds
|
||||||
locations = placement['locations']
|
locations = placement.resolved_locations
|
||||||
items = placement['items']
|
items = placement.items
|
||||||
maxcount = placement['count']['target']
|
maxcount = placement.count["target"]
|
||||||
from_pool = placement['from_pool']
|
from_pool = placement.from_pool
|
||||||
|
|
||||||
candidates = list(multiworld.get_unfilled_locations_for_players(locations, sorted(worlds)))
|
item_candidates = []
|
||||||
multiworld.random.shuffle(candidates)
|
if from_pool:
|
||||||
multiworld.random.shuffle(items)
|
instances = [item for item in multiworld.itempool if item.player == player and item.name in items]
|
||||||
count = 0
|
for item in multiworld.random.sample(items, maxcount):
|
||||||
err: typing.List[str] = []
|
candidate = next((i for i in instances if i.name == item), None)
|
||||||
successful_pairs: typing.List[typing.Tuple[int, Item, Location]] = []
|
if candidate is None:
|
||||||
claimed_indices: typing.Set[typing.Optional[int]] = set()
|
warn(f"Could not remove {item} from pool for {multiworld.player_name[player]} as "
|
||||||
for item_name in items:
|
f"it's already missing from it", placement.force)
|
||||||
index_to_delete: typing.Optional[int] = None
|
candidate = multiworld.worlds[player].create_item(item)
|
||||||
if from_pool:
|
|
||||||
try:
|
|
||||||
# If from_pool, try to find an existing item with this name & player in the itempool and use it
|
|
||||||
index_to_delete, item = next(
|
|
||||||
(i, item) for i, item in enumerate(multiworld.itempool)
|
|
||||||
if item.player == player and item.name == item_name and i not in claimed_indices
|
|
||||||
)
|
|
||||||
except StopIteration:
|
|
||||||
warn(
|
|
||||||
f"Could not remove {item_name} from pool for {multiworld.player_name[player]} as it's already missing from it.",
|
|
||||||
placement['force'])
|
|
||||||
item = multiworld.worlds[player].create_item(item_name)
|
|
||||||
else:
|
|
||||||
item = multiworld.worlds[player].create_item(item_name)
|
|
||||||
|
|
||||||
for location in reversed(candidates):
|
|
||||||
if (location.address is None) == (item.code is None): # either both None or both not None
|
|
||||||
if not location.item:
|
|
||||||
if location.item_rule(item):
|
|
||||||
if location.can_fill(multiworld.state, item, False):
|
|
||||||
successful_pairs.append((index_to_delete, item, location))
|
|
||||||
claimed_indices.add(index_to_delete)
|
|
||||||
candidates.remove(location)
|
|
||||||
count = count + 1
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
err.append(f"Can't place item at {location} due to fill condition not met.")
|
|
||||||
else:
|
|
||||||
err.append(f"{item_name} not allowed at {location}.")
|
|
||||||
else:
|
|
||||||
err.append(f"Cannot place {item_name} into already filled location {location}.")
|
|
||||||
else:
|
else:
|
||||||
err.append(f"Mismatch between {item_name} and {location}, only one is an event.")
|
multiworld.itempool.remove(candidate)
|
||||||
|
instances.remove(candidate)
|
||||||
if count == maxcount:
|
item_candidates.append(candidate)
|
||||||
break
|
else:
|
||||||
if count < placement['count']['min']:
|
item_candidates = [multiworld.worlds[player].create_item(item)
|
||||||
m = placement['count']['min']
|
for item in multiworld.random.sample(items, maxcount)]
|
||||||
failed(
|
if any(item.code is None for item in item_candidates) \
|
||||||
f"Plando block failed to place {m - count} of {m} item(s) for {multiworld.player_name[player]}, error(s): {' '.join(err)}",
|
and not all(item.code is None for item in item_candidates):
|
||||||
placement['force'])
|
failed(f"Plando block for player {player} ({multiworld.player_name[player]}) contains both "
|
||||||
|
f"event items and non-event items. "
|
||||||
# Sort indices in reverse so we can remove them one by one
|
f"Event items: {[item for item in item_candidates if item.code is None]}, "
|
||||||
successful_pairs = sorted(successful_pairs, key=lambda successful_pair: successful_pair[0] or 0, reverse=True)
|
f"Non-event items: {[item for item in item_candidates if item.code is not None]}",
|
||||||
|
placement.force)
|
||||||
for (index, item, location) in successful_pairs:
|
continue
|
||||||
multiworld.push_item(location, item, collect=False)
|
else:
|
||||||
location.locked = True
|
is_real = item_candidates[0].code is not None
|
||||||
logging.debug(f"Plando placed {item} at {location}")
|
candidates = [candidate for candidate in locations if candidate.item is None
|
||||||
if index is not None: # If this item is from_pool and was found in the pool, remove it.
|
and bool(candidate.address) == is_real]
|
||||||
multiworld.itempool.pop(index)
|
multiworld.random.shuffle(candidates)
|
||||||
|
allstate = multiworld.get_all_state(False)
|
||||||
|
mincount = placement.count["min"]
|
||||||
|
allowed_margin = len(item_candidates) - mincount
|
||||||
|
fill_restrictive(multiworld, allstate, candidates, item_candidates, lock=True,
|
||||||
|
allow_partial=True, name="Plando Main Fill")
|
||||||
|
|
||||||
|
if len(item_candidates) > allowed_margin:
|
||||||
|
failed(f"Could not place {len(item_candidates)} "
|
||||||
|
f"of {mincount + allowed_margin} item(s) "
|
||||||
|
f"for {multiworld.player_name[player]}, "
|
||||||
|
f"remaining items: {item_candidates}",
|
||||||
|
placement.force)
|
||||||
|
if from_pool:
|
||||||
|
multiworld.itempool.extend([item for item in item_candidates if item.code is not None])
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise Exception(
|
raise Exception(
|
||||||
f"Error running plando for player {player} ({multiworld.player_name[player]})") from e
|
f"Error running plando for player {player} ({multiworld.player_name[player]})") from e
|
||||||
|
14
Generate.py
14
Generate.py
@@ -334,12 +334,6 @@ def handle_name(name: str, player: int, name_counter: Counter):
|
|||||||
return new_name
|
return new_name
|
||||||
|
|
||||||
|
|
||||||
def roll_percentage(percentage: Union[int, float]) -> bool:
|
|
||||||
"""Roll a percentage chance.
|
|
||||||
percentage is expected to be in range [0, 100]"""
|
|
||||||
return random.random() < (float(percentage) / 100)
|
|
||||||
|
|
||||||
|
|
||||||
def update_weights(weights: dict, new_weights: dict, update_type: str, name: str) -> dict:
|
def update_weights(weights: dict, new_weights: dict, update_type: str, name: str) -> dict:
|
||||||
logging.debug(f'Applying {new_weights}')
|
logging.debug(f'Applying {new_weights}')
|
||||||
cleaned_weights = {}
|
cleaned_weights = {}
|
||||||
@@ -405,7 +399,7 @@ def roll_linked_options(weights: dict) -> dict:
|
|||||||
if "name" not in option_set:
|
if "name" not in option_set:
|
||||||
raise ValueError("One of your linked options does not have a name.")
|
raise ValueError("One of your linked options does not have a name.")
|
||||||
try:
|
try:
|
||||||
if roll_percentage(option_set["percentage"]):
|
if Options.roll_percentage(option_set["percentage"]):
|
||||||
logging.debug(f"Linked option {option_set['name']} triggered.")
|
logging.debug(f"Linked option {option_set['name']} triggered.")
|
||||||
new_options = option_set["options"]
|
new_options = option_set["options"]
|
||||||
for category_name, category_options in new_options.items():
|
for category_name, category_options in new_options.items():
|
||||||
@@ -438,7 +432,7 @@ def roll_triggers(weights: dict, triggers: list, valid_keys: set) -> dict:
|
|||||||
trigger_result = get_choice("option_result", option_set)
|
trigger_result = get_choice("option_result", option_set)
|
||||||
result = get_choice(key, currently_targeted_weights)
|
result = get_choice(key, currently_targeted_weights)
|
||||||
currently_targeted_weights[key] = result
|
currently_targeted_weights[key] = result
|
||||||
if result == trigger_result and roll_percentage(get_choice("percentage", option_set, 100)):
|
if result == trigger_result and Options.roll_percentage(get_choice("percentage", option_set, 100)):
|
||||||
for category_name, category_options in option_set["options"].items():
|
for category_name, category_options in option_set["options"].items():
|
||||||
currently_targeted_weights = weights
|
currently_targeted_weights = weights
|
||||||
if category_name:
|
if category_name:
|
||||||
@@ -542,10 +536,6 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b
|
|||||||
handle_option(ret, game_weights, option_key, option, plando_options)
|
handle_option(ret, game_weights, option_key, option, plando_options)
|
||||||
valid_keys.add(option_key)
|
valid_keys.add(option_key)
|
||||||
|
|
||||||
# TODO remove plando_items after moving it to the options system
|
|
||||||
valid_keys.add("plando_items")
|
|
||||||
if PlandoOptions.items in plando_options:
|
|
||||||
ret.plando_items = copy.deepcopy(game_weights.get("plando_items", []))
|
|
||||||
if ret.game == "A Link to the Past":
|
if ret.game == "A Link to the Past":
|
||||||
# TODO there are still more LTTP options not on the options system
|
# TODO there are still more LTTP options not on the options system
|
||||||
valid_keys |= {"sprite_pool", "sprite", "random_sprite_on_event"}
|
valid_keys |= {"sprite_pool", "sprite", "random_sprite_on_event"}
|
||||||
|
14
Main.py
14
Main.py
@@ -11,8 +11,8 @@ from typing import Dict, List, Optional, Set, Tuple, Union
|
|||||||
|
|
||||||
import worlds
|
import worlds
|
||||||
from BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld, Region
|
from BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld, Region
|
||||||
from Fill import FillError, balance_multiworld_progression, distribute_items_restrictive, distribute_planned, \
|
from Fill import FillError, balance_multiworld_progression, distribute_items_restrictive, flood_items, \
|
||||||
flood_items
|
parse_planned_blocks, distribute_planned_blocks, resolve_early_locations_for_planned
|
||||||
from Options import StartInventoryPool
|
from Options import StartInventoryPool
|
||||||
from Utils import __version__, output_path, version_tuple, get_settings
|
from Utils import __version__, output_path, version_tuple, get_settings
|
||||||
from settings import get_settings
|
from settings import get_settings
|
||||||
@@ -37,9 +37,6 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
|||||||
logger = logging.getLogger()
|
logger = logging.getLogger()
|
||||||
multiworld.set_seed(seed, args.race, str(args.outputname) if args.outputname else None)
|
multiworld.set_seed(seed, args.race, str(args.outputname) if args.outputname else None)
|
||||||
multiworld.plando_options = args.plando_options
|
multiworld.plando_options = args.plando_options
|
||||||
multiworld.plando_items = args.plando_items.copy()
|
|
||||||
multiworld.plando_texts = args.plando_texts.copy()
|
|
||||||
multiworld.plando_connections = args.plando_connections.copy()
|
|
||||||
multiworld.game = args.game.copy()
|
multiworld.game = args.game.copy()
|
||||||
multiworld.player_name = args.name.copy()
|
multiworld.player_name = args.name.copy()
|
||||||
multiworld.sprite = args.sprite.copy()
|
multiworld.sprite = args.sprite.copy()
|
||||||
@@ -135,6 +132,8 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
|||||||
multiworld.worlds[1].options.non_local_items.value = set()
|
multiworld.worlds[1].options.non_local_items.value = set()
|
||||||
multiworld.worlds[1].options.local_items.value = set()
|
multiworld.worlds[1].options.local_items.value = set()
|
||||||
|
|
||||||
|
multiworld.plando_item_blocks = parse_planned_blocks(multiworld)
|
||||||
|
|
||||||
AutoWorld.call_all(multiworld, "connect_entrances")
|
AutoWorld.call_all(multiworld, "connect_entrances")
|
||||||
AutoWorld.call_all(multiworld, "generate_basic")
|
AutoWorld.call_all(multiworld, "generate_basic")
|
||||||
|
|
||||||
@@ -179,8 +178,9 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
|||||||
multiworld._all_state = None
|
multiworld._all_state = None
|
||||||
|
|
||||||
logger.info("Running Item Plando.")
|
logger.info("Running Item Plando.")
|
||||||
|
resolve_early_locations_for_planned(multiworld)
|
||||||
distribute_planned(multiworld)
|
distribute_planned_blocks(multiworld, [x for player in multiworld.plando_item_blocks
|
||||||
|
for x in multiworld.plando_item_blocks[player]])
|
||||||
|
|
||||||
logger.info('Running Pre Main Fill.')
|
logger.info('Running Pre Main Fill.')
|
||||||
|
|
||||||
|
140
Options.py
140
Options.py
@@ -24,6 +24,12 @@ if typing.TYPE_CHECKING:
|
|||||||
import pathlib
|
import pathlib
|
||||||
|
|
||||||
|
|
||||||
|
def roll_percentage(percentage: int | float) -> bool:
|
||||||
|
"""Roll a percentage chance.
|
||||||
|
percentage is expected to be in range [0, 100]"""
|
||||||
|
return random.random() < (float(percentage) / 100)
|
||||||
|
|
||||||
|
|
||||||
class OptionError(ValueError):
|
class OptionError(ValueError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -1019,7 +1025,7 @@ class PlandoTexts(Option[typing.List[PlandoText]], VerifyKeys):
|
|||||||
if isinstance(data, typing.Iterable):
|
if isinstance(data, typing.Iterable):
|
||||||
for text in data:
|
for text in data:
|
||||||
if isinstance(text, typing.Mapping):
|
if isinstance(text, typing.Mapping):
|
||||||
if random.random() < float(text.get("percentage", 100)/100):
|
if roll_percentage(text.get("percentage", 100)):
|
||||||
at = text.get("at", None)
|
at = text.get("at", None)
|
||||||
if at is not None:
|
if at is not None:
|
||||||
if isinstance(at, dict):
|
if isinstance(at, dict):
|
||||||
@@ -1045,7 +1051,7 @@ class PlandoTexts(Option[typing.List[PlandoText]], VerifyKeys):
|
|||||||
else:
|
else:
|
||||||
raise OptionError("\"at\" must be a valid string or weighted list of strings!")
|
raise OptionError("\"at\" must be a valid string or weighted list of strings!")
|
||||||
elif isinstance(text, PlandoText):
|
elif isinstance(text, PlandoText):
|
||||||
if random.random() < float(text.percentage/100):
|
if roll_percentage(text.percentage):
|
||||||
texts.append(text)
|
texts.append(text)
|
||||||
else:
|
else:
|
||||||
raise Exception(f"Cannot create plando text from non-dictionary type, got {type(text)}")
|
raise Exception(f"Cannot create plando text from non-dictionary type, got {type(text)}")
|
||||||
@@ -1169,7 +1175,7 @@ class PlandoConnections(Option[typing.List[PlandoConnection]], metaclass=Connect
|
|||||||
for connection in data:
|
for connection in data:
|
||||||
if isinstance(connection, typing.Mapping):
|
if isinstance(connection, typing.Mapping):
|
||||||
percentage = connection.get("percentage", 100)
|
percentage = connection.get("percentage", 100)
|
||||||
if random.random() < float(percentage / 100):
|
if roll_percentage(percentage):
|
||||||
entrance = connection.get("entrance", None)
|
entrance = connection.get("entrance", None)
|
||||||
if is_iterable_except_str(entrance):
|
if is_iterable_except_str(entrance):
|
||||||
entrance = random.choice(sorted(entrance))
|
entrance = random.choice(sorted(entrance))
|
||||||
@@ -1187,7 +1193,7 @@ class PlandoConnections(Option[typing.List[PlandoConnection]], metaclass=Connect
|
|||||||
percentage
|
percentage
|
||||||
))
|
))
|
||||||
elif isinstance(connection, PlandoConnection):
|
elif isinstance(connection, PlandoConnection):
|
||||||
if random.random() < float(connection.percentage / 100):
|
if roll_percentage(connection.percentage):
|
||||||
value.append(connection)
|
value.append(connection)
|
||||||
else:
|
else:
|
||||||
raise Exception(f"Cannot create connection from non-Dict type, got {type(connection)}.")
|
raise Exception(f"Cannot create connection from non-Dict type, got {type(connection)}.")
|
||||||
@@ -1469,6 +1475,131 @@ class ItemLinks(OptionList):
|
|||||||
link["item_pool"] = list(pool)
|
link["item_pool"] = list(pool)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class PlandoItem:
|
||||||
|
items: list[str] | dict[str, typing.Any]
|
||||||
|
locations: list[str]
|
||||||
|
world: int | str | bool | None | typing.Iterable[str] | set[int] = False
|
||||||
|
from_pool: bool = True
|
||||||
|
force: bool | typing.Literal["silent"] = "silent"
|
||||||
|
count: int | bool | dict[str, int] = False
|
||||||
|
percentage: int = 100
|
||||||
|
|
||||||
|
|
||||||
|
class PlandoItems(Option[typing.List[PlandoItem]]):
|
||||||
|
"""Generic items plando."""
|
||||||
|
default = ()
|
||||||
|
supports_weighting = False
|
||||||
|
display_name = "Plando Items"
|
||||||
|
|
||||||
|
def __init__(self, value: typing.Iterable[PlandoItem]) -> None:
|
||||||
|
self.value = list(deepcopy(value))
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_any(cls, data: typing.Any) -> Option[typing.List[PlandoItem]]:
|
||||||
|
if not isinstance(data, typing.Iterable):
|
||||||
|
raise OptionError(f"Cannot create plando items from non-Iterable type, got {type(data)}")
|
||||||
|
|
||||||
|
value: typing.List[PlandoItem] = []
|
||||||
|
for item in data:
|
||||||
|
if isinstance(item, typing.Mapping):
|
||||||
|
percentage = item.get("percentage", 100)
|
||||||
|
if not isinstance(percentage, int):
|
||||||
|
raise OptionError(f"Plando `percentage` has to be int, not {type(percentage)}.")
|
||||||
|
if not (0 <= percentage <= 100):
|
||||||
|
raise OptionError(f"Plando `percentage` has to be between 0 and 100 (inclusive) not {percentage}.")
|
||||||
|
if roll_percentage(percentage):
|
||||||
|
count = item.get("count", False)
|
||||||
|
items = item.get("items", [])
|
||||||
|
if not items:
|
||||||
|
items = item.get("item", None) # explicitly throw an error here if not present
|
||||||
|
if not items:
|
||||||
|
raise OptionError("You must specify at least one item to place items with plando.")
|
||||||
|
count = 1
|
||||||
|
if isinstance(items, str):
|
||||||
|
items = [items]
|
||||||
|
elif not isinstance(items, (dict, list)):
|
||||||
|
raise OptionError(f"Plando 'items' has to be string, list, or "
|
||||||
|
f"dictionary, not {type(items)}")
|
||||||
|
locations = item.get("locations", [])
|
||||||
|
if not locations:
|
||||||
|
locations = item.get("location", ["Everywhere"])
|
||||||
|
if locations:
|
||||||
|
count = 1
|
||||||
|
if isinstance(locations, str):
|
||||||
|
locations = [locations]
|
||||||
|
if not isinstance(locations, list):
|
||||||
|
raise OptionError(f"Plando `location` has to be string or list, not {type(locations)}")
|
||||||
|
world = item.get("world", False)
|
||||||
|
from_pool = item.get("from_pool", True)
|
||||||
|
force = item.get("force", "silent")
|
||||||
|
if not isinstance(from_pool, bool):
|
||||||
|
raise OptionError(f"Plando 'from_pool' has to be true or false, not {from_pool!r}.")
|
||||||
|
if not (isinstance(force, bool) or force == "silent"):
|
||||||
|
raise OptionError(f"Plando `force` has to be true or false or `silent`, not {force!r}.")
|
||||||
|
value.append(PlandoItem(items, locations, world, from_pool, force, count, percentage))
|
||||||
|
elif isinstance(item, PlandoItem):
|
||||||
|
if roll_percentage(item.percentage):
|
||||||
|
value.append(item)
|
||||||
|
else:
|
||||||
|
raise OptionError(f"Cannot create plando item from non-Dict type, got {type(item)}.")
|
||||||
|
return cls(value)
|
||||||
|
|
||||||
|
def verify(self, world: typing.Type[World], player_name: str, plando_options: "PlandoOptions") -> None:
|
||||||
|
if not self.value:
|
||||||
|
return
|
||||||
|
from BaseClasses import PlandoOptions
|
||||||
|
if not (PlandoOptions.items & plando_options):
|
||||||
|
# plando is disabled but plando options were given so overwrite the options
|
||||||
|
self.value = []
|
||||||
|
logging.warning(f"The plando items module is turned off, "
|
||||||
|
f"so items for {player_name} will be ignored.")
|
||||||
|
else:
|
||||||
|
# filter down item groups
|
||||||
|
for plando in self.value:
|
||||||
|
# confirm a valid count
|
||||||
|
if isinstance(plando.count, dict):
|
||||||
|
if "min" in plando.count and "max" in plando.count:
|
||||||
|
if plando.count["min"] > plando.count["max"]:
|
||||||
|
raise OptionError("Plando cannot have count `min` greater than `max`.")
|
||||||
|
items_copy = plando.items.copy()
|
||||||
|
if isinstance(plando.items, dict):
|
||||||
|
for item in items_copy:
|
||||||
|
if item in world.item_name_groups:
|
||||||
|
value = plando.items.pop(item)
|
||||||
|
group = world.item_name_groups[item]
|
||||||
|
filtered_items = sorted(group.difference(list(plando.items.keys())))
|
||||||
|
if not filtered_items:
|
||||||
|
raise OptionError(f"Plando `items` contains the group \"{item}\" "
|
||||||
|
f"and every item in it. This is not allowed.")
|
||||||
|
if value is True:
|
||||||
|
for key in filtered_items:
|
||||||
|
plando.items[key] = True
|
||||||
|
else:
|
||||||
|
for key in random.choices(filtered_items, k=value):
|
||||||
|
plando.items[key] = plando.items.get(key, 0) + 1
|
||||||
|
else:
|
||||||
|
assert isinstance(plando.items, list) # pycharm can't figure out the hinting without the hint
|
||||||
|
for item in items_copy:
|
||||||
|
if item in world.item_name_groups:
|
||||||
|
plando.items.remove(item)
|
||||||
|
plando.items.extend(sorted(world.item_name_groups[item]))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_option_name(cls, value: list[PlandoItem]) -> str:
|
||||||
|
return ", ".join(["(%s: %s)" % (item.items, item.locations) for item in value]) #TODO: see what a better way to display would be
|
||||||
|
|
||||||
|
def __getitem__(self, index: typing.SupportsIndex) -> PlandoItem:
|
||||||
|
return self.value.__getitem__(index)
|
||||||
|
|
||||||
|
def __iter__(self) -> typing.Iterator[PlandoItem]:
|
||||||
|
yield from self.value
|
||||||
|
|
||||||
|
def __len__(self) -> int:
|
||||||
|
return len(self.value)
|
||||||
|
|
||||||
|
|
||||||
class Removed(FreeText):
|
class Removed(FreeText):
|
||||||
"""This Option has been Removed."""
|
"""This Option has been Removed."""
|
||||||
rich_text_doc = True
|
rich_text_doc = True
|
||||||
@@ -1491,6 +1622,7 @@ class PerGameCommonOptions(CommonOptions):
|
|||||||
exclude_locations: ExcludeLocations
|
exclude_locations: ExcludeLocations
|
||||||
priority_locations: PriorityLocations
|
priority_locations: PriorityLocations
|
||||||
item_links: ItemLinks
|
item_links: ItemLinks
|
||||||
|
plando_items: PlandoItems
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
@@ -53,6 +53,22 @@ class TestImplemented(unittest.TestCase):
|
|||||||
if failed_world_loads:
|
if failed_world_loads:
|
||||||
self.fail(f"The following worlds failed to load: {failed_world_loads}")
|
self.fail(f"The following worlds failed to load: {failed_world_loads}")
|
||||||
|
|
||||||
|
def test_prefill_items(self):
|
||||||
|
"""Test that every world can reach every location from allstate before pre_fill."""
|
||||||
|
for gamename, world_type in AutoWorldRegister.world_types.items():
|
||||||
|
if gamename not in ("Archipelago", "Sudoku", "Final Fantasy", "Test Game"):
|
||||||
|
with self.subTest(gamename):
|
||||||
|
multiworld = setup_solo_multiworld(world_type, ("generate_early", "create_regions", "create_items",
|
||||||
|
"set_rules", "connect_entrances", "generate_basic"))
|
||||||
|
allstate = multiworld.get_all_state(False)
|
||||||
|
locations = multiworld.get_locations()
|
||||||
|
reachable = multiworld.get_reachable_locations(allstate)
|
||||||
|
unreachable = [location for location in locations if location not in reachable]
|
||||||
|
|
||||||
|
self.assertTrue(not unreachable,
|
||||||
|
f"Locations were not reachable with all state before prefill: "
|
||||||
|
f"{unreachable}. Seed: {multiworld.seed}")
|
||||||
|
|
||||||
def test_explicit_indirect_conditions_spheres(self):
|
def test_explicit_indirect_conditions_spheres(self):
|
||||||
"""Tests that worlds using explicit indirect conditions produce identical spheres as when using implicit
|
"""Tests that worlds using explicit indirect conditions produce identical spheres as when using implicit
|
||||||
indirect conditions"""
|
indirect conditions"""
|
||||||
|
@@ -26,4 +26,4 @@ class TestBase(unittest.TestCase):
|
|||||||
for step in self.test_steps:
|
for step in self.test_steps:
|
||||||
with self.subTest("Step", step=step):
|
with self.subTest("Step", step=step):
|
||||||
call_all(multiworld, step)
|
call_all(multiworld, step)
|
||||||
self.assertTrue(multiworld.get_all_state(False, True))
|
self.assertTrue(multiworld.get_all_state(False, allow_partial_entrances=True))
|
||||||
|
@@ -54,16 +54,13 @@ def parse_arguments(argv, no_defaults=False):
|
|||||||
ret = parser.parse_args(argv)
|
ret = parser.parse_args(argv)
|
||||||
|
|
||||||
# cannot be set through CLI currently
|
# cannot be set through CLI currently
|
||||||
ret.plando_items = []
|
|
||||||
ret.plando_texts = {}
|
|
||||||
ret.plando_connections = []
|
|
||||||
|
|
||||||
if multiargs.multi:
|
if multiargs.multi:
|
||||||
defaults = copy.deepcopy(ret)
|
defaults = copy.deepcopy(ret)
|
||||||
for player in range(1, multiargs.multi + 1):
|
for player in range(1, multiargs.multi + 1):
|
||||||
playerargs = parse_arguments(shlex.split(getattr(ret, f"p{player}")), True)
|
playerargs = parse_arguments(shlex.split(getattr(ret, f"p{player}")), True)
|
||||||
|
|
||||||
for name in ["plando_items", "plando_texts", "plando_connections", "game", "sprite", "sprite_pool"]:
|
for name in ["game", "sprite", "sprite_pool"]:
|
||||||
value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name)
|
value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name)
|
||||||
if player == 1:
|
if player == 1:
|
||||||
setattr(ret, name, {1: value})
|
setattr(ret, name, {1: value})
|
||||||
|
@@ -505,20 +505,20 @@ class ALTTPWorld(World):
|
|||||||
def pre_fill(self):
|
def pre_fill(self):
|
||||||
from Fill import fill_restrictive, FillError
|
from Fill import fill_restrictive, FillError
|
||||||
attempts = 5
|
attempts = 5
|
||||||
world = self.multiworld
|
all_state = self.multiworld.get_all_state(use_cache=False)
|
||||||
player = self.player
|
|
||||||
all_state = world.get_all_state(use_cache=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']]
|
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),
|
for crystal in crystals:
|
||||||
world.get_location('Eastern Palace - Prize', player),
|
all_state.remove(crystal)
|
||||||
world.get_location('Desert Palace - Prize', player),
|
crystal_locations = [self.get_location('Turtle Rock - Prize'),
|
||||||
world.get_location('Tower of Hera - Prize', player),
|
self.get_location('Eastern Palace - Prize'),
|
||||||
world.get_location('Palace of Darkness - Prize', player),
|
self.get_location('Desert Palace - Prize'),
|
||||||
world.get_location('Thieves\' Town - Prize', player),
|
self.get_location('Tower of Hera - Prize'),
|
||||||
world.get_location('Skull Woods - Prize', player),
|
self.get_location('Palace of Darkness - Prize'),
|
||||||
world.get_location('Swamp Palace - Prize', player),
|
self.get_location('Thieves\' Town - Prize'),
|
||||||
world.get_location('Ice Palace - Prize', player),
|
self.get_location('Skull Woods - Prize'),
|
||||||
world.get_location('Misery Mire - Prize', player)]
|
self.get_location('Swamp Palace - Prize'),
|
||||||
|
self.get_location('Ice Palace - Prize'),
|
||||||
|
self.get_location('Misery Mire - Prize')]
|
||||||
placed_prizes = {loc.item.name for loc in crystal_locations if loc.item}
|
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]
|
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]
|
empty_crystal_locations = [loc for loc in crystal_locations if not loc.item]
|
||||||
@@ -526,8 +526,8 @@ class ALTTPWorld(World):
|
|||||||
try:
|
try:
|
||||||
prizepool = unplaced_prizes.copy()
|
prizepool = unplaced_prizes.copy()
|
||||||
prize_locs = empty_crystal_locations.copy()
|
prize_locs = empty_crystal_locations.copy()
|
||||||
world.random.shuffle(prize_locs)
|
self.multiworld.random.shuffle(prize_locs)
|
||||||
fill_restrictive(world, all_state, prize_locs, prizepool, True, lock=True,
|
fill_restrictive(self.multiworld, all_state, prize_locs, prizepool, True, lock=True,
|
||||||
name="LttP Dungeon Prizes")
|
name="LttP Dungeon Prizes")
|
||||||
except FillError as e:
|
except FillError as e:
|
||||||
lttp_logger.exception("Failed to place dungeon prizes (%s). Will retry %s more times", e,
|
lttp_logger.exception("Failed to place dungeon prizes (%s). Will retry %s more times", e,
|
||||||
@@ -541,7 +541,7 @@ class ALTTPWorld(World):
|
|||||||
if self.options.mode == 'standard' and self.options.small_key_shuffle \
|
if self.options.mode == 'standard' and self.options.small_key_shuffle \
|
||||||
and self.options.small_key_shuffle != small_key_shuffle.option_universal and \
|
and self.options.small_key_shuffle != small_key_shuffle.option_universal and \
|
||||||
self.options.small_key_shuffle != small_key_shuffle.option_own_dungeons:
|
self.options.small_key_shuffle != small_key_shuffle.option_own_dungeons:
|
||||||
world.local_early_items[player]["Small Key (Hyrule Castle)"] = 1
|
self.multiworld.local_early_items[self.player]["Small Key (Hyrule Castle)"] = 1
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def stage_pre_fill(cls, world):
|
def stage_pre_fill(cls, world):
|
||||||
@@ -811,12 +811,15 @@ class ALTTPWorld(World):
|
|||||||
return GetBeemizerItem(self.multiworld, self.player, item)
|
return GetBeemizerItem(self.multiworld, self.player, item)
|
||||||
|
|
||||||
def get_pre_fill_items(self):
|
def get_pre_fill_items(self):
|
||||||
res = []
|
res = [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')]
|
||||||
if self.dungeon_local_item_names:
|
if self.dungeon_local_item_names:
|
||||||
for dungeon in self.dungeons.values():
|
for dungeon in self.dungeons.values():
|
||||||
for item in dungeon.all_items:
|
for item in dungeon.all_items:
|
||||||
if item.name in self.dungeon_local_item_names:
|
if item.name in self.dungeon_local_item_names:
|
||||||
res.append(item)
|
res.append(item)
|
||||||
|
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def fill_slot_data(self):
|
def fill_slot_data(self):
|
||||||
|
@@ -207,7 +207,6 @@ class BlasphemousWorld(World):
|
|||||||
if not self.options.skill_randomizer:
|
if not self.options.skill_randomizer:
|
||||||
self.place_items_from_dict(skill_dict)
|
self.place_items_from_dict(skill_dict)
|
||||||
|
|
||||||
|
|
||||||
def place_items_from_set(self, location_set: Set[str], name: str):
|
def place_items_from_set(self, location_set: Set[str], name: str):
|
||||||
for loc in location_set:
|
for loc in location_set:
|
||||||
self.get_location(loc).place_locked_item(self.create_item(name))
|
self.get_location(loc).place_locked_item(self.create_item(name))
|
||||||
|
@@ -127,6 +127,10 @@ class Hylics2World(World):
|
|||||||
tv = tvs.pop()
|
tv = tvs.pop()
|
||||||
self.get_location(tv).place_locked_item(self.create_item(gesture))
|
self.get_location(tv).place_locked_item(self.create_item(gesture))
|
||||||
|
|
||||||
|
def get_pre_fill_items(self) -> List["Item"]:
|
||||||
|
if self.options.gesture_shuffle:
|
||||||
|
return [self.create_item(gesture["name"]) for gesture in Items.gesture_item_table.values()]
|
||||||
|
return []
|
||||||
|
|
||||||
def fill_slot_data(self) -> Dict[str, Any]:
|
def fill_slot_data(self) -> Dict[str, Any]:
|
||||||
slot_data: Dict[str, Any] = {
|
slot_data: Dict[str, Any] = {
|
||||||
|
@@ -436,6 +436,10 @@ class KH2World(World):
|
|||||||
for location in keyblade_locations:
|
for location in keyblade_locations:
|
||||||
location.locked = True
|
location.locked = True
|
||||||
|
|
||||||
|
def get_pre_fill_items(self) -> List["Item"]:
|
||||||
|
return [self.create_item(item) for item in [*DonaldAbility_Table.keys(), *GoofyAbility_Table.keys(),
|
||||||
|
*SupportAbility_Table.keys()]]
|
||||||
|
|
||||||
def starting_invo_verify(self):
|
def starting_invo_verify(self):
|
||||||
"""
|
"""
|
||||||
Making sure the player doesn't put too many abilities in their starting inventory.
|
Making sure the player doesn't put too many abilities in their starting inventory.
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from Fill import distribute_planned
|
from Fill import parse_planned_blocks, distribute_planned_blocks, resolve_early_locations_for_planned
|
||||||
|
from Options import PlandoItems
|
||||||
from test.general import setup_solo_multiworld
|
from test.general import setup_solo_multiworld
|
||||||
from worlds.AutoWorld import call_all
|
from worlds.AutoWorld import call_all
|
||||||
from . import LADXTestBase
|
from . import LADXTestBase
|
||||||
@@ -25,8 +26,11 @@ class PlandoTest(LADXTestBase):
|
|||||||
LinksAwakeningWorld,
|
LinksAwakeningWorld,
|
||||||
("generate_early", "create_regions", "create_items", "set_rules", "generate_basic")
|
("generate_early", "create_regions", "create_items", "set_rules", "generate_basic")
|
||||||
)
|
)
|
||||||
self.multiworld.plando_items[1] = self.options["plando_items"]
|
self.multiworld.worlds[1].options.plando_items = PlandoItems.from_any(self.options["plando_items"])
|
||||||
distribute_planned(self.multiworld)
|
self.multiworld.plando_item_blocks = parse_planned_blocks(self.multiworld)
|
||||||
|
resolve_early_locations_for_planned(self.multiworld)
|
||||||
|
distribute_planned_blocks(self.multiworld, [x for player in self.multiworld.plando_item_blocks
|
||||||
|
for x in self.multiworld.plando_item_blocks[player]])
|
||||||
call_all(self.multiworld, "pre_fill")
|
call_all(self.multiworld, "pre_fill")
|
||||||
|
|
||||||
def test_planned(self):
|
def test_planned(self):
|
||||||
|
@@ -32,7 +32,7 @@ from .Cosmetics import patch_cosmetics
|
|||||||
|
|
||||||
from settings import get_settings
|
from settings import get_settings
|
||||||
from BaseClasses import MultiWorld, CollectionState, Tutorial, LocationProgressType
|
from BaseClasses import MultiWorld, CollectionState, Tutorial, LocationProgressType
|
||||||
from Options import Range, Toggle, VerifyKeys, Accessibility, PlandoConnections
|
from Options import Range, Toggle, VerifyKeys, Accessibility, PlandoConnections, PlandoItems
|
||||||
from Fill import fill_restrictive, fast_fill, FillError
|
from Fill import fill_restrictive, fast_fill, FillError
|
||||||
from worlds.generic.Rules import exclusion_rules, add_item_rule
|
from worlds.generic.Rules import exclusion_rules, add_item_rule
|
||||||
from worlds.AutoWorld import World, AutoLogicRegister, WebWorld
|
from worlds.AutoWorld import World, AutoLogicRegister, WebWorld
|
||||||
@@ -220,6 +220,8 @@ class OOTWorld(World):
|
|||||||
option_value = result.value
|
option_value = result.value
|
||||||
elif isinstance(result, PlandoConnections):
|
elif isinstance(result, PlandoConnections):
|
||||||
option_value = result.value
|
option_value = result.value
|
||||||
|
elif isinstance(result, PlandoItems):
|
||||||
|
option_value = result.value
|
||||||
else:
|
else:
|
||||||
option_value = result.current_key
|
option_value = result.current_key
|
||||||
setattr(self, option_name, option_value)
|
setattr(self, option_name, option_value)
|
||||||
|
@@ -321,7 +321,7 @@ class PokemonRedBlueWorld(World):
|
|||||||
"Fuchsia Gym - Koga Prize", "Saffron Gym - Sabrina Prize",
|
"Fuchsia Gym - Koga Prize", "Saffron Gym - Sabrina Prize",
|
||||||
"Cinnabar Gym - Blaine Prize", "Viridian Gym - Giovanni Prize"
|
"Cinnabar Gym - Blaine Prize", "Viridian Gym - Giovanni Prize"
|
||||||
] if self.multiworld.get_location(loc, self.player).item is None]
|
] if self.multiworld.get_location(loc, self.player).item is None]
|
||||||
state = self.multiworld.get_all_state(False)
|
state = self.multiworld.get_all_state(False, True, False)
|
||||||
# Give it two tries to place badges with wild Pokemon and learnsets as-is.
|
# Give it two tries to place badges with wild Pokemon and learnsets as-is.
|
||||||
# If it can't, then try with all Pokemon collected, and we'll try to fix HM move availability after.
|
# If it can't, then try with all Pokemon collected, and we'll try to fix HM move availability after.
|
||||||
if attempt > 1:
|
if attempt > 1:
|
||||||
@@ -395,7 +395,7 @@ class PokemonRedBlueWorld(World):
|
|||||||
|
|
||||||
# Delete evolution events for Pokémon that are not in logic in an all_state so that accessibility check does not
|
# Delete evolution events for Pokémon that are not in logic in an all_state so that accessibility check does not
|
||||||
# fail. Re-use test_state from previous final loop.
|
# fail. Re-use test_state from previous final loop.
|
||||||
all_state = self.multiworld.get_all_state(False)
|
all_state = self.multiworld.get_all_state(False, True, False)
|
||||||
evolutions_region = self.multiworld.get_region("Evolution", self.player)
|
evolutions_region = self.multiworld.get_region("Evolution", self.player)
|
||||||
for location in evolutions_region.locations.copy():
|
for location in evolutions_region.locations.copy():
|
||||||
if not all_state.can_reach(location, player=self.player):
|
if not all_state.can_reach(location, player=self.player):
|
||||||
@@ -448,7 +448,7 @@ class PokemonRedBlueWorld(World):
|
|||||||
|
|
||||||
self.local_locs = locs
|
self.local_locs = locs
|
||||||
|
|
||||||
all_state = self.multiworld.get_all_state(False)
|
all_state = self.multiworld.get_all_state(False, True, False)
|
||||||
|
|
||||||
reachable_mons = set()
|
reachable_mons = set()
|
||||||
for mon in poke_data.pokemon_data:
|
for mon in poke_data.pokemon_data:
|
||||||
@@ -516,6 +516,11 @@ class PokemonRedBlueWorld(World):
|
|||||||
loc.item = None
|
loc.item = None
|
||||||
loc.place_locked_item(self.pc_item)
|
loc.place_locked_item(self.pc_item)
|
||||||
|
|
||||||
|
def get_pre_fill_items(self) -> typing.List["Item"]:
|
||||||
|
pool = [self.create_item(mon) for mon in poke_data.pokemon_data]
|
||||||
|
pool.append(self.pc_item)
|
||||||
|
return pool
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def stage_post_fill(cls, multiworld):
|
def stage_post_fill(cls, multiworld):
|
||||||
# Convert all but one of each instance of a wild Pokemon to useful classification.
|
# Convert all but one of each instance of a wild Pokemon to useful classification.
|
||||||
|
@@ -400,7 +400,7 @@ def verify_hm_moves(multiworld, world, player):
|
|||||||
last_intervene = None
|
last_intervene = None
|
||||||
while True:
|
while True:
|
||||||
intervene_move = None
|
intervene_move = None
|
||||||
test_state = multiworld.get_all_state(False)
|
test_state = multiworld.get_all_state(False, True, False)
|
||||||
if not logic.can_learn_hm(test_state, world, "Surf", player):
|
if not logic.can_learn_hm(test_state, world, "Surf", player):
|
||||||
intervene_move = "Surf"
|
intervene_move = "Surf"
|
||||||
elif not logic.can_learn_hm(test_state, world, "Strength", player):
|
elif not logic.can_learn_hm(test_state, world, "Strength", player):
|
||||||
|
@@ -66,11 +66,8 @@ def get_plando_locations(world: World) -> List[str]:
|
|||||||
if world is None:
|
if world is None:
|
||||||
return []
|
return []
|
||||||
plando_locations = []
|
plando_locations = []
|
||||||
for plando_setting in world.multiworld.plando_items[world.player]:
|
for plando_setting in world.options.plando_items:
|
||||||
plando_locations += plando_setting.get("locations", [])
|
plando_locations += plando_setting.locations
|
||||||
plando_setting_location = plando_setting.get("location", None)
|
|
||||||
if plando_setting_location is not None:
|
|
||||||
plando_locations.append(plando_setting_location)
|
|
||||||
|
|
||||||
return plando_locations
|
return plando_locations
|
||||||
|
|
||||||
|
@@ -245,7 +245,7 @@ class ShiversWorld(World):
|
|||||||
|
|
||||||
storage_items += [self.create_item("Empty") for _ in range(3)]
|
storage_items += [self.create_item("Empty") for _ in range(3)]
|
||||||
|
|
||||||
state = self.multiworld.get_all_state(False)
|
state = self.multiworld.get_all_state(False, True, False)
|
||||||
|
|
||||||
self.random.shuffle(storage_locs)
|
self.random.shuffle(storage_locs)
|
||||||
self.random.shuffle(storage_items)
|
self.random.shuffle(storage_items)
|
||||||
@@ -255,6 +255,27 @@ class ShiversWorld(World):
|
|||||||
self.storage_placements = {location.name.replace("Storage: ", ""): location.item.name.replace(" DUPE", "") for
|
self.storage_placements = {location.name.replace("Storage: ", ""): location.item.name.replace(" DUPE", "") for
|
||||||
location in storage_locs}
|
location in storage_locs}
|
||||||
|
|
||||||
|
def get_pre_fill_items(self) -> List[Item]:
|
||||||
|
if self.options.full_pots == "pieces":
|
||||||
|
return [self.create_item(name) for name, data in item_table.items() if
|
||||||
|
data.type == ItemType.POT_DUPLICATE]
|
||||||
|
elif self.options.full_pots == "complete":
|
||||||
|
return [self.create_item(name) for name, data in item_table.items() if
|
||||||
|
data.type == ItemType.POT_COMPELTE_DUPLICATE]
|
||||||
|
else:
|
||||||
|
pool = []
|
||||||
|
pieces = [self.create_item(name) for name, data in item_table.items() if
|
||||||
|
data.type == ItemType.POT_DUPLICATE]
|
||||||
|
complete = [self.create_item(name) for name, data in item_table.items() if
|
||||||
|
data.type == ItemType.POT_COMPELTE_DUPLICATE]
|
||||||
|
for i in range(10):
|
||||||
|
if self.pot_completed_list[i] == 0:
|
||||||
|
pool.append(pieces[i])
|
||||||
|
pool.append(pieces[i + 10])
|
||||||
|
else:
|
||||||
|
pool.append(complete[i])
|
||||||
|
return pool
|
||||||
|
|
||||||
def fill_slot_data(self) -> dict:
|
def fill_slot_data(self) -> dict:
|
||||||
return {
|
return {
|
||||||
"StoragePlacements": self.storage_placements,
|
"StoragePlacements": self.storage_placements,
|
||||||
|
@@ -35,9 +35,6 @@ class TestUniversalTrackerGenerationIsStable(SVTestBase):
|
|||||||
args.multi = 1
|
args.multi = 1
|
||||||
args.race = None
|
args.race = None
|
||||||
args.plando_options = self.multiworld.plando_options
|
args.plando_options = self.multiworld.plando_options
|
||||||
args.plando_items = self.multiworld.plando_items
|
|
||||||
args.plando_texts = self.multiworld.plando_texts
|
|
||||||
args.plando_connections = self.multiworld.plando_connections
|
|
||||||
args.game = self.multiworld.game
|
args.game = self.multiworld.game
|
||||||
args.name = self.multiworld.player_name
|
args.name = self.multiworld.player_name
|
||||||
args.sprite = {}
|
args.sprite = {}
|
||||||
|
@@ -214,20 +214,17 @@ class WitnessPlayerItems:
|
|||||||
# Remove items that are mentioned in any plando options. (Hopefully, in the future, plando will get resolved
|
# Remove items that are mentioned in any plando options. (Hopefully, in the future, plando will get resolved
|
||||||
# before create_items so that we'll be able to check placed items instead of just removing all items mentioned
|
# before create_items so that we'll be able to check placed items instead of just removing all items mentioned
|
||||||
# regardless of whether or not they actually wind up being manually placed.
|
# regardless of whether or not they actually wind up being manually placed.
|
||||||
for plando_setting in self._multiworld.plando_items[self._player_id]:
|
for plando_setting in self._world.options.plando_items:
|
||||||
if plando_setting.get("from_pool", True):
|
if plando_setting.from_pool:
|
||||||
for item_setting_key in [key for key in ["item", "items"] if key in plando_setting]:
|
if isinstance(plando_setting.items, dict):
|
||||||
if isinstance(plando_setting[item_setting_key], str):
|
output -= {item for item, weight in plando_setting.items.items() if weight}
|
||||||
output -= {plando_setting[item_setting_key]}
|
else:
|
||||||
elif isinstance(plando_setting[item_setting_key], dict):
|
# Assume this is some other kind of iterable.
|
||||||
output -= {item for item, weight in plando_setting[item_setting_key].items() if weight}
|
for inner_item in plando_setting.items:
|
||||||
else:
|
if isinstance(inner_item, str):
|
||||||
# Assume this is some other kind of iterable.
|
output -= {inner_item}
|
||||||
for inner_item in plando_setting[item_setting_key]:
|
elif isinstance(inner_item, dict):
|
||||||
if isinstance(inner_item, str):
|
output -= {item for item, weight in inner_item.items() if weight}
|
||||||
output -= {inner_item}
|
|
||||||
elif isinstance(inner_item, dict):
|
|
||||||
output -= {item for item, weight in inner_item.items() if weight}
|
|
||||||
|
|
||||||
# Sort the output for consistency across versions if the implementation changes but the logic does not.
|
# Sort the output for consistency across versions if the implementation changes but the logic does not.
|
||||||
return sorted(output)
|
return sorted(output)
|
||||||
|
Reference in New Issue
Block a user