From 9c6e33c7869d28b8fa1b3349c9a59a40aa8c1526 Mon Sep 17 00:00:00 2001
From: Star Rauchenberger <fefferburbia@gmail.com>
Date: Fri, 22 Mar 2024 15:28:41 -0500
Subject: Lingo: Add trap weights option (#2837)

---
 __init__.py     | 23 ++++++++++++++++++-----
 items.py        | 24 +++---------------------
 options.py      | 14 +++++++++++++-
 player_logic.py | 19 +++++++++++++++++--
 4 files changed, 51 insertions(+), 29 deletions(-)

diff --git a/__init__.py b/__init__.py
index c92e530..b749418 100644
--- a/__init__.py
+++ b/__init__.py
@@ -6,7 +6,7 @@ from logging import warning
 from BaseClasses import Item, ItemClassification, Tutorial
 from worlds.AutoWorld import WebWorld, World
 from .datatypes import Room, RoomEntrance
-from .items import ALL_ITEM_TABLE, ITEMS_BY_GROUP, LingoItem
+from .items import ALL_ITEM_TABLE, ITEMS_BY_GROUP, TRAP_ITEMS, LingoItem
 from .locations import ALL_LOCATION_TABLE, LOCATIONS_BY_GROUP
 from .options import LingoOptions
 from .player_logic import LingoPlayerLogic
@@ -91,10 +91,23 @@ class LingoWorld(World):
                     pool.append(self.create_item("Puzzle Skip"))
 
             if traps:
-                traps_list = ["Slowness Trap", "Iceland Trap", "Atbash Trap"]
-
-                for i in range(0, traps):
-                    pool.append(self.create_item(traps_list[i % len(traps_list)]))
+                total_weight = sum(self.options.trap_weights.values())
+
+                if total_weight == 0:
+                    raise Exception("Sum of trap weights must be at least one.")
+
+                trap_counts = {name: int(weight * traps / total_weight)
+                               for name, weight in self.options.trap_weights.items()}
+                
+                trap_difference = traps - sum(trap_counts.values())
+                if trap_difference > 0:
+                    allowed_traps = [name for name in TRAP_ITEMS if self.options.trap_weights[name] > 0]
+                    for i in range(0, trap_difference):
+                        trap_counts[allowed_traps[i % len(allowed_traps)]] += 1
+
+                for name, count in trap_counts.items():
+                    for i in range(0, count):
+                        pool.append(self.create_item(name))
 
         self.multiworld.itempool += pool
 
diff --git a/items.py b/items.py
index b9c4eb7..7c7928c 100644
--- a/items.py
+++ b/items.py
@@ -1,13 +1,9 @@
 from typing import Dict, List, NamedTuple, Optional, TYPE_CHECKING
 
 from BaseClasses import Item, ItemClassification
-from .options import ShuffleDoors
 from .static_logic import DOORS_BY_ROOM, PROGRESSION_BY_ROOM, PROGRESSIVE_ITEMS, get_door_group_item_id, \
     get_door_item_id, get_progressive_item_id, get_special_item_id
 
-if TYPE_CHECKING:
-    from . import LingoWorld
-
 
 class ItemData(NamedTuple):
     """
@@ -19,20 +15,6 @@ class ItemData(NamedTuple):
     has_doors: bool
     painting_ids: List[str]
 
-    def should_include(self, world: "LingoWorld") -> bool:
-        if self.mode == "colors":
-            return world.options.shuffle_colors > 0
-        elif self.mode == "doors":
-            return world.options.shuffle_doors != ShuffleDoors.option_none
-        elif self.mode == "complex door":
-            return world.options.shuffle_doors == ShuffleDoors.option_complex
-        elif self.mode == "door group":
-            return world.options.shuffle_doors == ShuffleDoors.option_simple
-        elif self.mode == "special":
-            return False
-        else:
-            return True
-
 
 class LingoItem(Item):
     """
@@ -44,6 +26,8 @@ class LingoItem(Item):
 ALL_ITEM_TABLE: Dict[str, ItemData] = {}
 ITEMS_BY_GROUP: Dict[str, List[str]] = {}
 
+TRAP_ITEMS: List[str] = ["Slowness Trap", "Iceland Trap", "Atbash Trap"]
+
 
 def load_item_data():
     global ALL_ITEM_TABLE, ITEMS_BY_GROUP
@@ -87,9 +71,7 @@ def load_item_data():
         "The Feeling of Being Lost": ItemClassification.filler,
         "Wanderlust":                ItemClassification.filler,
         "Empty White Hallways":      ItemClassification.filler,
-        "Slowness Trap":             ItemClassification.trap,
-        "Iceland Trap":              ItemClassification.trap,
-        "Atbash Trap":               ItemClassification.trap,
+        **{trap_name: ItemClassification.trap for trap_name in TRAP_ITEMS},
         "Puzzle Skip":               ItemClassification.useful,
     }
 
diff --git a/options.py b/options.py
index ed14264..293992a 100644
--- a/options.py
+++ b/options.py
@@ -1,6 +1,9 @@
 from dataclasses import dataclass
 
-from Options import Toggle, Choice, DefaultOnToggle, Range, PerGameCommonOptions, StartInventoryPool
+from schema import And, Schema
+
+from Options import Toggle, Choice, DefaultOnToggle, Range, PerGameCommonOptions, StartInventoryPool, OptionDict
+from worlds.lingo.items import TRAP_ITEMS
 
 
 class ShuffleDoors(Choice):
@@ -107,6 +110,14 @@ class TrapPercentage(Range):
     default = 20
 
 
+class TrapWeights(OptionDict):
+    """Specify the distribution of traps that should be placed into the pool.
+    If you don't want a specific type of trap, set the weight to zero."""
+    display_name = "Trap Weights"
+    schema = Schema({trap_name: And(int, lambda n: n >= 0) for trap_name in TRAP_ITEMS})
+    default = {trap_name: 1 for trap_name in TRAP_ITEMS}
+
+
 class PuzzleSkipPercentage(Range):
     """Replaces junk items with puzzle skips, at the specified rate."""
     display_name = "Puzzle Skip Percentage"
@@ -134,6 +145,7 @@ class LingoOptions(PerGameCommonOptions):
     level_2_requirement: Level2Requirement
     early_color_hallways: EarlyColorHallways
     trap_percentage: TrapPercentage
+    trap_weights: TrapWeights
     puzzle_skip_percentage: PuzzleSkipPercentage
     death_link: DeathLink
     start_inventory_from_pool: StartInventoryPool
diff --git a/player_logic.py b/player_logic.py
index b3cefa5..966f5a1 100644
--- a/player_logic.py
+++ b/player_logic.py
@@ -2,7 +2,7 @@ from enum import Enum
 from typing import Dict, List, NamedTuple, Optional, Set, Tuple, TYPE_CHECKING
 
 from .datatypes import Door, RoomAndDoor, RoomAndPanel
-from .items import ALL_ITEM_TABLE
+from .items import ALL_ITEM_TABLE, ItemData
 from .locations import ALL_LOCATION_TABLE, LocationClassification
 from .options import LocationChecks, ShuffleDoors, VictoryCondition
 from .static_logic import DOORS_BY_ROOM, PAINTINGS, PAINTING_ENTRANCES, PAINTING_EXITS, \
@@ -58,6 +58,21 @@ def should_split_progression(progression_name: str, world: "LingoWorld") -> Prog
     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:
     """
     Defines logic after a player's options have been applied
@@ -212,7 +227,7 @@ class LingoPlayerLogic:
 
         # Instantiate all real items.
         for name, item in ALL_ITEM_TABLE.items():
-            if item.should_include(world):
+            if should_include_item(item, world):
                 self.real_items.append(name)
 
         # Calculate the requirements for the fake pilgrimage.
-- 
cgit 1.4.1