Skip to content

Commit

Permalink
Merge branch 'main' into zillion-dead-end-priority
Browse files Browse the repository at this point in the history
  • Loading branch information
beauxq authored Jan 8, 2025
2 parents f48ffaf + a29ba4a commit 9548e9a
Show file tree
Hide file tree
Showing 61 changed files with 2,780 additions and 700 deletions.
93 changes: 80 additions & 13 deletions BaseClasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import Utils

if TYPE_CHECKING:
from entrance_rando import ERPlacementState
from worlds import AutoWorld


Expand Down Expand Up @@ -426,12 +427,12 @@ def get_entrance(self, entrance_name: str, player: int) -> Entrance:
def get_location(self, location_name: str, player: int) -> Location:
return self.regions.location_cache[player][location_name]

def get_all_state(self, use_cache: bool) -> CollectionState:
def get_all_state(self, use_cache: bool, allow_partial_entrances: bool = False) -> CollectionState:
cached = getattr(self, "_all_state", None)
if use_cache and cached:
return cached.copy()

ret = CollectionState(self)
ret = CollectionState(self, allow_partial_entrances)

for item in self.itempool:
self.worlds[item.player].collect(ret, item)
Expand Down Expand Up @@ -717,10 +718,11 @@ class CollectionState():
path: Dict[Union[Region, Entrance], PathValue]
locations_checked: Set[Location]
stale: Dict[int, bool]
allow_partial_entrances: bool
additional_init_functions: List[Callable[[CollectionState, MultiWorld], None]] = []
additional_copy_functions: List[Callable[[CollectionState, CollectionState], CollectionState]] = []

def __init__(self, parent: MultiWorld):
def __init__(self, parent: MultiWorld, allow_partial_entrances: bool = False):
self.prog_items = {player: Counter() for player in parent.get_all_ids()}
self.multiworld = parent
self.reachable_regions = {player: set() for player in parent.get_all_ids()}
Expand All @@ -729,6 +731,7 @@ def __init__(self, parent: MultiWorld):
self.path = {}
self.locations_checked = set()
self.stale = {player: True for player in parent.get_all_ids()}
self.allow_partial_entrances = allow_partial_entrances
for function in self.additional_init_functions:
function(self, parent)
for items in parent.precollected_items.values():
Expand Down Expand Up @@ -763,6 +766,8 @@ def _update_reachable_regions_explicit_indirect_conditions(self, player: int, qu
if new_region in reachable_regions:
blocked_connections.remove(connection)
elif connection.can_reach(self):
if self.allow_partial_entrances and not new_region:
continue
assert new_region, f"tried to search through an Entrance \"{connection}\" with no connected Region"
reachable_regions.add(new_region)
blocked_connections.remove(connection)
Expand All @@ -788,7 +793,9 @@ def _update_reachable_regions_auto_indirect_conditions(self, player: int, queue:
if new_region in reachable_regions:
blocked_connections.remove(connection)
elif connection.can_reach(self):
assert new_region, f"tried to search through an Entrance \"{connection}\" with no Region"
if self.allow_partial_entrances and not new_region:
continue
assert new_region, f"tried to search through an Entrance \"{connection}\" with no connected Region"
reachable_regions.add(new_region)
blocked_connections.remove(connection)
blocked_connections.update(new_region.exits)
Expand All @@ -808,6 +815,7 @@ def copy(self) -> CollectionState:
ret.advancements = self.advancements.copy()
ret.path = self.path.copy()
ret.locations_checked = self.locations_checked.copy()
ret.allow_partial_entrances = self.allow_partial_entrances
for function in self.additional_copy_functions:
ret = function(self, ret)
return ret
Expand Down Expand Up @@ -972,26 +980,36 @@ def remove(self, item: Item):
self.stale[item.player] = True


class EntranceType(IntEnum):
ONE_WAY = 1
TWO_WAY = 2


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
randomization_group: int
randomization_type: EntranceType
# LttP specific, TODO: should make a LttPEntrance
addresses = None
target = None

def __init__(self, player: int, name: str = "", parent: Optional[Region] = None) -> None:
def __init__(self, player: int, name: str = "", parent: Optional[Region] = None,
randomization_group: int = 0, randomization_type: EntranceType = EntranceType.ONE_WAY) -> None:
self.name = name
self.parent_region = parent
self.player = player
self.randomization_group = randomization_group
self.randomization_type = randomization_type

def can_reach(self, state: CollectionState) -> bool:
assert self.parent_region, f"called can_reach on an Entrance \"{self}\" with no parent_region"
if self.parent_region.can_reach(state) and self.access_rule(state):
if not self.hide_path and not self in state.path:
if not self.hide_path and self not in state.path:
state.path[self] = (self.name, state.path.get(self.parent_region, (self.parent_region.name, None)))
return True

Expand All @@ -1003,6 +1021,32 @@ def connect(self, region: Region, addresses: Any = None, target: Any = None) ->
self.addresses = addresses
region.entrances.append(self)

def is_valid_source_transition(self, er_state: "ERPlacementState") -> bool:
"""
Determines whether this is a valid source transition, that is, whether the entrance
randomizer is allowed to pair it to place any other regions. By default, this is the
same as a reachability check, but can be modified by Entrance implementations to add
other restrictions based on the placement state.
:param er_state: The current (partial) state of the ongoing entrance randomization
"""
return self.can_reach(er_state.collection_state)

def can_connect_to(self, other: Entrance, dead_end: bool, er_state: "ERPlacementState") -> bool:
"""
Determines whether a given Entrance is a valid target transition, that is, whether
the entrance randomizer is allowed to pair this Entrance to that Entrance. By default,
only allows connection between entrances of the same type (one ways only go to one ways,
two ways always go to two ways) and prevents connecting an exit to itself in coupled mode.
:param other: The proposed Entrance to connect to
:param dead_end: Whether the other entrance considered a dead end by Entrance randomization
:param er_state: The current (partial) state of the ongoing entrance randomization
"""
# the implementation of coupled causes issues for self-loops since the reverse entrance will be the
# same as the forward entrance. In uncoupled they are ok.
return self.randomization_type == other.randomization_type and (not er_state.coupled or self.name != other.name)

def __repr__(self):
multiworld = self.parent_region.multiworld if self.parent_region else None
return multiworld.get_name_string_for_object(self) if multiworld else f'{self.name} (Player {self.player})'
Expand Down Expand Up @@ -1152,6 +1196,16 @@ def create_exit(self, name: str) -> Entrance:
self.exits.append(exit_)
return exit_

def create_er_target(self, name: str) -> Entrance:
"""
Creates and returns an Entrance object as an entrance to this region
:param name: name of the Entrance being created
"""
entrance = self.entrance_type(self.player, name)
entrance.connect(self)
return entrance

def add_exits(self, exits: Union[Iterable[str], Dict[str, Optional[str]]],
rules: Dict[str, Callable[[CollectionState], bool]] = None) -> List[Entrance]:
"""
Expand Down Expand Up @@ -1254,13 +1308,26 @@ def hint_text(self) -> str:


class ItemClassification(IntFlag):
filler = 0b0000 # aka trash, as in filler items like ammo, currency etc,
progression = 0b0001 # Item that is logically relevant
useful = 0b0010 # Item that is generally quite useful, but not required for anything logical
trap = 0b0100 # detrimental item
skip_balancing = 0b1000 # should technically never occur on its own
# Item that is logically relevant, but progression balancing should not touch.
# Typically currency or other counted items.
filler = 0b0000
""" aka trash, as in filler items like ammo, currency etc """

progression = 0b0001
""" Item that is logically relevant.
Protects this item from being placed on excluded or unreachable locations. """

useful = 0b0010
""" Item that is especially useful.
Protects this item from being placed on excluded or unreachable locations.
When combined with another flag like "progression", it means "an especially useful progression item". """

trap = 0b0100
""" Item that is detrimental in some way. """

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:
Expand Down
23 changes: 18 additions & 5 deletions Fill.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,18 +235,30 @@ def remaining_fill(multiworld: MultiWorld,
locations: typing.List[Location],
itempool: typing.List[Item],
name: str = "Remaining",
move_unplaceable_to_start_inventory: bool = False) -> None:
move_unplaceable_to_start_inventory: bool = False,
check_location_can_fill: bool = False) -> None:
unplaced_items: typing.List[Item] = []
placements: typing.List[Location] = []
swapped_items: typing.Counter[typing.Tuple[int, str]] = Counter()
total = min(len(itempool), len(locations))
placed = 0

# Optimisation: Decide whether to do full location.can_fill check (respect excluded), or only check the item rule
if check_location_can_fill:
state = CollectionState(multiworld)

def location_can_fill_item(location_to_fill: Location, item_to_fill: Item):
return location_to_fill.can_fill(state, item_to_fill, check_access=False)
else:
def location_can_fill_item(location_to_fill: Location, item_to_fill: Item):
return location_to_fill.item_rule(item_to_fill)

while locations and itempool:
item_to_place = itempool.pop()
spot_to_fill: typing.Optional[Location] = None

for i, location in enumerate(locations):
if location.item_rule(item_to_place):
if location_can_fill_item(location, item_to_place):
# popping by index is faster than removing by content,
spot_to_fill = locations.pop(i)
# skipping a scan for the element
Expand All @@ -267,7 +279,7 @@ def remaining_fill(multiworld: MultiWorld,

location.item = None
placed_item.location = None
if location.item_rule(item_to_place):
if location_can_fill_item(location, item_to_place):
# Add this item to the existing placement, and
# add the old item to the back of the queue
spot_to_fill = placements.pop(i)
Expand Down Expand Up @@ -519,7 +531,8 @@ def mark_for_locking(location: Location):
if progitempool:
raise FillError(
f"Not enough locations for progression items. "
f"There are {len(progitempool)} more progression items than there are available locations.",
f"There are {len(progitempool)} more progression items than there are available locations.\n"
f"Unfilled locations:\n{multiworld.get_unfilled_locations()}.",
multiworld=multiworld,
)
accessibility_corrections(multiworld, multiworld.state, defaultlocations)
Expand All @@ -537,7 +550,7 @@ def mark_for_locking(location: Location):
if excludedlocations:
raise FillError(
f"Not enough filler items for excluded locations. "
f"There are {len(excludedlocations)} more excluded locations than filler or trap items.",
f"There are {len(excludedlocations)} more excluded locations than excludable items.",
multiworld=multiworld,
)

Expand Down
3 changes: 2 additions & 1 deletion Generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -500,7 +500,8 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b
for option_key in game_weights:
if option_key in {"triggers", *valid_keys}:
continue
logging.warning(f"{option_key} is not a valid option name for {ret.game} and is not present in triggers.")
logging.warning(f"{option_key} is not a valid option name for {ret.game} and is not present in triggers "
f"for player {ret.name}.")
if PlandoOptions.items in plando_options:
ret.plando_items = copy.deepcopy(game_weights.get("plando_items", []))
if ret.game == "A Link to the Past":
Expand Down
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
MIT License

Copyright (c) 2017 LLCoolDave
Copyright (c) 2022 Berserker66
Copyright (c) 2025 Berserker66
Copyright (c) 2022 CaitSith2
Copyright (c) 2021 LegendaryLinux

Expand Down
2 changes: 1 addition & 1 deletion NetUtils.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from Utils import ByValue, Version


class HintStatus(enum.IntEnum):
class HintStatus(ByValue, enum.IntEnum):
HINT_FOUND = 0
HINT_UNSPECIFIED = 1
HINT_NO_PRIORITY = 10
Expand Down
3 changes: 3 additions & 0 deletions SNIClient.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,9 @@ def on_package(self, cmd: str, args: typing.Dict[str, typing.Any]) -> None:
# Once the games handled by SNIClient gets made to be remote items,
# this will no longer be needed.
async_start(self.send_msgs([{"cmd": "LocationScouts", "locations": list(new_locations)}]))

if self.client_handler is not None:
self.client_handler.on_package(self, cmd, args)

def run_gui(self) -> None:
from kvui import GameManager
Expand Down
2 changes: 1 addition & 1 deletion WebHostLib/templates/islandFooter.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{% block footer %}
<footer id="island-footer">
<div id="copyright-notice">Copyright 2024 Archipelago</div>
<div id="copyright-notice">Copyright 2025 Archipelago</div>
<div id="links">
<a href="/sitemap">Site Map</a>
-
Expand Down
2 changes: 1 addition & 1 deletion docs/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@
/worlds/saving_princess/ @LeonarthCG

# Shivers
/worlds/shivers/ @GodlFire
/worlds/shivers/ @GodlFire @korydondzila

# A Short Hike
/worlds/shorthike/ @chandler05 @BrandenEK
Expand Down
Loading

0 comments on commit 9548e9a

Please sign in to comment.