Lingo: The Pilgrim Update (#2884)

* An option was added to enable or disable the pilgrimage, and it defaults to disabled. When disabled, the client will prevent you from performing a pilgrimage (i.e. the yellow border will not appear when you enter the 1 sunwarp). The sun painting is added to the item pool when pilgrimage is disabled, as otherwise there is no way into the Pilgrim Antechamber. Inversely, the sun painting is no longer in the item pool when pilgrimage is enabled (even if door shuffle is on), requiring you to perform a pilgrimage to get to that room.
* The canonical pilgrimage has been deprecated. Instead, there is logic for determining whether a pilgrimage is possible.
* Two options were added that allow the player to decide whether paintings and/or Crossroads - Roof Access are permitted during the pilgrimage. Both default to disabled. These options apply both to logical expectations in the generator, and are also enforced by the game client.
* An option was added to control how sunwarps are accessed. The default is for them to always be accessible, like in the base game. It is also possible to disable them entirely (which is not possible when pilgrimage is enabled), or lock them behind items similar to door shuffle. It can either be one item that unlocks all sunwarps at the same time, six progressive items that unlock the sunwarps from 1 to 6, or six individual items that unlock the sunwarps in any order. This option is independent from door shuffle.
* An option was added that shuffles sunwarps. This acts similarly to painting shuffle. The 12 sunwarps are re-ordered and re-paired. Sunwarps that were previously entrances or exits do not need to stay entrances or exits. Performing a pilgrimage requires proceeding through the sunwarps in the new order, rather than the original one.
* Pilgrimage was added as a win condition. It requires you to solve the blue PILGRIM panel in the Pilgrim Antechamber.
This commit is contained in:
Star Rauchenberger 2024-04-18 11:45:33 -05:00 committed by GitHub
parent 6b50c91ce2
commit 740b76ebd5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 1151 additions and 296 deletions

View File

@ -132,7 +132,8 @@ class LingoWorld(World):
def fill_slot_data(self): def fill_slot_data(self):
slot_options = [ slot_options = [
"death_link", "victory_condition", "shuffle_colors", "shuffle_doors", "shuffle_paintings", "shuffle_panels", "death_link", "victory_condition", "shuffle_colors", "shuffle_doors", "shuffle_paintings", "shuffle_panels",
"mastery_achievements", "level_2_requirement", "location_checks", "early_color_hallways" "enable_pilgrimage", "sunwarp_access", "mastery_achievements", "level_2_requirement", "location_checks",
"early_color_hallways", "pilgrimage_allows_roof_access", "pilgrimage_allows_paintings", "shuffle_sunwarps"
] ]
slot_data = { slot_data = {
@ -143,6 +144,9 @@ class LingoWorld(World):
if self.options.shuffle_paintings: if self.options.shuffle_paintings:
slot_data["painting_entrance_to_exit"] = self.player_logic.painting_mapping slot_data["painting_entrance_to_exit"] = self.player_logic.painting_mapping
if self.options.shuffle_sunwarps:
slot_data["sunwarp_permutation"] = self.player_logic.sunwarp_mapping
return slot_data return slot_data
def get_filler_item_name(self) -> str: def get_filler_item_name(self) -> str:

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@ -140,13 +140,9 @@ panels:
PURPLE: 444502 PURPLE: 444502
FIVE (1): 444503 FIVE (1): 444503
FIVE (2): 444504 FIVE (2): 444504
OUT: 444505
HIDE: 444506 HIDE: 444506
DAZE: 444507 DAZE: 444507
WALL: 444508 Compass Room:
KEEP: 444509
BAILEY: 444510
TOWER: 444511
NORTH: 444512 NORTH: 444512
DIAMONDS: 444513 DIAMONDS: 444513
FIRE: 444514 FIRE: 444514
@ -689,6 +685,12 @@ panels:
Arrow Garden: Arrow Garden:
MASTERY: 444948 MASTERY: 444948
SHARP: 444949 SHARP: 444949
Hallway Room (1):
OUT: 444505
WALL: 444508
KEEP: 444509
BAILEY: 444510
TOWER: 444511
Hallway Room (2): Hallway Room (2):
WISE: 444950 WISE: 444950
CLOCK: 444951 CLOCK: 444951
@ -995,6 +997,19 @@ doors:
Traveled Entrance: Traveled Entrance:
item: 444433 item: 444433
location: 444438 location: 444438
Sunwarps:
1 Sunwarp:
item: 444581
2 Sunwarp:
item: 444588
3 Sunwarp:
item: 444586
4 Sunwarp:
item: 444585
5 Sunwarp:
item: 444587
6 Sunwarp:
item: 444584
Pilgrim Antechamber: Pilgrim Antechamber:
Sun Painting: Sun Painting:
item: 444436 item: 444436
@ -1067,9 +1082,7 @@ doors:
location: 444501 location: 444501
Purple Barrier: Purple Barrier:
item: 444457 item: 444457
Hallway Door: Compass Room:
item: 444459
location: 445214
Lookout Entrance: Lookout Entrance:
item: 444579 item: 444579
location: 445271 location: 445271
@ -1342,6 +1355,10 @@ doors:
Exit: Exit:
item: 444552 item: 444552
location: 444947 location: 444947
Hallway Room (1):
Exit:
item: 444459
location: 445214
Hallway Room (2): Hallway Room (2):
Exit: Exit:
item: 444553 item: 444553
@ -1452,9 +1469,11 @@ door_groups:
Colorful Doors: 444498 Colorful Doors: 444498
Directional Gallery Doors: 444531 Directional Gallery Doors: 444531
Artistic Doors: 444545 Artistic Doors: 444545
Sunwarps: 444582
progression: progression:
Progressive Hallway Room: 444461 Progressive Hallway Room: 444461
Progressive Fearless: 444470 Progressive Fearless: 444470
Progressive Orange Tower: 444482 Progressive Orange Tower: 444482
Progressive Art Gallery: 444563 Progressive Art Gallery: 444563
Progressive Colorful: 444580 Progressive Colorful: 444580
Progressive Pilgrimage: 444583

View File

@ -1,3 +1,4 @@
from enum import Enum, Flag, auto
from typing import List, NamedTuple, Optional from typing import List, NamedTuple, Optional
@ -11,10 +12,18 @@ class RoomAndPanel(NamedTuple):
panel: str panel: str
class EntranceType(Flag):
NORMAL = auto()
PAINTING = auto()
SUNWARP = auto()
WARP = auto()
CROSSROADS_ROOF_ACCESS = auto()
class RoomEntrance(NamedTuple): class RoomEntrance(NamedTuple):
room: str # source room room: str # source room
door: Optional[RoomAndDoor] door: Optional[RoomAndDoor]
painting: bool type: EntranceType
class Room(NamedTuple): class Room(NamedTuple):
@ -22,6 +31,12 @@ class Room(NamedTuple):
entrances: List[RoomEntrance] entrances: List[RoomEntrance]
class DoorType(Enum):
NORMAL = 1
SUNWARP = 2
SUN_PAINTING = 3
class Door(NamedTuple): class Door(NamedTuple):
name: str name: str
item_name: str item_name: str
@ -34,7 +49,7 @@ class Door(NamedTuple):
event: bool event: bool
door_group: Optional[str] door_group: Optional[str]
include_reduce: bool include_reduce: bool
junk_item: bool type: DoorType
item_group: Optional[str] item_group: Optional[str]

View File

@ -1,8 +1,14 @@
from typing import Dict, List, NamedTuple, Optional, TYPE_CHECKING from enum import Enum
from typing import Dict, List, NamedTuple, Set
from BaseClasses import Item, ItemClassification from BaseClasses import Item, ItemClassification
from .static_logic import DOORS_BY_ROOM, PROGRESSION_BY_ROOM, PROGRESSIVE_ITEMS, get_door_group_item_id, \ from .static_logic import DOORS_BY_ROOM, PROGRESSIVE_ITEMS, get_door_group_item_id, get_door_item_id, \
get_door_item_id, get_progressive_item_id, get_special_item_id get_progressive_item_id, get_special_item_id
class ItemType(Enum):
NORMAL = 1
COLOR = 2
class ItemData(NamedTuple): class ItemData(NamedTuple):
@ -11,7 +17,7 @@ class ItemData(NamedTuple):
""" """
code: int code: int
classification: ItemClassification classification: ItemClassification
mode: Optional[str] type: ItemType
has_doors: bool has_doors: bool
painting_ids: List[str] painting_ids: List[str]
@ -34,36 +40,29 @@ def load_item_data():
for color in ["Black", "Red", "Blue", "Yellow", "Green", "Orange", "Gray", "Brown", "Purple"]: for color in ["Black", "Red", "Blue", "Yellow", "Green", "Orange", "Gray", "Brown", "Purple"]:
ALL_ITEM_TABLE[color] = ItemData(get_special_item_id(color), ItemClassification.progression, ALL_ITEM_TABLE[color] = ItemData(get_special_item_id(color), ItemClassification.progression,
"colors", [], []) ItemType.COLOR, False, [])
ITEMS_BY_GROUP.setdefault("Colors", []).append(color) ITEMS_BY_GROUP.setdefault("Colors", []).append(color)
door_groups: Dict[str, List[str]] = {} door_groups: Set[str] = set()
for room_name, doors in DOORS_BY_ROOM.items(): for room_name, doors in DOORS_BY_ROOM.items():
for door_name, door in doors.items(): for door_name, door in doors.items():
if door.skip_item is True or door.event is True: if door.skip_item is True or door.event is True:
continue continue
if door.door_group is None: if door.door_group is not None:
door_mode = "doors" door_groups.add(door.door_group)
else:
door_mode = "complex door"
door_groups.setdefault(door.door_group, [])
if room_name in PROGRESSION_BY_ROOM and door_name in PROGRESSION_BY_ROOM[room_name]:
door_mode = "special"
ALL_ITEM_TABLE[door.item_name] = \ ALL_ITEM_TABLE[door.item_name] = \
ItemData(get_door_item_id(room_name, door_name), ItemData(get_door_item_id(room_name, door_name), ItemClassification.progression, ItemType.NORMAL,
ItemClassification.filler if door.junk_item else ItemClassification.progression, door_mode,
door.has_doors, door.painting_ids) door.has_doors, door.painting_ids)
ITEMS_BY_GROUP.setdefault("Doors", []).append(door.item_name) ITEMS_BY_GROUP.setdefault("Doors", []).append(door.item_name)
if door.item_group is not None: if door.item_group is not None:
ITEMS_BY_GROUP.setdefault(door.item_group, []).append(door.item_name) ITEMS_BY_GROUP.setdefault(door.item_group, []).append(door.item_name)
for group, group_door_ids in door_groups.items(): for group in door_groups:
ALL_ITEM_TABLE[group] = ItemData(get_door_group_item_id(group), ALL_ITEM_TABLE[group] = ItemData(get_door_group_item_id(group),
ItemClassification.progression, "door group", True, []) ItemClassification.progression, ItemType.NORMAL, True, [])
ITEMS_BY_GROUP.setdefault("Doors", []).append(group) ITEMS_BY_GROUP.setdefault("Doors", []).append(group)
special_items: Dict[str, ItemClassification] = { special_items: Dict[str, ItemClassification] = {
@ -77,7 +76,7 @@ def load_item_data():
for item_name, classification in special_items.items(): for item_name, classification in special_items.items():
ALL_ITEM_TABLE[item_name] = ItemData(get_special_item_id(item_name), classification, ALL_ITEM_TABLE[item_name] = ItemData(get_special_item_id(item_name), classification,
"special", False, []) ItemType.NORMAL, False, [])
if classification == ItemClassification.filler: if classification == ItemClassification.filler:
ITEMS_BY_GROUP.setdefault("Junk", []).append(item_name) ITEMS_BY_GROUP.setdefault("Junk", []).append(item_name)
@ -86,7 +85,7 @@ def load_item_data():
for item_name in PROGRESSIVE_ITEMS: for item_name in PROGRESSIVE_ITEMS:
ALL_ITEM_TABLE[item_name] = ItemData(get_progressive_item_id(item_name), ALL_ITEM_TABLE[item_name] = ItemData(get_progressive_item_id(item_name),
ItemClassification.progression, "special", False, []) ItemClassification.progression, ItemType.NORMAL, False, [])
# Initialize the item data at module scope. # Initialize the item data at module scope.

View File

@ -56,7 +56,7 @@ def load_location_data():
for room_name, doors in DOORS_BY_ROOM.items(): for room_name, doors in DOORS_BY_ROOM.items():
for door_name, door in doors.items(): for door_name, door in doors.items():
if door.skip_location or door.event or door.panels is None: if door.skip_location or door.event or not door.panels:
continue continue
location_name = door.location_name location_name = door.location_name

View File

@ -61,15 +61,55 @@ class ShufflePaintings(Toggle):
display_name = "Shuffle Paintings" display_name = "Shuffle Paintings"
class EnablePilgrimage(Toggle):
"""If on, you are required to complete a pilgrimage in order to access the Pilgrim Antechamber.
If off, the pilgrimage will be deactivated, and the sun painting will be added to the pool, even if door shuffle is off."""
display_name = "Enable Pilgrimage"
class PilgrimageAllowsRoofAccess(DefaultOnToggle):
"""If on, you may use the Crossroads roof access during a pilgrimage (and you may be expected to do so).
Otherwise, pilgrimage will be deactivated when going up the stairs."""
display_name = "Allow Roof Access for Pilgrimage"
class PilgrimageAllowsPaintings(DefaultOnToggle):
"""If on, you may use paintings during a pilgrimage (and you may be expected to do so).
Otherwise, pilgrimage will be deactivated when going through a painting."""
display_name = "Allow Paintings for Pilgrimage"
class SunwarpAccess(Choice):
"""Determines how access to sunwarps works.
On "normal", all sunwarps are enabled from the start.
On "disabled", all sunwarps are disabled. Pilgrimage must be disabled when this is used.
On "unlock", sunwarps start off disabled, and all six activate once you receive an item.
On "individual", sunwarps start off disabled, and each has a corresponding item that unlocks it.
On "progressive", sunwarps start off disabled, and they unlock in order using a progressive item."""
display_name = "Sunwarp Access"
option_normal = 0
option_disabled = 1
option_unlock = 2
option_individual = 3
option_progressive = 4
class ShuffleSunwarps(Toggle):
"""If on, the pairing and ordering of the sunwarps in the game will be randomized."""
display_name = "Shuffle Sunwarps"
class VictoryCondition(Choice): class VictoryCondition(Choice):
"""Change the victory condition. """Change the victory condition.
On "the_end", the goal is to solve THE END at the top of the tower. On "the_end", the goal is to solve THE END at the top of the tower.
On "the_master", the goal is to solve THE MASTER at the top of the tower, after getting the number of achievements specified in the Mastery Achievements option. On "the_master", the goal is to solve THE MASTER at the top of the tower, after getting the number of achievements specified in the Mastery Achievements option.
On "level_2", the goal is to solve LEVEL 2 in the second room, after solving the number of panels specified in the Level 2 Requirement option.""" On "level_2", the goal is to solve LEVEL 2 in the second room, after solving the number of panels specified in the Level 2 Requirement option.
On "pilgrimage", the goal is to solve PILGRIM in the Pilgrim Antechamber, typically after performing a Pilgrimage."""
display_name = "Victory Condition" display_name = "Victory Condition"
option_the_end = 0 option_the_end = 0
option_the_master = 1 option_the_master = 1
option_level_2 = 2 option_level_2 = 2
option_pilgrimage = 3
class MasteryAchievements(Range): class MasteryAchievements(Range):
@ -140,6 +180,11 @@ class LingoOptions(PerGameCommonOptions):
shuffle_colors: ShuffleColors shuffle_colors: ShuffleColors
shuffle_panels: ShufflePanels shuffle_panels: ShufflePanels
shuffle_paintings: ShufflePaintings shuffle_paintings: ShufflePaintings
enable_pilgrimage: EnablePilgrimage
pilgrimage_allows_roof_access: PilgrimageAllowsRoofAccess
pilgrimage_allows_paintings: PilgrimageAllowsPaintings
sunwarp_access: SunwarpAccess
shuffle_sunwarps: ShuffleSunwarps
victory_condition: VictoryCondition victory_condition: VictoryCondition
mastery_achievements: MasteryAchievements mastery_achievements: MasteryAchievements
level_2_requirement: Level2Requirement level_2_requirement: Level2Requirement

View File

@ -1,12 +1,13 @@
from enum import Enum from enum import Enum
from typing import Dict, List, NamedTuple, Optional, Set, Tuple, TYPE_CHECKING from typing import Dict, List, NamedTuple, Optional, Set, Tuple, TYPE_CHECKING
from .datatypes import Door, RoomAndDoor, RoomAndPanel from .datatypes import Door, DoorType, RoomAndDoor, RoomAndPanel
from .items import ALL_ITEM_TABLE, ItemData from .items import ALL_ITEM_TABLE, ItemType
from .locations import ALL_LOCATION_TABLE, LocationClassification from .locations import ALL_LOCATION_TABLE, LocationClassification
from .options import LocationChecks, ShuffleDoors, VictoryCondition from .options import LocationChecks, ShuffleDoors, SunwarpAccess, VictoryCondition
from .static_logic import DOORS_BY_ROOM, PAINTINGS, PAINTING_ENTRANCES, PAINTING_EXITS, \ from .static_logic import DOORS_BY_ROOM, PAINTINGS, PAINTING_ENTRANCES, PAINTING_EXITS, \
PANELS_BY_ROOM, PROGRESSION_BY_ROOM, REQUIRED_PAINTING_ROOMS, REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS PANELS_BY_ROOM, PROGRESSION_BY_ROOM, REQUIRED_PAINTING_ROOMS, REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS, \
SUNWARP_ENTRANCES, SUNWARP_EXITS
if TYPE_CHECKING: if TYPE_CHECKING:
from . import LingoWorld from . import LingoWorld
@ -58,21 +59,6 @@ def should_split_progression(progression_name: str, world: "LingoWorld") -> Prog
return ProgressiveItemBehavior.PROGRESSIVE return ProgressiveItemBehavior.PROGRESSIVE
def should_include_item(item: ItemData, world: "LingoWorld") -> bool:
if item.mode == "colors":
return world.options.shuffle_colors > 0
elif item.mode == "doors":
return world.options.shuffle_doors != ShuffleDoors.option_none
elif item.mode == "complex door":
return world.options.shuffle_doors == ShuffleDoors.option_complex
elif item.mode == "door group":
return world.options.shuffle_doors == ShuffleDoors.option_simple
elif item.mode == "special":
return False
else:
return True
class LingoPlayerLogic: class LingoPlayerLogic:
""" """
Defines logic after a player's options have been applied Defines logic after a player's options have been applied
@ -99,6 +85,10 @@ class LingoPlayerLogic:
mastery_reqs: List[AccessRequirements] mastery_reqs: List[AccessRequirements]
counting_panel_reqs: Dict[str, List[Tuple[AccessRequirements, int]]] counting_panel_reqs: Dict[str, List[Tuple[AccessRequirements, int]]]
sunwarp_mapping: List[int]
sunwarp_entrances: List[str]
sunwarp_exits: List[str]
def add_location(self, room: str, name: str, code: Optional[int], panels: List[RoomAndPanel], world: "LingoWorld"): def add_location(self, room: str, name: str, code: Optional[int], panels: List[RoomAndPanel], world: "LingoWorld"):
""" """
Creates a location. This function determines the access requirements for the location by combining and Creates a location. This function determines the access requirements for the location by combining and
@ -132,6 +122,7 @@ class LingoPlayerLogic:
self.real_items.append(progressive_item_name) self.real_items.append(progressive_item_name)
else: else:
self.set_door_item(room_name, door_data.name, door_data.item_name) self.set_door_item(room_name, door_data.name, door_data.item_name)
self.real_items.append(door_data.item_name)
def __init__(self, world: "LingoWorld"): def __init__(self, world: "LingoWorld"):
self.item_by_door = {} self.item_by_door = {}
@ -148,6 +139,7 @@ class LingoPlayerLogic:
self.door_reqs = {} self.door_reqs = {}
self.mastery_reqs = [] self.mastery_reqs = []
self.counting_panel_reqs = {} self.counting_panel_reqs = {}
self.sunwarp_mapping = []
door_shuffle = world.options.shuffle_doors door_shuffle = world.options.shuffle_doors
color_shuffle = world.options.shuffle_colors color_shuffle = world.options.shuffle_colors
@ -161,15 +153,37 @@ class LingoPlayerLogic:
"be enough locations for all of the door items.") "be enough locations for all of the door items.")
# Create door items, where needed. # Create door items, where needed.
if door_shuffle != ShuffleDoors.option_none: door_groups: Set[str] = set()
for room_name, room_data in DOORS_BY_ROOM.items(): for room_name, room_data in DOORS_BY_ROOM.items():
for door_name, door_data in room_data.items(): for door_name, door_data in room_data.items():
if door_data.skip_item is False and door_data.event is False: if door_data.skip_item is False and door_data.event is False:
if door_data.type == DoorType.NORMAL and door_shuffle != ShuffleDoors.option_none:
if door_data.door_group is not None and door_shuffle == ShuffleDoors.option_simple: if door_data.door_group is not None and door_shuffle == ShuffleDoors.option_simple:
# Grouped doors are handled differently if shuffle doors is on simple. # Grouped doors are handled differently if shuffle doors is on simple.
self.set_door_item(room_name, door_name, door_data.door_group) self.set_door_item(room_name, door_name, door_data.door_group)
door_groups.add(door_data.door_group)
else: else:
self.handle_non_grouped_door(room_name, door_data, world) self.handle_non_grouped_door(room_name, door_data, world)
elif door_data.type == DoorType.SUNWARP:
if world.options.sunwarp_access == SunwarpAccess.option_unlock:
self.set_door_item(room_name, door_name, "Sunwarps")
door_groups.add("Sunwarps")
elif world.options.sunwarp_access == SunwarpAccess.option_individual:
self.set_door_item(room_name, door_name, door_data.item_name)
self.real_items.append(door_data.item_name)
elif world.options.sunwarp_access == SunwarpAccess.option_progressive:
self.set_door_item(room_name, door_name, "Progressive Pilgrimage")
self.real_items.append("Progressive Pilgrimage")
elif door_data.type == DoorType.SUN_PAINTING:
if not world.options.enable_pilgrimage:
self.set_door_item(room_name, door_name, door_data.item_name)
self.real_items.append(door_data.item_name)
self.real_items += door_groups
# Create color items, if needed.
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. # 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 room_name, room_data in PANELS_BY_ROOM.items():
@ -206,6 +220,11 @@ class LingoPlayerLogic:
if world.options.level_2_requirement == 1: if world.options.level_2_requirement == 1:
raise Exception("The Level 2 requirement must be at least 2 when LEVEL 2 is the victory condition.") raise Exception("The Level 2 requirement must be at least 2 when LEVEL 2 is the victory condition.")
elif victory_condition == VictoryCondition.option_pilgrimage:
self.victory_condition = "Pilgrim Antechamber - PILGRIM"
self.add_location("Pilgrim Antechamber", "PILGRIM (Solved)", None,
[RoomAndPanel("Pilgrim Antechamber", "PILGRIM")], world)
self.event_loc_to_item["PILGRIM (Solved)"] = "Victory"
# 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)
@ -225,28 +244,22 @@ class LingoPlayerLogic:
self.add_location(location_data.room, location_name, location_data.code, location_data.panels, world) self.add_location(location_data.room, location_name, location_data.code, location_data.panels, world)
self.real_locations.append(location_name) self.real_locations.append(location_name)
# Instantiate all real items. if world.options.enable_pilgrimage and world.options.sunwarp_access == SunwarpAccess.option_disabled:
for name, item in ALL_ITEM_TABLE.items(): raise Exception("Sunwarps cannot be disabled when pilgrimage is enabled.")
if should_include_item(item, world):
self.real_items.append(name)
# Calculate the requirements for the fake pilgrimage. if world.options.shuffle_sunwarps:
fake_pilgrimage = [ if world.options.sunwarp_access == SunwarpAccess.option_disabled:
["Second Room", "Exit Door"], ["Crossroads", "Tower Entrance"], raise Exception("Sunwarps cannot be shuffled if they are disabled.")
["Orange Tower Fourth Floor", "Hot Crusts Door"], ["Outside The Initiated", "Shortcut to Hub Room"],
["Orange Tower First Floor", "Shortcut to Hub Room"], ["Directional Gallery", "Shortcut to The Undeterred"], self.sunwarp_mapping = list(range(0, 12))
["Orange Tower First Floor", "Salt Pepper Door"], ["Hub Room", "Crossroads Entrance"], world.random.shuffle(self.sunwarp_mapping)
["Color Hunt", "Shortcut to The Steady"], ["The Bearer", "Entrance"], ["Art Gallery", "Exit"],
["The Tenacious", "Shortcut to Hub Room"], ["Outside The Agreeable", "Tenacious Entrance"] sunwarp_rooms = SUNWARP_ENTRANCES + SUNWARP_EXITS
] self.sunwarp_entrances = [sunwarp_rooms[i] for i in self.sunwarp_mapping[0:6]]
pilgrimage_reqs = AccessRequirements() self.sunwarp_exits = [sunwarp_rooms[i] for i in self.sunwarp_mapping[6:12]]
for door in fake_pilgrimage: else:
door_object = DOORS_BY_ROOM[door[0]][door[1]] self.sunwarp_entrances = SUNWARP_ENTRANCES
if door_object.event or world.options.shuffle_doors == ShuffleDoors.option_none: self.sunwarp_exits = SUNWARP_EXITS
pilgrimage_reqs.merge(self.calculate_door_requirements(door[0], door[1], world))
else:
pilgrimage_reqs.doors.add(RoomAndDoor(door[0], door[1]))
self.door_reqs.setdefault("Pilgrim Antechamber", {})["Pilgrimage"] = pilgrimage_reqs
# Create the paintings mapping, if painting shuffle is on. # Create the paintings mapping, if painting shuffle is on.
if painting_shuffle: if painting_shuffle:
@ -277,10 +290,11 @@ class LingoPlayerLogic:
# Starting Room - Exit Door gives access to OPEN and TRACE. # Starting Room - Exit Door gives access to OPEN and TRACE.
good_item_options: List[str] = ["Starting Room - Back Right Door", "Second Room - Exit Door"] good_item_options: List[str] = ["Starting Room - Back Right Door", "Second Room - Exit Door"]
if not color_shuffle: if not color_shuffle and not world.options.enable_pilgrimage:
# HOT CRUST and THIS. # HOT CRUST and THIS.
good_item_options.append("Pilgrim Room - Sun Painting") good_item_options.append("Pilgrim Room - Sun Painting")
if not color_shuffle:
if door_shuffle == ShuffleDoors.option_simple: if door_shuffle == ShuffleDoors.option_simple:
# WELCOME BACK, CLOCKWISE, and DRAWL + RUNS. # WELCOME BACK, CLOCKWISE, and DRAWL + RUNS.
good_item_options.append("Welcome Back Doors") good_item_options.append("Welcome Back Doors")

View File

@ -1,10 +1,11 @@
from typing import Dict, Optional, TYPE_CHECKING from typing import Dict, Optional, TYPE_CHECKING
from BaseClasses import Entrance, ItemClassification, Region from BaseClasses import Entrance, ItemClassification, Region
from .datatypes import Room, RoomAndDoor from .datatypes import EntranceType, Room, RoomAndDoor
from .items import LingoItem from .items import LingoItem
from .locations import LingoLocation from .locations import LingoLocation
from .rules import lingo_can_use_entrance, make_location_lambda from .options import SunwarpAccess
from .rules import lingo_can_do_pilgrimage, lingo_can_use_entrance, make_location_lambda
from .static_logic import ALL_ROOMS, PAINTINGS from .static_logic import ALL_ROOMS, PAINTINGS
if TYPE_CHECKING: if TYPE_CHECKING:
@ -25,8 +26,20 @@ def create_region(room: Room, world: "LingoWorld") -> Region:
return new_region return new_region
def is_acceptable_pilgrimage_entrance(entrance_type: EntranceType, world: "LingoWorld") -> bool:
allowed_entrance_types = EntranceType.NORMAL
if world.options.pilgrimage_allows_paintings:
allowed_entrance_types |= EntranceType.PAINTING
if world.options.pilgrimage_allows_roof_access:
allowed_entrance_types |= EntranceType.CROSSROADS_ROOF_ACCESS
return bool(entrance_type & allowed_entrance_types)
def connect_entrance(regions: Dict[str, Region], source_region: Region, target_region: Region, description: str, def connect_entrance(regions: Dict[str, Region], source_region: Region, target_region: Region, description: str,
door: Optional[RoomAndDoor], world: "LingoWorld"): door: Optional[RoomAndDoor], entrance_type: EntranceType, pilgrimage: bool, world: "LingoWorld"):
connection = Entrance(world.player, description, source_region) connection = Entrance(world.player, description, source_region)
connection.access_rule = lambda state: lingo_can_use_entrance(state, target_region.name, door, world) connection.access_rule = lambda state: lingo_can_use_entrance(state, target_region.name, door, world)
@ -38,6 +51,21 @@ def connect_entrance(regions: Dict[str, Region], source_region: Region, target_r
if door.door not in world.player_logic.item_by_door.get(effective_room, {}): if door.door not in world.player_logic.item_by_door.get(effective_room, {}):
for region in world.player_logic.calculate_door_requirements(effective_room, door.door, world).rooms: for region in world.player_logic.calculate_door_requirements(effective_room, door.door, world).rooms:
world.multiworld.register_indirect_condition(regions[region], connection) world.multiworld.register_indirect_condition(regions[region], connection)
if not pilgrimage and world.options.enable_pilgrimage and is_acceptable_pilgrimage_entrance(entrance_type, world)\
and source_region.name != "Menu":
for part in range(1, 6):
pilgrimage_descriptor = f" (Pilgrimage Part {part})"
pilgrim_source_region = regions[f"{source_region.name}{pilgrimage_descriptor}"]
pilgrim_target_region = regions[f"{target_region.name}{pilgrimage_descriptor}"]
effective_door = door
if effective_door is not None:
effective_room = target_region.name if door.room is None else door.room
effective_door = RoomAndDoor(effective_room, door.door)
connect_entrance(regions, pilgrim_source_region, pilgrim_target_region,
f"{description}{pilgrimage_descriptor}", effective_door, entrance_type, True, world)
def connect_painting(regions: Dict[str, Region], warp_enter: str, warp_exit: str, world: "LingoWorld") -> None: def connect_painting(regions: Dict[str, Region], warp_enter: str, warp_exit: str, world: "LingoWorld") -> None:
@ -48,7 +76,8 @@ def connect_painting(regions: Dict[str, Region], warp_enter: str, warp_exit: str
source_region = regions[source_painting.room] source_region = regions[source_painting.room]
entrance_name = f"{source_painting.room} to {target_painting.room} ({source_painting.id} Painting)" entrance_name = f"{source_painting.room} to {target_painting.room} ({source_painting.id} Painting)"
connect_entrance(regions, source_region, target_region, entrance_name, source_painting.required_door, world) connect_entrance(regions, source_region, target_region, entrance_name, source_painting.required_door,
EntranceType.PAINTING, False, world)
def create_regions(world: "LingoWorld") -> None: def create_regions(world: "LingoWorld") -> None:
@ -63,11 +92,26 @@ def create_regions(world: "LingoWorld") -> None:
for room in ALL_ROOMS: for room in ALL_ROOMS:
regions[room.name] = create_region(room, world) regions[room.name] = create_region(room, world)
if world.options.enable_pilgrimage:
for part in range(1, 6):
pilgrimage_region_name = f"{room.name} (Pilgrimage Part {part})"
regions[pilgrimage_region_name] = Region(pilgrimage_region_name, world.player, world.multiworld)
# Connect all created regions now that they exist. # Connect all created regions now that they exist.
allowed_entrance_types = EntranceType.NORMAL | EntranceType.WARP | EntranceType.CROSSROADS_ROOF_ACCESS
if not painting_shuffle:
# Don't use the vanilla painting connections if we are shuffling paintings.
allowed_entrance_types |= EntranceType.PAINTING
if world.options.sunwarp_access != SunwarpAccess.option_disabled and not world.options.shuffle_sunwarps:
# Don't connect sunwarps if sunwarps are disabled or if we're shuffling sunwarps.
allowed_entrance_types |= EntranceType.SUNWARP
for room in ALL_ROOMS: for room in ALL_ROOMS:
for entrance in room.entrances: for entrance in room.entrances:
# Don't use the vanilla painting connections if we are shuffling paintings. effective_entrance_type = entrance.type & allowed_entrance_types
if entrance.painting and painting_shuffle: if not effective_entrance_type:
continue continue
entrance_name = f"{entrance.room} to {room.name}" entrance_name = f"{entrance.room} to {room.name}"
@ -77,17 +121,56 @@ def create_regions(world: "LingoWorld") -> None:
else: else:
entrance_name += f" (through {room.name} - {entrance.door.door})" entrance_name += f" (through {room.name} - {entrance.door.door})"
connect_entrance(regions, regions[entrance.room], regions[room.name], entrance_name, entrance.door, world) effective_door = entrance.door
if entrance.type == EntranceType.SUNWARP and world.options.sunwarp_access == SunwarpAccess.option_normal:
effective_door = None
# Add the fake pilgrimage. connect_entrance(regions, regions[entrance.room], regions[room.name], entrance_name, effective_door,
connect_entrance(regions, regions["Outside The Agreeable"], regions["Pilgrim Antechamber"], "Pilgrimage", effective_entrance_type, False, world)
RoomAndDoor("Pilgrim Antechamber", "Pilgrimage"), world)
if world.options.enable_pilgrimage:
# Connect the start of the pilgrimage. We check for all sunwarp items here.
pilgrim_start_from = regions[world.player_logic.sunwarp_entrances[0]]
pilgrim_start_to = regions[f"{world.player_logic.sunwarp_exits[0]} (Pilgrimage Part 1)"]
if world.options.sunwarp_access >= SunwarpAccess.option_unlock:
pilgrim_start_from.connect(pilgrim_start_to, f"Pilgrimage Part 1",
lambda state: lingo_can_do_pilgrimage(state, world))
else:
pilgrim_start_from.connect(pilgrim_start_to, f"Pilgrimage Part 1")
# Create connections between each segment of the pilgrimage.
for i in range(1, 6):
from_room = f"{world.player_logic.sunwarp_entrances[i]} (Pilgrimage Part {i})"
to_room = f"{world.player_logic.sunwarp_exits[i]} (Pilgrimage Part {i+1})"
if i == 5:
to_room = "Pilgrim Antechamber"
regions[from_room].connect(regions[to_room], f"Pilgrimage Part {i+1}")
else:
connect_entrance(regions, regions["Starting Room"], regions["Pilgrim Antechamber"], "Sun Painting",
RoomAndDoor("Pilgrim Antechamber", "Sun Painting"), EntranceType.PAINTING, False, world)
if early_color_hallways: if early_color_hallways:
regions["Starting Room"].connect(regions["Outside The Undeterred"], "Early Color Hallways") connect_entrance(regions, regions["Starting Room"], regions["Outside The Undeterred"], "Early Color Hallways",
None, EntranceType.PAINTING, False, world)
if painting_shuffle: if painting_shuffle:
for warp_enter, warp_exit in world.player_logic.painting_mapping.items(): for warp_enter, warp_exit in world.player_logic.painting_mapping.items():
connect_painting(regions, warp_enter, warp_exit, world) connect_painting(regions, warp_enter, warp_exit, world)
if world.options.shuffle_sunwarps:
for i in range(0, 6):
if world.options.sunwarp_access == SunwarpAccess.option_normal:
effective_door = None
else:
effective_door = RoomAndDoor("Sunwarps", f"{i + 1} Sunwarp")
source_region = regions[world.player_logic.sunwarp_entrances[i]]
target_region = regions[world.player_logic.sunwarp_exits[i]]
entrance_name = f"{source_region.name} to {target_region.name} ({i + 1} Sunwarp)"
connect_entrance(regions, source_region, target_region, entrance_name, effective_door, EntranceType.SUNWARP,
False, world)
world.multiworld.regions += regions.values() world.multiworld.regions += regions.values()

View File

@ -17,6 +17,10 @@ def lingo_can_use_entrance(state: CollectionState, room: str, door: RoomAndDoor,
return _lingo_can_open_door(state, effective_room, door.door, world) return _lingo_can_open_door(state, effective_room, door.door, world)
def lingo_can_do_pilgrimage(state: CollectionState, world: "LingoWorld"):
return all(_lingo_can_open_door(state, "Sunwarps", f"{i} Sunwarp", world) for i in range(1, 7))
def lingo_can_use_location(state: CollectionState, location: PlayerLocation, world: "LingoWorld"): def lingo_can_use_location(state: CollectionState, location: PlayerLocation, world: "LingoWorld"):
return _lingo_can_satisfy_requirements(state, location.access, world) return _lingo_can_satisfy_requirements(state, location.access, world)

View File

@ -1,10 +1,9 @@
import os import os
import pkgutil import pkgutil
import pickle
from io import BytesIO from io import BytesIO
from typing import Dict, List, Set from typing import Dict, List, Set
import pickle
from .datatypes import Door, Painting, Panel, Progression, Room from .datatypes import Door, Painting, Panel, Progression, Room
ALL_ROOMS: List[Room] = [] ALL_ROOMS: List[Room] = []
@ -21,6 +20,9 @@ PAINTING_EXITS: int = 0
REQUIRED_PAINTING_ROOMS: List[str] = [] REQUIRED_PAINTING_ROOMS: List[str] = []
REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS: List[str] = [] REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS: List[str] = []
SUNWARP_ENTRANCES: List[str] = []
SUNWARP_EXITS: List[str] = []
SPECIAL_ITEM_IDS: Dict[str, int] = {} SPECIAL_ITEM_IDS: Dict[str, int] = {}
PANEL_LOCATION_IDS: Dict[str, Dict[str, int]] = {} PANEL_LOCATION_IDS: Dict[str, Dict[str, int]] = {}
DOOR_LOCATION_IDS: Dict[str, Dict[str, int]] = {} DOOR_LOCATION_IDS: Dict[str, Dict[str, int]] = {}
@ -99,6 +101,8 @@ def load_static_data_from_file():
PAINTING_EXITS = pickdata["PAINTING_EXITS"] PAINTING_EXITS = pickdata["PAINTING_EXITS"]
REQUIRED_PAINTING_ROOMS.extend(pickdata["REQUIRED_PAINTING_ROOMS"]) REQUIRED_PAINTING_ROOMS.extend(pickdata["REQUIRED_PAINTING_ROOMS"])
REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS.extend(pickdata["REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS"]) REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS.extend(pickdata["REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS"])
SUNWARP_ENTRANCES.extend(pickdata["SUNWARP_ENTRANCES"])
SUNWARP_EXITS.extend(pickdata["SUNWARP_EXITS"])
SPECIAL_ITEM_IDS.update(pickdata["SPECIAL_ITEM_IDS"]) SPECIAL_ITEM_IDS.update(pickdata["SPECIAL_ITEM_IDS"])
PANEL_LOCATION_IDS.update(pickdata["PANEL_LOCATION_IDS"]) PANEL_LOCATION_IDS.update(pickdata["PANEL_LOCATION_IDS"])
DOOR_LOCATION_IDS.update(pickdata["DOOR_LOCATION_IDS"]) DOOR_LOCATION_IDS.update(pickdata["DOOR_LOCATION_IDS"])

View File

@ -29,3 +29,23 @@ class TestAllPanelHunt(LingoTestBase):
"level_2_requirement": "800", "level_2_requirement": "800",
"early_color_hallways": "true" "early_color_hallways": "true"
} }
class TestShuffleSunwarps(LingoTestBase):
options = {
"shuffle_doors": "none",
"shuffle_colors": "false",
"victory_condition": "pilgrimage",
"shuffle_sunwarps": "true",
"sunwarp_access": "normal"
}
class TestShuffleSunwarpsAccess(LingoTestBase):
options = {
"shuffle_doors": "none",
"shuffle_colors": "false",
"victory_condition": "pilgrimage",
"shuffle_sunwarps": "true",
"sunwarp_access": "individual"
}

View File

@ -0,0 +1,114 @@
from . import LingoTestBase
class TestDisabledPilgrimage(LingoTestBase):
options = {
"enable_pilgrimage": "false",
"shuffle_colors": "false"
}
def test_access(self):
self.assertFalse(self.can_reach_location("Pilgrim Antechamber - PILGRIM"))
self.collect_by_name("Pilgrim Room - Sun Painting")
self.assertTrue(self.can_reach_location("Pilgrim Antechamber - PILGRIM"))
class TestPilgrimageWithRoofAndPaintings(LingoTestBase):
options = {
"enable_pilgrimage": "true",
"shuffle_colors": "false",
"shuffle_doors": "complex",
"pilgrimage_allows_roof_access": "true",
"pilgrimage_allows_paintings": "true",
"early_color_hallways": "false"
}
def test_access(self):
doors = ["Second Room - Exit Door", "Crossroads - Roof Access", "Hub Room - Crossroads Entrance",
"Outside The Undeterred - Green Painting"]
for door in doors:
print(door)
self.assertFalse(self.can_reach_location("Pilgrim Antechamber - PILGRIM"))
self.collect_by_name(door)
self.assertTrue(self.can_reach_location("Pilgrim Antechamber - PILGRIM"))
class TestPilgrimageNoRoofYesPaintings(LingoTestBase):
options = {
"enable_pilgrimage": "true",
"shuffle_colors": "false",
"shuffle_doors": "complex",
"pilgrimage_allows_roof_access": "false",
"pilgrimage_allows_paintings": "true",
"early_color_hallways": "false"
}
def test_access(self):
doors = ["Second Room - Exit Door", "Crossroads - Roof Access", "Hub Room - Crossroads Entrance",
"Outside The Undeterred - Green Painting", "Crossroads - Tower Entrance",
"Orange Tower Fourth Floor - Hot Crusts Door", "Orange Tower First Floor - Shortcut to Hub Room",
"Starting Room - Street Painting"]
for door in doors:
print(door)
self.assertFalse(self.can_reach_location("Pilgrim Antechamber - PILGRIM"))
self.collect_by_name(door)
self.assertTrue(self.can_reach_location("Pilgrim Antechamber - PILGRIM"))
class TestPilgrimageNoRoofNoPaintings(LingoTestBase):
options = {
"enable_pilgrimage": "true",
"shuffle_colors": "false",
"shuffle_doors": "complex",
"pilgrimage_allows_roof_access": "false",
"pilgrimage_allows_paintings": "false",
"early_color_hallways": "false"
}
def test_access(self):
doors = ["Second Room - Exit Door", "Crossroads - Roof Access", "Hub Room - Crossroads Entrance",
"Outside The Undeterred - Green Painting", "Orange Tower First Floor - Shortcut to Hub Room",
"Starting Room - Street Painting", "Outside The Initiated - Shortcut to Hub Room",
"Directional Gallery - Shortcut to The Undeterred", "Orange Tower First Floor - Salt Pepper Door",
"Color Hunt - Shortcut to The Steady", "The Bearer - Entrance",
"Orange Tower Fifth Floor - Quadruple Intersection", "The Tenacious - Shortcut to Hub Room",
"Outside The Agreeable - Tenacious Entrance", "Crossroads - Tower Entrance",
"Orange Tower Fourth Floor - Hot Crusts Door"]
for door in doors:
print(door)
self.assertFalse(self.can_reach_location("Pilgrim Antechamber - PILGRIM"))
self.collect_by_name(door)
self.assertTrue(self.can_reach_location("Pilgrim Antechamber - PILGRIM"))
class TestPilgrimageYesRoofNoPaintings(LingoTestBase):
options = {
"enable_pilgrimage": "true",
"shuffle_colors": "false",
"shuffle_doors": "complex",
"pilgrimage_allows_roof_access": "true",
"pilgrimage_allows_paintings": "false",
"early_color_hallways": "false"
}
def test_access(self):
doors = ["Second Room - Exit Door", "Crossroads - Roof Access", "Hub Room - Crossroads Entrance",
"Outside The Undeterred - Green Painting", "Orange Tower First Floor - Shortcut to Hub Room",
"Starting Room - Street Painting", "Outside The Initiated - Shortcut to Hub Room",
"Directional Gallery - Shortcut to The Undeterred", "Orange Tower First Floor - Salt Pepper Door",
"Color Hunt - Shortcut to The Steady", "The Bearer - Entrance",
"Orange Tower Fifth Floor - Quadruple Intersection"]
for door in doors:
print(door)
self.assertFalse(self.can_reach_location("Pilgrim Antechamber - PILGRIM"))
self.collect_by_name(door)
self.assertTrue(self.can_reach_location("Pilgrim Antechamber - PILGRIM"))

View File

@ -0,0 +1,213 @@
from . import LingoTestBase
class TestVanillaDoorsNormalSunwarps(LingoTestBase):
options = {
"shuffle_doors": "none",
"shuffle_colors": "true",
"sunwarp_access": "normal"
}
def test_access(self):
self.assertTrue(self.multiworld.state.can_reach("Crossroads", "Region", self.player))
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
self.collect_by_name("Yellow")
self.assertTrue(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
self.assertTrue(self.multiworld.state.can_reach("Outside The Initiated", "Region", self.player))
class TestSimpleDoorsNormalSunwarps(LingoTestBase):
options = {
"shuffle_doors": "simple",
"sunwarp_access": "normal"
}
def test_access(self):
self.assertFalse(self.multiworld.state.can_reach("Crossroads", "Region", self.player))
self.collect_by_name("Second Room - Exit Door")
self.assertTrue(self.multiworld.state.can_reach("Crossroads", "Region", self.player))
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
self.collect_by_name(["Crossroads - Tower Entrances", "Orange Tower Fourth Floor - Hot Crusts Door"])
self.assertTrue(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
self.assertTrue(self.multiworld.state.can_reach("Outside The Initiated", "Region", self.player))
class TestSimpleDoorsDisabledSunwarps(LingoTestBase):
options = {
"shuffle_doors": "simple",
"sunwarp_access": "disabled"
}
def test_access(self):
self.assertFalse(self.multiworld.state.can_reach("Crossroads", "Region", self.player))
self.collect_by_name("Second Room - Exit Door")
self.assertFalse(self.multiworld.state.can_reach("Crossroads", "Region", self.player))
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
self.collect_by_name(["Hub Room - Crossroads Entrance", "Crossroads - Tower Entrancse",
"Orange Tower Fourth Floor - Hot Crusts Door"])
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
self.assertFalse(self.multiworld.state.can_reach("Outside The Initiated", "Region", self.player))
class TestSimpleDoorsUnlockSunwarps(LingoTestBase):
options = {
"shuffle_doors": "simple",
"sunwarp_access": "unlock"
}
def test_access(self):
self.assertFalse(self.multiworld.state.can_reach("Crossroads", "Region", self.player))
self.collect_by_name("Second Room - Exit Door")
self.assertFalse(self.multiworld.state.can_reach("Crossroads", "Region", self.player))
self.collect_by_name(["Crossroads - Tower Entrances", "Orange Tower Fourth Floor - Hot Crusts Door"])
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
self.assertFalse(self.multiworld.state.can_reach("Outside The Initiated", "Region", self.player))
self.collect_by_name("Sunwarps")
self.assertTrue(self.multiworld.state.can_reach("Crossroads", "Region", self.player))
self.assertTrue(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
self.assertTrue(self.multiworld.state.can_reach("Outside The Initiated", "Region", self.player))
class TestComplexDoorsNormalSunwarps(LingoTestBase):
options = {
"shuffle_doors": "complex",
"sunwarp_access": "normal"
}
def test_access(self):
self.assertFalse(self.multiworld.state.can_reach("Crossroads", "Region", self.player))
self.collect_by_name("Second Room - Exit Door")
self.assertTrue(self.multiworld.state.can_reach("Crossroads", "Region", self.player))
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
self.collect_by_name(["Crossroads - Tower Entrance", "Orange Tower Fourth Floor - Hot Crusts Door"])
self.assertTrue(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
self.assertTrue(self.multiworld.state.can_reach("Outside The Initiated", "Region", self.player))
class TestComplexDoorsDisabledSunwarps(LingoTestBase):
options = {
"shuffle_doors": "complex",
"sunwarp_access": "disabled"
}
def test_access(self):
self.assertFalse(self.multiworld.state.can_reach("Crossroads", "Region", self.player))
self.collect_by_name("Second Room - Exit Door")
self.assertFalse(self.multiworld.state.can_reach("Crossroads", "Region", self.player))
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
self.collect_by_name(["Hub Room - Crossroads Entrance", "Crossroads - Tower Entrance",
"Orange Tower Fourth Floor - Hot Crusts Door"])
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
self.assertFalse(self.multiworld.state.can_reach("Outside The Initiated", "Region", self.player))
class TestComplexDoorsIndividualSunwarps(LingoTestBase):
options = {
"shuffle_doors": "complex",
"sunwarp_access": "individual"
}
def test_access(self):
self.assertFalse(self.multiworld.state.can_reach("Crossroads", "Region", self.player))
self.collect_by_name("Second Room - Exit Door")
self.assertFalse(self.multiworld.state.can_reach("Crossroads", "Region", self.player))
self.collect_by_name("1 Sunwarp")
self.assertTrue(self.multiworld.state.can_reach("Crossroads", "Region", self.player))
self.collect_by_name(["Crossroads - Tower Entrance", "Orange Tower Fourth Floor - Hot Crusts Door"])
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
self.assertFalse(self.multiworld.state.can_reach("Outside The Initiated", "Region", self.player))
self.collect_by_name("2 Sunwarp")
self.assertTrue(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
self.assertFalse(self.multiworld.state.can_reach("Outside The Initiated", "Region", self.player))
self.collect_by_name("3 Sunwarp")
self.assertTrue(self.multiworld.state.can_reach("Outside The Initiated", "Region", self.player))
class TestComplexDoorsProgressiveSunwarps(LingoTestBase):
options = {
"shuffle_doors": "complex",
"sunwarp_access": "progressive"
}
def test_access(self):
self.assertFalse(self.multiworld.state.can_reach("Crossroads", "Region", self.player))
self.collect_by_name("Second Room - Exit Door")
self.assertFalse(self.multiworld.state.can_reach("Crossroads", "Region", self.player))
progressive_pilgrimage = self.get_items_by_name("Progressive Pilgrimage")
self.collect(progressive_pilgrimage[0])
self.assertTrue(self.multiworld.state.can_reach("Crossroads", "Region", self.player))
self.collect_by_name(["Crossroads - Tower Entrance", "Orange Tower Fourth Floor - Hot Crusts Door"])
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
self.assertFalse(self.multiworld.state.can_reach("Outside The Initiated", "Region", self.player))
self.collect(progressive_pilgrimage[1])
self.assertTrue(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
self.assertFalse(self.multiworld.state.can_reach("Outside The Initiated", "Region", self.player))
self.collect(progressive_pilgrimage[2])
self.assertTrue(self.multiworld.state.can_reach("Outside The Initiated", "Region", self.player))
class TestUnlockSunwarpPilgrimage(LingoTestBase):
options = {
"sunwarp_access": "unlock",
"shuffle_colors": "false",
"enable_pilgrimage": "true"
}
def test_access(self):
self.assertFalse(self.can_reach_location("Pilgrim Antechamber - PILGRIM"))
self.collect_by_name("Sunwarps")
self.assertTrue(self.can_reach_location("Pilgrim Antechamber - PILGRIM"))
class TestIndividualSunwarpPilgrimage(LingoTestBase):
options = {
"sunwarp_access": "individual",
"shuffle_colors": "false",
"enable_pilgrimage": "true"
}
def test_access(self):
for i in range(1, 7):
self.assertFalse(self.can_reach_location("Pilgrim Antechamber - PILGRIM"))
self.collect_by_name(f"{i} Sunwarp")
self.assertTrue(self.can_reach_location("Pilgrim Antechamber - PILGRIM"))
class TestProgressiveSunwarpPilgrimage(LingoTestBase):
options = {
"sunwarp_access": "progressive",
"shuffle_colors": "false",
"enable_pilgrimage": "true"
}
def test_access(self):
for item in self.get_items_by_name("Progressive Pilgrimage"):
self.assertFalse(self.can_reach_location("Pilgrim Antechamber - PILGRIM"))
self.collect(item)
self.assertTrue(self.can_reach_location("Pilgrim Antechamber - PILGRIM"))

View File

@ -1,4 +1,4 @@
from typing import Dict, List, Set from typing import Dict, List, Set, Optional
import os import os
import sys import sys
@ -6,7 +6,8 @@ import sys
sys.path.append(os.path.join("worlds", "lingo")) sys.path.append(os.path.join("worlds", "lingo"))
sys.path.append(".") sys.path.append(".")
sys.path.append("..") sys.path.append("..")
from datatypes import Door, Painting, Panel, Progression, Room, RoomAndDoor, RoomAndPanel, RoomEntrance from datatypes import Door, DoorType, EntranceType, Painting, Panel, Progression, Room, RoomAndDoor, RoomAndPanel,\
RoomEntrance
import hashlib import hashlib
import pickle import pickle
@ -28,6 +29,9 @@ PAINTING_EXITS: int = 0
REQUIRED_PAINTING_ROOMS: List[str] = [] REQUIRED_PAINTING_ROOMS: List[str] = []
REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS: List[str] = [] REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS: List[str] = []
SUNWARP_ENTRANCES: List[str] = ["", "", "", "", "", ""]
SUNWARP_EXITS: List[str] = ["", "", "", "", "", ""]
SPECIAL_ITEM_IDS: Dict[str, int] = {} SPECIAL_ITEM_IDS: Dict[str, int] = {}
PANEL_LOCATION_IDS: Dict[str, Dict[str, int]] = {} PANEL_LOCATION_IDS: Dict[str, Dict[str, int]] = {}
DOOR_LOCATION_IDS: Dict[str, Dict[str, int]] = {} DOOR_LOCATION_IDS: Dict[str, Dict[str, int]] = {}
@ -96,41 +100,51 @@ def load_static_data(ll1_path, ids_path):
PAINTING_EXITS = len(PAINTING_EXIT_ROOMS) PAINTING_EXITS = len(PAINTING_EXIT_ROOMS)
def process_entrance(source_room, doors, room_obj): def process_single_entrance(source_room: str, room_name: str, door_obj) -> RoomEntrance:
global PAINTING_ENTRANCES, PAINTING_EXIT_ROOMS global PAINTING_ENTRANCES, PAINTING_EXIT_ROOMS
entrance_type = EntranceType.NORMAL
if "painting" in door_obj and door_obj["painting"]:
entrance_type = EntranceType.PAINTING
elif "sunwarp" in door_obj and door_obj["sunwarp"]:
entrance_type = EntranceType.SUNWARP
elif "warp" in door_obj and door_obj["warp"]:
entrance_type = EntranceType.WARP
elif source_room == "Crossroads" and room_name == "Roof":
entrance_type = EntranceType.CROSSROADS_ROOF_ACCESS
if "painting" in door_obj and door_obj["painting"]:
PAINTING_EXIT_ROOMS.add(room_name)
PAINTING_ENTRANCES += 1
if "door" in door_obj:
return RoomEntrance(source_room, RoomAndDoor(
door_obj["room"] if "room" in door_obj else None,
door_obj["door"]
), entrance_type)
else:
return RoomEntrance(source_room, None, entrance_type)
def process_entrance(source_room, doors, room_obj):
# If the value of an entrance is just True, that means that the entrance is always accessible. # If the value of an entrance is just True, that means that the entrance is always accessible.
if doors is True: if doors is True:
room_obj.entrances.append(RoomEntrance(source_room, None, False)) room_obj.entrances.append(RoomEntrance(source_room, None, EntranceType.NORMAL))
elif isinstance(doors, dict): elif isinstance(doors, dict):
# If the value of an entrance is a dictionary, that means the entrance requires a door to be accessible, is a # If the value of an entrance is a dictionary, that means the entrance requires a door to be accessible, is a
# painting-based entrance, or both. # painting-based entrance, or both.
if "painting" in doors and "door" not in doors: room_obj.entrances.append(process_single_entrance(source_room, room_obj.name, doors))
PAINTING_EXIT_ROOMS.add(room_obj.name)
PAINTING_ENTRANCES += 1
room_obj.entrances.append(RoomEntrance(source_room, None, True))
else:
if "painting" in doors and doors["painting"]:
PAINTING_EXIT_ROOMS.add(room_obj.name)
PAINTING_ENTRANCES += 1
room_obj.entrances.append(RoomEntrance(source_room, RoomAndDoor(
doors["room"] if "room" in doors else None,
doors["door"]
), doors["painting"] if "painting" in doors else False))
else: else:
# If the value of an entrance is a list, then there are multiple possible doors that can give access to the # If the value of an entrance is a list, then there are multiple possible doors that can give access to the
# entrance. # entrance. If there are multiple connections with the same door (or lack of door) that differ only by entrance
# type, coalesce them into one entrance.
entrances: Dict[Optional[RoomAndDoor], EntranceType] = {}
for door in doors: for door in doors:
if "painting" in door and door["painting"]: entrance = process_single_entrance(source_room, room_obj.name, door)
PAINTING_EXIT_ROOMS.add(room_obj.name) entrances[entrance.door] = entrances.get(entrance.door, EntranceType(0)) | entrance.type
PAINTING_ENTRANCES += 1
room_obj.entrances.append(RoomEntrance(source_room, RoomAndDoor( for door, entrance_type in entrances.items():
door["room"] if "room" in door else None, room_obj.entrances.append(RoomEntrance(source_room, door, entrance_type))
door["door"]
), door["painting"] if "painting" in door else False))
def process_panel(room_name, panel_name, panel_data): def process_panel(room_name, panel_name, panel_data):
@ -250,11 +264,6 @@ def process_door(room_name, door_name, door_data):
else: else:
include_reduce = False include_reduce = False
if "junk_item" in door_data:
junk_item = door_data["junk_item"]
else:
junk_item = False
if "door_group" in door_data: if "door_group" in door_data:
door_group = door_data["door_group"] door_group = door_data["door_group"]
else: else:
@ -276,7 +285,7 @@ def process_door(room_name, door_name, door_data):
panels.append(RoomAndPanel(None, panel)) panels.append(RoomAndPanel(None, panel))
else: else:
skip_location = True skip_location = True
panels = None panels = []
# The location name associated with a door can be explicitly specified in the configuration. If it is not, then the # The location name associated with a door can be explicitly specified in the configuration. If it is not, then the
# name is generated using a combination of all of the panels that would ordinarily open the door. This can get quite # name is generated using a combination of all of the panels that would ordinarily open the door. This can get quite
@ -312,8 +321,14 @@ def process_door(room_name, door_name, door_data):
else: else:
painting_ids = [] painting_ids = []
door_type = DoorType.NORMAL
if door_name.endswith(" Sunwarp"):
door_type = DoorType.SUNWARP
elif room_name == "Pilgrim Antechamber" and door_name == "Sun Painting":
door_type = DoorType.SUN_PAINTING
door_obj = Door(door_name, item_name, location_name, panels, skip_location, skip_item, has_doors, door_obj = Door(door_name, item_name, location_name, panels, skip_location, skip_item, has_doors,
painting_ids, event, door_group, include_reduce, junk_item, item_group) painting_ids, event, door_group, include_reduce, door_type, item_group)
DOORS_BY_ROOM[room_name][door_name] = door_obj DOORS_BY_ROOM[room_name][door_name] = door_obj
@ -377,6 +392,15 @@ def process_painting(room_name, painting_data):
PAINTINGS[painting_id] = painting_obj PAINTINGS[painting_id] = painting_obj
def process_sunwarp(room_name, sunwarp_data):
global SUNWARP_ENTRANCES, SUNWARP_EXITS
if sunwarp_data["direction"] == "enter":
SUNWARP_ENTRANCES[sunwarp_data["dots"] - 1] = room_name
else:
SUNWARP_EXITS[sunwarp_data["dots"] - 1] = room_name
def process_progression(room_name, progression_name, progression_doors): def process_progression(room_name, progression_name, progression_doors):
global PROGRESSIVE_ITEMS, PROGRESSION_BY_ROOM global PROGRESSIVE_ITEMS, PROGRESSION_BY_ROOM
@ -422,6 +446,10 @@ def process_room(room_name, room_data):
for painting_data in room_data["paintings"]: for painting_data in room_data["paintings"]:
process_painting(room_name, painting_data) process_painting(room_name, painting_data)
if "sunwarps" in room_data:
for sunwarp_data in room_data["sunwarps"]:
process_sunwarp(room_name, sunwarp_data)
if "progression" in room_data: if "progression" in room_data:
for progression_name, progression_doors in room_data["progression"].items(): for progression_name, progression_doors in room_data["progression"].items():
process_progression(room_name, progression_name, progression_doors) process_progression(room_name, progression_name, progression_doors)
@ -468,6 +496,8 @@ if __name__ == '__main__':
"PAINTING_EXITS": PAINTING_EXITS, "PAINTING_EXITS": PAINTING_EXITS,
"REQUIRED_PAINTING_ROOMS": REQUIRED_PAINTING_ROOMS, "REQUIRED_PAINTING_ROOMS": REQUIRED_PAINTING_ROOMS,
"REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS": REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS, "REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS": REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS,
"SUNWARP_ENTRANCES": SUNWARP_ENTRANCES,
"SUNWARP_EXITS": SUNWARP_EXITS,
"SPECIAL_ITEM_IDS": SPECIAL_ITEM_IDS, "SPECIAL_ITEM_IDS": SPECIAL_ITEM_IDS,
"PANEL_LOCATION_IDS": PANEL_LOCATION_IDS, "PANEL_LOCATION_IDS": PANEL_LOCATION_IDS,
"DOOR_LOCATION_IDS": DOOR_LOCATION_IDS, "DOOR_LOCATION_IDS": DOOR_LOCATION_IDS,

View File

@ -37,12 +37,14 @@ configured_panels = Set[]
mentioned_rooms = Set[] mentioned_rooms = Set[]
mentioned_doors = Set[] mentioned_doors = Set[]
mentioned_panels = Set[] mentioned_panels = Set[]
mentioned_sunwarp_entrances = Set[]
mentioned_sunwarp_exits = Set[]
door_groups = {} door_groups = {}
directives = Set["entrances", "panels", "doors", "paintings", "progression"] directives = Set["entrances", "panels", "doors", "paintings", "sunwarps", "progression"]
panel_directives = Set["id", "required_room", "required_door", "required_panel", "colors", "check", "exclude_reduce", "tag", "link", "subtag", "achievement", "copy_to_sign", "non_counting", "hunt"] panel_directives = Set["id", "required_room", "required_door", "required_panel", "colors", "check", "exclude_reduce", "tag", "link", "subtag", "achievement", "copy_to_sign", "non_counting", "hunt"]
door_directives = Set["id", "painting_id", "panels", "item_name", "item_group", "location_name", "skip_location", "skip_item", "door_group", "include_reduce", "junk_item", "event"] door_directives = Set["id", "painting_id", "panels", "item_name", "item_group", "location_name", "skip_location", "skip_item", "door_group", "include_reduce", "event", "warp_id"]
painting_directives = Set["id", "enter_only", "exit_only", "orientation", "required_door", "required", "required_when_no_doors", "move", "req_blocked", "req_blocked_when_no_doors"] painting_directives = Set["id", "enter_only", "exit_only", "orientation", "required_door", "required", "required_when_no_doors", "move", "req_blocked", "req_blocked_when_no_doors"]
non_counting = 0 non_counting = 0
@ -67,17 +69,17 @@ config.each do |room_name, room|
entrances = [] entrances = []
if entrance.kind_of? Hash if entrance.kind_of? Hash
if entrance.keys() != ["painting"] then entrances = [entrance]
entrances = [entrance]
end
elsif entrance.kind_of? Array elsif entrance.kind_of? Array
entrances = entrance entrances = entrance
end end
entrances.each do |e| entrances.each do |e|
entrance_room = e.include?("room") ? e["room"] : room_name if e.include?("door") then
mentioned_rooms.add(entrance_room) entrance_room = e.include?("room") ? e["room"] : room_name
mentioned_doors.add(entrance_room + " - " + e["door"]) mentioned_rooms.add(entrance_room)
mentioned_doors.add(entrance_room + " - " + e["door"])
end
end end
end end
@ -204,8 +206,8 @@ config.each do |room_name, room|
end end
end end
if not door.include?("id") and not door.include?("painting_id") and not door["skip_item"] and not door["event"] then if not door.include?("id") and not door.include?("painting_id") and not door.include?("warp_id") and not door["skip_item"] and not door["event"] then
puts "#{room_name} - #{door_name} :::: Should be marked skip_item or event if there are no doors or paintings" puts "#{room_name} - #{door_name} :::: Should be marked skip_item or event if there are no doors, paintings, or warps"
end end
if door.include?("panels") if door.include?("panels")
@ -292,6 +294,32 @@ config.each do |room_name, room|
end end
end end
(room["sunwarps"] || []).each do |sunwarp|
if sunwarp.include? "dots" and sunwarp.include? "direction" then
if sunwarp["dots"] < 1 or sunwarp["dots"] > 6 then
puts "#{room_name} :::: Contains a sunwarp with an invalid dots value"
end
if sunwarp["direction"] == "enter" then
if mentioned_sunwarp_entrances.include? sunwarp["dots"] then
puts "Multiple #{sunwarp["dots"]} sunwarp entrances were found"
else
mentioned_sunwarp_entrances.add(sunwarp["dots"])
end
elsif sunwarp["direction"] == "exit" then
if mentioned_sunwarp_exits.include? sunwarp["dots"] then
puts "Multiple #{sunwarp["dots"]} sunwarp exits were found"
else
mentioned_sunwarp_exits.add(sunwarp["dots"])
end
else
puts "#{room_name} :::: Contains a sunwarp with an invalid direction value"
end
else
puts "#{room_name} :::: Contains a sunwarp without a dots and direction"
end
end
(room["progression"] || {}).each do |progression_name, door_list| (room["progression"] || {}).each do |progression_name, door_list|
door_list.each do |door| door_list.each do |door|
if door.kind_of? Hash then if door.kind_of? Hash then