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 8 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275