From 7426eb86fb2e7313607493becab262fe3115ce7b Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Fri, 12 Sep 2025 10:54:14 -0400 Subject: [Apworld] Handle complete_at > 1 --- apworld/player_logic.py | 43 +++++++++++++++++++++++++++++++++++++++---- apworld/rules.py | 15 +++++++++++++++ 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/apworld/player_logic.py b/apworld/player_logic.py index 42b36e6..d435bbc 100644 --- a/apworld/player_logic.py +++ b/apworld/player_logic.py @@ -30,6 +30,11 @@ class AccessRequirements: # This is an AND of ORs. or_logic: list[list["AccessRequirements"]] + # When complete_at is set, at least that many of the requirements in possibilities must be accessible. This should + # only be used for doors with complete_at > 1, as or_logic is more efficient for complete_at == 1. + complete_at: int | None + possibilities: list["AccessRequirements"] + def __init__(self): self.items = set() self.progressives = dict() @@ -37,6 +42,8 @@ class AccessRequirements: self.letters = dict() self.cyans = False self.or_logic = list() + self.complete_at = None + self.possibilities = list() def merge(self, other: "AccessRequirements"): for item in other.items: @@ -56,9 +63,32 @@ class AccessRequirements: for disjunction in other.or_logic: self.or_logic.append(disjunction) + if other.complete_at is not None: + # Merging multiple requirements that use complete_at sucks, and is part of why we want to minimize use of + # it. If both requirements use complete_at, we will cheat by using the or_logic field, which supports + # conjunctions of requirements. + if self.complete_at is not None: + print("Merging requirements with complete_at > 1. This is messy and should be avoided!") + + left_req = AccessRequirements() + left_req.complete_at = self.complete_at + left_req.possibilities = self.possibilities + self.or_logic.append([left_req]) + + self.complete_at = None + self.possibilities = list() + + right_req = AccessRequirements() + right_req.complete_at = other.complete_at + right_req.possibilities = other.possibilities + self.or_logic.append([right_req]) + else: + self.complete_at = other.complete_at + self.possibilities = other.possibilities + def is_empty(self) -> bool: return (len(self.items) == 0 and len(self.progressives) == 0 and len(self.rooms) == 0 and len(self.letters) == 0 - and not self.cyans and len(self.or_logic) == 0) + and not self.cyans and len(self.or_logic) == 0 and self.complete_at is not None) def __repr__(self): parts = [] @@ -74,6 +104,10 @@ class AccessRequirements: parts.append(f"cyans=True") if len(self.or_logic) > 0: parts.append(f"or_logic={self.or_logic}") + if self.complete_at is not None: + parts.append(f"complete_at={self.complete_at}") + if len(self.possibilities) > 0: + parts.append(f"possibilities={self.possibilities}") return f"AccessRequirements({", ".join(parts)})" @@ -306,7 +340,6 @@ class Lingo2PlayerLogic: door = self.world.static_logic.objects.doors[door_id] reqs = AccessRequirements() - # TODO: lavender_cubes, endings 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) @@ -323,8 +356,10 @@ class Lingo2PlayerLogic: if len(disjunction) > 0: reqs.or_logic.append(disjunction) else: - # TODO: Handle complete_at > 1 - pass + reqs.complete_at = door.complete_at + for proxy in door.panels: + panel_reqs = self.get_panel_reqs(proxy.panel, proxy.answer if proxy.HasField("answer") else None) + reqs.possibilities.append(panel_reqs) if door.HasField("control_center_color"): # TODO: Logic for ensuring two CC states aren't needed at once. diff --git a/apworld/rules.py b/apworld/rules.py index 0bff056..6186637 100644 --- a/apworld/rules.py +++ b/apworld/rules.py @@ -32,6 +32,21 @@ def lingo2_can_satisfy_requirements(state: CollectionState, reqs: AccessRequirem for subjunction in reqs.or_logic): return False + if reqs.complete_at is not None: + completed = 0 + checked = 0 + for possibility in reqs.possibilities: + checked += 1 + if lingo2_can_satisfy_requirements(state, possibility, world): + completed += 1 + if completed >= reqs.complete_at: + break + elif len(reqs.possibilities) - checked + completed < reqs.complete_at: + # There aren't enough remaining possibilities for the check to pass. + return False + if completed < reqs.complete_at: + return False + return True def make_location_lambda(reqs: AccessRequirements, world: "Lingo2World") -> Callable[[CollectionState], bool]: -- cgit 1.4.1