mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-28 10:33:20 -07:00
Merge branch 'ArchipelagoMW:main' into main
This commit is contained in:
189
BaseClasses.py
189
BaseClasses.py
@@ -1,14 +1,15 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import itertools
|
||||
import functools
|
||||
import logging
|
||||
import random
|
||||
import secrets
|
||||
import typing # this can go away when Python 3.8 support is dropped
|
||||
from argparse import Namespace
|
||||
from collections import ChainMap, Counter, deque
|
||||
from collections.abc import Collection
|
||||
from collections import Counter, deque
|
||||
from collections.abc import Collection, MutableSequence
|
||||
from enum import IntEnum, IntFlag
|
||||
from typing import Any, Callable, Dict, Iterable, Iterator, List, NamedTuple, Optional, Set, Tuple, TypedDict, Union, \
|
||||
Type, ClassVar
|
||||
@@ -47,7 +48,6 @@ class ThreadBarrierProxy:
|
||||
class MultiWorld():
|
||||
debug_types = False
|
||||
player_name: Dict[int, str]
|
||||
_region_cache: Dict[int, Dict[str, Region]]
|
||||
difficulty_requirements: dict
|
||||
required_medallions: dict
|
||||
dark_room_logic: Dict[int, str]
|
||||
@@ -57,7 +57,7 @@ class MultiWorld():
|
||||
plando_connections: List
|
||||
worlds: Dict[int, auto_world]
|
||||
groups: Dict[int, Group]
|
||||
regions: List[Region]
|
||||
regions: RegionManager
|
||||
itempool: List[Item]
|
||||
is_race: bool = False
|
||||
precollected_items: Dict[int, List[Item]]
|
||||
@@ -92,6 +92,34 @@ class MultiWorld():
|
||||
def __getitem__(self, player) -> bool:
|
||||
return self.rule(player)
|
||||
|
||||
class RegionManager:
|
||||
region_cache: Dict[int, Dict[str, Region]]
|
||||
entrance_cache: Dict[int, Dict[str, Entrance]]
|
||||
location_cache: Dict[int, Dict[str, Location]]
|
||||
|
||||
def __init__(self, players: int):
|
||||
self.region_cache = {player: {} for player in range(1, players+1)}
|
||||
self.entrance_cache = {player: {} for player in range(1, players+1)}
|
||||
self.location_cache = {player: {} for player in range(1, players+1)}
|
||||
|
||||
def __iadd__(self, other: Iterable[Region]):
|
||||
self.extend(other)
|
||||
return self
|
||||
|
||||
def append(self, region: Region):
|
||||
self.region_cache[region.player][region.name] = region
|
||||
|
||||
def extend(self, regions: Iterable[Region]):
|
||||
for region in regions:
|
||||
self.region_cache[region.player][region.name] = region
|
||||
|
||||
def __iter__(self) -> Iterator[Region]:
|
||||
for regions in self.region_cache.values():
|
||||
yield from regions.values()
|
||||
|
||||
def __len__(self):
|
||||
return sum(len(regions) for regions in self.region_cache.values())
|
||||
|
||||
def __init__(self, players: int):
|
||||
# world-local random state is saved for multiple generations running concurrently
|
||||
self.random = ThreadBarrierProxy(random.Random())
|
||||
@@ -100,16 +128,12 @@ class MultiWorld():
|
||||
self.glitch_triforce = False
|
||||
self.algorithm = 'balanced'
|
||||
self.groups = {}
|
||||
self.regions = []
|
||||
self.regions = self.RegionManager(players)
|
||||
self.shops = []
|
||||
self.itempool = []
|
||||
self.seed = None
|
||||
self.seed_name: str = "Unavailable"
|
||||
self.precollected_items = {player: [] for player in self.player_ids}
|
||||
self._cached_entrances = None
|
||||
self._cached_locations = None
|
||||
self._entrance_cache = {}
|
||||
self._location_cache: Dict[Tuple[str, int], Location] = {}
|
||||
self.required_locations = []
|
||||
self.light_world_light_cone = False
|
||||
self.dark_world_light_cone = False
|
||||
@@ -137,7 +161,6 @@ class MultiWorld():
|
||||
def set_player_attr(attr, val):
|
||||
self.__dict__.setdefault(attr, {})[player] = val
|
||||
|
||||
set_player_attr('_region_cache', {})
|
||||
set_player_attr('shuffle', "vanilla")
|
||||
set_player_attr('logic', "noglitches")
|
||||
set_player_attr('mode', 'open')
|
||||
@@ -199,7 +222,6 @@ class MultiWorld():
|
||||
|
||||
self.game[new_id] = game
|
||||
self.player_types[new_id] = NetUtils.SlotType.group
|
||||
self._region_cache[new_id] = {}
|
||||
world_type = AutoWorld.AutoWorldRegister.world_types[game]
|
||||
self.worlds[new_id] = world_type.create_group(self, new_id, players)
|
||||
self.worlds[new_id].collect_item = classmethod(AutoWorld.World.collect_item).__get__(self.worlds[new_id])
|
||||
@@ -307,6 +329,10 @@ class MultiWorld():
|
||||
def get_game_players(self, game_name: str) -> Tuple[int, ...]:
|
||||
return tuple(player for player in self.player_ids if self.game[player] == game_name)
|
||||
|
||||
@functools.lru_cache()
|
||||
def get_game_groups(self, game_name: str) -> Tuple[int, ...]:
|
||||
return tuple(group_id for group_id in self.groups if self.game[group_id] == game_name)
|
||||
|
||||
@functools.lru_cache()
|
||||
def get_game_worlds(self, game_name: str):
|
||||
return tuple(world for player, world in self.worlds.items() if
|
||||
@@ -329,41 +355,17 @@ class MultiWorld():
|
||||
def world_name_lookup(self):
|
||||
return {self.player_name[player_id]: player_id for player_id in self.player_ids}
|
||||
|
||||
def _recache(self):
|
||||
"""Rebuild world cache"""
|
||||
self._cached_locations = None
|
||||
for region in self.regions:
|
||||
player = region.player
|
||||
self._region_cache[player][region.name] = region
|
||||
for exit in region.exits:
|
||||
self._entrance_cache[exit.name, player] = exit
|
||||
|
||||
for r_location in region.locations:
|
||||
self._location_cache[r_location.name, player] = r_location
|
||||
|
||||
def get_regions(self, player: Optional[int] = None) -> Collection[Region]:
|
||||
return self.regions if player is None else self._region_cache[player].values()
|
||||
return self.regions if player is None else self.regions.region_cache[player].values()
|
||||
|
||||
def get_region(self, regionname: str, player: int) -> Region:
|
||||
try:
|
||||
return self._region_cache[player][regionname]
|
||||
except KeyError:
|
||||
self._recache()
|
||||
return self._region_cache[player][regionname]
|
||||
def get_region(self, region_name: str, player: int) -> Region:
|
||||
return self.regions.region_cache[player][region_name]
|
||||
|
||||
def get_entrance(self, entrance: str, player: int) -> Entrance:
|
||||
try:
|
||||
return self._entrance_cache[entrance, player]
|
||||
except KeyError:
|
||||
self._recache()
|
||||
return self._entrance_cache[entrance, player]
|
||||
def get_entrance(self, entrance_name: str, player: int) -> Entrance:
|
||||
return self.regions.entrance_cache[player][entrance_name]
|
||||
|
||||
def get_location(self, location: str, player: int) -> Location:
|
||||
try:
|
||||
return self._location_cache[location, player]
|
||||
except KeyError:
|
||||
self._recache()
|
||||
return self._location_cache[location, player]
|
||||
def get_location(self, location_name: str, player: int) -> Location:
|
||||
return self.regions.location_cache[player][location_name]
|
||||
|
||||
def get_all_state(self, use_cache: bool) -> CollectionState:
|
||||
cached = getattr(self, "_all_state", None)
|
||||
@@ -424,28 +426,22 @@ class MultiWorld():
|
||||
|
||||
logging.debug('Placed %s at %s', item, location)
|
||||
|
||||
def get_entrances(self) -> List[Entrance]:
|
||||
if self._cached_entrances is None:
|
||||
self._cached_entrances = [entrance for region in self.regions for entrance in region.entrances]
|
||||
return self._cached_entrances
|
||||
|
||||
def clear_entrance_cache(self):
|
||||
self._cached_entrances = None
|
||||
def get_entrances(self, player: Optional[int] = None) -> Iterable[Entrance]:
|
||||
if player is not None:
|
||||
return self.regions.entrance_cache[player].values()
|
||||
return Utils.RepeatableChain(tuple(self.regions.entrance_cache[player].values()
|
||||
for player in self.regions.entrance_cache))
|
||||
|
||||
def register_indirect_condition(self, region: Region, entrance: Entrance):
|
||||
"""Report that access to this Region can result in unlocking this Entrance,
|
||||
state.can_reach(Region) in the Entrance's traversal condition, as opposed to pure transition logic."""
|
||||
self.indirect_connections.setdefault(region, set()).add(entrance)
|
||||
|
||||
def get_locations(self, player: Optional[int] = None) -> List[Location]:
|
||||
if self._cached_locations is None:
|
||||
self._cached_locations = [location for region in self.regions for location in region.locations]
|
||||
def get_locations(self, player: Optional[int] = None) -> Iterable[Location]:
|
||||
if player is not None:
|
||||
return [location for location in self._cached_locations if location.player == player]
|
||||
return self._cached_locations
|
||||
|
||||
def clear_location_cache(self):
|
||||
self._cached_locations = None
|
||||
return self.regions.location_cache[player].values()
|
||||
return Utils.RepeatableChain(tuple(self.regions.location_cache[player].values()
|
||||
for player in self.regions.location_cache))
|
||||
|
||||
def get_unfilled_locations(self, player: Optional[int] = None) -> List[Location]:
|
||||
return [location for location in self.get_locations(player) if location.item is None]
|
||||
@@ -467,16 +463,17 @@ class MultiWorld():
|
||||
valid_locations = [location.name for location in self.get_unfilled_locations(player)]
|
||||
else:
|
||||
valid_locations = location_names
|
||||
relevant_cache = self.regions.location_cache[player]
|
||||
for location_name in valid_locations:
|
||||
location = self._location_cache.get((location_name, player), None)
|
||||
if location is not None and location.item is None:
|
||||
location = relevant_cache.get(location_name, None)
|
||||
if location and location.item is None:
|
||||
yield location
|
||||
|
||||
def unlocks_new_location(self, item: Item) -> bool:
|
||||
temp_state = self.state.copy()
|
||||
temp_state.collect(item, True)
|
||||
|
||||
for location in self.get_unfilled_locations():
|
||||
for location in self.get_unfilled_locations(item.player):
|
||||
if temp_state.can_reach(location) and not self.state.can_reach(location):
|
||||
return True
|
||||
|
||||
@@ -816,15 +813,83 @@ class Region:
|
||||
locations: List[Location]
|
||||
entrance_type: ClassVar[Type[Entrance]] = Entrance
|
||||
|
||||
class Register(MutableSequence):
|
||||
region_manager: MultiWorld.RegionManager
|
||||
|
||||
def __init__(self, region_manager: MultiWorld.RegionManager):
|
||||
self._list = []
|
||||
self.region_manager = region_manager
|
||||
|
||||
def __getitem__(self, index: int) -> Location:
|
||||
return self._list.__getitem__(index)
|
||||
|
||||
def __setitem__(self, index: int, value: Location) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
def __len__(self) -> int:
|
||||
return self._list.__len__()
|
||||
|
||||
# This seems to not be needed, but that's a bit suspicious.
|
||||
# def __del__(self):
|
||||
# self.clear()
|
||||
|
||||
def copy(self):
|
||||
return self._list.copy()
|
||||
|
||||
class LocationRegister(Register):
|
||||
def __delitem__(self, index: int) -> None:
|
||||
location: Location = self._list.__getitem__(index)
|
||||
self._list.__delitem__(index)
|
||||
del(self.region_manager.location_cache[location.player][location.name])
|
||||
|
||||
def insert(self, index: int, value: Location) -> None:
|
||||
self._list.insert(index, value)
|
||||
self.region_manager.location_cache[value.player][value.name] = value
|
||||
|
||||
class EntranceRegister(Register):
|
||||
def __delitem__(self, index: int) -> None:
|
||||
entrance: Entrance = self._list.__getitem__(index)
|
||||
self._list.__delitem__(index)
|
||||
del(self.region_manager.entrance_cache[entrance.player][entrance.name])
|
||||
|
||||
def insert(self, index: int, value: Entrance) -> None:
|
||||
self._list.insert(index, value)
|
||||
self.region_manager.entrance_cache[value.player][value.name] = value
|
||||
|
||||
_locations: LocationRegister[Location]
|
||||
_exits: EntranceRegister[Entrance]
|
||||
|
||||
def __init__(self, name: str, player: int, multiworld: MultiWorld, hint: Optional[str] = None):
|
||||
self.name = name
|
||||
self.entrances = []
|
||||
self.exits = []
|
||||
self.locations = []
|
||||
self._exits = self.EntranceRegister(multiworld.regions)
|
||||
self._locations = self.LocationRegister(multiworld.regions)
|
||||
self.multiworld = multiworld
|
||||
self._hint_text = hint
|
||||
self.player = player
|
||||
|
||||
def get_locations(self):
|
||||
return self._locations
|
||||
|
||||
def set_locations(self, new):
|
||||
if new is self._locations:
|
||||
return
|
||||
self._locations.clear()
|
||||
self._locations.extend(new)
|
||||
|
||||
locations = property(get_locations, set_locations)
|
||||
|
||||
def get_exits(self):
|
||||
return self._exits
|
||||
|
||||
def set_exits(self, new):
|
||||
if new is self._exits:
|
||||
return
|
||||
self._exits.clear()
|
||||
self._exits.extend(new)
|
||||
|
||||
exits = property(get_exits, set_exits)
|
||||
|
||||
def can_reach(self, state: CollectionState) -> bool:
|
||||
if state.stale[self.player]:
|
||||
state.update_reachable_regions(self.player)
|
||||
|
||||
45
Fill.py
45
Fill.py
@@ -15,6 +15,10 @@ class FillError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def _log_fill_progress(name: str, placed: int, total_items: int) -> None:
|
||||
logging.info(f"Current fill step ({name}) at {placed}/{total_items} items placed.")
|
||||
|
||||
|
||||
def sweep_from_pool(base_state: CollectionState, itempool: typing.Sequence[Item] = tuple()) -> CollectionState:
|
||||
new_state = base_state.copy()
|
||||
for item in itempool:
|
||||
@@ -26,7 +30,7 @@ def sweep_from_pool(base_state: CollectionState, itempool: typing.Sequence[Item]
|
||||
def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations: typing.List[Location],
|
||||
item_pool: typing.List[Item], single_player_placement: bool = False, lock: bool = False,
|
||||
swap: bool = True, on_place: typing.Optional[typing.Callable[[Location], None]] = None,
|
||||
allow_partial: bool = False, allow_excluded: bool = False) -> None:
|
||||
allow_partial: bool = False, allow_excluded: bool = False, name: str = "Unknown") -> None:
|
||||
"""
|
||||
:param world: Multiworld to be filled.
|
||||
:param base_state: State assumed before fill.
|
||||
@@ -38,16 +42,20 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations:
|
||||
:param on_place: callback that is called when a placement happens
|
||||
:param allow_partial: only place what is possible. Remaining items will be in the item_pool list.
|
||||
:param allow_excluded: if true and placement fails, it is re-attempted while ignoring excluded on Locations
|
||||
:param name: name of this fill step for progress logging purposes
|
||||
"""
|
||||
unplaced_items: typing.List[Item] = []
|
||||
placements: typing.List[Location] = []
|
||||
cleanup_required = False
|
||||
|
||||
swapped_items: typing.Counter[typing.Tuple[int, str, bool]] = Counter()
|
||||
reachable_items: typing.Dict[int, typing.Deque[Item]] = {}
|
||||
for item in item_pool:
|
||||
reachable_items.setdefault(item.player, deque()).append(item)
|
||||
|
||||
# for progress logging
|
||||
total = min(len(item_pool), len(locations))
|
||||
placed = 0
|
||||
|
||||
while any(reachable_items.values()) and locations:
|
||||
# grab one item per player
|
||||
items_to_place = [items.pop()
|
||||
@@ -152,9 +160,15 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations:
|
||||
spot_to_fill.locked = lock
|
||||
placements.append(spot_to_fill)
|
||||
spot_to_fill.event = item_to_place.advancement
|
||||
placed += 1
|
||||
if not placed % 1000:
|
||||
_log_fill_progress(name, placed, total)
|
||||
if on_place:
|
||||
on_place(spot_to_fill)
|
||||
|
||||
if total > 1000:
|
||||
_log_fill_progress(name, placed, total)
|
||||
|
||||
if cleanup_required:
|
||||
# validate all placements and remove invalid ones
|
||||
state = sweep_from_pool(base_state, [])
|
||||
@@ -198,6 +212,8 @@ def remaining_fill(world: MultiWorld,
|
||||
unplaced_items: typing.List[Item] = []
|
||||
placements: typing.List[Location] = []
|
||||
swapped_items: typing.Counter[typing.Tuple[int, str]] = Counter()
|
||||
total = min(len(itempool), len(locations))
|
||||
placed = 0
|
||||
while locations and itempool:
|
||||
item_to_place = itempool.pop()
|
||||
spot_to_fill: typing.Optional[Location] = None
|
||||
@@ -247,6 +263,12 @@ def remaining_fill(world: MultiWorld,
|
||||
|
||||
world.push_item(spot_to_fill, item_to_place, False)
|
||||
placements.append(spot_to_fill)
|
||||
placed += 1
|
||||
if not placed % 1000:
|
||||
_log_fill_progress("Remaining", placed, total)
|
||||
|
||||
if total > 1000:
|
||||
_log_fill_progress("Remaining", placed, total)
|
||||
|
||||
if unplaced_items and locations:
|
||||
# There are leftover unplaceable items and locations that won't accept them
|
||||
@@ -282,7 +304,7 @@ def accessibility_corrections(world: MultiWorld, state: CollectionState, locatio
|
||||
locations.append(location)
|
||||
if pool and locations:
|
||||
locations.sort(key=lambda loc: loc.progress_type != LocationProgressType.PRIORITY)
|
||||
fill_restrictive(world, state, locations, pool)
|
||||
fill_restrictive(world, state, locations, pool, name="Accessibility Corrections")
|
||||
|
||||
|
||||
def inaccessible_location_rules(world: MultiWorld, state: CollectionState, locations):
|
||||
@@ -352,23 +374,25 @@ def distribute_early_items(world: MultiWorld,
|
||||
player_local = early_local_rest_items[player]
|
||||
fill_restrictive(world, base_state,
|
||||
[loc for loc in early_locations if loc.player == player],
|
||||
player_local, lock=True, allow_partial=True)
|
||||
player_local, lock=True, allow_partial=True, name=f"Local Early Items P{player}")
|
||||
if player_local:
|
||||
logging.warning(f"Could not fulfill rules of early items: {player_local}")
|
||||
early_rest_items.extend(early_local_rest_items[player])
|
||||
early_locations = [loc for loc in early_locations if not loc.item]
|
||||
fill_restrictive(world, base_state, early_locations, early_rest_items, lock=True, allow_partial=True)
|
||||
fill_restrictive(world, base_state, early_locations, early_rest_items, lock=True, allow_partial=True,
|
||||
name="Early Items")
|
||||
early_locations += early_priority_locations
|
||||
for player in world.player_ids:
|
||||
player_local = early_local_prog_items[player]
|
||||
fill_restrictive(world, base_state,
|
||||
[loc for loc in early_locations if loc.player == player],
|
||||
player_local, lock=True, allow_partial=True)
|
||||
player_local, lock=True, allow_partial=True, name=f"Local Early Progression P{player}")
|
||||
if player_local:
|
||||
logging.warning(f"Could not fulfill rules of early items: {player_local}")
|
||||
early_prog_items.extend(player_local)
|
||||
early_locations = [loc for loc in early_locations if not loc.item]
|
||||
fill_restrictive(world, base_state, early_locations, early_prog_items, lock=True, allow_partial=True)
|
||||
fill_restrictive(world, base_state, early_locations, early_prog_items, lock=True, allow_partial=True,
|
||||
name="Early Progression")
|
||||
unplaced_early_items = early_rest_items + early_prog_items
|
||||
if unplaced_early_items:
|
||||
logging.warning("Ran out of early locations for early items. Failed to place "
|
||||
@@ -422,13 +446,14 @@ def distribute_items_restrictive(world: MultiWorld) -> None:
|
||||
|
||||
if prioritylocations:
|
||||
# "priority fill"
|
||||
fill_restrictive(world, world.state, prioritylocations, progitempool, swap=False, on_place=mark_for_locking)
|
||||
fill_restrictive(world, world.state, prioritylocations, progitempool, swap=False, on_place=mark_for_locking,
|
||||
name="Priority")
|
||||
accessibility_corrections(world, world.state, prioritylocations, progitempool)
|
||||
defaultlocations = prioritylocations + defaultlocations
|
||||
|
||||
if progitempool:
|
||||
# "progression fill"
|
||||
fill_restrictive(world, world.state, defaultlocations, progitempool)
|
||||
# "advancement/progression fill"
|
||||
fill_restrictive(world, world.state, defaultlocations, progitempool, name="Progression")
|
||||
if progitempool:
|
||||
raise FillError(
|
||||
f'Not enough locations for progress items. There are {len(progitempool)} more items than locations')
|
||||
|
||||
7
Main.py
7
Main.py
@@ -122,10 +122,6 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
logger.info('Creating Items.')
|
||||
AutoWorld.call_all(world, "create_items")
|
||||
|
||||
# All worlds should have finished creating all regions, locations, and entrances.
|
||||
# Recache to ensure that they are all visible for locality rules.
|
||||
world._recache()
|
||||
|
||||
logger.info('Calculating Access Rules.')
|
||||
|
||||
for player in world.player_ids:
|
||||
@@ -233,7 +229,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
|
||||
region = Region("Menu", group_id, world, "ItemLink")
|
||||
world.regions.append(region)
|
||||
locations = region.locations = []
|
||||
locations = region.locations
|
||||
for item in world.itempool:
|
||||
count = common_item_count.get(item.player, {}).get(item.name, 0)
|
||||
if count:
|
||||
@@ -267,7 +263,6 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
world.itempool.extend(items_to_add[:itemcount - len(world.itempool)])
|
||||
|
||||
if any(world.item_links.values()):
|
||||
world._recache()
|
||||
world._all_state = None
|
||||
|
||||
logger.info("Running Item Plando")
|
||||
|
||||
27
Utils.py
27
Utils.py
@@ -5,6 +5,7 @@ import json
|
||||
import typing
|
||||
import builtins
|
||||
import os
|
||||
import itertools
|
||||
import subprocess
|
||||
import sys
|
||||
import pickle
|
||||
@@ -257,15 +258,13 @@ def get_public_ipv6() -> str:
|
||||
return ip
|
||||
|
||||
|
||||
OptionsType = Settings # TODO: remove ~2 versions after 0.4.1
|
||||
OptionsType = Settings # TODO: remove when removing get_options
|
||||
|
||||
|
||||
@cache_argsless
|
||||
def get_default_options() -> Settings: # TODO: remove ~2 versions after 0.4.1
|
||||
return Settings(None)
|
||||
|
||||
|
||||
get_options = get_settings # TODO: add a warning ~2 versions after 0.4.1 and remove once all games are ported
|
||||
def get_options() -> Settings:
|
||||
# TODO: switch to Utils.deprecate after 0.4.4
|
||||
warnings.warn("Utils.get_options() is deprecated. Use the settings API instead.", DeprecationWarning)
|
||||
return get_settings()
|
||||
|
||||
|
||||
def persistent_store(category: str, key: typing.Any, value: typing.Any):
|
||||
@@ -905,3 +904,17 @@ def visualize_regions(root_region: Region, file_name: str, *,
|
||||
|
||||
with open(file_name, "wt", encoding="utf-8") as f:
|
||||
f.write("\n".join(uml))
|
||||
|
||||
|
||||
class RepeatableChain:
|
||||
def __init__(self, iterable: typing.Iterable):
|
||||
self.iterable = iterable
|
||||
|
||||
def __iter__(self):
|
||||
return itertools.chain.from_iterable(self.iterable)
|
||||
|
||||
def __bool__(self):
|
||||
return any(sub_iterable for sub_iterable in self.iterable)
|
||||
|
||||
def __len__(self):
|
||||
return sum(len(iterable) for iterable in self.iterable)
|
||||
|
||||
@@ -2,13 +2,62 @@
|
||||
|
||||
## What is a randomizer?
|
||||
|
||||
A randomizer is a modification of a video game which reorganizes the items required to progress through the game. A
|
||||
normal play-through of a game might require you to use item A to unlock item B, then C, and so forth. In a randomized
|
||||
A randomizer is a modification of a game which reorganizes the items required to progress through that game. A
|
||||
normal play-through might require you to use item A to unlock item B, then C, and so forth. In a randomized
|
||||
game, you might first find item C, then A, then B.
|
||||
|
||||
This transforms games from a linear experience into a puzzle, presenting players with a new challenge each time they
|
||||
play a randomized game. Putting items in non-standard locations can require the player to think about the game world and
|
||||
the items they encounter in new and interesting ways.
|
||||
This transforms the game from a linear experience into a puzzle, presenting players with a new challenge each time they
|
||||
play. Putting items in non-standard locations can require the player to think about the game world and the items they
|
||||
encounter in new and interesting ways.
|
||||
|
||||
## What is a multiworld?
|
||||
|
||||
While a randomizer shuffles a game, a multiworld randomizer shuffles that game for multiple players. For example, in a
|
||||
two player multiworld, players A and B each get their own randomized version of a game, called a world. In each
|
||||
player's game, they may find items which belong to the other player. If player A finds an item which belongs to
|
||||
player B, the item will be sent to player B's world over the internet. This creates a cooperative experience, requiring
|
||||
players to rely upon each other to complete their game.
|
||||
|
||||
## What does multi-game mean?
|
||||
|
||||
While a multiworld game traditionally requires all players to be playing the same game, a multi-game multiworld allows
|
||||
players to randomize any of the supported games, and send items between them. This allows players of different
|
||||
games to interact with one another in a single multiplayer environment. Archipelago supports multi-game multiworld.
|
||||
Here is a list of our [Supported Games](https://archipelago.gg/games).
|
||||
|
||||
## Can I generate a single-player game with Archipelago?
|
||||
|
||||
Yes. All of our supported games can be generated as single-player experiences both on the website and by installing
|
||||
the Archipelago generator software. The fastest way to do this is on the website. Find the Supported Game you wish to
|
||||
play, open the Settings Page, pick your settings, and click Generate Game.
|
||||
|
||||
## How do I get started?
|
||||
|
||||
We have a [Getting Started](https://archipelago.gg/tutorial/Archipelago/setup/en) guide that will help you get the
|
||||
software set up. You can use that guide to learn how to generate multiworlds. There are also basic instructions for
|
||||
including multiple games, and hosting multiworlds on the website for ease and convenience.
|
||||
|
||||
If you are ready to start randomizing games, or want to start playing your favorite randomizer with others, please join
|
||||
our discord server at the [Archipelago Discord](https://discord.gg/8Z65BR2). There are always people ready to answer
|
||||
any questions you might have.
|
||||
|
||||
## What are some common terms I should know?
|
||||
|
||||
As randomizers and multiworld randomizers have been around for a while now, there are quite a few common terms used
|
||||
by the communities surrounding them. A list of Archipelago jargon and terms commonly used by the community can be
|
||||
found in the [Glossary](/glossary/en).
|
||||
|
||||
## Does everyone need to be connected at the same time?
|
||||
|
||||
There are two different play-styles that are common for Archipelago multiworld sessions. These sessions can either
|
||||
be considered synchronous (or "sync"), where everyone connects and plays at the same time, or asynchronous (or "async"),
|
||||
where players connect and play at their own pace. The setup for both is identical. The difference in play-style is how
|
||||
you and your friends choose to organize and play your multiworld. Most groups decide on the format before creating
|
||||
their multiworld.
|
||||
|
||||
If a player must leave early, they can use Archipelago's release system. When a player releases their game, all items
|
||||
in that game belonging to other players are sent out automatically. This allows other players to continue to play
|
||||
uninterrupted. Here is a list of all of our [Server Commands](https://archipelago.gg/tutorial/Archipelago/commands/en).
|
||||
|
||||
## What happens if an item is placed somewhere it is impossible to get?
|
||||
|
||||
@@ -17,53 +66,15 @@ is to ensure items necessary to complete the game will be accessible to the play
|
||||
rules allowing certain items to be placed in normally unreachable locations, provided the player has indicated they are
|
||||
comfortable exploiting certain glitches in the game.
|
||||
|
||||
## What is a multi-world?
|
||||
|
||||
While a randomizer shuffles a game, a multi-world randomizer shuffles that game for multiple players. For example, in a
|
||||
two player multi-world, players A and B each get their own randomized version of a game, called a world. In each player's
|
||||
game, they may find items which belong to the other player. If player A finds an item which belongs to player B, the
|
||||
item will be sent to player B's world over the internet.
|
||||
|
||||
This creates a cooperative experience during multi-world games, requiring players to rely upon each other to complete
|
||||
their game.
|
||||
|
||||
## What happens if a person has to leave early?
|
||||
|
||||
If a player must leave early, they can use Archipelago's release system. When a player releases their game, all the
|
||||
items in that game which belong to other players are sent out automatically, so other players can continue to play.
|
||||
|
||||
## What does multi-game mean?
|
||||
|
||||
While a multi-world game traditionally requires all players to be playing the same game, a multi-game multi-world allows
|
||||
players to randomize any of a number of supported games, and send items between them. This allows players of different
|
||||
games to interact with one another in a single multiplayer environment.
|
||||
|
||||
## Can I generate a single-player game with Archipelago?
|
||||
|
||||
Yes. All our supported games can be generated as single-player experiences, and so long as you download the software,
|
||||
the website is not required to generate them.
|
||||
|
||||
## How do I get started?
|
||||
|
||||
If you are ready to start randomizing games, or want to start playing your favorite randomizer with others, please join
|
||||
our discord server at the [Archipelago Discord](https://discord.gg/8Z65BR2). There are always people ready to answer
|
||||
any questions you might have.
|
||||
|
||||
## What are some common terms I should know?
|
||||
|
||||
As randomizers and multiworld randomizers have been around for a while now there are quite a lot of common terms
|
||||
and jargon that is used in conjunction by the communities surrounding them. For a lot of the terms that are more common
|
||||
to Archipelago and its specific systems please see the [Glossary](/glossary/en).
|
||||
|
||||
## I want to add a game to the Archipelago randomizer. How do I do that?
|
||||
|
||||
The best way to get started is to take a look at our code on GitHub
|
||||
at [Archipelago GitHub Page](https://github.com/ArchipelagoMW/Archipelago).
|
||||
The best way to get started is to take a look at our code on GitHub:
|
||||
[Archipelago GitHub Page](https://github.com/ArchipelagoMW/Archipelago).
|
||||
|
||||
There you will find examples of games in the worlds folder
|
||||
at [/worlds Folder in Archipelago Code](https://github.com/ArchipelagoMW/Archipelago/tree/main/worlds).
|
||||
There, you will find examples of games in the `worlds` folder:
|
||||
[/worlds Folder in Archipelago Code](https://github.com/ArchipelagoMW/Archipelago/tree/main/worlds).
|
||||
|
||||
You may also find developer documentation in the docs folder
|
||||
at [/docs Folder in Archipelago Code](https://github.com/ArchipelagoMW/Archipelago/tree/main/docs).
|
||||
You may also find developer documentation in the `docs` folder:
|
||||
[/docs Folder in Archipelago Code](https://github.com/ArchipelagoMW/Archipelago/tree/main/docs).
|
||||
|
||||
If you have more questions, feel free to ask in the **#archipelago-dev** channel on our Discord.
|
||||
|
||||
@@ -284,7 +284,7 @@ class WorldTestBase(unittest.TestCase):
|
||||
|
||||
# basically a shortened reimplementation of this method from core, in order to force the check is done
|
||||
def fulfills_accessibility() -> bool:
|
||||
locations = self.multiworld.get_locations(1).copy()
|
||||
locations = list(self.multiworld.get_locations(1))
|
||||
state = CollectionState(self.multiworld)
|
||||
while locations:
|
||||
sphere: typing.List[Location] = []
|
||||
|
||||
@@ -16,7 +16,7 @@ class TestIDs(unittest.TestCase):
|
||||
|
||||
def test_utils_in_yaml(self) -> None:
|
||||
"""Tests that the auto generated host.yaml has default settings in it"""
|
||||
for option_key, option_set in Utils.get_default_options().items():
|
||||
for option_key, option_set in Settings(None).items():
|
||||
with self.subTest(option_key):
|
||||
self.assertIn(option_key, self.yaml_options)
|
||||
for sub_option_key in option_set:
|
||||
@@ -24,7 +24,7 @@ class TestIDs(unittest.TestCase):
|
||||
|
||||
def test_yaml_in_utils(self) -> None:
|
||||
"""Tests that the auto generated host.yaml shows up in reference calls"""
|
||||
utils_options = Utils.get_default_options()
|
||||
utils_options = Settings(None)
|
||||
for option_key, option_set in self.yaml_options.items():
|
||||
with self.subTest(option_key):
|
||||
self.assertIn(option_key, utils_options)
|
||||
|
||||
@@ -36,7 +36,6 @@ class TestBase(unittest.TestCase):
|
||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||
with self.subTest("Game", game_name=game_name):
|
||||
multiworld = setup_solo_multiworld(world_type, gen_steps)
|
||||
multiworld._recache()
|
||||
region_count = len(multiworld.get_regions())
|
||||
location_count = len(multiworld.get_locations())
|
||||
|
||||
@@ -46,14 +45,12 @@ class TestBase(unittest.TestCase):
|
||||
self.assertEqual(location_count, len(multiworld.get_locations()),
|
||||
f"{game_name} modified locations count during rule creation")
|
||||
|
||||
multiworld._recache()
|
||||
call_all(multiworld, "generate_basic")
|
||||
self.assertEqual(region_count, len(multiworld.get_regions()),
|
||||
f"{game_name} modified region count during generate_basic")
|
||||
self.assertGreaterEqual(location_count, len(multiworld.get_locations()),
|
||||
f"{game_name} modified locations count during generate_basic")
|
||||
|
||||
multiworld._recache()
|
||||
call_all(multiworld, "pre_fill")
|
||||
self.assertEqual(region_count, len(multiworld.get_regions()),
|
||||
f"{game_name} modified region count during pre_fill")
|
||||
|
||||
@@ -4,6 +4,7 @@ import hashlib
|
||||
import logging
|
||||
import pathlib
|
||||
import sys
|
||||
import time
|
||||
from dataclasses import make_dataclass
|
||||
from typing import Any, Callable, ClassVar, Dict, Set, Tuple, FrozenSet, List, Optional, TYPE_CHECKING, TextIO, Type, \
|
||||
Union
|
||||
@@ -17,6 +18,8 @@ if TYPE_CHECKING:
|
||||
from . import GamesPackage
|
||||
from settings import Group
|
||||
|
||||
perf_logger = logging.getLogger("performance")
|
||||
|
||||
|
||||
class AutoWorldRegister(type):
|
||||
world_types: Dict[str, Type[World]] = {}
|
||||
@@ -103,10 +106,24 @@ class AutoLogicRegister(type):
|
||||
return new_class
|
||||
|
||||
|
||||
def _timed_call(method: Callable[..., Any], *args: Any,
|
||||
multiworld: Optional["MultiWorld"] = None, player: Optional[int] = None) -> Any:
|
||||
start = time.perf_counter()
|
||||
ret = method(*args)
|
||||
taken = time.perf_counter() - start
|
||||
if taken > 1.0:
|
||||
if player and multiworld:
|
||||
perf_logger.info(f"Took {taken} seconds in {method.__qualname__} for player {player}, "
|
||||
f"named {multiworld.player_name[player]}.")
|
||||
else:
|
||||
perf_logger.info(f"Took {taken} seconds in {method.__qualname__}.")
|
||||
return ret
|
||||
|
||||
|
||||
def call_single(multiworld: "MultiWorld", method_name: str, player: int, *args: Any) -> Any:
|
||||
method = getattr(multiworld.worlds[player], method_name)
|
||||
try:
|
||||
ret = method(*args)
|
||||
ret = _timed_call(method, *args, multiworld=multiworld, player=player)
|
||||
except Exception as e:
|
||||
message = f"Exception in {method} for player {player}, named {multiworld.player_name[player]}."
|
||||
if sys.version_info >= (3, 11, 0):
|
||||
@@ -132,18 +149,15 @@ def call_all(multiworld: "MultiWorld", method_name: str, *args: Any) -> None:
|
||||
f"Duplicate item reference of \"{item.name}\" in \"{multiworld.worlds[player].game}\" "
|
||||
f"of player \"{multiworld.player_name[player]}\". Please make a copy instead.")
|
||||
|
||||
for world_type in sorted(world_types, key=lambda world: world.__name__):
|
||||
stage_callable = getattr(world_type, f"stage_{method_name}", None)
|
||||
if stage_callable:
|
||||
stage_callable(multiworld, *args)
|
||||
call_stage(multiworld, method_name, *args)
|
||||
|
||||
|
||||
def call_stage(multiworld: "MultiWorld", method_name: str, *args: Any) -> None:
|
||||
world_types = {multiworld.worlds[player].__class__ for player in multiworld.player_ids}
|
||||
for world_type in world_types:
|
||||
for world_type in sorted(world_types, key=lambda world: world.__name__):
|
||||
stage_callable = getattr(world_type, f"stage_{method_name}", None)
|
||||
if stage_callable:
|
||||
stage_callable(multiworld, *args)
|
||||
_timed_call(stage_callable, multiworld, *args)
|
||||
|
||||
|
||||
class WebWorld:
|
||||
|
||||
@@ -5,6 +5,7 @@ checking or launching the client, otherwise it will probably cause circular impo
|
||||
|
||||
|
||||
import asyncio
|
||||
import enum
|
||||
import subprocess
|
||||
import traceback
|
||||
from typing import Any, Dict, Optional
|
||||
@@ -21,6 +22,13 @@ from .client import BizHawkClient, AutoBizHawkClientRegister
|
||||
EXPECTED_SCRIPT_VERSION = 1
|
||||
|
||||
|
||||
class AuthStatus(enum.IntEnum):
|
||||
NOT_AUTHENTICATED = 0
|
||||
NEED_INFO = 1
|
||||
PENDING = 2
|
||||
AUTHENTICATED = 3
|
||||
|
||||
|
||||
class BizHawkClientCommandProcessor(ClientCommandProcessor):
|
||||
def _cmd_bh(self):
|
||||
"""Shows the current status of the client's connection to BizHawk"""
|
||||
@@ -35,6 +43,8 @@ class BizHawkClientCommandProcessor(ClientCommandProcessor):
|
||||
|
||||
class BizHawkClientContext(CommonContext):
|
||||
command_processor = BizHawkClientCommandProcessor
|
||||
auth_status: AuthStatus
|
||||
password_requested: bool
|
||||
client_handler: Optional[BizHawkClient]
|
||||
slot_data: Optional[Dict[str, Any]] = None
|
||||
rom_hash: Optional[str] = None
|
||||
@@ -45,6 +55,8 @@ class BizHawkClientContext(CommonContext):
|
||||
|
||||
def __init__(self, server_address: Optional[str], password: Optional[str]):
|
||||
super().__init__(server_address, password)
|
||||
self.auth_status = AuthStatus.NOT_AUTHENTICATED
|
||||
self.password_requested = False
|
||||
self.client_handler = None
|
||||
self.bizhawk_ctx = BizHawkContext()
|
||||
self.watcher_timeout = 0.5
|
||||
@@ -61,10 +73,41 @@ class BizHawkClientContext(CommonContext):
|
||||
def on_package(self, cmd, args):
|
||||
if cmd == "Connected":
|
||||
self.slot_data = args.get("slot_data", None)
|
||||
self.auth_status = AuthStatus.AUTHENTICATED
|
||||
|
||||
if self.client_handler is not None:
|
||||
self.client_handler.on_package(self, cmd, args)
|
||||
|
||||
async def server_auth(self, password_requested: bool = False):
|
||||
self.password_requested = password_requested
|
||||
|
||||
if self.bizhawk_ctx.connection_status != ConnectionStatus.CONNECTED:
|
||||
logger.info("Awaiting connection to BizHawk before authenticating")
|
||||
return
|
||||
|
||||
if self.client_handler is None:
|
||||
return
|
||||
|
||||
# Ask handler to set auth
|
||||
if self.auth is None:
|
||||
self.auth_status = AuthStatus.NEED_INFO
|
||||
await self.client_handler.set_auth(self)
|
||||
|
||||
# Handler didn't set auth, ask user for slot name
|
||||
if self.auth is None:
|
||||
await self.get_username()
|
||||
|
||||
if password_requested and not self.password:
|
||||
self.auth_status = AuthStatus.NEED_INFO
|
||||
await super(BizHawkClientContext, self).server_auth(password_requested)
|
||||
|
||||
await self.send_connect()
|
||||
self.auth_status = AuthStatus.PENDING
|
||||
|
||||
async def disconnect(self, allow_autoreconnect: bool = False):
|
||||
self.auth_status = AuthStatus.NOT_AUTHENTICATED
|
||||
await super().disconnect(allow_autoreconnect)
|
||||
|
||||
|
||||
async def _game_watcher(ctx: BizHawkClientContext):
|
||||
showed_connecting_message = False
|
||||
@@ -109,12 +152,13 @@ async def _game_watcher(ctx: BizHawkClientContext):
|
||||
|
||||
rom_hash = await get_hash(ctx.bizhawk_ctx)
|
||||
if ctx.rom_hash is not None and ctx.rom_hash != rom_hash:
|
||||
if ctx.server is not None:
|
||||
if ctx.server is not None and not ctx.server.socket.closed:
|
||||
logger.info(f"ROM changed. Disconnecting from server.")
|
||||
await ctx.disconnect(True)
|
||||
|
||||
ctx.auth = None
|
||||
ctx.username = None
|
||||
ctx.client_handler = None
|
||||
await ctx.disconnect(False)
|
||||
ctx.rom_hash = rom_hash
|
||||
|
||||
if ctx.client_handler is None:
|
||||
@@ -136,15 +180,14 @@ async def _game_watcher(ctx: BizHawkClientContext):
|
||||
except NotConnectedError:
|
||||
continue
|
||||
|
||||
# Get slot name and send `Connect`
|
||||
if ctx.server is not None and ctx.username is None:
|
||||
await ctx.client_handler.set_auth(ctx)
|
||||
|
||||
if ctx.auth is None:
|
||||
await ctx.get_username()
|
||||
|
||||
await ctx.send_connect()
|
||||
# Server auth
|
||||
if ctx.server is not None and not ctx.server.socket.closed:
|
||||
if ctx.auth_status == AuthStatus.NOT_AUTHENTICATED:
|
||||
Utils.async_start(ctx.server_auth(ctx.password_requested))
|
||||
else:
|
||||
ctx.auth_status = AuthStatus.NOT_AUTHENTICATED
|
||||
|
||||
# Call the handler's game watcher
|
||||
await ctx.client_handler.game_watcher(ctx)
|
||||
|
||||
|
||||
|
||||
@@ -6,9 +6,8 @@ from typing import Optional, Any
|
||||
|
||||
import Utils
|
||||
from .Locations import AdventureLocation, LocationData
|
||||
from Utils import OptionsType
|
||||
from settings import get_settings
|
||||
from worlds.Files import APDeltaPatch, AutoPatchRegister, APContainer
|
||||
from itertools import chain
|
||||
|
||||
import bsdiff4
|
||||
|
||||
@@ -313,9 +312,8 @@ def get_base_rom_bytes(file_name: str = "") -> bytes:
|
||||
|
||||
|
||||
def get_base_rom_path(file_name: str = "") -> str:
|
||||
options: OptionsType = Utils.get_options()
|
||||
if not file_name:
|
||||
file_name = options["adventure_options"]["rom_file"]
|
||||
file_name = get_settings()["adventure_options"]["rom_file"]
|
||||
if not os.path.exists(file_name):
|
||||
file_name = Utils.user_path(file_name)
|
||||
return file_name
|
||||
|
||||
@@ -264,7 +264,8 @@ def fill_dungeons_restrictive(multiworld: MultiWorld):
|
||||
|
||||
if loc in all_state_base.events:
|
||||
all_state_base.events.remove(loc)
|
||||
fill_restrictive(multiworld, all_state_base, locations, in_dungeon_items, True, True)
|
||||
fill_restrictive(multiworld, all_state_base, locations, in_dungeon_items, True, True,
|
||||
name="LttP Dungeon Items")
|
||||
|
||||
|
||||
dungeon_music_addresses = {'Eastern Palace - Prize': [0x1559A],
|
||||
|
||||
@@ -293,7 +293,6 @@ def generate_itempool(world):
|
||||
loc.access_rule = lambda state: has_triforce_pieces(state, player)
|
||||
|
||||
region.locations.append(loc)
|
||||
multiworld.clear_location_cache()
|
||||
|
||||
multiworld.push_item(loc, ItemFactory('Triforce', player), False)
|
||||
loc.event = True
|
||||
|
||||
@@ -786,8 +786,8 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
||||
|
||||
# patch items
|
||||
|
||||
for location in world.get_locations():
|
||||
if location.player != player or location.address is None or location.shop_slot is not None:
|
||||
for location in world.get_locations(player):
|
||||
if location.address is None or location.shop_slot is not None:
|
||||
continue
|
||||
|
||||
itemid = location.item.code if location.item is not None else 0x5A
|
||||
@@ -2247,7 +2247,7 @@ def write_strings(rom, world, player):
|
||||
tt['sign_north_of_links_house'] = '> Randomizer The telepathic tiles can have hints!'
|
||||
hint_locations = HintLocations.copy()
|
||||
local_random.shuffle(hint_locations)
|
||||
all_entrances = [entrance for entrance in world.get_entrances() if entrance.player == player]
|
||||
all_entrances = list(world.get_entrances(player))
|
||||
local_random.shuffle(all_entrances)
|
||||
|
||||
# First we take care of the one inconvenient dungeon in the appropriately simple shuffles.
|
||||
|
||||
@@ -197,8 +197,13 @@ def global_rules(world, player):
|
||||
# determines which S&Q locations are available - hide from paths since it isn't an in-game location
|
||||
for exit in world.get_region('Menu', player).exits:
|
||||
exit.hide_path = True
|
||||
|
||||
set_rule(world.get_entrance('Old Man S&Q', player), lambda state: state.can_reach('Old Man', 'Location', player))
|
||||
try:
|
||||
old_man_sq = world.get_entrance('Old Man S&Q', player)
|
||||
except KeyError:
|
||||
pass # it doesn't exist, should be dungeon-only unittests
|
||||
else:
|
||||
old_man = world.get_location("Old Man", player)
|
||||
set_rule(old_man_sq, lambda state: old_man.can_reach(state))
|
||||
|
||||
set_rule(world.get_location('Sunken Treasure', player), lambda state: state.has('Open Floodgate', player))
|
||||
set_rule(world.get_location('Dark Blacksmith Ruins', player), lambda state: state.has('Return Smith', player))
|
||||
@@ -1526,16 +1531,16 @@ def set_bunny_rules(world: MultiWorld, player: int, inverted: bool):
|
||||
# Helper functions to determine if the moon pearl is required
|
||||
if inverted:
|
||||
def is_bunny(region):
|
||||
return region.is_light_world
|
||||
return region and region.is_light_world
|
||||
|
||||
def is_link(region):
|
||||
return region.is_dark_world
|
||||
return region and region.is_dark_world
|
||||
else:
|
||||
def is_bunny(region):
|
||||
return region.is_dark_world
|
||||
return region and region.is_dark_world
|
||||
|
||||
def is_link(region):
|
||||
return region.is_light_world
|
||||
return region and region.is_light_world
|
||||
|
||||
def get_rule_to_add(region, location = None, connecting_entrance = None):
|
||||
# In OWG, a location can potentially be superbunny-mirror accessible or
|
||||
@@ -1603,21 +1608,20 @@ def set_bunny_rules(world: MultiWorld, player: int, inverted: bool):
|
||||
return options_to_access_rule(possible_options)
|
||||
|
||||
# Add requirements for bunny-impassible caves if link is a bunny in them
|
||||
for region in [world.get_region(name, player) for name in bunny_impassable_caves]:
|
||||
|
||||
for region in (world.get_region(name, player) for name in bunny_impassable_caves):
|
||||
if not is_bunny(region):
|
||||
continue
|
||||
rule = get_rule_to_add(region)
|
||||
for exit in region.exits:
|
||||
add_rule(exit, rule)
|
||||
for region_exit in region.exits:
|
||||
add_rule(region_exit, rule)
|
||||
|
||||
paradox_shop = world.get_region('Light World Death Mountain Shop', player)
|
||||
if is_bunny(paradox_shop):
|
||||
add_rule(paradox_shop.entrances[0], get_rule_to_add(paradox_shop))
|
||||
|
||||
# Add requirements for all locations that are actually in the dark world, except those available to the bunny, including dungeon revival
|
||||
for entrance in world.get_entrances():
|
||||
if entrance.player == player and is_bunny(entrance.connected_region):
|
||||
for entrance in world.get_entrances(player):
|
||||
if is_bunny(entrance.connected_region):
|
||||
if world.logic[player] in ['minorglitches', 'owglitches', 'hybridglitches', 'nologic'] :
|
||||
if entrance.connected_region.type == LTTPRegionType.Dungeon:
|
||||
if entrance.parent_region.type != LTTPRegionType.Dungeon and entrance.connected_region.name in OverworldGlitchRules.get_invalid_bunny_revival_dungeons():
|
||||
|
||||
@@ -348,7 +348,6 @@ def create_shops(world, player: int):
|
||||
loc.item = ItemFactory(GetBeemizerItem(world, player, 'Nothing'), player)
|
||||
loc.shop_slot_disabled = True
|
||||
shop.region.locations.append(loc)
|
||||
world.clear_location_cache()
|
||||
|
||||
|
||||
class ShopData(NamedTuple):
|
||||
@@ -619,6 +618,4 @@ def create_dynamic_shop_locations(world, player):
|
||||
if shop.type == ShopType.TakeAny:
|
||||
loc.shop_slot_disabled = True
|
||||
shop.region.locations.append(loc)
|
||||
world.clear_location_cache()
|
||||
|
||||
loc.shop_slot = i
|
||||
|
||||
@@ -470,7 +470,8 @@ class ALTTPWorld(World):
|
||||
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)
|
||||
fill_restrictive(world, all_state, prize_locs, prizepool, True, lock=True,
|
||||
name="LttP Dungeon Prizes")
|
||||
except FillError as e:
|
||||
lttp_logger.exception("Failed to place dungeon prizes (%s). Will retry %s more times", e,
|
||||
attempts - attempt)
|
||||
@@ -585,27 +586,26 @@ class ALTTPWorld(World):
|
||||
|
||||
for player in checks_in_area:
|
||||
checks_in_area[player]["Total"] = 0
|
||||
|
||||
for location in multiworld.get_locations():
|
||||
if location.game == cls.game and type(location.address) is int:
|
||||
main_entrance = location.parent_region.get_connecting_entrance(is_main_entrance)
|
||||
if location.parent_region.dungeon:
|
||||
dungeonname = {'Inverted Agahnims Tower': 'Agahnims Tower',
|
||||
'Inverted Ganons Tower': 'Ganons Tower'} \
|
||||
.get(location.parent_region.dungeon.name, location.parent_region.dungeon.name)
|
||||
checks_in_area[location.player][dungeonname].append(location.address)
|
||||
elif location.parent_region.type == LTTPRegionType.LightWorld:
|
||||
checks_in_area[location.player]["Light World"].append(location.address)
|
||||
elif location.parent_region.type == LTTPRegionType.DarkWorld:
|
||||
checks_in_area[location.player]["Dark World"].append(location.address)
|
||||
elif main_entrance.parent_region.type == LTTPRegionType.LightWorld:
|
||||
checks_in_area[location.player]["Light World"].append(location.address)
|
||||
elif main_entrance.parent_region.type == LTTPRegionType.DarkWorld:
|
||||
checks_in_area[location.player]["Dark World"].append(location.address)
|
||||
else:
|
||||
assert False, "Unknown Location area."
|
||||
# TODO: remove Total as it's duplicated data and breaks consistent typing
|
||||
checks_in_area[location.player]["Total"] += 1
|
||||
for location in multiworld.get_locations(player):
|
||||
if location.game == cls.game and type(location.address) is int:
|
||||
main_entrance = location.parent_region.get_connecting_entrance(is_main_entrance)
|
||||
if location.parent_region.dungeon:
|
||||
dungeonname = {'Inverted Agahnims Tower': 'Agahnims Tower',
|
||||
'Inverted Ganons Tower': 'Ganons Tower'} \
|
||||
.get(location.parent_region.dungeon.name, location.parent_region.dungeon.name)
|
||||
checks_in_area[location.player][dungeonname].append(location.address)
|
||||
elif location.parent_region.type == LTTPRegionType.LightWorld:
|
||||
checks_in_area[location.player]["Light World"].append(location.address)
|
||||
elif location.parent_region.type == LTTPRegionType.DarkWorld:
|
||||
checks_in_area[location.player]["Dark World"].append(location.address)
|
||||
elif main_entrance.parent_region.type == LTTPRegionType.LightWorld:
|
||||
checks_in_area[location.player]["Light World"].append(location.address)
|
||||
elif main_entrance.parent_region.type == LTTPRegionType.DarkWorld:
|
||||
checks_in_area[location.player]["Dark World"].append(location.address)
|
||||
else:
|
||||
assert False, "Unknown Location area."
|
||||
# TODO: remove Total as it's duplicated data and breaks consistent typing
|
||||
checks_in_area[location.player]["Total"] += 1
|
||||
|
||||
multidata["checks_in_area"].update(checks_in_area)
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from BaseClasses import CollectionState, ItemClassification
|
||||
from worlds.alttp.Dungeons import create_dungeons, get_dungeon_item_pool
|
||||
from worlds.alttp.Dungeons import get_dungeon_item_pool
|
||||
from worlds.alttp.EntranceShuffle import mandatory_connections, connect_simple
|
||||
from worlds.alttp.ItemPool import difficulties
|
||||
from worlds.alttp.Items import ItemFactory
|
||||
|
||||
@@ -67,6 +67,7 @@ class StartingLocation(ChoiceIsRandom):
|
||||
|
||||
class Ending(Choice):
|
||||
"""Choose which ending is required to complete the game.
|
||||
Talking to Tirso in Albero will tell you the selected ending for the current game.
|
||||
Ending A: Collect all thorn upgrades.
|
||||
Ending C: Collect all thorn upgrades and the Holy Wound of Abnegation."""
|
||||
display_name = "Ending"
|
||||
|
||||
@@ -578,11 +578,12 @@ def rules(blasphemousworld):
|
||||
or state.has("Purified Hand of the Nun", player)
|
||||
or state.has("D01Z02S03[NW]", player)
|
||||
and (
|
||||
can_cross_gap(state, logic, player, 1)
|
||||
can_cross_gap(state, logic, player, 2)
|
||||
or state.has("Lorquiana", player)
|
||||
or aubade(state, player)
|
||||
or state.has("Cantina of the Blue Rose", player)
|
||||
or charge_beam(state, player)
|
||||
or state.has("Ranged Skill", player)
|
||||
)
|
||||
))
|
||||
set_rule(world.get_location("Albero: Lvdovico's 1st reward", player),
|
||||
@@ -702,10 +703,11 @@ def rules(blasphemousworld):
|
||||
# Items
|
||||
set_rule(world.get_location("WotBC: Cliffside Child of Moonlight", player),
|
||||
lambda state: (
|
||||
can_cross_gap(state, logic, player, 1)
|
||||
can_cross_gap(state, logic, player, 2)
|
||||
or aubade(state, player)
|
||||
or charge_beam(state, player)
|
||||
or state.has_any({"Lorquiana", "Cante Jondo of the Three Sisters", "Cantina of the Blue Rose", "Cloistered Ruby"}, player)
|
||||
or state.has_any({"Lorquiana", "Cante Jondo of the Three Sisters", "Cantina of the Blue Rose", \
|
||||
"Cloistered Ruby", "Ranged Skill"}, player)
|
||||
or precise_skips_allowed(logic)
|
||||
))
|
||||
# Doors
|
||||
|
||||
@@ -19,6 +19,7 @@ In addition, there are other changes to the game that make it better optimized f
|
||||
- The Apodictic Heart of Mea Culpa can be unequipped.
|
||||
- Dying with the Immaculate Bead is unnecessary, it is automatically upgraded to the Weight of True Guilt.
|
||||
- If the option is enabled, the 34 corpses in game will have their messages changed to give hints about certain items and locations. The Shroud of Dreamt Sins is not required to hear them.
|
||||
- Talking to Tirso in Albero will tell you the selected ending for the current game.
|
||||
|
||||
## What has been changed about the side quests?
|
||||
|
||||
|
||||
@@ -69,8 +69,8 @@ class ChecksFinderWorld(World):
|
||||
def create_regions(self):
|
||||
menu = Region("Menu", self.player, self.multiworld)
|
||||
board = Region("Board", self.player, self.multiworld)
|
||||
board.locations = [ChecksFinderAdvancement(self.player, loc_name, loc_data.id, board)
|
||||
for loc_name, loc_data in advancement_table.items() if loc_data.region == board.name]
|
||||
board.locations += [ChecksFinderAdvancement(self.player, loc_name, loc_data.id, board)
|
||||
for loc_name, loc_data in advancement_table.items() if loc_data.region == board.name]
|
||||
|
||||
connection = Entrance(self.player, "New Board", menu)
|
||||
menu.exits.append(connection)
|
||||
|
||||
@@ -19,18 +19,43 @@ lookup_type_to_names: Dict[str, Set[str]] = {}
|
||||
for item, item_data in item_table.items():
|
||||
lookup_type_to_names.setdefault(item_data.type, set()).add(item)
|
||||
|
||||
item_name_groups = {group: lookup_type_to_names[group] for group in ("Skill", "Charm", "Mask", "Vessel",
|
||||
"Relic", "Root", "Map", "Stag", "Cocoon",
|
||||
"Soul", "DreamWarrior", "DreamBoss")}
|
||||
|
||||
directionals = ('', 'Left_', 'Right_')
|
||||
|
||||
item_name_groups.update({
|
||||
item_name_groups = ({
|
||||
"BossEssence": lookup_type_to_names["DreamWarrior"] | lookup_type_to_names["DreamBoss"],
|
||||
"BossGeo": lookup_type_to_names["Boss_Geo"],
|
||||
"CDash": {x + "Crystal_Heart" for x in directionals},
|
||||
"Charms": lookup_type_to_names["Charm"],
|
||||
"CharmNotches": lookup_type_to_names["Notch"],
|
||||
"Claw": {x + "Mantis_Claw" for x in directionals},
|
||||
"Cloak": {x + "Mothwing_Cloak" for x in directionals} | {"Shade_Cloak", "Split_Shade_Cloak"},
|
||||
"Dive": {"Desolate_Dive", "Descending_Dark"},
|
||||
"LifebloodCocoons": lookup_type_to_names["Cocoon"],
|
||||
"Dreamers": {"Herrah", "Monomon", "Lurien"},
|
||||
"Cloak": {x + 'Mothwing_Cloak' for x in directionals} | {'Shade_Cloak', 'Split_Shade_Cloak'},
|
||||
"Claw": {x + 'Mantis_Claw' for x in directionals},
|
||||
"CDash": {x + 'Crystal_Heart' for x in directionals},
|
||||
"Fragments": {"Queen_Fragment", "King_Fragment", "Void_Heart"},
|
||||
"Fireball": {"Vengeful_Spirit", "Shade_Soul"},
|
||||
"GeoChests": lookup_type_to_names["Geo"],
|
||||
"GeoRocks": lookup_type_to_names["Rock"],
|
||||
"GrimmkinFlames": lookup_type_to_names["Flame"],
|
||||
"Grubs": lookup_type_to_names["Grub"],
|
||||
"JournalEntries": lookup_type_to_names["Journal"],
|
||||
"JunkPitChests": lookup_type_to_names["JunkPitChest"],
|
||||
"Keys": lookup_type_to_names["Key"],
|
||||
"LoreTablets": lookup_type_to_names["Lore"] | lookup_type_to_names["PalaceLore"],
|
||||
"Maps": lookup_type_to_names["Map"],
|
||||
"MaskShards": lookup_type_to_names["Mask"],
|
||||
"Mimics": lookup_type_to_names["Mimic"],
|
||||
"Nail": lookup_type_to_names["CursedNail"],
|
||||
"PalaceJournal": {"Journal_Entry-Seal_of_Binding"},
|
||||
"PalaceLore": lookup_type_to_names["PalaceLore"],
|
||||
"PalaceTotem": {"Soul_Totem-Palace", "Soul_Totem-Path_of_Pain"},
|
||||
"RancidEggs": lookup_type_to_names["Egg"],
|
||||
"Relics": lookup_type_to_names["Relic"],
|
||||
"Scream": {"Howling_Wraiths", "Abyss_Shriek"},
|
||||
"Skills": lookup_type_to_names["Skill"],
|
||||
"SoulTotems": lookup_type_to_names["Soul"],
|
||||
"Stags": lookup_type_to_names["Stag"],
|
||||
"VesselFragments": lookup_type_to_names["Vessel"],
|
||||
"WhisperingRoots": lookup_type_to_names["Root"],
|
||||
"WhiteFragments": {"Queen_Fragment", "King_Fragment", "Void_Heart"},
|
||||
})
|
||||
item_name_groups['Horizontal'] = item_name_groups['Cloak'] | item_name_groups['CDash']
|
||||
item_name_groups['Vertical'] = item_name_groups['Claw'] | {'Monarch_Wings'}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from ..generic.Rules import set_rule, add_rule
|
||||
from BaseClasses import MultiWorld
|
||||
from ..AutoWorld import World
|
||||
from .GeneratedRules import set_generated_rules
|
||||
from typing import NamedTuple
|
||||
@@ -39,14 +38,12 @@ def hk_set_rule(hk_world: World, location: str, rule):
|
||||
|
||||
def set_rules(hk_world: World):
|
||||
player = hk_world.player
|
||||
world = hk_world.multiworld
|
||||
set_generated_rules(hk_world, hk_set_rule)
|
||||
|
||||
# Shop costs
|
||||
for region in world.get_regions(player):
|
||||
for location in region.locations:
|
||||
if location.costs:
|
||||
for term, amount in location.costs.items():
|
||||
if term == "GEO": # No geo logic!
|
||||
continue
|
||||
add_rule(location, lambda state, term=term, amount=amount: state.count(term, player) >= amount)
|
||||
for location in hk_world.multiworld.get_locations(player):
|
||||
if location.costs:
|
||||
for term, amount in location.costs.items():
|
||||
if term == "GEO": # No geo logic!
|
||||
continue
|
||||
add_rule(location, lambda state, term=term, amount=amount: state.count(term, player) >= amount)
|
||||
|
||||
@@ -219,7 +219,7 @@ def create_regions_from_ladxr(player, multiworld, logic):
|
||||
|
||||
r = LinksAwakeningRegion(
|
||||
name=name, ladxr_region=l, hint="", player=player, world=multiworld)
|
||||
r.locations = [LinksAwakeningLocation(player, r, i) for i in l.items]
|
||||
r.locations += [LinksAwakeningLocation(player, r, i) for i in l.items]
|
||||
regions[l] = r
|
||||
|
||||
for ladxr_location in logic.location_list:
|
||||
|
||||
@@ -231,9 +231,7 @@ class LinksAwakeningWorld(World):
|
||||
# Find instrument, lock
|
||||
# TODO: we should be able to pinpoint the region we want, save a lookup table please
|
||||
found = False
|
||||
for r in self.multiworld.get_regions():
|
||||
if r.player != self.player:
|
||||
continue
|
||||
for r in self.multiworld.get_regions(self.player):
|
||||
if r.dungeon_index != item.item_data.dungeon_index:
|
||||
continue
|
||||
for loc in r.locations:
|
||||
@@ -269,10 +267,7 @@ class LinksAwakeningWorld(World):
|
||||
event_location.place_locked_item(self.create_event("Can Play Trendy Game"))
|
||||
|
||||
self.dungeon_locations_by_dungeon = [[], [], [], [], [], [], [], [], []]
|
||||
for r in self.multiworld.get_regions():
|
||||
if r.player != self.player:
|
||||
continue
|
||||
|
||||
for r in self.multiworld.get_regions(self.player):
|
||||
# Set aside dungeon locations
|
||||
if r.dungeon_index:
|
||||
self.dungeon_locations_by_dungeon[r.dungeon_index - 1] += r.locations
|
||||
|
||||
@@ -3,7 +3,7 @@ import os
|
||||
from typing import Optional
|
||||
|
||||
import Utils
|
||||
from Utils import OptionsType
|
||||
from settings import get_settings
|
||||
from worlds.Files import APDeltaPatch
|
||||
|
||||
L2USHASH: str = "6efc477d6203ed2b3b9133c1cd9e9c5d"
|
||||
@@ -35,9 +35,8 @@ def get_base_rom_bytes(file_name: str = "") -> bytes:
|
||||
|
||||
|
||||
def get_base_rom_path(file_name: str = "") -> str:
|
||||
options: OptionsType = Utils.get_options()
|
||||
if not file_name:
|
||||
file_name = options["lufia2ac_options"]["rom_file"]
|
||||
file_name = get_settings()["lufia2ac_options"]["rom_file"]
|
||||
if not os.path.exists(file_name):
|
||||
file_name = Utils.user_path(file_name)
|
||||
return file_name
|
||||
|
||||
@@ -54,12 +54,12 @@ def create_regions(world: MultiWorld, player: int):
|
||||
world.regions.append(boss_region)
|
||||
|
||||
region_final_boss = Region("Final Boss", player, world)
|
||||
region_final_boss.locations = [MeritousLocation(
|
||||
region_final_boss.locations += [MeritousLocation(
|
||||
player, "Wervyn Anixil", None, region_final_boss)]
|
||||
world.regions.append(region_final_boss)
|
||||
|
||||
region_tfb = Region("True Final Boss", player, world)
|
||||
region_tfb.locations = [MeritousLocation(
|
||||
region_tfb.locations += [MeritousLocation(
|
||||
player, "Wervyn Anixil?", None, region_tfb)]
|
||||
world.regions.append(region_tfb)
|
||||
|
||||
|
||||
@@ -173,7 +173,7 @@ class MinecraftWorld(World):
|
||||
|
||||
def generate_output(self, output_directory: str) -> None:
|
||||
data = self._get_mc_data()
|
||||
filename = f"AP_{self.multiworld.get_out_file_name_base(self.player)}.apmc"
|
||||
filename = f"{self.multiworld.get_out_file_name_base(self.player)}.apmc"
|
||||
with open(os.path.join(output_directory, filename), 'wb') as f:
|
||||
f.write(b64encode(bytes(json.dumps(data), 'utf-8')))
|
||||
|
||||
|
||||
@@ -404,7 +404,7 @@ trippers feeling!|8-4|Give Up TREATMENT Vol.3|True|5|7|9|11
|
||||
Lilith ambivalence lovers|8-5|Give Up TREATMENT Vol.3|False|5|8|10|
|
||||
Brave My Soul|7-0|Give Up TREATMENT Vol.2|False|4|6|8|
|
||||
Halcyon|7-1|Give Up TREATMENT Vol.2|False|4|7|10|
|
||||
Crimson Nightingle|7-2|Give Up TREATMENT Vol.2|True|4|7|10|
|
||||
Crimson Nightingale|7-2|Give Up TREATMENT Vol.2|True|4|7|10|
|
||||
Invader|7-3|Give Up TREATMENT Vol.2|True|3|7|11|
|
||||
Lyrith|7-4|Give Up TREATMENT Vol.2|False|5|7|10|
|
||||
GOODBOUNCE|7-5|Give Up TREATMENT Vol.2|False|4|6|9|
|
||||
@@ -488,4 +488,11 @@ Hatsune Creation Myth|66-5|Miku in Museland|False|6|8|10|
|
||||
The Vampire|66-6|Miku in Museland|False|4|6|9|
|
||||
Future Eve|66-7|Miku in Museland|False|4|8|11|
|
||||
Unknown Mother Goose|66-8|Miku in Museland|False|4|8|10|
|
||||
Shun-ran|66-9|Miku in Museland|False|4|7|9|
|
||||
Shun-ran|66-9|Miku in Museland|False|4|7|9|
|
||||
NICE TYPE feat. monii|43-41|MD Plus Project|True|3|6|8|
|
||||
Rainy Angel|67-0|Happy Otaku Pack Vol.18|True|4|6|9|11
|
||||
Gullinkambi|67-1|Happy Otaku Pack Vol.18|True|4|7|10|
|
||||
RakiRaki Rebuilders!!!|67-2|Happy Otaku Pack Vol.18|True|5|7|10|
|
||||
Laniakea|67-3|Happy Otaku Pack Vol.18|False|5|8|10|
|
||||
OTTAMA GAZER|67-4|Happy Otaku Pack Vol.18|True|5|8|10|
|
||||
Sleep Tight feat.Macoto|67-5|Happy Otaku Pack Vol.18|True|3|5|8|
|
||||
@@ -49,7 +49,7 @@ class MuseDashWorld(World):
|
||||
game = "Muse Dash"
|
||||
options_dataclass: ClassVar[Type[PerGameCommonOptions]] = MuseDashOptions
|
||||
topology_present = False
|
||||
data_version = 10
|
||||
data_version = 11
|
||||
web = MuseDashWebWorld()
|
||||
|
||||
# Necessary Data
|
||||
|
||||
@@ -44,20 +44,18 @@ def create_kantele(victory_condition: VictoryCondition) -> List[str]:
|
||||
return ["Kantele"] if victory_condition.value >= VictoryCondition.option_pure_ending else []
|
||||
|
||||
|
||||
def create_random_items(multiworld: MultiWorld, player: int, random_count: int) -> List[str]:
|
||||
filler_pool = filler_weights.copy()
|
||||
def create_random_items(multiworld: MultiWorld, player: int, weights: Dict[str, int], count: int) -> List[str]:
|
||||
filler_pool = weights.copy()
|
||||
if multiworld.bad_effects[player].value == 0:
|
||||
del filler_pool["Trap"]
|
||||
|
||||
return multiworld.random.choices(
|
||||
population=list(filler_pool.keys()),
|
||||
weights=list(filler_pool.values()),
|
||||
k=random_count
|
||||
)
|
||||
return multiworld.random.choices(population=list(filler_pool.keys()),
|
||||
weights=list(filler_pool.values()),
|
||||
k=count)
|
||||
|
||||
|
||||
def create_all_items(multiworld: MultiWorld, player: int) -> None:
|
||||
sum_locations = len(multiworld.get_unfilled_locations(player))
|
||||
locations_to_fill = len(multiworld.get_unfilled_locations(player))
|
||||
|
||||
itempool = (
|
||||
create_fixed_item_pool()
|
||||
@@ -66,9 +64,18 @@ def create_all_items(multiworld: MultiWorld, player: int) -> None:
|
||||
+ create_kantele(multiworld.victory_condition[player])
|
||||
)
|
||||
|
||||
random_count = sum_locations - len(itempool)
|
||||
itempool += create_random_items(multiworld, player, random_count)
|
||||
# if there's not enough shop-allowed items in the pool, we can encounter gen issues
|
||||
# 39 is the number of shop-valid items we need to guarantee
|
||||
if len(itempool) < 39:
|
||||
itempool += create_random_items(multiworld, player, shop_only_filler_weights, 39 - len(itempool))
|
||||
# this is so that it passes tests and gens if you have minimal locations and only one player
|
||||
if multiworld.players == 1:
|
||||
for location in multiworld.get_unfilled_locations(player):
|
||||
if "Shop Item" in location.name:
|
||||
location.item = create_item(player, itempool.pop())
|
||||
locations_to_fill = len(multiworld.get_unfilled_locations(player))
|
||||
|
||||
itempool += create_random_items(multiworld, player, filler_weights, locations_to_fill - len(itempool))
|
||||
multiworld.itempool += [create_item(player, name) for name in itempool]
|
||||
|
||||
|
||||
@@ -84,8 +91,8 @@ item_table: Dict[str, ItemData] = {
|
||||
"Wand (Tier 2)": ItemData(110007, "Wands", ItemClassification.useful),
|
||||
"Wand (Tier 3)": ItemData(110008, "Wands", ItemClassification.useful),
|
||||
"Wand (Tier 4)": ItemData(110009, "Wands", ItemClassification.useful),
|
||||
"Wand (Tier 5)": ItemData(110010, "Wands", ItemClassification.useful),
|
||||
"Wand (Tier 6)": ItemData(110011, "Wands", ItemClassification.useful),
|
||||
"Wand (Tier 5)": ItemData(110010, "Wands", ItemClassification.useful, 1),
|
||||
"Wand (Tier 6)": ItemData(110011, "Wands", ItemClassification.useful, 1),
|
||||
"Kantele": ItemData(110012, "Wands", ItemClassification.useful),
|
||||
"Fire Immunity Perk": ItemData(110013, "Perks", ItemClassification.progression, 1),
|
||||
"Toxic Immunity Perk": ItemData(110014, "Perks", ItemClassification.progression, 1),
|
||||
@@ -95,43 +102,46 @@ item_table: Dict[str, ItemData] = {
|
||||
"Tinker with Wands Everywhere Perk": ItemData(110018, "Perks", ItemClassification.progression, 1),
|
||||
"All-Seeing Eye Perk": ItemData(110019, "Perks", ItemClassification.progression, 1),
|
||||
"Spatial Awareness Perk": ItemData(110020, "Perks", ItemClassification.progression),
|
||||
"Extra Life Perk": ItemData(110021, "Repeatable Perks", ItemClassification.useful),
|
||||
"Extra Life Perk": ItemData(110021, "Repeatable Perks", ItemClassification.useful, 1),
|
||||
"Orb": ItemData(110022, "Orbs", ItemClassification.progression_skip_balancing),
|
||||
"Random Potion": ItemData(110023, "Items", ItemClassification.filler),
|
||||
"Secret Potion": ItemData(110024, "Items", ItemClassification.filler),
|
||||
"Powder Pouch": ItemData(110025, "Items", ItemClassification.filler),
|
||||
"Chaos Die": ItemData(110026, "Items", ItemClassification.filler),
|
||||
"Greed Die": ItemData(110027, "Items", ItemClassification.filler),
|
||||
"Kammi": ItemData(110028, "Items", ItemClassification.filler),
|
||||
"Refreshing Gourd": ItemData(110029, "Items", ItemClassification.filler),
|
||||
"Kammi": ItemData(110028, "Items", ItemClassification.filler, 1),
|
||||
"Refreshing Gourd": ItemData(110029, "Items", ItemClassification.filler, 1),
|
||||
"Sädekivi": ItemData(110030, "Items", ItemClassification.filler),
|
||||
"Broken Wand": ItemData(110031, "Items", ItemClassification.filler),
|
||||
}
|
||||
|
||||
shop_only_filler_weights: Dict[str, int] = {
|
||||
"Trap": 15,
|
||||
"Extra Max HP": 25,
|
||||
"Spell Refresher": 20,
|
||||
"Wand (Tier 1)": 10,
|
||||
"Wand (Tier 2)": 8,
|
||||
"Wand (Tier 3)": 7,
|
||||
"Wand (Tier 4)": 6,
|
||||
"Wand (Tier 5)": 5,
|
||||
"Wand (Tier 6)": 4,
|
||||
"Extra Life Perk": 10,
|
||||
}
|
||||
|
||||
filler_weights: Dict[str, int] = {
|
||||
"Trap": 15,
|
||||
"Extra Max HP": 25,
|
||||
"Spell Refresher": 20,
|
||||
"Potion": 40,
|
||||
"Gold (200)": 15,
|
||||
"Gold (1000)": 6,
|
||||
"Wand (Tier 1)": 10,
|
||||
"Wand (Tier 2)": 8,
|
||||
"Wand (Tier 3)": 7,
|
||||
"Wand (Tier 4)": 6,
|
||||
"Wand (Tier 5)": 5,
|
||||
"Wand (Tier 6)": 4,
|
||||
"Extra Life Perk": 10,
|
||||
"Random Potion": 9,
|
||||
"Secret Potion": 10,
|
||||
"Powder Pouch": 10,
|
||||
"Chaos Die": 4,
|
||||
"Greed Die": 4,
|
||||
"Kammi": 4,
|
||||
"Refreshing Gourd": 4,
|
||||
"Sädekivi": 3,
|
||||
"Broken Wand": 10,
|
||||
**shop_only_filler_weights,
|
||||
"Gold (200)": 15,
|
||||
"Gold (1000)": 6,
|
||||
"Potion": 40,
|
||||
"Random Potion": 9,
|
||||
"Secret Potion": 10,
|
||||
"Powder Pouch": 10,
|
||||
"Chaos Die": 4,
|
||||
"Greed Die": 4,
|
||||
"Kammi": 4,
|
||||
"Refreshing Gourd": 4,
|
||||
"Sädekivi": 3,
|
||||
"Broken Wand": 10,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Regions are areas in your game that you travel to.
|
||||
from typing import Dict, Set
|
||||
from typing import Dict, Set, List
|
||||
|
||||
from BaseClasses import Entrance, MultiWorld, Region
|
||||
from . import Locations
|
||||
@@ -79,70 +79,46 @@ def create_all_regions_and_connections(multiworld: MultiWorld, player: int) -> N
|
||||
# - Lake is connected to The Laboratory, since the boss is hard without specific set-ups (which means late game)
|
||||
# - Snowy Depths connects to Lava Lake orb since you need digging for it, so fairly early is acceptable
|
||||
# - Ancient Laboratory is connected to the Coal Pits, so that Ylialkemisti isn't sphere 1
|
||||
noita_connections: Dict[str, Set[str]] = {
|
||||
"Menu": {"Forest"},
|
||||
"Forest": {"Mines", "Floating Island", "Desert", "Snowy Wasteland"},
|
||||
"Snowy Wasteland": {"Forest"},
|
||||
"Frozen Vault": {"The Vault"},
|
||||
"Lake": {"The Laboratory"},
|
||||
"Desert": {"Forest"},
|
||||
"Floating Island": {"Forest"},
|
||||
"Pyramid": {"Hiisi Base"},
|
||||
"Overgrown Cavern": {"Sandcave", "Undeground Jungle"},
|
||||
"Sandcave": {"Overgrown Cavern"},
|
||||
noita_connections: Dict[str, List[str]] = {
|
||||
"Menu": ["Forest"],
|
||||
"Forest": ["Mines", "Floating Island", "Desert", "Snowy Wasteland"],
|
||||
"Frozen Vault": ["The Vault"],
|
||||
"Overgrown Cavern": ["Sandcave"],
|
||||
|
||||
###
|
||||
"Mines": {"Collapsed Mines", "Coal Pits Holy Mountain", "Lava Lake", "Forest"},
|
||||
"Collapsed Mines": {"Mines", "Dark Cave"},
|
||||
"Lava Lake": {"Mines", "Abyss Orb Room"},
|
||||
"Abyss Orb Room": {"Lava Lake"},
|
||||
"Below Lava Lake": {"Snowy Depths"},
|
||||
"Dark Cave": {"Collapsed Mines"},
|
||||
"Ancient Laboratory": {"Coal Pits"},
|
||||
"Mines": ["Collapsed Mines", "Coal Pits Holy Mountain", "Lava Lake"],
|
||||
"Lava Lake": ["Abyss Orb Room"],
|
||||
|
||||
###
|
||||
"Coal Pits Holy Mountain": {"Coal Pits"},
|
||||
"Coal Pits": {"Coal Pits Holy Mountain", "Fungal Caverns", "Snowy Depths Holy Mountain", "Ancient Laboratory"},
|
||||
"Fungal Caverns": {"Coal Pits"},
|
||||
"Coal Pits Holy Mountain": ["Coal Pits"],
|
||||
"Coal Pits": ["Fungal Caverns", "Snowy Depths Holy Mountain", "Ancient Laboratory"],
|
||||
|
||||
###
|
||||
"Snowy Depths Holy Mountain": {"Snowy Depths"},
|
||||
"Snowy Depths": {"Snowy Depths Holy Mountain", "Hiisi Base Holy Mountain", "Magical Temple", "Below Lava Lake"},
|
||||
"Magical Temple": {"Snowy Depths"},
|
||||
"Snowy Depths Holy Mountain": ["Snowy Depths"],
|
||||
"Snowy Depths": ["Hiisi Base Holy Mountain", "Magical Temple", "Below Lava Lake"],
|
||||
|
||||
###
|
||||
"Hiisi Base Holy Mountain": {"Hiisi Base"},
|
||||
"Hiisi Base": {"Hiisi Base Holy Mountain", "Secret Shop", "Pyramid", "Underground Jungle Holy Mountain"},
|
||||
"Secret Shop": {"Hiisi Base"},
|
||||
"Hiisi Base Holy Mountain": ["Hiisi Base"],
|
||||
"Hiisi Base": ["Secret Shop", "Pyramid", "Underground Jungle Holy Mountain"],
|
||||
|
||||
###
|
||||
"Underground Jungle Holy Mountain": {"Underground Jungle"},
|
||||
"Underground Jungle": {"Underground Jungle Holy Mountain", "Dragoncave", "Overgrown Cavern", "Vault Holy Mountain",
|
||||
"Lukki Lair"},
|
||||
"Dragoncave": {"Underground Jungle"},
|
||||
"Lukki Lair": {"Underground Jungle", "Snow Chasm", "Frozen Vault"},
|
||||
"Snow Chasm": {},
|
||||
"Underground Jungle Holy Mountain": ["Underground Jungle"],
|
||||
"Underground Jungle": ["Dragoncave", "Overgrown Cavern", "Vault Holy Mountain", "Lukki Lair", "Snow Chasm"],
|
||||
|
||||
###
|
||||
"Vault Holy Mountain": {"The Vault"},
|
||||
"The Vault": {"Vault Holy Mountain", "Frozen Vault", "Temple of the Art Holy Mountain"},
|
||||
"Vault Holy Mountain": ["The Vault"],
|
||||
"The Vault": ["Frozen Vault", "Temple of the Art Holy Mountain"],
|
||||
|
||||
###
|
||||
"Temple of the Art Holy Mountain": {"Temple of the Art"},
|
||||
"Temple of the Art": {"Temple of the Art Holy Mountain", "Laboratory Holy Mountain", "The Tower",
|
||||
"Wizards' Den"},
|
||||
"Wizards' Den": {"Temple of the Art", "Powerplant"},
|
||||
"Powerplant": {"Wizards' Den", "Deep Underground"},
|
||||
"The Tower": {"Forest"},
|
||||
"Deep Underground": {},
|
||||
"Temple of the Art Holy Mountain": ["Temple of the Art"],
|
||||
"Temple of the Art": ["Laboratory Holy Mountain", "The Tower", "Wizards' Den"],
|
||||
"Wizards' Den": ["Powerplant"],
|
||||
"Powerplant": ["Deep Underground"],
|
||||
|
||||
###
|
||||
"Laboratory Holy Mountain": {"The Laboratory"},
|
||||
"The Laboratory": {"Laboratory Holy Mountain", "The Work", "Friend Cave", "The Work (Hell)", "Lake"},
|
||||
"Friend Cave": {},
|
||||
"The Work": {},
|
||||
"The Work (Hell)": {},
|
||||
"Laboratory Holy Mountain": ["The Laboratory"],
|
||||
"The Laboratory": ["The Work", "Friend Cave", "The Work (Hell)", "Lake"],
|
||||
###
|
||||
}
|
||||
|
||||
noita_regions: Set[str] = set(noita_connections.keys()).union(*noita_connections.values())
|
||||
noita_regions: List[str] = sorted(set(noita_connections.keys()).union(*noita_connections.values()))
|
||||
|
||||
@@ -44,12 +44,10 @@ wand_tiers: List[str] = [
|
||||
"Wand (Tier 6)", # Temple of the Art
|
||||
]
|
||||
|
||||
|
||||
items_hidden_from_shops: List[str] = ["Gold (200)", "Gold (1000)", "Potion", "Random Potion", "Secret Potion",
|
||||
"Chaos Die", "Greed Die", "Kammi", "Refreshing Gourd", "Sädekivi", "Broken Wand",
|
||||
"Powder Pouch"]
|
||||
|
||||
|
||||
perk_list: List[str] = list(filter(Items.item_is_perk, Items.item_table.keys()))
|
||||
|
||||
|
||||
@@ -155,11 +153,12 @@ def victory_unlock_conditions(multiworld: MultiWorld, player: int) -> None:
|
||||
|
||||
|
||||
def create_all_rules(multiworld: MultiWorld, player: int) -> None:
|
||||
ban_items_from_shops(multiworld, player)
|
||||
ban_early_high_tier_wands(multiworld, player)
|
||||
lock_holy_mountains_into_spheres(multiworld, player)
|
||||
holy_mountain_unlock_conditions(multiworld, player)
|
||||
biome_unlock_conditions(multiworld, player)
|
||||
if multiworld.players > 1:
|
||||
ban_items_from_shops(multiworld, player)
|
||||
ban_early_high_tier_wands(multiworld, player)
|
||||
lock_holy_mountains_into_spheres(multiworld, player)
|
||||
holy_mountain_unlock_conditions(multiworld, player)
|
||||
biome_unlock_conditions(multiworld, player)
|
||||
victory_unlock_conditions(multiworld, player)
|
||||
|
||||
# Prevent the Map perk (used to find Toveri) from being on Toveri (boss)
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
|
||||
from BaseClasses import Entrance
|
||||
from .Regions import TimeOfDay
|
||||
|
||||
class OOTEntrance(Entrance):
|
||||
game: str = 'Ocarina of Time'
|
||||
@@ -29,16 +27,16 @@ class OOTEntrance(Entrance):
|
||||
self.connected_region = None
|
||||
return previously_connected
|
||||
|
||||
def get_new_target(self):
|
||||
def get_new_target(self, pool_type):
|
||||
root = self.multiworld.get_region('Root Exits', self.player)
|
||||
target_entrance = OOTEntrance(self.player, self.multiworld, 'Root -> ' + self.connected_region.name, root)
|
||||
target_entrance = OOTEntrance(self.player, self.multiworld, f'Root -> ({self.name}) ({pool_type})', root)
|
||||
target_entrance.connect(self.connected_region)
|
||||
target_entrance.replaces = self
|
||||
root.exits.append(target_entrance)
|
||||
return target_entrance
|
||||
|
||||
def assume_reachable(self):
|
||||
def assume_reachable(self, pool_type):
|
||||
if self.assumed == None:
|
||||
self.assumed = self.get_new_target()
|
||||
self.assumed = self.get_new_target(pool_type)
|
||||
self.disconnect()
|
||||
return self.assumed
|
||||
|
||||
@@ -2,6 +2,7 @@ from itertools import chain
|
||||
import logging
|
||||
|
||||
from worlds.generic.Rules import set_rule, add_rule
|
||||
from BaseClasses import CollectionState
|
||||
|
||||
from .Hints import get_hint_area, HintAreaNotFound
|
||||
from .Regions import TimeOfDay
|
||||
@@ -25,12 +26,12 @@ def set_all_entrances_data(world, player):
|
||||
return_entrance.data['index'] = 0x7FFF
|
||||
|
||||
|
||||
def assume_entrance_pool(entrance_pool, ootworld):
|
||||
def assume_entrance_pool(entrance_pool, ootworld, pool_type):
|
||||
assumed_pool = []
|
||||
for entrance in entrance_pool:
|
||||
assumed_forward = entrance.assume_reachable()
|
||||
assumed_forward = entrance.assume_reachable(pool_type)
|
||||
if entrance.reverse != None and not ootworld.decouple_entrances:
|
||||
assumed_return = entrance.reverse.assume_reachable()
|
||||
assumed_return = entrance.reverse.assume_reachable(pool_type)
|
||||
if not (ootworld.mix_entrance_pools != 'off' and (ootworld.shuffle_overworld_entrances or ootworld.shuffle_special_interior_entrances)):
|
||||
if (entrance.type in ('Dungeon', 'Grotto', 'Grave') and entrance.reverse.name != 'Spirit Temple Lobby -> Desert Colossus From Spirit Lobby') or \
|
||||
(entrance.type == 'Interior' and ootworld.shuffle_special_interior_entrances):
|
||||
@@ -41,15 +42,15 @@ def assume_entrance_pool(entrance_pool, ootworld):
|
||||
return assumed_pool
|
||||
|
||||
|
||||
def build_one_way_targets(world, types_to_include, exclude=(), target_region_names=()):
|
||||
def build_one_way_targets(world, pool, types_to_include, exclude=(), target_region_names=()):
|
||||
one_way_entrances = []
|
||||
for pool_type in types_to_include:
|
||||
one_way_entrances += world.get_shufflable_entrances(type=pool_type)
|
||||
valid_one_way_entrances = list(filter(lambda entrance: entrance.name not in exclude, one_way_entrances))
|
||||
if target_region_names:
|
||||
return [entrance.get_new_target() for entrance in valid_one_way_entrances
|
||||
return [entrance.get_new_target(pool) for entrance in valid_one_way_entrances
|
||||
if entrance.connected_region.name in target_region_names]
|
||||
return [entrance.get_new_target() for entrance in valid_one_way_entrances]
|
||||
return [entrance.get_new_target(pool) for entrance in valid_one_way_entrances]
|
||||
|
||||
|
||||
# Abbreviations
|
||||
@@ -423,14 +424,14 @@ multi_interior_regions = {
|
||||
}
|
||||
|
||||
interior_entrance_bias = {
|
||||
'Kakariko Village -> Kak Potion Shop Front': 4,
|
||||
'Kak Backyard -> Kak Potion Shop Back': 4,
|
||||
'Kakariko Village -> Kak Impas House': 3,
|
||||
'Kak Impas Ledge -> Kak Impas House Back': 3,
|
||||
'Goron City -> GC Shop': 2,
|
||||
'Zoras Domain -> ZD Shop': 2,
|
||||
'ToT Entrance -> Temple of Time': 4,
|
||||
'Kakariko Village -> Kak Potion Shop Front': 3,
|
||||
'Kak Backyard -> Kak Potion Shop Back': 3,
|
||||
'Kakariko Village -> Kak Impas House': 2,
|
||||
'Kak Impas Ledge -> Kak Impas House Back': 2,
|
||||
'Market Entrance -> Market Guard House': 2,
|
||||
'ToT Entrance -> Temple of Time': 1,
|
||||
'Goron City -> GC Shop': 1,
|
||||
'Zoras Domain -> ZD Shop': 1,
|
||||
}
|
||||
|
||||
|
||||
@@ -443,7 +444,8 @@ def shuffle_random_entrances(ootworld):
|
||||
player = ootworld.player
|
||||
|
||||
# Gather locations to keep reachable for validation
|
||||
all_state = world.get_all_state(use_cache=True)
|
||||
all_state = ootworld.get_state_with_complete_itempool()
|
||||
all_state.sweep_for_events(locations=ootworld.get_locations())
|
||||
locations_to_ensure_reachable = {loc for loc in world.get_reachable_locations(all_state, player) if not (loc.type == 'Drop' or (loc.type == 'Event' and 'Subrule' in loc.name))}
|
||||
|
||||
# Set entrance data for all entrances
|
||||
@@ -523,12 +525,12 @@ def shuffle_random_entrances(ootworld):
|
||||
for pool_type, entrance_pool in one_way_entrance_pools.items():
|
||||
if pool_type == 'OwlDrop':
|
||||
valid_target_types = ('WarpSong', 'OwlDrop', 'Overworld', 'Extra')
|
||||
one_way_target_entrance_pools[pool_type] = build_one_way_targets(ootworld, valid_target_types, exclude=['Prelude of Light Warp -> Temple of Time'])
|
||||
one_way_target_entrance_pools[pool_type] = build_one_way_targets(ootworld, pool_type, valid_target_types, exclude=['Prelude of Light Warp -> Temple of Time'])
|
||||
for target in one_way_target_entrance_pools[pool_type]:
|
||||
set_rule(target, lambda state: state._oot_reach_as_age(target.parent_region, 'child', player))
|
||||
elif pool_type in {'Spawn', 'WarpSong'}:
|
||||
valid_target_types = ('Spawn', 'WarpSong', 'OwlDrop', 'Overworld', 'Interior', 'SpecialInterior', 'Extra')
|
||||
one_way_target_entrance_pools[pool_type] = build_one_way_targets(ootworld, valid_target_types)
|
||||
one_way_target_entrance_pools[pool_type] = build_one_way_targets(ootworld, pool_type, valid_target_types)
|
||||
# Ensure that the last entrance doesn't assume the rest of the targets are reachable
|
||||
for target in one_way_target_entrance_pools[pool_type]:
|
||||
add_rule(target, (lambda entrances=entrance_pool: (lambda state: any(entrance.connected_region == None for entrance in entrances)))())
|
||||
@@ -538,14 +540,11 @@ def shuffle_random_entrances(ootworld):
|
||||
|
||||
target_entrance_pools = {}
|
||||
for pool_type, entrance_pool in entrance_pools.items():
|
||||
target_entrance_pools[pool_type] = assume_entrance_pool(entrance_pool, ootworld)
|
||||
target_entrance_pools[pool_type] = assume_entrance_pool(entrance_pool, ootworld, pool_type)
|
||||
|
||||
# Build all_state and none_state
|
||||
all_state = ootworld.get_state_with_complete_itempool()
|
||||
none_state = all_state.copy()
|
||||
for item_tuple in none_state.prog_items:
|
||||
if item_tuple[1] == player:
|
||||
none_state.prog_items[item_tuple] = 0
|
||||
none_state = CollectionState(ootworld.multiworld)
|
||||
|
||||
# Plando entrances
|
||||
if world.plando_connections[player]:
|
||||
@@ -628,7 +627,7 @@ def shuffle_random_entrances(ootworld):
|
||||
logging.getLogger('').error(f'Root Exit: {exit} -> {exit.connected_region}')
|
||||
logging.getLogger('').error(f'Root has too many entrances left after shuffling entrances')
|
||||
# Game is beatable
|
||||
new_all_state = world.get_all_state(use_cache=False)
|
||||
new_all_state = ootworld.get_state_with_complete_itempool()
|
||||
if not world.has_beaten_game(new_all_state, player):
|
||||
raise EntranceShuffleError('Cannot beat game')
|
||||
# Validate world
|
||||
@@ -700,7 +699,7 @@ def place_one_way_priority_entrance(ootworld, priority_name, allowed_regions, al
|
||||
raise EntranceShuffleError(f'Unable to place priority one-way entrance for {priority_name} in world {ootworld.player}')
|
||||
|
||||
|
||||
def shuffle_entrance_pool(ootworld, pool_type, entrance_pool, target_entrances, locations_to_ensure_reachable, all_state, none_state, check_all=False, retry_count=20):
|
||||
def shuffle_entrance_pool(ootworld, pool_type, entrance_pool, target_entrances, locations_to_ensure_reachable, all_state, none_state, check_all=False, retry_count=10):
|
||||
|
||||
restrictive_entrances, soft_entrances = split_entrances_by_requirements(ootworld, entrance_pool, target_entrances)
|
||||
|
||||
@@ -745,7 +744,6 @@ def shuffle_entrances(ootworld, pool_type, entrances, target_entrances, rollback
|
||||
|
||||
|
||||
def split_entrances_by_requirements(ootworld, entrances_to_split, assumed_entrances):
|
||||
world = ootworld.multiworld
|
||||
player = ootworld.player
|
||||
|
||||
# Disconnect all root assumed entrances and save original connections
|
||||
@@ -755,7 +753,7 @@ def split_entrances_by_requirements(ootworld, entrances_to_split, assumed_entran
|
||||
if entrance.connected_region:
|
||||
original_connected_regions[entrance] = entrance.disconnect()
|
||||
|
||||
all_state = world.get_all_state(use_cache=False)
|
||||
all_state = ootworld.get_state_with_complete_itempool()
|
||||
|
||||
restrictive_entrances = []
|
||||
soft_entrances = []
|
||||
@@ -793,8 +791,8 @@ def validate_world(ootworld, entrance_placed, locations_to_ensure_reachable, all
|
||||
all_state = all_state_orig.copy()
|
||||
none_state = none_state_orig.copy()
|
||||
|
||||
all_state.sweep_for_events()
|
||||
none_state.sweep_for_events()
|
||||
all_state.sweep_for_events(locations=ootworld.get_locations())
|
||||
none_state.sweep_for_events(locations=ootworld.get_locations())
|
||||
|
||||
if ootworld.shuffle_interior_entrances or ootworld.shuffle_overworld_entrances or ootworld.spawn_positions:
|
||||
time_travel_state = none_state.copy()
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
from collections import deque
|
||||
import logging
|
||||
import typing
|
||||
|
||||
from .Regions import TimeOfDay
|
||||
from .DungeonList import dungeon_table
|
||||
from .Hints import HintArea
|
||||
from .Items import oot_is_item_of_type
|
||||
from .LocationList import dungeon_song_locations
|
||||
|
||||
from BaseClasses import CollectionState
|
||||
from worlds.generic.Rules import set_rule, add_rule, add_item_rule, forbid_item
|
||||
@@ -150,11 +154,16 @@ def set_rules(ootworld):
|
||||
location = world.get_location('Forest Temple MQ First Room Chest', player)
|
||||
forbid_item(location, 'Boss Key (Forest Temple)', ootworld.player)
|
||||
|
||||
if ootworld.shuffle_song_items == 'song' and not ootworld.songs_as_items:
|
||||
if ootworld.shuffle_song_items in {'song', 'dungeon'} and not ootworld.songs_as_items:
|
||||
# Sheik in Ice Cavern is the only song location in a dungeon; need to ensure that it cannot be anything else.
|
||||
# This is required if map/compass included, or any_dungeon shuffle.
|
||||
location = world.get_location('Sheik in Ice Cavern', player)
|
||||
add_item_rule(location, lambda item: item.player == player and oot_is_item_of_type(item, 'Song'))
|
||||
add_item_rule(location, lambda item: oot_is_item_of_type(item, 'Song'))
|
||||
|
||||
if ootworld.shuffle_child_trade == 'skip_child_zelda':
|
||||
# Song from Impa must be local
|
||||
location = world.get_location('Song from Impa', player)
|
||||
add_item_rule(location, lambda item: item.player == player)
|
||||
|
||||
for name in ootworld.always_hints:
|
||||
add_rule(world.get_location(name, player), guarantee_hint)
|
||||
@@ -176,11 +185,6 @@ def create_shop_rule(location, parser):
|
||||
return parser.parse_rule('(Progressive_Wallet, %d)' % required_wallets(location.price))
|
||||
|
||||
|
||||
def limit_to_itemset(location, itemset):
|
||||
old_rule = location.item_rule
|
||||
location.item_rule = lambda item: item.name in itemset and old_rule(item)
|
||||
|
||||
|
||||
# This function should be run once after the shop items are placed in the world.
|
||||
# It should be run before other items are placed in the world so that logic has
|
||||
# the correct checks for them. This is safe to do since every shop is still
|
||||
|
||||
@@ -43,14 +43,14 @@ i_o_limiter = threading.Semaphore(2)
|
||||
|
||||
class OOTCollectionState(metaclass=AutoLogicRegister):
|
||||
def init_mixin(self, parent: MultiWorld):
|
||||
all_ids = parent.get_all_ids()
|
||||
self.child_reachable_regions = {player: set() for player in all_ids}
|
||||
self.adult_reachable_regions = {player: set() for player in all_ids}
|
||||
self.child_blocked_connections = {player: set() for player in all_ids}
|
||||
self.adult_blocked_connections = {player: set() for player in all_ids}
|
||||
self.day_reachable_regions = {player: set() for player in all_ids}
|
||||
self.dampe_reachable_regions = {player: set() for player in all_ids}
|
||||
self.age = {player: None for player in all_ids}
|
||||
oot_ids = parent.get_game_players(OOTWorld.game) + parent.get_game_groups(OOTWorld.game)
|
||||
self.child_reachable_regions = {player: set() for player in oot_ids}
|
||||
self.adult_reachable_regions = {player: set() for player in oot_ids}
|
||||
self.child_blocked_connections = {player: set() for player in oot_ids}
|
||||
self.adult_blocked_connections = {player: set() for player in oot_ids}
|
||||
self.day_reachable_regions = {player: set() for player in oot_ids}
|
||||
self.dampe_reachable_regions = {player: set() for player in oot_ids}
|
||||
self.age = {player: None for player in oot_ids}
|
||||
|
||||
def copy_mixin(self, ret) -> CollectionState:
|
||||
ret.child_reachable_regions = {player: copy.copy(self.child_reachable_regions[player]) for player in
|
||||
@@ -170,15 +170,19 @@ class OOTWorld(World):
|
||||
|
||||
location_name_groups = build_location_name_groups()
|
||||
|
||||
|
||||
def __init__(self, world, player):
|
||||
self.hint_data_available = threading.Event()
|
||||
self.collectible_flags_available = threading.Event()
|
||||
super(OOTWorld, self).__init__(world, player)
|
||||
|
||||
|
||||
@classmethod
|
||||
def stage_assert_generate(cls, multiworld: MultiWorld):
|
||||
rom = Rom(file=get_options()['oot_options']['rom_file'])
|
||||
|
||||
|
||||
# Option parsing, handling incompatible options, building useful-item table
|
||||
def generate_early(self):
|
||||
self.parser = Rule_AST_Transformer(self, self.player)
|
||||
|
||||
@@ -194,8 +198,10 @@ class OOTWorld(World):
|
||||
option_value = result.current_key
|
||||
setattr(self, option_name, option_value)
|
||||
|
||||
self.regions = [] # internal caches of regions for this world, used later
|
||||
self._regions_cache = {}
|
||||
|
||||
self.shop_prices = {}
|
||||
self.regions = [] # internal cache of regions for this world, used later
|
||||
self.remove_from_start_inventory = [] # some items will be precollected but not in the inventory
|
||||
self.starting_items = Counter()
|
||||
self.songs_as_items = False
|
||||
@@ -489,6 +495,8 @@ class OOTWorld(World):
|
||||
# Farore's Wind skippable if not used for this logic trick in Water Temple
|
||||
self.nonadvancement_items.add('Farores Wind')
|
||||
|
||||
|
||||
# Reads a group of regions from the given JSON file.
|
||||
def load_regions_from_json(self, file_path):
|
||||
region_json = read_json(file_path)
|
||||
|
||||
@@ -526,6 +534,10 @@ class OOTWorld(World):
|
||||
# We still need to fill the location even if ALR is off.
|
||||
logger.debug('Unreachable location: %s', new_location.name)
|
||||
new_location.player = self.player
|
||||
# Change some attributes of Drop locations
|
||||
if new_location.type == 'Drop':
|
||||
new_location.name = new_region.name + ' ' + new_location.name
|
||||
new_location.show_in_spoiler = False
|
||||
new_region.locations.append(new_location)
|
||||
if 'events' in region:
|
||||
for event, rule in region['events'].items():
|
||||
@@ -555,8 +567,10 @@ class OOTWorld(World):
|
||||
|
||||
self.multiworld.regions.append(new_region)
|
||||
self.regions.append(new_region)
|
||||
self.multiworld._recache()
|
||||
self._regions_cache[new_region.name] = new_region
|
||||
|
||||
|
||||
# Sets deku scrub prices
|
||||
def set_scrub_prices(self):
|
||||
# Get Deku Scrub Locations
|
||||
scrub_locations = [location for location in self.get_locations() if location.type in {'Scrub', 'GrottoScrub'}]
|
||||
@@ -585,6 +599,8 @@ class OOTWorld(World):
|
||||
if location.item is not None:
|
||||
location.item.price = price
|
||||
|
||||
|
||||
# Sets prices for shuffled shop locations
|
||||
def random_shop_prices(self):
|
||||
shop_item_indexes = ['7', '5', '8', '6']
|
||||
self.shop_prices = {}
|
||||
@@ -610,6 +626,8 @@ class OOTWorld(World):
|
||||
elif self.shopsanity_prices == 'tycoons_wallet':
|
||||
self.shop_prices[location.name] = self.multiworld.random.randrange(0,1000,5)
|
||||
|
||||
|
||||
# Fill boss prizes
|
||||
def fill_bosses(self, bossCount=9):
|
||||
boss_location_names = (
|
||||
'Queen Gohma',
|
||||
@@ -622,7 +640,7 @@ class OOTWorld(World):
|
||||
'Twinrova',
|
||||
'Links Pocket'
|
||||
)
|
||||
boss_rewards = [item for item in self.itempool if item.type == 'DungeonReward']
|
||||
boss_rewards = sorted(map(self.create_item, self.item_name_groups['rewards']))
|
||||
boss_locations = [self.multiworld.get_location(loc, self.player) for loc in boss_location_names]
|
||||
|
||||
placed_prizes = [loc.item.name for loc in boss_locations if loc.item is not None]
|
||||
@@ -636,9 +654,46 @@ class OOTWorld(World):
|
||||
item = prizepool.pop()
|
||||
loc = prize_locs.pop()
|
||||
loc.place_locked_item(item)
|
||||
self.multiworld.itempool.remove(item)
|
||||
self.hinted_dungeon_reward_locations[item.name] = loc
|
||||
|
||||
|
||||
# Separate the result from generate_itempool into main and prefill pools
|
||||
def divide_itempools(self):
|
||||
prefill_item_types = set()
|
||||
if self.shopsanity != 'off':
|
||||
prefill_item_types.add('Shop')
|
||||
if self.shuffle_song_items != 'any':
|
||||
prefill_item_types.add('Song')
|
||||
if self.shuffle_smallkeys != 'keysanity':
|
||||
prefill_item_types.add('SmallKey')
|
||||
if self.shuffle_bosskeys != 'keysanity':
|
||||
prefill_item_types.add('BossKey')
|
||||
if self.shuffle_hideoutkeys != 'keysanity':
|
||||
prefill_item_types.add('HideoutSmallKey')
|
||||
if self.shuffle_ganon_bosskey != 'keysanity':
|
||||
prefill_item_types.add('GanonBossKey')
|
||||
if self.shuffle_mapcompass != 'keysanity':
|
||||
prefill_item_types.update({'Map', 'Compass'})
|
||||
|
||||
main_items = []
|
||||
prefill_items = []
|
||||
for item in self.itempool:
|
||||
if item.type in prefill_item_types:
|
||||
prefill_items.append(item)
|
||||
else:
|
||||
main_items.append(item)
|
||||
return main_items, prefill_items
|
||||
|
||||
|
||||
# only returns proper result after create_items and divide_itempools are run
|
||||
def get_pre_fill_items(self):
|
||||
return self.pre_fill_items
|
||||
|
||||
|
||||
# Note on allow_arbitrary_name:
|
||||
# OoT defines many helper items and event names that are treated indistinguishably from regular items,
|
||||
# but are only defined in the logic files. This means we need to create items for any name.
|
||||
# Allowing any item name to be created is dangerous in case of plando, so this is a middle ground.
|
||||
def create_item(self, name: str, allow_arbitrary_name: bool = False):
|
||||
if name in item_table:
|
||||
return OOTItem(name, self.player, item_table[name], False,
|
||||
@@ -658,7 +713,9 @@ class OOTWorld(World):
|
||||
location.internal = True
|
||||
return item
|
||||
|
||||
def create_regions(self): # create and link regions
|
||||
|
||||
# Create regions, locations, and entrances
|
||||
def create_regions(self):
|
||||
if self.logic_rules == 'glitchless' or self.logic_rules == 'no_logic': # enables ER + NL
|
||||
world_type = 'World'
|
||||
else:
|
||||
@@ -671,7 +728,7 @@ class OOTWorld(World):
|
||||
self.multiworld.regions.append(menu)
|
||||
self.load_regions_from_json(overworld_data_path)
|
||||
self.load_regions_from_json(bosses_data_path)
|
||||
start.connect(self.multiworld.get_region('Root', self.player))
|
||||
start.connect(self.get_region('Root'))
|
||||
create_dungeons(self)
|
||||
self.parser.create_delayed_rules()
|
||||
|
||||
@@ -682,16 +739,13 @@ class OOTWorld(World):
|
||||
# Bind entrances to vanilla
|
||||
for region in self.regions:
|
||||
for exit in region.exits:
|
||||
exit.connect(self.multiworld.get_region(exit.vanilla_connected_region, self.player))
|
||||
exit.connect(self.get_region(exit.vanilla_connected_region))
|
||||
|
||||
|
||||
# Create items, starting item handling, boss prize fill (before entrance randomizer)
|
||||
def create_items(self):
|
||||
# Uniquely rename drop locations for each region and erase them from the spoiler
|
||||
set_drop_location_names(self)
|
||||
# Generate itempool
|
||||
generate_itempool(self)
|
||||
# Add dungeon rewards
|
||||
rewardlist = sorted(list(self.item_name_groups['rewards']))
|
||||
self.itempool += map(self.create_item, rewardlist)
|
||||
|
||||
junk_pool = get_junk_pool(self)
|
||||
removed_items = []
|
||||
@@ -714,12 +768,16 @@ class OOTWorld(World):
|
||||
if self.start_with_rupees:
|
||||
self.starting_items['Rupees'] = 999
|
||||
|
||||
# Divide itempool into prefill and main pools
|
||||
self.itempool, self.pre_fill_items = self.divide_itempools()
|
||||
|
||||
self.multiworld.itempool += self.itempool
|
||||
self.remove_from_start_inventory.extend(removed_items)
|
||||
|
||||
# Fill boss prizes. needs to happen before entrance shuffle
|
||||
self.fill_bosses()
|
||||
|
||||
|
||||
def set_rules(self):
|
||||
# This has to run AFTER creating items but BEFORE set_entrances_based_rules
|
||||
if self.entrance_shuffle:
|
||||
@@ -757,6 +815,7 @@ class OOTWorld(World):
|
||||
set_rules(self)
|
||||
set_entrances_based_rules(self)
|
||||
|
||||
|
||||
def generate_basic(self): # mostly killing locations that shouldn't exist by settings
|
||||
|
||||
# Gather items for ice trap appearances
|
||||
@@ -769,7 +828,8 @@ class OOTWorld(World):
|
||||
|
||||
# Kill unreachable events that can't be gotten even with all items
|
||||
# Make sure to only kill actual internal events, not in-game "events"
|
||||
all_state = self.multiworld.get_all_state(False)
|
||||
all_state = self.get_state_with_complete_itempool()
|
||||
all_state.sweep_for_events()
|
||||
all_locations = self.get_locations()
|
||||
reachable = self.multiworld.get_reachable_locations(all_state, self.player)
|
||||
unreachable = [loc for loc in all_locations if
|
||||
@@ -781,7 +841,6 @@ class OOTWorld(World):
|
||||
bigpoe = self.multiworld.get_location('Sell Big Poe from Market Guard House', self.player)
|
||||
if not all_state.has('Bottle with Big Poe', self.player) and bigpoe not in reachable:
|
||||
bigpoe.parent_region.locations.remove(bigpoe)
|
||||
self.multiworld.clear_location_cache()
|
||||
|
||||
# If fast scarecrow then we need to kill the Pierre location as it will be unreachable
|
||||
if self.free_scarecrow:
|
||||
@@ -792,35 +851,63 @@ class OOTWorld(World):
|
||||
loc = self.multiworld.get_location("Deliver Rutos Letter", self.player)
|
||||
loc.parent_region.locations.remove(loc)
|
||||
|
||||
|
||||
def pre_fill(self):
|
||||
|
||||
def prefill_state(base_state):
|
||||
state = base_state.copy()
|
||||
for item in self.get_pre_fill_items():
|
||||
self.collect(state, item)
|
||||
state.sweep_for_events(self.get_locations())
|
||||
return state
|
||||
|
||||
# Prefill shops, songs, and dungeon items
|
||||
items = self.get_pre_fill_items()
|
||||
locations = list(self.multiworld.get_unfilled_locations(self.player))
|
||||
self.multiworld.random.shuffle(locations)
|
||||
|
||||
# Set up initial state
|
||||
state = CollectionState(self.multiworld)
|
||||
for item in self.itempool:
|
||||
self.collect(state, item)
|
||||
state.sweep_for_events(self.get_locations())
|
||||
|
||||
# Place dungeon items
|
||||
special_fill_types = ['GanonBossKey', 'BossKey', 'SmallKey', 'HideoutSmallKey', 'Map', 'Compass']
|
||||
world_items = [item for item in self.multiworld.itempool if item.player == self.player]
|
||||
type_to_setting = {
|
||||
'Map': 'shuffle_mapcompass',
|
||||
'Compass': 'shuffle_mapcompass',
|
||||
'SmallKey': 'shuffle_smallkeys',
|
||||
'BossKey': 'shuffle_bosskeys',
|
||||
'HideoutSmallKey': 'shuffle_hideoutkeys',
|
||||
'GanonBossKey': 'shuffle_ganon_bosskey',
|
||||
}
|
||||
special_fill_types.sort(key=lambda x: 0 if getattr(self, type_to_setting[x]) == 'dungeon' else 1)
|
||||
|
||||
for fill_stage in special_fill_types:
|
||||
stage_items = list(filter(lambda item: oot_is_item_of_type(item, fill_stage), world_items))
|
||||
stage_items = list(filter(lambda item: oot_is_item_of_type(item, fill_stage), self.pre_fill_items))
|
||||
if not stage_items:
|
||||
continue
|
||||
if fill_stage in ['GanonBossKey', 'HideoutSmallKey']:
|
||||
locations = gather_locations(self.multiworld, fill_stage, self.player)
|
||||
if isinstance(locations, list):
|
||||
for item in stage_items:
|
||||
self.multiworld.itempool.remove(item)
|
||||
self.pre_fill_items.remove(item)
|
||||
self.multiworld.random.shuffle(locations)
|
||||
fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), locations, stage_items,
|
||||
fill_restrictive(self.multiworld, prefill_state(state), locations, stage_items,
|
||||
single_player_placement=True, lock=True, allow_excluded=True)
|
||||
else:
|
||||
for dungeon_info in dungeon_table:
|
||||
dungeon_name = dungeon_info['name']
|
||||
dungeon_items = list(filter(lambda item: dungeon_name in item.name, stage_items))
|
||||
if not dungeon_items:
|
||||
continue
|
||||
locations = gather_locations(self.multiworld, fill_stage, self.player, dungeon=dungeon_name)
|
||||
if isinstance(locations, list):
|
||||
dungeon_items = list(filter(lambda item: dungeon_name in item.name, stage_items))
|
||||
if not dungeon_items:
|
||||
continue
|
||||
for item in dungeon_items:
|
||||
self.multiworld.itempool.remove(item)
|
||||
self.pre_fill_items.remove(item)
|
||||
self.multiworld.random.shuffle(locations)
|
||||
fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), locations, dungeon_items,
|
||||
fill_restrictive(self.multiworld, prefill_state(state), locations, dungeon_items,
|
||||
single_player_placement=True, lock=True, allow_excluded=True)
|
||||
|
||||
# Place songs
|
||||
@@ -836,9 +923,9 @@ class OOTWorld(World):
|
||||
else:
|
||||
raise Exception(f"Unknown song shuffle type: {self.shuffle_song_items}")
|
||||
|
||||
songs = list(filter(lambda item: item.player == self.player and item.type == 'Song', self.multiworld.itempool))
|
||||
songs = list(filter(lambda item: item.type == 'Song', self.pre_fill_items))
|
||||
for song in songs:
|
||||
self.multiworld.itempool.remove(song)
|
||||
self.pre_fill_items.remove(song)
|
||||
|
||||
important_warps = (self.shuffle_special_interior_entrances or self.shuffle_overworld_entrances or
|
||||
self.warp_songs or self.spawn_positions)
|
||||
@@ -861,7 +948,7 @@ class OOTWorld(World):
|
||||
while tries:
|
||||
try:
|
||||
self.multiworld.random.shuffle(song_locations)
|
||||
fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), song_locations[:], songs[:],
|
||||
fill_restrictive(self.multiworld, prefill_state(state), song_locations[:], songs[:],
|
||||
single_player_placement=True, lock=True, allow_excluded=True)
|
||||
logger.debug(f"Successfully placed songs for player {self.player} after {6 - tries} attempt(s)")
|
||||
except FillError as e:
|
||||
@@ -883,10 +970,8 @@ class OOTWorld(World):
|
||||
# Place shop items
|
||||
# fast fill will fail because there is some logic on the shop items. we'll gather them up and place the shop items
|
||||
if self.shopsanity != 'off':
|
||||
shop_prog = list(filter(lambda item: item.player == self.player and item.type == 'Shop'
|
||||
and item.advancement, self.multiworld.itempool))
|
||||
shop_junk = list(filter(lambda item: item.player == self.player and item.type == 'Shop'
|
||||
and not item.advancement, self.multiworld.itempool))
|
||||
shop_prog = list(filter(lambda item: item.type == 'Shop' and item.advancement, self.pre_fill_items))
|
||||
shop_junk = list(filter(lambda item: item.type == 'Shop' and not item.advancement, self.pre_fill_items))
|
||||
shop_locations = list(
|
||||
filter(lambda location: location.type == 'Shop' and location.name not in self.shop_prices,
|
||||
self.multiworld.get_unfilled_locations(player=self.player)))
|
||||
@@ -896,30 +981,14 @@ class OOTWorld(World):
|
||||
'Buy Zora Tunic': 1,
|
||||
}.get(item.name, 0)) # place Deku Shields if needed, then tunics, then other advancement
|
||||
self.multiworld.random.shuffle(shop_locations)
|
||||
for item in shop_prog + shop_junk:
|
||||
self.multiworld.itempool.remove(item)
|
||||
fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), shop_locations, shop_prog,
|
||||
self.pre_fill_items = [] # all prefill should be done
|
||||
fill_restrictive(self.multiworld, prefill_state(state), shop_locations, shop_prog,
|
||||
single_player_placement=True, lock=True, allow_excluded=True)
|
||||
fast_fill(self.multiworld, shop_junk, shop_locations)
|
||||
for loc in shop_locations:
|
||||
loc.locked = True
|
||||
set_shop_rules(self) # sets wallet requirements on shop items, must be done after they are filled
|
||||
|
||||
# If skip child zelda is active and Song from Impa is unfilled, put a local giveable item into it.
|
||||
impa = self.multiworld.get_location("Song from Impa", self.player)
|
||||
if self.shuffle_child_trade == 'skip_child_zelda':
|
||||
if impa.item is None:
|
||||
candidate_items = list(item for item in self.multiworld.itempool if item.player == self.player)
|
||||
if candidate_items:
|
||||
item_to_place = self.multiworld.random.choice(candidate_items)
|
||||
self.multiworld.itempool.remove(item_to_place)
|
||||
else:
|
||||
item_to_place = self.create_item("Recovery Heart")
|
||||
impa.place_locked_item(item_to_place)
|
||||
# Give items to startinventory
|
||||
self.multiworld.push_precollected(impa.item)
|
||||
self.multiworld.push_precollected(self.create_item("Zeldas Letter"))
|
||||
|
||||
# Exclude locations in Ganon's Castle proportional to the number of items required to make the bridge
|
||||
# Check for dungeon ER later
|
||||
if self.logic_rules == 'glitchless':
|
||||
@@ -954,48 +1023,6 @@ class OOTWorld(World):
|
||||
or (self.shuffle_child_trade == 'skip_child_zelda' and loc.name in ['HC Zeldas Letter', 'Song from Impa'])):
|
||||
loc.address = None
|
||||
|
||||
# Handle item-linked dungeon items and songs
|
||||
@classmethod
|
||||
def stage_pre_fill(cls, multiworld: MultiWorld):
|
||||
special_fill_types = ['Song', 'GanonBossKey', 'BossKey', 'SmallKey', 'HideoutSmallKey', 'Map', 'Compass']
|
||||
for group_id, group in multiworld.groups.items():
|
||||
if group['game'] != cls.game:
|
||||
continue
|
||||
group_items = [item for item in multiworld.itempool if item.player == group_id]
|
||||
for fill_stage in special_fill_types:
|
||||
group_stage_items = list(filter(lambda item: oot_is_item_of_type(item, fill_stage), group_items))
|
||||
if not group_stage_items:
|
||||
continue
|
||||
if fill_stage in ['Song', 'GanonBossKey', 'HideoutSmallKey']:
|
||||
# No need to subdivide by dungeon name
|
||||
locations = gather_locations(multiworld, fill_stage, group['players'])
|
||||
if isinstance(locations, list):
|
||||
for item in group_stage_items:
|
||||
multiworld.itempool.remove(item)
|
||||
multiworld.random.shuffle(locations)
|
||||
fill_restrictive(multiworld, multiworld.get_all_state(False), locations, group_stage_items,
|
||||
single_player_placement=False, lock=True, allow_excluded=True)
|
||||
if fill_stage == 'Song':
|
||||
# We don't want song locations to contain progression unless it's a song
|
||||
# or it was marked as priority.
|
||||
# We do this manually because we'd otherwise have to either
|
||||
# iterate twice or do many function calls.
|
||||
for loc in locations:
|
||||
if loc.progress_type == LocationProgressType.DEFAULT:
|
||||
loc.progress_type = LocationProgressType.EXCLUDED
|
||||
add_item_rule(loc, lambda i: not (i.advancement or i.useful))
|
||||
else:
|
||||
# Perform the fill task once per dungeon
|
||||
for dungeon_info in dungeon_table:
|
||||
dungeon_name = dungeon_info['name']
|
||||
locations = gather_locations(multiworld, fill_stage, group['players'], dungeon=dungeon_name)
|
||||
if isinstance(locations, list):
|
||||
group_dungeon_items = list(filter(lambda item: dungeon_name in item.name, group_stage_items))
|
||||
for item in group_dungeon_items:
|
||||
multiworld.itempool.remove(item)
|
||||
multiworld.random.shuffle(locations)
|
||||
fill_restrictive(multiworld, multiworld.get_all_state(False), locations, group_dungeon_items,
|
||||
single_player_placement=False, lock=True, allow_excluded=True)
|
||||
|
||||
def generate_output(self, output_directory: str):
|
||||
if self.hints != 'none':
|
||||
@@ -1032,30 +1059,6 @@ class OOTWorld(World):
|
||||
player_name=self.multiworld.get_player_name(self.player))
|
||||
apz5.write()
|
||||
|
||||
# Write entrances to spoiler log
|
||||
all_entrances = self.get_shuffled_entrances()
|
||||
all_entrances.sort(reverse=True, key=lambda x: x.name)
|
||||
all_entrances.sort(reverse=True, key=lambda x: x.type)
|
||||
if not self.decouple_entrances:
|
||||
while all_entrances:
|
||||
loadzone = all_entrances.pop()
|
||||
if loadzone.type != 'Overworld':
|
||||
if loadzone.primary:
|
||||
entrance = loadzone
|
||||
else:
|
||||
entrance = loadzone.reverse
|
||||
if entrance.reverse is not None:
|
||||
self.multiworld.spoiler.set_entrance(entrance, entrance.replaces.reverse, 'both', self.player)
|
||||
else:
|
||||
self.multiworld.spoiler.set_entrance(entrance, entrance.replaces, 'entrance', self.player)
|
||||
else:
|
||||
reverse = loadzone.replaces.reverse
|
||||
if reverse in all_entrances:
|
||||
all_entrances.remove(reverse)
|
||||
self.multiworld.spoiler.set_entrance(loadzone, reverse, 'both', self.player)
|
||||
else:
|
||||
for entrance in all_entrances:
|
||||
self.multiworld.spoiler.set_entrance(entrance, entrance.replaces, 'entrance', self.player)
|
||||
|
||||
# Gathers hint data for OoT. Loops over all world locations for woth, barren, and major item locations.
|
||||
@classmethod
|
||||
@@ -1135,6 +1138,7 @@ class OOTWorld(World):
|
||||
for autoworld in multiworld.get_game_worlds("Ocarina of Time"):
|
||||
autoworld.hint_data_available.set()
|
||||
|
||||
|
||||
def fill_slot_data(self):
|
||||
self.collectible_flags_available.wait()
|
||||
return {
|
||||
@@ -1142,6 +1146,7 @@ class OOTWorld(World):
|
||||
'collectible_flag_offsets': self.collectible_flag_offsets
|
||||
}
|
||||
|
||||
|
||||
def modify_multidata(self, multidata: dict):
|
||||
|
||||
# Replace connect name
|
||||
@@ -1156,6 +1161,16 @@ class OOTWorld(World):
|
||||
continue
|
||||
multidata["precollected_items"][self.player].remove(item_id)
|
||||
|
||||
# If skip child zelda, push item onto autotracker
|
||||
if self.shuffle_child_trade == 'skip_child_zelda':
|
||||
impa_item_id = self.item_name_to_id.get(self.get_location('Song from Impa').item.name, None)
|
||||
zelda_item_id = self.item_name_to_id.get(self.get_location('HC Zeldas Letter').item.name, None)
|
||||
if impa_item_id:
|
||||
multidata["precollected_items"][self.player].append(impa_item_id)
|
||||
if zelda_item_id:
|
||||
multidata["precollected_items"][self.player].append(zelda_item_id)
|
||||
|
||||
|
||||
def extend_hint_information(self, er_hint_data: dict):
|
||||
|
||||
er_hint_data[self.player] = {}
|
||||
@@ -1202,6 +1217,7 @@ class OOTWorld(World):
|
||||
er_hint_data[self.player][location.address] = main_entrance.name
|
||||
logger.debug(f"Set {location.name} hint data to {main_entrance.name}")
|
||||
|
||||
|
||||
def write_spoiler(self, spoiler_handle: typing.TextIO) -> None:
|
||||
required_trials_str = ", ".join(t for t in self.skipped_trials if not self.skipped_trials[t])
|
||||
spoiler_handle.write(f"\n\nTrials ({self.multiworld.get_player_name(self.player)}): {required_trials_str}\n")
|
||||
@@ -1211,6 +1227,32 @@ class OOTWorld(World):
|
||||
for k, v in self.shop_prices.items():
|
||||
spoiler_handle.write(f"{k}: {v} Rupees\n")
|
||||
|
||||
# Write entrances to spoiler log
|
||||
all_entrances = self.get_shuffled_entrances()
|
||||
all_entrances.sort(reverse=True, key=lambda x: x.name)
|
||||
all_entrances.sort(reverse=True, key=lambda x: x.type)
|
||||
if not self.decouple_entrances:
|
||||
while all_entrances:
|
||||
loadzone = all_entrances.pop()
|
||||
if loadzone.type != 'Overworld':
|
||||
if loadzone.primary:
|
||||
entrance = loadzone
|
||||
else:
|
||||
entrance = loadzone.reverse
|
||||
if entrance.reverse is not None:
|
||||
self.multiworld.spoiler.set_entrance(entrance, entrance.replaces.reverse, 'both', self.player)
|
||||
else:
|
||||
self.multiworld.spoiler.set_entrance(entrance, entrance.replaces, 'entrance', self.player)
|
||||
else:
|
||||
reverse = loadzone.replaces.reverse
|
||||
if reverse in all_entrances:
|
||||
all_entrances.remove(reverse)
|
||||
self.multiworld.spoiler.set_entrance(loadzone, reverse, 'both', self.player)
|
||||
else:
|
||||
for entrance in all_entrances:
|
||||
self.multiworld.spoiler.set_entrance(entrance, entrance.replaces, 'entrance', self.player)
|
||||
|
||||
|
||||
# Key ring handling:
|
||||
# Key rings are multiple items glued together into one, so we need to give
|
||||
# the appropriate number of keys in the collection state when they are
|
||||
@@ -1242,24 +1284,29 @@ class OOTWorld(World):
|
||||
return False
|
||||
|
||||
def get_shufflable_entrances(self, type=None, only_primary=False):
|
||||
return [entrance for entrance in self.multiworld.get_entrances() if (entrance.player == self.player and
|
||||
(type == None or entrance.type == type) and
|
||||
(not only_primary or entrance.primary))]
|
||||
return [entrance for entrance in self.get_entrances() if ((type == None or entrance.type == type)
|
||||
and (not only_primary or entrance.primary))]
|
||||
|
||||
def get_shuffled_entrances(self, type=None, only_primary=False):
|
||||
return [entrance for entrance in self.get_shufflable_entrances(type=type, only_primary=only_primary) if
|
||||
entrance.shuffled]
|
||||
|
||||
def get_locations(self):
|
||||
for region in self.regions:
|
||||
for loc in region.locations:
|
||||
yield loc
|
||||
return self.multiworld.get_locations(self.player)
|
||||
|
||||
def get_location(self, location):
|
||||
return self.multiworld.get_location(location, self.player)
|
||||
|
||||
def get_region(self, region):
|
||||
return self.multiworld.get_region(region, self.player)
|
||||
def get_region(self, region_name):
|
||||
try:
|
||||
return self._regions_cache[region_name]
|
||||
except KeyError:
|
||||
ret = self.multiworld.get_region(region_name, self.player)
|
||||
self._regions_cache[region_name] = ret
|
||||
return ret
|
||||
|
||||
def get_entrances(self):
|
||||
return self.multiworld.get_entrances(self.player)
|
||||
|
||||
def get_entrance(self, entrance):
|
||||
return self.multiworld.get_entrance(entrance, self.player)
|
||||
@@ -1294,9 +1341,8 @@ class OOTWorld(World):
|
||||
# In particular, ensures that Time Travel needs to be found.
|
||||
def get_state_with_complete_itempool(self):
|
||||
all_state = CollectionState(self.multiworld)
|
||||
for item in self.multiworld.itempool:
|
||||
if item.player == self.player:
|
||||
self.multiworld.worlds[item.player].collect(all_state, item)
|
||||
for item in self.itempool + self.pre_fill_items:
|
||||
self.multiworld.worlds[item.player].collect(all_state, item)
|
||||
# If free_scarecrow give Scarecrow Song
|
||||
if self.free_scarecrow:
|
||||
all_state.collect(self.create_item("Scarecrow Song"), event=True)
|
||||
@@ -1336,7 +1382,6 @@ def gather_locations(multiworld: MultiWorld,
|
||||
dungeon: str = ''
|
||||
) -> Optional[List[OOTLocation]]:
|
||||
type_to_setting = {
|
||||
'Song': 'shuffle_song_items',
|
||||
'Map': 'shuffle_mapcompass',
|
||||
'Compass': 'shuffle_mapcompass',
|
||||
'SmallKey': 'shuffle_smallkeys',
|
||||
@@ -1355,21 +1400,12 @@ def gather_locations(multiworld: MultiWorld,
|
||||
players = {players}
|
||||
fill_opts = {p: getattr(multiworld.worlds[p], type_to_setting[item_type]) for p in players}
|
||||
locations = []
|
||||
if item_type == 'Song':
|
||||
if any(map(lambda v: v == 'any', fill_opts.values())):
|
||||
return None
|
||||
for player, option in fill_opts.items():
|
||||
if option == 'song':
|
||||
condition = lambda location: location.type == 'Song'
|
||||
elif option == 'dungeon':
|
||||
condition = lambda location: location.name in dungeon_song_locations
|
||||
locations += filter(condition, multiworld.get_unfilled_locations(player=player))
|
||||
else:
|
||||
if any(map(lambda v: v == 'keysanity', fill_opts.values())):
|
||||
return None
|
||||
for player, option in fill_opts.items():
|
||||
condition = functools.partial(valid_dungeon_item_location,
|
||||
multiworld.worlds[player], option, dungeon)
|
||||
locations += filter(condition, multiworld.get_unfilled_locations(player=player))
|
||||
if any(map(lambda v: v == 'keysanity', fill_opts.values())):
|
||||
return None
|
||||
for player, option in fill_opts.items():
|
||||
condition = functools.partial(valid_dungeon_item_location,
|
||||
multiworld.worlds[player], option, dungeon)
|
||||
locations += filter(condition, multiworld.get_unfilled_locations(player=player))
|
||||
|
||||
return locations
|
||||
|
||||
|
||||
@@ -445,13 +445,9 @@ class PokemonRedBlueWorld(World):
|
||||
# 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.
|
||||
evolutions_region = self.multiworld.get_region("Evolution", self.player)
|
||||
clear_cache = False
|
||||
for location in evolutions_region.locations.copy():
|
||||
if not test_state.can_reach(location, player=self.player):
|
||||
evolutions_region.locations.remove(location)
|
||||
clear_cache = True
|
||||
if clear_cache:
|
||||
self.multiworld.clear_location_cache()
|
||||
|
||||
if self.multiworld.old_man[self.player] == "early_parcel":
|
||||
self.multiworld.local_early_items[self.player]["Oak's Parcel"] = 1
|
||||
@@ -559,7 +555,6 @@ class PokemonRedBlueWorld(World):
|
||||
else:
|
||||
raise Exception("Failed to remove corresponding item while deleting unreachable Dexsanity location")
|
||||
|
||||
self.multiworld._recache()
|
||||
|
||||
if self.multiworld.door_shuffle[self.player] == "decoupled":
|
||||
swept_state = self.multiworld.state.copy()
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -546,10 +546,8 @@ def generate_output(self, output_directory: str):
|
||||
|
||||
write_quizzes(self, data, random)
|
||||
|
||||
for location in self.multiworld.get_locations():
|
||||
if location.player != self.player:
|
||||
continue
|
||||
elif location.party_data:
|
||||
for location in self.multiworld.get_locations(self.player):
|
||||
if location.party_data:
|
||||
for party in location.party_data:
|
||||
if not isinstance(party["party_address"], list):
|
||||
addresses = [rom_addresses[party["party_address"]]]
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
rom_addresses = {
|
||||
"Option_Encounter_Minimum_Steps": 0x3c1,
|
||||
"Option_Pitch_Black_Rock_Tunnel": 0x758,
|
||||
"Option_Blind_Trainers": 0x30c3,
|
||||
"Option_Trainersanity1": 0x3153,
|
||||
"Option_Split_Card_Key": 0x3e0c,
|
||||
"Option_Fix_Combat_Bugs": 0x3e0d,
|
||||
"Option_Pitch_Black_Rock_Tunnel": 0x75c,
|
||||
"Option_Blind_Trainers": 0x30c7,
|
||||
"Option_Trainersanity1": 0x3157,
|
||||
"Option_Split_Card_Key": 0x3e10,
|
||||
"Option_Fix_Combat_Bugs": 0x3e11,
|
||||
"Option_Lose_Money": 0x40d4,
|
||||
"Base_Stats_Mew": 0x4260,
|
||||
"Title_Mon_First": 0x4373,
|
||||
|
||||
@@ -103,25 +103,25 @@ def set_rules(multiworld, player):
|
||||
"Route 22 - Trainer Parties": lambda state: state.has("Oak's Parcel", player),
|
||||
|
||||
# # Rock Tunnel
|
||||
# "Rock Tunnel 1F - PokeManiac": lambda state: logic.rock_tunnel(state, player),
|
||||
# "Rock Tunnel 1F - Hiker 1": lambda state: logic.rock_tunnel(state, player),
|
||||
# "Rock Tunnel 1F - Hiker 2": lambda state: logic.rock_tunnel(state, player),
|
||||
# "Rock Tunnel 1F - Hiker 3": lambda state: logic.rock_tunnel(state, player),
|
||||
# "Rock Tunnel 1F - Jr. Trainer F 1": lambda state: logic.rock_tunnel(state, player),
|
||||
# "Rock Tunnel 1F - Jr. Trainer F 2": lambda state: logic.rock_tunnel(state, player),
|
||||
# "Rock Tunnel 1F - Jr. Trainer F 3": lambda state: logic.rock_tunnel(state, player),
|
||||
# "Rock Tunnel B1F - PokeManiac 1": lambda state: logic.rock_tunnel(state, player),
|
||||
# "Rock Tunnel B1F - PokeManiac 2": lambda state: logic.rock_tunnel(state, player),
|
||||
# "Rock Tunnel B1F - PokeManiac 3": lambda state: logic.rock_tunnel(state, player),
|
||||
# "Rock Tunnel B1F - Jr. Trainer F 1": lambda state: logic.rock_tunnel(state, player),
|
||||
# "Rock Tunnel B1F - Jr. Trainer F 2": lambda state: logic.rock_tunnel(state, player),
|
||||
# "Rock Tunnel B1F - Hiker 1": lambda state: logic.rock_tunnel(state, player),
|
||||
# "Rock Tunnel B1F - Hiker 2": lambda state: logic.rock_tunnel(state, player),
|
||||
# "Rock Tunnel B1F - Hiker 3": lambda state: logic.rock_tunnel(state, player),
|
||||
# "Rock Tunnel B1F - North Item": lambda state: logic.rock_tunnel(state, player),
|
||||
# "Rock Tunnel B1F - Northwest Item": lambda state: logic.rock_tunnel(state, player),
|
||||
# "Rock Tunnel B1F - Southwest Item": lambda state: logic.rock_tunnel(state, player),
|
||||
# "Rock Tunnel B1F - West Item": lambda state: logic.rock_tunnel(state, player),
|
||||
"Rock Tunnel 1F - PokeManiac": lambda state: logic.rock_tunnel(state, player),
|
||||
"Rock Tunnel 1F - Hiker 1": lambda state: logic.rock_tunnel(state, player),
|
||||
"Rock Tunnel 1F - Hiker 2": lambda state: logic.rock_tunnel(state, player),
|
||||
"Rock Tunnel 1F - Hiker 3": lambda state: logic.rock_tunnel(state, player),
|
||||
"Rock Tunnel 1F - Jr. Trainer F 1": lambda state: logic.rock_tunnel(state, player),
|
||||
"Rock Tunnel 1F - Jr. Trainer F 2": lambda state: logic.rock_tunnel(state, player),
|
||||
"Rock Tunnel 1F - Jr. Trainer F 3": lambda state: logic.rock_tunnel(state, player),
|
||||
"Rock Tunnel B1F - PokeManiac 1": lambda state: logic.rock_tunnel(state, player),
|
||||
"Rock Tunnel B1F - PokeManiac 2": lambda state: logic.rock_tunnel(state, player),
|
||||
"Rock Tunnel B1F - PokeManiac 3": lambda state: logic.rock_tunnel(state, player),
|
||||
"Rock Tunnel B1F - Jr. Trainer F 1": lambda state: logic.rock_tunnel(state, player),
|
||||
"Rock Tunnel B1F - Jr. Trainer F 2": lambda state: logic.rock_tunnel(state, player),
|
||||
"Rock Tunnel B1F - Hiker 1": lambda state: logic.rock_tunnel(state, player),
|
||||
"Rock Tunnel B1F - Hiker 2": lambda state: logic.rock_tunnel(state, player),
|
||||
"Rock Tunnel B1F - Hiker 3": lambda state: logic.rock_tunnel(state, player),
|
||||
"Rock Tunnel B1F - North Item": lambda state: logic.rock_tunnel(state, player),
|
||||
"Rock Tunnel B1F - Northwest Item": lambda state: logic.rock_tunnel(state, player),
|
||||
"Rock Tunnel B1F - Southwest Item": lambda state: logic.rock_tunnel(state, player),
|
||||
"Rock Tunnel B1F - West Item": lambda state: logic.rock_tunnel(state, player),
|
||||
|
||||
# Pokédex check
|
||||
"Oak's Lab - Oak's Parcel Reward": lambda state: state.has("Oak's Parcel", player),
|
||||
|
||||
@@ -96,8 +96,7 @@ def set_rules(multiworld: MultiWorld, player: int) -> None:
|
||||
# a long enough run to have enough director credits for scavengers and
|
||||
# help prevent being stuck in the same stages until that point.)
|
||||
|
||||
for location in multiworld.get_locations():
|
||||
if location.player != player: continue # ignore all checks that don't belong to this player
|
||||
for location in multiworld.get_locations(player):
|
||||
if "Scavenger" in location.name:
|
||||
add_rule(location, lambda state: state.has("Stage_5", player))
|
||||
# Regions
|
||||
|
||||
@@ -294,7 +294,7 @@ class SMWorld(World):
|
||||
for src, dest in self.variaRando.randoExec.areaGraph.InterAreaTransitions:
|
||||
src_region = self.multiworld.get_region(src.Name, self.player)
|
||||
dest_region = self.multiworld.get_region(dest.Name, self.player)
|
||||
if ((src.Name + "->" + dest.Name, self.player) not in self.multiworld._entrance_cache):
|
||||
if src.Name + "->" + dest.Name not in self.multiworld.regions.entrance_cache[self.player]:
|
||||
src_region.exits.append(Entrance(self.player, src.Name + "->" + dest.Name, src_region))
|
||||
srcDestEntrance = self.multiworld.get_entrance(src.Name + "->" + dest.Name, self.player)
|
||||
srcDestEntrance.connect(dest_region)
|
||||
@@ -563,8 +563,8 @@ class SMWorld(World):
|
||||
multiWorldItems: List[ByteEdit] = []
|
||||
idx = 0
|
||||
vanillaItemTypesCount = 21
|
||||
for itemLoc in self.multiworld.get_locations():
|
||||
if itemLoc.player == self.player and "Boss" not in locationsDict[itemLoc.name].Class:
|
||||
for itemLoc in self.multiworld.get_locations(self.player):
|
||||
if "Boss" not in locationsDict[itemLoc.name].Class:
|
||||
SMZ3NameToSMType = {
|
||||
"ETank": "ETank", "Missile": "Missile", "Super": "Super", "PowerBomb": "PowerBomb", "Bombs": "Bomb",
|
||||
"Charge": "Charge", "Ice": "Ice", "HiJump": "HiJump", "SpeedBooster": "SpeedBooster",
|
||||
|
||||
@@ -88,6 +88,12 @@ class ExclamationBoxes(Choice):
|
||||
option_Off = 0
|
||||
option_1Ups_Only = 1
|
||||
|
||||
class CompletionType(Choice):
|
||||
"""Set goal for game completion"""
|
||||
display_name = "Completion Goal"
|
||||
option_Last_Bowser_Stage = 0
|
||||
option_All_Bowser_Stages = 1
|
||||
|
||||
|
||||
class ProgressiveKeys(DefaultOnToggle):
|
||||
"""Keys will first grant you access to the Basement, then to the Secound Floor"""
|
||||
@@ -110,4 +116,5 @@ sm64_options: typing.Dict[str, type(Option)] = {
|
||||
"death_link": DeathLink,
|
||||
"BuddyChecks": BuddyChecks,
|
||||
"ExclamationBoxes": ExclamationBoxes,
|
||||
"CompletionType" : CompletionType,
|
||||
}
|
||||
|
||||
@@ -124,4 +124,9 @@ def set_rules(world, player: int, area_connections):
|
||||
add_rule(world.get_location("MIPS 1", player), lambda state: state.can_reach("Basement", 'Region', player) and state.has("Power Star", player, world.MIPS1Cost[player].value))
|
||||
add_rule(world.get_location("MIPS 2", player), lambda state: state.can_reach("Basement", 'Region', player) and state.has("Power Star", player, world.MIPS2Cost[player].value))
|
||||
|
||||
world.completion_condition[player] = lambda state: state.can_reach("Bowser in the Sky", 'Region', player)
|
||||
if world.CompletionType[player] == "last_bowser_stage":
|
||||
world.completion_condition[player] = lambda state: state.can_reach("Bowser in the Sky", 'Region', player)
|
||||
elif world.CompletionType[player] == "all_bowser_stages":
|
||||
world.completion_condition[player] = lambda state: state.can_reach("Bowser in the Dark World", 'Region', player) and \
|
||||
state.can_reach("Bowser in the Fire Sea", 'Region', player) and \
|
||||
state.can_reach("Bowser in the Sky", 'Region', player)
|
||||
|
||||
@@ -154,6 +154,7 @@ class SM64World(World):
|
||||
"MIPS2Cost": self.multiworld.MIPS2Cost[self.player].value,
|
||||
"StarsToFinish": self.multiworld.StarsToFinish[self.player].value,
|
||||
"DeathLink": self.multiworld.death_link[self.player].value,
|
||||
"CompletionType" : self.multiworld.CompletionType[self.player].value,
|
||||
}
|
||||
|
||||
def generate_output(self, output_directory: str):
|
||||
|
||||
@@ -417,7 +417,7 @@ class SoEWorld(World):
|
||||
flags += option.to_flag()
|
||||
|
||||
with open(placement_file, "wb") as f: # generate placement file
|
||||
for location in filter(lambda l: l.player == self.player, self.multiworld.get_locations()):
|
||||
for location in self.multiworld.get_locations(self.player):
|
||||
item = location.item
|
||||
assert item is not None, "Can't handle unfilled location"
|
||||
if item.code is None or location.address is None:
|
||||
|
||||
@@ -21,3 +21,11 @@ class ModNames:
|
||||
ayeisha = "Ayeisha - The Postal Worker (Custom NPC)"
|
||||
riley = "Custom NPC - Riley"
|
||||
skull_cavern_elevator = "Skull Cavern Elevator"
|
||||
|
||||
|
||||
all_mods = frozenset({ModNames.deepwoods, ModNames.tractor, ModNames.big_backpack,
|
||||
ModNames.luck_skill, ModNames.magic, ModNames.socializing_skill, ModNames.archaeology,
|
||||
ModNames.cooking_skill, ModNames.binning_skill, ModNames.juna,
|
||||
ModNames.jasper, ModNames.alec, ModNames.yoba, ModNames.eugene,
|
||||
ModNames.wellwick, ModNames.ginger, ModNames.shiko, ModNames.delores,
|
||||
ModNames.ayeisha, ModNames.riley, ModNames.skull_cavern_elevator})
|
||||
|
||||
@@ -88,6 +88,7 @@ assert true_ is True_()
|
||||
|
||||
class Or(StardewRule):
|
||||
rules: FrozenSet[StardewRule]
|
||||
_simplified: bool
|
||||
|
||||
def __init__(self, rule: Union[StardewRule, Iterable[StardewRule]], *rules: StardewRule):
|
||||
rules_list: Set[StardewRule]
|
||||
@@ -112,6 +113,7 @@ class Or(StardewRule):
|
||||
rules_list = new_rules
|
||||
|
||||
self.rules = frozenset(rules_list)
|
||||
self._simplified = False
|
||||
|
||||
def __call__(self, state: CollectionState) -> bool:
|
||||
return any(rule(state) for rule in self.rules)
|
||||
@@ -139,6 +141,8 @@ class Or(StardewRule):
|
||||
return min(rule.get_difficulty() for rule in self.rules)
|
||||
|
||||
def simplify(self) -> StardewRule:
|
||||
if self._simplified:
|
||||
return self
|
||||
if true_ in self.rules:
|
||||
return true_
|
||||
|
||||
@@ -151,11 +155,14 @@ class Or(StardewRule):
|
||||
if len(simplified_rules) == 1:
|
||||
return simplified_rules[0]
|
||||
|
||||
return Or(simplified_rules)
|
||||
self.rules = frozenset(simplified_rules)
|
||||
self._simplified = True
|
||||
return self
|
||||
|
||||
|
||||
class And(StardewRule):
|
||||
rules: FrozenSet[StardewRule]
|
||||
_simplified: bool
|
||||
|
||||
def __init__(self, rule: Union[StardewRule, Iterable[StardewRule]], *rules: StardewRule):
|
||||
rules_list: Set[StardewRule]
|
||||
@@ -180,6 +187,7 @@ class And(StardewRule):
|
||||
rules_list = new_rules
|
||||
|
||||
self.rules = frozenset(rules_list)
|
||||
self._simplified = False
|
||||
|
||||
def __call__(self, state: CollectionState) -> bool:
|
||||
return all(rule(state) for rule in self.rules)
|
||||
@@ -207,6 +215,8 @@ class And(StardewRule):
|
||||
return max(rule.get_difficulty() for rule in self.rules)
|
||||
|
||||
def simplify(self) -> StardewRule:
|
||||
if self._simplified:
|
||||
return self
|
||||
if false_ in self.rules:
|
||||
return false_
|
||||
|
||||
@@ -219,7 +229,9 @@ class And(StardewRule):
|
||||
if len(simplified_rules) == 1:
|
||||
return simplified_rules[0]
|
||||
|
||||
return And(simplified_rules)
|
||||
self.rules = frozenset(simplified_rules)
|
||||
self._simplified = True
|
||||
return self
|
||||
|
||||
|
||||
class Count(StardewRule):
|
||||
|
||||
@@ -5,40 +5,41 @@ from .. import options
|
||||
class TestBackpackVanilla(SVTestBase):
|
||||
options = {options.BackpackProgression.internal_name: options.BackpackProgression.option_vanilla}
|
||||
|
||||
def test_no_backpack_in_pool(self):
|
||||
item_names = {item.name for item in self.multiworld.get_items()}
|
||||
self.assertNotIn("Progressive Backpack", item_names)
|
||||
def test_no_backpack(self):
|
||||
with self.subTest("no items"):
|
||||
item_names = {item.name for item in self.multiworld.get_items()}
|
||||
self.assertNotIn("Progressive Backpack", item_names)
|
||||
|
||||
def test_no_backpack_locations(self):
|
||||
location_names = {location.name for location in self.multiworld.get_locations()}
|
||||
self.assertNotIn("Large Pack", location_names)
|
||||
self.assertNotIn("Deluxe Pack", location_names)
|
||||
with self.subTest("no locations"):
|
||||
location_names = {location.name for location in self.multiworld.get_locations()}
|
||||
self.assertNotIn("Large Pack", location_names)
|
||||
self.assertNotIn("Deluxe Pack", location_names)
|
||||
|
||||
|
||||
class TestBackpackProgressive(SVTestBase):
|
||||
options = {options.BackpackProgression.internal_name: options.BackpackProgression.option_progressive}
|
||||
|
||||
def test_backpack_is_in_pool_2_times(self):
|
||||
item_names = [item.name for item in self.multiworld.get_items()]
|
||||
self.assertEqual(item_names.count("Progressive Backpack"), 2)
|
||||
def test_backpack(self):
|
||||
with self.subTest(check="has items"):
|
||||
item_names = [item.name for item in self.multiworld.get_items()]
|
||||
self.assertEqual(item_names.count("Progressive Backpack"), 2)
|
||||
|
||||
def test_2_backpack_locations(self):
|
||||
location_names = {location.name for location in self.multiworld.get_locations()}
|
||||
self.assertIn("Large Pack", location_names)
|
||||
self.assertIn("Deluxe Pack", location_names)
|
||||
with self.subTest(check="has locations"):
|
||||
location_names = {location.name for location in self.multiworld.get_locations()}
|
||||
self.assertIn("Large Pack", location_names)
|
||||
self.assertIn("Deluxe Pack", location_names)
|
||||
|
||||
|
||||
class TestBackpackEarlyProgressive(SVTestBase):
|
||||
class TestBackpackEarlyProgressive(TestBackpackProgressive):
|
||||
options = {options.BackpackProgression.internal_name: options.BackpackProgression.option_early_progressive}
|
||||
|
||||
def test_backpack_is_in_pool_2_times(self):
|
||||
item_names = [item.name for item in self.multiworld.get_items()]
|
||||
self.assertEqual(item_names.count("Progressive Backpack"), 2)
|
||||
@property
|
||||
def run_default_tests(self) -> bool:
|
||||
# EarlyProgressive is default
|
||||
return False
|
||||
|
||||
def test_2_backpack_locations(self):
|
||||
location_names = {location.name for location in self.multiworld.get_locations()}
|
||||
self.assertIn("Large Pack", location_names)
|
||||
self.assertIn("Deluxe Pack", location_names)
|
||||
def test_backpack(self):
|
||||
super().test_backpack()
|
||||
|
||||
def test_progressive_backpack_is_in_early_pool(self):
|
||||
self.assertIn("Progressive Backpack", self.multiworld.early_items[1])
|
||||
with self.subTest(check="is early"):
|
||||
self.assertIn("Progressive Backpack", self.multiworld.early_items[1])
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import typing
|
||||
|
||||
from BaseClasses import ItemClassification, MultiWorld
|
||||
from . import setup_solo_multiworld, SVTestBase
|
||||
from . import setup_solo_multiworld, SVTestBase, SVTestCase, allsanity_options_with_mods, \
|
||||
allsanity_options_without_mods, minimal_locations_maximal_items
|
||||
from .. import locations, items, location_table, options
|
||||
from ..data.villagers_data import all_villagers_by_name, all_villagers_by_mod_by_name
|
||||
from ..items import items_by_group, Group
|
||||
@@ -7,11 +10,11 @@ from ..locations import LocationTags
|
||||
from ..mods.mod_data import ModNames
|
||||
|
||||
|
||||
def get_real_locations(tester: SVTestBase, multiworld: MultiWorld):
|
||||
def get_real_locations(tester: typing.Union[SVTestBase, SVTestCase], multiworld: MultiWorld):
|
||||
return [location for location in multiworld.get_locations(tester.player) if not location.event]
|
||||
|
||||
|
||||
def get_real_location_names(tester: SVTestBase, multiworld: MultiWorld):
|
||||
def get_real_location_names(tester: typing.Union[SVTestBase, SVTestCase], multiworld: MultiWorld):
|
||||
return [location.name for location in multiworld.get_locations(tester.player) if not location.event]
|
||||
|
||||
|
||||
@@ -115,21 +118,6 @@ class TestNoGingerIslandItemGeneration(SVTestBase):
|
||||
self.assertTrue(count == 0 or count == 2)
|
||||
|
||||
|
||||
class TestGivenProgressiveBackpack(SVTestBase):
|
||||
options = {options.BackpackProgression.internal_name: options.BackpackProgression.option_progressive}
|
||||
|
||||
def test_when_generate_world_then_two_progressive_backpack_are_added(self):
|
||||
self.assertEqual(self.multiworld.itempool.count(self.world.create_item("Progressive Backpack")), 2)
|
||||
|
||||
def test_when_generate_world_then_backpack_locations_are_added(self):
|
||||
created_locations = {location.name for location in self.multiworld.get_locations(1)}
|
||||
backpacks_exist = [location.name in created_locations
|
||||
for location in locations.locations_by_tag[LocationTags.BACKPACK]
|
||||
if location.name != "Premium Pack"]
|
||||
all_exist = all(backpacks_exist)
|
||||
self.assertTrue(all_exist)
|
||||
|
||||
|
||||
class TestRemixedMineRewards(SVTestBase):
|
||||
def test_when_generate_world_then_one_reward_is_added_per_chest(self):
|
||||
# assert self.world.create_item("Rusty Sword") in self.multiworld.itempool
|
||||
@@ -205,17 +193,17 @@ class TestLocationGeneration(SVTestBase):
|
||||
self.assertIn(location.name, location_table)
|
||||
|
||||
|
||||
class TestLocationAndItemCount(SVTestBase):
|
||||
class TestLocationAndItemCount(SVTestCase):
|
||||
|
||||
def test_minimal_location_maximal_items_still_valid(self):
|
||||
min_max_options = self.minimal_locations_maximal_items()
|
||||
min_max_options = minimal_locations_maximal_items()
|
||||
multiworld = setup_solo_multiworld(min_max_options)
|
||||
valid_locations = get_real_locations(self, multiworld)
|
||||
self.assertGreaterEqual(len(valid_locations), len(multiworld.itempool))
|
||||
|
||||
def test_allsanity_without_mods_has_at_least_locations(self):
|
||||
expected_locations = 994
|
||||
allsanity_options = self.allsanity_options_without_mods()
|
||||
allsanity_options = allsanity_options_without_mods()
|
||||
multiworld = setup_solo_multiworld(allsanity_options)
|
||||
number_locations = len(get_real_locations(self, multiworld))
|
||||
self.assertGreaterEqual(number_locations, expected_locations)
|
||||
@@ -228,7 +216,7 @@ class TestLocationAndItemCount(SVTestBase):
|
||||
|
||||
def test_allsanity_with_mods_has_at_least_locations(self):
|
||||
expected_locations = 1246
|
||||
allsanity_options = self.allsanity_options_with_mods()
|
||||
allsanity_options = allsanity_options_with_mods()
|
||||
multiworld = setup_solo_multiworld(allsanity_options)
|
||||
number_locations = len(get_real_locations(self, multiworld))
|
||||
self.assertGreaterEqual(number_locations, expected_locations)
|
||||
@@ -245,6 +233,11 @@ class TestFriendsanityNone(SVTestBase):
|
||||
options.Friendsanity.internal_name: options.Friendsanity.option_none,
|
||||
}
|
||||
|
||||
@property
|
||||
def run_default_tests(self) -> bool:
|
||||
# None is default
|
||||
return False
|
||||
|
||||
def test_no_friendsanity_items(self):
|
||||
for item in self.multiworld.itempool:
|
||||
self.assertFalse(item.name.endswith(" <3"))
|
||||
@@ -416,6 +409,7 @@ class TestFriendsanityAllNpcsWithMarriage(SVTestBase):
|
||||
self.assertLessEqual(int(hearts), 10)
|
||||
|
||||
|
||||
""" # Assuming math is correct if we check 2 points
|
||||
class TestFriendsanityAllNpcsWithMarriageHeartSize2(SVTestBase):
|
||||
options = {
|
||||
options.Friendsanity.internal_name: options.Friendsanity.option_all_with_marriage,
|
||||
@@ -528,6 +522,7 @@ class TestFriendsanityAllNpcsWithMarriageHeartSize4(SVTestBase):
|
||||
self.assertTrue(hearts == 4 or hearts == 8 or hearts == 12 or hearts == 14)
|
||||
else:
|
||||
self.assertTrue(hearts == 4 or hearts == 8 or hearts == 10)
|
||||
"""
|
||||
|
||||
|
||||
class TestFriendsanityAllNpcsWithMarriageHeartSize5(SVTestBase):
|
||||
|
||||
@@ -6,12 +6,12 @@ import random
|
||||
from typing import Set
|
||||
|
||||
from BaseClasses import ItemClassification, MultiWorld
|
||||
from . import setup_solo_multiworld, SVTestBase
|
||||
from . import setup_solo_multiworld, SVTestCase, allsanity_options_without_mods
|
||||
from .. import ItemData, StardewValleyWorld
|
||||
from ..items import Group, item_table
|
||||
|
||||
|
||||
class TestItems(SVTestBase):
|
||||
class TestItems(SVTestCase):
|
||||
def test_can_create_item_of_resource_pack(self):
|
||||
item_name = "Resource Pack: 500 Money"
|
||||
|
||||
@@ -46,7 +46,7 @@ class TestItems(SVTestBase):
|
||||
|
||||
def test_correct_number_of_stardrops(self):
|
||||
seed = random.randrange(sys.maxsize)
|
||||
allsanity_options = self.allsanity_options_without_mods()
|
||||
allsanity_options = allsanity_options_without_mods()
|
||||
multiworld = setup_solo_multiworld(allsanity_options, seed=seed)
|
||||
stardrop_items = [item for item in multiworld.get_items() if "Stardrop" in item.name]
|
||||
self.assertEqual(len(stardrop_items), 5)
|
||||
|
||||
@@ -1,56 +1,57 @@
|
||||
import unittest
|
||||
from .. import True_
|
||||
from ..logic import Received, Has, False_, And, Or
|
||||
|
||||
|
||||
def test_simplify_true_in_and():
|
||||
rules = {
|
||||
"Wood": True_(),
|
||||
"Rock": True_(),
|
||||
}
|
||||
summer = Received("Summer", 0, 1)
|
||||
assert (Has("Wood", rules) & summer & Has("Rock", rules)).simplify() == summer
|
||||
class TestSimplification(unittest.TestCase):
|
||||
def test_simplify_true_in_and(self):
|
||||
rules = {
|
||||
"Wood": True_(),
|
||||
"Rock": True_(),
|
||||
}
|
||||
summer = Received("Summer", 0, 1)
|
||||
self.assertEqual((Has("Wood", rules) & summer & Has("Rock", rules)).simplify(),
|
||||
summer)
|
||||
|
||||
def test_simplify_false_in_or(self):
|
||||
rules = {
|
||||
"Wood": False_(),
|
||||
"Rock": False_(),
|
||||
}
|
||||
summer = Received("Summer", 0, 1)
|
||||
self.assertEqual((Has("Wood", rules) | summer | Has("Rock", rules)).simplify(),
|
||||
summer)
|
||||
|
||||
def test_simplify_false_in_or():
|
||||
rules = {
|
||||
"Wood": False_(),
|
||||
"Rock": False_(),
|
||||
}
|
||||
summer = Received("Summer", 0, 1)
|
||||
assert (Has("Wood", rules) | summer | Has("Rock", rules)).simplify() == summer
|
||||
def test_simplify_and_in_and(self):
|
||||
rule = And(And(Received('Summer', 0, 1), Received('Fall', 0, 1)),
|
||||
And(Received('Winter', 0, 1), Received('Spring', 0, 1)))
|
||||
self.assertEqual(rule.simplify(),
|
||||
And(Received('Summer', 0, 1), Received('Fall', 0, 1),
|
||||
Received('Winter', 0, 1), Received('Spring', 0, 1)))
|
||||
|
||||
def test_simplify_duplicated_and(self):
|
||||
rule = And(And(Received('Summer', 0, 1), Received('Fall', 0, 1)),
|
||||
And(Received('Summer', 0, 1), Received('Fall', 0, 1)))
|
||||
self.assertEqual(rule.simplify(),
|
||||
And(Received('Summer', 0, 1), Received('Fall', 0, 1)))
|
||||
|
||||
def test_simplify_and_in_and():
|
||||
rule = And(And(Received('Summer', 0, 1), Received('Fall', 0, 1)),
|
||||
And(Received('Winter', 0, 1), Received('Spring', 0, 1)))
|
||||
assert rule.simplify() == And(Received('Summer', 0, 1), Received('Fall', 0, 1), Received('Winter', 0, 1),
|
||||
Received('Spring', 0, 1))
|
||||
def test_simplify_or_in_or(self):
|
||||
rule = Or(Or(Received('Summer', 0, 1), Received('Fall', 0, 1)),
|
||||
Or(Received('Winter', 0, 1), Received('Spring', 0, 1)))
|
||||
self.assertEqual(rule.simplify(),
|
||||
Or(Received('Summer', 0, 1), Received('Fall', 0, 1), Received('Winter', 0, 1),
|
||||
Received('Spring', 0, 1)))
|
||||
|
||||
def test_simplify_duplicated_or(self):
|
||||
rule = And(Or(Received('Summer', 0, 1), Received('Fall', 0, 1)),
|
||||
Or(Received('Summer', 0, 1), Received('Fall', 0, 1)))
|
||||
self.assertEqual(rule.simplify(),
|
||||
Or(Received('Summer', 0, 1), Received('Fall', 0, 1)))
|
||||
|
||||
def test_simplify_duplicated_and():
|
||||
rule = And(And(Received('Summer', 0, 1), Received('Fall', 0, 1)),
|
||||
And(Received('Summer', 0, 1), Received('Fall', 0, 1)))
|
||||
assert rule.simplify() == And(Received('Summer', 0, 1), Received('Fall', 0, 1))
|
||||
def test_simplify_true_in_or(self):
|
||||
rule = Or(True_(), Received('Summer', 0, 1))
|
||||
self.assertEqual(rule.simplify(), True_())
|
||||
|
||||
|
||||
def test_simplify_or_in_or():
|
||||
rule = Or(Or(Received('Summer', 0, 1), Received('Fall', 0, 1)),
|
||||
Or(Received('Winter', 0, 1), Received('Spring', 0, 1)))
|
||||
assert rule.simplify() == Or(Received('Summer', 0, 1), Received('Fall', 0, 1), Received('Winter', 0, 1),
|
||||
Received('Spring', 0, 1))
|
||||
|
||||
|
||||
def test_simplify_duplicated_or():
|
||||
rule = And(Or(Received('Summer', 0, 1), Received('Fall', 0, 1)),
|
||||
Or(Received('Summer', 0, 1), Received('Fall', 0, 1)))
|
||||
assert rule.simplify() == Or(Received('Summer', 0, 1), Received('Fall', 0, 1))
|
||||
|
||||
|
||||
def test_simplify_true_in_or():
|
||||
rule = Or(True_(), Received('Summer', 0, 1))
|
||||
assert rule.simplify() == True_()
|
||||
|
||||
|
||||
def test_simplify_false_in_and():
|
||||
rule = And(False_(), Received('Summer', 0, 1))
|
||||
assert rule.simplify() == False_()
|
||||
def test_simplify_false_in_and(self):
|
||||
rule = And(False_(), Received('Summer', 0, 1))
|
||||
self.assertEqual(rule.simplify(), False_())
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import itertools
|
||||
import unittest
|
||||
from random import random
|
||||
from typing import Dict
|
||||
|
||||
from BaseClasses import ItemClassification, MultiWorld
|
||||
from Options import SpecialRange
|
||||
from . import setup_solo_multiworld, SVTestBase
|
||||
from . import setup_solo_multiworld, SVTestBase, SVTestCase, allsanity_options_without_mods, allsanity_options_with_mods
|
||||
from .. import StardewItem, items_by_group, Group, StardewValleyWorld
|
||||
from ..locations import locations_by_tag, LocationTags, location_table
|
||||
from ..options import ExcludeGingerIsland, ToolProgression, Goal, SeasonRandomization, TrapItems, SpecialOrderLocations, ArcadeMachineLocations
|
||||
@@ -17,21 +18,21 @@ SEASONS = {Season.spring, Season.summer, Season.fall, Season.winter}
|
||||
TOOLS = {"Hoe", "Pickaxe", "Axe", "Watering Can", "Trash Can", "Fishing Rod"}
|
||||
|
||||
|
||||
def assert_can_win(tester: SVTestBase, multiworld: MultiWorld):
|
||||
def assert_can_win(tester: unittest.TestCase, multiworld: MultiWorld):
|
||||
for item in multiworld.get_items():
|
||||
multiworld.state.collect(item)
|
||||
|
||||
tester.assertTrue(multiworld.find_item("Victory", 1).can_reach(multiworld.state))
|
||||
|
||||
|
||||
def basic_checks(tester: SVTestBase, multiworld: MultiWorld):
|
||||
def basic_checks(tester: unittest.TestCase, multiworld: MultiWorld):
|
||||
tester.assertIn(StardewItem("Victory", ItemClassification.progression, None, 1), multiworld.get_items())
|
||||
assert_can_win(tester, multiworld)
|
||||
non_event_locations = [location for location in multiworld.get_locations() if not location.event]
|
||||
tester.assertEqual(len(multiworld.itempool), len(non_event_locations))
|
||||
|
||||
|
||||
def check_no_ginger_island(tester: SVTestBase, multiworld: MultiWorld):
|
||||
def check_no_ginger_island(tester: unittest.TestCase, multiworld: MultiWorld):
|
||||
ginger_island_items = [item_data.name for item_data in items_by_group[Group.GINGER_ISLAND]]
|
||||
ginger_island_locations = [location_data.name for location_data in locations_by_tag[LocationTags.GINGER_ISLAND]]
|
||||
for item in multiworld.get_items():
|
||||
@@ -48,9 +49,9 @@ def get_option_choices(option) -> Dict[str, int]:
|
||||
return {}
|
||||
|
||||
|
||||
class TestGenerateDynamicOptions(SVTestBase):
|
||||
class TestGenerateDynamicOptions(SVTestCase):
|
||||
def test_given_special_range_when_generate_then_basic_checks(self):
|
||||
options = self.world.options_dataclass.type_hints
|
||||
options = StardewValleyWorld.options_dataclass.type_hints
|
||||
for option_name, option in options.items():
|
||||
if not isinstance(option, SpecialRange):
|
||||
continue
|
||||
@@ -62,7 +63,7 @@ class TestGenerateDynamicOptions(SVTestBase):
|
||||
|
||||
def test_given_choice_when_generate_then_basic_checks(self):
|
||||
seed = int(random() * pow(10, 18) - 1)
|
||||
options = self.world.options_dataclass.type_hints
|
||||
options = StardewValleyWorld.options_dataclass.type_hints
|
||||
for option_name, option in options.items():
|
||||
if not option.options:
|
||||
continue
|
||||
@@ -73,7 +74,7 @@ class TestGenerateDynamicOptions(SVTestBase):
|
||||
basic_checks(self, multiworld)
|
||||
|
||||
|
||||
class TestGoal(SVTestBase):
|
||||
class TestGoal(SVTestCase):
|
||||
def test_given_goal_when_generate_then_victory_is_in_correct_location(self):
|
||||
for goal, location in [("community_center", GoalName.community_center),
|
||||
("grandpa_evaluation", GoalName.grandpa_evaluation),
|
||||
@@ -90,7 +91,7 @@ class TestGoal(SVTestBase):
|
||||
self.assertEqual(victory.name, location)
|
||||
|
||||
|
||||
class TestSeasonRandomization(SVTestBase):
|
||||
class TestSeasonRandomization(SVTestCase):
|
||||
def test_given_disabled_when_generate_then_all_seasons_are_precollected(self):
|
||||
world_options = {SeasonRandomization.internal_name: SeasonRandomization.option_disabled}
|
||||
multi_world = setup_solo_multiworld(world_options)
|
||||
@@ -114,7 +115,7 @@ class TestSeasonRandomization(SVTestBase):
|
||||
self.assertEqual(items.count(Season.progressive), 3)
|
||||
|
||||
|
||||
class TestToolProgression(SVTestBase):
|
||||
class TestToolProgression(SVTestCase):
|
||||
def test_given_vanilla_when_generate_then_no_tool_in_pool(self):
|
||||
world_options = {ToolProgression.internal_name: ToolProgression.option_vanilla}
|
||||
multi_world = setup_solo_multiworld(world_options)
|
||||
@@ -147,9 +148,9 @@ class TestToolProgression(SVTestBase):
|
||||
self.assertIn("Purchase Iridium Rod", locations)
|
||||
|
||||
|
||||
class TestGenerateAllOptionsWithExcludeGingerIsland(SVTestBase):
|
||||
class TestGenerateAllOptionsWithExcludeGingerIsland(SVTestCase):
|
||||
def test_given_special_range_when_generate_exclude_ginger_island(self):
|
||||
options = self.world.options_dataclass.type_hints
|
||||
options = StardewValleyWorld.options_dataclass.type_hints
|
||||
for option_name, option in options.items():
|
||||
if not isinstance(option, SpecialRange) or option_name == ExcludeGingerIsland.internal_name:
|
||||
continue
|
||||
@@ -162,7 +163,7 @@ class TestGenerateAllOptionsWithExcludeGingerIsland(SVTestBase):
|
||||
|
||||
def test_given_choice_when_generate_exclude_ginger_island(self):
|
||||
seed = int(random() * pow(10, 18) - 1)
|
||||
options = self.world.options_dataclass.type_hints
|
||||
options = StardewValleyWorld.options_dataclass.type_hints
|
||||
for option_name, option in options.items():
|
||||
if not option.options or option_name == ExcludeGingerIsland.internal_name:
|
||||
continue
|
||||
@@ -191,9 +192,9 @@ class TestGenerateAllOptionsWithExcludeGingerIsland(SVTestBase):
|
||||
basic_checks(self, multiworld)
|
||||
|
||||
|
||||
class TestTraps(SVTestBase):
|
||||
class TestTraps(SVTestCase):
|
||||
def test_given_no_traps_when_generate_then_no_trap_in_pool(self):
|
||||
world_options = self.allsanity_options_without_mods()
|
||||
world_options = allsanity_options_without_mods()
|
||||
world_options.update({TrapItems.internal_name: TrapItems.option_no_traps})
|
||||
multi_world = setup_solo_multiworld(world_options)
|
||||
|
||||
@@ -209,7 +210,7 @@ class TestTraps(SVTestBase):
|
||||
for value in trap_option.options:
|
||||
if value == "no_traps":
|
||||
continue
|
||||
world_options = self.allsanity_options_with_mods()
|
||||
world_options = allsanity_options_with_mods()
|
||||
world_options.update({TrapItems.internal_name: trap_option.options[value]})
|
||||
multi_world = setup_solo_multiworld(world_options)
|
||||
trap_items = [item_data.name for item_data in items_by_group[Group.TRAP] if Group.DEPRECATED not in item_data.groups and item_data.mod_name is None]
|
||||
@@ -219,7 +220,7 @@ class TestTraps(SVTestBase):
|
||||
self.assertIn(item, multiworld_items)
|
||||
|
||||
|
||||
class TestSpecialOrders(SVTestBase):
|
||||
class TestSpecialOrders(SVTestCase):
|
||||
def test_given_disabled_then_no_order_in_pool(self):
|
||||
world_options = {SpecialOrderLocations.internal_name: SpecialOrderLocations.option_disabled}
|
||||
multi_world = setup_solo_multiworld(world_options)
|
||||
|
||||
@@ -2,7 +2,7 @@ import random
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
from . import SVTestBase, setup_solo_multiworld
|
||||
from . import SVTestCase, setup_solo_multiworld
|
||||
from .. import options, StardewValleyWorld, StardewValleyOptions
|
||||
from ..options import EntranceRandomization, ExcludeGingerIsland
|
||||
from ..regions import vanilla_regions, vanilla_connections, randomize_connections, RandomizationFlag
|
||||
@@ -88,7 +88,7 @@ class TestEntranceRando(unittest.TestCase):
|
||||
f"Connections are duplicated in randomization. Seed = {seed}")
|
||||
|
||||
|
||||
class TestEntranceClassifications(SVTestBase):
|
||||
class TestEntranceClassifications(SVTestCase):
|
||||
|
||||
def test_non_progression_are_all_accessible_with_empty_inventory(self):
|
||||
for option, flag in [(options.EntranceRandomization.option_pelican_town, RandomizationFlag.PELICAN_TOWN),
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import os
|
||||
import unittest
|
||||
from argparse import Namespace
|
||||
from typing import Dict, FrozenSet, Tuple, Any, ClassVar
|
||||
|
||||
from BaseClasses import MultiWorld
|
||||
from Utils import cache_argsless
|
||||
from test.TestBase import WorldTestBase
|
||||
from test.general import gen_steps, setup_solo_multiworld as setup_base_solo_multiworld
|
||||
from .. import StardewValleyWorld
|
||||
@@ -13,11 +15,17 @@ from ..options import Cropsanity, SkillProgression, SpecialOrderLocations, Frien
|
||||
BundleRandomization, BundlePrice, FestivalLocations, FriendsanityHeartSize, ExcludeGingerIsland, TrapItems, Goal, Mods
|
||||
|
||||
|
||||
class SVTestBase(WorldTestBase):
|
||||
class SVTestCase(unittest.TestCase):
|
||||
player: ClassVar[int] = 1
|
||||
"""Set to False to not skip some 'extra' tests"""
|
||||
skip_extra_tests: bool = True
|
||||
"""Set to False to run tests that take long"""
|
||||
skip_long_tests: bool = True
|
||||
|
||||
|
||||
class SVTestBase(WorldTestBase, SVTestCase):
|
||||
game = "Stardew Valley"
|
||||
world: StardewValleyWorld
|
||||
player: ClassVar[int] = 1
|
||||
skip_long_tests: bool = True
|
||||
|
||||
def world_setup(self, *args, **kwargs):
|
||||
super().world_setup(*args, **kwargs)
|
||||
@@ -34,66 +42,73 @@ class SVTestBase(WorldTestBase):
|
||||
should_run_default_tests = is_not_stardew_test and super().run_default_tests
|
||||
return should_run_default_tests
|
||||
|
||||
def minimal_locations_maximal_items(self):
|
||||
min_max_options = {
|
||||
SeasonRandomization.internal_name: SeasonRandomization.option_randomized,
|
||||
Cropsanity.internal_name: Cropsanity.option_shuffled,
|
||||
BackpackProgression.internal_name: BackpackProgression.option_vanilla,
|
||||
ToolProgression.internal_name: ToolProgression.option_vanilla,
|
||||
SkillProgression.internal_name: SkillProgression.option_vanilla,
|
||||
BuildingProgression.internal_name: BuildingProgression.option_vanilla,
|
||||
ElevatorProgression.internal_name: ElevatorProgression.option_vanilla,
|
||||
ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_disabled,
|
||||
SpecialOrderLocations.internal_name: SpecialOrderLocations.option_disabled,
|
||||
HelpWantedLocations.internal_name: 0,
|
||||
Fishsanity.internal_name: Fishsanity.option_none,
|
||||
Museumsanity.internal_name: Museumsanity.option_none,
|
||||
Friendsanity.internal_name: Friendsanity.option_none,
|
||||
NumberOfMovementBuffs.internal_name: 12,
|
||||
NumberOfLuckBuffs.internal_name: 12,
|
||||
}
|
||||
return min_max_options
|
||||
|
||||
def allsanity_options_without_mods(self):
|
||||
allsanity = {
|
||||
Goal.internal_name: Goal.option_perfection,
|
||||
BundleRandomization.internal_name: BundleRandomization.option_shuffled,
|
||||
BundlePrice.internal_name: BundlePrice.option_expensive,
|
||||
SeasonRandomization.internal_name: SeasonRandomization.option_randomized,
|
||||
Cropsanity.internal_name: Cropsanity.option_shuffled,
|
||||
BackpackProgression.internal_name: BackpackProgression.option_progressive,
|
||||
ToolProgression.internal_name: ToolProgression.option_progressive,
|
||||
SkillProgression.internal_name: SkillProgression.option_progressive,
|
||||
BuildingProgression.internal_name: BuildingProgression.option_progressive,
|
||||
FestivalLocations.internal_name: FestivalLocations.option_hard,
|
||||
ElevatorProgression.internal_name: ElevatorProgression.option_progressive,
|
||||
ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_full_shuffling,
|
||||
SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi,
|
||||
HelpWantedLocations.internal_name: 56,
|
||||
Fishsanity.internal_name: Fishsanity.option_all,
|
||||
Museumsanity.internal_name: Museumsanity.option_all,
|
||||
Friendsanity.internal_name: Friendsanity.option_all_with_marriage,
|
||||
FriendsanityHeartSize.internal_name: 1,
|
||||
NumberOfMovementBuffs.internal_name: 12,
|
||||
NumberOfLuckBuffs.internal_name: 12,
|
||||
ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false,
|
||||
TrapItems.internal_name: TrapItems.option_nightmare,
|
||||
}
|
||||
return allsanity
|
||||
@cache_argsless
|
||||
def minimal_locations_maximal_items():
|
||||
min_max_options = {
|
||||
SeasonRandomization.internal_name: SeasonRandomization.option_randomized,
|
||||
Cropsanity.internal_name: Cropsanity.option_shuffled,
|
||||
BackpackProgression.internal_name: BackpackProgression.option_vanilla,
|
||||
ToolProgression.internal_name: ToolProgression.option_vanilla,
|
||||
SkillProgression.internal_name: SkillProgression.option_vanilla,
|
||||
BuildingProgression.internal_name: BuildingProgression.option_vanilla,
|
||||
ElevatorProgression.internal_name: ElevatorProgression.option_vanilla,
|
||||
ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_disabled,
|
||||
SpecialOrderLocations.internal_name: SpecialOrderLocations.option_disabled,
|
||||
HelpWantedLocations.internal_name: 0,
|
||||
Fishsanity.internal_name: Fishsanity.option_none,
|
||||
Museumsanity.internal_name: Museumsanity.option_none,
|
||||
Friendsanity.internal_name: Friendsanity.option_none,
|
||||
NumberOfMovementBuffs.internal_name: 12,
|
||||
NumberOfLuckBuffs.internal_name: 12,
|
||||
}
|
||||
return min_max_options
|
||||
|
||||
|
||||
@cache_argsless
|
||||
def allsanity_options_without_mods():
|
||||
allsanity = {
|
||||
Goal.internal_name: Goal.option_perfection,
|
||||
BundleRandomization.internal_name: BundleRandomization.option_shuffled,
|
||||
BundlePrice.internal_name: BundlePrice.option_expensive,
|
||||
SeasonRandomization.internal_name: SeasonRandomization.option_randomized,
|
||||
Cropsanity.internal_name: Cropsanity.option_shuffled,
|
||||
BackpackProgression.internal_name: BackpackProgression.option_progressive,
|
||||
ToolProgression.internal_name: ToolProgression.option_progressive,
|
||||
SkillProgression.internal_name: SkillProgression.option_progressive,
|
||||
BuildingProgression.internal_name: BuildingProgression.option_progressive,
|
||||
FestivalLocations.internal_name: FestivalLocations.option_hard,
|
||||
ElevatorProgression.internal_name: ElevatorProgression.option_progressive,
|
||||
ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_full_shuffling,
|
||||
SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi,
|
||||
HelpWantedLocations.internal_name: 56,
|
||||
Fishsanity.internal_name: Fishsanity.option_all,
|
||||
Museumsanity.internal_name: Museumsanity.option_all,
|
||||
Friendsanity.internal_name: Friendsanity.option_all_with_marriage,
|
||||
FriendsanityHeartSize.internal_name: 1,
|
||||
NumberOfMovementBuffs.internal_name: 12,
|
||||
NumberOfLuckBuffs.internal_name: 12,
|
||||
ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false,
|
||||
TrapItems.internal_name: TrapItems.option_nightmare,
|
||||
}
|
||||
return allsanity
|
||||
|
||||
|
||||
@cache_argsless
|
||||
def allsanity_options_with_mods():
|
||||
allsanity = {}
|
||||
allsanity.update(allsanity_options_without_mods())
|
||||
all_mods = (
|
||||
ModNames.deepwoods, ModNames.tractor, ModNames.big_backpack,
|
||||
ModNames.luck_skill, ModNames.magic, ModNames.socializing_skill, ModNames.archaeology,
|
||||
ModNames.cooking_skill, ModNames.binning_skill, ModNames.juna,
|
||||
ModNames.jasper, ModNames.alec, ModNames.yoba, ModNames.eugene,
|
||||
ModNames.wellwick, ModNames.ginger, ModNames.shiko, ModNames.delores,
|
||||
ModNames.ayeisha, ModNames.riley, ModNames.skull_cavern_elevator
|
||||
)
|
||||
allsanity.update({Mods.internal_name: all_mods})
|
||||
return allsanity
|
||||
|
||||
def allsanity_options_with_mods(self):
|
||||
allsanity = {}
|
||||
allsanity.update(self.allsanity_options_without_mods())
|
||||
all_mods = (
|
||||
ModNames.deepwoods, ModNames.tractor, ModNames.big_backpack,
|
||||
ModNames.luck_skill, ModNames.magic, ModNames.socializing_skill, ModNames.archaeology,
|
||||
ModNames.cooking_skill, ModNames.binning_skill, ModNames.juna,
|
||||
ModNames.jasper, ModNames.alec, ModNames.yoba, ModNames.eugene,
|
||||
ModNames.wellwick, ModNames.ginger, ModNames.shiko, ModNames.delores,
|
||||
ModNames.ayeisha, ModNames.riley, ModNames.skull_cavern_elevator
|
||||
)
|
||||
allsanity.update({Mods.internal_name: all_mods})
|
||||
return allsanity
|
||||
|
||||
pre_generated_worlds = {}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import unittest
|
||||
from typing import List
|
||||
|
||||
from BaseClasses import MultiWorld, ItemClassification
|
||||
from ... import StardewItem
|
||||
from .. import SVTestBase
|
||||
|
||||
|
||||
def get_all_item_names(multiworld: MultiWorld) -> List[str]:
|
||||
@@ -13,21 +13,21 @@ def get_all_location_names(multiworld: MultiWorld) -> List[str]:
|
||||
return [location.name for location in multiworld.get_locations() if not location.event]
|
||||
|
||||
|
||||
def assert_victory_exists(tester: SVTestBase, multiworld: MultiWorld):
|
||||
def assert_victory_exists(tester: unittest.TestCase, multiworld: MultiWorld):
|
||||
tester.assertIn(StardewItem("Victory", ItemClassification.progression, None, 1), multiworld.get_items())
|
||||
|
||||
|
||||
def collect_all_then_assert_can_win(tester: SVTestBase, multiworld: MultiWorld):
|
||||
def collect_all_then_assert_can_win(tester: unittest.TestCase, multiworld: MultiWorld):
|
||||
for item in multiworld.get_items():
|
||||
multiworld.state.collect(item)
|
||||
tester.assertTrue(multiworld.find_item("Victory", 1).can_reach(multiworld.state))
|
||||
|
||||
|
||||
def assert_can_win(tester: SVTestBase, multiworld: MultiWorld):
|
||||
def assert_can_win(tester: unittest.TestCase, multiworld: MultiWorld):
|
||||
assert_victory_exists(tester, multiworld)
|
||||
collect_all_then_assert_can_win(tester, multiworld)
|
||||
|
||||
|
||||
def assert_same_number_items_locations(tester: SVTestBase, multiworld: MultiWorld):
|
||||
def assert_same_number_items_locations(tester: unittest.TestCase, multiworld: MultiWorld):
|
||||
non_event_locations = [location for location in multiworld.get_locations() if not location.event]
|
||||
tester.assertEqual(len(multiworld.itempool), len(non_event_locations))
|
||||
@@ -1,23 +1,17 @@
|
||||
import unittest
|
||||
from typing import List, Union
|
||||
|
||||
from BaseClasses import MultiWorld
|
||||
from worlds.stardew_valley.mods.mod_data import ModNames
|
||||
from worlds.stardew_valley.mods.mod_data import all_mods
|
||||
from worlds.stardew_valley.test import setup_solo_multiworld
|
||||
from worlds.stardew_valley.test.TestOptions import basic_checks, SVTestBase
|
||||
from worlds.stardew_valley.test.TestOptions import basic_checks, SVTestCase
|
||||
from worlds.stardew_valley.items import item_table
|
||||
from worlds.stardew_valley.locations import location_table
|
||||
from worlds.stardew_valley.options import Mods
|
||||
from .option_names import options_to_include
|
||||
|
||||
all_mods = frozenset({ModNames.deepwoods, ModNames.tractor, ModNames.big_backpack,
|
||||
ModNames.luck_skill, ModNames.magic, ModNames.socializing_skill, ModNames.archaeology,
|
||||
ModNames.cooking_skill, ModNames.binning_skill, ModNames.juna,
|
||||
ModNames.jasper, ModNames.alec, ModNames.yoba, ModNames.eugene,
|
||||
ModNames.wellwick, ModNames.ginger, ModNames.shiko, ModNames.delores,
|
||||
ModNames.ayeisha, ModNames.riley, ModNames.skull_cavern_elevator})
|
||||
|
||||
|
||||
def check_stray_mod_items(chosen_mods: Union[List[str], str], tester: SVTestBase, multiworld: MultiWorld):
|
||||
def check_stray_mod_items(chosen_mods: Union[List[str], str], tester: unittest.TestCase, multiworld: MultiWorld):
|
||||
if isinstance(chosen_mods, str):
|
||||
chosen_mods = [chosen_mods]
|
||||
for multiworld_item in multiworld.get_items():
|
||||
@@ -30,7 +24,7 @@ def check_stray_mod_items(chosen_mods: Union[List[str], str], tester: SVTestBase
|
||||
tester.assertTrue(location.mod_name is None or location.mod_name in chosen_mods)
|
||||
|
||||
|
||||
class TestGenerateModsOptions(SVTestBase):
|
||||
class TestGenerateModsOptions(SVTestCase):
|
||||
|
||||
def test_given_mod_pairs_when_generate_then_basic_checks(self):
|
||||
if self.skip_long_tests:
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import unittest
|
||||
from typing import Dict
|
||||
|
||||
from BaseClasses import MultiWorld
|
||||
from Options import SpecialRange
|
||||
from .option_names import options_to_include
|
||||
from worlds.stardew_valley.test.checks.world_checks import assert_can_win, assert_same_number_items_locations
|
||||
from .. import setup_solo_multiworld, SVTestBase
|
||||
from .. import setup_solo_multiworld, SVTestCase
|
||||
|
||||
|
||||
def basic_checks(tester: SVTestBase, multiworld: MultiWorld):
|
||||
def basic_checks(tester: unittest.TestCase, multiworld: MultiWorld):
|
||||
assert_can_win(tester, multiworld)
|
||||
assert_same_number_items_locations(tester, multiworld)
|
||||
|
||||
@@ -20,7 +21,7 @@ def get_option_choices(option) -> Dict[str, int]:
|
||||
return {}
|
||||
|
||||
|
||||
class TestGenerateDynamicOptions(SVTestBase):
|
||||
class TestGenerateDynamicOptions(SVTestCase):
|
||||
def test_given_option_pair_when_generate_then_basic_checks(self):
|
||||
if self.skip_long_tests:
|
||||
return
|
||||
|
||||
@@ -4,7 +4,7 @@ import random
|
||||
from BaseClasses import MultiWorld
|
||||
from Options import SpecialRange, Range
|
||||
from .option_names import options_to_include
|
||||
from .. import setup_solo_multiworld, SVTestBase
|
||||
from .. import setup_solo_multiworld, SVTestCase
|
||||
from ..checks.goal_checks import assert_perfection_world_is_valid, assert_goal_world_is_valid
|
||||
from ..checks.option_checks import assert_can_reach_island_if_should, assert_cropsanity_same_number_items_and_locations, \
|
||||
assert_festivals_give_access_to_deluxe_scarecrow
|
||||
@@ -72,14 +72,14 @@ def generate_many_worlds(number_worlds: int, start_index: int) -> Dict[int, Mult
|
||||
return multiworlds
|
||||
|
||||
|
||||
def check_every_multiworld_is_valid(tester: SVTestBase, multiworlds: Dict[int, MultiWorld]):
|
||||
def check_every_multiworld_is_valid(tester: SVTestCase, multiworlds: Dict[int, MultiWorld]):
|
||||
for multiworld_id in multiworlds:
|
||||
multiworld = multiworlds[multiworld_id]
|
||||
with tester.subTest(f"Checking validity of world {multiworld_id}"):
|
||||
check_multiworld_is_valid(tester, multiworld_id, multiworld)
|
||||
|
||||
|
||||
def check_multiworld_is_valid(tester: SVTestBase, multiworld_id: int, multiworld: MultiWorld):
|
||||
def check_multiworld_is_valid(tester: SVTestCase, multiworld_id: int, multiworld: MultiWorld):
|
||||
assert_victory_exists(tester, multiworld)
|
||||
assert_same_number_items_locations(tester, multiworld)
|
||||
assert_goal_world_is_valid(tester, multiworld)
|
||||
@@ -88,7 +88,7 @@ def check_multiworld_is_valid(tester: SVTestBase, multiworld_id: int, multiworld
|
||||
assert_festivals_give_access_to_deluxe_scarecrow(tester, multiworld)
|
||||
|
||||
|
||||
class TestGenerateManyWorlds(SVTestBase):
|
||||
class TestGenerateManyWorlds(SVTestCase):
|
||||
def test_generate_many_worlds_then_check_results(self):
|
||||
if self.skip_long_tests:
|
||||
return
|
||||
|
||||
@@ -7,45 +7,40 @@ class TestBiggerBackpackVanilla(SVTestBase):
|
||||
options = {options.BackpackProgression.internal_name: options.BackpackProgression.option_vanilla,
|
||||
options.Mods.internal_name: ModNames.big_backpack}
|
||||
|
||||
def test_no_backpack_in_pool(self):
|
||||
item_names = {item.name for item in self.multiworld.get_items()}
|
||||
self.assertNotIn("Progressive Backpack", item_names)
|
||||
def test_no_backpack(self):
|
||||
with self.subTest(check="no items"):
|
||||
item_names = {item.name for item in self.multiworld.get_items()}
|
||||
self.assertNotIn("Progressive Backpack", item_names)
|
||||
|
||||
def test_no_backpack_locations(self):
|
||||
location_names = {location.name for location in self.multiworld.get_locations()}
|
||||
self.assertNotIn("Large Pack", location_names)
|
||||
self.assertNotIn("Deluxe Pack", location_names)
|
||||
self.assertNotIn("Premium Pack", location_names)
|
||||
with self.subTest(check="no locations"):
|
||||
location_names = {location.name for location in self.multiworld.get_locations()}
|
||||
self.assertNotIn("Large Pack", location_names)
|
||||
self.assertNotIn("Deluxe Pack", location_names)
|
||||
self.assertNotIn("Premium Pack", location_names)
|
||||
|
||||
|
||||
class TestBiggerBackpackProgressive(SVTestBase):
|
||||
options = {options.BackpackProgression.internal_name: options.BackpackProgression.option_progressive,
|
||||
options.Mods.internal_name: ModNames.big_backpack}
|
||||
|
||||
def test_backpack_is_in_pool_3_times(self):
|
||||
item_names = [item.name for item in self.multiworld.get_items()]
|
||||
self.assertEqual(item_names.count("Progressive Backpack"), 3)
|
||||
def test_backpack(self):
|
||||
with self.subTest(check="has items"):
|
||||
item_names = [item.name for item in self.multiworld.get_items()]
|
||||
self.assertEqual(item_names.count("Progressive Backpack"), 3)
|
||||
|
||||
def test_3_backpack_locations(self):
|
||||
location_names = {location.name for location in self.multiworld.get_locations()}
|
||||
self.assertIn("Large Pack", location_names)
|
||||
self.assertIn("Deluxe Pack", location_names)
|
||||
self.assertIn("Premium Pack", location_names)
|
||||
with self.subTest(check="has locations"):
|
||||
location_names = {location.name for location in self.multiworld.get_locations()}
|
||||
self.assertIn("Large Pack", location_names)
|
||||
self.assertIn("Deluxe Pack", location_names)
|
||||
self.assertIn("Premium Pack", location_names)
|
||||
|
||||
|
||||
class TestBiggerBackpackEarlyProgressive(SVTestBase):
|
||||
class TestBiggerBackpackEarlyProgressive(TestBiggerBackpackProgressive):
|
||||
options = {options.BackpackProgression.internal_name: options.BackpackProgression.option_early_progressive,
|
||||
options.Mods.internal_name: ModNames.big_backpack}
|
||||
|
||||
def test_backpack_is_in_pool_3_times(self):
|
||||
item_names = [item.name for item in self.multiworld.get_items()]
|
||||
self.assertEqual(item_names.count("Progressive Backpack"), 3)
|
||||
def test_backpack(self):
|
||||
super().test_backpack()
|
||||
|
||||
def test_3_backpack_locations(self):
|
||||
location_names = {location.name for location in self.multiworld.get_locations()}
|
||||
self.assertIn("Large Pack", location_names)
|
||||
self.assertIn("Deluxe Pack", location_names)
|
||||
self.assertIn("Premium Pack", location_names)
|
||||
|
||||
def test_progressive_backpack_is_in_early_pool(self):
|
||||
self.assertIn("Progressive Backpack", self.multiworld.early_items[1])
|
||||
with self.subTest(check="is early"):
|
||||
self.assertIn("Progressive Backpack", self.multiworld.early_items[1])
|
||||
|
||||
@@ -4,24 +4,17 @@ import random
|
||||
import sys
|
||||
|
||||
from BaseClasses import MultiWorld
|
||||
from ...mods.mod_data import ModNames
|
||||
from .. import setup_solo_multiworld
|
||||
from ..TestOptions import basic_checks, SVTestBase
|
||||
from ...mods.mod_data import all_mods
|
||||
from .. import setup_solo_multiworld, SVTestBase, SVTestCase, allsanity_options_without_mods
|
||||
from ..TestOptions import basic_checks
|
||||
from ... import items, Group, ItemClassification
|
||||
from ...regions import RandomizationFlag, create_final_connections, randomize_connections, create_final_regions
|
||||
from ...items import item_table, items_by_group
|
||||
from ...locations import location_table
|
||||
from ...options import Mods, EntranceRandomization, Friendsanity, SeasonRandomization, SpecialOrderLocations, ExcludeGingerIsland, TrapItems
|
||||
|
||||
all_mods = frozenset({ModNames.deepwoods, ModNames.tractor, ModNames.big_backpack,
|
||||
ModNames.luck_skill, ModNames.magic, ModNames.socializing_skill, ModNames.archaeology,
|
||||
ModNames.cooking_skill, ModNames.binning_skill, ModNames.juna,
|
||||
ModNames.jasper, ModNames.alec, ModNames.yoba, ModNames.eugene,
|
||||
ModNames.wellwick, ModNames.ginger, ModNames.shiko, ModNames.delores,
|
||||
ModNames.ayeisha, ModNames.riley, ModNames.skull_cavern_elevator})
|
||||
|
||||
|
||||
def check_stray_mod_items(chosen_mods: Union[List[str], str], tester: SVTestBase, multiworld: MultiWorld):
|
||||
def check_stray_mod_items(chosen_mods: Union[List[str], str], tester: unittest.TestCase, multiworld: MultiWorld):
|
||||
if isinstance(chosen_mods, str):
|
||||
chosen_mods = [chosen_mods]
|
||||
for multiworld_item in multiworld.get_items():
|
||||
@@ -34,7 +27,7 @@ def check_stray_mod_items(chosen_mods: Union[List[str], str], tester: SVTestBase
|
||||
tester.assertTrue(location.mod_name is None or location.mod_name in chosen_mods)
|
||||
|
||||
|
||||
class TestGenerateModsOptions(SVTestBase):
|
||||
class TestGenerateModsOptions(SVTestCase):
|
||||
|
||||
def test_given_single_mods_when_generate_then_basic_checks(self):
|
||||
for mod in all_mods:
|
||||
@@ -50,6 +43,8 @@ class TestGenerateModsOptions(SVTestBase):
|
||||
multiworld = setup_solo_multiworld({EntranceRandomization.internal_name: option, Mods: mod})
|
||||
basic_checks(self, multiworld)
|
||||
check_stray_mod_items(mod, self, multiworld)
|
||||
if self.skip_extra_tests:
|
||||
return # assume the rest will work as well
|
||||
|
||||
|
||||
class TestBaseItemGeneration(SVTestBase):
|
||||
@@ -103,7 +98,7 @@ class TestNoGingerIslandModItemGeneration(SVTestBase):
|
||||
self.assertIn(progression_item.name, all_created_items)
|
||||
|
||||
|
||||
class TestModEntranceRando(unittest.TestCase):
|
||||
class TestModEntranceRando(SVTestCase):
|
||||
|
||||
def test_mod_entrance_randomization(self):
|
||||
|
||||
@@ -137,12 +132,12 @@ class TestModEntranceRando(unittest.TestCase):
|
||||
f"Connections are duplicated in randomization. Seed = {seed}")
|
||||
|
||||
|
||||
class TestModTraps(SVTestBase):
|
||||
class TestModTraps(SVTestCase):
|
||||
def test_given_traps_when_generate_then_all_traps_in_pool(self):
|
||||
for value in TrapItems.options:
|
||||
if value == "no_traps":
|
||||
continue
|
||||
world_options = self.allsanity_options_without_mods()
|
||||
world_options = allsanity_options_without_mods()
|
||||
world_options.update({TrapItems.internal_name: TrapItems.options[value], Mods: "Magic"})
|
||||
multi_world = setup_solo_multiworld(world_options)
|
||||
trap_items = [item_data.name for item_data in items_by_group[Group.TRAP] if Group.DEPRECATED not in item_data.groups]
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
- Bundled with Archipelago: [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases)
|
||||
- The BizHawk emulator. Versions 2.3.1 and higher are supported.
|
||||
- [BizHawk at TASVideos](https://tasvideos.org/BizHawk)
|
||||
- Your legally acquired US v1.0 PRG0 ROM file, probably named `Legend of Zelda, The (U) (PRG0) [!].nes`
|
||||
|
||||
## Optional Software
|
||||
|
||||
|
||||
@@ -193,7 +193,7 @@ class UndertaleWorld(World):
|
||||
def create_regions(self):
|
||||
def UndertaleRegion(region_name: str, exits=[]):
|
||||
ret = Region(region_name, self.player, self.multiworld)
|
||||
ret.locations = [UndertaleAdvancement(self.player, loc_name, loc_data.id, ret)
|
||||
ret.locations += [UndertaleAdvancement(self.player, loc_name, loc_data.id, ret)
|
||||
for loc_name, loc_data in advancement_table.items()
|
||||
if loc_data.region == region_name and
|
||||
(loc_name not in exclusion_table["NoStats"] or
|
||||
|
||||
@@ -228,8 +228,8 @@ def make_hints(multiworld: MultiWorld, player: int, hint_amount: int):
|
||||
if item.player == player and item.code and item.advancement
|
||||
}
|
||||
loc_in_this_world = {
|
||||
location.name for location in multiworld.get_locations()
|
||||
if location.player == player and location.address
|
||||
location.name for location in multiworld.get_locations(player)
|
||||
if location.address
|
||||
}
|
||||
|
||||
always_locations = [
|
||||
|
||||
@@ -329,23 +329,22 @@ class ZillionWorld(World):
|
||||
empty = zz_items[4]
|
||||
multi_item = empty # a different patcher method differentiates empty from ap multi item
|
||||
multi_items: Dict[str, Tuple[str, str]] = {} # zz_loc_name to (item_name, player_name)
|
||||
for loc in self.multiworld.get_locations():
|
||||
if loc.player == self.player:
|
||||
z_loc = cast(ZillionLocation, loc)
|
||||
# debug_zz_loc_ids[z_loc.zz_loc.name] = id(z_loc.zz_loc)
|
||||
if z_loc.item is None:
|
||||
self.logger.warn("generate_output location has no item - is that ok?")
|
||||
z_loc.zz_loc.item = empty
|
||||
elif z_loc.item.player == self.player:
|
||||
z_item = cast(ZillionItem, z_loc.item)
|
||||
z_loc.zz_loc.item = z_item.zz_item
|
||||
else: # another player's item
|
||||
# print(f"put multi item in {z_loc.zz_loc.name}")
|
||||
z_loc.zz_loc.item = multi_item
|
||||
multi_items[z_loc.zz_loc.name] = (
|
||||
z_loc.item.name,
|
||||
self.multiworld.get_player_name(z_loc.item.player)
|
||||
)
|
||||
for loc in self.multiworld.get_locations(self.player):
|
||||
z_loc = cast(ZillionLocation, loc)
|
||||
# debug_zz_loc_ids[z_loc.zz_loc.name] = id(z_loc.zz_loc)
|
||||
if z_loc.item is None:
|
||||
self.logger.warn("generate_output location has no item - is that ok?")
|
||||
z_loc.zz_loc.item = empty
|
||||
elif z_loc.item.player == self.player:
|
||||
z_item = cast(ZillionItem, z_loc.item)
|
||||
z_loc.zz_loc.item = z_item.zz_item
|
||||
else: # another player's item
|
||||
# print(f"put multi item in {z_loc.zz_loc.name}")
|
||||
z_loc.zz_loc.item = multi_item
|
||||
multi_items[z_loc.zz_loc.name] = (
|
||||
z_loc.item.name,
|
||||
self.multiworld.get_player_name(z_loc.item.player)
|
||||
)
|
||||
# debug_zz_loc_ids.sort()
|
||||
# for name, id_ in debug_zz_loc_ids.items():
|
||||
# print(id_)
|
||||
|
||||
Reference in New Issue
Block a user