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)