Stardew Valley: Exclude maximum one resource packs from pool when in start inventory (#4839)

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
This commit is contained in:
Jérémie Bolduc
2025-04-20 10:51:03 -04:00
committed by GitHub
parent 22941168cd
commit 543dcb27d8
7 changed files with 42 additions and 26 deletions

View File

@@ -1,7 +1,7 @@
import logging import logging
import typing import typing
from random import Random from random import Random
from typing import Dict, Any, Iterable, Optional, List, TextIO, cast from typing import Dict, Any, Iterable, Optional, List, TextIO
from BaseClasses import Region, Entrance, Location, Item, Tutorial, ItemClassification, MultiWorld, CollectionState from BaseClasses import Region, Entrance, Location, Item, Tutorial, ItemClassification, MultiWorld, CollectionState
from Options import PerGameCommonOptions from Options import PerGameCommonOptions
@@ -148,8 +148,8 @@ class StardewValleyWorld(World):
self.precollect_building_items() self.precollect_building_items()
items_to_exclude = [excluded_items items_to_exclude = [excluded_items
for excluded_items in self.multiworld.precollected_items[self.player] for excluded_items in self.multiworld.precollected_items[self.player]
if not item_table[excluded_items.name].has_any_group(Group.RESOURCE_PACK, if item_table[excluded_items.name].has_any_group(Group.MAXIMUM_ONE)
Group.FRIENDSHIP_PACK)] or not item_table[excluded_items.name].has_any_group(Group.RESOURCE_PACK, Group.FRIENDSHIP_PACK)]
if self.options.season_randomization == SeasonRandomization.option_disabled: if self.options.season_randomization == SeasonRandomization.option_disabled:
items_to_exclude = [item for item in items_to_exclude items_to_exclude = [item for item in items_to_exclude

View File

@@ -748,7 +748,7 @@ id,name,classification,groups,mod_name
5208,Chest,filler,"RESOURCE_PACK,RESOURCE_PACK_USEFUL", 5208,Chest,filler,"RESOURCE_PACK,RESOURCE_PACK_USEFUL",
5209,Stone Chest,filler,"RESOURCE_PACK,RESOURCE_PACK_USEFUL", 5209,Stone Chest,filler,"RESOURCE_PACK,RESOURCE_PACK_USEFUL",
5210,Quality Bobber,filler,RESOURCE_PACK, 5210,Quality Bobber,filler,RESOURCE_PACK,
5211,Mini-Obelisk,filler,"EXACTLY_TWO,RESOURCE_PACK", 5211,Mini-Obelisk,filler,"AT_LEAST_TWO,RESOURCE_PACK",
5212,Monster Musk,filler,RESOURCE_PACK, 5212,Monster Musk,filler,RESOURCE_PACK,
5213,Sprinkler,filler,"RESOURCE_PACK,RESOURCE_PACK_USEFUL", 5213,Sprinkler,filler,"RESOURCE_PACK,RESOURCE_PACK_USEFUL",
5214,Quality Sprinkler,filler,"RESOURCE_PACK,RESOURCE_PACK_USEFUL", 5214,Quality Sprinkler,filler,"RESOURCE_PACK,RESOURCE_PACK_USEFUL",
@@ -760,7 +760,7 @@ id,name,classification,groups,mod_name
5220,Lightning Rod,filler,"RESOURCE_PACK,RESOURCE_PACK_USEFUL", 5220,Lightning Rod,filler,"RESOURCE_PACK,RESOURCE_PACK_USEFUL",
5221,Resource Pack: 5000 Money,useful,"BASE_RESOURCE,RESOURCE_PACK,RESOURCE_PACK_USEFUL", 5221,Resource Pack: 5000 Money,useful,"BASE_RESOURCE,RESOURCE_PACK,RESOURCE_PACK_USEFUL",
5222,Resource Pack: 10000 Money,useful,"BASE_RESOURCE,RESOURCE_PACK,RESOURCE_PACK_USEFUL", 5222,Resource Pack: 10000 Money,useful,"BASE_RESOURCE,RESOURCE_PACK,RESOURCE_PACK_USEFUL",
5223,Junimo Chest,filler,"EXACTLY_TWO,RESOURCE_PACK", 5223,Junimo Chest,filler,"AT_LEAST_TWO,RESOURCE_PACK",
5224,Horse Flute,useful,"MAXIMUM_ONE,RESOURCE_PACK,RESOURCE_PACK_USEFUL", 5224,Horse Flute,useful,"MAXIMUM_ONE,RESOURCE_PACK,RESOURCE_PACK_USEFUL",
5225,Pierre's Missing Stocklist,useful,"MAXIMUM_ONE,RESOURCE_PACK,RESOURCE_PACK_USEFUL", 5225,Pierre's Missing Stocklist,useful,"MAXIMUM_ONE,RESOURCE_PACK,RESOURCE_PACK_USEFUL",
5226,Hopper,filler,"RESOURCE_PACK,RESOURCE_PACK_USEFUL", 5226,Hopper,filler,"RESOURCE_PACK,RESOURCE_PACK_USEFUL",
1 id name classification groups mod_name
748 5208 Chest filler RESOURCE_PACK,RESOURCE_PACK_USEFUL
749 5209 Stone Chest filler RESOURCE_PACK,RESOURCE_PACK_USEFUL
750 5210 Quality Bobber filler RESOURCE_PACK
751 5211 Mini-Obelisk filler EXACTLY_TWO,RESOURCE_PACK AT_LEAST_TWO,RESOURCE_PACK
752 5212 Monster Musk filler RESOURCE_PACK
753 5213 Sprinkler filler RESOURCE_PACK,RESOURCE_PACK_USEFUL
754 5214 Quality Sprinkler filler RESOURCE_PACK,RESOURCE_PACK_USEFUL
760 5220 Lightning Rod filler RESOURCE_PACK,RESOURCE_PACK_USEFUL
761 5221 Resource Pack: 5000 Money useful BASE_RESOURCE,RESOURCE_PACK,RESOURCE_PACK_USEFUL
762 5222 Resource Pack: 10000 Money useful BASE_RESOURCE,RESOURCE_PACK,RESOURCE_PACK_USEFUL
763 5223 Junimo Chest filler EXACTLY_TWO,RESOURCE_PACK AT_LEAST_TWO,RESOURCE_PACK
764 5224 Horse Flute useful MAXIMUM_ONE,RESOURCE_PACK,RESOURCE_PACK_USEFUL
765 5225 Pierre's Missing Stocklist useful MAXIMUM_ONE,RESOURCE_PACK,RESOURCE_PACK_USEFUL
766 5226 Hopper filler RESOURCE_PACK,RESOURCE_PACK_USEFUL

View File

@@ -69,7 +69,7 @@ class Group(enum.Enum):
TRAP = enum.auto() TRAP = enum.auto()
BONUS = enum.auto() BONUS = enum.auto()
MAXIMUM_ONE = enum.auto() MAXIMUM_ONE = enum.auto()
EXACTLY_TWO = enum.auto() AT_LEAST_TWO = enum.auto()
DEPRECATED = enum.auto() DEPRECATED = enum.auto()
RESOURCE_PACK_USEFUL = enum.auto() RESOURCE_PACK_USEFUL = enum.auto()
SPECIAL_ORDER_BOARD = enum.auto() SPECIAL_ORDER_BOARD = enum.auto()
@@ -181,7 +181,7 @@ def create_items(item_factory: StardewItemFactory, locations_count: int, items_t
items += unique_filler_items items += unique_filler_items
logger.debug(f"Created {len(unique_filler_items)} unique filler items") logger.debug(f"Created {len(unique_filler_items)} unique filler items")
resource_pack_items = fill_with_resource_packs_and_traps(item_factory, options, random, items, locations_count) resource_pack_items = fill_with_resource_packs_and_traps(item_factory, options, random, items + items_to_exclude, locations_count - len(items))
items += resource_pack_items items += resource_pack_items
logger.debug(f"Created {len(resource_pack_items)} resource packs") logger.debug(f"Created {len(resource_pack_items)} resource packs")
@@ -711,7 +711,7 @@ def weapons_count(options: StardewValleyOptions):
def fill_with_resource_packs_and_traps(item_factory: StardewItemFactory, options: StardewValleyOptions, random: Random, def fill_with_resource_packs_and_traps(item_factory: StardewItemFactory, options: StardewValleyOptions, random: Random,
items_already_added: List[Item], items_already_added: List[Item],
number_locations: int) -> List[Item]: available_item_slots: int) -> List[Item]:
include_traps = options.trap_items != TrapItems.option_no_traps include_traps = options.trap_items != TrapItems.option_no_traps
items_already_added_names = [item.name for item in items_already_added] 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] useful_resource_packs = [pack for pack in items_by_group[Group.RESOURCE_PACK_USEFUL]
@@ -734,10 +734,9 @@ def fill_with_resource_packs_and_traps(item_factory: StardewItemFactory, options
priority_filler_items = remove_excluded_items(priority_filler_items, options) priority_filler_items = remove_excluded_items(priority_filler_items, options)
number_priority_items = len(priority_filler_items) number_priority_items = len(priority_filler_items)
required_resource_pack = number_locations - len(items_already_added) if available_item_slots < number_priority_items:
if required_resource_pack < number_priority_items:
chosen_priority_items = [item_factory(resource_pack) for resource_pack in chosen_priority_items = [item_factory(resource_pack) for resource_pack in
random.sample(priority_filler_items, required_resource_pack)] random.sample(priority_filler_items, available_item_slots)]
return chosen_priority_items return chosen_priority_items
items = [] items = []
@@ -745,24 +744,24 @@ def fill_with_resource_packs_and_traps(item_factory: StardewItemFactory, options
ItemClassification.trap if resource_pack.classification == ItemClassification.trap else ItemClassification.useful) ItemClassification.trap if resource_pack.classification == ItemClassification.trap else ItemClassification.useful)
for resource_pack in priority_filler_items] for resource_pack in priority_filler_items]
items.extend(chosen_priority_items) items.extend(chosen_priority_items)
required_resource_pack -= number_priority_items available_item_slots -= number_priority_items
all_filler_packs = [filler_pack for filler_pack in all_filler_packs all_filler_packs = [filler_pack for filler_pack in all_filler_packs
if Group.MAXIMUM_ONE not in filler_pack.groups or if Group.MAXIMUM_ONE not in filler_pack.groups or
(filler_pack.name not in [priority_item.name for priority_item in (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)] priority_filler_items] and filler_pack.name not in items_already_added_names)]
while required_resource_pack > 0: while available_item_slots > 0:
resource_pack = random.choice(all_filler_packs) resource_pack = random.choice(all_filler_packs)
exactly_2 = Group.EXACTLY_TWO in resource_pack.groups exactly_2 = Group.AT_LEAST_TWO in resource_pack.groups
while exactly_2 and required_resource_pack == 1: while exactly_2 and available_item_slots == 1:
resource_pack = random.choice(all_filler_packs) resource_pack = random.choice(all_filler_packs)
exactly_2 = Group.EXACTLY_TWO in resource_pack.groups exactly_2 = Group.AT_LEAST_TWO in resource_pack.groups
classification = ItemClassification.useful if resource_pack.classification == ItemClassification.progression else resource_pack.classification classification = ItemClassification.useful if resource_pack.classification == ItemClassification.progression else resource_pack.classification
items.append(item_factory(resource_pack, classification)) items.append(item_factory(resource_pack, classification))
required_resource_pack -= 1 available_item_slots -= 1
if exactly_2: if exactly_2:
items.append(item_factory(resource_pack, classification)) items.append(item_factory(resource_pack, classification))
required_resource_pack -= 1 available_item_slots -= 1
if exactly_2 or Group.MAXIMUM_ONE in resource_pack.groups: if exactly_2 or Group.MAXIMUM_ONE in resource_pack.groups:
all_filler_packs.remove(resource_pack) all_filler_packs.remove(resource_pack)
@@ -803,7 +802,7 @@ def generate_filler_choice_pool(options: StardewValleyOptions) -> list[str]:
def remove_limited_amount_packs(packs): def remove_limited_amount_packs(packs):
return [pack for pack in packs if Group.MAXIMUM_ONE not in pack.groups and Group.EXACTLY_TWO not in pack.groups] return [pack for pack in packs if Group.MAXIMUM_ONE not in pack.groups and Group.AT_LEAST_TWO not in pack.groups]
def get_all_filler_items(include_traps: bool, exclude_ginger_island: bool) -> List[ItemData]: def get_all_filler_items(include_traps: bool, exclude_ginger_island: bool) -> List[ItemData]:

View File

@@ -9,3 +9,4 @@ class Wallet:
dark_talisman = "Dark Talisman" dark_talisman = "Dark Talisman"
club_card = "Club Card" club_card = "Club Card"
mastery_of_the_five_ways = "Mastery Of The Five Ways" mastery_of_the_five_ways = "Mastery Of The Five Ways"
key_to_the_town = "Key To The Town"

View File

@@ -66,7 +66,7 @@ class TestBaseItemGeneration(SVTestBase):
def test_does_not_create_or_create_two_of_exactly_two_items(self): def test_does_not_create_or_create_two_of_exactly_two_items(self):
all_created_items = self.get_all_created_items() all_created_items = self.get_all_created_items()
for exactly_two_item in items.items_by_group[items.Group.EXACTLY_TWO]: for exactly_two_item in items.items_by_group[items.Group.AT_LEAST_TWO]:
with self.subTest(f"{exactly_two_item.name}"): with self.subTest(f"{exactly_two_item.name}"):
count = all_created_items.count(exactly_two_item.name) count = all_created_items.count(exactly_two_item.name)
self.assertTrue(count == 0 or count == 2) self.assertTrue(count == 0 or count == 2)
@@ -114,7 +114,7 @@ class TestNoGingerIslandItemGeneration(SVTestBase):
def test_does_not_create_exactly_two_items(self): def test_does_not_create_exactly_two_items(self):
all_created_items = self.get_all_created_items() all_created_items = self.get_all_created_items()
for exactly_two_item in items.items_by_group[items.Group.EXACTLY_TWO]: for exactly_two_item in items.items_by_group[items.Group.AT_LEAST_TWO]:
with self.subTest(f"{exactly_two_item.name}"): with self.subTest(f"{exactly_two_item.name}"):
count = all_created_items.count(exactly_two_item.name) count = all_created_items.count(exactly_two_item.name)
self.assertTrue(count == 0 or count == 2) self.assertTrue(count == 0 or count == 2)

View File

@@ -19,7 +19,7 @@ class TestItemLinksEverythingIncluded(SVTestBase):
continue continue
filler_generated.append(filler) filler_generated.append(filler)
self.assertNotIn(Group.MAXIMUM_ONE, item_table[filler].groups) self.assertNotIn(Group.MAXIMUM_ONE, item_table[filler].groups)
self.assertNotIn(Group.EXACTLY_TWO, item_table[filler].groups) self.assertNotIn(Group.AT_LEAST_TWO, item_table[filler].groups)
if Group.TRAP in item_table[filler].groups: if Group.TRAP in item_table[filler].groups:
at_least_one_trap = True at_least_one_trap = True
if Group.GINGER_ISLAND in item_table[filler].groups: if Group.GINGER_ISLAND in item_table[filler].groups:
@@ -46,7 +46,7 @@ class TestItemLinksNoIsland(SVTestBase):
filler_generated.append(filler) filler_generated.append(filler)
self.assertNotIn(Group.GINGER_ISLAND, item_table[filler].groups) self.assertNotIn(Group.GINGER_ISLAND, item_table[filler].groups)
self.assertNotIn(Group.MAXIMUM_ONE, item_table[filler].groups) self.assertNotIn(Group.MAXIMUM_ONE, item_table[filler].groups)
self.assertNotIn(Group.EXACTLY_TWO, item_table[filler].groups) self.assertNotIn(Group.AT_LEAST_TWO, item_table[filler].groups)
if Group.TRAP in item_table[filler].groups: if Group.TRAP in item_table[filler].groups:
at_least_one_trap = True at_least_one_trap = True
if len(filler_generated) >= max_number_filler: if len(filler_generated) >= max_number_filler:
@@ -70,7 +70,7 @@ class TestItemLinksNoTraps(SVTestBase):
filler_generated.append(filler) filler_generated.append(filler)
self.assertNotIn(Group.TRAP, item_table[filler].groups) self.assertNotIn(Group.TRAP, item_table[filler].groups)
self.assertNotIn(Group.MAXIMUM_ONE, item_table[filler].groups) self.assertNotIn(Group.MAXIMUM_ONE, item_table[filler].groups)
self.assertNotIn(Group.EXACTLY_TWO, item_table[filler].groups) self.assertNotIn(Group.AT_LEAST_TWO, item_table[filler].groups)
if Group.GINGER_ISLAND in item_table[filler].groups: if Group.GINGER_ISLAND in item_table[filler].groups:
at_least_one_island = True at_least_one_island = True
if len(filler_generated) >= max_number_filler: if len(filler_generated) >= max_number_filler:
@@ -94,7 +94,7 @@ class TestItemLinksNoTrapsAndIsland(SVTestBase):
self.assertNotIn(Group.GINGER_ISLAND, item_table[filler].groups) self.assertNotIn(Group.GINGER_ISLAND, item_table[filler].groups)
self.assertNotIn(Group.TRAP, item_table[filler].groups) self.assertNotIn(Group.TRAP, item_table[filler].groups)
self.assertNotIn(Group.MAXIMUM_ONE, item_table[filler].groups) self.assertNotIn(Group.MAXIMUM_ONE, item_table[filler].groups)
self.assertNotIn(Group.EXACTLY_TWO, item_table[filler].groups) self.assertNotIn(Group.AT_LEAST_TWO, item_table[filler].groups)
if len(filler_generated) >= max_number_filler: if len(filler_generated) >= max_number_filler:
break break
self.assertGreaterEqual(len(filler_generated), max_number_filler) self.assertGreaterEqual(len(filler_generated), max_number_filler)

View File

@@ -1,4 +1,4 @@
from BaseClasses import MultiWorld, get_seed from BaseClasses import MultiWorld, get_seed, ItemClassification
from . import setup_solo_multiworld, SVTestCase, solo_multiworld from . import setup_solo_multiworld, SVTestCase, solo_multiworld
from .options.presets import allsanity_no_mods_6_x_x, get_minsanity_options from .options.presets import allsanity_no_mods_6_x_x, get_minsanity_options
from .. import StardewValleyWorld from .. import StardewValleyWorld
@@ -72,6 +72,22 @@ class TestItems(SVTestCase):
self.assertEqual(len(starting_seasons_rolled), 4) self.assertEqual(len(starting_seasons_rolled), 4)
class TestStartInventoryFillersAreProperlyExcluded(SVTestCase):
def test_given_maximum_one_resource_pack_in_start_inventory_when_create_items_then_item_is_properly_excluded(self):
assert item_table[Wallet.key_to_the_town].classification == ItemClassification.useful \
and {Group.MAXIMUM_ONE, Group.RESOURCE_PACK_USEFUL}.issubset(item_table[Wallet.key_to_the_town].groups), \
"'Key to the Town' is no longer suitable to test this usecase."
options = {
"start_inventory": {
Wallet.key_to_the_town: 1,
}
}
with solo_multiworld(options, world_caching=False) as (multiworld, world):
self.assertNotIn(world.create_item(Wallet.key_to_the_town), multiworld.get_items())
class TestMetalDetectors(SVTestCase): class TestMetalDetectors(SVTestCase):
def test_minsanity_1_metal_detector(self): def test_minsanity_1_metal_detector(self):
options = get_minsanity_options() options = get_minsanity_options()