from typing import TYPE_CHECKING from BaseClasses import CollectionState if TYPE_CHECKING: from . import Lingo2World class AccessRequirements: items: set[str] progressives: dict[str, int] rooms: set[str] letters: dict[str, int] cyans: bool # This is an AND of ORs. or_logic: list[list["AccessRequirements"]] # When complete_at is set, at least that many of the requirements in possibilities must be accessible. This should # only be used for doors with complete_at > 1, as or_logic is more efficient for complete_at == 1. complete_at: int | None possibilities: list["AccessRequirements"] def __init__(self): self.items = set() self.progressives = dict() self.rooms = set() self.letters = dict() self.cyans = False self.or_logic = list() self.complete_at = None self.possibilities = list() def copy(self) -> "AccessRequirements": reqs = AccessRequirements() reqs.items = self.items.copy() reqs.progressives = self.progressives.copy() reqs.rooms = self.rooms.copy() reqs.letters = self.letters.copy() reqs.cyans = self.cyans reqs.or_logic = [[other_req.copy() for other_req in disjunction] for disjunction in self.or_logic] reqs.complete_at = self.complete_at reqs.possibilities = self.possibilities.copy() return reqs def merge(self, other: "AccessRequirements"): for item in other.items: self.items.add(item) for item, amount in other.progressives.items(): self.progressives[item] = max(amount, self.progressives.get(item, 0)) for room in other.rooms: self.rooms.add(room) for letter, level in other.letters.items(): self.letters[letter] = max(self.letters.get(letter, 0), level) self.cyans = self.cyans or other.cyans for disjunction in other.or_logic: self.or_logic.append([sub_req.copy() for sub_req in disjunction]) if other.complete_at is not None: # Merging multiple requirements that use complete_at sucks, and is part of why we want to minimize use of # it. If both requirements use complete_at, we will cheat by using the or_logic field, which supports # conjunctions of requirements. if self.complete_at is not None: print("Merging requirements with complete_at > 1. This is messy and should be avoided!") left_req = AccessRequirements() left_req.complete_at = self.complete_at left_req.possibilities = [sub_req.copy() for sub_req in self.possibilities] self.or_logic.append([left_req]) self.complete_at = None self.possibilities = list() right_req = AccessRequirements() right_req.complete_at = other.complete_at right_req.possibilities = [sub_req.copy() for sub_req in other.possibilities] self.or_logic.append([right_req]) else: self.complete_at = other.complete_at self.possibilities = [sub_req.copy() for sub_req in other.possibilities] def is_empty(self) -> bool: return (len(self.items) == 0 and len(self.progressives) == 0 and len(self.rooms) == 0 and len(self.letters) == 0 and not self.cyans and len(self.or_logic) == 0 and self.complete_at is None) def __eq__(self, other: "AccessRequirements"): return (self.items == other.items and self.progressives == other.progressives and self.rooms == other.rooms and self.letters == other.letters and self.cyans == other.cyans and self.or_logic == other.or_logic and self.complete_at == other.complete_at and self.possibilities == other.possibilities) def simplify(self): resimplify = False if len(self.or_logic) > 0: old_or_logic = self.or_logic def remove_redundant(sub_reqs: "AccessRequirements"): new_reqs = sub_reqs.copy() new_reqs.letters = {l: v for l, v in new_reqs.letters.items() if self.letters.get(l, 0) < v} if new_reqs != sub_reqs: return new_reqs else: return sub_reqs self.or_logic = [] for disjunction in old_or_logic: new_disjunction = [] for ssr in disjunction: new_ssr = remove_redundant(ssr) if not new_ssr.is_empty(): new_disjunction.append(new_ssr) else: new_disjunction.clear() break if len(new_disjunction) == 1: self.merge(new_disjunction[0]) resimplify = True elif len(new_disjunction) > 1: if all(cjr == new_disjunction[0] for cjr in new_disjunction): self.merge(new_disjunction[0]) resimplify = True else: self.or_logic.append(new_disjunction) if resimplify: self.simplify() def get_referenced_rooms(self): result = set(self.rooms) for disjunction in self.or_logic: for sub_req in disjunction: result = result.union(sub_req.get_referenced_rooms()) for sub_req in self.possibilities: result = result.union(sub_req.get_referenced_rooms()) return result def remove_room(self, room: str): if room in self.rooms: self.rooms.remove(room) for disjunction in self.or_logic: for sub_req in disjunction: sub_req.remove_room(room) for sub_req in self.possibilities: sub_req.remove_room(room) def __repr__(self): parts = [] if len(self.items) > 0: parts.append(f"items={self.items}") if len(self.progressives) > 0: parts.append(f"progressives={self.progressives}") if len(self.rooms) > 0: parts.append(f"rooms={self.rooms}") if len(self.letters) > 0: parts.append(f"letters={self.letters}") if self.cyans: parts.append(f"cyans=True") if len(self.or_logic) > 0: parts.append(f"or_logic={self.or_logic}") if self.complete_at is not None: parts.append(f"complete_at={self.complete_at}") if len(self.possibilities) > 0: parts.append(f"possibilities={self.possibilities}") return "AccessRequirements(" + ", ".join(parts) + ")" def check_access(self, state: CollectionState, world: "Lingo2World") -> bool: if not all(state.has(item, world.player) for item in self.items): return False if not all(state.has(item, world.player, amount) for item, amount in self.progressives.items()): return False if not all(state.can_reach_region(region_name, world.player) for region_name in self.rooms): return False for letter_key, letter_level in self.letters.items(): if not state.has(letter_key, world.player, letter_level): return False if self.cyans: if not any(state.has(letter, world.player, amount) for letter, amount in world.player_logic.double_letter_amount.items()): return False if len(self.or_logic) > 0: if not all(any(sub_reqs.check_access(state, world) for sub_reqs in subjunction) for subjunction in self.or_logic): return False if self.complete_at is not None: completed = 0 checked = 0 for possibility in self.possibilities: checked += 1 if possibility.check_access(state, world): completed += 1 if completed >= self.complete_at: break elif len(self.possibilities) - checked + completed < self.complete_at: # There aren't enough remaining possibilities for the check to pass. return False if completed < self.complete_at: return False return True