about summary refs log tree commit diff stats
path: root/data/maps/the_butterfly
diff options
context:
space:
mode:
Diffstat (limited to 'data/maps/the_butterfly')
0 files changed, 0 insertions, 0 deletions
ref='#n65'>65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 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
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)