Lingo: Various generation optimizations (#2479)
Almost all of the events have been eradicated, which significantly improves both generation speed and playthrough calculation. Previously, checking for access to a location involved checking for access to each panel in the location, as well as recursively checking for access to any panels required by those panels. This potentially performed the same check multiple times. The access requirements for locations are now calculated and flattened in generate_early, so that the access function can directly check for the required rooms, doors, and colors. These flattened access requirements are also used for Entrance checking, and register_indirect_condition is used to make sure that can_reach(Region) is safe to use. The Mastery and Level 2 rules now just run a bunch of access rules and count the number of them that succeed, instead of relying on event items. Finally: the Level 2 panel hunt is now enabled even when Level 2 is not the victory condition, as I feel that generation is fast enough now for that to be acceptable.
This commit is contained in:
parent
8a852abdc4
commit
6dccf36f88
|
@ -55,14 +55,14 @@ class LingoWorld(World):
|
|||
create_regions(self, self.player_logic)
|
||||
|
||||
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]
|
||||
|
||||
if self.player_logic.FORCED_GOOD_ITEM != "":
|
||||
new_item = self.create_item(self.player_logic.FORCED_GOOD_ITEM)
|
||||
if self.player_logic.forced_good_item != "":
|
||||
new_item = self.create_item(self.player_logic.forced_good_item)
|
||||
location_obj = self.multiworld.get_location("Second Room - Good Luck", self.player)
|
||||
location_obj.place_locked_item(new_item)
|
||||
|
||||
item_difference = len(self.player_logic.REAL_LOCATIONS) - len(pool)
|
||||
item_difference = len(self.player_logic.real_locations) - len(pool)
|
||||
if item_difference:
|
||||
trap_percentage = self.options.trap_percentage
|
||||
traps = int(item_difference * trap_percentage / 100.0)
|
||||
|
@ -93,7 +93,7 @@ class LingoWorld(World):
|
|||
|
||||
classification = item.classification
|
||||
if hasattr(self, "options") and self.options.shuffle_paintings and len(item.painting_ids) > 0\
|
||||
and len(item.door_ids) == 0 and all(painting_id not in self.player_logic.PAINTING_MAPPING
|
||||
and len(item.door_ids) == 0 and all(painting_id not in self.player_logic.painting_mapping
|
||||
for painting_id in item.painting_ids):
|
||||
# If this is a "door" that just moves one or more paintings, and painting shuffle is on and those paintings
|
||||
# go nowhere, then this item should not be progression.
|
||||
|
@ -116,6 +116,6 @@ class LingoWorld(World):
|
|||
}
|
||||
|
||||
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
|
||||
|
||||
return slot_data
|
||||
|
|
|
@ -379,8 +379,6 @@
|
|||
tag: forbid
|
||||
non_counting: True
|
||||
check: True
|
||||
required_panel:
|
||||
- panel: ANOTHER TRY
|
||||
doors:
|
||||
Exit Door:
|
||||
id: Entry Room Area Doors/Door_hi_high
|
||||
|
|
|
@ -52,7 +52,10 @@ class ShufflePaintings(Toggle):
|
|||
|
||||
|
||||
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_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."""
|
||||
display_name = "Victory Condition"
|
||||
option_the_end = 0
|
||||
option_the_master = 1
|
||||
|
@ -75,9 +78,10 @@ class Level2Requirement(Range):
|
|||
"""The number of panel solves required to unlock LEVEL 2.
|
||||
In the base game, 223 are needed.
|
||||
Note that this count includes ANOTHER TRY.
|
||||
When set to 1, the panel hunt is disabled, and you can access LEVEL 2 for free.
|
||||
"""
|
||||
display_name = "Level 2 Requirement"
|
||||
range_start = 2
|
||||
range_start = 1
|
||||
range_end = 800
|
||||
default = 223
|
||||
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
from typing import Dict, List, NamedTuple, Optional, TYPE_CHECKING
|
||||
from typing import Dict, List, NamedTuple, Optional, Set, Tuple, TYPE_CHECKING
|
||||
|
||||
from .items import ALL_ITEM_TABLE
|
||||
from .locations import ALL_LOCATION_TABLE, LocationClassification
|
||||
from .options import LocationChecks, ShuffleDoors, VictoryCondition
|
||||
from .static_logic import DOORS_BY_ROOM, Door, PAINTINGS, PAINTINGS_BY_ROOM, PAINTING_ENTRANCES, PAINTING_EXITS, \
|
||||
PANELS_BY_ROOM, PROGRESSION_BY_ROOM, REQUIRED_PAINTING_ROOMS, REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS, ROOMS, \
|
||||
PANELS_BY_ROOM, PROGRESSION_BY_ROOM, REQUIRED_PAINTING_ROOMS, REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS, RoomAndDoor, \
|
||||
RoomAndPanel
|
||||
from .testing import LingoTestOptions
|
||||
|
||||
|
@ -12,10 +12,29 @@ if TYPE_CHECKING:
|
|||
from . import LingoWorld
|
||||
|
||||
|
||||
class AccessRequirements:
|
||||
rooms: Set[str]
|
||||
doors: Set[RoomAndDoor]
|
||||
colors: Set[str]
|
||||
|
||||
def __init__(self):
|
||||
self.rooms = set()
|
||||
self.doors = set()
|
||||
self.colors = set()
|
||||
|
||||
def merge(self, other: "AccessRequirements"):
|
||||
self.rooms |= other.rooms
|
||||
self.doors |= other.doors
|
||||
self.colors |= other.colors
|
||||
|
||||
def __str__(self):
|
||||
return f"AccessRequirements(rooms={self.rooms}, doors={self.doors}, colors={self.colors})"
|
||||
|
||||
|
||||
class PlayerLocation(NamedTuple):
|
||||
name: str
|
||||
code: Optional[int] = None
|
||||
panels: List[RoomAndPanel] = []
|
||||
code: Optional[int]
|
||||
access: AccessRequirements
|
||||
|
||||
|
||||
class LingoPlayerLogic:
|
||||
|
@ -23,27 +42,45 @@ class LingoPlayerLogic:
|
|||
Defines logic after a player's options have been applied
|
||||
"""
|
||||
|
||||
ITEM_BY_DOOR: Dict[str, Dict[str, str]]
|
||||
item_by_door: Dict[str, Dict[str, str]]
|
||||
|
||||
LOCATIONS_BY_ROOM: Dict[str, List[PlayerLocation]]
|
||||
REAL_LOCATIONS: List[str]
|
||||
locations_by_room: Dict[str, List[PlayerLocation]]
|
||||
real_locations: List[str]
|
||||
|
||||
EVENT_LOC_TO_ITEM: Dict[str, str]
|
||||
REAL_ITEMS: List[str]
|
||||
event_loc_to_item: Dict[str, str]
|
||||
real_items: List[str]
|
||||
|
||||
VICTORY_CONDITION: str
|
||||
MASTERY_LOCATION: str
|
||||
LEVEL_2_LOCATION: str
|
||||
victory_condition: str
|
||||
mastery_location: str
|
||||
level_2_location: str
|
||||
|
||||
PAINTING_MAPPING: Dict[str, str]
|
||||
painting_mapping: Dict[str, str]
|
||||
|
||||
FORCED_GOOD_ITEM: str
|
||||
forced_good_item: str
|
||||
|
||||
def add_location(self, room: str, loc: PlayerLocation):
|
||||
self.LOCATIONS_BY_ROOM.setdefault(room, []).append(loc)
|
||||
panel_reqs: Dict[str, Dict[str, AccessRequirements]]
|
||||
door_reqs: Dict[str, Dict[str, AccessRequirements]]
|
||||
mastery_reqs: List[AccessRequirements]
|
||||
counting_panel_reqs: Dict[str, List[Tuple[AccessRequirements, int]]]
|
||||
|
||||
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
|
||||
flattening the requirements for each of the given panels.
|
||||
"""
|
||||
access_reqs = AccessRequirements()
|
||||
for panel in panels:
|
||||
if panel.room is not None and panel.room != room:
|
||||
access_reqs.rooms.add(panel.room)
|
||||
|
||||
panel_room = room if panel.room is None else panel.room
|
||||
sub_access_reqs = self.calculate_panel_requirements(panel_room, panel.panel, world)
|
||||
access_reqs.merge(sub_access_reqs)
|
||||
|
||||
self.locations_by_room.setdefault(room, []).append(PlayerLocation(name, code, access_reqs))
|
||||
|
||||
def set_door_item(self, room: str, door: str, item: str):
|
||||
self.ITEM_BY_DOOR.setdefault(room, {})[door] = item
|
||||
self.item_by_door.setdefault(room, {})[door] = item
|
||||
|
||||
def handle_non_grouped_door(self, room_name: str, door_data: Door, world: "LingoWorld"):
|
||||
if room_name in PROGRESSION_BY_ROOM and door_data.name in PROGRESSION_BY_ROOM[room_name]:
|
||||
|
@ -52,21 +89,25 @@ class LingoPlayerLogic:
|
|||
else:
|
||||
progressive_item_name = PROGRESSION_BY_ROOM[room_name][door_data.name].item_name
|
||||
self.set_door_item(room_name, door_data.name, progressive_item_name)
|
||||
self.REAL_ITEMS.append(progressive_item_name)
|
||||
self.real_items.append(progressive_item_name)
|
||||
else:
|
||||
self.set_door_item(room_name, door_data.name, door_data.item_name)
|
||||
|
||||
def __init__(self, world: "LingoWorld"):
|
||||
self.ITEM_BY_DOOR = {}
|
||||
self.LOCATIONS_BY_ROOM = {}
|
||||
self.REAL_LOCATIONS = []
|
||||
self.EVENT_LOC_TO_ITEM = {}
|
||||
self.REAL_ITEMS = []
|
||||
self.VICTORY_CONDITION = ""
|
||||
self.MASTERY_LOCATION = ""
|
||||
self.LEVEL_2_LOCATION = ""
|
||||
self.PAINTING_MAPPING = {}
|
||||
self.FORCED_GOOD_ITEM = ""
|
||||
self.item_by_door = {}
|
||||
self.locations_by_room = {}
|
||||
self.real_locations = []
|
||||
self.event_loc_to_item = {}
|
||||
self.real_items = []
|
||||
self.victory_condition = ""
|
||||
self.mastery_location = ""
|
||||
self.level_2_location = ""
|
||||
self.painting_mapping = {}
|
||||
self.forced_good_item = ""
|
||||
self.panel_reqs = {}
|
||||
self.door_reqs = {}
|
||||
self.mastery_reqs = []
|
||||
self.counting_panel_reqs = {}
|
||||
|
||||
door_shuffle = world.options.shuffle_doors
|
||||
color_shuffle = world.options.shuffle_colors
|
||||
|
@ -79,17 +120,10 @@ class LingoPlayerLogic:
|
|||
raise Exception("You cannot have reduced location checks when door shuffle is on, because there would not "
|
||||
"be enough locations for all of the door items.")
|
||||
|
||||
# Create an event for every door, representing whether that door has been opened. Also create event items for
|
||||
# doors that are event-only.
|
||||
for room_name, room_data in DOORS_BY_ROOM.items():
|
||||
for door_name, door_data in room_data.items():
|
||||
if door_shuffle == ShuffleDoors.option_none:
|
||||
itemloc_name = f"{room_name} - {door_name} (Opened)"
|
||||
self.add_location(room_name, PlayerLocation(itemloc_name, None, door_data.panels))
|
||||
self.EVENT_LOC_TO_ITEM[itemloc_name] = itemloc_name
|
||||
self.set_door_item(room_name, door_name, itemloc_name)
|
||||
else:
|
||||
# This line is duplicated from StaticLingoItems
|
||||
# Create door items, where needed.
|
||||
if door_shuffle != ShuffleDoors.option_none:
|
||||
for room_name, room_data in DOORS_BY_ROOM.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.group is not None and door_shuffle == ShuffleDoors.option_simple:
|
||||
# Grouped doors are handled differently if shuffle doors is on simple.
|
||||
|
@ -97,49 +131,44 @@ class LingoPlayerLogic:
|
|||
else:
|
||||
self.handle_non_grouped_door(room_name, door_data, world)
|
||||
|
||||
if door_data.event:
|
||||
self.add_location(room_name, PlayerLocation(door_data.item_name, None, door_data.panels))
|
||||
self.EVENT_LOC_TO_ITEM[door_data.item_name] = door_data.item_name + " (Opened)"
|
||||
self.set_door_item(room_name, door_name, door_data.item_name + " (Opened)")
|
||||
|
||||
# Create events for each achievement panel, so that we can determine when THE MASTER is accessible. We also
|
||||
# create events for each counting panel, so that we can determine when LEVEL 2 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 panel_name, panel_data in room_data.items():
|
||||
if panel_data.achievement:
|
||||
event_name = room_name + " - " + panel_name + " (Achieved)"
|
||||
self.add_location(room_name, PlayerLocation(event_name, None,
|
||||
[RoomAndPanel(room_name, panel_name)]))
|
||||
self.EVENT_LOC_TO_ITEM[event_name] = "Mastery Achievement"
|
||||
access_req = AccessRequirements()
|
||||
access_req.merge(self.calculate_panel_requirements(room_name, panel_name, world))
|
||||
access_req.rooms.add(room_name)
|
||||
|
||||
if not panel_data.non_counting and victory_condition == VictoryCondition.option_level_2:
|
||||
event_name = room_name + " - " + panel_name + " (Counted)"
|
||||
self.add_location(room_name, PlayerLocation(event_name, None,
|
||||
[RoomAndPanel(room_name, panel_name)]))
|
||||
self.EVENT_LOC_TO_ITEM[event_name] = "Counting Panel Solved"
|
||||
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"
|
||||
self.LEVEL_2_LOCATION = "N/A"
|
||||
self.mastery_location = "Orange Tower Seventh Floor - THE MASTER"
|
||||
self.level_2_location = "Second Room - LEVEL 2"
|
||||
|
||||
if victory_condition == VictoryCondition.option_the_end:
|
||||
self.VICTORY_CONDITION = "Orange Tower Seventh Floor - THE END"
|
||||
self.add_location("Orange Tower Seventh Floor", PlayerLocation("The End (Solved)"))
|
||||
self.EVENT_LOC_TO_ITEM["The End (Solved)"] = "Victory"
|
||||
self.victory_condition = "Orange Tower Seventh Floor - THE END"
|
||||
self.add_location("Orange Tower Seventh Floor", "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"
|
||||
self.MASTERY_LOCATION = "Orange Tower Seventh Floor - Mastery Achievements"
|
||||
self.victory_condition = "Orange Tower Seventh Floor - THE MASTER"
|
||||
self.mastery_location = "Orange Tower Seventh Floor - Mastery Achievements"
|
||||
|
||||
self.add_location("Orange Tower Seventh Floor", PlayerLocation(self.MASTERY_LOCATION, None, []))
|
||||
self.EVENT_LOC_TO_ITEM[self.MASTERY_LOCATION] = "Victory"
|
||||
self.add_location("Orange Tower Seventh Floor", self.mastery_location, None, [], world)
|
||||
self.event_loc_to_item[self.mastery_location] = "Victory"
|
||||
elif victory_condition == VictoryCondition.option_level_2:
|
||||
self.VICTORY_CONDITION = "Second Room - LEVEL 2"
|
||||
self.LEVEL_2_LOCATION = "Second Room - Unlock Level 2"
|
||||
self.victory_condition = "Second Room - LEVEL 2"
|
||||
self.level_2_location = "Second Room - Unlock Level 2"
|
||||
|
||||
self.add_location("Second Room", PlayerLocation(self.LEVEL_2_LOCATION, None,
|
||||
[RoomAndPanel("Second Room", "LEVEL 2")]))
|
||||
self.EVENT_LOC_TO_ITEM[self.LEVEL_2_LOCATION] = "Victory"
|
||||
self.add_location("Second Room", self.level_2_location, None, [RoomAndPanel("Second Room", "LEVEL 2")],
|
||||
world)
|
||||
self.event_loc_to_item[self.level_2_location] = "Victory"
|
||||
|
||||
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.")
|
||||
|
||||
# Create groups of counting panel access requirements for the LEVEL 2 check.
|
||||
self.create_panel_hunt_events(world)
|
||||
|
||||
# Instantiate all real locations.
|
||||
location_classification = LocationClassification.normal
|
||||
|
@ -149,18 +178,17 @@ class LingoPlayerLogic:
|
|||
location_classification = LocationClassification.insanity
|
||||
|
||||
for location_name, location_data in ALL_LOCATION_TABLE.items():
|
||||
if location_name != self.VICTORY_CONDITION:
|
||||
if location_name != self.victory_condition:
|
||||
if location_classification not in location_data.classification:
|
||||
continue
|
||||
|
||||
self.add_location(location_data.room, PlayerLocation(location_name, location_data.code,
|
||||
location_data.panels))
|
||||
self.REAL_LOCATIONS.append(location_name)
|
||||
self.add_location(location_data.room, location_name, location_data.code, location_data.panels, world)
|
||||
self.real_locations.append(location_name)
|
||||
|
||||
# Instantiate all real items.
|
||||
for name, item in ALL_ITEM_TABLE.items():
|
||||
if item.should_include(world):
|
||||
self.REAL_ITEMS.append(name)
|
||||
self.real_items.append(name)
|
||||
|
||||
# Create the paintings mapping, if painting shuffle is on.
|
||||
if painting_shuffle:
|
||||
|
@ -201,7 +229,7 @@ class LingoPlayerLogic:
|
|||
continue
|
||||
|
||||
# If painting shuffle is on, we only want to consider paintings that actually go somewhere.
|
||||
if painting_shuffle and painting_obj.id not in self.PAINTING_MAPPING.keys():
|
||||
if painting_shuffle and painting_obj.id not in self.painting_mapping.keys():
|
||||
continue
|
||||
|
||||
pdoor = DOORS_BY_ROOM[painting_obj.required_door.room][painting_obj.required_door.door]
|
||||
|
@ -226,12 +254,12 @@ class LingoPlayerLogic:
|
|||
good_item_options.remove(item)
|
||||
|
||||
if len(good_item_options) > 0:
|
||||
self.FORCED_GOOD_ITEM = world.random.choice(good_item_options)
|
||||
self.REAL_ITEMS.remove(self.FORCED_GOOD_ITEM)
|
||||
self.REAL_LOCATIONS.remove("Second Room - Good Luck")
|
||||
self.forced_good_item = world.random.choice(good_item_options)
|
||||
self.real_items.remove(self.forced_good_item)
|
||||
self.real_locations.remove("Second Room - Good Luck")
|
||||
|
||||
def randomize_paintings(self, world: "LingoWorld") -> bool:
|
||||
self.PAINTING_MAPPING.clear()
|
||||
self.painting_mapping.clear()
|
||||
|
||||
door_shuffle = world.options.shuffle_doors
|
||||
|
||||
|
@ -253,7 +281,7 @@ class LingoPlayerLogic:
|
|||
if painting.exit_only and painting.required]
|
||||
req_entrances = world.random.sample(req_enterable, len(req_exits))
|
||||
|
||||
self.PAINTING_MAPPING = dict(zip(req_entrances, req_exits))
|
||||
self.painting_mapping = dict(zip(req_entrances, req_exits))
|
||||
|
||||
# Next, determine the rest of the exit paintings.
|
||||
exitable = [painting_id for painting_id, painting in PAINTINGS.items()
|
||||
|
@ -272,25 +300,125 @@ class LingoPlayerLogic:
|
|||
for warp_exit in nonreq_exits:
|
||||
warp_enter = world.random.choice(chosen_entrances)
|
||||
chosen_entrances.remove(warp_enter)
|
||||
self.PAINTING_MAPPING[warp_enter] = warp_exit
|
||||
self.painting_mapping[warp_enter] = warp_exit
|
||||
|
||||
# Assign each of the remaining entrances to any required or non-required exit.
|
||||
for warp_enter in chosen_entrances:
|
||||
warp_exit = world.random.choice(chosen_exits)
|
||||
self.PAINTING_MAPPING[warp_enter] = warp_exit
|
||||
self.painting_mapping[warp_enter] = warp_exit
|
||||
|
||||
# The Eye Wall painting is unique in that it is both double-sided and also enter only (because it moves).
|
||||
# There is only one eligible double-sided exit painting, which is the vanilla exit for this warp. If the
|
||||
# exit painting is an entrance in the shuffle, we will disable the Eye Wall painting. Otherwise, Eye Wall
|
||||
# is forced to point to the vanilla exit.
|
||||
if "eye_painting_2" not in self.PAINTING_MAPPING.keys():
|
||||
self.PAINTING_MAPPING["eye_painting"] = "eye_painting_2"
|
||||
if "eye_painting_2" not in self.painting_mapping.keys():
|
||||
self.painting_mapping["eye_painting"] = "eye_painting_2"
|
||||
|
||||
# Just for sanity's sake, ensure that all required painting rooms are accessed.
|
||||
for painting_id, painting in PAINTINGS.items():
|
||||
if painting_id not in self.PAINTING_MAPPING.values() \
|
||||
if painting_id not in self.painting_mapping.values() \
|
||||
and (painting.required or (painting.required_when_no_doors and
|
||||
door_shuffle == ShuffleDoors.option_none)):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def calculate_panel_requirements(self, room: str, panel: str, world: "LingoWorld"):
|
||||
"""
|
||||
Calculate and return the access requirements for solving a given panel. The goal is to eliminate recursion in
|
||||
the access rule function by collecting the rooms, doors, and colors needed by this panel and any panel required
|
||||
by this panel. Memoization is used so that no panel is evaluated more than once.
|
||||
"""
|
||||
if panel not in self.panel_reqs.setdefault(room, {}):
|
||||
access_reqs = AccessRequirements()
|
||||
panel_object = PANELS_BY_ROOM[room][panel]
|
||||
|
||||
for req_room in panel_object.required_rooms:
|
||||
access_reqs.rooms.add(req_room)
|
||||
|
||||
for req_door in panel_object.required_doors:
|
||||
door_object = DOORS_BY_ROOM[room if req_door.room is None else req_door.room][req_door.door]
|
||||
if door_object.event or world.options.shuffle_doors == ShuffleDoors.option_none:
|
||||
sub_access_reqs = self.calculate_door_requirements(
|
||||
room if req_door.room is None else req_door.room, req_door.door, world)
|
||||
access_reqs.merge(sub_access_reqs)
|
||||
else:
|
||||
access_reqs.doors.add(RoomAndDoor(room if req_door.room is None else req_door.room, req_door.door))
|
||||
|
||||
for color in panel_object.colors:
|
||||
access_reqs.colors.add(color)
|
||||
|
||||
for req_panel in panel_object.required_panels:
|
||||
if req_panel.room is not None and req_panel.room != room:
|
||||
access_reqs.rooms.add(req_panel.room)
|
||||
|
||||
sub_access_reqs = self.calculate_panel_requirements(room if req_panel.room is None else req_panel.room,
|
||||
req_panel.panel, world)
|
||||
access_reqs.merge(sub_access_reqs)
|
||||
|
||||
self.panel_reqs[room][panel] = access_reqs
|
||||
|
||||
return self.panel_reqs[room][panel]
|
||||
|
||||
def calculate_door_requirements(self, room: str, door: str, world: "LingoWorld"):
|
||||
"""
|
||||
Similar to calculate_panel_requirements, but for event doors.
|
||||
"""
|
||||
if door not in self.door_reqs.setdefault(room, {}):
|
||||
access_reqs = AccessRequirements()
|
||||
door_object = DOORS_BY_ROOM[room][door]
|
||||
|
||||
for req_panel in door_object.panels:
|
||||
if req_panel.room is not None and req_panel.room != room:
|
||||
access_reqs.rooms.add(req_panel.room)
|
||||
|
||||
sub_access_reqs = self.calculate_panel_requirements(room if req_panel.room is None else req_panel.room,
|
||||
req_panel.panel, world)
|
||||
access_reqs.merge(sub_access_reqs)
|
||||
|
||||
self.door_reqs[room][door] = access_reqs
|
||||
|
||||
return self.door_reqs[room][door]
|
||||
|
||||
def create_panel_hunt_events(self, world: "LingoWorld"):
|
||||
"""
|
||||
Creates the event locations/items used for determining access to the LEVEL 2 panel. Instead of creating an event
|
||||
for every single counting panel in the game, we try to coalesce panels with identical access rules into the same
|
||||
event. Right now, this means the following:
|
||||
|
||||
When color shuffle is off, panels in a room with no extra access requirements (room, door, or other panel) are
|
||||
all coalesced into one event.
|
||||
|
||||
When color shuffle is on, single-colored panels (including white) in a room are combined into one event per
|
||||
color. Multicolored panels and panels with any extra access requirements are not coalesced, and will each
|
||||
receive their own event.
|
||||
"""
|
||||
for room_name, room_data in PANELS_BY_ROOM.items():
|
||||
unhindered_panels_by_color: dict[Optional[str], int] = {}
|
||||
|
||||
for panel_name, panel_data in room_data.items():
|
||||
# We won't count non-counting panels.
|
||||
if panel_data.non_counting:
|
||||
continue
|
||||
|
||||
# We won't coalesce any panels that have requirements beyond colors. To simplify things for now, we will
|
||||
# only coalesce single-color panels. Chains/stacks/combo puzzles will be separate.
|
||||
if len(panel_data.required_panels) > 0 or len(panel_data.required_doors) > 0\
|
||||
or len(panel_data.required_rooms) > 0\
|
||||
or (world.options.shuffle_colors and len(panel_data.colors) > 1):
|
||||
self.counting_panel_reqs.setdefault(room_name, []).append(
|
||||
(self.calculate_panel_requirements(room_name, panel_name, world), 1))
|
||||
else:
|
||||
if len(panel_data.colors) == 0 or not world.options.shuffle_colors:
|
||||
color = None
|
||||
else:
|
||||
color = panel_data.colors[0]
|
||||
|
||||
unhindered_panels_by_color[color] = unhindered_panels_by_color.get(color, 0) + 1
|
||||
|
||||
for color, panel_count in unhindered_panels_by_color.items():
|
||||
access_reqs = AccessRequirements()
|
||||
if color is not None:
|
||||
access_reqs.colors.add(color)
|
||||
|
||||
self.counting_panel_reqs.setdefault(room_name, []).append((access_reqs, panel_count))
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
from typing import Dict, TYPE_CHECKING
|
||||
from typing import Dict, Optional, TYPE_CHECKING
|
||||
|
||||
from BaseClasses import ItemClassification, Region
|
||||
from BaseClasses import Entrance, ItemClassification, Region
|
||||
from .items import LingoItem
|
||||
from .locations import LingoLocation
|
||||
from .player_logic import LingoPlayerLogic
|
||||
from .rules import lingo_can_use_entrance, lingo_can_use_pilgrimage, make_location_lambda
|
||||
from .static_logic import ALL_ROOMS, PAINTINGS, Room
|
||||
from .static_logic import ALL_ROOMS, PAINTINGS, Room, RoomAndDoor
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import LingoWorld
|
||||
|
@ -13,12 +13,12 @@ if TYPE_CHECKING:
|
|||
|
||||
def create_region(room: Room, world: "LingoWorld", player_logic: LingoPlayerLogic) -> Region:
|
||||
new_region = Region(room.name, world.player, world.multiworld)
|
||||
for location in player_logic.LOCATIONS_BY_ROOM.get(room.name, {}):
|
||||
for location in player_logic.locations_by_room.get(room.name, {}):
|
||||
new_location = LingoLocation(world.player, location.name, location.code, new_region)
|
||||
new_location.access_rule = make_location_lambda(location, room.name, world, player_logic)
|
||||
new_location.access_rule = make_location_lambda(location, world, player_logic)
|
||||
new_region.locations.append(new_location)
|
||||
if location.name in player_logic.EVENT_LOC_TO_ITEM:
|
||||
event_name = player_logic.EVENT_LOC_TO_ITEM[location.name]
|
||||
if location.name in player_logic.event_loc_to_item:
|
||||
event_name = player_logic.event_loc_to_item[location.name]
|
||||
event_item = LingoItem(event_name, ItemClassification.progression, None, world.player)
|
||||
new_location.place_locked_item(event_item)
|
||||
|
||||
|
@ -31,7 +31,22 @@ def handle_pilgrim_room(regions: Dict[str, Region], world: "LingoWorld", player_
|
|||
source_region.connect(
|
||||
target_region,
|
||||
"Pilgrimage",
|
||||
lambda state: lingo_can_use_pilgrimage(state, world.player, player_logic))
|
||||
lambda state: lingo_can_use_pilgrimage(state, world, player_logic))
|
||||
|
||||
|
||||
def connect_entrance(regions: Dict[str, Region], source_region: Region, target_region: Region, description: str,
|
||||
door: Optional[RoomAndDoor], world: "LingoWorld", player_logic: LingoPlayerLogic):
|
||||
connection = Entrance(world.player, description, source_region)
|
||||
connection.access_rule = lambda state: lingo_can_use_entrance(state, target_region.name, door, world, player_logic)
|
||||
|
||||
source_region.exits.append(connection)
|
||||
connection.connect(target_region)
|
||||
|
||||
if door is not None:
|
||||
effective_room = target_region.name if door.room is None else door.room
|
||||
if door.door not in player_logic.item_by_door.get(effective_room, {}):
|
||||
for region in player_logic.calculate_door_requirements(effective_room, door.door, world).rooms:
|
||||
world.multiworld.register_indirect_condition(regions[region], connection)
|
||||
|
||||
|
||||
def connect_painting(regions: Dict[str, Region], warp_enter: str, warp_exit: str, world: "LingoWorld",
|
||||
|
@ -41,11 +56,10 @@ def connect_painting(regions: Dict[str, Region], warp_enter: str, warp_exit: str
|
|||
|
||||
target_region = regions[target_painting.room]
|
||||
source_region = regions[source_painting.room]
|
||||
source_region.connect(
|
||||
target_region,
|
||||
f"{source_painting.room} to {target_painting.room} ({source_painting.id} Painting)",
|
||||
lambda state: lingo_can_use_entrance(state, target_painting.room, source_painting.required_door, world.player,
|
||||
player_logic))
|
||||
|
||||
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,
|
||||
player_logic)
|
||||
|
||||
|
||||
def create_regions(world: "LingoWorld", player_logic: LingoPlayerLogic) -> None:
|
||||
|
@ -74,10 +88,8 @@ def create_regions(world: "LingoWorld", player_logic: LingoPlayerLogic) -> None:
|
|||
else:
|
||||
entrance_name += f" (through {room.name} - {entrance.door.door})"
|
||||
|
||||
regions[entrance.room].connect(
|
||||
regions[room.name], entrance_name,
|
||||
lambda state, r=room, e=entrance: lingo_can_use_entrance(state, r.name, e.door, world.player,
|
||||
player_logic))
|
||||
connect_entrance(regions, regions[entrance.room], regions[room.name], entrance_name, entrance.door, world,
|
||||
player_logic)
|
||||
|
||||
handle_pilgrim_room(regions, world, player_logic)
|
||||
|
||||
|
@ -85,7 +97,7 @@ def create_regions(world: "LingoWorld", player_logic: LingoPlayerLogic) -> None:
|
|||
regions["Starting Room"].connect(regions["Outside The Undeterred"], "Early Color Hallways")
|
||||
|
||||
if painting_shuffle:
|
||||
for warp_enter, warp_exit in player_logic.PAINTING_MAPPING.items():
|
||||
for warp_enter, warp_exit in player_logic.painting_mapping.items():
|
||||
connect_painting(regions, warp_enter, warp_exit, world, player_logic)
|
||||
|
||||
world.multiworld.regions += regions.values()
|
||||
|
|
|
@ -1,23 +1,23 @@
|
|||
from typing import TYPE_CHECKING
|
||||
|
||||
from BaseClasses import CollectionState
|
||||
from .options import VictoryCondition
|
||||
from .player_logic import LingoPlayerLogic, PlayerLocation
|
||||
from .static_logic import PANELS_BY_ROOM, PROGRESSION_BY_ROOM, PROGRESSIVE_ITEMS, RoomAndDoor
|
||||
from .player_logic import AccessRequirements, LingoPlayerLogic, PlayerLocation
|
||||
from .static_logic import PROGRESSION_BY_ROOM, PROGRESSIVE_ITEMS, RoomAndDoor
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import LingoWorld
|
||||
|
||||
|
||||
def lingo_can_use_entrance(state: CollectionState, room: str, door: RoomAndDoor, player: int,
|
||||
def lingo_can_use_entrance(state: CollectionState, room: str, door: RoomAndDoor, world: "LingoWorld",
|
||||
player_logic: LingoPlayerLogic):
|
||||
if door is None:
|
||||
return True
|
||||
|
||||
return _lingo_can_open_door(state, room, room if door.room is None else door.room, door.door, player, player_logic)
|
||||
effective_room = room if door.room is None else door.room
|
||||
return _lingo_can_open_door(state, effective_room, door.door, world, player_logic)
|
||||
|
||||
|
||||
def lingo_can_use_pilgrimage(state: CollectionState, player: int, player_logic: LingoPlayerLogic):
|
||||
def lingo_can_use_pilgrimage(state: CollectionState, world: "LingoWorld", player_logic: LingoPlayerLogic):
|
||||
fake_pilgrimage = [
|
||||
["Second Room", "Exit Door"], ["Crossroads", "Tower Entrance"],
|
||||
["Orange Tower Fourth Floor", "Hot Crusts Door"], ["Outside The Initiated", "Shortcut to Hub Room"],
|
||||
|
@ -28,77 +28,77 @@ def lingo_can_use_pilgrimage(state: CollectionState, player: int, player_logic:
|
|||
["Outside The Agreeable", "Tenacious Entrance"]
|
||||
]
|
||||
for entrance in fake_pilgrimage:
|
||||
if not state.has(player_logic.ITEM_BY_DOOR[entrance[0]][entrance[1]], player):
|
||||
if not _lingo_can_open_door(state, entrance[0], entrance[1], world, player_logic):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def lingo_can_use_location(state: CollectionState, location: PlayerLocation, room_name: str, world: "LingoWorld",
|
||||
def lingo_can_use_location(state: CollectionState, location: PlayerLocation, world: "LingoWorld",
|
||||
player_logic: LingoPlayerLogic):
|
||||
for panel in location.panels:
|
||||
panel_room = room_name if panel.room is None else panel.room
|
||||
if not _lingo_can_solve_panel(state, room_name, panel_room, panel.panel, world, player_logic):
|
||||
return False
|
||||
|
||||
return True
|
||||
return _lingo_can_satisfy_requirements(state, location.access, world, player_logic)
|
||||
|
||||
|
||||
def lingo_can_use_mastery_location(state: CollectionState, world: "LingoWorld"):
|
||||
return state.has("Mastery Achievement", world.player, world.options.mastery_achievements.value)
|
||||
def lingo_can_use_mastery_location(state: CollectionState, world: "LingoWorld", player_logic: LingoPlayerLogic):
|
||||
satisfied_count = 0
|
||||
for access_req in player_logic.mastery_reqs:
|
||||
if _lingo_can_satisfy_requirements(state, access_req, world, player_logic):
|
||||
satisfied_count += 1
|
||||
return satisfied_count >= world.options.mastery_achievements.value
|
||||
|
||||
|
||||
def _lingo_can_open_door(state: CollectionState, start_room: str, room: str, door: str, player: int,
|
||||
player_logic: LingoPlayerLogic):
|
||||
"""
|
||||
Determines whether a door can be opened
|
||||
"""
|
||||
item_name = player_logic.ITEM_BY_DOOR[room][door]
|
||||
if item_name in PROGRESSIVE_ITEMS:
|
||||
progression = PROGRESSION_BY_ROOM[room][door]
|
||||
return state.has(item_name, player, progression.index)
|
||||
|
||||
return state.has(item_name, player)
|
||||
def lingo_can_use_level_2_location(state: CollectionState, world: "LingoWorld", player_logic: LingoPlayerLogic):
|
||||
counted_panels = 0
|
||||
state.update_reachable_regions(world.player)
|
||||
for region in state.reachable_regions[world.player]:
|
||||
for access_req, panel_count in player_logic.counting_panel_reqs.get(region.name, []):
|
||||
if _lingo_can_satisfy_requirements(state, access_req, world, player_logic):
|
||||
counted_panels += panel_count
|
||||
if counted_panels >= world.options.level_2_requirement.value - 1:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _lingo_can_solve_panel(state: CollectionState, start_room: str, room: str, panel: str, world: "LingoWorld",
|
||||
player_logic: LingoPlayerLogic):
|
||||
"""
|
||||
Determines whether a panel can be solved
|
||||
"""
|
||||
if start_room != room and not state.can_reach(room, "Region", world.player):
|
||||
return False
|
||||
|
||||
if room == "Second Room" and panel == "ANOTHER TRY" \
|
||||
and world.options.victory_condition == VictoryCondition.option_level_2 \
|
||||
and not state.has("Counting Panel Solved", world.player, world.options.level_2_requirement.value - 1):
|
||||
return False
|
||||
|
||||
panel_object = PANELS_BY_ROOM[room][panel]
|
||||
for req_room in panel_object.required_rooms:
|
||||
def _lingo_can_satisfy_requirements(state: CollectionState, access: AccessRequirements, world: "LingoWorld",
|
||||
player_logic: LingoPlayerLogic):
|
||||
for req_room in access.rooms:
|
||||
if not state.can_reach(req_room, "Region", world.player):
|
||||
return False
|
||||
|
||||
for req_door in panel_object.required_doors:
|
||||
if not _lingo_can_open_door(state, start_room, room if req_door.room is None else req_door.room,
|
||||
req_door.door, world.player, player_logic):
|
||||
for req_door in access.doors:
|
||||
if not _lingo_can_open_door(state, req_door.room, req_door.door, world, player_logic):
|
||||
return False
|
||||
|
||||
for req_panel in panel_object.required_panels:
|
||||
if not _lingo_can_solve_panel(state, start_room, room if req_panel.room is None else req_panel.room,
|
||||
req_panel.panel, world, player_logic):
|
||||
return False
|
||||
|
||||
if len(panel_object.colors) > 0 and world.options.shuffle_colors:
|
||||
for color in panel_object.colors:
|
||||
if len(access.colors) > 0 and world.options.shuffle_colors:
|
||||
for color in access.colors:
|
||||
if not state.has(color.capitalize(), world.player):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def make_location_lambda(location: PlayerLocation, room_name: str, world: "LingoWorld", player_logic: LingoPlayerLogic):
|
||||
if location.name == player_logic.MASTERY_LOCATION:
|
||||
return lambda state: lingo_can_use_mastery_location(state, world)
|
||||
def _lingo_can_open_door(state: CollectionState, room: str, door: str, world: "LingoWorld",
|
||||
player_logic: LingoPlayerLogic):
|
||||
"""
|
||||
Determines whether a door can be opened
|
||||
"""
|
||||
if door not in player_logic.item_by_door.get(room, {}):
|
||||
return _lingo_can_satisfy_requirements(state, player_logic.door_reqs[room][door], world, player_logic)
|
||||
|
||||
return lambda state: lingo_can_use_location(state, location, room_name, world, player_logic)
|
||||
item_name = player_logic.item_by_door[room][door]
|
||||
if item_name in PROGRESSIVE_ITEMS:
|
||||
progression = PROGRESSION_BY_ROOM[room][door]
|
||||
return state.has(item_name, world.player, progression.index)
|
||||
|
||||
return state.has(item_name, world.player)
|
||||
|
||||
|
||||
def make_location_lambda(location: PlayerLocation, world: "LingoWorld", player_logic: LingoPlayerLogic):
|
||||
if location.name == player_logic.mastery_location:
|
||||
return lambda state: lingo_can_use_mastery_location(state, world, player_logic)
|
||||
|
||||
if world.options.level_2_requirement > 1\
|
||||
and (location.name == "Second Room - ANOTHER TRY" or location.name == player_logic.level_2_location):
|
||||
return lambda state: lingo_can_use_level_2_location(state, world, player_logic)
|
||||
|
||||
return lambda state: lingo_can_use_location(state, location, world, player_logic)
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
from . import LingoTestBase
|
||||
|
||||
|
||||
class TestPanelHunt(LingoTestBase):
|
||||
options = {
|
||||
"shuffle_doors": "complex",
|
||||
"location_checks": "insanity",
|
||||
"victory_condition": "level_2",
|
||||
"level_2_requirement": "15"
|
||||
}
|
||||
|
||||
def test_another_try(self) -> None:
|
||||
self.collect_by_name("The Traveled - Entrance") # idk why this is needed
|
||||
self.assertFalse(self.can_reach_location("Second Room - ANOTHER TRY"))
|
||||
self.assertFalse(self.can_reach_location("Second Room - Unlock Level 2"))
|
||||
|
||||
self.collect_by_name("Second Room - Exit Door")
|
||||
self.assertTrue(self.can_reach_location("Second Room - ANOTHER TRY"))
|
||||
self.assertTrue(self.can_reach_location("Second Room - Unlock Level 2"))
|
Loading…
Reference in New Issue