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