Skip to content

Commit 35aa28d

Browse files
alwaysintrebleFlySniper
authored andcommitted
Core: Region connection helpers (ArchipelagoMW#1923)
* Region.create_exit and Region.connect helpers * reduce code duplication and better naming in Region.connect * thank you tests * reorder class definition * define entrance_type on Region * document helpers * drop __class_getitem__ for now * review changes
1 parent ae2517b commit 35aa28d

File tree

2 files changed

+95
-69
lines changed

2 files changed

+95
-69
lines changed

BaseClasses.py

+76-48
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
from argparse import Namespace
1010
from collections import ChainMap, Counter, deque
1111
from enum import IntEnum, IntFlag
12-
from typing import Any, Callable, Dict, Iterable, Iterator, List, NamedTuple, Optional, Set, Tuple, TypedDict, Union
12+
from typing import Any, Callable, Dict, Iterable, Iterator, List, NamedTuple, Optional, Set, Tuple, TypedDict, Union, \
13+
Type, ClassVar
1314

1415
import NetUtils
1516
import Options
@@ -788,6 +789,44 @@ def remove(self, item: Item):
788789
self.stale[item.player] = True
789790

790791

792+
class Entrance:
793+
access_rule: Callable[[CollectionState], bool] = staticmethod(lambda state: True)
794+
hide_path: bool = False
795+
player: int
796+
name: str
797+
parent_region: Optional[Region]
798+
connected_region: Optional[Region] = None
799+
# LttP specific, TODO: should make a LttPEntrance
800+
addresses = None
801+
target = None
802+
803+
def __init__(self, player: int, name: str = '', parent: Region = None):
804+
self.name = name
805+
self.parent_region = parent
806+
self.player = player
807+
808+
def can_reach(self, state: CollectionState) -> bool:
809+
if self.parent_region.can_reach(state) and self.access_rule(state):
810+
if not self.hide_path and not self in state.path:
811+
state.path[self] = (self.name, state.path.get(self.parent_region, (self.parent_region.name, None)))
812+
return True
813+
814+
return False
815+
816+
def connect(self, region: Region, addresses: Any = None, target: Any = None) -> None:
817+
self.connected_region = region
818+
self.target = target
819+
self.addresses = addresses
820+
region.entrances.append(self)
821+
822+
def __repr__(self):
823+
return self.__str__()
824+
825+
def __str__(self):
826+
world = self.parent_region.multiworld if self.parent_region else None
827+
return world.get_name_string_for_object(self) if world else f'{self.name} (Player {self.player})'
828+
829+
791830
class Region:
792831
name: str
793832
_hint_text: str
@@ -796,6 +835,7 @@ class Region:
796835
entrances: List[Entrance]
797836
exits: List[Entrance]
798837
locations: List[Location]
838+
entrance_type: ClassVar[Type[Entrance]] = Entrance
799839

800840
def __init__(self, name: str, player: int, multiworld: MultiWorld, hint: Optional[str] = None):
801841
self.name = name
@@ -823,20 +863,48 @@ def can_reach_private(self, state: CollectionState) -> bool:
823863
def hint_text(self) -> str:
824864
return self._hint_text if self._hint_text else self.name
825865

826-
def get_connecting_entrance(self, is_main_entrance: typing.Callable[[Entrance], bool]) -> Entrance:
866+
def get_connecting_entrance(self, is_main_entrance: Callable[[Entrance], bool]) -> Entrance:
827867
for entrance in self.entrances:
828868
if is_main_entrance(entrance):
829869
return entrance
830870
for entrance in self.entrances: # BFS might be better here, trying DFS for now.
831871
return entrance.parent_region.get_connecting_entrance(is_main_entrance)
832872

833-
def add_locations(self, locations: Dict[str, Optional[int]], location_type: Optional[typing.Type[Location]] = None) -> None:
834-
"""Adds locations to the Region object, where location_type is your Location class and locations is a dict of
835-
location names to address."""
873+
def add_locations(self, locations: Dict[str, Optional[int]],
874+
location_type: Optional[Type[Location]] = None) -> None:
875+
"""
876+
Adds locations to the Region object, where location_type is your Location class and locations is a dict of
877+
location names to address.
878+
879+
:param locations: dictionary of locations to be created and added to this Region `{name: ID}`
880+
:param location_type: Location class to be used to create the locations with"""
836881
if location_type is None:
837882
location_type = Location
838883
for location, address in locations.items():
839884
self.locations.append(location_type(self.player, location, address, self))
885+
886+
def connect(self, connecting_region: Region, name: Optional[str] = None,
887+
rule: Optional[Callable[[CollectionState], bool]] = None) -> None:
888+
"""
889+
Connects this Region to another Region, placing the provided rule on the connection.
890+
891+
:param connecting_region: Region object to connect to path is `self -> exiting_region`
892+
:param name: name of the connection being created
893+
:param rule: callable to determine access of this connection to go from self to the exiting_region"""
894+
exit_ = self.create_exit(name if name else f"{self.name} -> {connecting_region.name}")
895+
if rule:
896+
exit_.access_rule = rule
897+
exit_.connect(connecting_region)
898+
899+
def create_exit(self, name: str) -> Entrance:
900+
"""
901+
Creates and returns an Entrance object as an exit of this region.
902+
903+
:param name: name of the Entrance being created
904+
"""
905+
exit_ = self.entrance_type(self.player, name, self)
906+
self.exits.append(exit_)
907+
return exit_
840908

841909
def add_exits(self, exits: Union[Iterable[str], Dict[str, Optional[str]]],
842910
rules: Dict[str, Callable[[CollectionState], bool]] = None) -> None:
@@ -850,11 +918,9 @@ def add_exits(self, exits: Union[Iterable[str], Dict[str, Optional[str]]],
850918
if not isinstance(exits, Dict):
851919
exits = dict.fromkeys(exits)
852920
for connecting_region, name in exits.items():
853-
entrance = Entrance(self.player, name if name else f"{self.name} -> {connecting_region}", self)
854-
if rules and connecting_region in rules:
855-
entrance.access_rule = rules[connecting_region]
856-
self.exits.append(entrance)
857-
entrance.connect(self.multiworld.get_region(connecting_region, self.player))
921+
self.connect(self.multiworld.get_region(connecting_region, self.player),
922+
name,
923+
rules[connecting_region] if rules and connecting_region in rules else None)
858924

859925
def __repr__(self):
860926
return self.__str__()
@@ -863,44 +929,6 @@ def __str__(self):
863929
return self.multiworld.get_name_string_for_object(self) if self.multiworld else f'{self.name} (Player {self.player})'
864930

865931

866-
class Entrance:
867-
access_rule: Callable[[CollectionState], bool] = staticmethod(lambda state: True)
868-
hide_path: bool = False
869-
player: int
870-
name: str
871-
parent_region: Optional[Region]
872-
connected_region: Optional[Region] = None
873-
# LttP specific, TODO: should make a LttPEntrance
874-
addresses = None
875-
target = None
876-
877-
def __init__(self, player: int, name: str = '', parent: Region = None):
878-
self.name = name
879-
self.parent_region = parent
880-
self.player = player
881-
882-
def can_reach(self, state: CollectionState) -> bool:
883-
if self.parent_region.can_reach(state) and self.access_rule(state):
884-
if not self.hide_path and not self in state.path:
885-
state.path[self] = (self.name, state.path.get(self.parent_region, (self.parent_region.name, None)))
886-
return True
887-
888-
return False
889-
890-
def connect(self, region: Region, addresses: Any = None, target: Any = None) -> None:
891-
self.connected_region = region
892-
self.target = target
893-
self.addresses = addresses
894-
region.entrances.append(self)
895-
896-
def __repr__(self):
897-
return self.__str__()
898-
899-
def __str__(self):
900-
world = self.parent_region.multiworld if self.parent_region else None
901-
return world.get_name_string_for_object(self) if world else f'{self.name} (Player {self.player})'
902-
903-
904932
class LocationProgressType(IntEnum):
905933
DEFAULT = 1
906934
PRIORITY = 2

docs/world api.md

+19-21
Original file line numberDiff line numberDiff line change
@@ -512,30 +512,28 @@ def create_items(self) -> None:
512512
def create_regions(self) -> None:
513513
# Add regions to the multiworld. "Menu" is the required starting point.
514514
# Arguments to Region() are name, player, world, and optionally hint_text
515-
r = Region("Menu", self.player, self.multiworld)
516-
# Set Region.exits to a list of entrances that are reachable from region
517-
r.exits = [Entrance(self.player, "New game", r)] # or use r.exits.append
518-
# Append region to MultiWorld's regions
519-
self.multiworld.regions.append(r) # or use += [r...]
515+
menu_region = Region("Menu", self.player, self.multiworld)
516+
self.multiworld.regions.append(menu_region) # or use += [menu_region...]
520517

521-
r = Region("Main Area", self.player, self.multiworld)
518+
main_region = Region("Main Area", self.player, self.multiworld)
522519
# Add main area's locations to main area (all but final boss)
523-
r.locations = [MyGameLocation(self.player, location.name,
524-
self.location_name_to_id[location.name], r)]
525-
r.exits = [Entrance(self.player, "Boss Door", r)]
526-
self.multiworld.regions.append(r)
520+
main_region.add_locations(main_region_locations, MyGameLocation)
521+
# or
522+
# main_region.locations = \
523+
# [MyGameLocation(self.player, location_name, self.location_name_to_id[location_name], main_region]
524+
self.multiworld.regions.append(main_region)
527525

528-
r = Region("Boss Room", self.player, self.multiworld)
529-
# add event to Boss Room
530-
r.locations = [MyGameLocation(self.player, "Final Boss", None, r)]
531-
self.multiworld.regions.append(r)
532-
533-
# If entrances are not randomized, they should be connected here, otherwise
534-
# they can also be connected at a later stage.
535-
self.multiworld.get_entrance("New Game", self.player)
536-
.connect(self.multiworld.get_region("Main Area", self.player))
537-
self.multiworld.get_entrance("Boss Door", self.player)
538-
.connect(self.multiworld.get_region("Boss Room", self.player))
526+
boss_region = Region("Boss Room", self.player, self.multiworld)
527+
# Add event to Boss Room
528+
boss_region.locations.append(MyGameLocation(self.player, "Final Boss", None, boss_region))
529+
530+
# If entrances are not randomized, they should be connected here,
531+
# otherwise they can also be connected at a later stage.
532+
# Create Entrances and connect the Regions
533+
menu_region.connect(main_region) # connects the "Menu" and "Main Area", can also pass a rule
534+
# or
535+
main_region.add_exits({"Boss Room": "Boss Door"}, {"Boss Room": lambda state: state.has("Sword", self.player)})
536+
# Connects the "Main Area" and "Boss Room" regions, and places a rule requiring the "Sword" item to traverse
539537

540538
# If setting location access rules from data is easier here, set_rules can
541539
# possibly omitted.

0 commit comments

Comments
 (0)