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
This commit is contained in:
Star Rauchenberger 2024-07-24 08:34:51 -04:00 committed by GitHub
parent 878d5141ce
commit e714d2e129
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 145 additions and 18 deletions

View File

@ -3,7 +3,7 @@ Archipelago init file for Lingo
""" """
from logging import warning from logging import warning
from BaseClasses import Item, ItemClassification, Tutorial from BaseClasses import CollectionState, Item, ItemClassification, Tutorial
from Options import OptionError from Options import OptionError
from worlds.AutoWorld import WebWorld, World from worlds.AutoWorld import WebWorld, World
from .datatypes import Room, RoomEntrance from .datatypes import Room, RoomEntrance
@ -68,6 +68,37 @@ class LingoWorld(World):
def create_regions(self): def create_regions(self):
create_regions(self) create_regions(self)
if not self.options.shuffle_postgame:
state = CollectionState(self.multiworld)
state.collect(LingoItem("Prevent Victory", ItemClassification.progression, None, self.player), True)
# Note: relies on the assumption that real_items is a definitive list of real progression items in this
# world, and is not modified after being created.
for item in self.player_logic.real_items:
state.collect(self.create_item(item), True)
# Exception to the above: a forced good item is not considered a "real item", but needs to be here anyway.
if self.player_logic.forced_good_item != "":
state.collect(self.create_item(self.player_logic.forced_good_item), True)
all_locations = self.multiworld.get_locations(self.player)
state.sweep_for_events(locations=all_locations)
unreachable_locations = [location for location in all_locations
if not state.can_reach_location(location.name, self.player)]
for location in unreachable_locations:
if location.name in self.player_logic.event_loc_to_item.keys():
continue
self.player_logic.real_locations.remove(location.name)
location.parent_region.locations.remove(location)
if len(self.player_logic.real_items) > len(self.player_logic.real_locations):
raise OptionError(f"{self.player_name}'s Lingo world does not have enough locations to fit the number"
f" of required items without shuffling the postgame. Either enable postgame"
f" shuffling, or choose different options.")
def create_items(self): def create_items(self):
pool = [self.create_item(name) for name in self.player_logic.real_items] pool = [self.create_item(name) for name in self.player_logic.real_items]

View File

@ -879,6 +879,8 @@
panel: DRAWL + RUNS panel: DRAWL + RUNS
- room: Owl Hallway - room: Owl Hallway
panel: READS + RUST panel: READS + RUST
- room: Ending Area
panel: THE END
paintings: paintings:
- id: eye_painting - id: eye_painting
disable: True disable: True
@ -2322,7 +2324,7 @@
orientation: east orientation: east
- id: hi_solved_painting - id: hi_solved_painting
orientation: west orientation: west
Orange Tower Seventh Floor: Ending Area:
entrances: entrances:
Orange Tower Sixth Floor: Orange Tower Sixth Floor:
room: Orange Tower room: Orange Tower
@ -2334,6 +2336,18 @@
check: True check: True
tag: forbid tag: forbid
non_counting: True non_counting: True
location_name: Orange Tower Seventh Floor - THE END
doors:
End:
event: True
panels:
- THE END
Orange Tower Seventh Floor:
entrances:
Ending Area:
room: Ending Area
door: End
panels:
THE MASTER: THE MASTER:
# We will set up special rules for this in code. # We will set up special rules for this in code.
id: Countdown Panels/Panel_master_master id: Countdown Panels/Panel_master_master

Binary file not shown.

View File

@ -272,8 +272,9 @@ panels:
PAINTING (4): 445081 PAINTING (4): 445081
PAINTING (5): 445082 PAINTING (5): 445082
ROOM: 445083 ROOM: 445083
Orange Tower Seventh Floor: Ending Area:
THE END: 444620 THE END: 444620
Orange Tower Seventh Floor:
THE MASTER: 444621 THE MASTER: 444621
MASTERY: 444622 MASTERY: 444622
Behind A Smile: Behind A Smile:

View File

@ -194,6 +194,11 @@ class EarlyColorHallways(Toggle):
display_name = "Early Color Hallways" display_name = "Early Color Hallways"
class ShufflePostgame(Toggle):
"""When off, locations that could not be reached without also reaching your victory condition are removed."""
display_name = "Shuffle Postgame"
class TrapPercentage(Range): class TrapPercentage(Range):
"""Replaces junk items with traps, at the specified rate.""" """Replaces junk items with traps, at the specified rate."""
display_name = "Trap Percentage" display_name = "Trap Percentage"
@ -263,6 +268,7 @@ class LingoOptions(PerGameCommonOptions):
mastery_achievements: MasteryAchievements mastery_achievements: MasteryAchievements
level_2_requirement: Level2Requirement level_2_requirement: Level2Requirement
early_color_hallways: EarlyColorHallways early_color_hallways: EarlyColorHallways
shuffle_postgame: ShufflePostgame
trap_percentage: TrapPercentage trap_percentage: TrapPercentage
trap_weights: TrapWeights trap_weights: TrapWeights
puzzle_skip_percentage: PuzzleSkipPercentage puzzle_skip_percentage: PuzzleSkipPercentage

View File

@ -19,22 +19,25 @@ class AccessRequirements:
doors: Set[RoomAndDoor] doors: Set[RoomAndDoor]
colors: Set[str] colors: Set[str]
the_master: bool the_master: bool
postgame: bool
def __init__(self): def __init__(self):
self.rooms = set() self.rooms = set()
self.doors = set() self.doors = set()
self.colors = set() self.colors = set()
self.the_master = False self.the_master = False
self.postgame = False
def merge(self, other: "AccessRequirements"): def merge(self, other: "AccessRequirements"):
self.rooms |= other.rooms self.rooms |= other.rooms
self.doors |= other.doors self.doors |= other.doors
self.colors |= other.colors self.colors |= other.colors
self.the_master |= other.the_master self.the_master |= other.the_master
self.postgame |= other.postgame
def __str__(self): def __str__(self):
return f"AccessRequirements(rooms={self.rooms}, doors={self.doors}, colors={self.colors})," \ return f"AccessRequirements(rooms={self.rooms}, doors={self.doors}, colors={self.colors}," \
f" the_master={self.the_master}" f" the_master={self.the_master}, postgame={self.postgame})"
class PlayerLocation(NamedTuple): class PlayerLocation(NamedTuple):
@ -190,16 +193,6 @@ class LingoPlayerLogic:
if color_shuffle: if color_shuffle:
self.real_items += [name for name, item in ALL_ITEM_TABLE.items() if item.type == ItemType.COLOR] self.real_items += [name for name, item in ALL_ITEM_TABLE.items() if item.type == ItemType.COLOR]
# Create events for each achievement panel, so that we can determine when THE MASTER is accessible.
for room_name, room_data in PANELS_BY_ROOM.items():
for panel_name, panel_data in room_data.items():
if panel_data.achievement:
access_req = AccessRequirements()
access_req.merge(self.calculate_panel_requirements(room_name, panel_name, world))
access_req.rooms.add(room_name)
self.mastery_reqs.append(access_req)
# Handle the victory condition. Victory conditions other than the chosen one become regular checks, so we need # Handle the victory condition. Victory conditions other than the chosen one become regular checks, so we need
# to prevent the actual victory condition from becoming a check. # to prevent the actual victory condition from becoming a check.
self.mastery_location = "Orange Tower Seventh Floor - THE MASTER" self.mastery_location = "Orange Tower Seventh Floor - THE MASTER"
@ -207,7 +200,7 @@ class LingoPlayerLogic:
if victory_condition == VictoryCondition.option_the_end: if victory_condition == VictoryCondition.option_the_end:
self.victory_condition = "Orange Tower Seventh Floor - THE END" self.victory_condition = "Orange Tower Seventh Floor - THE END"
self.add_location("Orange Tower Seventh Floor", "The End (Solved)", None, [], world) self.add_location("Ending Area", "The End (Solved)", None, [], world)
self.event_loc_to_item["The End (Solved)"] = "Victory" self.event_loc_to_item["The End (Solved)"] = "Victory"
elif victory_condition == VictoryCondition.option_the_master: elif victory_condition == VictoryCondition.option_the_master:
self.victory_condition = "Orange Tower Seventh Floor - THE MASTER" self.victory_condition = "Orange Tower Seventh Floor - THE MASTER"
@ -231,6 +224,16 @@ class LingoPlayerLogic:
[RoomAndPanel("Pilgrim Antechamber", "PILGRIM")], world) [RoomAndPanel("Pilgrim Antechamber", "PILGRIM")], world)
self.event_loc_to_item["PILGRIM (Solved)"] = "Victory" self.event_loc_to_item["PILGRIM (Solved)"] = "Victory"
# Create events for each achievement panel, so that we can determine when THE MASTER is accessible.
for room_name, room_data in PANELS_BY_ROOM.items():
for panel_name, panel_data in room_data.items():
if panel_data.achievement:
access_req = AccessRequirements()
access_req.merge(self.calculate_panel_requirements(room_name, panel_name, world))
access_req.rooms.add(room_name)
self.mastery_reqs.append(access_req)
# Create groups of counting panel access requirements for the LEVEL 2 check. # Create groups of counting panel access requirements for the LEVEL 2 check.
self.create_panel_hunt_events(world) self.create_panel_hunt_events(world)
@ -470,6 +473,11 @@ class LingoPlayerLogic:
if panel == "THE MASTER": if panel == "THE MASTER":
access_reqs.the_master = True access_reqs.the_master = True
# Evil python magic (so sayeth NewSoupVi): this checks victory_condition against the panel's location name
# override if it exists, or the auto-generated location name if it's None.
if self.victory_condition == (panel_object.location_name or f"{room} - {panel}"):
access_reqs.postgame = True
self.panel_reqs[room][panel] = access_reqs self.panel_reqs[room][panel] = access_reqs
return self.panel_reqs[room][panel] return self.panel_reqs[room][panel]

View File

@ -62,6 +62,9 @@ def _lingo_can_satisfy_requirements(state: CollectionState, access: AccessRequir
if access.the_master and not lingo_can_use_mastery_location(state, world): if access.the_master and not lingo_can_use_mastery_location(state, world):
return False return False
if access.postgame and state.has("Prevent Victory", world.player):
return False
return True return True

View File

@ -5,7 +5,8 @@ class TestMasteryWhenVictoryIsTheEnd(LingoTestBase):
options = { options = {
"mastery_achievements": "22", "mastery_achievements": "22",
"victory_condition": "the_end", "victory_condition": "the_end",
"shuffle_colors": "true" "shuffle_colors": "true",
"shuffle_postgame": "true",
} }
def test_requirement(self): def test_requirement(self):
@ -43,7 +44,8 @@ class TestMasteryBlocksDependents(LingoTestBase):
options = { options = {
"mastery_achievements": "24", "mastery_achievements": "24",
"shuffle_colors": "true", "shuffle_colors": "true",
"location_checks": "insanity" "location_checks": "insanity",
"victory_condition": "level_2",
} }
def test_requirement(self): def test_requirement(self):

View File

@ -0,0 +1,62 @@
from . import LingoTestBase
class TestPostgameVanillaTheEnd(LingoTestBase):
options = {
"shuffle_doors": "none",
"victory_condition": "the_end",
"shuffle_postgame": "false",
}
def test_requirement(self):
location_names = [location.name for location in self.multiworld.get_locations(self.player)]
self.assertTrue("The End (Solved)" in location_names)
self.assertTrue("Champion's Rest - YOU" in location_names)
self.assertFalse("Orange Tower Seventh Floor - THE MASTER" in location_names)
self.assertFalse("The Red - Achievement" in location_names)
class TestPostgameComplexDoorsTheEnd(LingoTestBase):
options = {
"shuffle_doors": "complex",
"victory_condition": "the_end",
"shuffle_postgame": "false",
}
def test_requirement(self):
location_names = [location.name for location in self.multiworld.get_locations(self.player)]
self.assertTrue("The End (Solved)" in location_names)
self.assertFalse("Orange Tower Seventh Floor - THE MASTER" in location_names)
self.assertTrue("The Red - Achievement" in location_names)
class TestPostgameLateColorHunt(LingoTestBase):
options = {
"shuffle_doors": "none",
"victory_condition": "the_end",
"sunwarp_access": "disabled",
"shuffle_postgame": "false",
}
def test_requirement(self):
location_names = [location.name for location in self.multiworld.get_locations(self.player)]
self.assertFalse("Champion's Rest - YOU" in location_names)
class TestPostgameVanillaTheMaster(LingoTestBase):
options = {
"shuffle_doors": "none",
"victory_condition": "the_master",
"shuffle_postgame": "false",
}
def test_requirement(self):
location_names = [location.name for location in self.multiworld.get_locations(self.player)]
self.assertTrue("Orange Tower Seventh Floor - THE END" in location_names)
self.assertTrue("Orange Tower Seventh Floor - Mastery Achievements" in location_names)
self.assertTrue("The Red - Achievement" in location_names)
self.assertFalse("Mastery Panels" in location_names)