about summary refs log tree commit diff stats
path: root/data/maps/control_center/rooms/Perceptive Entrance.txtpb
blob: 6eec265abf19fa8a865e3e9836842891e8e40f08 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
name: "Percep
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]]
    event_loc_item_by_room: dict[int, dict[str, str]]

    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.event_loc_item_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()))

        for ending in world.static_logic.objects.endings:
            self.locations_by_room.setdefault(ending.room_id, []).append(PlayerLocation(ending.ap_id,
                                                                                        AccessRequirements()))

            event_name = f"{ending.name.capitalize()} Ending (Achieved)"
            item_name = event_name

            if world.options.victory_condition.current_key.removesuffix("_ending").upper() == ending.name:
                item_name = "Victory"

            self.event_loc_item_by_room.setdefault(ending.room_id, {})[event_name] = item_name

    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])

            if not any(proxy.answer == panel.answer for proxy in panel.proxies):
                proxy_reqs = AccessRequirements()
                proxy_reqs.add_solution(panel.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()

        # TODO: complete_at, control_center_color, switches, keyholders
        if not door.HasField("complete_at") or door.complete_at == 0:
            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)
        elif door.complete_at == 1:
            reqs.or_logic.append([self.get_panel_reqs(proxy.panel,
                                                      proxy.answer if proxy.HasField("answer") else None)
                                  for proxy in door.panels])
        else:
            # TODO: Handle complete_at > 1
            pass

        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)