2020-03-02 23:12:14 +00:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2024-07-31 10:04:21 +00:00
|
|
|
import collections
|
2017-05-15 18:28:04 +00:00
|
|
|
import copy
|
2023-10-29 18:47:37 +00:00
|
|
|
import itertools
|
2021-02-20 01:30:55 +00:00
|
|
|
import functools
|
2023-01-15 17:10:26 +00:00
|
|
|
import logging
|
|
|
|
import random
|
|
|
|
import secrets
|
|
|
|
import typing # this can go away when Python 3.8 support is dropped
|
|
|
|
from argparse import Namespace
|
2023-10-29 18:47:37 +00:00
|
|
|
from collections import Counter, deque
|
|
|
|
from collections.abc import Collection, MutableSequence
|
2023-03-08 21:19:38 +00:00
|
|
|
from enum import IntEnum, IntFlag
|
2024-05-07 06:15:09 +00:00
|
|
|
from typing import Any, Callable, Dict, Iterable, Iterator, List, Mapping, NamedTuple, Optional, Set, Tuple, \
|
|
|
|
TypedDict, Union, Type, ClassVar
|
2020-07-14 05:01:51 +00:00
|
|
|
|
2023-01-15 17:10:26 +00:00
|
|
|
import NetUtils
|
2021-09-16 22:17:54 +00:00
|
|
|
import Options
|
2021-09-13 00:01:15 +00:00
|
|
|
import Utils
|
|
|
|
|
2024-02-14 21:56:21 +00:00
|
|
|
if typing.TYPE_CHECKING:
|
|
|
|
from worlds import AutoWorld
|
|
|
|
|
2023-03-08 21:19:38 +00:00
|
|
|
|
2022-02-17 05:07:11 +00:00
|
|
|
class Group(TypedDict, total=False):
|
2022-02-05 14:49:19 +00:00
|
|
|
name: str
|
|
|
|
game: str
|
2024-02-14 21:56:21 +00:00
|
|
|
world: "AutoWorld.World"
|
2022-02-05 14:49:19 +00:00
|
|
|
players: Set[int]
|
2022-02-17 05:07:11 +00:00
|
|
|
item_pool: Set[str]
|
|
|
|
replacement_items: Dict[int, Optional[str]]
|
2022-05-15 14:41:11 +00:00
|
|
|
local_items: Set[str]
|
|
|
|
non_local_items: Set[str]
|
2022-12-07 05:37:47 +00:00
|
|
|
link_replacement: bool
|
2022-02-05 14:49:19 +00:00
|
|
|
|
2020-07-14 05:01:51 +00:00
|
|
|
|
2023-05-24 23:24:12 +00:00
|
|
|
class ThreadBarrierProxy:
|
2023-02-02 00:14:23 +00:00
|
|
|
"""Passes through getattr while passthrough is True"""
|
2023-05-24 23:24:12 +00:00
|
|
|
def __init__(self, obj: object) -> None:
|
2023-02-02 00:14:23 +00:00
|
|
|
self.passthrough = True
|
|
|
|
self.obj = obj
|
|
|
|
|
2023-05-24 23:24:12 +00:00
|
|
|
def __getattr__(self, name: str) -> Any:
|
2023-02-02 00:14:23 +00:00
|
|
|
if self.passthrough:
|
2023-05-24 23:24:12 +00:00
|
|
|
return getattr(self.obj, name)
|
2023-02-02 00:14:23 +00:00
|
|
|
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.")
|
|
|
|
|
|
|
|
|
2020-10-24 03:38:56 +00:00
|
|
|
class MultiWorld():
|
2020-04-10 18:54:18 +00:00
|
|
|
debug_types = False
|
2021-08-09 07:15:41 +00:00
|
|
|
player_name: Dict[int, str]
|
2021-01-02 21:41:03 +00:00
|
|
|
plando_texts: List[Dict[str, str]]
|
2022-03-27 23:47:47 +00:00
|
|
|
plando_items: List[List[Dict[str, Any]]]
|
2021-07-21 16:08:15 +00:00
|
|
|
plando_connections: List
|
2024-02-14 21:56:21 +00:00
|
|
|
worlds: Dict[int, "AutoWorld.World"]
|
2022-02-05 14:49:19 +00:00
|
|
|
groups: Dict[int, Group]
|
2023-10-29 18:47:37 +00:00
|
|
|
regions: RegionManager
|
2022-03-27 23:47:47 +00:00
|
|
|
itempool: List[Item]
|
2021-07-20 19:19:53 +00:00
|
|
|
is_race: bool = False
|
2021-10-10 14:50:01 +00:00
|
|
|
precollected_items: Dict[int, List[Item]]
|
2022-02-17 05:07:11 +00:00
|
|
|
state: CollectionState
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2023-01-17 16:25:59 +00:00
|
|
|
plando_options: PlandoOptions
|
2022-11-16 16:32:33 +00:00
|
|
|
early_items: Dict[int, Dict[str, int]]
|
|
|
|
local_early_items: Dict[int, Dict[str, int]]
|
2022-03-27 23:47:47 +00:00
|
|
|
local_items: Dict[int, Options.LocalItems]
|
|
|
|
non_local_items: Dict[int, Options.NonLocalItems]
|
|
|
|
progression_balancing: Dict[int, Options.ProgressionBalancing]
|
2022-04-07 17:42:30 +00:00
|
|
|
completion_condition: Dict[int, Callable[[CollectionState], bool]]
|
2022-09-30 02:58:19 +00:00
|
|
|
indirect_connections: Dict[Region, Set[Entrance]]
|
2022-09-28 21:54:10 +00:00
|
|
|
exclude_locations: Dict[int, Options.ExcludeLocations]
|
2023-03-13 22:45:56 +00:00
|
|
|
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]
|
2022-03-27 23:47:47 +00:00
|
|
|
|
2022-10-18 16:54:41 +00:00
|
|
|
game: Dict[int, str]
|
2022-09-30 02:58:19 +00:00
|
|
|
|
2023-02-02 00:14:23 +00:00
|
|
|
random: random.Random
|
2024-03-10 17:47:45 +00:00
|
|
|
per_slot_randoms: Utils.DeprecateDict[int, random.Random]
|
2023-07-02 10:50:14 +00:00
|
|
|
"""Deprecated. Please use `self.random` instead."""
|
2023-02-02 00:14:23 +00:00
|
|
|
|
2021-04-10 04:36:06 +00:00
|
|
|
class AttributeProxy():
|
|
|
|
def __init__(self, rule):
|
|
|
|
self.rule = rule
|
2021-04-10 16:45:11 +00:00
|
|
|
|
2021-04-10 04:36:06 +00:00
|
|
|
def __getitem__(self, player) -> bool:
|
|
|
|
return self.rule(player)
|
|
|
|
|
2023-10-29 18:47:37 +00:00
|
|
|
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):
|
2024-02-25 20:56:27 +00:00
|
|
|
assert region.name not in self.region_cache[region.player], \
|
|
|
|
f"{region.name} already exists in region cache."
|
2023-10-29 18:47:37 +00:00
|
|
|
self.region_cache[region.player][region.name] = region
|
|
|
|
|
|
|
|
def extend(self, regions: Iterable[Region]):
|
|
|
|
for region in regions:
|
2024-02-25 20:56:27 +00:00
|
|
|
assert region.name not in self.region_cache[region.player], \
|
|
|
|
f"{region.name} already exists in region cache."
|
2023-10-29 18:47:37 +00:00
|
|
|
self.region_cache[region.player][region.name] = region
|
|
|
|
|
2023-11-16 10:55:18 +00:00
|
|
|
def add_group(self, new_id: int):
|
|
|
|
self.region_cache[new_id] = {}
|
|
|
|
self.entrance_cache[new_id] = {}
|
|
|
|
self.location_cache[new_id] = {}
|
|
|
|
|
2023-10-29 18:47:37 +00:00
|
|
|
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):
|
2023-02-02 00:14:23 +00:00
|
|
|
# world-local random state is saved for multiple generations running concurrently
|
|
|
|
self.random = ThreadBarrierProxy(random.Random())
|
2019-04-18 09:23:24 +00:00
|
|
|
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'
|
2022-02-05 14:49:19 +00:00
|
|
|
self.groups = {}
|
2023-10-29 18:47:37 +00:00
|
|
|
self.regions = self.RegionManager(players)
|
2018-02-17 23:38:54 +00:00
|
|
|
self.shops = []
|
2017-05-15 18:28:04 +00:00
|
|
|
self.itempool = []
|
2017-05-20 12:03:15 +00:00
|
|
|
self.seed = None
|
2021-05-15 22:21:00 +00:00
|
|
|
self.seed_name: str = "Unavailable"
|
2021-10-10 14:50:01 +00:00
|
|
|
self.precollected_items = {player: [] for player in self.player_ids}
|
2017-06-04 14:15:59 +00:00
|
|
|
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
|
2018-01-22 02:43:44 +00:00
|
|
|
self.rupoor_cost = 10
|
2017-08-01 16:58:42 +00:00
|
|
|
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
|
2017-07-18 10:44:13 +00:00
|
|
|
self.spoiler = Spoiler(self)
|
2022-11-16 16:32:33 +00:00
|
|
|
self.early_items = {player: {} for player in self.player_ids}
|
|
|
|
self.local_early_items = {player: {} for player in self.player_ids}
|
2022-09-30 02:58:19 +00:00
|
|
|
self.indirect_connections = {}
|
2023-04-11 01:18:29 +00:00
|
|
|
self.start_inventory_from_pool: Dict[int, Options.StartInventoryPool] = {}
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2019-12-17 20:09:33 +00:00
|
|
|
for player in range(1, players + 1):
|
|
|
|
def set_player_attr(attr, val):
|
|
|
|
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', [])
|
2024-04-18 16:33:16 +00:00
|
|
|
set_player_attr('game', "Archipelago")
|
2021-02-22 10:18:53 +00:00
|
|
|
set_player_attr('completion_condition', lambda state: True)
|
2021-06-11 12:22:44 +00:00
|
|
|
self.worlds = {}
|
2024-03-10 17:47:45 +00:00
|
|
|
self.per_slot_randoms = Utils.DeprecateDict("Using per_slot_randoms is now deprecated. Please use the "
|
|
|
|
"world's random object instead (usually self.random)")
|
2023-01-17 16:25:59 +00:00
|
|
|
self.plando_options = PlandoOptions.none
|
2021-10-06 09:32:49 +00:00
|
|
|
|
2022-11-20 19:50:32 +00:00
|
|
|
def get_all_ids(self) -> Tuple[int, ...]:
|
2022-02-17 05:07:11 +00:00
|
|
|
return self.player_ids + tuple(self.groups)
|
|
|
|
|
2022-02-05 14:49:19 +00:00
|
|
|
def add_group(self, name: str, game: str, players: Set[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."""
|
2024-02-14 21:56:21 +00:00
|
|
|
from worlds import AutoWorld
|
|
|
|
|
2022-02-05 14:49:19 +00:00
|
|
|
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
|
|
|
|
2023-11-16 10:55:18 +00:00
|
|
|
self.regions.add_group(new_id)
|
2022-02-05 14:49:19 +00:00
|
|
|
self.game[new_id] = game
|
|
|
|
self.player_types[new_id] = NetUtils.SlotType.group
|
|
|
|
world_type = AutoWorld.AutoWorldRegister.world_types[game]
|
2023-09-23 23:51:26 +00:00
|
|
|
self.worlds[new_id] = world_type.create_group(self, new_id, players)
|
2022-02-22 09:14:26 +00:00
|
|
|
self.worlds[new_id].collect_item = classmethod(AutoWorld.World.collect_item).__get__(self.worlds[new_id])
|
2022-02-05 14:49:19 +00:00
|
|
|
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
|
|
|
|
|
2022-02-05 15:55:11 +00:00
|
|
|
def get_player_groups(self, player) -> Set[int]:
|
2022-02-05 14:49:19 +00:00
|
|
|
return {group_id for group_id, group in self.groups.items() if player in group["players"]}
|
|
|
|
|
2021-10-06 09:32:49 +00:00
|
|
|
def set_seed(self, seed: Optional[int] = None, secure: bool = False, name: Optional[str] = None):
|
2024-03-10 17:47:45 +00:00
|
|
|
assert not self.worlds, "seed needs to be initialized before Worlds"
|
2021-10-06 09:32:49 +00:00
|
|
|
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
|
|
|
|
2022-10-18 16:54:41 +00:00
|
|
|
def set_options(self, args: Namespace) -> None:
|
2023-12-16 21:21:05 +00:00
|
|
|
# TODO - remove this section once all worlds use options dataclasses
|
2024-02-14 21:56:21 +00:00
|
|
|
from worlds import AutoWorld
|
|
|
|
|
2023-12-16 21:21:05 +00:00
|
|
|
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:
|
2021-07-04 14:18:21 +00:00
|
|
|
world_type = AutoWorld.AutoWorldRegister.world_types[self.game[player]]
|
|
|
|
self.worlds[player] = world_type(self, player)
|
2023-12-16 21:21:05 +00:00
|
|
|
options_dataclass: typing.Type[Options.PerGameCommonOptions] = world_type.options_dataclass
|
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
|
|
|
|
2022-02-17 06:07:34 +00:00
|
|
|
def set_item_links(self):
|
2024-02-14 21:56:21 +00:00
|
|
|
from worlds import AutoWorld
|
|
|
|
|
2022-02-17 05:07:11 +00:00
|
|
|
item_links = {}
|
2022-12-07 05:37:47 +00:00
|
|
|
replacement_prio = [False, True, None]
|
2022-02-17 05:07:11 +00:00
|
|
|
for player in self.player_ids:
|
2023-10-10 20:30:20 +00:00
|
|
|
for item_link in self.worlds[player].options.item_links.value:
|
2022-02-17 05:07:11 +00:00
|
|
|
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]:
|
2022-04-03 17:09:05 +00:00
|
|
|
raise Exception(f"Cannot ItemLink across games. Link: {item_link['name']}")
|
2022-12-07 05:37:47 +00:00
|
|
|
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"]))
|
2022-02-17 05:07:11 +00:00
|
|
|
else:
|
|
|
|
if item_link["name"] in self.player_name.values():
|
2022-12-07 05:37:47 +00:00
|
|
|
raise Exception(f"Cannot name a ItemLink group the same as a player ({item_link['name']}) "
|
|
|
|
f"({self.get_player_name(player)}).")
|
2022-02-17 05:07:11 +00:00
|
|
|
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", [])),
|
2022-05-15 14:41:11 +00:00
|
|
|
"game": self.game[player],
|
|
|
|
"local_items": set(item_link.get("local_items", [])),
|
2022-12-07 05:37:47 +00:00
|
|
|
"non_local_items": set(item_link.get("non_local_items", [])),
|
|
|
|
"link_replacement": replacement_prio.index(item_link["link_replacement"]),
|
2022-02-17 05:07:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
for name, item_link in item_links.items():
|
|
|
|
current_item_name_groups = AutoWorld.AutoWorldRegister.world_types[item_link["game"]].item_name_groups
|
|
|
|
pool = set()
|
2022-05-15 14:41:11 +00:00
|
|
|
local_items = set()
|
|
|
|
non_local_items = set()
|
2022-02-17 05:07:11 +00:00
|
|
|
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})
|
2022-05-15 14:41:11 +00:00
|
|
|
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
|
2022-02-17 05:07:11 +00:00
|
|
|
item_link["item_pool"] = pool
|
2022-05-15 14:41:11 +00:00
|
|
|
item_link["local_items"] = local_items
|
|
|
|
item_link["non_local_items"] = non_local_items
|
2022-02-17 05:07:11 +00:00
|
|
|
|
|
|
|
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"]))
|
2022-12-07 05:37:47 +00:00
|
|
|
|
2022-02-17 05:07:11 +00:00
|
|
|
group["item_pool"] = item_link["item_pool"]
|
|
|
|
group["replacement_items"] = item_link["players"]
|
2022-05-15 14:41:11 +00:00
|
|
|
group["local_items"] = item_link["local_items"]
|
|
|
|
group["non_local_items"] = item_link["non_local_items"]
|
2022-12-07 05:37:47 +00:00
|
|
|
group["link_replacement"] = replacement_prio[item_link["link_replacement"]]
|
2022-02-17 05:07:11 +00:00
|
|
|
|
2024-07-31 10:04:21 +00:00
|
|
|
def link_items(self) -> None:
|
|
|
|
"""Called to link together items in the itempool related to the registered item link groups."""
|
2024-08-04 11:55:34 +00:00
|
|
|
from worlds import AutoWorld
|
|
|
|
|
2024-07-31 10:04:21 +00:00
|
|
|
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
|
2024-08-04 11:55:34 +00:00
|
|
|
|
2024-07-31 10:04:21 +00:00
|
|
|
for player in players.copy():
|
|
|
|
if all([counters[player][item] == 0 for item in shared_pool]):
|
|
|
|
players.remove(player)
|
|
|
|
del (counters[player])
|
2024-08-04 11:55:34 +00:00
|
|
|
|
2024-07-31 10:04:21 +00:00
|
|
|
if not players:
|
|
|
|
return None, None
|
2024-08-04 11:55:34 +00:00
|
|
|
|
2024-07-31 10:04:21 +00:00
|
|
|
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
|
2024-08-04 11:55:34 +00:00
|
|
|
|
2024-07-31 10:04:21 +00:00
|
|
|
common_item_count, classifications = find_common_pool(group["players"], group["item_pool"])
|
|
|
|
if not common_item_count:
|
|
|
|
continue
|
2024-08-04 11:55:34 +00:00
|
|
|
|
2024-07-31 10:04:21 +00:00
|
|
|
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)
|
2024-08-04 11:55:34 +00:00
|
|
|
|
2024-07-31 10:04:21 +00:00
|
|
|
region = Region("Menu", group_id, self, "ItemLink")
|
|
|
|
self.regions.append(region)
|
|
|
|
locations = region.locations
|
|
|
|
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_)
|
2024-08-04 11:55:34 +00:00
|
|
|
|
2024-07-31 10:04:21 +00:00
|
|
|
locations.append(loc)
|
|
|
|
loc.place_locked_item(item)
|
|
|
|
common_item_count[item.player][item.name] -= 1
|
|
|
|
else:
|
|
|
|
new_itempool.append(item)
|
2024-08-04 11:55:34 +00:00
|
|
|
|
2024-07-31 10:04:21 +00:00
|
|
|
itemcount = len(self.itempool)
|
|
|
|
self.itempool = new_itempool
|
2024-08-04 11:55:34 +00:00
|
|
|
|
2024-07-31 10:04:21 +00:00
|
|
|
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)])
|
|
|
|
|
2020-07-14 05:01:51 +00:00
|
|
|
def secure(self):
|
2023-02-02 00:14:23 +00:00
|
|
|
self.random = ThreadBarrierProxy(secrets.SystemRandom())
|
2021-07-20 18:21:27 +00:00
|
|
|
self.is_race = True
|
2020-07-14 05:01:51 +00:00
|
|
|
|
2021-07-04 14:44:27 +00:00
|
|
|
@functools.cached_property
|
2022-11-20 19:50:32 +00:00
|
|
|
def player_ids(self) -> Tuple[int, ...]:
|
2021-07-04 20:21:53 +00:00
|
|
|
return tuple(range(1, self.players + 1))
|
2020-06-19 01:01:23 +00:00
|
|
|
|
2023-10-31 01:08:56 +00:00
|
|
|
@Utils.cache_self1
|
2022-11-20 19:50:32 +00:00
|
|
|
def get_game_players(self, game_name: str) -> Tuple[int, ...]:
|
2021-07-21 16:08:15 +00:00
|
|
|
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
|
|
|
|
2023-10-31 01:08:56 +00:00
|
|
|
@Utils.cache_self1
|
2023-10-28 01:13:08 +00:00
|
|
|
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)
|
|
|
|
|
2023-10-31 01:08:56 +00:00
|
|
|
@Utils.cache_self1
|
2021-08-30 14:31:56 +00:00
|
|
|
def get_game_worlds(self, game_name: str):
|
2022-02-05 14:49:19 +00:00
|
|
|
return tuple(world for player, world in self.worlds.items() if
|
|
|
|
player not in self.groups and self.game[player] == game_name)
|
2021-08-30 14:31:56 +00:00
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def get_name_string_for_object(self, obj) -> str:
|
2021-08-09 07:15:41 +00:00
|
|
|
return obj.name if self.players == 1 else f'{obj.name} ({self.get_player_name(obj.player)})'
|
2020-01-14 09:42:27 +00:00
|
|
|
|
2021-08-09 07:15:41 +00:00
|
|
|
def get_player_name(self, player: int) -> str:
|
|
|
|
return self.player_name[player]
|
2020-01-14 09:42:27 +00:00
|
|
|
|
2022-04-03 03:47:42 +00:00
|
|
|
def get_file_safe_player_name(self, player: int) -> str:
|
2023-03-20 16:01:08 +00:00
|
|
|
return Utils.get_file_safe_name(self.get_player_name(player))
|
2022-04-03 03:47:42 +00:00
|
|
|
|
2022-10-02 14:53:18 +00:00
|
|
|
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 """
|
2022-11-20 19:39:52 +00:00
|
|
|
return f"AP_{self.seed_name}_P{player}_{self.get_file_safe_player_name(player).replace(' ', '_')}"
|
2022-10-02 14:53:18 +00:00
|
|
|
|
2021-02-20 01:30:55 +00:00
|
|
|
@functools.cached_property
|
|
|
|
def world_name_lookup(self):
|
2021-08-09 07:15:41 +00:00
|
|
|
return {self.player_name[player_id]: player_id for player_id in self.player_ids}
|
2021-02-20 01:30:55 +00:00
|
|
|
|
2023-10-29 18:47:37 +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
|
|
|
|
2023-10-29 18:47: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
|
|
|
|
2023-10-29 18:47:37 +00:00
|
|
|
def get_location(self, location_name: str, player: int) -> Location:
|
|
|
|
return self.regions.location_cache[player][location_name]
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2021-09-01 19:01:54 +00:00
|
|
|
def get_all_state(self, use_cache: bool) -> CollectionState:
|
2021-09-01 00:19:26 +00:00
|
|
|
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()
|
|
|
|
|
2017-06-17 12:40:37 +00:00
|
|
|
ret = CollectionState(self)
|
2017-11-04 18:23:57 +00:00
|
|
|
|
2017-06-17 12:40:37 +00:00
|
|
|
for item in self.itempool:
|
2021-07-04 13:47:11 +00:00
|
|
|
self.worlds[item.player].collect(ret, item)
|
2022-02-13 22:02:18 +00:00
|
|
|
for player in self.player_ids:
|
|
|
|
subworld = self.worlds[player]
|
|
|
|
for item in subworld.get_pre_fill_items():
|
2021-09-01 00:19:26 +00:00
|
|
|
subworld.collect(ret, item)
|
2017-07-17 21:14:31 +00:00
|
|
|
ret.sweep_for_events()
|
2021-10-06 09:32:49 +00:00
|
|
|
|
2021-09-01 19:01:54 +00:00
|
|
|
if use_cache:
|
2021-09-01 18:20:43 +00:00
|
|
|
self._all_state = ret
|
2017-06-17 12:40:37 +00:00
|
|
|
return ret
|
|
|
|
|
2022-01-31 21:23:01 +00:00
|
|
|
def get_items(self) -> List[Item]:
|
2018-01-03 01:01:16 +00:00
|
|
|
return [loc.item for loc in self.get_filled_locations()] + self.itempool
|
|
|
|
|
2023-01-24 02:42:13 +00:00
|
|
|
def find_item_locations(self, item, 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
|
2021-11-27 21:57:54 +00:00
|
|
|
location.item and location.item.name == item and location.item.player == player]
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2021-03-13 23:27:06 +00:00
|
|
|
def find_item(self, item, 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)
|
|
|
|
|
2023-01-24 02:42:13 +00:00
|
|
|
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)]
|
2021-11-27 21:57:54 +00:00
|
|
|
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)
|
2021-03-13 23:27:06 +00:00
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def push_precollected(self, item: Item):
|
2021-10-10 14:50:01 +00:00
|
|
|
self.precollected_items[item.player].append(item)
|
2019-08-10 19:30:14 +00:00
|
|
|
self.state.collect(item, True)
|
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def push_item(self, location: Location, item: Item, collect: bool = True):
|
2022-07-14 07:46:03 +00:00
|
|
|
location.item = item
|
|
|
|
item.location = location
|
|
|
|
if collect:
|
2024-04-14 18:37:48 +00:00
|
|
|
self.state.collect(item, location.advancement, location)
|
2022-07-14 07:46:03 +00:00
|
|
|
|
|
|
|
logging.debug('Placed %s at %s', item, location)
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2023-10-29 18:47:37 +00:00
|
|
|
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))
|
2019-01-20 07:01:02 +00:00
|
|
|
|
2022-09-30 02:58:19 +00:00
|
|
|
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)
|
|
|
|
|
2023-10-29 18:47:37 +00:00
|
|
|
def get_locations(self, player: Optional[int] = None) -> Iterable[Location]:
|
2022-11-20 19:50:32 +00:00
|
|
|
if player is not None:
|
2023-10-29 18:47:37 +00:00
|
|
|
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
|
|
|
|
2022-03-27 23:47:47 +00:00
|
|
|
def get_unfilled_locations(self, player: Optional[int] = None) -> List[Location]:
|
2022-11-20 19:50:32 +00:00
|
|
|
return [location for location in self.get_locations(player) if location.item is None]
|
2020-10-07 17:51:46 +00:00
|
|
|
|
2022-03-27 23:47:47 +00:00
|
|
|
def get_filled_locations(self, player: Optional[int] = None) -> List[Location]:
|
2022-11-20 19:50:32 +00:00
|
|
|
return [location for location in self.get_locations(player) if location.item is not None]
|
2017-06-17 12:40:37 +00:00
|
|
|
|
2022-03-27 23:47:47 +00:00
|
|
|
def get_reachable_locations(self, state: Optional[CollectionState] = None, player: Optional[int] = None) -> List[Location]:
|
2022-11-20 19:50:32 +00:00
|
|
|
state: CollectionState = state if state else self.state
|
|
|
|
return [location for location in self.get_locations(player) if location.can_reach(state)]
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2021-10-06 09:32:49 +00:00
|
|
|
def get_placeable_locations(self, state=None, player=None) -> List[Location]:
|
2022-11-20 19:50:32 +00:00
|
|
|
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)]
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2022-11-20 19:50:32 +00:00
|
|
|
def get_unfilled_locations_for_players(self, location_names: List[str], players: Iterable[int]):
|
2021-01-04 14:14:20 +00:00
|
|
|
for player in players:
|
2022-11-20 19:50:32 +00:00
|
|
|
if not location_names:
|
2023-07-31 21:16:42 +00:00
|
|
|
valid_locations = [location.name for location in self.get_unfilled_locations(player)]
|
|
|
|
else:
|
|
|
|
valid_locations = location_names
|
2023-10-29 18:47:37 +00:00
|
|
|
relevant_cache = self.regions.location_cache[player]
|
2023-07-31 21:16:42 +00:00
|
|
|
for location_name in valid_locations:
|
2023-10-29 18:47:37 +00:00
|
|
|
location = relevant_cache.get(location_name, None)
|
|
|
|
if location and location.item is None:
|
2022-01-20 18:34:17 +00:00
|
|
|
yield location
|
2021-01-04 14:14:20 +00:00
|
|
|
|
2022-03-27 23:47:47 +00:00
|
|
|
def unlocks_new_location(self, item: Item) -> bool:
|
2017-05-15 18:28:04 +00:00
|
|
|
temp_state = self.state.copy()
|
2017-06-17 12:40:37 +00:00
|
|
|
temp_state.collect(item, True)
|
2017-05-26 07:55:49 +00:00
|
|
|
|
2023-10-29 18:47:37 +00:00
|
|
|
for location in self.get_unfilled_locations(item.player):
|
2017-05-26 07:55:49 +00:00
|
|
|
if temp_state.can_reach(location) and not self.state.can_reach(location):
|
|
|
|
return True
|
|
|
|
|
|
|
|
return False
|
2017-11-19 01:43:37 +00:00
|
|
|
|
2022-03-27 23:47:47 +00:00
|
|
|
def has_beaten_game(self, state: CollectionState, player: Optional[int] = None) -> bool:
|
2019-07-10 02:18:24 +00:00
|
|
|
if player:
|
2021-02-22 10:18:53 +00:00
|
|
|
return self.completion_condition[player](state)
|
2019-07-10 02:18:24 +00:00
|
|
|
else:
|
|
|
|
return all((self.has_beaten_game(state, p) for p in range(1, self.players + 1)))
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2023-12-10 05:43:17 +00:00
|
|
|
def can_beat_game(self, starting_state: Optional[CollectionState] = None) -> bool:
|
2017-06-23 20:15:29 +00:00
|
|
|
if starting_state:
|
2020-03-07 22:20:11 +00:00
|
|
|
if self.has_beaten_game(starting_state):
|
|
|
|
return True
|
2017-06-23 20:15:29 +00:00
|
|
|
state = starting_state.copy()
|
|
|
|
else:
|
2020-03-07 22:20:11 +00:00
|
|
|
if self.has_beaten_game(self.state):
|
|
|
|
return True
|
2017-06-23 20:15:29 +00:00
|
|
|
state = CollectionState(self)
|
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}
|
2018-01-01 20:55:13 +00:00
|
|
|
|
2017-05-16 19:23:47 +00:00
|
|
|
while prog_locations:
|
2023-12-10 05:43:17 +00:00
|
|
|
sphere: Set[Location] = set()
|
2021-03-07 21:05:07 +00:00
|
|
|
# build up spheres of collection radius.
|
|
|
|
# Everything in each sphere is independent from each other in dependencies and only depends on lower spheres
|
2017-05-16 19:23:47 +00:00
|
|
|
for location in prog_locations:
|
2019-07-11 04:12:09 +00:00
|
|
|
if location.can_reach(state):
|
2021-04-29 07:54:49 +00:00
|
|
|
sphere.add(location)
|
2017-05-16 19:23:47 +00:00
|
|
|
|
|
|
|
if not sphere:
|
2019-07-11 04:12:09 +00:00
|
|
|
# ran out of places and did not finish yet, quit
|
2017-05-16 19:23:47 +00:00
|
|
|
return False
|
|
|
|
|
|
|
|
for location in sphere:
|
2018-01-01 20:55:13 +00:00
|
|
|
state.collect(location.item, True, location)
|
2021-04-29 07:54:49 +00:00
|
|
|
prog_locations -= sphere
|
2017-05-16 19:23:47 +00:00
|
|
|
|
2019-07-11 04:12:09 +00:00
|
|
|
if self.has_beaten_game(state):
|
|
|
|
return True
|
|
|
|
|
2017-05-16 19:23:47 +00:00
|
|
|
return False
|
|
|
|
|
2023-12-10 05:43:17 +00:00
|
|
|
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.
|
|
|
|
"""
|
2021-01-17 21:58:52 +00:00
|
|
|
state = CollectionState(self)
|
2021-02-27 17:58:17 +00:00
|
|
|
locations = set(self.get_filled_locations())
|
2021-01-17 21:58:52 +00:00
|
|
|
|
|
|
|
while locations:
|
2023-12-10 05:43:17 +00:00
|
|
|
sphere: Set[Location] = set()
|
2021-01-17 21:58:52 +00:00
|
|
|
|
|
|
|
for location in locations:
|
|
|
|
if location.can_reach(state):
|
|
|
|
sphere.add(location)
|
2021-02-27 17:58:17 +00:00
|
|
|
yield sphere
|
2021-01-17 21:58:52 +00:00
|
|
|
if not sphere:
|
|
|
|
if locations:
|
|
|
|
yield locations # unreachable locations
|
|
|
|
break
|
|
|
|
|
|
|
|
for location in sphere:
|
|
|
|
state.collect(location.item, True, location)
|
|
|
|
locations -= sphere
|
|
|
|
|
2021-01-13 13:27:17 +00:00
|
|
|
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)
|
2022-10-30 14:13:53 +00:00
|
|
|
players: Dict[str, Set[int]] = {
|
|
|
|
"minimal": set(),
|
|
|
|
"items": set(),
|
2024-07-31 10:13:14 +00:00
|
|
|
"full": set()
|
2022-10-30 14:13:53 +00:00
|
|
|
}
|
2024-07-31 10:13:14 +00:00
|
|
|
for player, world in self.worlds.items():
|
|
|
|
players[world.options.accessibility.current_key].add(player)
|
2021-01-11 18:56:18 +00:00
|
|
|
|
|
|
|
beatable_fulfilled = False
|
|
|
|
|
2024-07-31 10:13:14 +00:00
|
|
|
def location_condition(location: Location) -> bool:
|
2021-01-13 13:27:17 +00:00
|
|
|
"""Determine if this location has to be accessible, location is already filtered by location_relevant"""
|
2024-07-31 10:13:14 +00:00
|
|
|
return location.player in players["full"] or \
|
|
|
|
(location.item and location.item.player not in players["minimal"])
|
2021-01-11 18:56:18 +00:00
|
|
|
|
2024-07-31 10:13:14 +00:00
|
|
|
def location_relevant(location: Location) -> bool:
|
2021-01-13 13:27:17 +00:00
|
|
|
"""Determine if this location is relevant to sweep."""
|
2024-07-31 10:13:14 +00:00
|
|
|
return location.progress_type != LocationProgressType.EXCLUDED \
|
|
|
|
and (location.player in players["full"] or location.advancement)
|
2021-01-11 18:56:18 +00:00
|
|
|
|
2022-10-30 14:13:53 +00:00
|
|
|
def all_done() -> bool:
|
2021-01-11 18:56:18 +00:00
|
|
|
"""Check if all access rules are fulfilled"""
|
2022-10-30 14:13:53 +00:00
|
|
|
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
|
2021-01-11 18:56:18 +00:00
|
|
|
|
2022-10-30 14:13:53 +00:00
|
|
|
locations = [location for location in self.get_locations() if location_relevant(location)]
|
2021-01-13 13:27:17 +00:00
|
|
|
|
2021-01-11 18:56:18 +00:00
|
|
|
while locations:
|
2022-10-30 14:13:53 +00:00
|
|
|
sphere: List[Location] = []
|
2022-10-31 19:29:22 +00:00
|
|
|
for n in range(len(locations) - 1, -1, -1):
|
|
|
|
if locations[n].can_reach(state):
|
|
|
|
sphere.append(locations.pop(n))
|
2021-01-11 18:56:18 +00:00
|
|
|
|
|
|
|
if not sphere:
|
|
|
|
# ran out of places and did not finish yet, quit
|
2021-02-26 20:03:16 +00:00
|
|
|
logging.warning(f"Could not access required locations for accessibility check."
|
|
|
|
f" Missing: {locations}")
|
2021-01-11 18:56:18 +00:00
|
|
|
return False
|
|
|
|
|
|
|
|
for location in sphere:
|
2022-10-30 14:13:53 +00:00
|
|
|
if location.item:
|
|
|
|
state.collect(location.item, True, location)
|
2021-01-11 18:56:18 +00:00
|
|
|
|
|
|
|
if self.has_beaten_game(state):
|
|
|
|
beatable_fulfilled = True
|
|
|
|
|
|
|
|
if all_done():
|
|
|
|
return True
|
|
|
|
|
|
|
|
return False
|
2017-05-16 19:23:47 +00:00
|
|
|
|
2021-01-13 13:27:17 +00:00
|
|
|
|
2022-04-27 19:19:53 +00:00
|
|
|
PathValue = Tuple[str, Optional["PathValue"]]
|
|
|
|
|
|
|
|
|
2022-02-17 06:07:34 +00:00
|
|
|
class CollectionState():
|
2023-11-02 05:41:20 +00:00
|
|
|
prog_items: Dict[int, Counter[str]]
|
2022-11-01 02:41:21 +00:00
|
|
|
multiworld: MultiWorld
|
2022-04-27 19:19:53 +00:00
|
|
|
reachable_regions: Dict[int, Set[Region]]
|
|
|
|
blocked_connections: Dict[int, Set[Entrance]]
|
|
|
|
events: 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]] = []
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2020-10-24 03:38:56 +00:00
|
|
|
def __init__(self, parent: MultiWorld):
|
2023-11-16 10:55:18 +00:00
|
|
|
self.prog_items = {player: Counter() for player in parent.get_all_ids()}
|
2022-11-01 02:41:21 +00:00
|
|
|
self.multiworld = parent
|
2022-02-17 05:07:11 +00:00
|
|
|
self.reachable_regions = {player: set() for player in parent.get_all_ids()}
|
|
|
|
self.blocked_connections = {player: set() for player in parent.get_all_ids()}
|
2020-08-22 17:19:29 +00:00
|
|
|
self.events = set()
|
2018-01-01 20:55:13 +00:00
|
|
|
self.path = {}
|
|
|
|
self.locations_checked = set()
|
2022-02-17 05:07:11 +00:00
|
|
|
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)
|
2021-10-10 14:50:01 +00:00
|
|
|
for items in parent.precollected_items.values():
|
|
|
|
for item in items:
|
|
|
|
self.collect(item, True)
|
2018-01-01 20:55:13 +00:00
|
|
|
|
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
|
2023-12-21 03:11:11 +00:00
|
|
|
reachable_regions = self.reachable_regions[player]
|
|
|
|
blocked_connections = self.blocked_connections[player]
|
2020-05-10 09:27:13 +00:00
|
|
|
queue = deque(self.blocked_connections[player])
|
2023-12-21 03:11:11 +00:00
|
|
|
start = self.multiworld.get_region("Menu", player)
|
2020-05-10 09:27:13 +00:00
|
|
|
|
|
|
|
# init on first call - this can't be done on construction since the regions don't exist yet
|
2023-12-21 03:11:11 +00:00
|
|
|
if start not in reachable_regions:
|
|
|
|
reachable_regions.add(start)
|
|
|
|
blocked_connections.update(start.exits)
|
2020-05-10 09:27:13 +00:00
|
|
|
queue.extend(start.exits)
|
|
|
|
|
|
|
|
# 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
|
2023-12-21 03:11:11 +00:00
|
|
|
if new_region in reachable_regions:
|
|
|
|
blocked_connections.remove(connection)
|
2020-06-30 05:32:05 +00:00
|
|
|
elif connection.can_reach(self):
|
2022-11-03 14:17:34 +00:00
|
|
|
assert new_region, f"tried to search through an Entrance \"{connection}\" with no Region"
|
2023-12-21 03:11:11 +00:00
|
|
|
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
|
2022-11-01 02:41:21 +00:00
|
|
|
for new_entrance in self.multiworld.indirect_connections.get(new_region, set()):
|
2023-12-21 03:11:11 +00:00
|
|
|
if new_entrance in blocked_connections and new_entrance not in queue:
|
2020-06-30 05:32:05 +00:00
|
|
|
queue.append(new_entrance)
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def copy(self) -> CollectionState:
|
2022-11-01 02:41:21 +00:00
|
|
|
ret = CollectionState(self.multiworld)
|
2023-11-02 05:41:20 +00:00
|
|
|
ret.prog_items = copy.deepcopy(self.prog_items)
|
2020-03-02 23:12:14 +00:00
|
|
|
ret.reachable_regions = {player: copy.copy(self.reachable_regions[player]) for player in
|
2022-02-17 05:07:11 +00:00
|
|
|
self.reachable_regions}
|
2021-10-06 09:32:49 +00:00
|
|
|
ret.blocked_connections = {player: copy.copy(self.blocked_connections[player]) for player in
|
2022-02-17 05:07:11 +00:00
|
|
|
self.blocked_connections}
|
2017-06-17 12:40:37 +00:00
|
|
|
ret.events = copy.copy(self.events)
|
2018-01-01 20:55:13 +00:00
|
|
|
ret.path = copy.copy(self.path)
|
|
|
|
ret.locations_checked = copy.copy(self.locations_checked)
|
2022-02-17 06:07:34 +00:00
|
|
|
for function in self.additional_copy_functions:
|
|
|
|
ret = function(self, ret)
|
2017-05-15 18:28:04 +00:00
|
|
|
return ret
|
|
|
|
|
2022-03-27 23:47:47 +00:00
|
|
|
def can_reach(self,
|
|
|
|
spot: Union[Location, Entrance, Region, str],
|
|
|
|
resolution_hint: Optional[str] = None,
|
|
|
|
player: Optional[int] = None) -> bool:
|
2022-04-27 19:19:53 +00:00
|
|
|
if isinstance(spot, str):
|
|
|
|
assert isinstance(player, int), "can_reach: player is required if spot is str"
|
2017-05-15 18:28:04 +00:00
|
|
|
# try to resolve a name
|
|
|
|
if resolution_hint == 'Location':
|
2024-03-03 16:25:21 +00:00
|
|
|
return self.can_reach_location(spot, player)
|
2017-05-15 18:28:04 +00:00
|
|
|
elif resolution_hint == 'Entrance':
|
2024-03-03 16:25:21 +00:00
|
|
|
return self.can_reach_entrance(spot, player)
|
2017-05-15 18:28:04 +00:00
|
|
|
else:
|
|
|
|
# default to Region
|
2024-03-03 16:25:21 +00:00
|
|
|
return self.can_reach_region(spot, player)
|
2019-07-09 02:48:16 +00:00
|
|
|
return spot.can_reach(self)
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2024-03-03 16:25:21 +00:00
|
|
|
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)
|
|
|
|
|
2024-07-27 23:32:25 +00:00
|
|
|
def sweep_for_events(self, locations: Optional[Iterable[Location]] = None) -> None:
|
2019-12-13 21:37:52 +00:00
|
|
|
if locations is None:
|
2022-11-01 02:41:21 +00:00
|
|
|
locations = self.multiworld.get_filled_locations()
|
2022-10-13 03:46:07 +00:00
|
|
|
reachable_events = True
|
2021-02-14 16:52:01 +00:00
|
|
|
# since the loop has a good chance to run more than once, only filter the events once
|
2024-07-27 23:32:25 +00:00
|
|
|
locations = {location for location in locations if location.advancement and location not in self.events}
|
|
|
|
|
2022-10-13 03:46:07 +00:00
|
|
|
while reachable_events:
|
2022-03-31 01:30:06 +00:00
|
|
|
reachable_events = {location for location in locations if location.can_reach(self)}
|
2022-10-13 03:46:07 +00:00
|
|
|
locations -= reachable_events
|
|
|
|
for event in reachable_events:
|
2020-08-22 17:19:29 +00:00
|
|
|
self.events.add(event)
|
2022-04-27 19:19:53 +00:00
|
|
|
assert isinstance(event.item, Item), "tried to collect Event with no Item"
|
2020-08-22 17:19:29 +00:00
|
|
|
self.collect(event.item, True, event)
|
2019-07-13 22:17:16 +00:00
|
|
|
|
2023-11-23 23:35:37 +00:00
|
|
|
# item name related
|
2022-03-26 00:12:54 +00:00
|
|
|
def has(self, item: str, player: int, count: int = 1) -> bool:
|
2023-11-02 05:41:20 +00:00
|
|
|
return self.prog_items[player][item] >= count
|
2017-11-19 01:43:37 +00:00
|
|
|
|
2023-11-15 05:53:37 +00:00
|
|
|
def has_all(self, items: Iterable[str], player: int) -> bool:
|
2023-04-16 10:59:53 +00:00
|
|
|
"""Returns True if each item name of items is in state at least once."""
|
2023-11-02 05:41:20 +00:00
|
|
|
return all(self.prog_items[player][item] for item in items)
|
2021-07-15 06:50:08 +00:00
|
|
|
|
2023-11-15 05:53:37 +00:00
|
|
|
def has_any(self, items: Iterable[str], player: int) -> bool:
|
2023-04-16 10:59:53 +00:00
|
|
|
"""Returns True if at least one item name of items is in state at least once."""
|
2023-11-02 05:41:20 +00:00
|
|
|
return any(self.prog_items[player][item] for item in items)
|
2021-07-15 06:50:08 +00:00
|
|
|
|
2024-05-07 06:15:09 +00:00
|
|
|
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())
|
|
|
|
|
2022-03-26 00:12:54 +00:00
|
|
|
def count(self, item: str, player: int) -> int:
|
2023-11-02 05:41:20 +00:00
|
|
|
return self.prog_items[player][item]
|
2022-03-26 00:12:54 +00:00
|
|
|
|
2024-05-07 07:23:25 +00:00
|
|
|
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
|
2024-05-08 16:34:32 +00:00
|
|
|
|
2024-05-25 11:14:13 +00:00
|
|
|
def has_from_list_unique(self, items: Iterable[str], player: int, count: int) -> bool:
|
2024-05-08 16:34:32 +00:00
|
|
|
"""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
|
2024-05-07 07:23:25 +00:00
|
|
|
|
|
|
|
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)
|
2024-05-08 16:34:32 +00:00
|
|
|
|
2024-05-25 11:14:13 +00:00
|
|
|
def count_from_list_unique(self, items: Iterable[str], player: int) -> int:
|
2024-05-08 16:34:32 +00:00
|
|
|
"""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)
|
2024-05-07 07:23:25 +00:00
|
|
|
|
2023-11-23 23:35:37 +00:00
|
|
|
# item name group related
|
2022-04-27 19:19:53 +00:00
|
|
|
def has_group(self, item_name_group: str, player: int, count: int = 1) -> bool:
|
2024-05-07 07:23:25 +00:00
|
|
|
"""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
|
2023-11-15 05:53:37 +00:00
|
|
|
player_prog_items = self.prog_items[player]
|
2022-11-01 02:41:21 +00:00
|
|
|
for item_name in self.multiworld.worlds[player].item_name_groups[item_name_group]:
|
2023-11-15 05:53:37 +00:00
|
|
|
found += player_prog_items[item_name]
|
2021-07-21 07:45:15 +00:00
|
|
|
if found >= count:
|
|
|
|
return True
|
|
|
|
return False
|
|
|
|
|
2024-05-25 11:14:13 +00:00
|
|
|
def has_group_unique(self, item_name_group: str, player: int, count: int = 1) -> bool:
|
2024-05-08 16:34:32 +00:00
|
|
|
"""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
|
|
|
|
|
2022-04-27 19:19:53 +00:00
|
|
|
def count_group(self, item_name_group: str, player: int) -> int:
|
2024-05-07 07:23:25 +00:00
|
|
|
"""Returns the cumulative count of items from an item group present in state."""
|
2023-11-15 05:53:37 +00:00
|
|
|
player_prog_items = self.prog_items[player]
|
2024-05-07 07:23:25 +00:00
|
|
|
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
|
|
|
|
2024-05-25 11:14:13 +00:00
|
|
|
def count_group_unique(self, item_name_group: str, player: int) -> int:
|
2024-05-08 16:34:32 +00:00
|
|
|
"""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]
|
|
|
|
)
|
|
|
|
|
2023-11-23 23:35:37 +00:00
|
|
|
# Item related
|
2022-03-27 23:47:47 +00:00
|
|
|
def collect(self, item: Item, event: bool = False, location: Optional[Location] = None) -> bool:
|
2018-01-01 20:55:13 +00:00
|
|
|
if location:
|
|
|
|
self.locations_checked.add(location)
|
2021-02-26 20:03:16 +00:00
|
|
|
|
2022-11-01 02:41:21 +00:00
|
|
|
changed = self.multiworld.worlds[item.player].collect(self, item)
|
2021-07-04 13:47:11 +00:00
|
|
|
|
|
|
|
if not changed and event:
|
2023-11-02 05:41:20 +00:00
|
|
|
self.prog_items[item.player][item.name] += 1
|
2017-05-26 07:55:49 +00:00
|
|
|
changed = True
|
2020-03-02 23:12:14 +00:00
|
|
|
|
2019-07-11 04:18:30 +00:00
|
|
|
self.stale[item.player] = True
|
2017-05-26 07:55:49 +00:00
|
|
|
|
2021-02-14 16:52:01 +00:00
|
|
|
if changed and not event:
|
|
|
|
self.sweep_for_events()
|
|
|
|
|
|
|
|
return changed
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2021-11-30 04:33:56 +00:00
|
|
|
def remove(self, item: Item):
|
2022-11-01 02:41:21 +00:00
|
|
|
changed = self.multiworld.worlds[item.player].remove(self, item)
|
2021-08-10 07:47:28 +00:00
|
|
|
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
|
2017-05-16 19:23:47 +00:00
|
|
|
|
2021-10-06 09:32:49 +00:00
|
|
|
|
2023-07-09 15:52:20 +00:00
|
|
|
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: Region = None):
|
|
|
|
self.name = name
|
|
|
|
self.parent_region = parent
|
|
|
|
self.player = player
|
|
|
|
|
|
|
|
def can_reach(self, state: CollectionState) -> bool:
|
|
|
|
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):
|
|
|
|
return self.__str__()
|
|
|
|
|
|
|
|
def __str__(self):
|
2024-02-04 23:38:00 +00:00
|
|
|
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})'
|
2023-07-09 15:52:20 +00:00
|
|
|
|
|
|
|
|
2022-02-20 18:19:56 +00:00
|
|
|
class Region:
|
|
|
|
name: str
|
2023-02-14 00:06:43 +00:00
|
|
|
_hint_text: str
|
2022-02-20 18:19:56 +00:00
|
|
|
player: int
|
2022-11-01 02:41:21 +00:00
|
|
|
multiworld: Optional[MultiWorld]
|
2022-02-20 18:19:56 +00:00
|
|
|
entrances: List[Entrance]
|
|
|
|
exits: List[Entrance]
|
|
|
|
locations: List[Location]
|
2023-07-09 15:52:20 +00:00
|
|
|
entrance_type: ClassVar[Type[Entrance]] = Entrance
|
2022-02-20 18:19:56 +00:00
|
|
|
|
2023-10-29 18:47:37 +00:00
|
|
|
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:
|
2024-02-25 20:56:27 +00:00
|
|
|
assert value.name not in self.region_manager.location_cache[value.player], \
|
|
|
|
f"{value.name} already exists in the location cache."
|
2023-10-29 18:47:37 +00:00
|
|
|
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:
|
2024-02-25 20:56:27 +00:00
|
|
|
assert value.name not in self.region_manager.entrance_cache[value.player], \
|
|
|
|
f"{value.name} already exists in the entrance cache."
|
2023-10-29 18:47:37 +00:00
|
|
|
self._list.insert(index, value)
|
|
|
|
self.region_manager.entrance_cache[value.player][value.name] = value
|
|
|
|
|
|
|
|
_locations: LocationRegister[Location]
|
|
|
|
_exits: EntranceRegister[Entrance]
|
|
|
|
|
2023-02-14 00:06:43 +00:00
|
|
|
def __init__(self, name: str, player: int, multiworld: MultiWorld, hint: Optional[str] = None):
|
2017-05-15 18:28:04 +00:00
|
|
|
self.name = name
|
|
|
|
self.entrances = []
|
2023-10-29 18:47:37 +00:00
|
|
|
self._exits = self.EntranceRegister(multiworld.regions)
|
|
|
|
self._locations = self.LocationRegister(multiworld.regions)
|
2023-02-14 00:06:43 +00:00
|
|
|
self.multiworld = multiworld
|
|
|
|
self._hint_text = hint
|
2019-04-18 09:23:24 +00:00
|
|
|
self.player = player
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2023-10-29 18:47:37 +00:00
|
|
|
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]
|
2019-07-09 02:48:16 +00:00
|
|
|
|
2023-02-14 00:06:43 +00:00
|
|
|
@property
|
|
|
|
def hint_text(self) -> str:
|
|
|
|
return self._hint_text if self._hint_text else self.name
|
|
|
|
|
2023-07-09 15:52:20 +00:00
|
|
|
def get_connecting_entrance(self, is_main_entrance: Callable[[Entrance], bool]) -> Entrance:
|
2022-09-18 12:30:43 +00:00
|
|
|
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)
|
|
|
|
|
2023-07-09 15:52:20 +00:00
|
|
|
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.
|
2023-10-10 20:30:20 +00:00
|
|
|
|
2023-07-09 15:52: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))
|
2023-10-10 20:30:20 +00:00
|
|
|
|
2023-07-09 15:52:20 +00:00
|
|
|
def connect(self, connecting_region: Region, name: Optional[str] = None,
|
2023-10-30 20:14:14 +00:00
|
|
|
rule: Optional[Callable[[CollectionState], bool]] = None) -> entrance_type:
|
2023-07-09 15:52:20 +00:00
|
|
|
"""
|
|
|
|
Connects this Region to another Region, placing the provided rule on the connection.
|
2023-10-10 20:30:20 +00:00
|
|
|
|
2023-07-09 15:52: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)
|
2023-10-30 20:14:14 +00:00
|
|
|
return exit_
|
2023-10-10 20:30:20 +00:00
|
|
|
|
2023-07-09 15:52:20 +00:00
|
|
|
def create_exit(self, name: str) -> Entrance:
|
|
|
|
"""
|
|
|
|
Creates and returns an Entrance object as an exit of this region.
|
2023-10-10 20:30:20 +00:00
|
|
|
|
2023-07-09 15:52: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
|
|
|
|
2023-07-01 01:37:44 +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.
|
|
|
|
|
2023-07-01 01:37:44 +00:00
|
|
|
: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}
|
|
|
|
"""
|
2023-07-01 01:37:44 +00:00
|
|
|
if not isinstance(exits, Dict):
|
|
|
|
exits = dict.fromkeys(exits)
|
|
|
|
for connecting_region, name in exits.items():
|
2023-07-09 15:52:20 +00:00
|
|
|
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
|
|
|
|
2020-04-10 19:31:15 +00:00
|
|
|
def __repr__(self):
|
|
|
|
return self.__str__()
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2020-04-10 19:31:15 +00:00
|
|
|
def __str__(self):
|
2022-11-01 02:41:21 +00:00
|
|
|
return self.multiworld.get_name_string_for_object(self) if self.multiworld else f'{self.name} (Player {self.player})'
|
2017-05-15 18:28:04 +00:00
|
|
|
|
|
|
|
|
2022-06-17 01:23:27 +00:00
|
|
|
class LocationProgressType(IntEnum):
|
2022-01-20 03:19:07 +00:00
|
|
|
DEFAULT = 1
|
|
|
|
PRIORITY = 2
|
|
|
|
EXCLUDED = 3
|
|
|
|
|
2022-02-01 15:36:14 +00:00
|
|
|
|
2022-02-20 18:19:56 +00:00
|
|
|
class Location:
|
2022-08-04 12:10:58 +00:00
|
|
|
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
|
2022-01-20 03:19:07 +00:00
|
|
|
progress_type: LocationProgressType = LocationProgressType.DEFAULT
|
2024-03-12 22:29:32 +00:00
|
|
|
always_allow = staticmethod(lambda state, item: False)
|
2022-09-28 21:54:10 +00:00
|
|
|
access_rule: Callable[[CollectionState], bool] = staticmethod(lambda state: True)
|
2021-07-15 06:50:08 +00:00
|
|
|
item_rule = staticmethod(lambda item: True)
|
2022-02-20 18:19:56 +00:00
|
|
|
item: Optional[Item] = None
|
2021-01-10 18:23:57 +00:00
|
|
|
|
2022-08-04 12:10:58 +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
|
2022-02-20 20:54:00 +00:00
|
|
|
self.parent_region = parent
|
2017-10-28 22:34:37 +00:00
|
|
|
|
2020-07-09 14:16:31 +00:00
|
|
|
def can_fill(self, state: CollectionState, item: Item, check_access=True) -> bool:
|
2024-05-12 16:51:20 +00:00
|
|
|
return ((self.always_allow(state, item) and item.name not in state.multiworld.worlds[item.player].options.non_local_items)
|
2022-10-30 23:47:23 +00:00
|
|
|
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))))
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2020-07-09 14:16:31 +00:00
|
|
|
def can_reach(self, state: CollectionState) -> bool:
|
2020-08-21 16:35:48 +00:00
|
|
|
# self.access_rule computes faster on average, so placing it first for faster abort
|
2022-10-30 23:47:23 +00:00
|
|
|
assert self.parent_region, "Can't reach location without region"
|
|
|
|
return self.access_rule(state) and self.parent_region.can_reach(state)
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2021-06-06 14:08:17 +00:00
|
|
|
def place_locked_item(self, item: Item):
|
|
|
|
if self.item:
|
|
|
|
raise Exception(f"Location {self} already filled.")
|
|
|
|
self.item = item
|
2022-03-25 21:57:00 +00:00
|
|
|
item.location = self
|
2021-06-06 14:08:17 +00:00
|
|
|
self.locked = True
|
|
|
|
|
2020-04-10 19:31:15 +00:00
|
|
|
def __repr__(self):
|
|
|
|
return self.__str__()
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2020-04-10 19:31:15 +00:00
|
|
|
def __str__(self):
|
2024-02-04 23:38:00 +00:00
|
|
|
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})'
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2020-08-22 17:19:29 +00:00
|
|
|
def __hash__(self):
|
|
|
|
return hash((self.name, self.player))
|
|
|
|
|
2022-03-23 23:46:26 +00:00
|
|
|
def __lt__(self, other: Location):
|
2021-01-17 21:58:52 +00:00
|
|
|
return (self.player, self.name) < (other.player, other.name)
|
|
|
|
|
2024-04-14 18:37:48 +00:00
|
|
|
@property
|
|
|
|
def advancement(self) -> bool:
|
|
|
|
return self.item is not None and self.item.advancement
|
|
|
|
|
2024-04-11 22:49:22 +00:00
|
|
|
@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
|
|
|
|
|
2021-07-12 12:10:49 +00:00
|
|
|
@property
|
|
|
|
def native_item(self) -> bool:
|
|
|
|
"""Returns True if the item in this location matches game."""
|
|
|
|
return self.item and self.item.game == self.game
|
|
|
|
|
2021-02-21 19:17:24 +00:00
|
|
|
@property
|
2022-03-23 23:46:26 +00:00
|
|
|
def hint_text(self) -> str:
|
2021-08-28 21:18:45 +00:00
|
|
|
return "at " + self.name.replace("_", " ").replace("-", " ")
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2021-07-20 16:23:06 +00:00
|
|
|
|
2022-06-17 01:23:27 +00:00
|
|
|
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 or entirely useless (nothing) 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."""
|
2022-06-17 04:10:30 +00:00
|
|
|
return int(self & 0b0111)
|
2022-06-17 01:23:27 +00:00
|
|
|
|
|
|
|
|
|
|
|
class Item:
|
2021-02-21 19:17:24 +00:00
|
|
|
game: str = "Generic"
|
2022-08-05 22:49:54 +00:00
|
|
|
__slots__ = ("name", "classification", "code", "player", "location")
|
|
|
|
name: str
|
2022-06-17 01:23:27 +00:00
|
|
|
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]
|
2021-02-21 19:17:24 +00:00
|
|
|
|
2022-06-17 01:23:27 +00:00
|
|
|
def __init__(self, name: str, classification: ItemClassification, code: Optional[int], player: int):
|
2017-05-15 18:28:04 +00:00
|
|
|
self.name = name
|
2022-06-17 01:23:27 +00:00
|
|
|
self.classification = classification
|
2019-04-18 09:23:24 +00:00
|
|
|
self.player = player
|
2021-02-21 19:17:24 +00:00
|
|
|
self.code = code
|
2022-08-05 22:49:54 +00:00
|
|
|
self.location = None
|
2021-02-21 19:17:24 +00:00
|
|
|
|
|
|
|
@property
|
2022-08-05 15:09:21 +00:00
|
|
|
def hint_text(self) -> str:
|
2021-06-14 00:20:13 +00:00
|
|
|
return getattr(self, "_hint_text", self.name.replace("_", " ").replace("-", " "))
|
2021-02-21 19:17:24 +00:00
|
|
|
|
|
|
|
@property
|
2022-08-05 15:09:21 +00:00
|
|
|
def pedestal_hint_text(self) -> str:
|
2021-06-14 00:23:41 +00:00
|
|
|
return getattr(self, "_pedestal_hint_text", self.name.replace("_", " ").replace("-", " "))
|
2017-10-28 22:34:37 +00:00
|
|
|
|
2022-06-17 01:23:27 +00:00
|
|
|
@property
|
|
|
|
def advancement(self) -> bool:
|
2022-07-02 11:27:50 +00:00
|
|
|
return ItemClassification.progression in self.classification
|
2022-06-17 01:23:27 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def skip_in_prog_balancing(self) -> bool:
|
2022-07-02 11:27:50 +00:00
|
|
|
return ItemClassification.progression_skip_balancing in self.classification
|
2022-06-17 01:23:27 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def useful(self) -> bool:
|
2022-07-02 11:27:50 +00:00
|
|
|
return ItemClassification.useful in self.classification
|
2022-06-17 01:23:27 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def trap(self) -> bool:
|
2022-07-02 11:27:50 +00:00
|
|
|
return ItemClassification.trap in self.classification
|
2022-06-17 01:23:27 +00:00
|
|
|
|
2022-01-18 05:16:16 +00:00
|
|
|
@property
|
|
|
|
def flags(self) -> int:
|
2022-06-17 04:10:30 +00:00
|
|
|
return self.classification.as_flag()
|
2022-01-18 05:16:16 +00:00
|
|
|
|
2023-05-24 23:24:12 +00:00
|
|
|
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
|
|
|
|
|
2023-05-24 23:24:12 +00:00
|
|
|
def __lt__(self, other: object) -> bool:
|
|
|
|
if not isinstance(other, Item):
|
|
|
|
return NotImplemented
|
2021-02-03 13:24:29 +00:00
|
|
|
if other.player != self.player:
|
|
|
|
return other.player < self.player
|
|
|
|
return self.name < other.name
|
|
|
|
|
2023-05-24 23:24:12 +00:00
|
|
|
def __hash__(self) -> int:
|
2021-01-02 11:59:19 +00:00
|
|
|
return hash((self.name, self.player))
|
|
|
|
|
2022-08-05 15:09:21 +00:00
|
|
|
def __repr__(self) -> str:
|
2020-04-10 19:31:15 +00:00
|
|
|
return self.__str__()
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2022-08-05 15:09:21 +00:00
|
|
|
def __str__(self) -> str:
|
2022-11-01 02:41:21 +00:00
|
|
|
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)
|
2022-08-05 15:09:21 +00:00
|
|
|
return f"{self.name} (Player {self.player})"
|
2017-05-20 12:03:15 +00:00
|
|
|
|
|
|
|
|
2023-05-24 23:24:12 +00:00
|
|
|
class EntranceInfo(TypedDict, total=False):
|
|
|
|
player: int
|
|
|
|
entrance: str
|
|
|
|
exit: str
|
|
|
|
direction: str
|
|
|
|
|
|
|
|
|
|
|
|
class Spoiler:
|
2022-11-01 02:41:21 +00:00
|
|
|
multiworld: MultiWorld
|
2023-05-24 23:24:12 +00:00
|
|
|
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]
|
2023-05-24 23:24:12 +00:00
|
|
|
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
|
|
|
|
2023-05-24 23:24:12 +00:00
|
|
|
def __init__(self, multiworld: MultiWorld) -> None:
|
|
|
|
self.multiworld = multiworld
|
2020-01-14 09:42:27 +00:00
|
|
|
self.hashes = {}
|
2023-05-24 23:24:12 +00:00
|
|
|
self.entrances = {}
|
2017-07-18 10:44:13 +00:00
|
|
|
self.playthrough = {}
|
2022-02-06 23:26:44 +00:00
|
|
|
self.unreachables = set()
|
2018-01-01 20:55:13 +00:00
|
|
|
self.paths = {}
|
2017-07-18 10:44:13 +00:00
|
|
|
|
2023-05-24 23:24:12 +00:00
|
|
|
def set_entrance(self, entrance: str, exit_: str, direction: str, player: int) -> None:
|
2022-11-01 02:41:21 +00:00
|
|
|
if self.multiworld.players == 1:
|
2023-05-24 23:24:12 +00:00
|
|
|
self.entrances[(entrance, direction, player)] = \
|
|
|
|
{"entrance": entrance, "exit": exit_, "direction": direction}
|
2019-07-13 22:11:43 +00:00
|
|
|
else:
|
2023-05-24 23:24:12 +00:00
|
|
|
self.entrances[(entrance, direction, player)] = \
|
|
|
|
{"player": player, "entrance": entrance, "exit": exit_, "direction": direction}
|
2017-07-18 10:44:13 +00:00
|
|
|
|
2023-05-24 23:24:12 +00:00
|
|
|
def create_playthrough(self, create_paths: bool = True) -> None:
|
2024-02-04 23:38:00 +00:00
|
|
|
"""Destructive to the multiworld while it is run, damage gets repaired afterwards."""
|
2022-12-11 19:48:26 +00:00
|
|
|
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}
|
2023-05-24 23:24:12 +00:00
|
|
|
state_cache: List[Optional[CollectionState]] = [None]
|
2022-12-11 19:48:26 +00:00
|
|
|
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])
|
2024-05-12 16:51:20 +00:00
|
|
|
if any([multiworld.worlds[location.item.player].options.accessibility != 'minimal' for location in sphere_candidates]):
|
2022-12-11 19:48:26 +00:00
|
|
|
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 = {}
|
|
|
|
for num, sphere in reversed(tuple(enumerate(collection_spheres))):
|
|
|
|
to_delete = 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 = []
|
|
|
|
for item in (i for i in chain.from_iterable(multiworld.precollected_items.values()) if i.advancement):
|
|
|
|
logging.debug('Checking if %s (Player %d) is required to beat the game.', item.name, item.player)
|
|
|
|
multiworld.precollected_items[item.player].remove(item)
|
|
|
|
multiworld.state.remove(item)
|
|
|
|
if not multiworld.can_beat_game():
|
|
|
|
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))
|
2024-02-22 08:44:03 +00:00
|
|
|
|
|
|
|
required_locations -= sphere
|
2022-12-11 19:48:26 +00:00
|
|
|
if not sphere:
|
|
|
|
raise RuntimeError(f'Not all required items reachable. Unreachable locations: {required_locations}')
|
|
|
|
|
|
|
|
# we can finally output our playthrough
|
2023-03-27 17:42:15 +00:00
|
|
|
self.playthrough = {"0": sorted([self.multiworld.get_name_string_for_object(item) for item in
|
2022-12-11 19:48:26 +00:00
|
|
|
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)
|
|
|
|
|
2023-05-24 23:24:12 +00:00
|
|
|
def create_paths(self, state: CollectionState, collection_spheres: List[Set[Location]]) -> None:
|
2022-12-11 19:48:26 +00:00
|
|
|
from itertools import zip_longest
|
|
|
|
multiworld = self.multiworld
|
|
|
|
|
2023-05-24 23:24:12 +00:00
|
|
|
def flist_to_iter(path_value: Optional[PathValue]) -> Iterator[str]:
|
|
|
|
while path_value:
|
|
|
|
region_or_entrance, path_value = path_value
|
|
|
|
yield region_or_entrance
|
2022-12-11 19:48:26 +00:00
|
|
|
|
2023-05-24 23:24:12 +00:00
|
|
|
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))
|
2022-12-11 19:48:26 +00:00
|
|
|
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):
|
|
|
|
if multiworld.mode[player] != '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))
|
|
|
|
|
2023-05-24 23:24:12 +00:00
|
|
|
def to_file(self, filename: str) -> None:
|
2024-04-14 18:05:16 +00:00
|
|
|
from itertools import chain
|
2024-02-14 21:56:21 +00:00
|
|
|
from worlds import AutoWorld
|
2024-04-14 18:49:43 +00:00
|
|
|
from Options import Visibility
|
2024-02-14 21:56:21 +00:00
|
|
|
|
2023-05-24 23:24:12 +00:00
|
|
|
def write_option(option_key: str, option_obj: Options.AssembleOptions) -> None:
|
2023-10-10 20:30:20 +00:00
|
|
|
res = getattr(self.multiworld.worlds[player].options, option_key)
|
2024-04-14 18:49:43 +00:00
|
|
|
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")
|
2021-09-16 22:17:54 +00:00
|
|
|
|
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' % (
|
2022-11-01 02:41:21 +00:00
|
|
|
Utils.__version__, self.multiworld.seed))
|
|
|
|
outfile.write('Filling Algorithm: %s\n' % self.multiworld.algorithm)
|
|
|
|
outfile.write('Players: %d\n' % self.multiworld.players)
|
2023-01-17 16:25:59 +00:00
|
|
|
outfile.write(f'Plando Options: {self.multiworld.plando_options}\n')
|
2022-11-01 02:41:21 +00:00
|
|
|
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])
|
2023-03-08 21:19:38 +00:00
|
|
|
|
2023-10-10 20:30:20 +00:00
|
|
|
for f_option, option in self.multiworld.worlds[player].options_dataclass.type_hints.items():
|
2021-09-16 22:17:54 +00:00
|
|
|
write_option(f_option, option)
|
2023-03-08 21:19:38 +00:00
|
|
|
|
2022-11-01 02:41:21 +00:00
|
|
|
AutoWorld.call_single(self.multiworld, "write_spoiler_header", player, outfile)
|
2021-06-03 22:29:59 +00:00
|
|
|
|
2017-07-18 10:44:13 +00:00
|
|
|
if self.entrances:
|
|
|
|
outfile.write('\n\nEntrances:\n\n')
|
2022-11-01 02:41:21 +00:00
|
|
|
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
|
|
|
|
2022-11-01 02:41:21 +00:00
|
|
|
AutoWorld.call_all(self.multiworld, "write_spoiler", outfile)
|
2021-07-07 08:14:58 +00:00
|
|
|
|
2024-04-14 18:05:16 +00:00
|
|
|
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]))
|
|
|
|
|
2023-02-25 03:02:51 +00:00
|
|
|
locations = [(str(location), str(location.item) if location.item is not None else "Nothing")
|
2023-05-24 23:24:12 +00:00
|
|
|
for location in self.multiworld.get_locations() if location.show_in_spoiler]
|
2017-07-18 10:44:13 +00:00
|
|
|
outfile.write('\n\nLocations:\n\n')
|
2021-10-06 09:32:49 +00:00
|
|
|
outfile.write('\n'.join(
|
2023-02-25 03:02:51 +00:00
|
|
|
['%s: %s' % (location, item) for location, item in locations]))
|
|
|
|
|
2017-07-18 10:44:13 +00:00
|
|
|
outfile.write('\n\nPlaythrough:\n\n')
|
2021-10-06 09:32:49 +00:00
|
|
|
outfile.write('\n'.join(['%s: {\n%s\n}' % (sphere_nr, '\n'.join(
|
2023-05-24 23:24:12 +00:00
|
|
|
[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()]))
|
2019-12-21 12:33:07 +00:00
|
|
|
if self.unreachables:
|
|
|
|
outfile.write('\n\nUnreachable Items:\n\n')
|
2021-10-06 09:32:49 +00:00
|
|
|
outfile.write(
|
|
|
|
'\n'.join(['%s: %s' % (unreachable.item, unreachable) for unreachable in self.unreachables]))
|
2018-01-01 20:55:13 +00:00
|
|
|
|
2021-06-06 15:13:34 +00:00
|
|
|
if self.paths:
|
|
|
|
outfile.write('\n\nPaths:\n\n')
|
|
|
|
path_listings = []
|
|
|
|
for location, path in sorted(self.paths.items()):
|
|
|
|
path_lines = []
|
|
|
|
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))
|
2022-11-01 02:41:21 +00:00
|
|
|
AutoWorld.call_all(self.multiworld, "write_spoiler_end", outfile)
|
2021-10-06 09:32:49 +00:00
|
|
|
|
|
|
|
|
2022-05-11 18:05:53 +00:00
|
|
|
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
|
2022-05-27 00:39:08 +00:00
|
|
|
authors: List[str]
|
2022-05-11 18:05:53 +00:00
|
|
|
|
|
|
|
|
2023-01-17 16:25:59 +00:00
|
|
|
class PlandoOptions(IntFlag):
|
2023-01-15 17:10:26 +00:00
|
|
|
none = 0b0000
|
|
|
|
items = 0b0001
|
|
|
|
connections = 0b0010
|
|
|
|
texts = 0b0100
|
|
|
|
bosses = 0b1000
|
|
|
|
|
|
|
|
@classmethod
|
2023-01-17 16:25:59 +00:00
|
|
|
def from_option_string(cls, option_string: str) -> PlandoOptions:
|
2023-01-15 17:10:26 +00:00
|
|
|
result = cls(0)
|
|
|
|
for part in option_string.split(","):
|
|
|
|
part = part.strip().lower()
|
|
|
|
if part:
|
|
|
|
result = cls._handle_part(part, result)
|
|
|
|
return result
|
|
|
|
|
|
|
|
@classmethod
|
2023-01-17 16:25:59 +00:00
|
|
|
def from_set(cls, option_set: Set[str]) -> PlandoOptions:
|
2023-01-15 17:10:26 +00:00
|
|
|
result = cls(0)
|
|
|
|
for part in option_set:
|
|
|
|
result = cls._handle_part(part, result)
|
|
|
|
return result
|
|
|
|
|
|
|
|
@classmethod
|
2023-01-17 16:25:59 +00:00
|
|
|
def _handle_part(cls, part: str, base: PlandoOptions) -> PlandoOptions:
|
2023-01-15 17:10:26 +00:00
|
|
|
try:
|
2023-05-24 23:24:12 +00:00
|
|
|
return base | cls[part]
|
2023-01-15 17:10:26 +00:00
|
|
|
except Exception as e:
|
|
|
|
raise KeyError(f"{part} is not a recognized name for a plando module. "
|
2023-05-24 23:24:12 +00:00
|
|
|
f"Known options: {', '.join(str(flag.name) for flag in cls)}") from e
|
2023-01-15 17:10:26 +00:00
|
|
|
|
|
|
|
def __str__(self) -> str:
|
|
|
|
if self.value:
|
2023-05-24 23:24:12 +00:00
|
|
|
return ", ".join(str(flag.name) for flag in PlandoOptions if self.value & flag.value)
|
2023-01-15 17:10:26 +00:00
|
|
|
return "None"
|
|
|
|
|
|
|
|
|
2021-10-06 09:32:49 +00:00
|
|
|
seeddigits = 20
|
|
|
|
|
|
|
|
|
2023-05-24 23:24:12 +00:00
|
|
|
def get_seed(seed: Optional[int] = None) -> int:
|
2021-10-06 09:32:49 +00:00
|
|
|
if seed is None:
|
|
|
|
random.seed(None)
|
|
|
|
return random.randint(0, pow(10, seeddigits) - 1)
|
|
|
|
return seed
|