from .generated import data_pb2 as data_pb2 from typing import TYPE_CHECKING, NamedTuple if TYPE_CHECKING: from . import Lingo2World def calculate_letter_histogram(solution: str) -> dict[str, int]: histogram = dict() for l in solution: if l.isalpha(): real_l = l.upper() histogram[real_l] = min(histogram.get(l, 0) + 1, 2) return histogram class AccessRequirements: items: set[str] rooms: set[str] symbols: set[str] letters: dict[str, int] # This is an AND of ORs. or_logic: list[list["AccessRequirements"]] def __init__(self): self.items = set() self.rooms = set() self.symbols = set() self.letters = dict() self.or_logic = list() def add_solution(self, solution: str): histogram = calculate_letter_histogram(solution) for l, a in histogram.items(): self.letters[l] = max(self.letters.get(l, 0), histogram.get(l)) def merge(self, other: "AccessRequirements"): for item in other.items: self.items.add(item) for room in other.rooms: self.rooms.add(room) for symbol in other.symbols: self.symbols.add(symbol) for letter, level in other.letters.items(): self.letters[letter] = max(self.letters.get(letter, 0), level) for disjunction in other.or_logic: self.or_logic.append(disjunction) class PlayerLocation(NamedTuple): code: int | None reqs: AccessRequirements class Lingo2PlayerLogic: world: "Lingo2World" locations_by_room: dict[int, list[PlayerLocation]] item_by_door: dict[int, str] panel_reqs: dict[int, AccessRequirements] proxy_reqs: dict[int, dict[str, AccessRequirements]] door_reqs: dict[int, AccessRequirements] real_items: list[str] def __init__(self, world: "Lingo2World"): self.world = world self.locations_by_room = {} self.item_by_door = {} self.panel_reqs = dict() self.proxy_reqs = dict() self.door_reqs = dict() self.real_items = list() # We iterate through the doors in two parts because it is essential that we determine which doors are shuffled # before we calculate any access requirements. for door in world.static_logic.objects.doors: if door.type in [data_pb2.DoorType.STANDARD, data_pb2.DoorType.ITEM_ONLY] and self.world.options.shuffle_doors: door_item_name = self.world.static_logic.get_door_item_name(door.id) self.item_by_door[door.id] = door_item_name self.real_items.append(door_item_name) for door in world.static_logic.objects.doors: if door.type in [data_pb2.DoorType.STANDARD, data_pb2.DoorType.LOCATION_ONLY, data_pb2.DoorType.GRAVESTONE]: self.locations_by_room.setdefault(door.room_id, []).append(PlayerLocation(door.ap_id, self.get_door_reqs(door.id))) for letter in world.static_logic.objects.letters: self.locations_by_room.setdefault(letter.room_id, []).append(PlayerLocation(letter.ap_id, AccessRequirements())) for mastery in world.static_logic.objects.masteries: self.locations_by_room.setdefault(mastery.room_id, []).append(PlayerLocation(mastery.ap_id, AccessRequirements())) def get_panel_reqs(self, panel_id: int, answer: str | None) -> AccessRequirements: if answer is None: if panel_id not in self.panel_reqs: self.panel_reqs[panel_id] = self.calculate_panel_reqs(panel_id, answer) return self.panel_reqs.get(panel_id) else: if panel_id not in self.proxy_reqs or answer not in self.proxy_reqs.get(panel_id): self.proxy_reqs.setdefault(panel_id, {})[answer] = self.calculate_panel_reqs(panel_id, answer) return self.proxy_reqs.get(panel_id).get(answer) def calculate_panel_reqs(self, panel_id: int, answer: str | None) -> AccessRequirements: panel = self.world.static_logic.objects.panels[panel_id] reqs = AccessRequirements() reqs.rooms.add(self.world.static_logic.get_room_region_name(panel.room_id)) if answer is not None: reqs.add_solution(answer) elif len(panel.proxies) > 0: for proxy in panel.proxies: proxy_reqs = AccessRequirements() proxy_reqs.add_solution(proxy.answer) reqs.or_logic.append([proxy_reqs]) else: reqs.add_solution(panel.answer) for symbol in panel.symbols: reqs.symbols.add(symbol) if panel.HasField("required_door"): door_reqs = self.get_door_open_reqs(panel.required_door) reqs.merge(door_reqs) if panel.HasField("required_room"): reqs.rooms.add(self.world.static_logic.get_room_region_name(panel.required_room)) return reqs # This gets/calculates the requirements described by the door object. This is most notably used as the requirements # for clearing a location, or opening a door when the door is not shuffled. def get_door_reqs(self, door_id: int) -> AccessRequirements: if door_id not in self.door_reqs: self.door_reqs[door_id] = self.calculate_door_reqs(door_id) return self.door_reqs.get(door_id) def calculate_door_reqs(self, door_id: int) -> AccessRequirements: door = self.world.static_logic.objects.doors[door_id] reqs = AccessRequirements() use_item = False if door.type in [data_pb2.DoorType.STANDARD, data_pb2.DoorType.ITEM_ONLY] and self.world.options.shuffle_doors: use_item = True if use_item: reqs.items.add(self.world.static_logic.get_door_item_name(door.id)) else: # TODO: complete_at, control_center_color, switches, keyholders for proxy in door.panels: panel_reqs = self.get_panel_reqs(proxy.panel, proxy.answer if proxy.HasField("answer") else None) reqs.merge(panel_reqs) for room in door.rooms: reqs.rooms.add(self.world.static_logic.get_room_region_name(room)) for sub_door_id in door.doors: sub_reqs = self.get_door_open_reqs(sub_door_id) reqs.merge(sub_reqs) return reqs # This gets the requirements to open a door within the world. When a door is shuffled, this means having the item # that acts as the door's key. def get_door_open_reqs(self, door_id: int) -> AccessRequirements: if door_id in self.item_by_door: reqs = AccessRequirements() reqs.items.add(self.item_by_door.get(door_id)) return reqs else: return self.get_door_reqs(door_id)