From 447a222b57e498f7904033c59e68d21d6a246abd Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Tue, 12 Aug 2025 12:33:24 -0400 Subject: Items and connections in the apworld --- apworld/player_logic.py | 143 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 140 insertions(+), 3 deletions(-) (limited to 'apworld/player_logic.py') diff --git a/apworld/player_logic.py b/apworld/player_logic.py index a3b86bf..958abc5 100644 --- a/apworld/player_logic.py +++ b/apworld/player_logic.py @@ -5,22 +5,159 @@ 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]] + 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.panel_reqs = dict() + self.proxy_reqs = dict() + self.door_reqs = dict() + self.real_items = list() for door in world.static_logic.objects.doors: if door.type in [common_pb2.DoorType.STANDARD, common_pb2.DoorType.LOCATION_ONLY]: - self.locations_by_room.setdefault(door.room_id, []).append(PlayerLocation(door.ap_id)) + self.locations_by_room.setdefault(door.room_id, []).append(PlayerLocation(door.ap_id, + self.get_door_reqs(door.id))) + + if door.type in [common_pb2.DoorType.STANDARD, common_pb2.DoorType.ITEM_ONLY] and self.world.options.shuffle_doors: + self.real_items.append(self.world.static_logic.get_door_item_name(door.id)) for letter in world.static_logic.objects.letters: - self.locations_by_room.setdefault(letter.room_id, []).append(PlayerLocation(letter.ap_id)) + 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)) + 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_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 + + 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 [common_pb2.DoorType.STANDARD, common_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)) + + return reqs -- cgit 1.4.1