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 BaseClasses import Item, ItemClassification, Tutorial
from BaseClasses import CollectionState, Item, ItemClassification, Tutorial
from Options import OptionError
from worlds.AutoWorld import WebWorld, World
from .datatypes import Room, RoomEntrance
@ -68,6 +68,37 @@ class LingoWorld(World):
def 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):
pool = [self.create_item(name) for name in self.player_logic.real_items]

View File

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

Binary file not shown.

View File

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

View File

@ -194,6 +194,11 @@ class EarlyColorHallways(Toggle):
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):
"""Replaces junk items with traps, at the specified rate."""
display_name = "Trap Percentage"
@ -263,6 +268,7 @@ class LingoOptions(PerGameCommonOptions):
mastery_achievements: MasteryAchievements
level_2_requirement: Level2Requirement
early_color_hallways: EarlyColorHallways
shuffle_postgame: ShufflePostgame
trap_percentage: TrapPercentage
trap_weights: TrapWeights
puzzle_skip_percentage: PuzzleSkipPercentage

View File

@ -19,22 +19,25 @@ class AccessRequirements:
doors: Set[RoomAndDoor]
colors: Set[str]
the_master: bool
postgame: bool
def __init__(self):
self.rooms = set()
self.doors = set()
self.colors = set()
self.the_master = False
self.postgame = False
def merge(self, other: "AccessRequirements"):
self.rooms |= other.rooms
self.doors |= other.doors
self.colors |= other.colors
self.the_master |= other.the_master
self.postgame |= other.postgame
def __str__(self):
return f"AccessRequirements(rooms={self.rooms}, doors={self.doors}, colors={self.colors})," \
f" the_master={self.the_master}"
return f"AccessRequirements(rooms={self.rooms}, doors={self.doors}, colors={self.colors}," \
f" the_master={self.the_master}, postgame={self.postgame})"
class PlayerLocation(NamedTuple):
@ -190,16 +193,6 @@ class LingoPlayerLogic:
if color_shuffle:
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
# to prevent the actual victory condition from becoming a check.
self.mastery_location = "Orange Tower Seventh Floor - THE MASTER"
@ -207,7 +200,7 @@ class LingoPlayerLogic:
if victory_condition == VictoryCondition.option_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"
elif victory_condition == VictoryCondition.option_the_master:
self.victory_condition = "Orange Tower Seventh Floor - THE MASTER"
@ -231,6 +224,16 @@ class LingoPlayerLogic:
[RoomAndPanel("Pilgrim Antechamber", "PILGRIM")], world)
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.
self.create_panel_hunt_events(world)
@ -470,6 +473,11 @@ class LingoPlayerLogic:
if panel == "THE MASTER":
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
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):
return False
if access.postgame and state.has("Prevent Victory", world.player):
return False
return True

View File

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