about summary refs log tree commit diff stats
path: root/apworld
diff options
context:
space:
mode:
Diffstat (limited to 'apworld')
-rw-r--r--apworld/CHANGELOG.md13
-rw-r--r--apworld/__init__.py1
-rw-r--r--apworld/options.py6
-rw-r--r--apworld/player_logic.py56
-rw-r--r--apworld/regions.py28
-rw-r--r--apworld/rules.py22
-rw-r--r--apworld/version.py2
7 files changed, 114 insertions, 14 deletions
diff --git a/apworld/CHANGELOG.md b/apworld/CHANGELOG.md new file mode 100644 index 0000000..7db040c --- /dev/null +++ b/apworld/CHANGELOG.md
@@ -0,0 +1,13 @@
1# lingo2-archipelago Apworld Releases
2
3## v3.2 - 2025-09-12
4
5- Initial release for testing. Features include door shuffle, letter shuffle,
6 and symbol shuffle.
7
8Download:
9[lingo2.apworld](https://files.fourisland.com/releases/lingo2-archipelago/apworld/v3.2/lingo2.apworld)<br/>
10Template YAML:
11[Lingo 2.yaml](https://files.fourisland.com/releases/lingo2-archipelago/apworld/v3.2/Lingo%202.yaml)<br/>
12Source:
13[v3.2](https://code.fourisland.com/lingo2-archipelago/tag/?h=apworld-v3.2)
diff --git a/apworld/__init__.py b/apworld/__init__.py index 4044d76..54f870f 100644 --- a/apworld/__init__.py +++ b/apworld/__init__.py
@@ -82,6 +82,7 @@ class Lingo2World(World):
82 "keyholder_sanity", 82 "keyholder_sanity",
83 "shuffle_control_center_colors", 83 "shuffle_control_center_colors",
84 "shuffle_doors", 84 "shuffle_doors",
85 "shuffle_gallery_paintings",
85 "shuffle_letters", 86 "shuffle_letters",
86 "shuffle_symbols", 87 "shuffle_symbols",
87 "victory_condition", 88 "victory_condition",
diff --git a/apworld/options.py b/apworld/options.py index 240f8af..4f0b32a 100644 --- a/apworld/options.py +++ b/apworld/options.py
@@ -16,6 +16,11 @@ class ShuffleControlCenterColors(Toggle):
16 display_name = "Shuffle Control Center Colors" 16 display_name = "Shuffle Control Center Colors"
17 17
18 18
19class ShuffleGalleryPaintings(Toggle):
20 """If enabled, gallery paintings will appear from receiving an item rather than by triggering them normally."""
21 display_name = "Shuffle Gallery Paintings"
22
23
19class ShuffleLetters(Choice): 24class ShuffleLetters(Choice):
20 """ 25 """
21 Controls how letter unlocks are handled. Note that H1, I1, N1, and T1 will always be present at their vanilla 26 Controls how letter unlocks are handled. Note that H1, I1, N1, and T1 will always be present at their vanilla
@@ -125,6 +130,7 @@ class VictoryCondition(Choice):
125class Lingo2Options(PerGameCommonOptions): 130class Lingo2Options(PerGameCommonOptions):
126 shuffle_doors: ShuffleDoors 131 shuffle_doors: ShuffleDoors
127 shuffle_control_center_colors: ShuffleControlCenterColors 132 shuffle_control_center_colors: ShuffleControlCenterColors
133 shuffle_gallery_paintings: ShuffleGalleryPaintings
128 shuffle_letters: ShuffleLetters 134 shuffle_letters: ShuffleLetters
129 shuffle_symbols: ShuffleSymbols 135 shuffle_symbols: ShuffleSymbols
130 keyholder_sanity: KeyholderSanity 136 keyholder_sanity: KeyholderSanity
diff --git a/apworld/player_logic.py b/apworld/player_logic.py index d435bbc..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 = []
@@ -190,6 +239,9 @@ class Lingo2PlayerLogic:
190 not self.world.options.shuffle_control_center_colors): 239 not self.world.options.shuffle_control_center_colors):
191 continue 240 continue
192 241
242 if door.type == data_pb2.DoorType.GALLERY_PAINTING and not self.world.options.shuffle_gallery_paintings:
243 continue
244
193 door_item_name = self.world.static_logic.get_door_item_name(door) 245 door_item_name = self.world.static_logic.get_door_item_name(door)
194 self.item_by_door[door.id] = (door_item_name, 1) 246 self.item_by_door[door.id] = (door_item_name, 1)
195 self.real_items.append(door_item_name) 247 self.real_items.append(door_item_name)
@@ -400,6 +452,8 @@ class Lingo2PlayerLogic:
400 sub_reqs = self.get_door_open_reqs(sub_door_id) 452 sub_reqs = self.get_door_open_reqs(sub_door_id)
401 reqs.merge(sub_reqs) 453 reqs.merge(sub_reqs)
402 454
455 reqs.simplify()
456
403 return reqs 457 return reqs
404 458
405 # 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)
diff --git a/apworld/version.py b/apworld/version.py index 12cea7d..87f8797 100644 --- a/apworld/version.py +++ b/apworld/version.py
@@ -1 +1 @@
APWORLD_VERSION = 1 APWORLD_VERSION = 2