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:
agilbert1412
2025-05-10 17:57:24 -04:00
committed by GitHub
parent 5f24da7e18
commit 8f71dac417
15 changed files with 426 additions and 209 deletions

View File

@@ -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

View File

@@ -0,0 +1 @@
from .item_data import item_table, ItemData, Group, items_by_group, load_item_csv

View File

@@ -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)

View 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()

View File

@@ -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

View File

@@ -52,7 +52,8 @@ else:
options.DebrisMultiplier,
options.NumberOfMovementBuffs,
options.EnabledFillerBuffs,
options.TrapItems,
options.TrapDifficulty,
options.TrapDistribution,
options.MultipleDaySleepEnabled,
options.MultipleDaySleepCost,
options.QuickStart,

View File

@@ -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

View File

@@ -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,

View File

@@ -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)

View File

@@ -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

View File

@@ -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]

View 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)

View File

@@ -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]

View File

@@ -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}"):

View File

@@ -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