Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Core: move region and location management to worlds #4028

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
162 changes: 101 additions & 61 deletions BaseClasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,81 @@ class HasNameAndPlayer(Protocol):
player: int


_T_Reg = typing.TypeVar("_T_Reg", bound="Region")
_T_Ent = typing.TypeVar("_T_Ent", bound="Entrance")
_T_Loc = typing.TypeVar("_T_Loc", bound="Location")


class RegionManager(typing.Generic[_T_Reg, _T_Ent, _T_Loc]):
region_cache: Dict[str, _T_Reg]
entrance_cache: Dict[str, _T_Ent]
location_cache: Dict[str, _T_Loc]
multiworld: "MultiWorld"

def __init__(self, multiworld: "MultiWorld" = None):
# players is no longer needed. The multiworld is passed in here so we can reference the worlds' caches
# while they continue to use multiworld.regions
# TODO remove later
self.multiworld = multiworld
self.region_cache = {}
self.entrance_cache = {}
self.location_cache = {}

def __iadd__(self, other: Iterable[Region]):
self.extend(other)
return self

def append(self, region: Region):
# TODO
if self.multiworld is not None:
region_cache = self.multiworld.worlds[region.player].regions.region_cache
else:
region_cache = self.region_cache
assert region.name not in region_cache, f"{region.name} already exists in region cache."
region_cache[region.name] = region

def extend(self, regions: Iterable[Region]):
for region in regions:
# TODO
if self.multiworld is not None:
region_cache = self.multiworld.worlds[region.player].regions.region_cache
else:
region_cache = self.region_cache
assert region.name not in region_cache, f"{region.name} already exists in region cache."
region_cache[region.name] = region

def __iter__(self) -> Iterator[Region]:
# TODO
if self.multiworld is not None:
yield from self.multiworld.get_regions()
else:
yield from self.region_cache.values()

def __len__(self):
# TODO
if self.multiworld is not None:
return len(self.multiworld.get_regions())
return len(self.region_cache.values())

def get_regions(self) -> typing.Iterable[_T_Reg]:
return self.region_cache.values()

def get_region(self, name: str) -> _T_Reg:
return self.region_cache[name]

def get_locations(self) -> typing.Iterable[_T_Loc]:
return self.location_cache.values()

def get_location(self, name: str) -> _T_Loc:
return self.location_cache[name]

def get_entrances(self) -> typing.Iterable[_T_Ent]:
return self.entrance_cache.values()

def get_entrance(self, name: str) -> _T_Ent:
return self.entrance_cache[name]


class MultiWorld():
debug_types = False
player_name: Dict[int, str]
Expand Down Expand Up @@ -97,51 +172,14 @@ def __init__(self, rule):
def __getitem__(self, player) -> bool:
return self.rule(player)

class RegionManager:
region_cache: Dict[int, Dict[str, Region]]
entrance_cache: Dict[int, Dict[str, Entrance]]
location_cache: Dict[int, Dict[str, Location]]

def __init__(self, players: int):
self.region_cache = {player: {} for player in range(1, players+1)}
self.entrance_cache = {player: {} for player in range(1, players+1)}
self.location_cache = {player: {} for player in range(1, players+1)}

def __iadd__(self, other: Iterable[Region]):
self.extend(other)
return self

def append(self, region: Region):
assert region.name not in self.region_cache[region.player], \
f"{region.name} already exists in region cache."
self.region_cache[region.player][region.name] = region

def extend(self, regions: Iterable[Region]):
for region in regions:
assert region.name not in self.region_cache[region.player], \
f"{region.name} already exists in region cache."
self.region_cache[region.player][region.name] = region

def add_group(self, new_id: int):
self.region_cache[new_id] = {}
self.entrance_cache[new_id] = {}
self.location_cache[new_id] = {}

def __iter__(self) -> Iterator[Region]:
for regions in self.region_cache.values():
yield from regions.values()

def __len__(self):
return sum(len(regions) for regions in self.region_cache.values())

def __init__(self, players: int):
# world-local random state is saved for multiple generations running concurrently
self.random = ThreadBarrierProxy(random.Random())
self.players = players
self.player_types = {player: NetUtils.SlotType.player for player in self.player_ids}
self.algorithm = 'balanced'
self.groups = {}
self.regions = self.RegionManager(players)
self.regions = RegionManager(self)
self.shops = []
self.itempool = []
self.seed = None
Expand Down Expand Up @@ -189,7 +227,6 @@ def add_group(self, name: str, game: str, players: AbstractSet[int] = frozenset(
return group_id, group
new_id: int = self.players + len(self.groups) + 1

self.regions.add_group(new_id)
self.game[new_id] = game
self.player_types[new_id] = NetUtils.SlotType.group
world_type = AutoWorld.AutoWorldRegister.world_types[game]
Expand Down Expand Up @@ -416,17 +453,20 @@ def get_out_file_name_base(self, player: int) -> str:
def world_name_lookup(self):
return {self.player_name[player_id]: player_id for player_id in self.player_ids}

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_regions(self, player: Optional[int] = None) -> Iterable[Region]:
if player is not None:
return self.worlds[player].regions.get_regions()
return Utils.RepeatableChain(tuple(self.worlds[player].regions.get_regions()
for player in self.get_all_ids()))

def get_region(self, region_name: str, player: int) -> Region:
return self.regions.region_cache[player][region_name]
return self.worlds[player].regions.get_region(region_name)

def get_entrance(self, entrance_name: str, player: int) -> Entrance:
return self.regions.entrance_cache[player][entrance_name]
return self.worlds[player].regions.get_entrance(entrance_name)

def get_location(self, location_name: str, player: int) -> Location:
return self.regions.location_cache[player][location_name]
return self.worlds[player].regions.get_location(location_name)

def get_all_state(self, use_cache: bool) -> CollectionState:
cached = getattr(self, "_all_state", None)
Expand Down Expand Up @@ -489,9 +529,9 @@ def push_item(self, location: Location, item: Item, collect: bool = True):

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))
return self.worlds[player].regions.get_entrances()
return Utils.RepeatableChain(tuple(self.worlds[player].regions.get_entrances()
for player in self.get_all_ids()))

def register_indirect_condition(self, region: Region, entrance: Entrance):
"""Report that access to this Region can result in unlocking this Entrance,
Expand All @@ -500,9 +540,9 @@ def register_indirect_condition(self, region: Region, entrance: Entrance):

def get_locations(self, player: Optional[int] = None) -> Iterable[Location]:
if player is not None:
return self.regions.location_cache[player].values()
return Utils.RepeatableChain(tuple(self.regions.location_cache[player].values()
for player in self.regions.location_cache))
return self.worlds[player].regions.get_locations()
return Utils.RepeatableChain(tuple(self.worlds[player].regions.get_locations()
for player in self.get_all_ids()))

def get_unfilled_locations(self, player: Optional[int] = None) -> List[Location]:
return [location for location in self.get_locations(player) if location.item is None]
Expand All @@ -524,7 +564,7 @@ def get_unfilled_locations_for_players(self, location_names: List[str], players:
valid_locations = [location.name for location in self.get_unfilled_locations(player)]
else:
valid_locations = location_names
relevant_cache = self.regions.location_cache[player]
relevant_cache = self.worlds[player].regions.location_cache
for location_name in valid_locations:
location = relevant_cache.get(location_name, None)
if location and location.item is None:
Expand Down Expand Up @@ -978,9 +1018,9 @@ class Region:
entrance_type: ClassVar[Type[Entrance]] = Entrance

class Register(MutableSequence):
region_manager: MultiWorld.RegionManager
region_manager: RegionManager

def __init__(self, region_manager: MultiWorld.RegionManager):
def __init__(self, region_manager: RegionManager):
self._list = []
self.region_manager = region_manager

Expand All @@ -1004,34 +1044,34 @@ 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])
del(self.region_manager.location_cache[location.name])

def insert(self, index: int, value: Location) -> None:
assert value.name not in self.region_manager.location_cache[value.player], \
assert value.name not in self.region_manager.location_cache, \
f"{value.name} already exists in the location cache."
self._list.insert(index, value)
self.region_manager.location_cache[value.player][value.name] = value
self.region_manager.location_cache[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])
del (self.region_manager.entrance_cache[entrance.name])

def insert(self, index: int, value: Entrance) -> None:
assert value.name not in self.region_manager.entrance_cache[value.player], \
assert value.name not in self.region_manager.entrance_cache, \
f"{value.name} already exists in the entrance cache."
self._list.insert(index, value)
self.region_manager.entrance_cache[value.player][value.name] = value
self.region_manager.entrance_cache[value.name] = value

_locations: LocationRegister[Location]
_exits: EntranceRegister[Entrance]

def __init__(self, name: str, player: int, multiworld: MultiWorld, hint: Optional[str] = None):
self.name = name
self.entrances = []
self._exits = self.EntranceRegister(multiworld.regions)
self._locations = self.LocationRegister(multiworld.regions)
self._exits = self.EntranceRegister(multiworld.worlds[player].regions)
self._locations = self.LocationRegister(multiworld.worlds[player].regions)
self.multiworld = multiworld
self._hint_text = hint
self.player = player
Expand Down
4 changes: 3 additions & 1 deletion test/general/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from argparse import Namespace
from typing import List, Optional, Tuple, Type, Union

from tornado.gen import multi
alwaysintreble marked this conversation as resolved.
Show resolved Hide resolved

from BaseClasses import CollectionState, Item, ItemClassification, Location, MultiWorld, Region
from worlds import network_data_package
from worlds.AutoWorld import World, call_all
Expand Down Expand Up @@ -73,7 +75,7 @@ def generate_test_multiworld(players: int = 1) -> MultiWorld:
:return: The generated test multiworld
"""
multiworld = setup_multiworld([TestWorld] * players, seed=0)
multiworld.regions += [Region("Menu", player_id + 1, multiworld) for player_id in range(players)]
multiworld.regions += [Region("Menu", player_id, multiworld) for player_id in multiworld.player_ids]

return multiworld

Expand Down
6 changes: 2 additions & 4 deletions test/general/test_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,15 @@
from typing import Callable, Dict, Optional

from BaseClasses import CollectionState, MultiWorld, Region
from test.general import TestWorld, setup_solo_multiworld


class TestHelpers(unittest.TestCase):
multiworld: MultiWorld
player: int = 1

def setUp(self) -> None:
self.multiworld = MultiWorld(self.player)
self.multiworld.game[self.player] = "helper_test_game"
self.multiworld.player_name = {1: "Tester"}
self.multiworld.set_seed()
self.multiworld = setup_solo_multiworld(TestWorld, ())

def test_region_helpers(self) -> None:
"""Tests `Region.add_locations()` and `Region.add_exits()` have correct behavior"""
Expand Down
12 changes: 8 additions & 4 deletions worlds/AutoWorld.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from BaseClasses import CollectionState

if TYPE_CHECKING:
from BaseClasses import MultiWorld, Item, Location, Tutorial, Region, Entrance
from BaseClasses import MultiWorld, Item, Location, Tutorial, Region, Entrance, RegionManager
from . import GamesPackage
from settings import Group

Expand Down Expand Up @@ -294,6 +294,8 @@ class World(metaclass=AutoWorldRegister):

origin_region_name: str = "Menu"
"""Name of the Region from which accessibility is tested."""
regions: "RegionManager"
"""Regions for this world instance. Regions should be added to this, and not override it."""

explicit_indirect_conditions: bool = True
"""If True, the world implementation is supposed to use MultiWorld.register_indirect_condition() correctly.
Expand Down Expand Up @@ -334,6 +336,8 @@ def __init__(self, multiworld: "MultiWorld", player: int):
self.player = player
self.random = Random(multiworld.random.getrandbits(64))
multiworld.per_slot_randoms[player] = self.random
from BaseClasses import RegionManager
self.regions = RegionManager()

def __getattr__(self, item: str) -> Any:
if item == "settings":
Expand Down Expand Up @@ -529,13 +533,13 @@ def create_filler(self) -> "Item":

# convenience methods
def get_location(self, location_name: str) -> "Location":
return self.multiworld.get_location(location_name, self.player)
return self.regions.get_location(location_name)

def get_entrance(self, entrance_name: str) -> "Entrance":
return self.multiworld.get_entrance(entrance_name, self.player)
return self.regions.get_entrance(entrance_name)

def get_region(self, region_name: str) -> "Region":
return self.multiworld.get_region(region_name, self.player)
return self.regions.get_region(region_name)

@property
def player_name(self) -> str:
Expand Down
14 changes: 8 additions & 6 deletions worlds/aquaria/Regions.py
Original file line number Diff line number Diff line change
Expand Up @@ -1396,12 +1396,7 @@ def add_regions_to_world(self) -> None:
self.__add_abyss_regions_to_world()
self.__add_body_regions_to_world()

def __init__(self, multiworld: MultiWorld, player: int):
"""
Initialisation of the regions
"""
self.multiworld = multiworld
self.player = player
def create_regions(self) -> None:
self.__create_home_water_area()
self.__create_energy_temple()
self.__create_openwater()
Expand All @@ -1412,3 +1407,10 @@ def __init__(self, multiworld: MultiWorld, player: int):
self.__create_abyss()
self.__create_sunken_city()
self.__create_body()

def __init__(self, multiworld: MultiWorld, player: int):
"""
Initialisation of the regions
"""
self.multiworld = multiworld
self.player = player
Loading
Loading