mirror of
https://github.com/MarioSpore/Grinch-AP.git
synced 2025-10-21 12:11:33 -06:00
Stardew valley: Add Trap Distribution setting (#4601)
Co-authored-by: Jouramie <16137441+Jouramie@users.noreply.github.com> Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
This commit is contained in:
@@ -10,11 +10,13 @@ from .bundles.bundle_room import BundleRoom
|
||||
from .bundles.bundles import get_all_bundles
|
||||
from .content import StardewContent, create_content
|
||||
from .early_items import setup_early_items
|
||||
from .items import item_table, create_items, ItemData, Group, items_by_group, generate_filler_choice_pool
|
||||
from .items import item_table, ItemData, Group, items_by_group
|
||||
from .items.item_creation import create_items, get_all_filler_items, remove_limited_amount_packs, \
|
||||
generate_filler_choice_pool
|
||||
from .locations import location_table, create_locations, LocationData, locations_by_tag
|
||||
from .logic.logic import StardewLogic
|
||||
from .options import StardewValleyOptions, SeasonRandomization, Goal, BundleRandomization, EnabledFillerBuffs, NumberOfMovementBuffs, \
|
||||
BuildingProgression, EntranceRandomization, FarmType
|
||||
from .options import StardewValleyOptions, SeasonRandomization, Goal, BundleRandomization, EnabledFillerBuffs, \
|
||||
NumberOfMovementBuffs, BuildingProgression, EntranceRandomization, FarmType
|
||||
from .options.forced_options import force_change_options_if_incompatible
|
||||
from .options.option_groups import sv_option_groups
|
||||
from .options.presets import sv_options_presets
|
||||
|
1
worlds/stardew_valley/items/__init__.py
Normal file
1
worlds/stardew_valley/items/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .item_data import item_table, ItemData, Group, items_by_group, load_item_csv
|
@@ -1,165 +1,26 @@
|
||||
import csv
|
||||
import enum
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
from functools import reduce
|
||||
from pathlib import Path
|
||||
from random import Random
|
||||
from typing import Dict, List, Protocol, Union, Set, Optional
|
||||
from typing import List, Set
|
||||
|
||||
from BaseClasses import Item, ItemClassification
|
||||
from . import data
|
||||
from .content.feature import friendsanity
|
||||
from .content.game_content import StardewContent
|
||||
from .data.game_item import ItemTag
|
||||
from .logic.logic_event import all_events
|
||||
from .mods.mod_data import ModNames
|
||||
from .options import StardewValleyOptions, TrapItems, FestivalLocations, ExcludeGingerIsland, SpecialOrderLocations, SeasonRandomization, Museumsanity, \
|
||||
from .item_data import StardewItemFactory, items_by_group, Group, item_table, ItemData
|
||||
from ..content.feature import friendsanity
|
||||
from ..content.game_content import StardewContent
|
||||
from ..data.game_item import ItemTag
|
||||
from ..mods.mod_data import ModNames
|
||||
from ..options import StardewValleyOptions, FestivalLocations, ExcludeGingerIsland, SpecialOrderLocations, SeasonRandomization, Museumsanity, \
|
||||
ElevatorProgression, BackpackProgression, ArcadeMachineLocations, Monstersanity, Goal, \
|
||||
Chefsanity, Craftsanity, BundleRandomization, EntranceRandomization, Shipsanity, Walnutsanity, EnabledFillerBuffs
|
||||
from .strings.ap_names.ap_option_names import BuffOptionName, WalnutsanityOptionName
|
||||
from .strings.ap_names.ap_weapon_names import APWeapon
|
||||
from .strings.ap_names.buff_names import Buff
|
||||
from .strings.ap_names.community_upgrade_names import CommunityUpgrade
|
||||
from .strings.ap_names.mods.mod_items import SVEQuestItem
|
||||
from .strings.currency_names import Currency
|
||||
from .strings.tool_names import Tool
|
||||
from .strings.wallet_item_names import Wallet
|
||||
|
||||
ITEM_CODE_OFFSET = 717000
|
||||
Chefsanity, Craftsanity, BundleRandomization, EntranceRandomization, Shipsanity, Walnutsanity, EnabledFillerBuffs, TrapDifficulty
|
||||
from ..strings.ap_names.ap_option_names import BuffOptionName, WalnutsanityOptionName
|
||||
from ..strings.ap_names.ap_weapon_names import APWeapon
|
||||
from ..strings.ap_names.buff_names import Buff
|
||||
from ..strings.ap_names.community_upgrade_names import CommunityUpgrade
|
||||
from ..strings.ap_names.mods.mod_items import SVEQuestItem
|
||||
from ..strings.currency_names import Currency
|
||||
from ..strings.tool_names import Tool
|
||||
from ..strings.wallet_item_names import Wallet
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
world_folder = Path(__file__).parent
|
||||
|
||||
|
||||
class Group(enum.Enum):
|
||||
RESOURCE_PACK = enum.auto()
|
||||
FRIENDSHIP_PACK = enum.auto()
|
||||
COMMUNITY_REWARD = enum.auto()
|
||||
TRASH = enum.auto()
|
||||
FOOTWEAR = enum.auto()
|
||||
HATS = enum.auto()
|
||||
RING = enum.auto()
|
||||
WEAPON = enum.auto()
|
||||
WEAPON_GENERIC = enum.auto()
|
||||
WEAPON_SWORD = enum.auto()
|
||||
WEAPON_CLUB = enum.auto()
|
||||
WEAPON_DAGGER = enum.auto()
|
||||
WEAPON_SLINGSHOT = enum.auto()
|
||||
PROGRESSIVE_TOOLS = enum.auto()
|
||||
SKILL_LEVEL_UP = enum.auto()
|
||||
SKILL_MASTERY = enum.auto()
|
||||
BUILDING = enum.auto()
|
||||
WIZARD_BUILDING = enum.auto()
|
||||
ARCADE_MACHINE_BUFFS = enum.auto()
|
||||
BASE_RESOURCE = enum.auto()
|
||||
WARP_TOTEM = enum.auto()
|
||||
GEODE = enum.auto()
|
||||
ORE = enum.auto()
|
||||
FERTILIZER = enum.auto()
|
||||
SEED = enum.auto()
|
||||
CROPSANITY = enum.auto()
|
||||
FISHING_RESOURCE = enum.auto()
|
||||
SEASON = enum.auto()
|
||||
TRAVELING_MERCHANT_DAY = enum.auto()
|
||||
MUSEUM = enum.auto()
|
||||
FRIENDSANITY = enum.auto()
|
||||
FESTIVAL = enum.auto()
|
||||
RARECROW = enum.auto()
|
||||
TRAP = enum.auto()
|
||||
BONUS = enum.auto()
|
||||
MAXIMUM_ONE = enum.auto()
|
||||
AT_LEAST_TWO = enum.auto()
|
||||
DEPRECATED = enum.auto()
|
||||
RESOURCE_PACK_USEFUL = enum.auto()
|
||||
SPECIAL_ORDER_BOARD = enum.auto()
|
||||
SPECIAL_ORDER_QI = enum.auto()
|
||||
BABY = enum.auto()
|
||||
GINGER_ISLAND = enum.auto()
|
||||
WALNUT_PURCHASE = enum.auto()
|
||||
TV_CHANNEL = enum.auto()
|
||||
QI_CRAFTING_RECIPE = enum.auto()
|
||||
CHEFSANITY = enum.auto()
|
||||
CHEFSANITY_STARTER = enum.auto()
|
||||
CHEFSANITY_QOS = enum.auto()
|
||||
CHEFSANITY_PURCHASE = enum.auto()
|
||||
CHEFSANITY_FRIENDSHIP = enum.auto()
|
||||
CHEFSANITY_SKILL = enum.auto()
|
||||
CRAFTSANITY = enum.auto()
|
||||
BOOK_POWER = enum.auto()
|
||||
LOST_BOOK = enum.auto()
|
||||
PLAYER_BUFF = enum.auto()
|
||||
# Mods
|
||||
MAGIC_SPELL = enum.auto()
|
||||
MOD_WARP = enum.auto()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ItemData:
|
||||
code_without_offset: Optional[int]
|
||||
name: str
|
||||
classification: ItemClassification
|
||||
mod_name: Optional[str] = None
|
||||
groups: Set[Group] = field(default_factory=frozenset)
|
||||
|
||||
def __post_init__(self):
|
||||
if not isinstance(self.groups, frozenset):
|
||||
super().__setattr__("groups", frozenset(self.groups))
|
||||
|
||||
@property
|
||||
def code(self):
|
||||
return ITEM_CODE_OFFSET + self.code_without_offset if self.code_without_offset is not None else None
|
||||
|
||||
def has_any_group(self, *group: Group) -> bool:
|
||||
groups = set(group)
|
||||
return bool(groups.intersection(self.groups))
|
||||
|
||||
|
||||
class StardewItemFactory(Protocol):
|
||||
def __call__(self, name: Union[str, ItemData], override_classification: ItemClassification = None) -> Item:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def load_item_csv():
|
||||
from importlib.resources import files
|
||||
|
||||
items = []
|
||||
with files(data).joinpath("items.csv").open() as file:
|
||||
item_reader = csv.DictReader(file)
|
||||
for item in item_reader:
|
||||
id = int(item["id"]) if item["id"] else None
|
||||
classification = reduce((lambda a, b: a | b), {ItemClassification[str_classification] for str_classification in item["classification"].split(",")})
|
||||
groups = {Group[group] for group in item["groups"].split(",") if group}
|
||||
mod_name = str(item["mod_name"]) if item["mod_name"] else None
|
||||
items.append(ItemData(id, item["name"], classification, mod_name, groups))
|
||||
return items
|
||||
|
||||
|
||||
events = [
|
||||
ItemData(None, e, ItemClassification.progression)
|
||||
for e in sorted(all_events)
|
||||
]
|
||||
|
||||
all_items: List[ItemData] = load_item_csv() + events
|
||||
item_table: Dict[str, ItemData] = {}
|
||||
items_by_group: Dict[Group, List[ItemData]] = {}
|
||||
|
||||
|
||||
def initialize_groups():
|
||||
for item in all_items:
|
||||
for group in item.groups:
|
||||
item_group = items_by_group.get(group, list())
|
||||
item_group.append(item)
|
||||
items_by_group[group] = item_group
|
||||
|
||||
|
||||
def initialize_item_table():
|
||||
item_table.update({item.name: item for item in all_items})
|
||||
|
||||
|
||||
initialize_item_table()
|
||||
initialize_groups()
|
||||
|
||||
|
||||
def get_too_many_items_error_message(locations_count: int, items_count: int) -> str:
|
||||
return f"There should be at least as many locations [{locations_count}] as there are mandatory items [{items_count}]"
|
||||
@@ -712,13 +573,15 @@ def weapons_count(options: StardewValleyOptions):
|
||||
def fill_with_resource_packs_and_traps(item_factory: StardewItemFactory, options: StardewValleyOptions, random: Random,
|
||||
items_already_added: List[Item],
|
||||
available_item_slots: int) -> List[Item]:
|
||||
include_traps = options.trap_items != TrapItems.option_no_traps
|
||||
include_traps = options.trap_difficulty != TrapDifficulty.option_no_traps
|
||||
items_already_added_names = [item.name for item in items_already_added]
|
||||
useful_resource_packs = [pack for pack in items_by_group[Group.RESOURCE_PACK_USEFUL]
|
||||
if pack.name not in items_already_added_names]
|
||||
trap_items = [trap for trap in items_by_group[Group.TRAP]
|
||||
if trap.name not in items_already_added_names and
|
||||
(trap.mod_name is None or trap.mod_name in options.mods)]
|
||||
Group.DEPRECATED not in trap.groups and
|
||||
(trap.mod_name is None or trap.mod_name in options.mods) and
|
||||
options.trap_distribution[trap.name] > 0]
|
||||
player_buffs = get_allowed_player_buffs(options.enabled_filler_buffs)
|
||||
|
||||
priority_filler_items = []
|
||||
@@ -750,11 +613,13 @@ def fill_with_resource_packs_and_traps(item_factory: StardewItemFactory, options
|
||||
(filler_pack.name not in [priority_item.name for priority_item in
|
||||
priority_filler_items] and filler_pack.name not in items_already_added_names)]
|
||||
|
||||
filler_weights = get_filler_weights(options, all_filler_packs)
|
||||
|
||||
while available_item_slots > 0:
|
||||
resource_pack = random.choice(all_filler_packs)
|
||||
resource_pack = random.choices(all_filler_packs, weights=filler_weights, k=1)[0]
|
||||
exactly_2 = Group.AT_LEAST_TWO in resource_pack.groups
|
||||
while exactly_2 and available_item_slots == 1:
|
||||
resource_pack = random.choice(all_filler_packs)
|
||||
resource_pack = random.choices(all_filler_packs, weights=filler_weights, k=1)[0]
|
||||
exactly_2 = Group.AT_LEAST_TWO in resource_pack.groups
|
||||
classification = ItemClassification.useful if resource_pack.classification == ItemClassification.progression else resource_pack.classification
|
||||
items.append(item_factory(resource_pack, classification))
|
||||
@@ -763,11 +628,24 @@ def fill_with_resource_packs_and_traps(item_factory: StardewItemFactory, options
|
||||
items.append(item_factory(resource_pack, classification))
|
||||
available_item_slots -= 1
|
||||
if exactly_2 or Group.MAXIMUM_ONE in resource_pack.groups:
|
||||
all_filler_packs.remove(resource_pack)
|
||||
index = all_filler_packs.index(resource_pack)
|
||||
all_filler_packs.pop(index)
|
||||
filler_weights.pop(index)
|
||||
|
||||
return items
|
||||
|
||||
|
||||
def get_filler_weights(options: StardewValleyOptions, all_filler_packs: List[ItemData]):
|
||||
weights = []
|
||||
for filler in all_filler_packs:
|
||||
if filler.name in options.trap_distribution:
|
||||
num = options.trap_distribution[filler.name]
|
||||
else:
|
||||
num = options.trap_distribution.default_weight
|
||||
weights.append(num)
|
||||
return weights
|
||||
|
||||
|
||||
def filter_deprecated_items(items: List[ItemData]) -> List[ItemData]:
|
||||
return [item for item in items if Group.DEPRECATED not in item.groups]
|
||||
|
||||
@@ -792,7 +670,7 @@ def remove_excluded_items_island_mods(items, exclude_ginger_island: bool, mods:
|
||||
|
||||
|
||||
def generate_filler_choice_pool(options: StardewValleyOptions) -> list[str]:
|
||||
include_traps = options.trap_items != TrapItems.option_no_traps
|
||||
include_traps = options.trap_difficulty != TrapDifficulty.option_no_traps
|
||||
exclude_island = options.exclude_ginger_island == ExcludeGingerIsland.option_true
|
||||
|
||||
available_filler = get_all_filler_items(include_traps, exclude_island)
|
143
worlds/stardew_valley/items/item_data.py
Normal file
143
worlds/stardew_valley/items/item_data.py
Normal file
@@ -0,0 +1,143 @@
|
||||
import csv
|
||||
import enum
|
||||
from dataclasses import dataclass, field
|
||||
from functools import reduce
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Protocol, Union, Set, Optional
|
||||
|
||||
from BaseClasses import Item, ItemClassification
|
||||
from .. import data
|
||||
from ..logic.logic_event import all_events
|
||||
|
||||
ITEM_CODE_OFFSET = 717000
|
||||
|
||||
world_folder = Path(__file__).parent
|
||||
|
||||
|
||||
class Group(enum.Enum):
|
||||
RESOURCE_PACK = enum.auto()
|
||||
FRIENDSHIP_PACK = enum.auto()
|
||||
COMMUNITY_REWARD = enum.auto()
|
||||
TRASH = enum.auto()
|
||||
FOOTWEAR = enum.auto()
|
||||
HATS = enum.auto()
|
||||
RING = enum.auto()
|
||||
WEAPON = enum.auto()
|
||||
WEAPON_GENERIC = enum.auto()
|
||||
WEAPON_SWORD = enum.auto()
|
||||
WEAPON_CLUB = enum.auto()
|
||||
WEAPON_DAGGER = enum.auto()
|
||||
WEAPON_SLINGSHOT = enum.auto()
|
||||
PROGRESSIVE_TOOLS = enum.auto()
|
||||
SKILL_LEVEL_UP = enum.auto()
|
||||
SKILL_MASTERY = enum.auto()
|
||||
BUILDING = enum.auto()
|
||||
WIZARD_BUILDING = enum.auto()
|
||||
ARCADE_MACHINE_BUFFS = enum.auto()
|
||||
BASE_RESOURCE = enum.auto()
|
||||
WARP_TOTEM = enum.auto()
|
||||
GEODE = enum.auto()
|
||||
ORE = enum.auto()
|
||||
FERTILIZER = enum.auto()
|
||||
SEED = enum.auto()
|
||||
CROPSANITY = enum.auto()
|
||||
FISHING_RESOURCE = enum.auto()
|
||||
SEASON = enum.auto()
|
||||
TRAVELING_MERCHANT_DAY = enum.auto()
|
||||
MUSEUM = enum.auto()
|
||||
FRIENDSANITY = enum.auto()
|
||||
FESTIVAL = enum.auto()
|
||||
RARECROW = enum.auto()
|
||||
TRAP = enum.auto()
|
||||
BONUS = enum.auto()
|
||||
MAXIMUM_ONE = enum.auto()
|
||||
AT_LEAST_TWO = enum.auto()
|
||||
DEPRECATED = enum.auto()
|
||||
RESOURCE_PACK_USEFUL = enum.auto()
|
||||
SPECIAL_ORDER_BOARD = enum.auto()
|
||||
SPECIAL_ORDER_QI = enum.auto()
|
||||
BABY = enum.auto()
|
||||
GINGER_ISLAND = enum.auto()
|
||||
WALNUT_PURCHASE = enum.auto()
|
||||
TV_CHANNEL = enum.auto()
|
||||
QI_CRAFTING_RECIPE = enum.auto()
|
||||
CHEFSANITY = enum.auto()
|
||||
CHEFSANITY_STARTER = enum.auto()
|
||||
CHEFSANITY_QOS = enum.auto()
|
||||
CHEFSANITY_PURCHASE = enum.auto()
|
||||
CHEFSANITY_FRIENDSHIP = enum.auto()
|
||||
CHEFSANITY_SKILL = enum.auto()
|
||||
CRAFTSANITY = enum.auto()
|
||||
BOOK_POWER = enum.auto()
|
||||
LOST_BOOK = enum.auto()
|
||||
PLAYER_BUFF = enum.auto()
|
||||
# Mods
|
||||
MAGIC_SPELL = enum.auto()
|
||||
MOD_WARP = enum.auto()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ItemData:
|
||||
code_without_offset: Optional[int]
|
||||
name: str
|
||||
classification: ItemClassification
|
||||
mod_name: Optional[str] = None
|
||||
groups: Set[Group] = field(default_factory=frozenset)
|
||||
|
||||
def __post_init__(self):
|
||||
if not isinstance(self.groups, frozenset):
|
||||
super().__setattr__("groups", frozenset(self.groups))
|
||||
|
||||
@property
|
||||
def code(self):
|
||||
return ITEM_CODE_OFFSET + self.code_without_offset if self.code_without_offset is not None else None
|
||||
|
||||
def has_any_group(self, *group: Group) -> bool:
|
||||
groups = set(group)
|
||||
return bool(groups.intersection(self.groups))
|
||||
|
||||
|
||||
class StardewItemFactory(Protocol):
|
||||
def __call__(self, name: Union[str, ItemData], override_classification: ItemClassification = None) -> Item:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def load_item_csv():
|
||||
from importlib.resources import files
|
||||
|
||||
items = []
|
||||
with files(data).joinpath("items.csv").open() as file:
|
||||
item_reader = csv.DictReader(file)
|
||||
for item in item_reader:
|
||||
id = int(item["id"]) if item["id"] else None
|
||||
classification = reduce((lambda a, b: a | b), {ItemClassification[str_classification] for str_classification in item["classification"].split(",")})
|
||||
groups = {Group[group] for group in item["groups"].split(",") if group}
|
||||
mod_name = str(item["mod_name"]) if item["mod_name"] else None
|
||||
items.append(ItemData(id, item["name"], classification, mod_name, groups))
|
||||
return items
|
||||
|
||||
|
||||
events = [
|
||||
ItemData(None, e, ItemClassification.progression)
|
||||
for e in sorted(all_events)
|
||||
]
|
||||
|
||||
all_items: List[ItemData] = load_item_csv() + events
|
||||
item_table: Dict[str, ItemData] = {}
|
||||
items_by_group: Dict[Group, List[ItemData]] = {}
|
||||
|
||||
|
||||
def initialize_groups():
|
||||
for item in all_items:
|
||||
for group in item.groups:
|
||||
item_group = items_by_group.get(group, list())
|
||||
item_group.append(item)
|
||||
items_by_group[group] = item_group
|
||||
|
||||
|
||||
def initialize_item_table():
|
||||
item_table.update({item.name: item for item in all_items})
|
||||
|
||||
|
||||
initialize_item_table()
|
||||
initialize_groups()
|
@@ -1,6 +1,6 @@
|
||||
from .options import StardewValleyOption, Goal, FarmType, StartingMoney, ProfitMargin, BundleRandomization, BundlePrice, EntranceRandomization, \
|
||||
SeasonRandomization, Cropsanity, BackpackProgression, ToolProgression, ElevatorProgression, SkillProgression, BuildingProgression, FestivalLocations, \
|
||||
ArcadeMachineLocations, SpecialOrderLocations, QuestLocations, Fishsanity, Museumsanity, Monstersanity, Shipsanity, Cooksanity, Chefsanity, Craftsanity, \
|
||||
Friendsanity, FriendsanityHeartSize, Booksanity, Walnutsanity, NumberOfMovementBuffs, EnabledFillerBuffs, ExcludeGingerIsland, TrapItems, \
|
||||
Friendsanity, FriendsanityHeartSize, Booksanity, Walnutsanity, NumberOfMovementBuffs, EnabledFillerBuffs, ExcludeGingerIsland, TrapDifficulty, \
|
||||
MultipleDaySleepEnabled, MultipleDaySleepCost, ExperienceMultiplier, FriendshipMultiplier, DebrisMultiplier, QuickStart, Gifting, Mods, BundlePlando, \
|
||||
StardewValleyOptions, enabled_mods, disabled_mods, all_mods
|
||||
StardewValleyOptions, enabled_mods, disabled_mods, all_mods, TrapDistribution, TrapItems, StardewValleyOptions
|
||||
|
@@ -52,7 +52,8 @@ else:
|
||||
options.DebrisMultiplier,
|
||||
options.NumberOfMovementBuffs,
|
||||
options.EnabledFillerBuffs,
|
||||
options.TrapItems,
|
||||
options.TrapDifficulty,
|
||||
options.TrapDistribution,
|
||||
options.MultipleDaySleepEnabled,
|
||||
options.MultipleDaySleepCost,
|
||||
options.QuickStart,
|
||||
|
@@ -3,7 +3,8 @@ import typing
|
||||
from dataclasses import dataclass
|
||||
from typing import Protocol, ClassVar
|
||||
|
||||
from Options import Range, NamedRange, Toggle, Choice, OptionSet, PerGameCommonOptions, DeathLink, OptionList, Visibility
|
||||
from Options import Range, NamedRange, Toggle, Choice, OptionSet, PerGameCommonOptions, DeathLink, OptionList, Visibility, Removed, OptionCounter
|
||||
from ..items import items_by_group, Group
|
||||
from ..mods.mod_data import ModNames
|
||||
from ..strings.ap_names.ap_option_names import BuffOptionName, WalnutsanityOptionName
|
||||
from ..strings.bundle_names import all_cc_bundle_names
|
||||
@@ -658,13 +659,29 @@ class ExcludeGingerIsland(Toggle):
|
||||
default = 0
|
||||
|
||||
|
||||
class TrapItems(Choice):
|
||||
"""When rolling filler items, including resource packs, the game can also roll trap items.
|
||||
Trap items are negative items that cause problems or annoyances for the player
|
||||
This setting is for choosing if traps will be in the item pool, and if so, how punishing they will be.
|
||||
class TrapItems(Removed):
|
||||
"""Deprecated setting, replaced by TrapDifficulty
|
||||
"""
|
||||
internal_name = "trap_items"
|
||||
display_name = "Trap Items"
|
||||
default = ""
|
||||
visibility = Visibility.none
|
||||
|
||||
def __init__(self, value: str):
|
||||
if value:
|
||||
raise Exception("Option trap_items was replaced by trap_difficulty, please update your options file")
|
||||
super().__init__(value)
|
||||
|
||||
|
||||
class TrapDifficulty(Choice):
|
||||
"""When rolling filler items, including resource packs, the game can also roll trap items.
|
||||
Trap items are negative items that cause problems or annoyances for the player.
|
||||
This setting is for choosing how punishing traps will be.
|
||||
Lower difficulties will be on the funny annoyance side, higher difficulty will be on the extreme problems side.
|
||||
Only play Nightmare at your own risk.
|
||||
"""
|
||||
internal_name = "trap_difficulty"
|
||||
display_name = "Trap Difficulty"
|
||||
default = 2
|
||||
option_no_traps = 0
|
||||
option_easy = 1
|
||||
@@ -674,6 +691,34 @@ class TrapItems(Choice):
|
||||
option_nightmare = 5
|
||||
|
||||
|
||||
trap_default_weight = 100
|
||||
|
||||
|
||||
class TrapDistribution(OptionCounter):
|
||||
"""
|
||||
Specify the weighted chance of rolling individual traps when rolling random filler items.
|
||||
The average filler item should be considered to be "100", as in 100%.
|
||||
So a trap on "200" will be twice as likely to roll as any filler item. A trap on "10" will be 10% as likely.
|
||||
You can use weight "0" to disable this trap entirely. The maximum weight is 1000, for x10 chance
|
||||
"""
|
||||
internal_name = "trap_distribution"
|
||||
display_name = "Trap Distribution"
|
||||
default_weight = trap_default_weight
|
||||
visibility = Visibility.all ^ Visibility.simple_ui
|
||||
min = 0
|
||||
max = 1000
|
||||
valid_keys = frozenset({
|
||||
trap_data.name
|
||||
for trap_data in items_by_group[Group.TRAP]
|
||||
if Group.DEPRECATED not in trap_data.groups
|
||||
})
|
||||
default = {
|
||||
trap_data.name: trap_default_weight
|
||||
for trap_data in items_by_group[Group.TRAP]
|
||||
if Group.DEPRECATED not in trap_data.groups
|
||||
}
|
||||
|
||||
|
||||
class MultipleDaySleepEnabled(Toggle):
|
||||
"""Enable the ability to sleep automatically for multiple days straight?"""
|
||||
internal_name = "multiple_day_sleep_enabled"
|
||||
@@ -851,10 +896,14 @@ class StardewValleyOptions(PerGameCommonOptions):
|
||||
debris_multiplier: DebrisMultiplier
|
||||
movement_buff_number: NumberOfMovementBuffs
|
||||
enabled_filler_buffs: EnabledFillerBuffs
|
||||
trap_items: TrapItems
|
||||
trap_difficulty: TrapDifficulty
|
||||
trap_distribution: TrapDistribution
|
||||
multiple_day_sleep_enabled: MultipleDaySleepEnabled
|
||||
multiple_day_sleep_cost: MultipleDaySleepCost
|
||||
gifting: Gifting
|
||||
mods: Mods
|
||||
bundle_plando: BundlePlando
|
||||
death_link: DeathLink
|
||||
|
||||
# removed:
|
||||
trap_items: TrapItems
|
@@ -38,7 +38,7 @@ all_random_settings = {
|
||||
options.Booksanity.internal_name: "random",
|
||||
options.NumberOfMovementBuffs.internal_name: "random",
|
||||
options.ExcludeGingerIsland.internal_name: "random",
|
||||
options.TrapItems.internal_name: "random",
|
||||
options.TrapDifficulty.internal_name: "random",
|
||||
options.MultipleDaySleepEnabled.internal_name: "random",
|
||||
options.MultipleDaySleepCost.internal_name: "random",
|
||||
options.ExperienceMultiplier.internal_name: "random",
|
||||
@@ -82,7 +82,7 @@ easy_settings = {
|
||||
options.NumberOfMovementBuffs.internal_name: 8,
|
||||
options.EnabledFillerBuffs.internal_name: options.EnabledFillerBuffs.preset_all,
|
||||
options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true,
|
||||
options.TrapItems.internal_name: options.TrapItems.option_easy,
|
||||
options.TrapDifficulty.internal_name: options.TrapDifficulty.option_easy,
|
||||
options.MultipleDaySleepEnabled.internal_name: options.MultipleDaySleepEnabled.option_true,
|
||||
options.MultipleDaySleepCost.internal_name: "free",
|
||||
options.ExperienceMultiplier.internal_name: "triple",
|
||||
@@ -126,7 +126,7 @@ medium_settings = {
|
||||
options.NumberOfMovementBuffs.internal_name: 6,
|
||||
options.EnabledFillerBuffs.internal_name: options.EnabledFillerBuffs.preset_all,
|
||||
options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true,
|
||||
options.TrapItems.internal_name: options.TrapItems.option_medium,
|
||||
options.TrapDifficulty.internal_name: options.TrapDifficulty.option_medium,
|
||||
options.MultipleDaySleepEnabled.internal_name: options.MultipleDaySleepEnabled.option_true,
|
||||
options.MultipleDaySleepCost.internal_name: "free",
|
||||
options.ExperienceMultiplier.internal_name: "double",
|
||||
@@ -170,7 +170,7 @@ hard_settings = {
|
||||
options.NumberOfMovementBuffs.internal_name: 4,
|
||||
options.EnabledFillerBuffs.internal_name: options.EnabledFillerBuffs.default,
|
||||
options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_false,
|
||||
options.TrapItems.internal_name: options.TrapItems.option_hard,
|
||||
options.TrapDifficulty.internal_name: options.TrapDifficulty.option_hard,
|
||||
options.MultipleDaySleepEnabled.internal_name: options.MultipleDaySleepEnabled.option_true,
|
||||
options.MultipleDaySleepCost.internal_name: "cheap",
|
||||
options.ExperienceMultiplier.internal_name: "vanilla",
|
||||
@@ -214,7 +214,7 @@ nightmare_settings = {
|
||||
options.NumberOfMovementBuffs.internal_name: 2,
|
||||
options.EnabledFillerBuffs.internal_name: options.EnabledFillerBuffs.preset_none,
|
||||
options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_false,
|
||||
options.TrapItems.internal_name: options.TrapItems.option_hell,
|
||||
options.TrapDifficulty.internal_name: options.TrapDifficulty.option_hell,
|
||||
options.MultipleDaySleepEnabled.internal_name: options.MultipleDaySleepEnabled.option_true,
|
||||
options.MultipleDaySleepCost.internal_name: "expensive",
|
||||
options.ExperienceMultiplier.internal_name: "half",
|
||||
@@ -258,7 +258,7 @@ short_settings = {
|
||||
options.NumberOfMovementBuffs.internal_name: 10,
|
||||
options.EnabledFillerBuffs.internal_name: options.EnabledFillerBuffs.preset_all,
|
||||
options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true,
|
||||
options.TrapItems.internal_name: options.TrapItems.option_easy,
|
||||
options.TrapDifficulty.internal_name: options.TrapDifficulty.option_easy,
|
||||
options.MultipleDaySleepEnabled.internal_name: options.MultipleDaySleepEnabled.option_true,
|
||||
options.MultipleDaySleepCost.internal_name: "free",
|
||||
options.ExperienceMultiplier.internal_name: "quadruple",
|
||||
@@ -302,7 +302,7 @@ minsanity_settings = {
|
||||
options.NumberOfMovementBuffs.internal_name: options.NumberOfMovementBuffs.default,
|
||||
options.EnabledFillerBuffs.internal_name: options.EnabledFillerBuffs.default,
|
||||
options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true,
|
||||
options.TrapItems.internal_name: options.TrapItems.default,
|
||||
options.TrapDifficulty.internal_name: options.TrapDifficulty.default,
|
||||
options.MultipleDaySleepEnabled.internal_name: options.MultipleDaySleepEnabled.default,
|
||||
options.MultipleDaySleepCost.internal_name: options.MultipleDaySleepCost.default,
|
||||
options.ExperienceMultiplier.internal_name: options.ExperienceMultiplier.default,
|
||||
@@ -346,7 +346,7 @@ allsanity_settings = {
|
||||
options.NumberOfMovementBuffs.internal_name: 12,
|
||||
options.EnabledFillerBuffs.internal_name: options.EnabledFillerBuffs.preset_all,
|
||||
options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_false,
|
||||
options.TrapItems.internal_name: options.TrapItems.default,
|
||||
options.TrapDifficulty.internal_name: options.TrapDifficulty.default,
|
||||
options.MultipleDaySleepEnabled.internal_name: options.MultipleDaySleepEnabled.default,
|
||||
options.MultipleDaySleepCost.internal_name: options.MultipleDaySleepCost.default,
|
||||
options.ExperienceMultiplier.internal_name: options.ExperienceMultiplier.default,
|
||||
|
@@ -2,8 +2,8 @@ from typing import List
|
||||
|
||||
from BaseClasses import ItemClassification, Item
|
||||
from . import SVTestBase
|
||||
from .. import items, location_table, options
|
||||
from ..items import Group, ItemData
|
||||
from .. import location_table, options, items
|
||||
from ..items import Group, ItemData, item_data
|
||||
from ..locations import LocationTags
|
||||
from ..options import Friendsanity, SpecialOrderLocations, Shipsanity, Chefsanity, SeasonRandomization, Craftsanity, ExcludeGingerIsland, SkillProgression, \
|
||||
Booksanity, Walnutsanity
|
||||
@@ -15,10 +15,10 @@ def get_all_permanent_progression_items() -> List[ItemData]:
|
||||
"""
|
||||
return [
|
||||
item
|
||||
for item in items.all_items
|
||||
for item in item_data.all_items
|
||||
if ItemClassification.progression in item.classification
|
||||
if item.mod_name is None
|
||||
if item.name not in {event.name for event in items.events}
|
||||
if item.name not in {event.name for event in item_data.events}
|
||||
if item.name not in {deprecated.name for deprecated in items.items_by_group[Group.DEPRECATED]}
|
||||
if item.name not in {season.name for season in items.items_by_group[Group.SEASON]}
|
||||
if item.name not in {weapon.name for weapon in items.items_by_group[Group.WEAPON]}
|
||||
@@ -54,19 +54,19 @@ class TestBaseItemGeneration(SVTestBase):
|
||||
|
||||
def test_does_not_create_deprecated_items(self):
|
||||
all_created_items = set(self.get_all_created_items())
|
||||
for deprecated_item in items.items_by_group[items.Group.DEPRECATED]:
|
||||
for deprecated_item in item_data.items_by_group[item_data.Group.DEPRECATED]:
|
||||
with self.subTest(f"{deprecated_item.name}"):
|
||||
self.assertNotIn(deprecated_item.name, all_created_items)
|
||||
|
||||
def test_does_not_create_more_than_one_maximum_one_items(self):
|
||||
all_created_items = self.get_all_created_items()
|
||||
for maximum_one_item in items.items_by_group[items.Group.MAXIMUM_ONE]:
|
||||
for maximum_one_item in item_data.items_by_group[item_data.Group.MAXIMUM_ONE]:
|
||||
with self.subTest(f"{maximum_one_item.name}"):
|
||||
self.assertLessEqual(all_created_items.count(maximum_one_item.name), 1)
|
||||
|
||||
def test_does_not_create_or_create_two_of_exactly_two_items(self):
|
||||
all_created_items = self.get_all_created_items()
|
||||
for exactly_two_item in items.items_by_group[items.Group.AT_LEAST_TWO]:
|
||||
for exactly_two_item in item_data.items_by_group[item_data.Group.AT_LEAST_TWO]:
|
||||
with self.subTest(f"{exactly_two_item.name}"):
|
||||
count = all_created_items.count(exactly_two_item.name)
|
||||
self.assertTrue(count == 0 or count == 2)
|
||||
@@ -102,19 +102,19 @@ class TestNoGingerIslandItemGeneration(SVTestBase):
|
||||
|
||||
def test_does_not_create_deprecated_items(self):
|
||||
all_created_items = self.get_all_created_items()
|
||||
for deprecated_item in items.items_by_group[items.Group.DEPRECATED]:
|
||||
for deprecated_item in item_data.items_by_group[item_data.Group.DEPRECATED]:
|
||||
with self.subTest(f"Deprecated item: {deprecated_item.name}"):
|
||||
self.assertNotIn(deprecated_item.name, all_created_items)
|
||||
|
||||
def test_does_not_create_more_than_one_maximum_one_items(self):
|
||||
all_created_items = self.get_all_created_items()
|
||||
for maximum_one_item in items.items_by_group[items.Group.MAXIMUM_ONE]:
|
||||
for maximum_one_item in item_data.items_by_group[item_data.Group.MAXIMUM_ONE]:
|
||||
with self.subTest(f"{maximum_one_item.name}"):
|
||||
self.assertLessEqual(all_created_items.count(maximum_one_item.name), 1)
|
||||
|
||||
def test_does_not_create_exactly_two_items(self):
|
||||
all_created_items = self.get_all_created_items()
|
||||
for exactly_two_item in items.items_by_group[items.Group.AT_LEAST_TWO]:
|
||||
for exactly_two_item in item_data.items_by_group[item_data.Group.AT_LEAST_TWO]:
|
||||
with self.subTest(f"{exactly_two_item.name}"):
|
||||
count = all_created_items.count(exactly_two_item.name)
|
||||
self.assertTrue(count == 0 or count == 2)
|
||||
|
@@ -6,7 +6,7 @@ max_iterations = 2000
|
||||
|
||||
class TestItemLinksEverythingIncluded(SVTestBase):
|
||||
options = {options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_false,
|
||||
options.TrapItems.internal_name: options.TrapItems.option_medium}
|
||||
options.TrapDifficulty.internal_name: options.TrapDifficulty.option_medium}
|
||||
|
||||
def test_filler_of_all_types_generated(self):
|
||||
max_number_filler = 114
|
||||
@@ -33,7 +33,7 @@ class TestItemLinksEverythingIncluded(SVTestBase):
|
||||
|
||||
class TestItemLinksNoIsland(SVTestBase):
|
||||
options = {options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true,
|
||||
options.TrapItems.internal_name: options.TrapItems.option_medium}
|
||||
options.TrapDifficulty.internal_name: options.TrapDifficulty.option_medium}
|
||||
|
||||
def test_filler_has_no_island_but_has_traps(self):
|
||||
max_number_filler = 109
|
||||
@@ -57,7 +57,7 @@ class TestItemLinksNoIsland(SVTestBase):
|
||||
|
||||
class TestItemLinksNoTraps(SVTestBase):
|
||||
options = {options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_false,
|
||||
options.TrapItems.internal_name: options.TrapItems.option_no_traps}
|
||||
options.TrapDifficulty.internal_name: options.TrapDifficulty.option_no_traps}
|
||||
|
||||
def test_filler_has_no_traps_but_has_island(self):
|
||||
max_number_filler = 99
|
||||
@@ -81,7 +81,7 @@ class TestItemLinksNoTraps(SVTestBase):
|
||||
|
||||
class TestItemLinksNoTrapsAndIsland(SVTestBase):
|
||||
options = {options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true,
|
||||
options.TrapItems.internal_name: options.TrapItems.option_no_traps}
|
||||
options.TrapDifficulty.internal_name: options.TrapDifficulty.option_no_traps}
|
||||
|
||||
def test_filler_generated_without_island_or_traps(self):
|
||||
max_number_filler = 94
|
||||
|
@@ -9,7 +9,7 @@ from .options.option_names import all_option_choices
|
||||
from .options.presets import allsanity_no_mods_6_x_x, allsanity_mods_6_x_x
|
||||
from .. import items_by_group, Group
|
||||
from ..locations import locations_by_tag, LocationTags, location_table
|
||||
from ..options import ExcludeGingerIsland, ToolProgression, Goal, SeasonRandomization, TrapItems, SpecialOrderLocations, ArcadeMachineLocations
|
||||
from ..options import ExcludeGingerIsland, ToolProgression, Goal, SeasonRandomization, TrapDifficulty, SpecialOrderLocations, ArcadeMachineLocations
|
||||
from ..strings.goal_names import Goal as GoalName
|
||||
from ..strings.season_names import Season
|
||||
from ..strings.special_order_names import SpecialOrder
|
||||
@@ -126,7 +126,7 @@ class TestGenerateAllOptionsWithExcludeGingerIsland(WorldAssertMixin, SVTestCase
|
||||
class TestTraps(SVTestCase):
|
||||
def test_given_no_traps_when_generate_then_no_trap_in_pool(self):
|
||||
world_options = allsanity_no_mods_6_x_x().copy()
|
||||
world_options[TrapItems.internal_name] = TrapItems.option_no_traps
|
||||
world_options[TrapDifficulty.internal_name] = TrapDifficulty.option_no_traps
|
||||
with solo_multiworld(world_options) as (multi_world, _):
|
||||
trap_items = [item_data.name for item_data in items_by_group[Group.TRAP]]
|
||||
multiworld_items = [item.name for item in multi_world.get_items()]
|
||||
@@ -136,12 +136,12 @@ class TestTraps(SVTestCase):
|
||||
self.assertNotIn(item, multiworld_items)
|
||||
|
||||
def test_given_traps_when_generate_then_all_traps_in_pool(self):
|
||||
trap_option = TrapItems
|
||||
trap_option = TrapDifficulty
|
||||
for value in trap_option.options:
|
||||
if value == "no_traps":
|
||||
continue
|
||||
world_options = allsanity_mods_6_x_x()
|
||||
world_options.update({TrapItems.internal_name: trap_option.options[value]})
|
||||
world_options.update({TrapDifficulty.internal_name: trap_option.options[value]})
|
||||
with solo_multiworld(world_options) as (multi_world, _):
|
||||
trap_items = [item_data.name for item_data in items_by_group[Group.TRAP] if
|
||||
Group.DEPRECATED not in item_data.groups and item_data.mod_name is None]
|
||||
|
122
worlds/stardew_valley/test/TestTraps.py
Normal file
122
worlds/stardew_valley/test/TestTraps.py
Normal file
@@ -0,0 +1,122 @@
|
||||
import unittest
|
||||
|
||||
from . import SVTestBase
|
||||
from .assertion import WorldAssertMixin
|
||||
from .. import options, items_by_group, Group
|
||||
from ..options import TrapDistribution
|
||||
|
||||
default_distribution = {trap.name: TrapDistribution.default_weight for trap in items_by_group[Group.TRAP] if Group.DEPRECATED not in trap.groups}
|
||||
threshold_difference = 2
|
||||
threshold_ballpark = 3
|
||||
|
||||
|
||||
class TestTrapDifficultyCanRemoveAllTraps(WorldAssertMixin, SVTestBase):
|
||||
options = {
|
||||
options.QuestLocations.internal_name: 56,
|
||||
options.Fishsanity.internal_name: options.Fishsanity.option_all,
|
||||
options.Museumsanity.internal_name: options.Museumsanity.option_all,
|
||||
options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_board_qi,
|
||||
options.Shipsanity.internal_name: options.Shipsanity.option_everything,
|
||||
options.Cooksanity.internal_name: options.Cooksanity.option_all,
|
||||
options.Craftsanity.internal_name: options.Craftsanity.option_all,
|
||||
options.Mods.internal_name: frozenset(options.Mods.valid_keys),
|
||||
options.TrapDifficulty.internal_name: options.TrapDifficulty.option_no_traps,
|
||||
}
|
||||
|
||||
def test_no_traps_in_item_pool(self):
|
||||
items = self.multiworld.get_items()
|
||||
item_names = set(item.name for item in items)
|
||||
for trap in items_by_group[Group.TRAP]:
|
||||
if Group.DEPRECATED in trap.groups:
|
||||
continue
|
||||
self.assertNotIn(trap.name, item_names)
|
||||
|
||||
|
||||
class TestDefaultDistributionHasAllTraps(WorldAssertMixin, SVTestBase):
|
||||
options = {
|
||||
options.QuestLocations.internal_name: 56,
|
||||
options.Fishsanity.internal_name: options.Fishsanity.option_all,
|
||||
options.Museumsanity.internal_name: options.Museumsanity.option_all,
|
||||
options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_board_qi,
|
||||
options.Shipsanity.internal_name: options.Shipsanity.option_everything,
|
||||
options.Cooksanity.internal_name: options.Cooksanity.option_all,
|
||||
options.Craftsanity.internal_name: options.Craftsanity.option_all,
|
||||
options.Mods.internal_name: frozenset(options.Mods.valid_keys),
|
||||
options.TrapDifficulty.internal_name: options.TrapDifficulty.option_medium,
|
||||
}
|
||||
|
||||
def test_all_traps_in_item_pool(self):
|
||||
items = self.multiworld.get_items()
|
||||
item_names = set(item.name for item in items)
|
||||
for trap in items_by_group[Group.TRAP]:
|
||||
if Group.DEPRECATED in trap.groups:
|
||||
continue
|
||||
self.assertIn(trap.name, item_names)
|
||||
|
||||
|
||||
class TestDistributionIsRespectedAllTraps(WorldAssertMixin, SVTestBase):
|
||||
options = {
|
||||
options.QuestLocations.internal_name: 56,
|
||||
options.Fishsanity.internal_name: options.Fishsanity.option_all,
|
||||
options.Museumsanity.internal_name: options.Museumsanity.option_all,
|
||||
options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_board_qi,
|
||||
options.Shipsanity.internal_name: options.Shipsanity.option_everything,
|
||||
options.Cooksanity.internal_name: options.Cooksanity.option_all,
|
||||
options.Craftsanity.internal_name: options.Craftsanity.option_all,
|
||||
options.Mods.internal_name: frozenset(options.Mods.valid_keys),
|
||||
options.TrapDifficulty.internal_name: options.TrapDifficulty.option_medium,
|
||||
options.TrapDistribution.internal_name: default_distribution | {"Nudge Trap": 100, "Bark Trap": 1, "Meow Trap": 1000, "Shuffle Trap": 0}
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
super().setUpClass()
|
||||
if cls.skip_long_tests:
|
||||
raise unittest.SkipTest("Unstable tests disabled to not annoy anyone else when it rarely fails")
|
||||
|
||||
def test_about_as_many_nudges_as_other_filler(self):
|
||||
items = self.multiworld.get_items()
|
||||
item_names = [item.name for item in items]
|
||||
num_nudge = len([item for item in item_names if item == "Nudge Trap"])
|
||||
other_fillers = ["Resource Pack: 4 Frozen Geode", "Resource Pack: 50 Wood", "Resource Pack: 5 Warp Totem: Farm",
|
||||
"Resource Pack: 500 Money", "Resource Pack: 75 Copper Ore", "Resource Pack: 30 Speed-Gro"]
|
||||
at_least_one_in_ballpark = False
|
||||
for filler_item in other_fillers:
|
||||
num_filler = len([item for item in item_names if item == filler_item])
|
||||
diff_num = abs(num_filler - num_nudge)
|
||||
is_in_ballpark = diff_num <= threshold_ballpark
|
||||
at_least_one_in_ballpark = at_least_one_in_ballpark or is_in_ballpark
|
||||
self.assertTrue(at_least_one_in_ballpark)
|
||||
|
||||
def test_fewer_barks_than_nudges_in_item_pool(self):
|
||||
items = self.multiworld.get_items()
|
||||
item_names = [item.name for item in items]
|
||||
num_nudge = len([item for item in item_names if item == "Nudge Trap"])
|
||||
num_bark = len([item for item in item_names if item == "Bark Trap"])
|
||||
self.assertLess(num_bark, num_nudge - threshold_difference)
|
||||
|
||||
def test_more_meows_than_nudges_in_item_pool(self):
|
||||
items = self.multiworld.get_items()
|
||||
item_names = [item.name for item in items]
|
||||
num_nudge = len([item for item in item_names if item == "Nudge Trap"])
|
||||
num_meow = len([item for item in item_names if item == "Meow Trap"])
|
||||
self.assertGreater(num_meow, num_nudge + threshold_difference)
|
||||
|
||||
def test_no_shuffles_in_item_pool(self):
|
||||
items = self.multiworld.get_items()
|
||||
item_names = [item.name for item in items]
|
||||
num_shuffle = len([item for item in item_names if item == "Shuffle Trap"])
|
||||
self.assertEqual(0, num_shuffle)
|
||||
|
||||
def test_omitted_item_same_as_nudge_in_item_pool(self):
|
||||
items = self.multiworld.get_items()
|
||||
item_names = [item.name for item in items]
|
||||
num_time_flies = len([item for item in item_names if item == "Time Flies Trap"])
|
||||
num_debris = len([item for item in item_names if item == "Debris Trap"])
|
||||
num_bark = len([item for item in item_names if item == "Bark Trap"])
|
||||
num_meow = len([item for item in item_names if item == "Meow Trap"])
|
||||
self.assertLess(num_bark, num_time_flies - threshold_difference)
|
||||
self.assertLess(num_bark, num_debris - threshold_difference)
|
||||
self.assertGreater(num_meow, num_time_flies + threshold_difference)
|
||||
self.assertGreater(num_meow, num_debris + threshold_difference)
|
||||
|
@@ -14,7 +14,7 @@ from .assertion import RuleAssertMixin
|
||||
from .options.utils import fill_namespace_with_default, parse_class_option_keys, fill_dataclass_with_default
|
||||
from .. import StardewValleyWorld, StardewItem, StardewRule
|
||||
from ..logic.time_logic import MONTH_COEFFICIENT
|
||||
from ..options import StardewValleyOption
|
||||
from ..options import StardewValleyOption, options
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -221,9 +221,9 @@ def setup_solo_multiworld(test_options: Optional[Dict[Union[str, StardewValleyOp
|
||||
|
||||
# Yes I reuse the worlds generated between tests, its speeds the execution by a couple seconds
|
||||
# If the simple dict caching ends up taking too much memory, we could replace it with some kind of lru cache.
|
||||
should_cache = "start_inventory" not in test_options
|
||||
should_cache = should_cache_world(test_options)
|
||||
if should_cache:
|
||||
frozen_options = frozenset(test_options.items()).union({("seed", seed)})
|
||||
frozen_options = make_hashable(test_options, seed)
|
||||
cached_multi_world = search_world_cache(_cache, frozen_options)
|
||||
if cached_multi_world:
|
||||
print(f"Using cached solo multi world [Seed = {cached_multi_world.seed}] [Cache size = {len(_cache)}]")
|
||||
@@ -252,6 +252,27 @@ def setup_solo_multiworld(test_options: Optional[Dict[Union[str, StardewValleyOp
|
||||
return multiworld
|
||||
|
||||
|
||||
def should_cache_world(test_options):
|
||||
if "start_inventory" in test_options:
|
||||
return False
|
||||
|
||||
trap_distribution_key = "trap_distribution"
|
||||
if trap_distribution_key not in test_options:
|
||||
return True
|
||||
|
||||
trap_distribution = test_options[trap_distribution_key]
|
||||
for key in trap_distribution:
|
||||
if trap_distribution[key] != options.TrapDistribution.default_weight:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
||||
def make_hashable(test_options, seed):
|
||||
return frozenset(test_options.items()).union({("seed", seed)})
|
||||
|
||||
|
||||
def search_world_cache(cache: Dict[frozenset, MultiWorld], frozen_options: frozenset) -> Optional[MultiWorld]:
|
||||
try:
|
||||
return cache[frozen_options]
|
||||
|
@@ -1,16 +1,16 @@
|
||||
from Options import PerGameCommonOptions, OptionSet
|
||||
from Options import PerGameCommonOptions, OptionSet, OptionDict
|
||||
from .. import SVTestCase
|
||||
from ...options import StardewValleyOptions
|
||||
from ...options import StardewValleyOptions, TrapItems
|
||||
from ...options.presets import sv_options_presets
|
||||
|
||||
|
||||
class TestPresets(SVTestCase):
|
||||
def test_all_presets_explicitly_set_all_options(self):
|
||||
all_option_names = {option_key for option_key in StardewValleyOptions.type_hints}
|
||||
omitted_option_names = {option_key for option_key in PerGameCommonOptions.type_hints}
|
||||
omitted_option_names = {option_key for option_key in PerGameCommonOptions.type_hints} | {TrapItems.internal_name}
|
||||
mandatory_option_names = {option_key for option_key in all_option_names
|
||||
if option_key not in omitted_option_names and
|
||||
not issubclass(StardewValleyOptions.type_hints[option_key], OptionSet)}
|
||||
not issubclass(StardewValleyOptions.type_hints[option_key], OptionSet | OptionDict)}
|
||||
|
||||
for preset_name in sv_options_presets:
|
||||
with self.subTest(f"{preset_name}"):
|
||||
|
@@ -70,7 +70,7 @@ def allsanity_no_mods_6_x_x():
|
||||
options.SkillProgression.internal_name: options.SkillProgression.option_progressive_with_masteries,
|
||||
options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_board_qi,
|
||||
options.ToolProgression.internal_name: options.ToolProgression.option_progressive,
|
||||
options.TrapItems.internal_name: options.TrapItems.option_nightmare,
|
||||
options.TrapDifficulty.internal_name: options.TrapDifficulty.option_nightmare,
|
||||
options.Walnutsanity.internal_name: options.Walnutsanity.preset_all
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ def get_minsanity_options():
|
||||
options.SkillProgression.internal_name: options.SkillProgression.option_vanilla,
|
||||
options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_vanilla,
|
||||
options.ToolProgression.internal_name: options.ToolProgression.option_vanilla,
|
||||
options.TrapItems.internal_name: options.TrapItems.option_no_traps,
|
||||
options.TrapDifficulty.internal_name: options.TrapDifficulty.option_no_traps,
|
||||
options.Walnutsanity.internal_name: options.Walnutsanity.preset_none
|
||||
}
|
||||
|
||||
@@ -156,7 +156,7 @@ def minimal_locations_maximal_items():
|
||||
options.SkillProgression.internal_name: options.SkillProgression.option_vanilla,
|
||||
options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_vanilla,
|
||||
options.ToolProgression.internal_name: options.ToolProgression.option_vanilla,
|
||||
options.TrapItems.internal_name: options.TrapItems.option_nightmare,
|
||||
options.TrapDifficulty.internal_name: options.TrapDifficulty.option_nightmare,
|
||||
options.Walnutsanity.internal_name: options.Walnutsanity.preset_none
|
||||
}
|
||||
return min_max_options
|
||||
|
Reference in New Issue
Block a user