From 99128f67a3b4478a5561b1ba33a2f3588a61a1df Mon Sep 17 00:00:00 2001
From: Star Rauchenberger <fefferburbia@gmail.com>
Date: Sun, 10 Dec 2023 13:15:42 -0500
Subject: Lingo: Fix entrance checking being broken on default settings (#2506)

The most serious issue this PR addresses is that entrances that use doors without items (a small subset of doors when door shuffle is on, but *every* door when door shuffle is off, which is the default) underestimate the requirements needed to use that entrance. The logic would calculate the panels needed to open the door, but would neglect to keep track of the rooms those panels were in, meaning that doors would be considered openable if you had the colors needed to solve a panel that's in a room you have no access to.

Another issue is that, previously, logic would always consider the "ANOTHER TRY" panel accessible for the purposes of the LEVEL 2 panel hunt. This could result in seeds where the player is expected to have exactly the correct number of solves to reach LEVEL 2, but in reality is short by one because ANOTHER TRY itself is not revealed until the panel hunt is complete. This change marks ANOTHER TRY as non-counting, because even though it is technically a counting panel in-game, it can never contribute to the LEVEL 2 panel hunt. This issue could also apply to THE MASTER, since it is the only other counting panel with special access rules, although it is much less likely. This change adds special handling for counting THE MASTER. These issues were possible to manifest whenever the LEVEL 2 panel hunt was enabled, which it is by default.

Smaller logic issues also fixed in this PR:

* The Orange Tower Basement MASTERY panel was marked as requiring the mastery doors to be opened, when it was actually possible to get it without them by using a painting to get into the room.
* The Pilgrim Room painting item was incorrectly being marked as a filler item, despite it being progression.
* There has been another update to the game that adds connections between areas that were previously not connected. These changes were additive, which is why they are not critical.
* The panel stacks in the rhyme room now require both colours on each panel.
---
 __init__.py              |   6 ++-
 data/LL1.yaml            | 112 +++++++++++++++++++++++++++++++++++++----------
 data/ids.yaml            |   5 ++-
 player_logic.py          |  31 ++++++++++---
 regions.py               |  15 ++-----
 rules.py                 |  23 +++-------
 utils/validate_config.rb |   2 +-
 7 files changed, 131 insertions(+), 63 deletions(-)

diff --git a/__init__.py b/__init__.py
index a8dac86..f22d344 100644
--- a/__init__.py
+++ b/__init__.py
@@ -104,9 +104,11 @@ 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
-                                                    for painting_id in item.painting_ids):
+                                                    for painting_id in item.painting_ids)\
+                and "pilgrim_painting2" not 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.
+            # go nowhere, then this item should not be progression. The Pilgrim Room painting is special and needs to be
+            # excluded from this.
             classification = ItemClassification.filler
 
         return LingoItem(name, classification, item.code, self.player)
diff --git a/data/LL1.yaml b/data/LL1.yaml
index 8a4f831..ea5886f 100644
--- a/data/LL1.yaml
+++ b/data/LL1.yaml
@@ -373,6 +373,7 @@
       ANOTHER TRY:
         id: Entry Room/Panel_advance
         tag: topwhite
+        non_counting: True # This is a counting panel in-game, but it can never count towards the LEVEL 2 panel hunt.
       LEVEL 2:
         # We will set up special rules for this in code.
         id: EndPanel/Panel_level_2
@@ -1033,6 +1034,8 @@
       Hallway Room (3): True
       Hallway Room (4): True
       Hedge Maze: True # through the door to the sectioned-off part of the hedge maze
+      Cellar:
+        door: Lookout Entrance
     panels:
       MASSACRED:
         id: Palindrome Room/Panel_massacred_sacred
@@ -1168,11 +1171,21 @@
           - KEEP
           - BAILEY
           - TOWER
+      Lookout Entrance:
+        id: Cross Room Doors/Door_missing
+        location_name: Outside The Agreeable - Lookout Panels
+        panels:
+          - NORTH
+          - WINTER
+          - DIAMONDS
+          - FIRE
     paintings:
       - id: panda_painting
         orientation: south
       - id: eyes_yellow_painting
         orientation: east
+      - id: pencil_painting7
+        orientation: north
     progression:
       Progressive Hallway Room:
         - Hallway Door
@@ -2043,7 +2056,7 @@
         door: Sixth Floor
       Cellar:
         room: Room Room
-        door: Shortcut to Fifth Floor
+        door: Cellar Exit
       Welcome Back Area:
         door: Welcome Back
       Art Gallery:
@@ -2302,9 +2315,6 @@
         id: Master Room/Panel_mastery_mastery3
         tag: midwhite
         hunt: True
-        required_door:
-          room: Orange Tower Seventh Floor
-          door: Mastery
       THE LIBRARY:
         id: EndPanel/Panel_library
         check: True
@@ -2675,6 +2685,10 @@
       Outside The Undeterred: True
       Outside The Agreeable: True
       Outside The Wanderer: True
+      The Observant: True
+      Art Gallery: True
+      The Scientific: True
+      Cellar: True
       Orange Tower Fifth Floor:
         room: Orange Tower Fifth Floor
         door: Welcome Back
@@ -2991,8 +3005,7 @@
       PATS:
         id: Rhyme Room/Panel_wrath_path
         colors: purple
-        tag: midpurp and rhyme
-        copy_to_sign: sign15
+        tag: forbid
       KNIGHT:
         id: Rhyme Room/Panel_knight_write
         colors: purple
@@ -3158,6 +3171,8 @@
         door: Painting Shortcut
         painting: True
       Room Room: True # trapdoor
+      Outside The Agreeable:
+        painting: True
     panels:
       UNOPEN:
         id: Truncate Room/Panel_unopened_open
@@ -6299,17 +6314,22 @@
       SKELETON:
         id: Double Room/Panel_bones_syn
         tag: syn rhyme
+        colors: purple
         subtag: bot
         link: rhyme BONES
       REPENTANCE:
         id: Double Room/Panel_sentence_rhyme
-        colors: purple
+        colors:
+          - purple
+          - blue
         tag: whole rhyme
         subtag: top
         link: rhyme SENTENCE
       WORD:
         id: Double Room/Panel_sentence_whole
-        colors: blue
+        colors:
+          - purple
+          - blue
         tag: whole rhyme
         subtag: bot
         link: rhyme SENTENCE
@@ -6321,6 +6341,7 @@
         link: rhyme DREAM
       FANTASY:
         id: Double Room/Panel_dream_syn
+        colors: purple
         tag: syn rhyme
         subtag: bot
         link: rhyme DREAM
@@ -6332,6 +6353,7 @@
         link: rhyme MYSTERY
       SECRET:
         id: Double Room/Panel_mystery_syn
+        colors: purple
         tag: syn rhyme
         subtag: bot
         link: rhyme MYSTERY
@@ -6386,25 +6408,33 @@
           door: Nines
       FERN:
         id: Double Room/Panel_return_rhyme
-        colors: purple
+        colors:
+          - purple
+          - black
         tag: ant rhyme
         subtag: top
         link: rhyme RETURN
       STAY:
         id: Double Room/Panel_return_ant
-        colors: black
+        colors:
+          - purple
+          - black
         tag: ant rhyme
         subtag: bot
         link: rhyme RETURN
       FRIEND:
         id: Double Room/Panel_descend_rhyme
-        colors: purple
+        colors:
+          - purple
+          - black
         tag: ant rhyme
         subtag: top
         link: rhyme DESCEND
       RISE:
         id: Double Room/Panel_descend_ant
-        colors: black
+        colors:
+          - purple
+          - black
         tag: ant rhyme
         subtag: bot
         link: rhyme DESCEND
@@ -6416,6 +6446,7 @@
         link: rhyme JUMP
       BOUNCE:
         id: Double Room/Panel_jump_syn
+        colors: purple
         tag: syn rhyme
         subtag: bot
         link: rhyme JUMP
@@ -6427,6 +6458,7 @@
         link: rhyme FALL
       PLUNGE:
         id: Double Room/Panel_fall_syn
+        colors: purple
         tag: syn rhyme
         subtag: bot
         link: rhyme FALL
@@ -6456,13 +6488,17 @@
     panels:
       BIRD:
         id: Double Room/Panel_word_rhyme
-        colors: purple
+        colors:
+          - purple
+          - blue
         tag: whole rhyme
         subtag: top
         link: rhyme WORD
       LETTER:
         id: Double Room/Panel_word_whole
-        colors: blue
+        colors:
+          - purple
+          - blue
         tag: whole rhyme
         subtag: bot
         link: rhyme WORD
@@ -6474,6 +6510,7 @@
         link: rhyme HIDDEN
       CONCEALED:
         id: Double Room/Panel_hidden_syn
+        colors: purple
         tag: syn rhyme
         subtag: bot
         link: rhyme HIDDEN
@@ -6485,6 +6522,7 @@
         link: rhyme SILENT
       MUTE:
         id: Double Room/Panel_silent_syn
+        colors: purple
         tag: syn rhyme
         subtag: bot
         link: rhyme SILENT
@@ -6531,6 +6569,7 @@
         link: rhyme BLOCKED
       OBSTRUCTED:
         id: Double Room/Panel_blocked_syn
+        colors: purple
         tag: syn rhyme
         subtag: bot
         link: rhyme BLOCKED
@@ -6542,6 +6581,7 @@
         link: rhyme RISE
       SWELL:
         id: Double Room/Panel_rise_syn
+        colors: purple
         tag: syn rhyme
         subtag: bot
         link: rhyme RISE
@@ -6553,6 +6593,7 @@
         link: rhyme ASCEND
       CLIMB:
         id: Double Room/Panel_ascend_syn
+        colors: purple
         tag: syn rhyme
         subtag: bot
         link: rhyme ASCEND
@@ -6564,6 +6605,7 @@
         link: rhyme DOUBLE
       DUPLICATE:
         id: Double Room/Panel_double_syn
+        colors: purple
         tag: syn rhyme
         subtag: bot
         link: rhyme DOUBLE
@@ -6642,6 +6684,7 @@
         link: rhyme CHILD
       KID:
         id: Double Room/Panel_child_syn
+        colors: purple
         tag: syn rhyme
         subtag: bot
         link: rhyme CHILD
@@ -6653,6 +6696,7 @@
         link: rhyme CRYSTAL
       QUARTZ:
         id: Double Room/Panel_crystal_syn
+        colors: purple
         tag: syn rhyme
         subtag: bot
         link: rhyme CRYSTAL
@@ -6664,6 +6708,7 @@
         link: rhyme CREATIVE
       INNOVATIVE (Bottom):
         id: Double Room/Panel_creative_syn
+        colors: purple
         tag: syn rhyme
         subtag: bot
         link: rhyme CREATIVE
@@ -6882,7 +6927,7 @@
         event: True
         panels:
           - WALL (1)
-      Shortcut to Fifth Floor:
+      Cellar Exit:
         id:
           - Tower Room Area Doors/Door_panel_basement
           - Tower Room Area Doors/Door_panel_basement2
@@ -6895,7 +6940,10 @@
         door: Excavation
       Orange Tower Fifth Floor:
         room: Room Room
-        door: Shortcut to Fifth Floor
+        door: Cellar Exit
+      Outside The Agreeable:
+        room: Outside The Agreeable
+        door: Lookout Entrance
   Outside The Wise:
     entrances:
       Orange Tower Sixth Floor:
@@ -7319,49 +7367,65 @@
         link: change GRAVITY
       PART:
         id: Chemistry Room/Panel_physics_2
-        colors: blue
+        colors:
+          - blue
+          - red
         tag: blue mid red bot
         subtag: mid
         link: xur PARTICLE
       MATTER:
         id: Chemistry Room/Panel_physics_1
-        colors: red
+        colors:
+          - blue
+          - red
         tag: blue mid red bot
         subtag: bot
         link: xur PARTICLE
       ELECTRIC:
         id: Chemistry Room/Panel_physics_6
-        colors: purple
+        colors:
+          - purple
+          - red
         tag: purple mid red bot
         subtag: mid
         link: xpr ELECTRON
       ATOM (1):
         id: Chemistry Room/Panel_physics_3
-        colors: red
+        colors:
+          - purple
+          - red
         tag: purple mid red bot
         subtag: bot
         link: xpr ELECTRON
       NEUTRAL:
         id: Chemistry Room/Panel_physics_7
-        colors: purple
+        colors:
+          - purple
+          - red
         tag: purple mid red bot
         subtag: mid
         link: xpr NEUTRON
       ATOM (2):
         id: Chemistry Room/Panel_physics_4
-        colors: red
+        colors:
+          - purple
+          - red
         tag: purple mid red bot
         subtag: bot
         link: xpr NEUTRON
       PROPEL:
         id: Chemistry Room/Panel_physics_8
-        colors: purple
+        colors:
+          - purple
+          - red
         tag: purple mid red bot
         subtag: mid
         link: xpr PROTON
       ATOM (3):
         id: Chemistry Room/Panel_physics_5
-        colors: red
+        colors:
+          - purple
+          - red
         tag: purple mid red bot
         subtag: bot
         link: xpr PROTON
diff --git a/data/ids.yaml b/data/ids.yaml
index 1a1ceca..3239f21 100644
--- a/data/ids.yaml
+++ b/data/ids.yaml
@@ -1064,6 +1064,9 @@ doors:
     Hallway Door:
       item: 444459
       location: 445214
+    Lookout Entrance:
+      item: 444579
+      location: 445271
   Dread Hallway:
     Tenacious Entrance:
       item: 444462
@@ -1402,7 +1405,7 @@ doors:
       item: 444570
       location: 445266
   Room Room:
-    Shortcut to Fifth Floor:
+    Cellar Exit:
       item: 444571
       location: 445076
   Outside The Wise:
diff --git a/player_logic.py b/player_logic.py
index a0b33d1..b046f1c 100644
--- a/player_logic.py
+++ b/player_logic.py
@@ -190,6 +190,25 @@ class LingoPlayerLogic:
             if item.should_include(world):
                 self.real_items.append(name)
 
+        # Calculate the requirements for the fake pilgrimage.
+        fake_pilgrimage = [
+            ["Second Room", "Exit Door"], ["Crossroads", "Tower Entrance"],
+            ["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"],
+            ["Orange Tower First Floor", "Salt Pepper Door"], ["Hub Room", "Crossroads Entrance"],
+            ["Champion's Rest", "Shortcut to The Steady"], ["The Bearer", "Shortcut to The Bold"],
+            ["Art Gallery", "Exit"], ["The Tenacious", "Shortcut to Hub Room"],
+            ["Outside The Agreeable", "Tenacious Entrance"]
+        ]
+        pilgrimage_reqs = AccessRequirements()
+        for door in fake_pilgrimage:
+            door_object = DOORS_BY_ROOM[door[0]][door[1]]
+            if door_object.event or world.options.shuffle_doors == ShuffleDoors.option_none:
+                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.
         if painting_shuffle:
             # Shuffle paintings until we get something workable.
@@ -369,11 +388,9 @@ class LingoPlayerLogic:
             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)
+                panel_room = room if req_panel.room is None else req_panel.room
+                access_reqs.rooms.add(panel_room)
+                sub_access_reqs = self.calculate_panel_requirements(panel_room, req_panel.panel, world)
                 access_reqs.merge(sub_access_reqs)
 
             self.door_reqs[room][door] = access_reqs
@@ -397,8 +414,8 @@ class LingoPlayerLogic:
             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:
+                # We won't count non-counting panels. THE MASTER has special access rules and is handled separately.
+                if panel_data.non_counting or panel_name == "THE MASTER":
                     continue
 
                 # We won't coalesce any panels that have requirements beyond colors. To simplify things for now, we will
diff --git a/regions.py b/regions.py
index c24144a..bdc42f4 100644
--- a/regions.py
+++ b/regions.py
@@ -4,7 +4,7 @@ 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 .rules import lingo_can_use_entrance, make_location_lambda
 from .static_logic import ALL_ROOMS, PAINTINGS, Room, RoomAndDoor
 
 if TYPE_CHECKING:
@@ -25,15 +25,6 @@ def create_region(room: Room, world: "LingoWorld", player_logic: LingoPlayerLogi
     return new_region
 
 
-def handle_pilgrim_room(regions: Dict[str, Region], world: "LingoWorld", player_logic: LingoPlayerLogic) -> None:
-    target_region = regions["Pilgrim Antechamber"]
-    source_region = regions["Outside The Agreeable"]
-    source_region.connect(
-        target_region,
-        "Pilgrimage",
-        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)
@@ -91,7 +82,9 @@ def create_regions(world: "LingoWorld", player_logic: LingoPlayerLogic) -> None:
             connect_entrance(regions, regions[entrance.room], regions[room.name], entrance_name, entrance.door, world,
                              player_logic)
 
-    handle_pilgrim_room(regions, world, player_logic)
+    # Add the fake pilgrimage.
+    connect_entrance(regions, regions["Outside The Agreeable"], regions["Pilgrim Antechamber"], "Pilgrimage",
+                     RoomAndDoor("Pilgrim Antechamber", "Pilgrimage"), world, player_logic)
 
     if early_color_hallways:
         regions["Starting Room"].connect(regions["Outside The Undeterred"], "Early Color Hallways")
diff --git a/rules.py b/rules.py
index ee9dcc4..481fab1 100644
--- a/rules.py
+++ b/rules.py
@@ -17,23 +17,6 @@ def lingo_can_use_entrance(state: CollectionState, room: str, door: RoomAndDoor,
     return _lingo_can_open_door(state, effective_room, door.door, world, player_logic)
 
 
-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"],
-        ["Orange Tower First Floor", "Shortcut to Hub Room"], ["Directional Gallery", "Shortcut to The Undeterred"],
-        ["Orange Tower First Floor", "Salt Pepper Door"], ["Hub Room", "Crossroads Entrance"],
-        ["Champion's Rest", "Shortcut to The Steady"], ["The Bearer", "Shortcut to The Bold"],
-        ["Art Gallery", "Exit"], ["The Tenacious", "Shortcut to Hub Room"],
-        ["Outside The Agreeable", "Tenacious Entrance"]
-    ]
-    for entrance in fake_pilgrimage:
-        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, world: "LingoWorld",
                            player_logic: LingoPlayerLogic):
     return _lingo_can_satisfy_requirements(state, location.access, world, player_logic)
@@ -56,6 +39,12 @@ def lingo_can_use_level_2_location(state: CollectionState, world: "LingoWorld",
                 counted_panels += panel_count
         if counted_panels >= world.options.level_2_requirement.value - 1:
             return True
+    # THE MASTER has to be handled separately, because it has special access rules.
+    if state.can_reach("Orange Tower Seventh Floor", "Region", world.player)\
+            and lingo_can_use_mastery_location(state, world, player_logic):
+        counted_panels += 1
+    if counted_panels >= world.options.level_2_requirement.value - 1:
+        return True
     return False
 
 
diff --git a/utils/validate_config.rb b/utils/validate_config.rb
index bed5188..3ac49dc 100644
--- a/utils/validate_config.rb
+++ b/utils/validate_config.rb
@@ -40,7 +40,7 @@ mentioned_panels = Set[]
 door_groups = {}
 
 directives = Set["entrances", "panels", "doors", "paintings", "progression"]
-panel_directives = Set["id", "required_room", "required_door", "required_panel", "colors", "check", "exclude_reduce", "tag", "link", "subtag", "achievement", "copy_to_sign", "non_counting"]
+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", "location_name", "skip_location", "skip_item", "group", "include_reduce", "junk_item", "event"]
 painting_directives = Set["id", "enter_only", "exit_only", "orientation", "required_door", "required", "required_when_no_doors", "move", "req_blocked", "req_blocked_when_no_doors"]
 
-- 
cgit 1.4.1