summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorStar Rauchenberger <fefferburbia@gmail.com>2024-07-24 08:34:51 -0400
committerGitHub <noreply@github.com>2024-07-24 14:34:51 +0200
commit1c142350c379d503de512953072f55a8737c30d2 (patch)
tree40b469c4be80a2f273706a66683e9074217a6045
parentcccdf6481571cd883c9519cde0a717b6f336fbda (diff)
downloadlingo-apworld-1c142350c379d503de512953072f55a8737c30d2.tar.gz
lingo-apworld-1c142350c379d503de512953072f55a8737c30d2.tar.bz2
lingo-apworld-1c142350c379d503de512953072f55a8737c30d2.zip
Lingo: Add option to prevent shuffling postgame (#3350)
* Lingo: Add option to prevent shuffling postgame

* Allow roof access on door shuffle

* Fix broken unit test

* Simplified THE END edge case

* Revert unnecessary change

* Review comments

* Fix mastery unit test

* Update generated.dat

* Added player's name to error message
-rw-r--r--__init__.py33
-rw-r--r--data/LL1.yaml16
-rw-r--r--data/generated.datbin136277 -> 136563 bytes
-rw-r--r--data/ids.yaml3
-rw-r--r--options.py6
-rw-r--r--player_logic.py34
-rw-r--r--rules.py3
-rw-r--r--test/TestMastery.py6
-rw-r--r--test/TestPostgame.py62
9 files changed, 145 insertions, 18 deletions
diff --git a/__init__.py b/__init__.py index 8d6a7fc..3b67617 100644 --- a/__init__.py +++ b/__init__.py
@@ -3,7 +3,7 @@ Archipelago init file for Lingo
3""" 3"""
4from logging import warning 4from logging import warning
5 5
6from BaseClasses import Item, ItemClassification, Tutorial 6from BaseClasses import CollectionState, Item, ItemClassification, Tutorial
7from Options import OptionError 7from Options import OptionError
8from worlds.AutoWorld import WebWorld, World 8from worlds.AutoWorld import WebWorld, World
9from .datatypes import Room, RoomEntrance 9from .datatypes import Room, RoomEntrance
@@ -68,6 +68,37 @@ class LingoWorld(World):
68 def create_regions(self): 68 def create_regions(self):
69 create_regions(self) 69 create_regions(self)
70 70
71 if not self.options.shuffle_postgame:
72 state = CollectionState(self.multiworld)
73 state.collect(LingoItem("Prevent Victory", ItemClassification.progression, None, self.player), True)
74
75 # Note: relies on the assumption that real_items is a definitive list of real progression items in this
76 # world, and is not modified after being created.
77 for item in self.player_logic.real_items:
78 state.collect(self.create_item(item), True)
79
80 # Exception to the above: a forced good item is not considered a "real item", but needs to be here anyway.
81 if self.player_logic.forced_good_item != "":
82 state.collect(self.create_item(self.player_logic.forced_good_item), True)
83
84 all_locations = self.multiworld.get_locations(self.player)
85 state.sweep_for_events(locations=all_locations)
86
87 unreachable_locations = [location for location in all_locations
88 if not state.can_reach_location(location.name, self.player)]
89
90 for location in unreachable_locations:
91 if location.name in self.player_logic.event_loc_to_item.keys():
92 continue
93
94 self.player_logic.real_locations.remove(location.name)
95 location.parent_region.locations.remove(location)
96
97 if len(self.player_logic.real_items) > len(self.player_logic.real_locations):
98 raise OptionError(f"{self.player_name}'s Lingo world does not have enough locations to fit the number"
99 f" of required items without shuffling the postgame. Either enable postgame"
100 f" shuffling, or choose different options.")
101
71 def create_items(self): 102 def create_items(self):
72 pool = [self.create_item(name) for name in self.player_logic.real_items] 103 pool = [self.create_item(name) for name in self.player_logic.real_items]
73 104
diff --git a/data/LL1.yaml b/data/LL1.yaml index e12ca02..3035446 100644 --- a/data/LL1.yaml +++ b/data/LL1.yaml
@@ -879,6 +879,8 @@
879 panel: DRAWL + RUNS 879 panel: DRAWL + RUNS
880 - room: Owl Hallway 880 - room: Owl Hallway
881 panel: READS + RUST 881 panel: READS + RUST
882 - room: Ending Area
883 panel: THE END
882 paintings: 884 paintings:
883 - id: eye_painting 885 - id: eye_painting
884 disable: True 886 disable: True
@@ -2322,7 +2324,7 @@
2322 orientation: east 2324 orientation: east
2323 - id: hi_solved_painting 2325 - id: hi_solved_painting
2324 orientation: west 2326 orientation: west
2325 Orange Tower Seventh Floor: 2327 Ending Area:
2326 entrances: 2328 entrances:
2327 Orange Tower Sixth Floor: 2329 Orange Tower Sixth Floor:
2328 room: Orange Tower 2330 room: Orange Tower
@@ -2334,6 +2336,18 @@
2334 check: True 2336 check: True
2335 tag: forbid 2337 tag: forbid
2336 non_counting: True 2338 non_counting: True
2339 location_name: Orange Tower Seventh Floor - THE END
2340 doors:
2341 End:
2342 event: True
2343 panels:
2344 - THE END
2345 Orange Tower Seventh Floor:
2346 entrances:
2347 Ending Area:
2348 room: Ending Area
2349 door: End
2350 panels:
2337 THE MASTER: 2351 THE MASTER:
2338 # We will set up special rules for this in code. 2352 # We will set up special rules for this in code.
2339 id: Countdown Panels/Panel_master_master 2353 id: Countdown Panels/Panel_master_master
diff --git a/data/generated.dat b/data/generated.dat index 3ed6cb2..4a751b2 100644 --- a/data/generated.dat +++ b/data/generated.dat
Binary files differ
diff --git a/data/ids.yaml b/data/ids.yaml index 1fa06d2..c49a8df 100644 --- a/data/ids.yaml +++ b/data/ids.yaml
@@ -272,8 +272,9 @@ panels:
272 PAINTING (4): 445081 272 PAINTING (4): 445081
273 PAINTING (5): 445082 273 PAINTING (5): 445082
274 ROOM: 445083 274 ROOM: 445083
275 Orange Tower Seventh Floor: 275 Ending Area:
276 THE END: 444620 276 THE END: 444620
277 Orange Tower Seventh Floor:
277 THE MASTER: 444621 278 THE MASTER: 444621
278 MASTERY: 444622 279 MASTERY: 444622
279 Behind A Smile: 280 Behind A Smile:
diff --git a/options.py b/options.py index 333b3e1..5a076e5 100644 --- a/options.py +++ b/options.py
@@ -194,6 +194,11 @@ class EarlyColorHallways(Toggle):
194 display_name = "Early Color Hallways" 194 display_name = "Early Color Hallways"
195 195
196 196
197class ShufflePostgame(Toggle):
198 """When off, locations that could not be reached without also reaching your victory condition are removed."""
199 display_name = "Shuffle Postgame"
200
201
197class TrapPercentage(Range): 202class TrapPercentage(Range):
198 """Replaces junk items with traps, at the specified rate.""" 203 """Replaces junk items with traps, at the specified rate."""
199 display_name = "Trap Percentage" 204 display_name = "Trap Percentage"
@@ -263,6 +268,7 @@ class LingoOptions(PerGameCommonOptions):
263 mastery_achievements: MasteryAchievements 268 mastery_achievements: MasteryAchievements
264 level_2_requirement: Level2Requirement 269 level_2_requirement: Level2Requirement
265 early_color_hallways: EarlyColorHallways 270 early_color_hallways: EarlyColorHallways
271 shuffle_postgame: ShufflePostgame
266 trap_percentage: TrapPercentage 272 trap_percentage: TrapPercentage
267 trap_weights: TrapWeights 273 trap_weights: TrapWeights
268 puzzle_skip_percentage: PuzzleSkipPercentage 274 puzzle_skip_percentage: PuzzleSkipPercentage
diff --git a/player_logic.py b/player_logic.py index 1621620..35080ac 100644 --- a/player_logic.py +++ b/player_logic.py
@@ -19,22 +19,25 @@ class AccessRequirements:
19 doors: Set[RoomAndDoor] 19 doors: Set[RoomAndDoor]
20 colors: Set[str] 20 colors: Set[str]
21 the_master: bool 21 the_master: bool
22 postgame: bool
22 23
23 def __init__(self): 24 def __init__(self):
24 self.rooms = set() 25 self.rooms = set()
25 self.doors = set() 26 self.doors = set()
26 self.colors = set() 27 self.colors = set()
27 self.the_master = False 28 self.the_master = False
29 self.postgame = False
28 30
29 def merge(self, other: "AccessRequirements"): 31 def merge(self, other: "AccessRequirements"):
30 self.rooms |= other.rooms 32 self.rooms |= other.rooms
31 self.doors |= other.doors 33 self.doors |= other.doors
32 self.colors |= other.colors 34 self.colors |= other.colors
33 self.the_master |= other.the_master 35 self.the_master |= other.the_master
36 self.postgame |= other.postgame
34 37
35 def __str__(self): 38 def __str__(self):
36 return f"AccessRequirements(rooms={self.rooms}, doors={self.doors}, colors={self.colors})," \ 39 return f"AccessRequirements(rooms={self.rooms}, doors={self.doors}, colors={self.colors}," \
37 f" the_master={self.the_master}" 40 f" the_master={self.the_master}, postgame={self.postgame})"
38 41
39 42
40class PlayerLocation(NamedTuple): 43class PlayerLocation(NamedTuple):
@@ -190,16 +193,6 @@ class LingoPlayerLogic:
190 if color_shuffle: 193 if color_shuffle:
191 self.real_items += [name for name, item in ALL_ITEM_TABLE.items() if item.type == ItemType.COLOR] 194 self.real_items += [name for name, item in ALL_ITEM_TABLE.items() if item.type == ItemType.COLOR]
192 195
193 # Create events for each achievement panel, so that we can determine when THE MASTER is accessible.
194 for room_name, room_data in PANELS_BY_ROOM.items():
195 for panel_name, panel_data in room_data.items():
196 if panel_data.achievement:
197 access_req = AccessRequirements()
198 access_req.merge(self.calculate_panel_requirements(room_name, panel_name, world))
199 access_req.rooms.add(room_name)
200
201 self.mastery_reqs.append(access_req)
202
203 # Handle the victory condition. Victory conditions other than the chosen one become regular checks, so we need 196 # Handle the victory condition. Victory conditions other than the chosen one become regular checks, so we need
204 # to prevent the actual victory condition from becoming a check. 197 # to prevent the actual victory condition from becoming a check.
205 self.mastery_location = "Orange Tower Seventh Floor - THE MASTER" 198 self.mastery_location = "Orange Tower Seventh Floor - THE MASTER"
@@ -207,7 +200,7 @@ class LingoPlayerLogic:
207 200
208 if victory_condition == VictoryCondition.option_the_end: 201 if victory_condition == VictoryCondition.option_the_end:
209 self.victory_condition = "Orange Tower Seventh Floor - THE END" 202 self.victory_condition = "Orange Tower Seventh Floor - THE END"
210 self.add_location("Orange Tower Seventh Floor", "The End (Solved)", None, [], world) 203 self.add_location("Ending Area", "The End (Solved)", None, [], world)
211 self.event_loc_to_item["The End (Solved)"] = "Victory" 204 self.event_loc_to_item["The End (Solved)"] = "Victory"
212 elif victory_condition == VictoryCondition.option_the_master: 205 elif victory_condition == VictoryCondition.option_the_master:
213 self.victory_condition = "Orange Tower Seventh Floor - THE MASTER" 206 self.victory_condition = "Orange Tower Seventh Floor - THE MASTER"
@@ -231,6 +224,16 @@ class LingoPlayerLogic:
231 [RoomAndPanel("Pilgrim Antechamber", "PILGRIM")], world) 224 [RoomAndPanel("Pilgrim Antechamber", "PILGRIM")], world)
232 self.event_loc_to_item["PILGRIM (Solved)"] = "Victory" 225 self.event_loc_to_item["PILGRIM (Solved)"] = "Victory"
233 226
227 # Create events for each achievement panel, so that we can determine when THE MASTER is accessible.
228 for room_name, room_data in PANELS_BY_ROOM.items():
229 for panel_name, panel_data in room_data.items():
230 if panel_data.achievement:
231 access_req = AccessRequirements()
232 access_req.merge(self.calculate_panel_requirements(room_name, panel_name, world))
233 access_req.rooms.add(room_name)
234
235 self.mastery_reqs.append(access_req)
236
234 # Create groups of counting panel access requirements for the LEVEL 2 check. 237 # Create groups of counting panel access requirements for the LEVEL 2 check.
235 self.create_panel_hunt_events(world) 238 self.create_panel_hunt_events(world)
236 239
@@ -470,6 +473,11 @@ class LingoPlayerLogic:
470 if panel == "THE MASTER": 473 if panel == "THE MASTER":
471 access_reqs.the_master = True 474 access_reqs.the_master = True
472 475
476 # Evil python magic (so sayeth NewSoupVi): this checks victory_condition against the panel's location name
477 # override if it exists, or the auto-generated location name if it's None.
478 if self.victory_condition == (panel_object.location_name or f"{room} - {panel}"):
479 access_reqs.postgame = True
480
473 self.panel_reqs[room][panel] = access_reqs 481 self.panel_reqs[room][panel] = access_reqs
474 482
475 return self.panel_reqs[room][panel] 483 return self.panel_reqs[room][panel]
diff --git a/rules.py b/rules.py index d91c53f..ed84c56 100644 --- a/rules.py +++ b/rules.py
@@ -62,6 +62,9 @@ def _lingo_can_satisfy_requirements(state: CollectionState, access: AccessRequir
62 if access.the_master and not lingo_can_use_mastery_location(state, world): 62 if access.the_master and not lingo_can_use_mastery_location(state, world):
63 return False 63 return False
64 64
65 if access.postgame and state.has("Prevent Victory", world.player):
66 return False
67
65 return True 68 return True
66 69
67 70
diff --git a/test/TestMastery.py b/test/TestMastery.py index 3ebe40a..c9c79a9 100644 --- a/test/TestMastery.py +++ b/test/TestMastery.py
@@ -5,7 +5,8 @@ class TestMasteryWhenVictoryIsTheEnd(LingoTestBase):
5 options = { 5 options = {
6 "mastery_achievements": "22", 6 "mastery_achievements": "22",
7 "victory_condition": "the_end", 7 "victory_condition": "the_end",
8 "shuffle_colors": "true" 8 "shuffle_colors": "true",
9 "shuffle_postgame": "true",
9 } 10 }
10 11
11 def test_requirement(self): 12 def test_requirement(self):
@@ -43,7 +44,8 @@ class TestMasteryBlocksDependents(LingoTestBase):
43 options = { 44 options = {
44 "mastery_achievements": "24", 45 "mastery_achievements": "24",
45 "shuffle_colors": "true", 46 "shuffle_colors": "true",
46 "location_checks": "insanity" 47 "location_checks": "insanity",
48 "victory_condition": "level_2",
47 } 49 }
48 50
49 def test_requirement(self): 51 def test_requirement(self):
diff --git a/test/TestPostgame.py b/test/TestPostgame.py new file mode 100644 index 0000000..d2e2232 --- /dev/null +++ b/test/TestPostgame.py
@@ -0,0 +1,62 @@
1from . import LingoTestBase
2
3
4class TestPostgameVanillaTheEnd(LingoTestBase):
5 options = {
6 "shuffle_doors": "none",
7 "victory_condition": "the_end",
8 "shuffle_postgame": "false",
9 }
10
11 def test_requirement(self):
12 location_names = [location.name for location in self.multiworld.get_locations(self.player)]
13
14 self.assertTrue("The End (Solved)" in location_names)
15 self.assertTrue("Champion's Rest - YOU" in location_names)
16 self.assertFalse("Orange Tower Seventh Floor - THE MASTER" in location_names)
17 self.assertFalse("The Red - Achievement" in location_names)
18
19
20class TestPostgameComplexDoorsTheEnd(LingoTestBase):
21 options = {
22 "shuffle_doors": "complex",
23 "victory_condition": "the_end",
24 "shuffle_postgame": "false",
25 }
26
27 def test_requirement(self):
28 location_names = [location.name for location in self.multiworld.get_locations(self.player)]
29
30 self.assertTrue("The End (Solved)" in location_names)
31 self.assertFalse("Orange Tower Seventh Floor - THE MASTER" in location_names)
32 self.assertTrue("The Red - Achievement" in location_names)
33
34
35class TestPostgameLateColorHunt(LingoTestBase):
36 options = {
37 "shuffle_doors": "none",
38 "victory_condition": "the_end",
39 "sunwarp_access": "disabled",
40 "shuffle_postgame": "false",
41 }
42
43 def test_requirement(self):
44 location_names = [location.name for location in self.multiworld.get_locations(self.player)]
45
46 self.assertFalse("Champion's Rest - YOU" in location_names)
47
48
49class TestPostgameVanillaTheMaster(LingoTestBase):
50 options = {
51 "shuffle_doors": "none",
52 "victory_condition": "the_master",
53 "shuffle_postgame": "false",
54 }
55
56 def test_requirement(self):
57 location_names = [location.name for location in self.multiworld.get_locations(self.player)]
58
59 self.assertTrue("Orange Tower Seventh Floor - THE END" in location_names)
60 self.assertTrue("Orange Tower Seventh Floor - Mastery Achievements" in location_names)
61 self.assertTrue("The Red - Achievement" in location_names)
62 self.assertFalse("Mastery Panels" in location_names)