from __future__ import annotations from collections.abc import Container from dataclasses import dataclass, field from enum import IntFlag connector_keyword = " to " def reverse_connection_name(name: str) -> str | None: try: origin, destination = name.split(connector_keyword) except ValueError: return None return f"{destination}{connector_keyword}{origin}" class MergeFlag(IntFlag): ADD_EXITS = 0 REMOVE_EXITS = 1 class RandomizationFlag(IntFlag): NOT_RANDOMIZED = 0 # Randomization options # The first 4 bits are used to mark if an entrance is eligible for randomization according to the entrance randomization options. BIT_PELICAN_TOWN = 1 # 0b0001 BIT_NON_PROGRESSION = 1 << 1 # 0b0010 BIT_BUILDINGS = 1 << 2 # 0b0100 BIT_EVERYTHING = 1 << 3 # 0b1000 # Content flag for entrances exclusions # The next 2 bits are used to mark if an entrance is to be excluded from randomization according to the content options. # Those bits must be removed from an entrance flags when then entrance must be excluded. __UNUSED = 1 << 4 # 0b010000 EXCLUDE_MASTERIES = 1 << 5 # 0b100000 # Entrance groups # The last bit is used to add additional qualifiers on entrances to group them # Those bits should be added when an entrance need additional qualifiers. LEAD_TO_OPEN_AREA = 1 << 6 # Tags to apply on connections EVERYTHING = EXCLUDE_MASTERIES | BIT_EVERYTHING BUILDINGS = EVERYTHING | BIT_BUILDINGS NON_PROGRESSION = BUILDINGS | BIT_NON_PROGRESSION PELICAN_TOWN = NON_PROGRESSION | BIT_PELICAN_TOWN @dataclass(frozen=True) class RegionData: name: str exits: tuple[str, ...] = field(default_factory=tuple) flag: MergeFlag = MergeFlag.ADD_EXITS def __post_init__(self): assert not isinstance(self.exits, str), "Exits must be a tuple of strings, you probably forgot a trailing comma." def merge_with(self, other: RegionData) -> RegionData: assert self.name == other.name, "Regions must have the same name to be merged" if other.flag == MergeFlag.REMOVE_EXITS: return self.get_without_exits(other.exits) merged_exits = self.exits + other.exits assert len(merged_exits) == len(set(merged_exits)), "Two regions getting merged have duplicated exists..." return RegionData(self.name, merged_exits) def get_without_exits(self, exits_to_remove: Container[str]) -> RegionData: exits = tuple(exit_ for exit_ in self.exits if exit_ not in exits_to_remove) return RegionData(self.name, exits) @dataclass(frozen=True) class ConnectionData: name: str destination: str flag: RandomizationFlag = RandomizationFlag.NOT_RANDOMIZED @property def reverse(self) -> str | None: return reverse_connection_name(self.name) def is_eligible_for_randomization(self, chosen_randomization_flag: RandomizationFlag) -> bool: return chosen_randomization_flag and chosen_randomization_flag in self.flag @dataclass(frozen=True) class ModRegionsData: mod_name: str regions: list[RegionData] connections: list[ConnectionData]