Skip to content

Commit

Permalink
Lingo: Various generation optimizations (#2479)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
hatkirby authored Nov 25, 2023
1 parent 8a852ab commit 6dccf36
Show file tree
Hide file tree
Showing 7 changed files with 330 additions and 169 deletions.
12 changes: 6 additions & 6 deletions worlds/lingo/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,14 @@ def create_regions(self):
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)
Expand Down Expand Up @@ -93,7 +93,7 @@ def create_item(self, name: str) -> Item:

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.
Expand All @@ -116,6 +116,6 @@ def fill_slot_data(self):
}

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
2 changes: 0 additions & 2 deletions worlds/lingo/data/LL1.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 6 additions & 2 deletions worlds/lingo/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
298 changes: 213 additions & 85 deletions worlds/lingo/player_logic.py

Large diffs are not rendered by default.

48 changes: 30 additions & 18 deletions worlds/lingo/regions.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
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


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)

Expand All @@ -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",
Expand All @@ -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:
Expand Down Expand Up @@ -74,18 +88,16 @@ 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)

if early_color_hallways:
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()
112 changes: 56 additions & 56 deletions worlds/lingo/rules.py
Original file line number Diff line number Diff line change
@@ -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"],
Expand All @@ -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_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_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_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
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

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):
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):
for req_door in access.doors:
if not _lingo_can_open_door(state, req_door.room, req_door.door, 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)

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, room_name, world, player_logic)
return lambda state: lingo_can_use_location(state, location, world, player_logic)
19 changes: 19 additions & 0 deletions worlds/lingo/test/TestPanelsanity.py
Original file line number Diff line number Diff line change
@@ -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"))

0 comments on commit 6dccf36

Please sign in to comment.