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.md54
-rw-r--r--apworld/__init__.py20
-rw-r--r--apworld/items.py2
-rw-r--r--apworld/options.py55
-rw-r--r--apworld/player_logic.py127
-rw-r--r--apworld/regions.py47
-rw-r--r--apworld/rules.py35
-rw-r--r--apworld/static_logic.py16
-rw-r--r--apworld/version.py2
9 files changed, 328 insertions, 30 deletions
diff --git a/apworld/CHANGELOG.md b/apworld/CHANGELOG.md new file mode 100644 index 0000000..af45992 --- /dev/null +++ b/apworld/CHANGELOG.md
@@ -0,0 +1,54 @@
1# lingo2-archipelago Apworld Releases
2
3## v5.5 - 2025-09-16
4
5- Fixed a panel in The Ancient that was missing a symbol.
6- Fixed an issue where you could be expected to get S1 in The Darkroom without
7 having U.
8- Renamed a few locations.
9
10Download:
11[lingo2.apworld](https://files.fourisland.com/releases/lingo2-archipelago/apworld/v5.5/lingo2.apworld)<br/>
12Template YAML:
13[Lingo 2.yaml](https://files.fourisland.com/releases/lingo2-archipelago/apworld/v5.5/Lingo%202.yaml)<br/>
14Source:
15[v5.5](https://code.fourisland.com/lingo2-archipelago/tag/?h=apworld-v5.5)
16
17## v4.4 - 2025-09-14
18
19- Fixed panel set location names.
20
21Download:
22[lingo2.apworld](https://files.fourisland.com/releases/lingo2-archipelago/apworld/v4.4/lingo2.apworld)<br/>
23Template YAML:
24[Lingo 2.yaml](https://files.fourisland.com/releases/lingo2-archipelago/apworld/v4.4/Lingo%202.yaml)<br/>
25Source:
26[v4.4](https://code.fourisland.com/lingo2-archipelago/tag/?h=apworld-v4.4)
27
28## v4.3 - 2025-09-13
29
30- Added a location for the anti-collectable in The Repetitive.
31- Added trap items. These remove letters from your keyboard until you use the
32 Key Return in The Entry, similar to the anti-collectable in The Repetitive.
33 This can be controlled using the `trap_percentage` option, which defaults to
34 zero.
35- Fixed crash on load when using Python 3.11.
36
37Download:
38[lingo2.apworld](https://files.fourisland.com/releases/lingo2-archipelago/apworld/v4.3/lingo2.apworld)<br/>
39Template YAML:
40[Lingo 2.yaml](https://files.fourisland.com/releases/lingo2-archipelago/apworld/v4.3/Lingo%202.yaml)<br/>
41Source:
42[v4.3](https://code.fourisland.com/lingo2-archipelago/tag/?h=apworld-v4.3)
43
44## v3.2 - 2025-09-12
45
46- Initial release for testing. Features include door shuffle, letter shuffle,
47 and symbol shuffle.
48
49Download:
50[lingo2.apworld](https://files.fourisland.com/releases/lingo2-archipelago/apworld/v3.2/lingo2.apworld)<br/>
51Template YAML:
52[Lingo 2.yaml](https://files.fourisland.com/releases/lingo2-archipelago/apworld/v3.2/Lingo%202.yaml)<br/>
53Source:
54[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..f1de503 100644 --- a/apworld/__init__.py +++ b/apworld/__init__.py
@@ -3,7 +3,7 @@ Archipelago init file for Lingo 2
3""" 3"""
4from BaseClasses import ItemClassification, Item, Tutorial 4from BaseClasses import ItemClassification, Item, Tutorial
5from worlds.AutoWorld import WebWorld, World 5from worlds.AutoWorld import WebWorld, World
6from .items import Lingo2Item 6from .items import Lingo2Item, ANTI_COLLECTABLE_TRAPS
7from .options import Lingo2Options 7from .options import Lingo2Options
8from .player_logic import Lingo2PlayerLogic 8from .player_logic import Lingo2PlayerLogic
9from .regions import create_regions 9from .regions import create_regions
@@ -62,6 +62,20 @@ class Lingo2World(World):
62 total_locations = sum(len(locs) for locs in self.player_logic.locations_by_room.values()) 62 total_locations = sum(len(locs) for locs in self.player_logic.locations_by_room.values())
63 63
64 item_difference = total_locations - len(pool) 64 item_difference = total_locations - len(pool)
65
66 if self.options.trap_percentage > 0:
67 num_traps = int(item_difference * self.options.trap_percentage / 100)
68 item_difference = item_difference - num_traps
69
70 trap_names = []
71 trap_weights = []
72 for letter_name, weight in self.static_logic.letter_weights.items():
73 trap_names.append(f"Anti {letter_name}")
74 trap_weights.append(weight)
75
76 bad_letters = self.random.choices(trap_names, weights=trap_weights, k=num_traps)
77 pool += [self.create_item(trap_name) for trap_name in bad_letters]
78
65 for i in range(0, item_difference): 79 for i in range(0, item_difference):
66 pool.append(self.create_item(self.get_filler_item_name())) 80 pool.append(self.create_item(self.get_filler_item_name()))
67 81
@@ -69,6 +83,7 @@ class Lingo2World(World):
69 83
70 def create_item(self, name: str) -> Item: 84 def create_item(self, name: str) -> Item:
71 return Lingo2Item(name, ItemClassification.filler if name == self.get_filler_item_name() else 85 return Lingo2Item(name, ItemClassification.filler if name == self.get_filler_item_name() else
86 ItemClassification.trap if name in ANTI_COLLECTABLE_TRAPS else
72 ItemClassification.progression, 87 ItemClassification.progression,
73 self.item_name_to_id.get(name), self.player) 88 self.item_name_to_id.get(name), self.player)
74 89
@@ -82,8 +97,11 @@ class Lingo2World(World):
82 "keyholder_sanity", 97 "keyholder_sanity",
83 "shuffle_control_center_colors", 98 "shuffle_control_center_colors",
84 "shuffle_doors", 99 "shuffle_doors",
100 "shuffle_gallery_paintings",
85 "shuffle_letters", 101 "shuffle_letters",
86 "shuffle_symbols", 102 "shuffle_symbols",
103 "strict_cyan_ending",
104 "strict_purple_ending",
87 "victory_condition", 105 "victory_condition",
88 ] 106 ]
89 107
diff --git a/apworld/items.py b/apworld/items.py index 32568a3..28158c3 100644 --- a/apworld/items.py +++ b/apworld/items.py
@@ -27,3 +27,5 @@ SYMBOL_ITEMS: dict[data_pb2.PuzzleSymbol, str] = {
27 data_pb2.PuzzleSymbol.LINGO: "Lingo Symbol", 27 data_pb2.PuzzleSymbol.LINGO: "Lingo Symbol",
28 data_pb2.PuzzleSymbol.QUESTION: "Question Symbol", 28 data_pb2.PuzzleSymbol.QUESTION: "Question Symbol",
29} 29}
30
31ANTI_COLLECTABLE_TRAPS: list[str] = [f"Anti {letter}" for letter in "ABCDEFGHIJKLMNOPQRSTUVWXYZ"]
diff --git a/apworld/options.py b/apworld/options.py index f72e826..3646eea 100644 --- a/apworld/options.py +++ b/apworld/options.py
@@ -1,9 +1,9 @@
1from dataclasses import dataclass 1from dataclasses import dataclass
2 2
3from Options import PerGameCommonOptions, Toggle, Choice 3from Options import PerGameCommonOptions, Toggle, Choice, DefaultOnToggle, Range
4 4
5 5
6class ShuffleDoors(Toggle): 6class ShuffleDoors(DefaultOnToggle):
7 """If enabled, most doors will open from receiving an item rather than fulfilling the in-game requirements.""" 7 """If enabled, most doors will open from receiving an item rather than fulfilling the in-game requirements."""
8 display_name = "Shuffle Doors" 8 display_name = "Shuffle Doors"
9 9
@@ -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
@@ -87,8 +92,40 @@ class DaedalusRoofAccess(Toggle):
87 display_name = "Allow Daedalus Roof Access" 92 display_name = "Allow Daedalus Roof Access"
88 93
89 94
95class StrictPurpleEnding(DefaultOnToggle):
96 """
97 If enabled, the player will be required to have all purple (level 1) letters in order to get Purple Ending.
98 Otherwise, some of the letters may be skippable depending on the other options.
99 """
100 display_name = "Strict Purple Ending"
101
102
103class StrictCyanEnding(DefaultOnToggle):
104 """
105 If enabled, the player will be required to have all cyan (level 2) letters in order to get Cyan Ending. Otherwise,
106 at least J2, Q2, and V2 are skippable. Others may also be skippable depending on the options chosen.
107 """
108 display_name = "Strict Cyan Ending"
109
110
90class VictoryCondition(Choice): 111class VictoryCondition(Choice):
91 """Victory condition.""" 112 """
113 This option determines what your goal is.
114
115 - **Gray Ending** (The Colorful)
116 - **Purple Ending** (The Sun Temple). This ordinarily requires all level 1 (purple) letters.
117 - **Mint Ending** (typing EXIT into the keyholders in Control Center)
118 - **Black Ending** (The Graveyard)
119 - **Blue Ending** (The Words)
120 - **Cyan Ending** (The Parthenon). This ordinarily requires almost all level 2 (cyan) letters.
121 - **Red Ending** (The Tower)
122 - **Plum Ending** (The Wondrous / The Door)
123 - **Orange Ending** (the castle in Daedalus)
124 - **Gold Ending** (The Gold). This involves going through the color rooms in Daedalus.
125 - **Yellow Ending** (The Gallery). This requires unlocking all gallery paintings.
126 - **Green Ending** (The Ancient). This requires filling all keyholders with specific letters.
127 - **White Ending** (Control Center). This combines every other ending.
128 """
92 display_name = "Victory Condition" 129 display_name = "Victory Condition"
93 option_gray_ending = 0 130 option_gray_ending = 0
94 option_purple_ending = 1 131 option_purple_ending = 1
@@ -105,13 +142,25 @@ class VictoryCondition(Choice):
105 option_white_ending = 12 142 option_white_ending = 12
106 143
107 144
145class TrapPercentage(Range):
146 """Replaces junk items with traps, at the specified rate."""
147 display_name = "Trap Percentage"
148 range_start = 0
149 range_end = 100
150 default = 0
151
152
108@dataclass 153@dataclass
109class Lingo2Options(PerGameCommonOptions): 154class Lingo2Options(PerGameCommonOptions):
110 shuffle_doors: ShuffleDoors 155 shuffle_doors: ShuffleDoors
111 shuffle_control_center_colors: ShuffleControlCenterColors 156 shuffle_control_center_colors: ShuffleControlCenterColors
157 shuffle_gallery_paintings: ShuffleGalleryPaintings
112 shuffle_letters: ShuffleLetters 158 shuffle_letters: ShuffleLetters
113 shuffle_symbols: ShuffleSymbols 159 shuffle_symbols: ShuffleSymbols
114 keyholder_sanity: KeyholderSanity 160 keyholder_sanity: KeyholderSanity
115 cyan_door_behavior: CyanDoorBehavior 161 cyan_door_behavior: CyanDoorBehavior
116 daedalus_roof_access: DaedalusRoofAccess 162 daedalus_roof_access: DaedalusRoofAccess
163 strict_purple_ending: StrictPurpleEnding
164 strict_cyan_ending: StrictCyanEnding
117 victory_condition: VictoryCondition 165 victory_condition: VictoryCondition
166 trap_percentage: TrapPercentage
diff --git a/apworld/player_logic.py b/apworld/player_logic.py index 42b36e6..4aa481d 100644 --- a/apworld/player_logic.py +++ b/apworld/player_logic.py
@@ -30,6 +30,11 @@ class AccessRequirements:
30 # This is an AND of ORs. 30 # This is an AND of ORs.
31 or_logic: list[list["AccessRequirements"]] 31 or_logic: list[list["AccessRequirements"]]
32 32
33 # When complete_at is set, at least that many of the requirements in possibilities must be accessible. This should
34 # only be used for doors with complete_at > 1, as or_logic is more efficient for complete_at == 1.
35 complete_at: int | None
36 possibilities: list["AccessRequirements"]
37
33 def __init__(self): 38 def __init__(self):
34 self.items = set() 39 self.items = set()
35 self.progressives = dict() 40 self.progressives = dict()
@@ -37,6 +42,20 @@ class AccessRequirements:
37 self.letters = dict() 42 self.letters = dict()
38 self.cyans = False 43 self.cyans = False
39 self.or_logic = list() 44 self.or_logic = list()
45 self.complete_at = None
46 self.possibilities = list()
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
40 59
41 def merge(self, other: "AccessRequirements"): 60 def merge(self, other: "AccessRequirements"):
42 for item in other.items: 61 for item in other.items:
@@ -56,9 +75,97 @@ class AccessRequirements:
56 for disjunction in other.or_logic: 75 for disjunction in other.or_logic:
57 self.or_logic.append(disjunction) 76 self.or_logic.append(disjunction)
58 77
78 if other.complete_at is not None:
79 # Merging multiple requirements that use complete_at sucks, and is part of why we want to minimize use of
80 # it. If both requirements use complete_at, we will cheat by using the or_logic field, which supports
81 # conjunctions of requirements.
82 if self.complete_at is not None:
83 print("Merging requirements with complete_at > 1. This is messy and should be avoided!")
84
85 left_req = AccessRequirements()
86 left_req.complete_at = self.complete_at
87 left_req.possibilities = self.possibilities
88 self.or_logic.append([left_req])
89
90 self.complete_at = None
91 self.possibilities = list()
92
93 right_req = AccessRequirements()
94 right_req.complete_at = other.complete_at
95 right_req.possibilities = other.possibilities
96 self.or_logic.append([right_req])
97 else:
98 self.complete_at = other.complete_at
99 self.possibilities = other.possibilities
100
59 def is_empty(self) -> bool: 101 def is_empty(self) -> bool:
60 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
61 and not self.cyans and len(self.or_logic) == 0) 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 new_reqs = sub_reqs.copy()
118 new_reqs.letters = {l: v for l, v in new_reqs.letters.items() if self.letters.get(l, 0) < v}
119 if new_reqs != sub_reqs:
120 return new_reqs
121 else:
122 return sub_reqs
123
124 self.or_logic = []
125 for disjunction in old_or_logic:
126 new_disjunction = []
127 for ssr in disjunction:
128 new_ssr = remove_redundant(ssr)
129 if not new_ssr.is_empty():
130 new_disjunction.append(new_ssr)
131 else:
132 new_disjunction.clear()
133 break
134 if len(new_disjunction) == 1:
135 self.merge(new_disjunction[0])
136 resimplify = True
137 elif len(new_disjunction) > 1:
138 if all(cjr == new_disjunction[0] for cjr in new_disjunction):
139 self.merge(new_disjunction[0])
140 resimplify = True
141 else:
142 self.or_logic.append(new_disjunction)
143
144 if resimplify:
145 self.simplify()
146
147 def get_referenced_rooms(self):
148 result = set(self.rooms)
149
150 for disjunction in self.or_logic:
151 for sub_req in disjunction:
152 result = result.union(sub_req.get_referenced_rooms())
153
154 for sub_req in self.possibilities:
155 result = result.union(sub_req.get_referenced_rooms())
156
157 return result
158
159 def remove_room(self, room: str):
160 if room in self.rooms:
161 self.rooms.remove(room)
162
163 for disjunction in self.or_logic:
164 for sub_req in disjunction:
165 sub_req.remove_room(room)
166
167 for sub_req in self.possibilities:
168 sub_req.remove_room(room)
62 169
63 def __repr__(self): 170 def __repr__(self):
64 parts = [] 171 parts = []
@@ -74,7 +181,11 @@ class AccessRequirements:
74 parts.append(f"cyans=True") 181 parts.append(f"cyans=True")
75 if len(self.or_logic) > 0: 182 if len(self.or_logic) > 0:
76 parts.append(f"or_logic={self.or_logic}") 183 parts.append(f"or_logic={self.or_logic}")
77 return f"AccessRequirements({", ".join(parts)})" 184 if self.complete_at is not None:
185 parts.append(f"complete_at={self.complete_at}")
186 if len(self.possibilities) > 0:
187 parts.append(f"possibilities={self.possibilities}")
188 return "AccessRequirements(" + ", ".join(parts) + ")"
78 189
79 190
80class PlayerLocation(NamedTuple): 191class PlayerLocation(NamedTuple):
@@ -156,6 +267,9 @@ class Lingo2PlayerLogic:
156 not self.world.options.shuffle_control_center_colors): 267 not self.world.options.shuffle_control_center_colors):
157 continue 268 continue
158 269
270 if door.type == data_pb2.DoorType.GALLERY_PAINTING and not self.world.options.shuffle_gallery_paintings:
271 continue
272
159 door_item_name = self.world.static_logic.get_door_item_name(door) 273 door_item_name = self.world.static_logic.get_door_item_name(door)
160 self.item_by_door[door.id] = (door_item_name, 1) 274 self.item_by_door[door.id] = (door_item_name, 1)
161 self.real_items.append(door_item_name) 275 self.real_items.append(door_item_name)
@@ -306,7 +420,6 @@ class Lingo2PlayerLogic:
306 door = self.world.static_logic.objects.doors[door_id] 420 door = self.world.static_logic.objects.doors[door_id]
307 reqs = AccessRequirements() 421 reqs = AccessRequirements()
308 422
309 # TODO: lavender_cubes, endings
310 if not door.HasField("complete_at") or door.complete_at == 0: 423 if not door.HasField("complete_at") or door.complete_at == 0:
311 for proxy in door.panels: 424 for proxy in door.panels:
312 panel_reqs = self.get_panel_reqs(proxy.panel, proxy.answer if proxy.HasField("answer") else None) 425 panel_reqs = self.get_panel_reqs(proxy.panel, proxy.answer if proxy.HasField("answer") else None)
@@ -323,8 +436,10 @@ class Lingo2PlayerLogic:
323 if len(disjunction) > 0: 436 if len(disjunction) > 0:
324 reqs.or_logic.append(disjunction) 437 reqs.or_logic.append(disjunction)
325 else: 438 else:
326 # TODO: Handle complete_at > 1 439 reqs.complete_at = door.complete_at
327 pass 440 for proxy in door.panels:
441 panel_reqs = self.get_panel_reqs(proxy.panel, proxy.answer if proxy.HasField("answer") else None)
442 reqs.possibilities.append(panel_reqs)
328 443
329 if door.HasField("control_center_color"): 444 if door.HasField("control_center_color"):
330 # TODO: Logic for ensuring two CC states aren't needed at once. 445 # TODO: Logic for ensuring two CC states aren't needed at once.
@@ -365,6 +480,8 @@ class Lingo2PlayerLogic:
365 sub_reqs = self.get_door_open_reqs(sub_door_id) 480 sub_reqs = self.get_door_open_reqs(sub_door_id)
366 reqs.merge(sub_reqs) 481 reqs.merge(sub_reqs)
367 482
483 reqs.simplify()
484
368 return reqs 485 return reqs
369 486
370 # This gets the requirements to open a door within the world. When a door is shuffled, this means having the item 487 # 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..993eec8 100644 --- a/apworld/regions.py +++ b/apworld/regions.py
@@ -11,12 +11,17 @@ 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 reqs.remove_room(new_region.name)
21
17 new_location = Lingo2Location(world.player, world.static_logic.location_id_to_name[location.code], 22 new_location = Lingo2Location(world.player, world.static_logic.location_id_to_name[location.code],
18 location.code, new_region) 23 location.code, new_region)
19 new_location.access_rule = make_location_lambda(location.reqs, world) 24 new_location.access_rule = make_location_lambda(reqs, world, regions)
20 new_region.locations.append(new_location) 25 new_region.locations.append(new_location)
21 26
22 for event_name, item_name in world.player_logic.event_loc_item_by_room.get(room.id, {}).items(): 27 for event_name, item_name in world.player_logic.event_loc_item_by_room.get(room.id, {}).items():
@@ -25,17 +30,23 @@ def create_region(room, world: "Lingo2World") -> Region:
25 new_location.place_locked_item(event_item) 30 new_location.place_locked_item(event_item)
26 new_region.locations.append(new_location) 31 new_region.locations.append(new_location)
27 32
28 return new_region
29
30
31def create_regions(world: "Lingo2World"): 33def create_regions(world: "Lingo2World"):
32 regions = { 34 regions = {
33 "Menu": Region("Menu", world.player, world.multiworld) 35 "Menu": Region("Menu", world.player, world.multiworld)
34 } 36 }
35 37
38 region_and_room = []
39
40 # Create the regions in two stages. First, make the actual region objects and memoize them. Then, add all of the
41 # locations. This allows us to reference the actual region objects in the access rules for the locations, which is
42 # faster than having to look them up during access checking.
36 for room in world.static_logic.objects.rooms: 43 for room in world.static_logic.objects.rooms:
37 region = create_region(room, world) 44 region = create_region(room, world)
38 regions[region.name] = region 45 regions[region.name] = region
46 region_and_room.append((region, room))
47
48 for (region, room) in region_and_room:
49 create_locations(room, region, world, regions)
39 50
40 regions["Menu"].connect(regions["The Entry - Starting Room"], "Start Game") 51 regions["Menu"].connect(regions["The Entry - Starting Room"], "Start Game")
41 52
@@ -46,6 +57,10 @@ def create_regions(world: "Lingo2World"):
46 57
47 from_region = world.static_logic.get_room_region_name(connection.from_room) 58 from_region = world.static_logic.get_room_region_name(connection.from_room)
48 to_region = world.static_logic.get_room_region_name(connection.to_room) 59 to_region = world.static_logic.get_room_region_name(connection.to_room)
60
61 if from_region not in regions or to_region not in regions:
62 continue
63
49 connection_name = f"{from_region} -> {to_region}" 64 connection_name = f"{from_region} -> {to_region}"
50 65
51 reqs = AccessRequirements() 66 reqs = AccessRequirements()
@@ -82,14 +97,22 @@ def create_regions(world: "Lingo2World"):
82 else: 97 else:
83 connection_name = f"{connection_name} (via panel {panel.name})" 98 connection_name = f"{connection_name} (via panel {panel.name})"
84 99
85 if from_region in regions and to_region in regions: 100 if connection.HasField("purple_ending") and connection.purple_ending and world.options.strict_purple_ending:
86 connection = Entrance(world.player, connection_name, regions[from_region]) 101 world.player_logic.add_solution_reqs(reqs, "abcdefghijklmnopqrstuvwxyz")
87 connection.access_rule = make_location_lambda(reqs, world) 102
103 if connection.HasField("cyan_ending") and connection.cyan_ending and world.options.strict_cyan_ending:
104 world.player_logic.add_solution_reqs(reqs, "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz")
105
106 reqs.simplify()
107 reqs.remove_room(from_region)
108
109 connection = Entrance(world.player, connection_name, regions[from_region])
110 connection.access_rule = make_location_lambda(reqs, world, regions)
88 111
89 regions[from_region].exits.append(connection) 112 regions[from_region].exits.append(connection)
90 connection.connect(regions[to_region]) 113 connection.connect(regions[to_region])
91 114
92 for region in reqs.rooms: 115 for region in reqs.get_referenced_rooms():
93 world.multiworld.register_indirect_condition(regions[region], connection) 116 world.multiworld.register_indirect_condition(regions[region], connection)
94 117
95 world.multiworld.regions += regions.values() 118 world.multiworld.regions += regions.values()
diff --git a/apworld/rules.py b/apworld/rules.py index 0bff056..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,11 +32,32 @@ 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
39 if reqs.complete_at is not None:
40 completed = 0
41 checked = 0
42 for possibility in reqs.possibilities:
43 checked += 1
44 if lingo2_can_satisfy_requirements(state, possibility, [], world):
45 completed += 1
46 if completed >= reqs.complete_at:
47 break
48 elif len(reqs.possibilities) - checked + completed < reqs.complete_at:
49 # There aren't enough remaining possibilities for the check to pass.
50 return False
51 if completed < reqs.complete_at:
52 return False
53
35 return True 54 return True
36 55
37def make_location_lambda(reqs: AccessRequirements, world: "Lingo2World") -> Callable[[CollectionState], bool]: 56def make_location_lambda(reqs: AccessRequirements, world: "Lingo2World",
38 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/static_logic.py b/apworld/static_logic.py index 07800f8..e4d7d49 100644 --- a/apworld/static_logic.py +++ b/apworld/static_logic.py
@@ -1,5 +1,5 @@
1from .generated import data_pb2 as data_pb2 1from .generated import data_pb2 as data_pb2
2from .items import SYMBOL_ITEMS 2from .items import SYMBOL_ITEMS, ANTI_COLLECTABLE_TRAPS
3import pkgutil 3import pkgutil
4 4
5class Lingo2StaticLogic: 5class Lingo2StaticLogic:
@@ -12,11 +12,14 @@ class Lingo2StaticLogic:
12 item_name_groups: dict[str, list[str]] 12 item_name_groups: dict[str, list[str]]
13 location_name_groups: dict[str, list[str]] 13 location_name_groups: dict[str, list[str]]
14 14
15 letter_weights: dict[str, int]
16
15 def __init__(self): 17 def __init__(self):
16 self.item_id_to_name = {} 18 self.item_id_to_name = {}
17 self.location_id_to_name = {} 19 self.location_id_to_name = {}
18 self.item_name_groups = {} 20 self.item_name_groups = {}
19 self.location_name_groups = {} 21 self.location_name_groups = {}
22 self.letter_weights = {}
20 23
21 file = pkgutil.get_data(__name__, "generated/data.binpb") 24 file = pkgutil.get_data(__name__, "generated/data.binpb")
22 self.objects = data_pb2.AllObjects() 25 self.objects = data_pb2.AllObjects()
@@ -68,9 +71,16 @@ class Lingo2StaticLogic:
68 for symbol_name in SYMBOL_ITEMS.values(): 71 for symbol_name in SYMBOL_ITEMS.values():
69 self.item_id_to_name[self.objects.special_ids[symbol_name]] = symbol_name 72 self.item_id_to_name[self.objects.special_ids[symbol_name]] = symbol_name
70 73
74 for trap_name in ANTI_COLLECTABLE_TRAPS:
75 self.item_id_to_name[self.objects.special_ids[trap_name]] = trap_name
76
71 self.item_name_to_id = {name: ap_id for ap_id, name in self.item_id_to_name.items()} 77 self.item_name_to_id = {name: ap_id for ap_id, name in self.item_id_to_name.items()}
72 self.location_name_to_id = {name: ap_id for ap_id, name in self.location_id_to_name.items()} 78 self.location_name_to_id = {name: ap_id for ap_id, name in self.location_id_to_name.items()}
73 79
80 for panel in self.objects.panels:
81 for letter in panel.answer.upper():
82 self.letter_weights[letter] = self.letter_weights.get(letter, 0) + 1
83
74 def get_door_item_name(self, door: data_pb2.Door) -> str: 84 def get_door_item_name(self, door: data_pb2.Door) -> str:
75 return f"{self.get_map_object_map_name(door)} - {door.name}" 85 return f"{self.get_map_object_map_name(door)} - {door.name}"
76 86
@@ -94,7 +104,7 @@ class Lingo2StaticLogic:
94 if door.type != data_pb2.DoorType.STANDARD: 104 if door.type != data_pb2.DoorType.STANDARD:
95 return None 105 return None
96 106
97 if len(door.keyholders) > 0 or len(door.endings) > 0: 107 if len(door.keyholders) > 0 or len(door.endings) > 0 or door.HasField("complete_at"):
98 return None 108 return None
99 109
100 if len(door.panels) > 4: 110 if len(door.panels) > 4:
@@ -130,7 +140,7 @@ class Lingo2StaticLogic:
130 for panel_id in door.panels] 140 for panel_id in door.panels]
131 panel_names.sort() 141 panel_names.sort()
132 142
133 return f"{map_part} - {", ".join(panel_names)}" 143 return map_part + " - " + ", ".join(panel_names)
134 144
135 def get_door_location_name_by_id(self, door_id: int) -> str: 145 def get_door_location_name_by_id(self, door_id: int) -> str:
136 door = self.objects.doors[door_id] 146 door = self.objects.doors[door_id]
diff --git a/apworld/version.py b/apworld/version.py index 645cce6..ac799cd 100644 --- a/apworld/version.py +++ b/apworld/version.py
@@ -1 +1 @@
APWORLD_VERSION = 0 APWORLD_VERSION = 6