about summary refs log tree commit diff stats
path: root/apworld
diff options
context:
space:
mode:
authorStar Rauchenberger <fefferburbia@gmail.com>2025-09-12 13:20:39 -0400
committerStar Rauchenberger <fefferburbia@gmail.com>2025-09-12 13:20:39 -0400
commite187853a7cd3fbbfdf99d23a306841e63121e1d8 (patch)
tree36799a8f6b9a5e23471b3ca31808cd62687249e0 /apworld
parent67b721503443274351e0729ac57cbff83d31d753 (diff)
downloadlingo2-archipelago-e187853a7cd3fbbfdf99d23a306841e63121e1d8.tar.gz
lingo2-archipelago-e187853a7cd3fbbfdf99d23a306841e63121e1d8.tar.bz2
lingo2-archipelago-e187853a7cd3fbbfdf99d23a306841e63121e1d8.zip
[Apworld] Some access checking optimizations
Letter requirements in OR logic (which is the main thing OR logic is
used for) is simplified now. Any requirement within the OR logic that is
redundant with the top level requirement now has the redundant letters
removed. If a clause in a disjunction becomes empty due to this, the
disjunction can be removed. Additionally, if all of the clauses in a
disjunction are identical, then they can be merged into the top level
requirement.

I manually verified that every requirement that is affected by this
simplification looks correct.

Region objects are also now used in access checking instead of looking
up the regions by name during access checking. This is a little faster
for access checks that involve a lot of rooms, such as the Maze
Gravestone.

Finally, locations no longer check for access to the region the location
is in, and connections no longer check for access to the source region,
because these are both implied by how the graph works.
Diffstat (limited to 'apworld')
-rw-r--r--apworld/player_logic.py53
-rw-r--r--apworld/regions.py28
-rw-r--r--apworld/rules.py22
3 files changed, 90 insertions, 13 deletions
diff --git a/apworld/player_logic.py b/apworld/player_logic.py index 2ff7163..8e2a523 100644 --- a/apworld/player_logic.py +++ b/apworld/player_logic.py
@@ -45,6 +45,18 @@ class AccessRequirements:
45 self.complete_at = None 45 self.complete_at = None
46 self.possibilities = list() 46 self.possibilities = list()
47 47
48 def copy(self) -> "AccessRequirements":
49 reqs = AccessRequirements()
50 reqs.items = self.items.copy()
51 reqs.progressives = self.progressives.copy()
52 reqs.rooms = self.rooms.copy()
53 reqs.letters = self.letters.copy()
54 reqs.cyans = self.cyans
55 reqs.or_logic = [[other_req.copy() for other_req in disjunction] for disjunction in self.or_logic]
56 reqs.complete_at = self.complete_at
57 reqs.possibilities = self.possibilities.copy()
58 return reqs
59
48 def merge(self, other: "AccessRequirements"): 60 def merge(self, other: "AccessRequirements"):
49 for item in other.items: 61 for item in other.items:
50 self.items.add(item) 62 self.items.add(item)
@@ -88,7 +100,44 @@ class AccessRequirements:
88 100
89 def is_empty(self) -> bool: 101 def is_empty(self) -> bool:
90 return (len(self.items) == 0 and len(self.progressives) == 0 and len(self.rooms) == 0 and len(self.letters) == 0 102 return (len(self.items) == 0 and len(self.progressives) == 0 and len(self.rooms) == 0 and len(self.letters) == 0
91 and not self.cyans and len(self.or_logic) == 0 and self.complete_at is not None) 103 and not self.cyans and len(self.or_logic) == 0 and self.complete_at is None)
104
105 def __eq__(self, other: "AccessRequirements"):
106 return (self.items == other.items and self.progressives == other.progressives and self.rooms == other.rooms and
107 self.letters == other.letters and self.cyans == other.cyans and self.or_logic == other.or_logic and
108 self.complete_at == other.complete_at and self.possibilities == other.possibilities)
109
110 def simplify(self):
111 resimplify = False
112
113 if len(self.or_logic) > 0:
114 old_or_logic = self.or_logic
115
116 def remove_redundant(sub_reqs: "AccessRequirements"):
117 sub_reqs.letters = {l: v for l, v in sub_reqs.letters.items() if self.letters.get(l, 0) < v}
118
119 self.or_logic = []
120 for disjunction in old_or_logic:
121 new_disjunction = []
122 for ssr in disjunction:
123 remove_redundant(ssr)
124 if not ssr.is_empty():
125 new_disjunction.append(ssr)
126 else:
127 new_disjunction.clear()
128 break
129 if len(new_disjunction) == 1:
130 self.merge(new_disjunction[0])
131 resimplify = True
132 elif len(new_disjunction) > 1:
133 if all(cjr == new_disjunction[0] for cjr in new_disjunction):
134 self.merge(new_disjunction[0])
135 resimplify = True
136 else:
137 self.or_logic.append(new_disjunction)
138
139 if resimplify:
140 self.simplify()
92 141
93 def __repr__(self): 142 def __repr__(self):
94 parts = [] 143 parts = []
@@ -403,6 +452,8 @@ class Lingo2PlayerLogic:
403 sub_reqs = self.get_door_open_reqs(sub_door_id) 452 sub_reqs = self.get_door_open_reqs(sub_door_id)
404 reqs.merge(sub_reqs) 453 reqs.merge(sub_reqs)
405 454
455 reqs.simplify()
456
406 return reqs 457 return reqs
407 458
408 # This gets the requirements to open a door within the world. When a door is shuffled, this means having the item 459 # This gets the requirements to open a door within the world. When a door is shuffled, this means having the item
diff --git a/apworld/regions.py b/apworld/regions.py index e30493c..4f1dd55 100644 --- a/apworld/regions.py +++ b/apworld/regions.py
@@ -11,12 +11,18 @@ if TYPE_CHECKING:
11 11
12 12
13def create_region(room, world: "Lingo2World") -> Region: 13def create_region(room, world: "Lingo2World") -> Region:
14 new_region = Region(world.static_logic.get_room_region_name(room.id), world.player, world.multiworld) 14 return Region(world.static_logic.get_room_region_name(room.id), world.player, world.multiworld)
15 15
16
17def create_locations(room, new_region: Region, world: "Lingo2World", regions: dict[str, Region]):
16 for location in world.player_logic.locations_by_room.get(room.id, {}): 18 for location in world.player_logic.locations_by_room.get(room.id, {}):
19 reqs = location.reqs.copy()
20 if new_region.name in reqs.rooms:
21 reqs.rooms.remove(new_region.name)
22
17 new_location = Lingo2Location(world.player, world.static_logic.location_id_to_name[location.code], 23 new_location = Lingo2Location(world.player, world.static_logic.location_id_to_name[location.code],
18 location.code, new_region) 24 location.code, new_region)
19 new_location.access_rule = make_location_lambda(location.reqs, world) 25 new_location.access_rule = make_location_lambda(reqs, world, regions)
20 new_region.locations.append(new_location) 26 new_region.locations.append(new_location)
21 27
22 for event_name, item_name in world.player_logic.event_loc_item_by_room.get(room.id, {}).items(): 28 for event_name, item_name in world.player_logic.event_loc_item_by_room.get(room.id, {}).items():
@@ -25,17 +31,23 @@ def create_region(room, world: "Lingo2World") -> Region:
25 new_location.place_locked_item(event_item) 31 new_location.place_locked_item(event_item)
26 new_region.locations.append(new_location) 32 new_region.locations.append(new_location)
27 33
28 return new_region
29
30
31def create_regions(world: "Lingo2World"): 34def create_regions(world: "Lingo2World"):
32 regions = { 35 regions = {
33 "Menu": Region("Menu", world.player, world.multiworld) 36 "Menu": Region("Menu", world.player, world.multiworld)
34 } 37 }
35 38
39 region_and_room = []
40
41 # Create the regions in two stages. First, make the actual region objects and memoize them. Then, add all of the
42 # locations. This allows us to reference the actual region objects in the access rules for the locations, which is
43 # faster than having to look them up during access checking.
36 for room in world.static_logic.objects.rooms: 44 for room in world.static_logic.objects.rooms:
37 region = create_region(room, world) 45 region = create_region(room, world)
38 regions[region.name] = region 46 regions[region.name] = region
47 region_and_room.append((region, room))
48
49 for (region, room) in region_and_room:
50 create_locations(room, region, world, regions)
39 51
40 regions["Menu"].connect(regions["The Entry - Starting Room"], "Start Game") 52 regions["Menu"].connect(regions["The Entry - Starting Room"], "Start Game")
41 53
@@ -82,14 +94,18 @@ def create_regions(world: "Lingo2World"):
82 else: 94 else:
83 connection_name = f"{connection_name} (via panel {panel.name})" 95 connection_name = f"{connection_name} (via panel {panel.name})"
84 96
97 reqs.simplify()
98
85 if from_region in regions and to_region in regions: 99 if from_region in regions and to_region in regions:
86 connection = Entrance(world.player, connection_name, regions[from_region]) 100 connection = Entrance(world.player, connection_name, regions[from_region])
87 connection.access_rule = make_location_lambda(reqs, world) 101 connection.access_rule = make_location_lambda(reqs, world, regions)
88 102
89 regions[from_region].exits.append(connection) 103 regions[from_region].exits.append(connection)
90 connection.connect(regions[to_region]) 104 connection.connect(regions[to_region])
91 105
92 for region in reqs.rooms: 106 for region in reqs.rooms:
107 if region == from_region:
108 continue
93 world.multiworld.register_indirect_condition(regions[region], connection) 109 world.multiworld.register_indirect_condition(regions[region], connection)
94 110
95 world.multiworld.regions += regions.values() 111 world.multiworld.regions += regions.values()
diff --git a/apworld/rules.py b/apworld/rules.py index 6186637..c077858 100644 --- a/apworld/rules.py +++ b/apworld/rules.py
@@ -1,14 +1,15 @@
1from collections.abc import Callable 1from collections.abc import Callable
2from typing import TYPE_CHECKING 2from typing import TYPE_CHECKING
3 3
4from BaseClasses import CollectionState 4from BaseClasses import CollectionState, Region
5from .player_logic import AccessRequirements 5from .player_logic import AccessRequirements
6 6
7if TYPE_CHECKING: 7if TYPE_CHECKING:
8 from . import Lingo2World 8 from . import Lingo2World
9 9
10 10
11def lingo2_can_satisfy_requirements(state: CollectionState, reqs: AccessRequirements, world: "Lingo2World") -> bool: 11def lingo2_can_satisfy_requirements(state: CollectionState, reqs: AccessRequirements, regions: list[Region],
12 world: "Lingo2World") -> bool:
12 if not all(state.has(item, world.player) for item in reqs.items): 13 if not all(state.has(item, world.player) for item in reqs.items):
13 return False 14 return False
14 15
@@ -18,6 +19,9 @@ def lingo2_can_satisfy_requirements(state: CollectionState, reqs: AccessRequirem
18 if not all(state.can_reach_region(region_name, world.player) for region_name in reqs.rooms): 19 if not all(state.can_reach_region(region_name, world.player) for region_name in reqs.rooms):
19 return False 20 return False
20 21
22 if not all(state.can_reach(region) for region in regions):
23 return False
24
21 for letter_key, letter_level in reqs.letters.items(): 25 for letter_key, letter_level in reqs.letters.items():
22 if not state.has(letter_key, world.player, letter_level): 26 if not state.has(letter_key, world.player, letter_level):
23 return False 27 return False
@@ -28,7 +32,7 @@ def lingo2_can_satisfy_requirements(state: CollectionState, reqs: AccessRequirem
28 return False 32 return False
29 33
30 if len(reqs.or_logic) > 0: 34 if len(reqs.or_logic) > 0:
31 if not all(any(lingo2_can_satisfy_requirements(state, sub_reqs, world) for sub_reqs in subjunction) 35 if not all(any(lingo2_can_satisfy_requirements(state, sub_reqs, [], world) for sub_reqs in subjunction)
32 for subjunction in reqs.or_logic): 36 for subjunction in reqs.or_logic):
33 return False 37 return False
34 38
@@ -37,7 +41,7 @@ def lingo2_can_satisfy_requirements(state: CollectionState, reqs: AccessRequirem
37 checked = 0 41 checked = 0
38 for possibility in reqs.possibilities: 42 for possibility in reqs.possibilities:
39 checked += 1 43 checked += 1
40 if lingo2_can_satisfy_requirements(state, possibility, world): 44 if lingo2_can_satisfy_requirements(state, possibility, [], world):
41 completed += 1 45 completed += 1
42 if completed >= reqs.complete_at: 46 if completed >= reqs.complete_at:
43 break 47 break
@@ -49,5 +53,11 @@ def lingo2_can_satisfy_requirements(state: CollectionState, reqs: AccessRequirem
49 53
50 return True 54 return True
51 55
52def make_location_lambda(reqs: AccessRequirements, world: "Lingo2World") -> Callable[[CollectionState], bool]: 56def make_location_lambda(reqs: AccessRequirements, world: "Lingo2World",
53 return lambda state: lingo2_can_satisfy_requirements(state, reqs, world) 57 regions: dict[str, Region]) -> Callable[[CollectionState], bool]:
58 # Replace required rooms with regions for the top level requirement, which saves looking up the regions during rule
59 # checking.
60 required_regions = [regions[room_name] for room_name in reqs.rooms]
61 new_reqs = reqs.copy()
62 new_reqs.rooms.clear()
63 return lambda state: lingo2_can_satisfy_requirements(state, new_reqs, required_regions, world)