Archipelago/BaseClasses.py

1622 lines
72 KiB
Python
Raw Normal View History

2020-03-02 23:12:14 +00:00
from __future__ import annotations
import collections
2021-02-20 01:30:55 +00:00
import functools
import logging
import random
import secrets
from argparse import Namespace
from collections import Counter, deque
from collections.abc import Collection, MutableSequence
from enum import IntEnum, IntFlag
from typing import (AbstractSet, Any, Callable, ClassVar, Dict, Iterable, Iterator, List, Mapping, NamedTuple,
Optional, Protocol, Set, Tuple, Union, TYPE_CHECKING)
from typing_extensions import NotRequired, TypedDict
import NetUtils
import Options
import Utils
if TYPE_CHECKING:
from worlds import AutoWorld
class Group(TypedDict):
name: str
game: str
world: "AutoWorld.World"
players: AbstractSet[int]
item_pool: NotRequired[Set[str]]
replacement_items: NotRequired[Dict[int, Optional[str]]]
local_items: NotRequired[Set[str]]
non_local_items: NotRequired[Set[str]]
link_replacement: NotRequired[bool]
class ThreadBarrierProxy:
"""Passes through getattr while passthrough is True"""
def __init__(self, obj: object) -> None:
self.passthrough = True
self.obj = obj
def __getattr__(self, name: str) -> Any:
if self.passthrough:
return getattr(self.obj, name)
else:
raise RuntimeError("You are in a threaded context and global random state was removed for your safety. "
"Please use multiworld.per_slot_randoms[player] or randomize ahead of output.")
class HasNameAndPlayer(Protocol):
name: str
player: int
2020-10-24 03:38:56 +00:00
class MultiWorld():
2020-04-10 18:54:18 +00:00
debug_types = False
player_name: Dict[int, str]
2021-01-02 21:41:03 +00:00
plando_texts: List[Dict[str, str]]
plando_items: List[List[Dict[str, Any]]]
plando_connections: List
worlds: Dict[int, "AutoWorld.World"]
groups: Dict[int, Group]
regions: RegionManager
itempool: List[Item]
2021-07-20 19:19:53 +00:00
is_race: bool = False
precollected_items: Dict[int, List[Item]]
state: CollectionState
plando_options: PlandoOptions
early_items: Dict[int, Dict[str, int]]
local_early_items: Dict[int, Dict[str, int]]
local_items: Dict[int, Options.LocalItems]
non_local_items: Dict[int, Options.NonLocalItems]
progression_balancing: Dict[int, Options.ProgressionBalancing]
completion_condition: Dict[int, Callable[[CollectionState], bool]]
indirect_connections: Dict[Region, Set[Entrance]]
exclude_locations: Dict[int, Options.ExcludeLocations]
priority_locations: Dict[int, Options.PriorityLocations]
start_inventory: Dict[int, Options.StartInventory]
start_hints: Dict[int, Options.StartHints]
start_location_hints: Dict[int, Options.StartLocationHints]
item_links: Dict[int, Options.ItemLinks]
game: Dict[int, str]
random: random.Random
per_slot_randoms: Utils.DeprecateDict[int, random.Random]
"""Deprecated. Please use `self.random` instead."""
class AttributeProxy():
def __init__(self, rule):
self.rule = rule
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):
assert region.name not in self.region_cache[region.player], \
f"{region.name} already exists in region cache."
self.region_cache[region.player][region.name] = region
def extend(self, regions: Iterable[Region]):
for region in regions:
assert region.name not in self.region_cache[region.player], \
f"{region.name} already exists in region cache."
self.region_cache[region.player][region.name] = region
def add_group(self, new_id: int):
self.region_cache[new_id] = {}
self.entrance_cache[new_id] = {}
self.location_cache[new_id] = {}
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())
2021-03-14 07:38:02 +00:00
def __init__(self, players: int):
# world-local random state is saved for multiple generations running concurrently
self.random = ThreadBarrierProxy(random.Random())
self.players = players
2022-01-30 12:57:12 +00:00
self.player_types = {player: NetUtils.SlotType.player for player in self.player_ids}
2021-03-14 07:38:02 +00:00
self.algorithm = 'balanced'
self.groups = {}
self.regions = self.RegionManager(players)
self.shops = []
self.itempool = []
2017-05-20 12:03:15 +00:00
self.seed = None
self.seed_name: str = "Unavailable"
self.precollected_items = {player: [] for player in self.player_ids}
self.required_locations = []
2017-06-03 19:27:34 +00:00
self.light_world_light_cone = False
2017-06-03 13:46:05 +00:00
self.dark_world_light_cone = False
self.rupoor_cost = 10
self.aga_randomness = True
2018-09-23 02:51:54 +00:00
self.save_and_quit_from_boss = True
2021-03-14 07:38:02 +00:00
self.custom = False
self.customitemarray = []
self.shuffle_ganon = True
self.spoiler = Spoiler(self)
self.early_items = {player: {} for player in self.player_ids}
self.local_early_items = {player: {} for player in self.player_ids}
self.indirect_connections = {}
self.start_inventory_from_pool: Dict[int, Options.StartInventoryPool] = {}
for player in range(1, players + 1):
def set_player_attr(attr: str, val) -> None:
self.__dict__.setdefault(attr, {})[player] = val
2021-01-02 11:49:43 +00:00
set_player_attr('plando_items', [])
2021-01-02 15:44:58 +00:00
set_player_attr('plando_texts', {})
2021-01-02 21:41:03 +00:00
set_player_attr('plando_connections', [])
set_player_attr('game', "Archipelago")
2021-02-22 10:18:53 +00:00
set_player_attr('completion_condition', lambda state: True)
self.worlds = {}
self.per_slot_randoms = Utils.DeprecateDict("Using per_slot_randoms is now deprecated. Please use the "
"world's random object instead (usually self.random)")
self.plando_options = PlandoOptions.none
def get_all_ids(self) -> Tuple[int, ...]:
return self.player_ids + tuple(self.groups)
def add_group(self, name: str, game: str, players: AbstractSet[int] = frozenset()) -> Tuple[int, Group]:
"""Create a group with name and return the assigned player ID and group.
If a group of this name already exists, the set of players is extended instead of creating a new one."""
from worlds import AutoWorld
for group_id, group in self.groups.items():
if group["name"] == name:
group["players"] |= players
return group_id, group
new_id: int = self.players + len(self.groups) + 1
2022-02-18 19:29:35 +00:00
self.regions.add_group(new_id)
self.game[new_id] = game
self.player_types[new_id] = NetUtils.SlotType.group
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 = AutoWorld.World.collect_item.__get__(self.worlds[new_id])
self.worlds[new_id].collect = AutoWorld.World.collect.__get__(self.worlds[new_id])
self.worlds[new_id].remove = AutoWorld.World.remove.__get__(self.worlds[new_id])
self.player_name[new_id] = name
new_group = self.groups[new_id] = Group(name=name, game=game, players=players,
world=self.worlds[new_id])
return new_id, new_group
def get_player_groups(self, player: int) -> Set[int]:
return {group_id for group_id, group in self.groups.items() if player in group["players"]}
def set_seed(self, seed: Optional[int] = None, secure: bool = False, name: Optional[str] = None):
assert not self.worlds, "seed needs to be initialized before Worlds"
self.seed = get_seed(seed)
if secure:
self.secure()
else:
self.random.seed(self.seed)
self.seed_name = name if name else str(self.seed)
2021-06-11 16:02:48 +00:00
def set_options(self, args: Namespace) -> None:
# TODO - remove this section once all worlds use options dataclasses
from worlds import AutoWorld
all_keys: Set[str] = {key for player in self.player_ids for key in
AutoWorld.AutoWorldRegister.world_types[self.game[player]].options_dataclass.type_hints}
for option_key in all_keys:
option = Utils.DeprecateDict(f"Getting options from multiworld is now deprecated. "
f"Please use `self.options.{option_key}` instead.")
option.update(getattr(args, option_key, {}))
setattr(self, option_key, option)
2021-06-11 16:02:48 +00:00
for player in self.player_ids:
world_type = AutoWorld.AutoWorldRegister.world_types[self.game[player]]
self.worlds[player] = world_type(self, player)
options_dataclass: type[Options.PerGameCommonOptions] = world_type.options_dataclass
Core: move option results to the World class instead of MultiWorld (#993) :crossed_fingers: * map option objects to a `World.options` dict * convert RoR2 to options dict system for testing * add temp behavior for lttp with notes * copy/paste bad * convert `set_default_common_options` to a namespace property * reorganize test call order * have fill_restrictive use the new options system * update world api * update soe tests * fix world api * core: auto initialize a dataclass on the World class with the option results * core: auto initialize a dataclass on the World class with the option results: small tying improvement * add `as_dict` method to the options dataclass * fix namespace issues with tests * have current option updates use `.value` instead of changing the option * update ror2 to use the new options system again * revert the junk pool dict since it's cased differently * fix begin_with_loop typo * write new and old options to spoiler * change factorio option behavior back * fix comparisons * move common and per_game_common options to new system * core: automatically create missing options_dataclass from legacy option_definitions * remove spoiler special casing and add back the Factorio option changing but in new system * give ArchipIDLE the default options_dataclass so its options get generated and spoilered properly * reimplement `inspect.get_annotations` * move option info generation for webhost to new system * need to include Common and PerGame common since __annotations__ doesn't include super * use get_type_hints for the options dictionary * typing.get_type_hints returns the bases too. * forgot to sweep through generate * sweep through all the tests * swap to a metaclass property * move remaining usages from get_type_hints to metaclass property * move remaining usages from __annotations__ to metaclass property * move remaining usages from legacy dictionaries to metaclass property * remove legacy dictionaries * cache the metaclass property * clarify inheritance in world api * move the messenger to new options system * add an assert for my dumb * update the doc * rename o to options * missed a spot * update new messenger options * comment spacing Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com> * fix tests * fix missing import * make the documentation definition more accurate * use options system for loc creation * type cast MessengerWorld * fix typo and use quotes for cast * LTTP: set random seed in tests * ArchipIdle: remove change here as it's default on AutoWorld * Stardew: Need to set state because `set_default_common_options` used to * The Messenger: update shop rando and helpers to new system; optimize imports * Add a kwarg to `as_dict` to do the casing for you * RoR2: use new kwarg for less code * RoR2: revert some accidental reverts * The Messenger: remove an unnecessary variable * remove TypeVar that isn't used * CommonOptions not abstract * Docs: fix mistake in options api.md Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com> * create options for item link worlds * revert accidental doc removals * Item Links: set default options on group * change Zillion to new options dataclass * remove unused parameter to function * use TypeGuard for Literal narrowing * move dlc quest to new api * move overcooked 2 to new api * fixed some missed code in oc2 * - Tried to be compliant with 993 (WIP?) * - I think it all works now * - Removed last trace of me touching core * typo * It now passes all tests! * Improve options, fix all issues I hope * - Fixed init options * dlcquest: fix bad imports * missed a file * - Reduce code duplication * add as_dict documentation * - Use .items(), get option name more directly, fix slot data content * - Remove generic options from the slot data * improve slot data documentation * remove `CommonOptions.get_value` (#21) * better slot data description Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --------- Co-authored-by: el-u <109771707+el-u@users.noreply.github.com> Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com> Co-authored-by: Doug Hoskisson <beauxq@yahoo.com> Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> Co-authored-by: Alex Gilbert <alexgilbert@yahoo.com>
2023-10-10 20:30:20 +00:00
self.worlds[player].options = options_dataclass(**{option_key: getattr(args, option_key)[player]
for option_key in options_dataclass.type_hints})
2021-03-20 23:47:17 +00:00
def set_item_links(self):
from worlds import AutoWorld
item_links = {}
replacement_prio = [False, True, None]
for player in self.player_ids:
Core: move option results to the World class instead of MultiWorld (#993) :crossed_fingers: * map option objects to a `World.options` dict * convert RoR2 to options dict system for testing * add temp behavior for lttp with notes * copy/paste bad * convert `set_default_common_options` to a namespace property * reorganize test call order * have fill_restrictive use the new options system * update world api * update soe tests * fix world api * core: auto initialize a dataclass on the World class with the option results * core: auto initialize a dataclass on the World class with the option results: small tying improvement * add `as_dict` method to the options dataclass * fix namespace issues with tests * have current option updates use `.value` instead of changing the option * update ror2 to use the new options system again * revert the junk pool dict since it's cased differently * fix begin_with_loop typo * write new and old options to spoiler * change factorio option behavior back * fix comparisons * move common and per_game_common options to new system * core: automatically create missing options_dataclass from legacy option_definitions * remove spoiler special casing and add back the Factorio option changing but in new system * give ArchipIDLE the default options_dataclass so its options get generated and spoilered properly * reimplement `inspect.get_annotations` * move option info generation for webhost to new system * need to include Common and PerGame common since __annotations__ doesn't include super * use get_type_hints for the options dictionary * typing.get_type_hints returns the bases too. * forgot to sweep through generate * sweep through all the tests * swap to a metaclass property * move remaining usages from get_type_hints to metaclass property * move remaining usages from __annotations__ to metaclass property * move remaining usages from legacy dictionaries to metaclass property * remove legacy dictionaries * cache the metaclass property * clarify inheritance in world api * move the messenger to new options system * add an assert for my dumb * update the doc * rename o to options * missed a spot * update new messenger options * comment spacing Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com> * fix tests * fix missing import * make the documentation definition more accurate * use options system for loc creation * type cast MessengerWorld * fix typo and use quotes for cast * LTTP: set random seed in tests * ArchipIdle: remove change here as it's default on AutoWorld * Stardew: Need to set state because `set_default_common_options` used to * The Messenger: update shop rando and helpers to new system; optimize imports * Add a kwarg to `as_dict` to do the casing for you * RoR2: use new kwarg for less code * RoR2: revert some accidental reverts * The Messenger: remove an unnecessary variable * remove TypeVar that isn't used * CommonOptions not abstract * Docs: fix mistake in options api.md Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com> * create options for item link worlds * revert accidental doc removals * Item Links: set default options on group * change Zillion to new options dataclass * remove unused parameter to function * use TypeGuard for Literal narrowing * move dlc quest to new api * move overcooked 2 to new api * fixed some missed code in oc2 * - Tried to be compliant with 993 (WIP?) * - I think it all works now * - Removed last trace of me touching core * typo * It now passes all tests! * Improve options, fix all issues I hope * - Fixed init options * dlcquest: fix bad imports * missed a file * - Reduce code duplication * add as_dict documentation * - Use .items(), get option name more directly, fix slot data content * - Remove generic options from the slot data * improve slot data documentation * remove `CommonOptions.get_value` (#21) * better slot data description Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --------- Co-authored-by: el-u <109771707+el-u@users.noreply.github.com> Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com> Co-authored-by: Doug Hoskisson <beauxq@yahoo.com> Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> Co-authored-by: Alex Gilbert <alexgilbert@yahoo.com>
2023-10-10 20:30:20 +00:00
for item_link in self.worlds[player].options.item_links.value:
if item_link["name"] in item_links:
2022-04-06 17:13:57 +00:00
if item_links[item_link["name"]]["game"] != self.game[player]:
raise Exception(f"Cannot ItemLink across games. Link: {item_link['name']}")
current_link = item_links[item_link["name"]]
current_link["players"][player] = item_link["replacement_item"]
current_link["item_pool"] &= set(item_link["item_pool"])
current_link["exclude"] |= set(item_link.get("exclude", []))
current_link["local_items"] &= set(item_link.get("local_items", []))
current_link["non_local_items"] &= set(item_link.get("non_local_items", []))
current_link["link_replacement"] = min(current_link["link_replacement"],
replacement_prio.index(item_link["link_replacement"]))
else:
if item_link["name"] in self.player_name.values():
raise Exception(f"Cannot name a ItemLink group the same as a player ({item_link['name']}) "
f"({self.get_player_name(player)}).")
item_links[item_link["name"]] = {
"players": {player: item_link["replacement_item"]},
"item_pool": set(item_link["item_pool"]),
2022-05-11 23:37:18 +00:00
"exclude": set(item_link.get("exclude", [])),
"game": self.game[player],
"local_items": set(item_link.get("local_items", [])),
"non_local_items": set(item_link.get("non_local_items", [])),
"link_replacement": replacement_prio.index(item_link["link_replacement"]),
}
for _name, item_link in item_links.items():
current_item_name_groups = AutoWorld.AutoWorldRegister.world_types[item_link["game"]].item_name_groups
pool = set()
local_items = set()
non_local_items = set()
for item in item_link["item_pool"]:
pool |= current_item_name_groups.get(item, {item})
2022-05-11 23:37:18 +00:00
for item in item_link["exclude"]:
pool -= current_item_name_groups.get(item, {item})
for item in item_link["local_items"]:
local_items |= current_item_name_groups.get(item, {item})
for item in item_link["non_local_items"]:
non_local_items |= current_item_name_groups.get(item, {item})
local_items &= pool
non_local_items &= pool
item_link["item_pool"] = pool
item_link["local_items"] = local_items
item_link["non_local_items"] = non_local_items
for group_name, item_link in item_links.items():
game = item_link["game"]
group_id, group = self.add_group(group_name, game, set(item_link["players"]))
group["item_pool"] = item_link["item_pool"]
group["replacement_items"] = item_link["players"]
group["local_items"] = item_link["local_items"]
group["non_local_items"] = item_link["non_local_items"]
group["link_replacement"] = replacement_prio[item_link["link_replacement"]]
def link_items(self) -> None:
"""Called to link together items in the itempool related to the registered item link groups."""
from worlds import AutoWorld
for group_id, group in self.groups.items():
def find_common_pool(players: Set[int], shared_pool: Set[str]) -> Tuple[
Optional[Dict[int, Dict[str, int]]], Optional[Dict[str, int]]
]:
classifications: Dict[str, int] = collections.defaultdict(int)
counters = {player: {name: 0 for name in shared_pool} for player in players}
for item in self.itempool:
if item.player in counters and item.name in shared_pool:
counters[item.player][item.name] += 1
classifications[item.name] |= item.classification
for player in players.copy():
if all([counters[player][item] == 0 for item in shared_pool]):
players.remove(player)
del (counters[player])
if not players:
return None, None
for item in shared_pool:
count = min(counters[player][item] for player in players)
if count:
for player in players:
counters[player][item] = count
else:
for player in players:
del (counters[player][item])
return counters, classifications
common_item_count, classifications = find_common_pool(group["players"], group["item_pool"])
if not common_item_count:
continue
new_itempool: List[Item] = []
for item_name, item_count in next(iter(common_item_count.values())).items():
for _ in range(item_count):
new_item = group["world"].create_item(item_name)
# mangle together all original classification bits
new_item.classification |= classifications[item_name]
new_itempool.append(new_item)
region = Region(group["world"].origin_region_name, group_id, self, "ItemLink")
self.regions.append(region)
locations = region.locations
# ensure that progression items are linked first, then non-progression
self.itempool.sort(key=lambda item: item.advancement)
for item in self.itempool:
count = common_item_count.get(item.player, {}).get(item.name, 0)
if count:
loc = Location(group_id, f"Item Link: {item.name} -> {self.player_name[item.player]} {count}",
None, region)
loc.access_rule = lambda state, item_name = item.name, group_id_ = group_id, count_ = count: \
state.has(item_name, group_id_, count_)
locations.append(loc)
loc.place_locked_item(item)
common_item_count[item.player][item.name] -= 1
else:
new_itempool.append(item)
itemcount = len(self.itempool)
self.itempool = new_itempool
while itemcount > len(self.itempool):
items_to_add = []
for player in group["players"]:
if group["link_replacement"]:
item_player = group_id
else:
item_player = player
if group["replacement_items"][player]:
items_to_add.append(AutoWorld.call_single(self, "create_item", item_player,
group["replacement_items"][player]))
else:
items_to_add.append(AutoWorld.call_single(self, "create_filler", item_player))
self.random.shuffle(items_to_add)
self.itempool.extend(items_to_add[:itemcount - len(self.itempool)])
def secure(self):
self.random = ThreadBarrierProxy(secrets.SystemRandom())
self.is_race = True
2021-07-04 14:44:27 +00:00
@functools.cached_property
def player_ids(self) -> Tuple[int, ...]:
2021-07-04 20:21:53 +00:00
return tuple(range(1, self.players + 1))
@Utils.cache_self1
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)
Minecraft Randomizer Squash merge, original Commits: * Minecraft locations, items, and generation without logic * added id lookup for minecraft * typing import fix in minecraft/Items.py * fix 2 * implementing Minecraft options and hard/postgame advancement exclusion * first logic pass (75/80) * logic pass 2 and proper completion conditions * added insane difficulty pool, modified method of excluding item pools for easier extension * bump network_data_package version * minecraft testing framework * switch Ancient Debris to Netherite Scrap to avoid advancement triggering on receiving that item * Testing now functions, split tests up by advancement pane, added some story tests * Newer testing framework: every advancement gets its own function, for ease of testing * fixed logic for The End... Again... * changed option names to "include_hard_advancements" etc. * village/pillager-related advancements now require can_adventure: weapon + food * a few minecraft tests * rename "Flint & Steel" to "Flint and Steel" for parity with in-game name * additional MC tests * more tests, mostly nether-related tests * more tests, removed anvil path for Two Birds One Arrow * include Minecraft slot data, and a world seed for each Minecraft player slot * Added new items: ender pearls, lapis, porkchops * All remaining Minecraft tests * formatting of Minecraft tests and logic for better readability * require Wither kill for Monsters Hunted * properly removed 8 Emeralds item from item pool * enchanting required for wither; fishing rod required for water breathing; water breathing required for elder guardian kill * Added 12 new advancements (ported from old achievement system) * renamed "On a Rail" for consistency with modern advancements * tests for the new advancements * moved slot_data generation for minecraft into worlds/minecraft/__init__.py, added logic_version to slot_data * output minecraft options in the spoiler log * modified advancement goal values for new advancements * make non-native Minecraft items appear as Shovel in ALttP, and unknown-game items as Power Stars * fixed glowstone block logic for Not Quite Nine Lives * setup for shuffling MC structures: building ER world and shuffling regions/entrances * ensured Nether Fortresses can't be placed in the End * finished logic for structure randomization * fixed nonnative items always showing up as Hammers in ALttP shops * output minecraft structure info in the spoiler * generate .apmc file for communication with MC client * fixed structure rando always using the same seed * move stuff to worlds/minecraft/Regions.py * make output apmc file have consistent name with other files * added minecraft bottle macro; fixed tests imports * generalizing MC region generation * restructured structure shuffling in preparation for structure plando * only output structure rando info in spoiler if they are shuffled * Force structure rando to always be off, for the stable release * added Minecraft options to player settings * formally added combat_difficulty as an option * Added Ender Dragon into playthrough, cleaned up goal map * Added new difficulties: Easy, Normal, Hard combat * moved .apmc generation time to prevent outputs on failed generation * updated tests for new combat logic * Fixed bug causing generation to fail; removed Nether Fortress event since it should no longer be needed with the fix * moved all MC-specific functions into gen_minecraft * renamed "logic_version" to "client_version" * bug fixes properly flagged event locations/items with id None moved generation back to Main.py to fix mysterious generation failures * moved link_minecraft_regions into minecraft init, left create_regions in Main for caching * added seed_name, player_name, client_version to apmc file * reenabled structure shuffle * added entrance tests for minecraft Co-authored-by: achuang <alexander.w.chuang@gmail.com>
2021-05-08 11:38:57 +00:00
@Utils.cache_self1
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)
@Utils.cache_self1
def get_game_worlds(self, game_name: str):
return tuple(world for player, world in self.worlds.items() if
player not in self.groups and self.game[player] == game_name)
def get_name_string_for_object(self, obj: HasNameAndPlayer) -> str:
return obj.name if self.players == 1 else f'{obj.name} ({self.get_player_name(obj.player)})'
def get_player_name(self, player: int) -> str:
return self.player_name[player]
def get_file_safe_player_name(self, player: int) -> str:
return Utils.get_file_safe_name(self.get_player_name(player))
def get_out_file_name_base(self, player: int) -> str:
""" the base name (without file extension) for each player's output file for a seed """
return f"AP_{self.seed_name}_P{player}_{self.get_file_safe_player_name(player).replace(' ', '_')}"
2021-02-20 01:30:55 +00:00
@functools.cached_property
def world_name_lookup(self):
return {self.player_name[player_id]: player_id for player_id in self.player_ids}
2021-02-20 01:30:55 +00:00
def get_regions(self, player: Optional[int] = None) -> Collection[Region]:
return self.regions if player is None else self.regions.region_cache[player].values()
def get_region(self, region_name: str, player: int) -> Region:
return self.regions.region_cache[player][region_name]
2020-09-08 13:02:37 +00:00
def get_entrance(self, entrance_name: str, player: int) -> Entrance:
return self.regions.entrance_cache[player][entrance_name]
2020-09-08 13:02:37 +00:00
def get_location(self, location_name: str, player: int) -> Location:
return self.regions.location_cache[player][location_name]
2021-09-01 19:01:54 +00:00
def get_all_state(self, use_cache: bool) -> CollectionState:
cached = getattr(self, "_all_state", None)
2021-09-01 19:01:54 +00:00
if use_cache and cached:
2021-08-09 04:33:26 +00:00
return cached.copy()
ret = CollectionState(self)
2017-11-04 18:23:57 +00:00
for item in self.itempool:
2021-07-04 13:47:11 +00:00
self.worlds[item.player].collect(ret, item)
for player in self.player_ids:
subworld = self.worlds[player]
for item in subworld.get_pre_fill_items():
subworld.collect(ret, item)
ret.sweep_for_advancements()
2021-09-01 19:01:54 +00:00
if use_cache:
self._all_state = ret
return ret
def get_items(self) -> List[Item]:
return [loc.item for loc in self.get_filled_locations()] + self.itempool
def find_item_locations(self, item: str, player: int, resolve_group_locations: bool = False) -> List[Location]:
if resolve_group_locations:
player_groups = self.get_player_groups(player)
return [location for location in self.get_locations() if
location.item and location.item.name == item and location.player not in player_groups and
(location.item.player == player or location.item.player in player_groups)]
2020-03-02 23:12:14 +00:00
return [location for location in self.get_locations() if
location.item and location.item.name == item and location.item.player == player]
def find_item(self, item: str, player: int) -> Location:
return next(location for location in self.get_locations() if
location.item and location.item.name == item and location.item.player == player)
def find_items_in_locations(self, items: Set[str], player: int, resolve_group_locations: bool = False) -> List[Location]:
if resolve_group_locations:
player_groups = self.get_player_groups(player)
return [location for location in self.get_locations() if
location.item and location.item.name in items and location.player not in player_groups and
(location.item.player == player or location.item.player in player_groups)]
return [location for location in self.get_locations() if
location.item and location.item.name in items and location.item.player == player]
2021-07-12 11:54:47 +00:00
def create_item(self, item_name: str, player: int) -> Item:
return self.worlds[player].create_item(item_name)
2020-03-02 23:12:14 +00:00
def push_precollected(self, item: Item):
self.precollected_items[item.player].append(item)
self.state.collect(item, True)
2020-03-02 23:12:14 +00:00
def push_item(self, location: Location, item: Item, collect: bool = True):
location.item = item
item.location = location
if collect:
self.state.collect(item, location.advancement, location)
logging.debug('Placed %s at %s', item, location)
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) -> Iterable[Location]:
if player is not 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))
2018-03-23 03:18:40 +00:00
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]
def get_filled_locations(self, player: Optional[int] = None) -> List[Location]:
return [location for location in self.get_locations(player) if location.item is not None]
def get_reachable_locations(self, state: Optional[CollectionState] = None, player: Optional[int] = None) -> List[Location]:
state: CollectionState = state if state else self.state
return [location for location in self.get_locations(player) if location.can_reach(state)]
def get_placeable_locations(self, state=None, player=None) -> List[Location]:
state: CollectionState = state if state else self.state
return [location for location in self.get_locations(player) if location.item is None and location.can_reach(state)]
def get_unfilled_locations_for_players(self, location_names: List[str], players: Iterable[int]):
for player in players:
if not location_names:
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 = relevant_cache.get(location_name, None)
if location and location.item is None:
2022-01-20 18:34:17 +00:00
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(item.player):
if temp_state.can_reach(location) and not self.state.can_reach(location):
return True
return False
def has_beaten_game(self, state: CollectionState, player: Optional[int] = None) -> bool:
if player:
2021-02-22 10:18:53 +00:00
return self.completion_condition[player](state)
else:
return all((self.has_beaten_game(state, p) for p in range(1, self.players + 1)))
def can_beat_game(self, starting_state: Optional[CollectionState] = None) -> bool:
if starting_state:
2020-03-07 22:20:11 +00:00
if self.has_beaten_game(starting_state):
return True
state = starting_state.copy()
else:
state = CollectionState(self)
if self.has_beaten_game(state):
return True
2021-04-29 07:54:49 +00:00
prog_locations = {location for location in self.get_locations() if location.item
and location.item.advancement and location not in state.locations_checked}
while prog_locations:
sphere: Set[Location] = set()
# build up spheres of collection radius.
# Everything in each sphere is independent from each other in dependencies and only depends on lower spheres
for location in prog_locations:
if location.can_reach(state):
2021-04-29 07:54:49 +00:00
sphere.add(location)
if not sphere:
# ran out of places and did not finish yet, quit
return False
for location in sphere:
state.collect(location.item, True, location)
2021-04-29 07:54:49 +00:00
prog_locations -= sphere
if self.has_beaten_game(state):
return True
return False
def get_spheres(self) -> Iterator[Set[Location]]:
"""
yields a set of locations for each logical sphere
If there are unreachable locations, the last sphere of reachable
locations is followed by an empty set, and then a set of all of the
unreachable locations.
"""
state = CollectionState(self)
2021-02-27 17:58:17 +00:00
locations = set(self.get_filled_locations())
while locations:
sphere: Set[Location] = set()
for location in locations:
if location.can_reach(state):
sphere.add(location)
2021-02-27 17:58:17 +00:00
yield sphere
if not sphere:
if locations:
yield locations # unreachable locations
break
for location in sphere:
state.collect(location.item, True, location)
locations -= sphere
def fulfills_accessibility(self, state: Optional[CollectionState] = None):
"""Check if accessibility rules are fulfilled with current or supplied state."""
if not state:
state = CollectionState(self)
players: Dict[str, Set[int]] = {
"minimal": set(),
"items": set(),
"full": set()
}
for player, world in self.worlds.items():
players[world.options.accessibility.current_key].add(player)
beatable_fulfilled = False
def location_condition(location: Location) -> bool:
"""Determine if this location has to be accessible, location is already filtered by location_relevant"""
return location.player in players["full"] or \
(location.item and location.item.player not in players["minimal"])
def location_relevant(location: Location) -> bool:
"""Determine if this location is relevant to sweep."""
return location.player in players["full"] or location.advancement
def all_done() -> bool:
"""Check if all access rules are fulfilled"""
if not beatable_fulfilled:
return False
if any(location_condition(location) for location in locations):
return False # still locations required to be collected
return True
locations = [location for location in self.get_locations() if location_relevant(location)]
while locations:
sphere: List[Location] = []
for n in range(len(locations) - 1, -1, -1):
if locations[n].can_reach(state):
sphere.append(locations.pop(n))
if not sphere:
# ran out of places and did not finish yet, quit
logging.warning(f"Could not access required locations for accessibility check."
f" Missing: {locations}")
return False
for location in sphere:
if location.item:
state.collect(location.item, True, location)
if self.has_beaten_game(state):
beatable_fulfilled = True
if all_done():
return True
return False
PathValue = Tuple[str, Optional["PathValue"]]
class CollectionState():
prog_items: Dict[int, Counter[str]]
multiworld: MultiWorld
reachable_regions: Dict[int, Set[Region]]
blocked_connections: Dict[int, Set[Entrance]]
advancements: Set[Location]
path: Dict[Union[Region, Entrance], PathValue]
locations_checked: Set[Location]
stale: Dict[int, bool]
additional_init_functions: List[Callable[[CollectionState, MultiWorld], None]] = []
additional_copy_functions: List[Callable[[CollectionState, CollectionState], CollectionState]] = []
2020-10-24 03:38:56 +00:00
def __init__(self, parent: MultiWorld):
self.prog_items = {player: Counter() for player in parent.get_all_ids()}
self.multiworld = parent
self.reachable_regions = {player: set() for player in parent.get_all_ids()}
self.blocked_connections = {player: set() for player in parent.get_all_ids()}
self.advancements = set()
self.path = {}
self.locations_checked = set()
self.stale = {player: True for player in parent.get_all_ids()}
2022-02-19 16:43:16 +00:00
for function in self.additional_init_functions:
function(self, parent)
for items in parent.precollected_items.values():
for item in items:
self.collect(item, True)
2020-03-02 23:12:14 +00:00
def update_reachable_regions(self, player: int):
2019-07-11 04:18:30 +00:00
self.stale[player] = False
world: AutoWorld.World = self.multiworld.worlds[player]
reachable_regions = self.reachable_regions[player]
queue = deque(self.blocked_connections[player])
start: Region = world.get_region(world.origin_region_name)
# init on first call - this can't be done on construction since the regions don't exist yet
if start not in reachable_regions:
reachable_regions.add(start)
self.blocked_connections[player].update(start.exits)
queue.extend(start.exits)
if world.explicit_indirect_conditions:
self._update_reachable_regions_explicit_indirect_conditions(player, queue)
else:
self._update_reachable_regions_auto_indirect_conditions(player, queue)
def _update_reachable_regions_explicit_indirect_conditions(self, player: int, queue: deque):
reachable_regions = self.reachable_regions[player]
blocked_connections = self.blocked_connections[player]
# run BFS on all connections, and keep track of those blocked by missing items
2020-06-30 05:32:05 +00:00
while queue:
connection = queue.popleft()
new_region = connection.connected_region
if new_region in reachable_regions:
blocked_connections.remove(connection)
2020-06-30 05:32:05 +00:00
elif connection.can_reach(self):
assert new_region, f"tried to search through an Entrance \"{connection}\" with no connected Region"
reachable_regions.add(new_region)
blocked_connections.remove(connection)
blocked_connections.update(new_region.exits)
2020-06-30 05:32:05 +00:00
queue.extend(new_region.exits)
self.path[new_region] = (new_region.name, self.path.get(connection, None))
# Retry connections if the new region can unblock them
for new_entrance in self.multiworld.indirect_connections.get(new_region, set()):
if new_entrance in blocked_connections and new_entrance not in queue:
2020-06-30 05:32:05 +00:00
queue.append(new_entrance)
def _update_reachable_regions_auto_indirect_conditions(self, player: int, queue: deque):
reachable_regions = self.reachable_regions[player]
blocked_connections = self.blocked_connections[player]
new_connection: bool = True
# run BFS on all connections, and keep track of those blocked by missing items
while new_connection:
new_connection = False
while queue:
connection = queue.popleft()
new_region = connection.connected_region
if new_region in reachable_regions:
blocked_connections.remove(connection)
elif connection.can_reach(self):
assert new_region, f"tried to search through an Entrance \"{connection}\" with no Region"
reachable_regions.add(new_region)
blocked_connections.remove(connection)
blocked_connections.update(new_region.exits)
queue.extend(new_region.exits)
self.path[new_region] = (new_region.name, self.path.get(connection, None))
new_connection = True
# sweep for indirect connections, mostly Entrance.can_reach(unrelated_Region)
queue.extend(blocked_connections)
2020-03-02 23:12:14 +00:00
def copy(self) -> CollectionState:
ret = CollectionState(self.multiworld)
Core: Speed up CollectionState.copy() using built-in copy methods (#3678) All the types being copied are built-in types with their own `copy()` methods, so using the `copy` module was a bit overkill and also slower. This patch replaces the use of the `copy` module in `CollectionState.copy()` with using the built-in `.copy()` methods. The copying of `reachable_regions` and `blocked_connections` was also iterating the keys of each dictionary and then looking up the value in the dictionary for that key. It is faster, and I think more readable, to iterate the dictionary's `.items()` instead. For me, when generating a multiworld including the template yaml of every world with `python -O .\Generate.py --skip_output`, this patch saves about 2.1s. The overall generation duration for these yamls varies quite a lot, but averages around 160s for me, so on average this patch reduced overall generation duration (excluding output duration) by around 1.3%. Timing comparisons were made by calling time.perf_counter() at the start and end of `CollectionState.copy()`'s body, and summing the differences between the starts and ends of the method body into a global variable that was printed at the end of generation. Additional timing comparisons were made, using the `timeit` module, of the individual function calls or dictionary comprehensions used to perform the copying. The main performance cost was `copy.deepcopy()`, which gets slow as the number of keys multiplied by the number of values within the sets/Counters gets large, e.g., to deepcopy a `dict[int, Counter[str]]` with 100 keys and where each Counter contains 100 keys was 30x slower than most other tested copying methods. Increasing the number of dict keys or Counter keys only makes it slower.
2024-08-09 12:25:39 +00:00
ret.prog_items = {player: counter.copy() for player, counter in self.prog_items.items()}
ret.reachable_regions = {player: region_set.copy() for player, region_set in
self.reachable_regions.items()}
ret.blocked_connections = {player: entrance_set.copy() for player, entrance_set in
self.blocked_connections.items()}
ret.advancements = self.advancements.copy()
Core: Speed up CollectionState.copy() using built-in copy methods (#3678) All the types being copied are built-in types with their own `copy()` methods, so using the `copy` module was a bit overkill and also slower. This patch replaces the use of the `copy` module in `CollectionState.copy()` with using the built-in `.copy()` methods. The copying of `reachable_regions` and `blocked_connections` was also iterating the keys of each dictionary and then looking up the value in the dictionary for that key. It is faster, and I think more readable, to iterate the dictionary's `.items()` instead. For me, when generating a multiworld including the template yaml of every world with `python -O .\Generate.py --skip_output`, this patch saves about 2.1s. The overall generation duration for these yamls varies quite a lot, but averages around 160s for me, so on average this patch reduced overall generation duration (excluding output duration) by around 1.3%. Timing comparisons were made by calling time.perf_counter() at the start and end of `CollectionState.copy()`'s body, and summing the differences between the starts and ends of the method body into a global variable that was printed at the end of generation. Additional timing comparisons were made, using the `timeit` module, of the individual function calls or dictionary comprehensions used to perform the copying. The main performance cost was `copy.deepcopy()`, which gets slow as the number of keys multiplied by the number of values within the sets/Counters gets large, e.g., to deepcopy a `dict[int, Counter[str]]` with 100 keys and where each Counter contains 100 keys was 30x slower than most other tested copying methods. Increasing the number of dict keys or Counter keys only makes it slower.
2024-08-09 12:25:39 +00:00
ret.path = self.path.copy()
ret.locations_checked = self.locations_checked.copy()
for function in self.additional_copy_functions:
ret = function(self, ret)
return ret
def can_reach(self,
spot: Union[Location, Entrance, Region, str],
resolution_hint: Optional[str] = None,
player: Optional[int] = None) -> bool:
if isinstance(spot, str):
assert isinstance(player, int), "can_reach: player is required if spot is str"
# try to resolve a name
if resolution_hint == 'Location':
return self.can_reach_location(spot, player)
elif resolution_hint == 'Entrance':
return self.can_reach_entrance(spot, player)
else:
# default to Region
return self.can_reach_region(spot, player)
return spot.can_reach(self)
def can_reach_location(self, spot: str, player: int) -> bool:
return self.multiworld.get_location(spot, player).can_reach(self)
def can_reach_entrance(self, spot: str, player: int) -> bool:
return self.multiworld.get_entrance(spot, player).can_reach(self)
def can_reach_region(self, spot: str, player: int) -> bool:
return self.multiworld.get_region(spot, player).can_reach(self)
def sweep_for_events(self, locations: Optional[Iterable[Location]] = None) -> None:
Utils.deprecate("sweep_for_events has been renamed to sweep_for_advancements. The functionality is the same. "
"Please switch over to sweep_for_advancements.")
return self.sweep_for_advancements(locations)
def sweep_for_advancements(self, locations: Optional[Iterable[Location]] = None) -> None:
if locations is None:
locations = self.multiworld.get_filled_locations()
reachable_advancements = True
# since the loop has a good chance to run more than once, only filter the advancements once
locations = {location for location in locations if location.advancement and location not in self.advancements}
while reachable_advancements:
reachable_advancements = {location for location in locations if location.can_reach(self)}
locations -= reachable_advancements
for advancement in reachable_advancements:
self.advancements.add(advancement)
assert isinstance(advancement.item, Item), "tried to collect Event with no Item"
self.collect(advancement.item, True, advancement)
# item name related
def has(self, item: str, player: int, count: int = 1) -> bool:
return self.prog_items[player][item] >= count
def has_all(self, items: Iterable[str], player: int) -> bool:
"""Returns True if each item name of items is in state at least once."""
return all(self.prog_items[player][item] for item in items)
def has_any(self, items: Iterable[str], player: int) -> bool:
"""Returns True if at least one item name of items is in state at least once."""
return any(self.prog_items[player][item] for item in items)
def has_all_counts(self, item_counts: Mapping[str, int], player: int) -> bool:
"""Returns True if each item name is in the state at least as many times as specified."""
return all(self.prog_items[player][item] >= count for item, count in item_counts.items())
def has_any_count(self, item_counts: Mapping[str, int], player: int) -> bool:
"""Returns True if at least one item name is in the state at least as many times as specified."""
return any(self.prog_items[player][item] >= count for item, count in item_counts.items())
def count(self, item: str, player: int) -> int:
return self.prog_items[player][item]
def has_from_list(self, items: Iterable[str], player: int, count: int) -> bool:
"""Returns True if the state contains at least `count` items matching any of the item names from a list."""
found: int = 0
player_prog_items = self.prog_items[player]
for item_name in items:
found += player_prog_items[item_name]
if found >= count:
return True
return False
def has_from_list_unique(self, items: Iterable[str], player: int, count: int) -> bool:
"""Returns True if the state contains at least `count` items matching any of the item names from a list.
Ignores duplicates of the same item."""
found: int = 0
player_prog_items = self.prog_items[player]
for item_name in items:
found += player_prog_items[item_name] > 0
if found >= count:
return True
return False
def count_from_list(self, items: Iterable[str], player: int) -> int:
"""Returns the cumulative count of items from a list present in state."""
return sum(self.prog_items[player][item_name] for item_name in items)
def count_from_list_unique(self, items: Iterable[str], player: int) -> int:
"""Returns the cumulative count of items from a list present in state. Ignores duplicates of the same item."""
return sum(self.prog_items[player][item_name] > 0 for item_name in items)
# item name group related
def has_group(self, item_name_group: str, player: int, count: int = 1) -> bool:
"""Returns True if the state contains at least `count` items present in a specified item group."""
2021-07-21 07:45:15 +00:00
found: int = 0
player_prog_items = self.prog_items[player]
for item_name in self.multiworld.worlds[player].item_name_groups[item_name_group]:
found += player_prog_items[item_name]
2021-07-21 07:45:15 +00:00
if found >= count:
return True
return False
def has_group_unique(self, item_name_group: str, player: int, count: int = 1) -> bool:
"""Returns True if the state contains at least `count` items present in a specified item group.
Ignores duplicates of the same item.
"""
found: int = 0
player_prog_items = self.prog_items[player]
for item_name in self.multiworld.worlds[player].item_name_groups[item_name_group]:
found += player_prog_items[item_name] > 0
if found >= count:
return True
return False
def count_group(self, item_name_group: str, player: int) -> int:
"""Returns the cumulative count of items from an item group present in state."""
player_prog_items = self.prog_items[player]
return sum(
player_prog_items[item_name]
for item_name in self.multiworld.worlds[player].item_name_groups[item_name_group]
)
2021-07-21 07:45:15 +00:00
def count_group_unique(self, item_name_group: str, player: int) -> int:
"""Returns the cumulative count of items from an item group present in state.
Ignores duplicates of the same item."""
player_prog_items = self.prog_items[player]
return sum(
player_prog_items[item_name] > 0
for item_name in self.multiworld.worlds[player].item_name_groups[item_name_group]
)
# Item related
def collect(self, item: Item, prevent_sweep: bool = False, location: Optional[Location] = None) -> bool:
if location:
self.locations_checked.add(location)
changed = self.multiworld.worlds[item.player].collect(self, item)
2021-07-04 13:47:11 +00:00
2019-07-11 04:18:30 +00:00
self.stale[item.player] = True
if changed and not prevent_sweep:
self.sweep_for_advancements()
return changed
2021-11-30 04:33:56 +00:00
def remove(self, item: Item):
changed = self.multiworld.worlds[item.player].remove(self, item)
if changed:
# invalidate caches, nothing can be trusted anymore now
self.reachable_regions[item.player] = set()
self.blocked_connections[item.player] = set()
self.stale[item.player] = True
class Entrance:
access_rule: Callable[[CollectionState], bool] = staticmethod(lambda state: True)
hide_path: bool = False
player: int
name: str
parent_region: Optional[Region]
connected_region: Optional[Region] = None
# LttP specific, TODO: should make a LttPEntrance
addresses = None
target = None
def __init__(self, player: int, name: str = "", parent: Optional[Region] = None) -> None:
self.name = name
self.parent_region = parent
self.player = player
def can_reach(self, state: CollectionState) -> bool:
assert self.parent_region, f"called can_reach on an Entrance \"{self}\" with no parent_region"
if self.parent_region.can_reach(state) and self.access_rule(state):
if not self.hide_path and not self in state.path:
state.path[self] = (self.name, state.path.get(self.parent_region, (self.parent_region.name, None)))
return True
return False
def connect(self, region: Region, addresses: Any = None, target: Any = None) -> None:
self.connected_region = region
self.target = target
self.addresses = addresses
region.entrances.append(self)
def __repr__(self):
multiworld = self.parent_region.multiworld if self.parent_region else None
return multiworld.get_name_string_for_object(self) if multiworld else f'{self.name} (Player {self.player})'
class Region:
name: str
_hint_text: str
player: int
multiworld: Optional[MultiWorld]
entrances: List[Entrance]
exits: List[Entrance]
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:
assert value.name not in self.region_manager.location_cache[value.player], \
f"{value.name} already exists in the location cache."
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:
assert value.name not in self.region_manager.entrance_cache[value.player], \
f"{value.name} already exists in the entrance cache."
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.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)
2021-11-30 04:33:56 +00:00
def can_reach(self, state: CollectionState) -> bool:
2019-07-11 04:18:30 +00:00
if state.stale[self.player]:
state.update_reachable_regions(self.player)
return self in state.reachable_regions[self.player]
@property
def hint_text(self) -> str:
return self._hint_text if self._hint_text else self.name
def get_connecting_entrance(self, is_main_entrance: Callable[[Entrance], bool]) -> Entrance:
for entrance in self.entrances:
if is_main_entrance(entrance):
return entrance
for entrance in self.entrances: # BFS might be better here, trying DFS for now.
return entrance.parent_region.get_connecting_entrance(is_main_entrance)
def add_locations(self, locations: Dict[str, Optional[int]],
location_type: Optional[type[Location]] = None) -> None:
"""
Adds locations to the Region object, where location_type is your Location class and locations is a dict of
location names to address.
Core: move option results to the World class instead of MultiWorld (#993) :crossed_fingers: * map option objects to a `World.options` dict * convert RoR2 to options dict system for testing * add temp behavior for lttp with notes * copy/paste bad * convert `set_default_common_options` to a namespace property * reorganize test call order * have fill_restrictive use the new options system * update world api * update soe tests * fix world api * core: auto initialize a dataclass on the World class with the option results * core: auto initialize a dataclass on the World class with the option results: small tying improvement * add `as_dict` method to the options dataclass * fix namespace issues with tests * have current option updates use `.value` instead of changing the option * update ror2 to use the new options system again * revert the junk pool dict since it's cased differently * fix begin_with_loop typo * write new and old options to spoiler * change factorio option behavior back * fix comparisons * move common and per_game_common options to new system * core: automatically create missing options_dataclass from legacy option_definitions * remove spoiler special casing and add back the Factorio option changing but in new system * give ArchipIDLE the default options_dataclass so its options get generated and spoilered properly * reimplement `inspect.get_annotations` * move option info generation for webhost to new system * need to include Common and PerGame common since __annotations__ doesn't include super * use get_type_hints for the options dictionary * typing.get_type_hints returns the bases too. * forgot to sweep through generate * sweep through all the tests * swap to a metaclass property * move remaining usages from get_type_hints to metaclass property * move remaining usages from __annotations__ to metaclass property * move remaining usages from legacy dictionaries to metaclass property * remove legacy dictionaries * cache the metaclass property * clarify inheritance in world api * move the messenger to new options system * add an assert for my dumb * update the doc * rename o to options * missed a spot * update new messenger options * comment spacing Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com> * fix tests * fix missing import * make the documentation definition more accurate * use options system for loc creation * type cast MessengerWorld * fix typo and use quotes for cast * LTTP: set random seed in tests * ArchipIdle: remove change here as it's default on AutoWorld * Stardew: Need to set state because `set_default_common_options` used to * The Messenger: update shop rando and helpers to new system; optimize imports * Add a kwarg to `as_dict` to do the casing for you * RoR2: use new kwarg for less code * RoR2: revert some accidental reverts * The Messenger: remove an unnecessary variable * remove TypeVar that isn't used * CommonOptions not abstract * Docs: fix mistake in options api.md Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com> * create options for item link worlds * revert accidental doc removals * Item Links: set default options on group * change Zillion to new options dataclass * remove unused parameter to function * use TypeGuard for Literal narrowing * move dlc quest to new api * move overcooked 2 to new api * fixed some missed code in oc2 * - Tried to be compliant with 993 (WIP?) * - I think it all works now * - Removed last trace of me touching core * typo * It now passes all tests! * Improve options, fix all issues I hope * - Fixed init options * dlcquest: fix bad imports * missed a file * - Reduce code duplication * add as_dict documentation * - Use .items(), get option name more directly, fix slot data content * - Remove generic options from the slot data * improve slot data documentation * remove `CommonOptions.get_value` (#21) * better slot data description Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --------- Co-authored-by: el-u <109771707+el-u@users.noreply.github.com> Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com> Co-authored-by: Doug Hoskisson <beauxq@yahoo.com> Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> Co-authored-by: Alex Gilbert <alexgilbert@yahoo.com>
2023-10-10 20:30:20 +00:00
:param locations: dictionary of locations to be created and added to this Region `{name: ID}`
:param location_type: Location class to be used to create the locations with"""
2023-04-10 19:07:37 +00:00
if location_type is None:
location_type = Location
for location, address in locations.items():
self.locations.append(location_type(self.player, location, address, self))
Core: move option results to the World class instead of MultiWorld (#993) :crossed_fingers: * map option objects to a `World.options` dict * convert RoR2 to options dict system for testing * add temp behavior for lttp with notes * copy/paste bad * convert `set_default_common_options` to a namespace property * reorganize test call order * have fill_restrictive use the new options system * update world api * update soe tests * fix world api * core: auto initialize a dataclass on the World class with the option results * core: auto initialize a dataclass on the World class with the option results: small tying improvement * add `as_dict` method to the options dataclass * fix namespace issues with tests * have current option updates use `.value` instead of changing the option * update ror2 to use the new options system again * revert the junk pool dict since it's cased differently * fix begin_with_loop typo * write new and old options to spoiler * change factorio option behavior back * fix comparisons * move common and per_game_common options to new system * core: automatically create missing options_dataclass from legacy option_definitions * remove spoiler special casing and add back the Factorio option changing but in new system * give ArchipIDLE the default options_dataclass so its options get generated and spoilered properly * reimplement `inspect.get_annotations` * move option info generation for webhost to new system * need to include Common and PerGame common since __annotations__ doesn't include super * use get_type_hints for the options dictionary * typing.get_type_hints returns the bases too. * forgot to sweep through generate * sweep through all the tests * swap to a metaclass property * move remaining usages from get_type_hints to metaclass property * move remaining usages from __annotations__ to metaclass property * move remaining usages from legacy dictionaries to metaclass property * remove legacy dictionaries * cache the metaclass property * clarify inheritance in world api * move the messenger to new options system * add an assert for my dumb * update the doc * rename o to options * missed a spot * update new messenger options * comment spacing Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com> * fix tests * fix missing import * make the documentation definition more accurate * use options system for loc creation * type cast MessengerWorld * fix typo and use quotes for cast * LTTP: set random seed in tests * ArchipIdle: remove change here as it's default on AutoWorld * Stardew: Need to set state because `set_default_common_options` used to * The Messenger: update shop rando and helpers to new system; optimize imports * Add a kwarg to `as_dict` to do the casing for you * RoR2: use new kwarg for less code * RoR2: revert some accidental reverts * The Messenger: remove an unnecessary variable * remove TypeVar that isn't used * CommonOptions not abstract * Docs: fix mistake in options api.md Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com> * create options for item link worlds * revert accidental doc removals * Item Links: set default options on group * change Zillion to new options dataclass * remove unused parameter to function * use TypeGuard for Literal narrowing * move dlc quest to new api * move overcooked 2 to new api * fixed some missed code in oc2 * - Tried to be compliant with 993 (WIP?) * - I think it all works now * - Removed last trace of me touching core * typo * It now passes all tests! * Improve options, fix all issues I hope * - Fixed init options * dlcquest: fix bad imports * missed a file * - Reduce code duplication * add as_dict documentation * - Use .items(), get option name more directly, fix slot data content * - Remove generic options from the slot data * improve slot data documentation * remove `CommonOptions.get_value` (#21) * better slot data description Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --------- Co-authored-by: el-u <109771707+el-u@users.noreply.github.com> Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com> Co-authored-by: Doug Hoskisson <beauxq@yahoo.com> Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> Co-authored-by: Alex Gilbert <alexgilbert@yahoo.com>
2023-10-10 20:30:20 +00:00
def connect(self, connecting_region: Region, name: Optional[str] = None,
rule: Optional[Callable[[CollectionState], bool]] = None) -> Entrance:
"""
Connects this Region to another Region, placing the provided rule on the connection.
Core: move option results to the World class instead of MultiWorld (#993) :crossed_fingers: * map option objects to a `World.options` dict * convert RoR2 to options dict system for testing * add temp behavior for lttp with notes * copy/paste bad * convert `set_default_common_options` to a namespace property * reorganize test call order * have fill_restrictive use the new options system * update world api * update soe tests * fix world api * core: auto initialize a dataclass on the World class with the option results * core: auto initialize a dataclass on the World class with the option results: small tying improvement * add `as_dict` method to the options dataclass * fix namespace issues with tests * have current option updates use `.value` instead of changing the option * update ror2 to use the new options system again * revert the junk pool dict since it's cased differently * fix begin_with_loop typo * write new and old options to spoiler * change factorio option behavior back * fix comparisons * move common and per_game_common options to new system * core: automatically create missing options_dataclass from legacy option_definitions * remove spoiler special casing and add back the Factorio option changing but in new system * give ArchipIDLE the default options_dataclass so its options get generated and spoilered properly * reimplement `inspect.get_annotations` * move option info generation for webhost to new system * need to include Common and PerGame common since __annotations__ doesn't include super * use get_type_hints for the options dictionary * typing.get_type_hints returns the bases too. * forgot to sweep through generate * sweep through all the tests * swap to a metaclass property * move remaining usages from get_type_hints to metaclass property * move remaining usages from __annotations__ to metaclass property * move remaining usages from legacy dictionaries to metaclass property * remove legacy dictionaries * cache the metaclass property * clarify inheritance in world api * move the messenger to new options system * add an assert for my dumb * update the doc * rename o to options * missed a spot * update new messenger options * comment spacing Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com> * fix tests * fix missing import * make the documentation definition more accurate * use options system for loc creation * type cast MessengerWorld * fix typo and use quotes for cast * LTTP: set random seed in tests * ArchipIdle: remove change here as it's default on AutoWorld * Stardew: Need to set state because `set_default_common_options` used to * The Messenger: update shop rando and helpers to new system; optimize imports * Add a kwarg to `as_dict` to do the casing for you * RoR2: use new kwarg for less code * RoR2: revert some accidental reverts * The Messenger: remove an unnecessary variable * remove TypeVar that isn't used * CommonOptions not abstract * Docs: fix mistake in options api.md Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com> * create options for item link worlds * revert accidental doc removals * Item Links: set default options on group * change Zillion to new options dataclass * remove unused parameter to function * use TypeGuard for Literal narrowing * move dlc quest to new api * move overcooked 2 to new api * fixed some missed code in oc2 * - Tried to be compliant with 993 (WIP?) * - I think it all works now * - Removed last trace of me touching core * typo * It now passes all tests! * Improve options, fix all issues I hope * - Fixed init options * dlcquest: fix bad imports * missed a file * - Reduce code duplication * add as_dict documentation * - Use .items(), get option name more directly, fix slot data content * - Remove generic options from the slot data * improve slot data documentation * remove `CommonOptions.get_value` (#21) * better slot data description Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --------- Co-authored-by: el-u <109771707+el-u@users.noreply.github.com> Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com> Co-authored-by: Doug Hoskisson <beauxq@yahoo.com> Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> Co-authored-by: Alex Gilbert <alexgilbert@yahoo.com>
2023-10-10 20:30:20 +00:00
:param connecting_region: Region object to connect to path is `self -> exiting_region`
:param name: name of the connection being created
:param rule: callable to determine access of this connection to go from self to the exiting_region"""
exit_ = self.create_exit(name if name else f"{self.name} -> {connecting_region.name}")
if rule:
exit_.access_rule = rule
exit_.connect(connecting_region)
return exit_
Core: move option results to the World class instead of MultiWorld (#993) :crossed_fingers: * map option objects to a `World.options` dict * convert RoR2 to options dict system for testing * add temp behavior for lttp with notes * copy/paste bad * convert `set_default_common_options` to a namespace property * reorganize test call order * have fill_restrictive use the new options system * update world api * update soe tests * fix world api * core: auto initialize a dataclass on the World class with the option results * core: auto initialize a dataclass on the World class with the option results: small tying improvement * add `as_dict` method to the options dataclass * fix namespace issues with tests * have current option updates use `.value` instead of changing the option * update ror2 to use the new options system again * revert the junk pool dict since it's cased differently * fix begin_with_loop typo * write new and old options to spoiler * change factorio option behavior back * fix comparisons * move common and per_game_common options to new system * core: automatically create missing options_dataclass from legacy option_definitions * remove spoiler special casing and add back the Factorio option changing but in new system * give ArchipIDLE the default options_dataclass so its options get generated and spoilered properly * reimplement `inspect.get_annotations` * move option info generation for webhost to new system * need to include Common and PerGame common since __annotations__ doesn't include super * use get_type_hints for the options dictionary * typing.get_type_hints returns the bases too. * forgot to sweep through generate * sweep through all the tests * swap to a metaclass property * move remaining usages from get_type_hints to metaclass property * move remaining usages from __annotations__ to metaclass property * move remaining usages from legacy dictionaries to metaclass property * remove legacy dictionaries * cache the metaclass property * clarify inheritance in world api * move the messenger to new options system * add an assert for my dumb * update the doc * rename o to options * missed a spot * update new messenger options * comment spacing Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com> * fix tests * fix missing import * make the documentation definition more accurate * use options system for loc creation * type cast MessengerWorld * fix typo and use quotes for cast * LTTP: set random seed in tests * ArchipIdle: remove change here as it's default on AutoWorld * Stardew: Need to set state because `set_default_common_options` used to * The Messenger: update shop rando and helpers to new system; optimize imports * Add a kwarg to `as_dict` to do the casing for you * RoR2: use new kwarg for less code * RoR2: revert some accidental reverts * The Messenger: remove an unnecessary variable * remove TypeVar that isn't used * CommonOptions not abstract * Docs: fix mistake in options api.md Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com> * create options for item link worlds * revert accidental doc removals * Item Links: set default options on group * change Zillion to new options dataclass * remove unused parameter to function * use TypeGuard for Literal narrowing * move dlc quest to new api * move overcooked 2 to new api * fixed some missed code in oc2 * - Tried to be compliant with 993 (WIP?) * - I think it all works now * - Removed last trace of me touching core * typo * It now passes all tests! * Improve options, fix all issues I hope * - Fixed init options * dlcquest: fix bad imports * missed a file * - Reduce code duplication * add as_dict documentation * - Use .items(), get option name more directly, fix slot data content * - Remove generic options from the slot data * improve slot data documentation * remove `CommonOptions.get_value` (#21) * better slot data description Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --------- Co-authored-by: el-u <109771707+el-u@users.noreply.github.com> Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com> Co-authored-by: Doug Hoskisson <beauxq@yahoo.com> Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> Co-authored-by: Alex Gilbert <alexgilbert@yahoo.com>
2023-10-10 20:30:20 +00:00
def create_exit(self, name: str) -> Entrance:
"""
Creates and returns an Entrance object as an exit of this region.
Core: move option results to the World class instead of MultiWorld (#993) :crossed_fingers: * map option objects to a `World.options` dict * convert RoR2 to options dict system for testing * add temp behavior for lttp with notes * copy/paste bad * convert `set_default_common_options` to a namespace property * reorganize test call order * have fill_restrictive use the new options system * update world api * update soe tests * fix world api * core: auto initialize a dataclass on the World class with the option results * core: auto initialize a dataclass on the World class with the option results: small tying improvement * add `as_dict` method to the options dataclass * fix namespace issues with tests * have current option updates use `.value` instead of changing the option * update ror2 to use the new options system again * revert the junk pool dict since it's cased differently * fix begin_with_loop typo * write new and old options to spoiler * change factorio option behavior back * fix comparisons * move common and per_game_common options to new system * core: automatically create missing options_dataclass from legacy option_definitions * remove spoiler special casing and add back the Factorio option changing but in new system * give ArchipIDLE the default options_dataclass so its options get generated and spoilered properly * reimplement `inspect.get_annotations` * move option info generation for webhost to new system * need to include Common and PerGame common since __annotations__ doesn't include super * use get_type_hints for the options dictionary * typing.get_type_hints returns the bases too. * forgot to sweep through generate * sweep through all the tests * swap to a metaclass property * move remaining usages from get_type_hints to metaclass property * move remaining usages from __annotations__ to metaclass property * move remaining usages from legacy dictionaries to metaclass property * remove legacy dictionaries * cache the metaclass property * clarify inheritance in world api * move the messenger to new options system * add an assert for my dumb * update the doc * rename o to options * missed a spot * update new messenger options * comment spacing Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com> * fix tests * fix missing import * make the documentation definition more accurate * use options system for loc creation * type cast MessengerWorld * fix typo and use quotes for cast * LTTP: set random seed in tests * ArchipIdle: remove change here as it's default on AutoWorld * Stardew: Need to set state because `set_default_common_options` used to * The Messenger: update shop rando and helpers to new system; optimize imports * Add a kwarg to `as_dict` to do the casing for you * RoR2: use new kwarg for less code * RoR2: revert some accidental reverts * The Messenger: remove an unnecessary variable * remove TypeVar that isn't used * CommonOptions not abstract * Docs: fix mistake in options api.md Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com> * create options for item link worlds * revert accidental doc removals * Item Links: set default options on group * change Zillion to new options dataclass * remove unused parameter to function * use TypeGuard for Literal narrowing * move dlc quest to new api * move overcooked 2 to new api * fixed some missed code in oc2 * - Tried to be compliant with 993 (WIP?) * - I think it all works now * - Removed last trace of me touching core * typo * It now passes all tests! * Improve options, fix all issues I hope * - Fixed init options * dlcquest: fix bad imports * missed a file * - Reduce code duplication * add as_dict documentation * - Use .items(), get option name more directly, fix slot data content * - Remove generic options from the slot data * improve slot data documentation * remove `CommonOptions.get_value` (#21) * better slot data description Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --------- Co-authored-by: el-u <109771707+el-u@users.noreply.github.com> Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com> Co-authored-by: Doug Hoskisson <beauxq@yahoo.com> Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> Co-authored-by: Alex Gilbert <alexgilbert@yahoo.com>
2023-10-10 20:30:20 +00:00
:param name: name of the Entrance being created
"""
exit_ = self.entrance_type(self.player, name, self)
self.exits.append(exit_)
return exit_
2023-04-10 19:07:37 +00:00
def add_exits(self, exits: Union[Iterable[str], Dict[str, Optional[str]]],
rules: Dict[str, Callable[[CollectionState], bool]] = None) -> None:
2023-04-10 19:07:37 +00:00
"""
Connects current region to regions in exit dictionary. Passed region names must exist first.
:param exits: exits from the region. format is {"connecting_region": "exit_name"}. if a non dict is provided,
created entrances will be named "self.name -> connecting_region"
2023-04-10 19:07:37 +00:00
:param rules: rules for the exits from this region. format is {"connecting_region", rule}
"""
if not isinstance(exits, Dict):
exits = dict.fromkeys(exits)
for connecting_region, name in exits.items():
self.connect(self.multiworld.get_region(connecting_region, self.player),
name,
rules[connecting_region] if rules and connecting_region in rules else None)
2023-04-10 19:07:37 +00:00
def __repr__(self):
return self.multiworld.get_name_string_for_object(self) if self.multiworld else f'{self.name} (Player {self.player})'
class LocationProgressType(IntEnum):
DEFAULT = 1
PRIORITY = 2
EXCLUDED = 3
class Location:
game: str = "Generic"
player: int
name: str
address: Optional[int]
parent_region: Optional[Region]
2021-01-10 18:23:57 +00:00
locked: bool = False
Ocarina of Time (#64) * first commit (not including OoT data files yet) * added some basic options * rule parser works now at least * make sure to commit everything this time * temporary change to BaseClasses for oot * overworld location graph builds mostly correctly * adding oot data files * commenting out world options until later since they only existed to make the RuleParser work * conversion functions between AP ids and OOT ids * world graph outputs * set scrub prices * itempool generates, entrances connected, way too many options added * fixed set_rules and set_shop_rules * temp baseclasses changes * Reaches the fill step now, old event-based system retained in case the new way breaks * Song placements and misc fixes everywhere * temporary changes to make oot work * changed root exits for AP fill framework * prevent infinite recursion due to OoT sharing usage of the address field * age reachability works hopefully, songs are broken again * working spoiler log generation on beatable-only * Logic tricks implemented * need this for logic tricks * fixed map/compass being placed on Serenade location * kill unreachable events before filling the world * add a bunch of utility functions to prepare for rom patching * move OptionList into generic options * fixed some silly bugs with OptionList * properly seed all random behavior (so far) * ROM generation working * fix hints trying to get alttp dungeon hint texts * continue fixing hints * add oot to network data package * change item and location IDs to 66000 and 67000 range respectively * push removed items to precollected items * fixed various issues with cross-contamination with multiple world generation * reenable glitched logic (hopefully) * glitched world files age-check fix * cleaned up some get_locations calls * added token shuffle and scrub shuffle, modified some options slightly to make the parsing work * reenable MQ dungeons * fix forest mq exception * made targeting style an option for now, will be cosmetic later * reminder to move targeting to cosmetics * some oot option maintenance * enabled starting time of day * fixed issue breaking shop slots in multiworld generation * added "off" option for text shuffle and hints * shopsanity functionality restored * change patch file extension * remove unnecessary utility functions + imports * update MIT license * change option to "patch_uncompressed_rom" instead of "compress_rom" * compliance with new AutoWorld systems * Kill only internal events, remove non-internal big poe event in code * re-add the big poe event and handle it correctly * remove extra method in Range option * fix typo * Starting items, starting with consumables option * do not remove nonexistent item * move set_shop_rules to after shop items are placed * some cleanup * add retries for song placement * flagged Skull Mask and Mask of Truth as advancement items * update OoT to use LogicMixin * Fixed trying to assign starting items from the wrong players * fixed song retry step * improved option handling, comments, and starting item replacements * DefaultOnToggle writes Yes or No to spoiler * enable compression of output if Compress executable is present * clean up compression * check whether (de)compressor exists before running the process * allow specification of rom path in host.yaml * check if decompressed file already exists before decompressing again * fix triforce hunt generation * rename all the oot state functions with prefix * OoT: mark triforce pieces as completion goal for triforce hunt * added overworld and any-dungeon shuffle for dungeon items * Hide most unshuffled locations and events from the list of locations in spoiler * build oot option ranges with a generic function instead of defining each separately * move oot output-type control to host.yaml instead of individual yamls * implement dungeon song shuffle * minor improvements to overworld dungeon item shuffle * remove random ice trap names in shops, mostly to avoid maintaining a massive censor list * always output patch file to folder, remove option to generate ROM in preparation for removal * re-add the fix for infinite recursion due to not being light or dark world * change AP-sendable to Ocarina of Time model, since the triforce piece has some extra code apparently * oot: remove item_names and location_names * oot: minor fixes * oot: comment out ROM patching * oot: only add CollectionState objects on creation if actually needed * main entrance shuffle method and entrances-based rules * fix entrances based rules * disable master quest and big poe count options for client compatibility * use get_player_name instead of get_player_names * fix OptionList * fix oot options for new option system * new coop section in oot rom: expand player names to 16 bytes, write AP_PLAYER_NAME at end of PLAYER_NAMES * fill AP player name in oot rom with 0 instead of 0xDF * encode player name with ASCII for fixed-width * revert oot player name array to 8 bytes per name * remove Pierre location if fast scarecrow is on * check player name length * "free_scarecrow" not "fast_scarecrow" * OoT locations now properly store the AP ID instead of the oot internal ID * oot __version__ updates in lockstep with AP version * pull in unmodified oot cosmetic files * also grab JSONDump since it's needed apparently * gather extra needed methods, modify imports * delete cosmetics log, replace all instances of SettingsList with OOTWorld * cosmetic options working, except for sound effects (due to ear-safe issues) * SFX, Music, and Fanfare randomization reenabled * move OoT data files into the worlds folder * move Compress and Decompress into oot data folder * Replace get_all_state with custom method to avoid the cache * OoT ROM: increment item counter before setting incoming item/player values to 0, preventing desync issues * set data_version to 0 * make Kokiri Sword shuffle off by default * reenable "Random Choice" for various cosmetic options * kill Ruto's Letter turnin if open fountain also fix for shopsanity * place Buy Goron/Zora Tunic first in shop shuffle * make ice traps appear as other items instead of breaking generation * managed to break ice traps on non-major-only * only handle ice traps if they are on * fix shopsanity for non-oot games, and write player name instead of player number * light arrows hint uses player name instead of player number * Reenable "skip child zelda" option * fix entrances_based_rules * fix ganondorf hint if starting with light arrows * fix dungeonitem shuffle and shopsanity interaction * remove has_all_of, has_any_of, count_of in BaseClasses, replace usage with has_all, has_any, has_group * force local giveable item on ZL if skip_child_zelda and shuffle_song_items is any * keep bosses and bombchu bowling chus out of data package * revert workaround for infinite recursion and fix it properly * fix shared shop id caches during patching process * fix shop text box overflows, as much as possible * add default oot host.yaml option * add .apz5, .n64, .z64 to gitignore * Properly document and name all (functioning) OOT options * clean up some imports * remove unnecessary files from oot's data * fix typo in gitignore * readd the Compress and Decompress utilities, since they are needed for generation * cleanup of imports and some minor optimizations * increase shop offset for item IDs to 0xCB * remove shop item AP ids entirely * prevent triforce pieces for other players from being received by yourself * add "excluded" property to Location * Hint system adapted and reenabled; hints still unseeded * make hints deterministic with lists instead of sets * do not allow hints to point to Light Arrows on non-vanilla bridge * foreign locations hint as their full name in OoT rather than their region * checkedLocations now stores hint names by player ID, so that the same location in different worlds can have hints associated * consolidate versioning in Utils * ice traps appear as major items rather than any progression item * set prescription and claim check as defaults for adult trade item settings * add oot options to playerSettings * allow case-insensitive logic tricks in yaml * fix oot shopsanity option formatting * Write OoT override info even if local item, enabling local checks to show up immediately in the client * implement CollectionState.can_live_dmg for oot glitched logic * filter item names for invalid characters when patching shops * make ice traps appear according to the settings of the world they are shuffled into, rather than the original world * set hidden-spoiler items and locations with Shop items to events * make GF carpenters, Gerudo Card, Malon, ZL, and Impa events if the relevant settings are enabled, preventing them from appearing in the client on game start * Fix oot Glitched and No Logic generation * fix indenting * Greatly reduce displayed cosmetic options * Change oot data version to 1 * add apz5 distribution to webhost * print player name if an ALttP dungeon contains a good item for OoT world * delete unneeded commented code * remove OcarinaSongs import to satisfy lint
2021-09-02 12:35:05 +00:00
show_in_spoiler: bool = True
progress_type: LocationProgressType = LocationProgressType.DEFAULT
always_allow: Callable[[CollectionState, Item], bool] = staticmethod(lambda state, item: False)
access_rule: Callable[[CollectionState], bool] = staticmethod(lambda state: True)
item_rule: Callable[[Item], bool] = staticmethod(lambda item: True)
item: Optional[Item] = None
2021-01-10 18:23:57 +00:00
def __init__(self, player: int, name: str = '', address: Optional[int] = None, parent: Optional[Region] = None):
self.player = player
self.name = name
self.address = address
self.parent_region = parent
def can_fill(self, state: CollectionState, item: Item, check_access: bool = True) -> bool:
return ((
self.always_allow(state, item)
and item.name not in state.multiworld.worlds[item.player].options.non_local_items
) or (
(self.progress_type != LocationProgressType.EXCLUDED or not (item.advancement or item.useful))
and self.item_rule(item)
and (not check_access or self.can_reach(state))
))
2020-07-09 14:16:31 +00:00
def can_reach(self, state: CollectionState) -> bool:
# Region.can_reach is just a cache lookup, so placing it first for faster abort on average
assert self.parent_region, f"called can_reach on a Location \"{self}\" with no parent_region"
return self.parent_region.can_reach(state) and self.access_rule(state)
def place_locked_item(self, item: Item):
if self.item:
raise Exception(f"Location {self} already filled.")
self.item = item
item.location = self
self.locked = True
def __repr__(self):
multiworld = self.parent_region.multiworld if self.parent_region and self.parent_region.multiworld else None
return multiworld.get_name_string_for_object(self) if multiworld else f'{self.name} (Player {self.player})'
def __hash__(self):
return hash((self.name, self.player))
def __lt__(self, other: Location):
return (self.player, self.name) < (other.player, other.name)
@property
def advancement(self) -> bool:
return self.item is not None and self.item.advancement
@property
def is_event(self) -> bool:
"""Returns True if the address of this location is None, denoting it is an Event Location."""
return self.address is None
@property
def native_item(self) -> bool:
"""Returns True if the item in this location matches game."""
return self.item is not None and self.item.game == self.game
@property
def hint_text(self) -> str:
return "at " + self.name.replace("_", " ").replace("-", " ")
class ItemClassification(IntFlag):
filler = 0b0000 # aka trash, as in filler items like ammo, currency etc,
progression = 0b0001 # Item that is logically relevant
useful = 0b0010 # Item that is generally quite useful, but not required for anything logical
trap = 0b0100 # detrimental item
skip_balancing = 0b1000 # should technically never occur on its own
# Item that is logically relevant, but progression balancing should not touch.
# Typically currency or other counted items.
progression_skip_balancing = 0b1001 # only progression gets balanced
def as_flag(self) -> int:
"""As Network API flag int."""
return int(self & 0b0111)
class Item:
game: str = "Generic"
2022-08-05 22:49:54 +00:00
__slots__ = ("name", "classification", "code", "player", "location")
name: str
classification: ItemClassification
2022-08-05 22:49:54 +00:00
code: Optional[int]
"""an item with code None is called an Event, and does not get written to multidata"""
player: int
location: Optional[Location]
def __init__(self, name: str, classification: ItemClassification, code: Optional[int], player: int):
self.name = name
self.classification = classification
self.player = player
self.code = code
2022-08-05 22:49:54 +00:00
self.location = None
@property
def hint_text(self) -> str:
2021-06-14 00:20:13 +00:00
return getattr(self, "_hint_text", self.name.replace("_", " ").replace("-", " "))
@property
def pedestal_hint_text(self) -> str:
return getattr(self, "_pedestal_hint_text", self.name.replace("_", " ").replace("-", " "))
@property
def advancement(self) -> bool:
return ItemClassification.progression in self.classification
@property
def skip_in_prog_balancing(self) -> bool:
return ItemClassification.progression_skip_balancing in self.classification
@property
def useful(self) -> bool:
return ItemClassification.useful in self.classification
@property
def trap(self) -> bool:
return ItemClassification.trap in self.classification
@property
def filler(self) -> bool:
return not (self.advancement or self.useful or self.trap)
@property
def excludable(self) -> bool:
return not (self.advancement or self.useful)
@property
def flags(self) -> int:
return self.classification.as_flag()
def __eq__(self, other: object) -> bool:
if not isinstance(other, Item):
return NotImplemented
2021-01-02 11:49:43 +00:00
return self.name == other.name and self.player == other.player
def __lt__(self, other: object) -> bool:
if not isinstance(other, Item):
return NotImplemented
if other.player != self.player:
return other.player < self.player
return self.name < other.name
def __hash__(self) -> int:
2021-01-02 11:59:19 +00:00
return hash((self.name, self.player))
def __repr__(self) -> str:
if self.location and self.location.parent_region and self.location.parent_region.multiworld:
return self.location.parent_region.multiworld.get_name_string_for_object(self)
return f"{self.name} (Player {self.player})"
2017-05-20 12:03:15 +00:00
class EntranceInfo(TypedDict, total=False):
player: int
entrance: str
exit: str
direction: str
class Spoiler:
multiworld: MultiWorld
hashes: Dict[int, str]
entrances: Dict[Tuple[str, str, int], EntranceInfo]
playthrough: Dict[str, Union[List[str], Dict[str, str]]] # sphere "0" is list, others are dict
2022-02-06 23:26:44 +00:00
unreachables: Set[Location]
paths: Dict[str, List[Union[Tuple[str, str], Tuple[str, None]]]] # last step takes no further exits
2020-08-25 12:31:20 +00:00
def __init__(self, multiworld: MultiWorld) -> None:
self.multiworld = multiworld
self.hashes = {}
self.entrances = {}
self.playthrough = {}
2022-02-06 23:26:44 +00:00
self.unreachables = set()
self.paths = {}
def set_entrance(self, entrance: str, exit_: str, direction: str, player: int) -> None:
if self.multiworld.players == 1:
self.entrances[(entrance, direction, player)] = \
{"entrance": entrance, "exit": exit_, "direction": direction}
else:
self.entrances[(entrance, direction, player)] = \
{"player": player, "entrance": entrance, "exit": exit_, "direction": direction}
def create_playthrough(self, create_paths: bool = True) -> None:
"""Destructive to the multiworld while it is run, damage gets repaired afterwards."""
from itertools import chain
# get locations containing progress items
multiworld = self.multiworld
prog_locations = {location for location in multiworld.get_filled_locations() if location.item.advancement}
state_cache: List[Optional[CollectionState]] = [None]
collection_spheres: List[Set[Location]] = []
state = CollectionState(multiworld)
sphere_candidates = set(prog_locations)
logging.debug('Building up collection spheres.')
while sphere_candidates:
# build up spheres of collection radius.
# Everything in each sphere is independent from each other in dependencies and only depends on lower spheres
sphere = {location for location in sphere_candidates if state.can_reach(location)}
for location in sphere:
state.collect(location.item, True, location)
sphere_candidates -= sphere
collection_spheres.append(sphere)
state_cache.append(state.copy())
logging.debug('Calculated sphere %i, containing %i of %i progress items.', len(collection_spheres),
len(sphere),
len(prog_locations))
if not sphere:
logging.debug('The following items could not be reached: %s', ['%s (Player %d) at %s (Player %d)' % (
location.item.name, location.item.player, location.name, location.player) for location in
sphere_candidates])
if any([multiworld.worlds[location.item.player].options.accessibility != 'minimal' for location in sphere_candidates]):
raise RuntimeError(f'Not all progression items reachable ({sphere_candidates}). '
f'Something went terribly wrong here.')
else:
self.unreachables = sphere_candidates
break
# in the second phase, we cull each sphere such that the game is still beatable,
# reducing each range of influence to the bare minimum required inside it
restore_later: Dict[Location, Item] = {}
for num, sphere in reversed(tuple(enumerate(collection_spheres))):
to_delete: Set[Location] = set()
for location in sphere:
# we remove the item at location and check if game is still beatable
logging.debug('Checking if %s (Player %d) is required to beat the game.', location.item.name,
location.item.player)
old_item = location.item
location.item = None
if multiworld.can_beat_game(state_cache[num]):
to_delete.add(location)
restore_later[location] = old_item
else:
# still required, got to keep it around
location.item = old_item
# cull entries in spheres for spoiler walkthrough at end
sphere -= to_delete
# second phase, sphere 0
removed_precollected: List[Item] = []
for precollected_items in multiworld.precollected_items.values():
# The list of items is mutated by removing one item at a time to determine if each item is required to beat
# the game, and re-adding that item if it was required, so a copy needs to be made before iterating.
for item in precollected_items.copy():
if not item.advancement:
continue
logging.debug('Checking if %s (Player %d) is required to beat the game.', item.name, item.player)
precollected_items.remove(item)
multiworld.state.remove(item)
if not multiworld.can_beat_game():
# Add the item back into `precollected_items` and collect it into `multiworld.state`.
multiworld.push_precollected(item)
else:
removed_precollected.append(item)
# we are now down to just the required progress items in collection_spheres. Unfortunately
# the previous pruning stage could potentially have made certain items dependant on others
# in the same or later sphere (because the location had 2 ways to access but the item originally
# used to access it was deemed not required.) So we need to do one final sphere collection pass
# to build up the correct spheres
required_locations = {item for sphere in collection_spheres for item in sphere}
state = CollectionState(multiworld)
collection_spheres = []
while required_locations:
sphere = set(filter(state.can_reach, required_locations))
for location in sphere:
state.collect(location.item, True, location)
collection_spheres.append(sphere)
logging.debug('Calculated final sphere %i, containing %i of %i progress items.', len(collection_spheres),
len(sphere), len(required_locations))
required_locations -= sphere
if not sphere:
raise RuntimeError(f'Not all required items reachable. Unreachable locations: {required_locations}')
# we can finally output our playthrough
self.playthrough = {"0": sorted([self.multiworld.get_name_string_for_object(item) for item in
chain.from_iterable(multiworld.precollected_items.values())
if item.advancement])}
for i, sphere in enumerate(collection_spheres):
self.playthrough[str(i + 1)] = {
str(location): str(location.item) for location in sorted(sphere)}
if create_paths:
self.create_paths(state, collection_spheres)
# repair the multiworld again
for location, item in restore_later.items():
location.item = item
for item in removed_precollected:
multiworld.push_precollected(item)
def create_paths(self, state: CollectionState, collection_spheres: List[Set[Location]]) -> None:
from itertools import zip_longest
multiworld = self.multiworld
def flist_to_iter(path_value: Optional[PathValue]) -> Iterator[str]:
while path_value:
region_or_entrance, path_value = path_value
yield region_or_entrance
def get_path(state: CollectionState, region: Region) -> List[Union[Tuple[str, str], Tuple[str, None]]]:
reversed_path_as_flist: PathValue = state.path.get(region, (str(region), None))
string_path_flat = reversed(list(map(str, flist_to_iter(reversed_path_as_flist))))
# Now we combine the flat string list into (region, exit) pairs
pathsiter = iter(string_path_flat)
pathpairs = zip_longest(pathsiter, pathsiter)
return list(pathpairs)
self.paths = {}
topology_worlds = (player for player in multiworld.player_ids if multiworld.worlds[player].topology_present)
for player in topology_worlds:
self.paths.update(
{str(location): get_path(state, location.parent_region)
for sphere in collection_spheres for location in sphere
if location.player == player})
if player in multiworld.get_game_players("A Link to the Past"):
# If Pyramid Fairy Entrance needs to be reached, also path to Big Bomb Shop
# Maybe move the big bomb over to the Event system instead?
if any(exit_path == 'Pyramid Fairy' for path in self.paths.values()
for (_, exit_path) in path):
2024-08-13 16:02:09 +00:00
if multiworld.worlds[player].options.mode != 'inverted':
self.paths[str(multiworld.get_region('Big Bomb Shop', player))] = \
get_path(state, multiworld.get_region('Big Bomb Shop', player))
else:
self.paths[str(multiworld.get_region('Inverted Big Bomb Shop', player))] = \
get_path(state, multiworld.get_region('Inverted Big Bomb Shop', player))
def to_file(self, filename: str) -> None:
from itertools import chain
from worlds import AutoWorld
from Options import Visibility
def write_option(option_key: str, option_obj: Options.AssembleOptions) -> None:
Core: move option results to the World class instead of MultiWorld (#993) :crossed_fingers: * map option objects to a `World.options` dict * convert RoR2 to options dict system for testing * add temp behavior for lttp with notes * copy/paste bad * convert `set_default_common_options` to a namespace property * reorganize test call order * have fill_restrictive use the new options system * update world api * update soe tests * fix world api * core: auto initialize a dataclass on the World class with the option results * core: auto initialize a dataclass on the World class with the option results: small tying improvement * add `as_dict` method to the options dataclass * fix namespace issues with tests * have current option updates use `.value` instead of changing the option * update ror2 to use the new options system again * revert the junk pool dict since it's cased differently * fix begin_with_loop typo * write new and old options to spoiler * change factorio option behavior back * fix comparisons * move common and per_game_common options to new system * core: automatically create missing options_dataclass from legacy option_definitions * remove spoiler special casing and add back the Factorio option changing but in new system * give ArchipIDLE the default options_dataclass so its options get generated and spoilered properly * reimplement `inspect.get_annotations` * move option info generation for webhost to new system * need to include Common and PerGame common since __annotations__ doesn't include super * use get_type_hints for the options dictionary * typing.get_type_hints returns the bases too. * forgot to sweep through generate * sweep through all the tests * swap to a metaclass property * move remaining usages from get_type_hints to metaclass property * move remaining usages from __annotations__ to metaclass property * move remaining usages from legacy dictionaries to metaclass property * remove legacy dictionaries * cache the metaclass property * clarify inheritance in world api * move the messenger to new options system * add an assert for my dumb * update the doc * rename o to options * missed a spot * update new messenger options * comment spacing Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com> * fix tests * fix missing import * make the documentation definition more accurate * use options system for loc creation * type cast MessengerWorld * fix typo and use quotes for cast * LTTP: set random seed in tests * ArchipIdle: remove change here as it's default on AutoWorld * Stardew: Need to set state because `set_default_common_options` used to * The Messenger: update shop rando and helpers to new system; optimize imports * Add a kwarg to `as_dict` to do the casing for you * RoR2: use new kwarg for less code * RoR2: revert some accidental reverts * The Messenger: remove an unnecessary variable * remove TypeVar that isn't used * CommonOptions not abstract * Docs: fix mistake in options api.md Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com> * create options for item link worlds * revert accidental doc removals * Item Links: set default options on group * change Zillion to new options dataclass * remove unused parameter to function * use TypeGuard for Literal narrowing * move dlc quest to new api * move overcooked 2 to new api * fixed some missed code in oc2 * - Tried to be compliant with 993 (WIP?) * - I think it all works now * - Removed last trace of me touching core * typo * It now passes all tests! * Improve options, fix all issues I hope * - Fixed init options * dlcquest: fix bad imports * missed a file * - Reduce code duplication * add as_dict documentation * - Use .items(), get option name more directly, fix slot data content * - Remove generic options from the slot data * improve slot data documentation * remove `CommonOptions.get_value` (#21) * better slot data description Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --------- Co-authored-by: el-u <109771707+el-u@users.noreply.github.com> Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com> Co-authored-by: Doug Hoskisson <beauxq@yahoo.com> Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> Co-authored-by: Alex Gilbert <alexgilbert@yahoo.com>
2023-10-10 20:30:20 +00:00
res = getattr(self.multiworld.worlds[player].options, option_key)
if res.visibility & Visibility.spoiler:
display_name = getattr(option_obj, "display_name", option_key)
outfile.write(f"{display_name + ':':33}{res.current_option_name}\n")
2020-03-09 23:36:26 +00:00
with open(filename, 'w', encoding="utf-8-sig") as outfile:
outfile.write(
2021-01-03 13:32:32 +00:00
'Archipelago Version %s - Seed: %s\n\n' % (
Utils.__version__, self.multiworld.seed))
outfile.write('Filling Algorithm: %s\n' % self.multiworld.algorithm)
outfile.write('Players: %d\n' % self.multiworld.players)
outfile.write(f'Plando Options: {self.multiworld.plando_options}\n')
AutoWorld.call_stage(self.multiworld, "write_spoiler_header", outfile)
for player in range(1, self.multiworld.players + 1):
if self.multiworld.players > 1:
outfile.write('\nPlayer %d: %s\n' % (player, self.multiworld.get_player_name(player)))
outfile.write('Game: %s\n' % self.multiworld.game[player])
Core: move option results to the World class instead of MultiWorld (#993) :crossed_fingers: * map option objects to a `World.options` dict * convert RoR2 to options dict system for testing * add temp behavior for lttp with notes * copy/paste bad * convert `set_default_common_options` to a namespace property * reorganize test call order * have fill_restrictive use the new options system * update world api * update soe tests * fix world api * core: auto initialize a dataclass on the World class with the option results * core: auto initialize a dataclass on the World class with the option results: small tying improvement * add `as_dict` method to the options dataclass * fix namespace issues with tests * have current option updates use `.value` instead of changing the option * update ror2 to use the new options system again * revert the junk pool dict since it's cased differently * fix begin_with_loop typo * write new and old options to spoiler * change factorio option behavior back * fix comparisons * move common and per_game_common options to new system * core: automatically create missing options_dataclass from legacy option_definitions * remove spoiler special casing and add back the Factorio option changing but in new system * give ArchipIDLE the default options_dataclass so its options get generated and spoilered properly * reimplement `inspect.get_annotations` * move option info generation for webhost to new system * need to include Common and PerGame common since __annotations__ doesn't include super * use get_type_hints for the options dictionary * typing.get_type_hints returns the bases too. * forgot to sweep through generate * sweep through all the tests * swap to a metaclass property * move remaining usages from get_type_hints to metaclass property * move remaining usages from __annotations__ to metaclass property * move remaining usages from legacy dictionaries to metaclass property * remove legacy dictionaries * cache the metaclass property * clarify inheritance in world api * move the messenger to new options system * add an assert for my dumb * update the doc * rename o to options * missed a spot * update new messenger options * comment spacing Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com> * fix tests * fix missing import * make the documentation definition more accurate * use options system for loc creation * type cast MessengerWorld * fix typo and use quotes for cast * LTTP: set random seed in tests * ArchipIdle: remove change here as it's default on AutoWorld * Stardew: Need to set state because `set_default_common_options` used to * The Messenger: update shop rando and helpers to new system; optimize imports * Add a kwarg to `as_dict` to do the casing for you * RoR2: use new kwarg for less code * RoR2: revert some accidental reverts * The Messenger: remove an unnecessary variable * remove TypeVar that isn't used * CommonOptions not abstract * Docs: fix mistake in options api.md Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com> * create options for item link worlds * revert accidental doc removals * Item Links: set default options on group * change Zillion to new options dataclass * remove unused parameter to function * use TypeGuard for Literal narrowing * move dlc quest to new api * move overcooked 2 to new api * fixed some missed code in oc2 * - Tried to be compliant with 993 (WIP?) * - I think it all works now * - Removed last trace of me touching core * typo * It now passes all tests! * Improve options, fix all issues I hope * - Fixed init options * dlcquest: fix bad imports * missed a file * - Reduce code duplication * add as_dict documentation * - Use .items(), get option name more directly, fix slot data content * - Remove generic options from the slot data * improve slot data documentation * remove `CommonOptions.get_value` (#21) * better slot data description Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --------- Co-authored-by: el-u <109771707+el-u@users.noreply.github.com> Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com> Co-authored-by: Doug Hoskisson <beauxq@yahoo.com> Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> Co-authored-by: Alex Gilbert <alexgilbert@yahoo.com>
2023-10-10 20:30:20 +00:00
for f_option, option in self.multiworld.worlds[player].options_dataclass.type_hints.items():
write_option(f_option, option)
AutoWorld.call_single(self.multiworld, "write_spoiler_header", player, outfile)
2021-06-03 22:29:59 +00:00
if self.entrances:
outfile.write('\n\nEntrances:\n\n')
outfile.write('\n'.join(['%s%s %s %s' % (f'{self.multiworld.get_player_name(entry["player"])}: '
if self.multiworld.players > 1 else '', entry['entrance'],
2020-08-23 19:38:21 +00:00
'<=>' if entry['direction'] == 'both' else
'<=' if entry['direction'] == 'exit' else '=>',
entry['exit']) for entry in self.entrances.values()]))
2021-06-06 15:13:34 +00:00
AutoWorld.call_all(self.multiworld, "write_spoiler", outfile)
precollected_items = [f"{item.name} ({self.multiworld.get_player_name(item.player)})"
if self.multiworld.players > 1
else item.name
for item in chain.from_iterable(self.multiworld.precollected_items.values())]
if precollected_items:
outfile.write("\n\nStarting Items:\n\n")
outfile.write("\n".join([item for item in precollected_items]))
locations = [(str(location), str(location.item) if location.item is not None else "Nothing")
for location in self.multiworld.get_locations() if location.show_in_spoiler]
outfile.write('\n\nLocations:\n\n')
outfile.write('\n'.join(
['%s: %s' % (location, item) for location, item in locations]))
outfile.write('\n\nPlaythrough:\n\n')
outfile.write('\n'.join(['%s: {\n%s\n}' % (sphere_nr, '\n'.join(
[f" {location}: {item}" for (location, item) in sphere.items()] if isinstance(sphere, dict) else
[f" {item}" for item in sphere])) for (sphere_nr, sphere) in self.playthrough.items()]))
if self.unreachables:
outfile.write('\n\nUnreachable Items:\n\n')
outfile.write(
'\n'.join(['%s: %s' % (unreachable.item, unreachable) for unreachable in self.unreachables]))
2021-06-06 15:13:34 +00:00
if self.paths:
outfile.write('\n\nPaths:\n\n')
path_listings: List[str] = []
2021-06-06 15:13:34 +00:00
for location, path in sorted(self.paths.items()):
path_lines: List[str] = []
2021-06-06 15:13:34 +00:00
for region, exit in path:
if exit is not None:
path_lines.append("{} -> {}".format(region, exit))
else:
path_lines.append(region)
path_listings.append("{}\n {}".format(location, "\n => ".join(path_lines)))
outfile.write('\n'.join(path_listings))
AutoWorld.call_all(self.multiworld, "write_spoiler_end", outfile)
class Tutorial(NamedTuple):
"""Class to build website tutorial pages from a .md file in the world's /docs folder. Order is as follows.
Name of the tutorial as it will appear on the site. Concise description covering what the guide will entail.
Language the guide is written in. Name of the file ex 'setup_en.md'. Name of the link on the site; game name is
filled automatically so 'setup/en' etc. Author or authors."""
tutorial_name: str
description: str
language: str
file_name: str
link: str
authors: List[str]
class PlandoOptions(IntFlag):
none = 0b0000
items = 0b0001
connections = 0b0010
texts = 0b0100
bosses = 0b1000
@classmethod
def from_option_string(cls, option_string: str) -> PlandoOptions:
result = cls(0)
for part in option_string.split(","):
part = part.strip().lower()
if part:
result = cls._handle_part(part, result)
return result
@classmethod
def from_set(cls, option_set: Set[str]) -> PlandoOptions:
result = cls(0)
for part in option_set:
result = cls._handle_part(part, result)
return result
@classmethod
def _handle_part(cls, part: str, base: PlandoOptions) -> PlandoOptions:
try:
return base | cls[part]
except Exception as e:
raise KeyError(f"{part} is not a recognized name for a plando module. "
f"Known options: {', '.join(str(flag.name) for flag in cls)}") from e
def __str__(self) -> str:
if self.value:
return ", ".join(str(flag.name) for flag in PlandoOptions if self.value & flag.value)
return "None"
seeddigits = 20
def get_seed(seed: Optional[int] = None) -> int:
if seed is None:
random.seed(None)
return random.randint(0, pow(10, seeddigits) - 1)
return seed